Why You Should Combine Symfony Console and Dependency Injection

Found a typo? Edit me

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!


Have you find this post useful? Do you want more?

Follow me on Twitter, RSS or support me on GitHub Sponsors.