Symfony2: Rich Console Command Output Using AOP

Posted on by Matthias Noback

I really like to write console commands for Symfony2 applications. There is something very cool about the Symfony Console Component which always makes me look for new things that I could do from the command line. A very simple command might look like this:

namespace Matthias\BatchProcessBundle\Command;

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

class BatchProcessCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this->setName('matthias:batch-process');
    }

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

        ...
    }
}

Writing good, clean command classes can very problematic: you want to provide detailed output to the user, and tell him what is currently going on, what the result of some action was, or why something failed. You need the console output for this. It is an instance of OutputInterface and it is provided as the second argument of the command's execute() method. The body of this method will usually look like this:

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

    foreach (array('main', 'sub', 'special') as $collection) {
        $output->writeln(sprintf(
            'Processing collection %s',
            $collection
        ));

        $returnValue = $processor->process($collection);

        if ($returnValue) {
            $output->writeln('Succeeded');
        } else {
            $output->writeln('Failed');
        }
    }
}

This is only a very simple example. Soon you will execute lots of methods inside the execute() method and write lots of messages to the console output. You will then be tempted to pass the given $output to the batch_processor service, so it can write messages to the console all by itself:

namespace Matthias\BatchProcessBundle;

class BatchProcessor
{
    public function process($collection, OutputInterface)
    {
        $output->writeln(sprintf(
            'Processing collection %s',
            $collection
        ));

        ...
    }
}

This, however, results in very bad code - a class with too many responsibilities, high coupling and bad cohesion (what's processing got to do with outputting things to the console?).

It's time for separating concerns and in this case it is most easily done using AOP - aspect oriented programming. It will result in very clean classes, both the BatchProcessCommand class and the BatchProcessor won't have to use the console output directly anymore.

AOP with the JMSAopBundle

AOP basically means: intercepting method calls and their return values and do with them whatever you want. I'm using the JMSAopBundle for this purpose. You can install it using Composer: composer.phar require jms/aop-bundle 1.*. When you have installed the JMSDiExtraBundle or the JMSSecurityExtraBundle the JMSAopBundle is already available in your project.

There are two classes you need to define to be able to intercept method calls. First you need a pointcut class: the pointcut is used to determine if a combination of a class and a method should be intercepted. In our case, as an example, we want to intercept the process() method of the Matthias\BatchProcessBundle\BatchProcessor class. So the pointcut class would look like this:

namespace Matthias\BatchProcessBundle\Interception;

use JMS\AopBundle\Aop\PointcutInterface;

class BatchProcessorPointcut implements PointcutInterface
{
    public function matchesClass(\ReflectionClass $class)
    {
        return $class->getName() === 'Matthias\BatchProcessBundle\BatchProcessor';
    }

    public function matchesMethod(\ReflectionMethod $method)
    {
        return $method->getName() === 'process';
    }
}

To register the pointcut, create a service for the pointcut and give it the jms_aop.pointcut tag:

<service id="batch_processor_pointcut"
         class="Matthias\BatchProcessBundle\Interception\BatchProcessorPointcut">
    <tag name="jms_aop.pointcut" interceptor="batch_processor_interceptor" />
</service>

The interceptor attribute of the tag points to another service, the interceptor. This service will be able to intercept all calls to the process() method objects of class Matthias\BatchProcessBundle\BatchProcessor. While intercepting these calls, I want to write some lines to the console output showing which collection is being processed, and what the result of processing the collection was:

namespace Matthias\BatchProcessBundle\Interception;

use CG\Proxy\MethodInterceptorInterface;
use CG\Proxy\MethodInvocation;
use Symfony\Component\Console\Output\ConsoleOutput;

class BatchProcessorInterceptor implements MethodInterceptorInterface
{
    private $output;

    public function __construct()
    {
        $this->output = new ConsoleOutput();
    }

    public function intercept(MethodInvocation $invocation)
    {
        list($collection) = $invocation->arguments;

        // show the first argument of the call to process()
        $this->output->writeln(sprintf(
            'Processing collection %s',
            $collection
        ));

        $returnValue = $invocation->proceed();

        // show the result of calling the real process() method
        if ($returnValue) {
            $this->output->writeln('Succeeded');
        } else {
            $this->output->writeln('Failed');
        }

        // don't forget to return the return value
        return $returnValue;
    }
}

This would be the corresponding service definition (there is nothing special about it):

<service id="batch_processor_interceptor"
         class="Matthias\BatchProcessBundle\Interception\BatchProcessorInterceptor">
</service>

The intercept() method of the interceptor will receive a MethodInvocation object. We can use it to inspect the arguments of the original call to process(). We can then let the method call proceed, catch the return value and do anything we want with it. Finally, we return the return value itself (this does not have to be the original return value).

An auto-generated proxy class

With the above service definitions and classes in place, the JMSAopBundle does something fantastic: it inspects all service definitions in the service container and for each service definition, it asks the pointcut if it should intercept calls for this class. It will then iterate over all the methods of the class and ask for each method if it should intercept calls to that specific method. When the answer is affirmative, the bundle will create a proxy class for the given class. This class is stored in /app/cache/{env}/jms_aop. The original class as provided in the service definition is then replaced with the proxy class.

To demonstrate this, I have defined a service for the BatchProcessor class:

<service id="batch_processor"
         class="Matthias\BatchProcessBundle\BatchProcessor">
</service>

Inside the dumped service container (which you can find in /app/cache/{env}) this normally results in the following code for creating the batch_processor service:

protected function getBatchProcessorService()
{
    return $this->services['batch_processor'] =
        new \Matthias\BatchProcessBundle\BatchProcessor();
}

But since I have told the JMSAopBundle that I want to intercept calls to the batch_processor service, it now looks like this:

protected function getBatchProcessorService()
{
    require_once '[...]/app/cache/dev/jms_aop/proxies/Matthias-BatchProcessBundle-BatchProcessor.php';

    $this->services['batch_processor'] =
        $instance = new \EnhancedProxy_3679290e8639cf35fb690067e6b2d82603d366ac\__CG__\Matthias\BatchProcessBundle\BatchProcessor();

    $instance->__CGInterception__setLoader($this->get('jms_aop.interceptor_loader'));

    return $instance;
}

And a file /app/cache/{env}/jms_aop/proxies/Matthias-BatchProcessBundle-BatchProcessor.php has been created containing this class:


namespace EnhancedProxy_3679290e8639cf35fb690067e6b2d82603d366ac\__CG__\Matthias\BatchProcessBundle; class BatchProcessor extends \Matthias\BatchProcessBundle\BatchProcessor { private $__CGInterception__loader; public function process($collection) { $ref = new \ReflectionMethod('Matthias\\BatchProcessBundle\\BatchProcessor', 'process'); $interceptors = $this->__CGInterception__loader->loadInterceptors($ref, $this, array($collection)); $invocation = new \CG\Proxy\MethodInvocation($ref, $this, array($collection), $interceptors); return $invocation->proceed(); } public function __CGInterception__setLoader(\CG\Proxy\InterceptorLoaderInterface $loader) { $this->__CGInterception__loader = $loader; } }

This basically means that the service definition has been silently modified to use another class and as you can see: this proxy class overrides the process() method (as we requested).

What this means...

Since all output to the console is handled by the BatchProcessorInterceptor, which is called whenever someone makes a call to the process() method of the batch_processor service, we can now clean up both the BatchProcessor class and the BatchProcessorCommand class:

class BatchProcessCommand extends ContainerAwareCommand
{
    ...

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

        foreach (array('main', 'sub', 'special') as $collection) {
            $processor->process($collection);
        }
    }
}

No more direct calls to $output here! And no need to pass the $output through to the batch processor.

class BatchProcessor
{
    public function process($collection)
    {
        // true or false
        $result = ...;

        return $result;
    }
}

Same here, you will find no console-specific things inside the processor anymore.

Still, your console command will show some nice output (which is why you love it)...

Nice output

PHP Symfony2 AOP console
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
cordoval

I have two more comments:

1. I still believe aop is for this use case. I don't think symfony's way is the way in this case for console apps that do not use bundles. I believe we need to use a stand alone library to achieve this and not a bundle.

2. Gush needs this and other console apps need this and also a way to support phars and still be able to do this. I am sure we will find a good way together.

Thanks, please fix the typo tThis

your rock'n it man

Tobias Schultze

Hey,

nice article. I described a different solution to this problem at https://blog.liip.ch/archiv...

Greets

Mauricio

Correct me if I am wrong. But AOP as a concept is nice for things like ACL or Global behaviors.

I am a symfony2 fan, but I am also using other platforms because other projects require them. One is LIthium php framework. It has a AOP in the very core. Sometimes I notice that developers overuse that, and finding problems is very difficult without documentation.

So I wanted to know your opinion about when is correct to use AOP and when not.

Matthias Noback

I'm also still struggling with the how and why of AOP. I always thought that it should be used for non-critical things, but I don't believe this anymore. It can be used for anything that is a cross-cutting concern, i.e. something that permeates all layers of the applications (indeed, like ACL, or logging, or outputting things). Many times it helps with moving code with a certain responsibility to some other place, so that you can stick with the single responsibility principle.

About the debugging aspect: it can sometimes be a bit difficult to find out where a certain behavior comes from, though this is the case with many more things, like annotations for persistence and validation metadata, or options in an array. As long as I can step through the code with a debugger, I'm happy.

Jan Kramer

Awesome example!

slightly offtopic:
Besides logging (like in this example), AOP is also very suited for separating security, or other cross-cutting concerns more generally speaking. In fact, the JMSSecurityExtraBundle depends heavily on AOP as you mentioned.

I recently encountered several projects with more than average complex security rules. At the controller / service level, this can be quite nicely handled using the SecurityExtraBundle, but the templates are still riddled with "is-granted"-rules which increase the complexity of templates massively.

By reading your post I realized this could possibly be solved by applying the AOP paradigm (not the bundle per se) to twig blocks, e.g. by creating a "secured_block" tag which extends the regular block tag by wrapping the block contents with predefined security checks added at compile time. The pointcut could match on block name. What are your thoughts about such application of AOP? Do you think this is desirable/feasible, or do you know better alternatives?

Matthias Noback

Thanks Jan,
AOP-for-Twig is a very interesting idea. This way you could add logging, timers and security-related checks to Twig nodes. When you ever start working on something like this, I would happy to participate!

Jan Kramer

That's great! I actually started working on a proposal today on how things could be implemented. It's work-in-progress, but you're very welcome to participate!

https://github.com/deft/twi....

Matthias Noback

Nice work, I will take a closer look later, but this is a good start!

Marcos Passos

What is the advantage of this approach over events?

Matthias Noback

Hi Marcos,
Using events for this purpose is great and it will result in a better separation of concerns. In fact I used to do it this way too. However, using event listeners to show extra information during the process, will also make you add extra events to existing code. These events won't serve any other purpose than allowing the event listeners to output something. It should be the other way around I think. Also, making many calls to the event dispatcher will clutter the original classes. By just reading the code it will be less clear what they do.

Marcos Passos

Matthias,

Thanks for your reply. In fact, I think you wont clutter the original classes once Symfony2 now supports commands events so it would be transparent for you. Furthermore, reflection is slow.

AOP is a cool approach indeed, and thank you for raising it, but I think that events can achieve this goal in a more elegant, simple and performatic way.

More informations about console events:
http://symfony.com/doc/mast...

Matthias Noback

Hi Marcus,

Command events are indeed very nice, but they won't help much in this situation, since you will still have to write things like:

[php]
class BatchProcessor
{
public function process($collection)
{
$this->dispatcher->dispatch(
'batch_processor.process_collection',
new CollectionEvent($collection)
);

...
}
}
[/php]

This is what I meant with adding events that are just there to allow an event listener to output some information. Such events do not serve any other purpose - it is not essential for the BatchProcessor class so from a clean code point of view they don't belong there.

Still, if you don't like proxy classes, this is the way to go and I've implemented it like this very often.

By the way, I don't think reflection is that slow - many people mention this, but when it comes to actual time needed to process, it's still microseconds. Your application won't halt or something.

Best regards,

cordoval

great post, i actually had a problem and was already passing the output all around :)

this definitely saves the day, however i have a question, what is the impact in performance for these replacements of services

another question is when to use it, if for instance solely to decouple like this or when it would be abusing this?

There could be perhaps a way to disable it real quick for production or when dropping the level of loggging to debug is enabled but when on production only critical
now this would be to inject or redirect the Output to a monolog consumer or such

thanks

Matthias Noback

Hi Luis,
Thanks for asking these good questions! Performance is not a big problem I guess. The proxy classes will be generated only when the container is built (so for production only once). Besides, this is the way the JMSSecurityExtraBundle wraps all controllers with @Secure and related annotations.
Also: in the case of console commands it's pretty harmless since those will be executed less often than controllers.