Lucas Courot

Symfony2 Console Component on steroids

Posted on by

One of the projects I was working on was to build a console application that needed to be run by a cron. I chose to use the great Symfony2 Console Component as I had to access MongoDB (php-mongo extension), logs (Monolog), call 2 private APIs (Guzzle), send emails (SwiftMailer), unit tests (PHPUnit), etc.

This article is written to cover all the parts you don't usually need to worry about when you use the full-stack framework.

The console compiling the container

My console file (the front controller) looks like

#!/usr/bin/env php
<?php
define('AUTOLOAD_PATH', __DIR__ . '/vendor/autoload.php');
set_time_limit(0);

if (!file_exists(AUTOLOAD_PATH)) {
    fwrite(STDERR, 'You must set up the project dependencies.' . PHP_EOL);
    exit(1);
}

require_once AUTOLOAD_PATH;

$container = new \Symfony\Component\DependencyInjection\ContainerBuilder();

$extension = new \Acme\DependencyInjection\AcmeTestExtension();

$container->registerExtension($extension);

$configLoader = new \Symfony\Component\DependencyInjection\Loader\YamlFileLoader(
    $container,
    new \Symfony\Component\Config\FileLocator(__DIR__ . '/config')
);
$configLoader->load('config_prod.yml');

$container->compile();

$container->get('console.application')->run();

I created a DI Extension to validate the configuration. Once the extension is registered in the container, I load the appropriate config file (prod in this case) using the YamlFileLoader which handles things like "imports" and "parameters". Finally, I compile my container (this will run different CompilerPasses, resolve parameter values, etc.) and I run my console application which is constructed in my service definition file.

The configuration

# config/config_prod.yml
imports:
    - { resource: config.yml }

acme_test:
    name: "<bg=yellow;fg=black;options=bold>PROD Acme Test</bg=yellow;fg=black;options=bold>"
    ...

# config/config.yml
imports:
    - { resource: parameters.yml }

acme_test:
    name: Test
    logs:
        path: "%logfile_path%"
    ...

# config/parameters.yml.dist
parameters:
    logfile_path: /var/log/acme/app.log
    ...

The Extension has nothing special

// src/Acme/DependencyInjection/AcmeTestExtension.php
class AcmeTestExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $container->setParameter('console.application.name', $config['name']);
        $container->setParameter('console.logs.path', $config['logs']['path']);
        // ...

        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services/console.xml');
        $loader->load('services/logger.xml');
    }
}

Here is where my configuration is validated.

// src/Acme/DependencyInjection/Configuration.php
class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritDoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('acme_test');
        $rootNode
            ->children()
                ->scalarNode('name')->cannotBeEmpty()->defaultValue('Acme Test')->end()
                ->arrayNode('logs')
                    ->children()
                        ->scalarNode('path')
                            ->isRequired()
                            ->cannotBeEmpty()
                            ->validate()
                                ->ifTrue(function($logFile) {
                                    return false === is_writable($logFile);
                                })
                                ->thenInvalid('%s is not writable.')
                            ->end()
                        ->end()
                    ->end()
                ->end()
                // ...
            ->end();

        return $treeBuilder;
    }
}

Miscellaneous

I also used the "incenteev/composer-parameter-handler" component to provide an interactive way to fill the parameters.yml file and to sync it on every composer update.

// composer.json
{
    "name": "acme/test",
    "description": "Acme test",
    "keywords": ["Acme", "Test"],
    "homepage": "http://www.example.com/",
    "license": "MIT",
    "authors": [
        {
            "name": "Lucas Courot",
            "email": "john.doe@example.com"
        }
    ],
    "require": {
        "php": ">=5.5.0",
        "symfony/console": "~2.4",
        "symfony/dependency-injection": "~2.4",
        "symfony/config": "~2.4",
        "monolog/monolog": "~1.8",
        "guzzlehttp/guzzle": "~4.0",
        "swiftmailer/swiftmailer": "~5.1",
        "incenteev/composer-parameter-handler": "~2.0"
    },
    "require-dev": {
        "phpunit/phpunit": "~3.7@stable"
    },
    "autoload": {
        "psr-4": {
            "": "src"
        }
    },
    "scripts": {
        "post-install-cmd": [
            "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters"
        ],
        "post-update-cmd": [
            "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters"
        ]
    },
    "extra": {
        "incenteev-parameters": {
            "file": "config/parameters.yml"
        }
    },
    "config": {
        "bin-dir": "bin"
    }
}

If you want to use PHPUnit, you also have to specify the correct bootstrap file:

<!-- http://www.phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit
    backupGlobals               = "false"
    backupStaticAttributes      = "false"
    colors                      = "true"
    convertErrorsToExceptions   = "true"
    convertNoticesToExceptions  = "true"
    convertWarningsToExceptions = "true"
    processIsolation            = "false"
    stopOnFailure               = "false"
    syntaxCheck                 = "false"
    bootstrap                   = "./vendor/autoload.php">

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>./src/*/Tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Must-read: Symfony2 Console component, by example by Loïc Chardonnet.

See the other articles published in Symfony .