Kohana-style before and after methods in Symfony2

Spanish or English?

When I decided to start my blog adventure, I spent some time wondering if it was better to go for Spanish or English.

And I decided to go for Spanish because although there are so many top-level PHP documentation resources and blog sites written in English, there are not that many in Spanish. However, most of my Symfony2 posts will go in English because there is a lot of people trying to deal with this amazing framework but it is still quite difficult to find good examples apart from the ones in the official website book and cookbook.

Having made this clear and hoping no-one gets mad about it, let’s go with the topic.

Kohana3 vs Symfony2

Let’s be clear, I love Kohana framework. I like the easiness to get started and the moderate learning curve that makes me improve my Kohana code in every project where I choose this framework. But one of the main problems against the massive use of Kohana is the lack of books and documentation resources. Most of the time you have to dive into the code and see what’s happening there to get things done. And this is not good in a real business where you need to achieve short time to market software deployment.

Some posts ago, I said that if I could choose a framework to start a big project from scratch I would use Symfony2 because it uses all the power of PHP5.3, it embraces best practices and most important there is a lot of community behind it. So when I started to work at Ulabox, we decided to go for it to develop the backoffice and the company core.

However, being Symfony2 such different from the rest of PHP5.2 based frameworks we have found that the learning curve is much steeper than we expected and there’s a lot of people asking how to this and that and very few people knowing the answers. Well, the hello world thing and a simple website is quite easy to get done but a real backoffice or a company public api are not that simple and most framework features have yet to be properly documented.

The Github incident

This past week I opened an issue on Github asking about the possibility to have before/after methods in controllers, like Kohana does (or if you’re used to Symfony1, preExecute and postExecute methods, or the _init* in Zend Framework).

Let me say that I became quite amazed that some of the Symfony2 core developers (@stof, @lsmith77, @schmittjoh and the lead developer @fabpot) commented on the issue and pointed me to the right direction: the symfony2 event listeners. You can see the discussion topic here.

To put things into context, let me explain what I was trying to do and why I needed before/after methods.

My problem

I am developing an API for Ulabox. The API will be accessed by the incoming Iphone App, probably some day by an Android App and parts of the website will also go through this API.

The API es JSON-REST based so the URLs are like /auth/login.json, /catalogue/{productid}/getProductDetails.json and authentication / access control is token-based. So, there are some actions that will be token-free, some of them will work for all devices but will need a correct token, and some of them will be restricted (for instance we may want some functionality to work on Iphone but not on Android/Blackberry).

So, in my case, I need to perform some validations BEFORE executing the action based on the tokens and grant access or not.

Proposals by gurus

@fabpot told me to check for kernel.controller event to do the before logic and kernel.response to do the after logic.

@stof proposed a really elegant approach, creating a Listener and executing logic depending on a Controller Interface. The code is something like this.

<?php
namespace Acme\DemoBundle\EventListener;
 
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
 
class BeforeControllerListener
{
     public function onKernelController(FilterControllerEvent $event)
     {
           $controller = $event->getController();
           if (!is_array($controller)) {
                // not a object but a different kind of callable. Do nothing
                return;
            }
 
            $controllerObject = $controller[0];
            if ($controllerObject instanceof InitializableControllerInterface) {
                   $controllerObject->initialize($event->getRequest());
                   // this method is the one that is part of the interface.
            }
     }
}
?>

And @schmittjoh proposed a solution that mimetizes exactly the good old before / after methods.

<?php
 
class Listener
{
    public function onKernelController($event)
    {
        $currentController = $event->getController();
        $newController = function() use ($currentController) {
            // pre-execute
            $rs = call_user_func_array($currentController, func_get_args());
            // post-execute
 
            return $rs;
        };
        $event->setController($newController);
    }
}
?>

And this is how I solved the problem based on their comments

My solution

Tokens configuration in config.yml

ulabox_api:
    # Tokens for API
    tokens:
        iphone: iphonesecrettoken
        web: websecrettoken
        android: androidsecrettoken

Reading this token configuration in DependencyInjection folder:

<?php
// This goes inside Configuration.php, amongst other config loading:
        $rootNode
            ->children()
                ->arrayNode('tokens')
                    ->useAttributeAsKey('id')
                        ->children()
                            ->scalarNode('iphone')->end()
                            ->scalarNode('web')->end()
                            ->scalarNode('android')->end()
                        ->end()
                ->end()
            ->end()
 
// And this goes inside UlaboxApiExtension (change this for your bundle name)
// You get $tokens array and inside it, $tokens['iphone'], $tokens['web'] and $tokens['android']
$container->setParameter('tokens', $config['tokens']);
?>

Setting the listener to be executed

<?php
 
// service listeners configured through yml, which I like best than XML
$ymlloader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$ymlloader->load('services.yml');
 
?>

And this is services.yml configuration content

# services.yml content
services:
    ulabox_api.tokens.action_listener:
      class: Ulabox\ApiBundle\EventListener\ApiActionListener
      arguments: [@service_container]
      tags:
            -   { name: kernel.event_listener, event: kernel.controller, method: onKernelController }

And finally, the listener code:

<?php
 
namespace Ulabox\ApiBundle\EventListener;
 
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
 
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
 
/**
 * This code gets executed everytime Kernel sends a event to a ApiBundle Controller
 * Here tokens are checked and if token is not ok, exception is thrown
 *
 * @throws AccessDeniedHttpException in case token is not valid
 */
class ApiActionListener
{
    /**
     * This variable gets kernel container object
     *
     * @var ContainerInterface
     */
    protected $container;
 
    /**
     * This constructor method injects a Container object in order to have access to YML bundle configuration inside the listener
     *
     * @param ContainerInterface $container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }
 
    /**
     * This method handles kernelControllerEvent checking if token is valid
     *
     * @param FilterControllerEvent $event
     * @throws AccessDeniedHttpException in case token is not valid
     */
    public function onKernelController(FilterControllerEvent $event)
    {
        $controller = $event->getController();
 
        /**
         * $controller passed can be either a class or a Closure. This is not usual in Symfony2 but it may happien.
         * If it is a class, it comes in array format, so this works as @stof said
         * @see https://github.com/symfony/symfony/issues/1975
         */
        if(!is_array($controller)) return;
 
        /**
         * @todo This works because right now all API actions need a token and are available to all devices.
         * A cleaner method will need to be done if we want to restrict actions depending on agent token. 
         * This code gets executed every time, even when a Exception occurs, where Symfony2 executes ExceptionController, so on that case, no actions based on tokens needs to be done
         */
        if($controller[0] instanceof ExceptionController)  return;
 
        /**
         * Each request sends a token through apitoken param, and we need to check this against "tokens" bundle configuration 
         */
        $tokens = $this->container->getParameter('tokens');
        $tokenpassed = $event->getRequest()->get('apitoken');
 
        if(!in_array($tokenpassed, $tokens))
            throw new AccessDeniedHttpException('Invalid API Token');
      }
}
?>

As you can see, right now this is not really sofisticated but if more complex logic needs to be done, we just have to set some kind of “IphoneExclusiveInterface” and check token against $tokens[‘iphone’] for instance.

And we protect all controllers except the ExceptionController to be provided a token with one single piece of code.

Final thoughts

Symfony2 is a really nice piece of software but it still needs to be documented and most important, a big cookbook.
They have developed a way to do things that is extremely different to all that we are used to and sometimes it can be quite difficult to get used to it. But trust me, once you get used to the Symfony2 way of live you wonder how you could live without it.

Hope you liked this post, and let’s hope it can help someone some day!

You may also like...