Why You Should Combine Symfony Console and Dependency Injection

I saw 2 links to Symfony\Console in today's Week of Symfony (what a time reference, huh?). There are plenty of such posts out there, even in Pehapkari community blog: Best Practice for Symfony Console in Nette or Symfony Console from the Scratch.
But nobody seems to write about the greatest bottleneck of Console applications - static cancer. Why is that?

1. Current Status in PHP Console Applications

Your web application has an entry point in www/index.php, where it loads the DI Container, gets Application class and calls run() on it (with explicit or implicit Request):

require __DIR__ . '/vendor/autoload.php';

// Kernel or Configurator
$container = $kernel->getContainer();
$application = $container->get(Application::class);
$application->run(Request::createFromGlobals());

Console Applications (further as CLI Apps) have very similar entry point. Not in index.php, but usually in bin/something file.

When we look at entry points of popular PHP Console Applications, like:


PHP_CodeSniffer

$runner = new PHP_CodeSniffer\Runner();
$runner->runPHPCS();


PHP CS Fixer

$application = new PhpCsFixer\Console\Application();
$application->run();


PHPStan

$application = new Symfony\Component\Console\Application('PHPStan');
$application->add(new AnalyseCommand());
$application->run();


If we mimic such approach in web apps, how would our www/index.php look like?

require __DIR__ . '/vendor/autoload.php';

$application = new Application;
$application->addController(new HomepageController);
$application->addController(new PostController);
$application->addController(new ContactController);
$application->addController(new ProducController);
// ...
$application->run();

How do you feel seeing such code? I feel a bit weird and I don't get on well with static code.

On the other hand, if we take the web app approach to cli apps:

$container = $kernel->getContainer();

$application = $container->get(Application::class);
$application->run(new ArgInput);

Why is That?

I wish I knew this answer :). In my opinion and experience with building cli apps, there might be few...

Advantages

Disadvantages

2. Container Inceptions

The container is slowly appearing not as the backbone of application as in web apps, but as part of commands.

E.g. AnalyseCommand in PHPStan:

use Symfony\Component\Console\Command\Command;

class AnalyseCommand extends Command
{
    // ...

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $container = $this->containerFactory->createFromConfig($input->getOption('config'));

        $someService = $container->get(SomeService::class);
        // ...
    }
}

Or in FixerFactory in PHP CS Fixer:

# much simplified

class FixerFactory
{
    public function registerBuiltInFixers()
    {
        static $fixers = [];

        foreach (Finder::findAllFixerClasses() as $fixerClass) {
            $fixers[] = new $fixerClass;
        }
    }
}

Disadvantages

Advantages


Imagine a code like this in your web application:

class ProductController
{
    /**
     * @var Connection
     */
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connectoin;
    }

    public function detail($id);
    {
        $productRepository = new ProductRepository($this->connection);
        $product = $productRepository->get($id);

        // ...
    }
}

How do you feel about it?

Injection Inception Problem

CLI apps authors often struggle with the question: When should be the container created?

And how to create container when user provides config with services via --config option? The complexity of this question usually leads to choice 2 or 1.

I won't get into more details now, since I'll write about possible solutions in following posts.

This application cycle has these steps:

Compare it to a web application:

3. Symfony\Console meets Symfony\DependencyInjection

Why not inspire by web apps, where Controllers are lazy and dependency injection is the first-class citizen? Moreover, Symfony 3.4 allows Lazy Commands, that make application cycle more and more similar to web apps. Be careful - there are few WTFs during migration to Lazy Commands, as Shopsys describes.

# bin/rector

// ...

$container = $kernel->getContainer();

$application = $container->get(Application::class);
$application->run();

Disadvantages

Advantages

How To Migrate from 1 to 3?

I wish there was Rector for that like there is for Doctrine Repositories as Services, but it is a too complex task at the moment. Maybe one day.

In the meantime you can use few guides:


That's what works for me in CLI apps I've been working on. Look for yourself to get real code inspiration:


Which approach do you find the best in your own practice for the long-term code?



Happy injecting!


  Continue Learning


Typo? Fix it, please  and join 47 people who build this website

GitHub RSS @votrubaT Runs on Statie Hosted on GitHub Build by 48 people

Like what I write about? Hire me & we can work together