how do I use Symfony console with dependency injection, without the Symfony framework bundle?

Viewed 2549

I have a command-line application, which so far uses the Symfony dependency injection component. I now find that I want to add command-line options and improve the formatting of the output, and the Symfony console component seems like a good choice for that.

However, I can't fathom how to get my Symfony console command classes to receive the container object.

The documentation I have found uses the ContainerAwareCommand class, but that is from the FrameworkBundle -- which seems a huge amount of overhead to add to a pure CLI app, as it requires further bundles such as routing, http, config, cache, etc, none of which are of any relevance to me whatsoever here.

(Existing SO question How can i inject dependencies to Symfony Console commands? also assumes the FrameworkBundle, BTW.)

I've made a test repository here with a basic command that illustrates the problem: https://github.com/joachim-n/console-with-di

3 Answers

Symfony 3/4/5 Way

Since 2018 and Symfony 3.4+ DI features, you can make use of commands as services.

You can find working demo here, thanks to @TravisCarden

In short:

1. App Kernel

<?php

# app/Kernel.php

namespace App;

use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\DependencyInjection\ContainerBuilder;

final class AppKernel extends Kernel
{
    public function registerBundles(): array
    {
        return [];
    }

    public function registerContainerConfiguration(LoaderInterface $loader): void
    {
        $loader->load(__DIR__.'/../config/services.yml');
    }

    protected function build(ContainerBuilder $containerBuilder): void
    {
        $containerBuilder->addCompilerPass($this->createCollectingCompilerPass());
    }

    private function createCollectingCompilerPass(): CompilerPassInterface
    {
        return new class implements CompilerPassInterface
        {
            public function process(ContainerBuilder $containerBuilder)
            {
                $applicationDefinition = $containerBuilder->findDefinition(Application::class);

                foreach ($containerBuilder->getDefinitions() as $definition) {
                    if (! is_a($definition->getClass(), Command::class, true)) {
                        continue;
                    }

                    $applicationDefinition->addMethodCall('add', [new Reference($definition->getClass())]);
                }
            }
        };
    }
}

2. Services

# config/services.yml

services:
    _defaults:
        autowire: true

    App\:
        resource: '../app'

    Symfony\Component\Console\Application:
        public: true

3. Bin File

# index.php

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

use Symfony\Component\Console\Application;

$kernel = new AppKernel;
$kernel->boot();

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

Run it

php index.php

If you're interested in a more detailed explanation, I wrote a post Why You Should Combine Symfony Console and Dependency Injection.

Four years after, but intended for somebody looking for a similar solution. I had a look alike scenario and implemented my own boilerplate with Dependency Injection ready to go for a standalone symfony console application, supporting auto-wiring and auto-configure for commands and services.

composer create-project coral-media/crune crune

A sample command receiving the container it's in place. Source code available at Github

Happy Coding!

Related