PHP & Symfony About PHP and Symfony2 development

Symfony2: Console Commands as Services - Why?

Posted on by Matthias Noback

As you may have read on the Symfony blog: as of Symfony 2.4 you can register console commands using the service tag console.command.

What is the big deal, you would say. Well, let's discover it now. This is how the documentation tells you to write your console commands:

namespace Matthias\ConsoleBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class MyCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this->setName('my:action');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // do something
    }
}

How to make sure your command will be noticed?

Until Symfony 2.4 there were two requirements for your command to be picked up by the console application itself:

  1. Your command class should extend Symfony\Component\Console\Command\Command

  2. Your command class should be placed inside the Command directory (which means also in the Command namespace) of your bundle, in a file with the same name as the class.

Following these rules, it would indeed be possible to register all the available commands, which in reality goes like this:

First, in app/console an instance of Symfony\Bundle\FrameworkBundle\Console\Application is created. This object is aware of the service container, your application's kernel (e.g. AppKernel) and the bundles that are registered inside the kernel. At the end of the file, Application::run() is called:

$kernel = new AppKernel($env, $debug);
$application = new Application($kernel);
$application->run($input);

Then inside Application::run() all registered bundles are asked to register their commands:

class Application extends BaseApplication
{
    public function doRun(InputInterface $input, OutputInterface $output)
    {
        ...

        $this->registerCommands();

        ...
    }

    protected function registerCommands()
    {
        foreach ($this->kernel->getBundles() as $bundle) {
            if ($bundle instanceof Bundle) {
                $bundle->registerCommands($this);
            }
        }
    }
}

Then each bundle can register any command on its own, but most bundles extend the abstract Bundle class which contains the default implementation for registering commands:

// by the way, an abstract class should have "Abstract" as a prefix, right?

abstract class Bundle extends ContainerAware implements BundleInterface
{
    public function registerCommands(Application $application)
    {
        if (!is_dir($dir = $this->getPath().'/Command')) {
            return;
        }

        $finder = new Finder();
        $finder->files()->name('*Command.php')->in($dir);

        $prefix = $this->getNamespace().'\\Command';
        foreach ($finder as $file) {
            $ns = $prefix;
            if ($relativePath = $file->getRelativePath()) {
                $ns .= '\\'.strtr($relativePath, '/', '\\');
            }
            $r = new \ReflectionClass($ns.'\\'.$file->getBasename('.php'));
            if ($r->isSubclassOf('Symfony\\Component\\Console\\Command\\Command') && !$r->isAbstract() && !$r->getConstructor()->getNumberOfRequiredParameters()) {
                $application->add($r->newInstance());
            }
        }
    }
}

So this is where the magic happens! My clean code eyes don't like this at all, but nevertheless: what happens inside this method?

  1. The Finder (yes, the Finder!) is used to find files ending with "Command.php" in the directory "Command".

  2. It tries to load the class inside this file, assuming that its name will correspond to the file name (it should, considering PSR-0).

  3. It checks using reflection if the class is an instance of the Command class I mentioned earlier.

  4. It makes sure that the class is not an abstract class.

  5. It also makes sure that its constructor has no required arguments.

  6. If this is not the case, it can be instantiated, so: it creates an instance of the class.

Bad bundle!

I really dislike all of this, because:

  1. All this code is executed each time you run any console command, which is obviously overkill.

  2. All commands from the entire application are being instantiated every time.

  3. This code violates the open closed principle in that there may be situations where a command is accidentally registered, or not registered when it was intended to be. These corner cases will result in more conditions being added (what about private constructors for instance?).

  4. In fact, the whole command class is being robbed of its creation logic. The only way such an object may be created is using newInstance(), which is the same as new ClassName().

  5. I am supposed to put my commands in the Command directory, but what if one of my commands is not Symfony-specific (like the Doctrine commands) and I want to import it from some PHP library package?

I think it is possible to refactor the registerCommands() method so that number 1 is no problem anymore. I think 2 is a design problem, which is not easy to solve, since you can only know the name of a command and its arguments when you have an instance of it and you can call its getName() method. So commands can never be truly lazy-loading.

Number 3 is not such a big problem anymore, now that you have your own way to register commands (i.e. using the console.command service tag, since this means that you already have to make sure that a service does not point to an abstract class, or has constructor arguments which may not be supplied. This also solves number 4, since all creation logic is entirely in your own hands again. Number 5 is also no problem anymore: you can just create a new service for the command that exists outside of your bundle.

As a matter of fact, I have been registering my commands manually since some time now, which skips all the not-so-nice code in the regular Bundle class:


namespace Matthias; use Symfony\Component\HttpKernel\Bundle\Bundle; use Matthias\ConsoleBundle\Command\MyCommand; class ConsoleBundle extends Bundle { public function registerCommands(Application $application) { // ah, nice and clean! $application->add(new MyCommand()); } }

Coupling...

All seems well! But it isn't... As you may have seen in the sample command class above: it extends ContainerAwareCommand. This means that inside the execute() method you can take any service you like from the service container:

class MyCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this->setName('my:action');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $someService = $this->getContainer()->get('some_service');

        // $someService is expected to be an instance SomeService

        $someService->doSomething();
    }
}

As you can imagine: this couples your command horribly to the Symfony2 service container. But there is nothing to a console command that requires a service container really. You may do everything inline, you may even use another service container/locator from another vendor. The only thing you need in this particular command is an instance of SomeService.

Making your console command a service using the new service tag does not make any difference when it comes to this kind of coupling. It just skips all the ugly digging around in directories and files (or in fact, it just adds another way to this, it will still take a look in the Command directory!).

... and decoupling

What can you do, to make your commands cleaner, less coupled to the framework? This!

use Symfony\Component\Console\Command\Command

class MyCommand extends Command
{
    private $someService;

    public function __construct(SomeService $someService)
    {
        $this->someService = $someService;

        parent::__construct();
    }

    protected function configure()
    {
        $this
            ->setName('my:action');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $this->someService->doSomething();
    }
}

We know now that $this->someService is an instance of SomeService. We don't need a service container anymore, since dependencies are injected nicely via constructor arguments. We only need to register the command like this:

<service id="my_command" class="Matthias\ConsoleBundle\MyCommand">
  <argument type="service" id="some_service" />
  <tag name="console.command" />
</service>

Categories: PHP Symfony2

Tags: console dependency injection service container coupling

Comments: Comments