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...

15 Responses

  1. cordoval says:

    Thanks, now I understand somehow better the approach.
    It would have been better if you would not assume that the reader is aware of the following:

    // what this does exactly ?
    $controllerObject->initialize($event->getRequest());

    // how the event object holds the controllers ?
    $controller = $event->getController();

    // on the stof method: please show an interface sample

    // do a bit more of background information on how the controllers and event listener works, a diagram or the like, that would make this post into a superb explanation and will help you and us all to understand this crystal clear.

    thanks!

  2. Ricard Clau says:

    @cordoval I agree that the explanation might be enough for people quite deep into Symfony2 but can be quite obscure for people who is just watching the whole symfony2 hype.

    I have been linked from your site and from symfony.com so there can be more people wondering similar things like you are. So, I promise that this weekend I will provide an improved implementation (with two interfaces, one general for all 3 tokens, one for iphone) and some explanation of what’s really happening behind the scenes.

    Until that, regarding your questions:

    the $object->initialize($event->getRequest()); just executes a method that MUST exist in the controller. So you create InitializableControllerInterface with this “initialize” method, you make your controller implement this interface and you get a “before” method like Kohana, but it is only mandatory in those controllers that implement the interface. It is not actually full working code, just an explanation and how could this be achieved inside the listener.

    Regarding $event->getController(), as @stof also says in the github thread, “The kernel.controller event gives you access to the controller matched by the request. $event->getController() returns the callable (so generally something like array($controllerObject, ‘myAction’) in the framework). So you can do some logic based on the controller you get (checking that it implements a given interface for instance)”

    Hope this makes a little more clear until my second post on this issue 🙂

  3. unixo says:

    Very interesting and well described experience, thanks for sharing, Ricard! 🙂

  4. mauricio lopez says:

    Hola Ricard.

    Deseo consultarte lo siguiente.

    Digamos que tengo varios bundles en un sitio symfony. Un bundle es frontend el otro es backend.

    Quiero que el bundle frontend solo sea accesado por computadores de escritorio, no mobiles.

    Ya tengo la logica para saber cuando el request es lo uno o lo otro.

    Como implementarias tu esta logica en un evento aplicable solo a ciertos controladores (frontend bundle) y redireccionarias al usuario a otro sitio si es que esta navegando con un mobile.

    El bundle backend y sus controladores no tendrian esta logica de redireccion.

    El tema de los events es muy poderoso pero un poco confuso.

    saludos

  5. Ricard, many thanks for this! I followed your discussion on GitHub and then adapted your blog post to work for me. I needed a more general pre/post Action EventListener so I went for the solution proposed by @schmittjoh.

    It works brilliantly!

  6. Ricard Clau says:

    @Alex Yes, both options are absolutely correct, it is just a matter of which suits better. And it works perfectly with no performance issue at all. Good for Symfony2 core team.

    @mauricio Un caso como el que comentas necesita una implementación muy parecida a la mía. Aquellos controllers que debas controlar su redirección en caso de que el dispositivo sea móvil, deberías hacerlos extender de un interface común a todos. Y en el Listener, si el controller es de ese interface y es un dispositivo móvil, hacer el redirect 302.

  7. Unlike other websites This one has info that is very excellent. Keep up thegreat work. Thanks

  8. Unai Roldán says:

    @mauricio Quizás llego un poco tarde, pero todo sea por aportar algo de info:

    Aquí tienes un enlace que hace exactamente lo que Ricard comenta: https://matt.drollette.com/2012/06/calling-a-method-before-every-controller-action-in-symfony2/

  9. It’s very trouble-free to find out any matter on net as compared to textbooks, as I found this article at this website.

  10. Hi there! I simply want to offer you a big thumbs up for your great info you have got right here on this post.

    I will be coming back to your web site for more soon.

  11. soup.io says:

    You have opened up my eyes right now on this thanks a
    lot

  1. 23/08/2011

    […] is a nice article written here about implementing some hooks for controllers right before and after processing the controller. […]

  2. 04/09/2011

    […] post continues last week post about implementing before / after hooks like Kohana and other frameworks do, but in a Symfony2 […]

  3. 13/01/2013

    […] Required for symfony2 bootstrap alternative.http://www.ricardclau.com/2011/08/kohana-style-before-and-after-methods-in-symfony2/ […]