Symfony2: Rich Console Command Output Using AOP
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)…
