Introduction

Recently I decided to write small console tool and I wanted to use Symfony Console component. The tool will have many commands. Any command may have some dependencies on some services. In Symfony Standard Edition it is very easy. All you need is add the command and extend the Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand class.

The problem

The problem with the solution above is that I would need to add the whole Symfony Framework Bundle which has lot’s of dependencies I do not need including routing, assets etc. I want to keep the tool as small as it is possible. I had to find out a better way to resolve the problem.

Before we start let’s download all the dependencies we really need.

mkdir my-commands
cd my-commands
composer require symfony/console
composer require symfony/config
composer require symfony/dependency-injection

We need a “main” file where we will start the application, load configs and so on. In Symfony Standard Edition the file is in bin/console. We will create the same file.

#!/usr/bin/env php
<?php
require __DIR__.'/../vendor/autoload.php';
 
use Symfony\Component\Console\Application;
 
$application = new Application();
 
$application->add(new GenerateAdminCommand());
$application->add(new AnotherAdminCommand());
// etc...
 
$application->run();

… and remember to make it executable

chmod +x bin/console

When you create very simple solution it will be enough. However, when you add many of the commands and the commands will have many dependencies the file will become very complicated and ugly. The ideal solution would add the commands to the application as services and load them via dependency injection container. But how to do that?

First command and DI configuration file

Let’s say we have a \ConsoleDI\Command\HiCommand command which says “Hi!” on the stdout. Above is my simple implementation of the class.

<?php
 
namespace ConsoleDI\Command;
 
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
 
class HiCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('say:hi')
            ->addArgument('name', InputArgument::REQUIRED, 'The person you want to say hi!')
        ;
    }
 
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        $output->writeln(sprintf('Hi %s!', $name));
    }
}

Now, it is time to create the services.xml file where we will keep all the service’s definitions. I create it in /config/services.xml file which you can find above

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="command.hi" class="ConsoleDI\Command\HiCommand">
        </service>
    </services>
</container>

As you can see it is a standard file. Very similar you can find in the official docs.

We created our first command, wrote the definition of it, but how does symfony console should know he should load it to be available using system console? The key part of the tool is the Application class. It is time to take a deeper look on it.

What will we find there? We define the name of the application, have methods to add/find new commands and some helpers methods. Not very interesting for us except getDefaultCommands() method. In this method, the first 2 commands (help and list) are added. Bingo! Let’s write our own application class and load the commands in the method we found.

Our Application class

As you remember, we want to load the commands from DI container so let’s load it in the constructor.

<?php
 
namespace ConsoleDI\Console;
 
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
 
 
class Application extends \Symfony\Component\Console\Application
{
    /**
     * @var ContainerBuilder
     */
    protected $container;
 
    /**
     * Constructor.
     *
     * @param string $name    The name of the application
     * @param string $version The version of the application
     */
    public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
    {
        $this->container = new ContainerBuilder();
        $loader = new XmlFileLoader($this->container, new FileLocator(__DIR__.'/../../config'));
        $loader->load('services.xml');
 
        parent::__construct($name, $version);
    }
}

Before we go any further, we need to think about getting only commands from the container. The easiest way is to use tags. It is time to update the services.xml file.

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="command.hi" class="ConsoleDI\Command\HiCommand">
            <tag name="console.command" />
        </service>
    </services>
</container>

Now, we have over the hump. My sample implementation you can find above.

<?php
    /**
     * Gets the default commands that should always be available.
     *
     * @return Command[] An array of default Command instances
     */
    protected function getDefaultCommands()
    {
        $commands = [];
 
        foreach ($this->container->findTaggedServiceIds('console.command') as $commandId => $command) {
            $commands[] = $this->container->get($commandId);
        }
 
        return $commands;
    }

As you can see, we removed the default commands. To add them you need to update the services.xml again. Remember about the special tag we created.

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="list" class="Symfony\Component\Console\Command\ListCommand">
            <tag name="console.command" />
        </service>
 
        <service id="help" class="Symfony\Component\Console\Command\HelpCommand">
            <tag name="console.command" />
        </service>
 
        <service id="command.hi" class="ConsoleDI\Command\HiCommand">
            <tag name="console.command" />
        </service>
    </services>
</container>

Summary

Since now we have the fully functional command line tool with dependency injection. We create very simple command line tool, we do not need the whole Symfony stack, but we can just pick some we really need. That’s why I love Symfony. If you want to see example project, do not be shy – go to my github and see how I did it.

About the author

Bartłomiej Kiełbasa

Bartłomiej Kiełbasa

Hi! I'm Bartek. I'm a PHP developer but other languages are not scary to me. My hobby is security and I try to learn as much as it's possible how to not be hacked. I'd ike to know how things work. After hours I like playing Dota2 and watching Dragon Ball :) If you will have any question, feel free to ask.