Kohana-style before and after methods in Symfony2 (Vol. II)

This post continues last week post about implementing before / after hooks like Kohana and other frameworks do, but in a Symfony2 context. Today I will try to explain a bit further what is really happening behind the stages and provide a better implementation with the interface approach that @stof suggested in The Github incident. Here we go!

Symfony2 configuration

Most things remain the same:

Tokens configuration in config.yml

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

Loading this 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 custom listener:

Also in UlaboxApiExtension file:

<?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 services.yml 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 }

What is really happening?

After all this setup, we are enhancing kernel.event_listener service, which is actually “listening” to what is happening and executing some code depending on the event requested. In that case, we want to execute some code BEFORE the actual controller->action code gets executed, so the event for that is event.controller.

And our code will be inside Ulabox\ApiBundle\EventListener\ApiActionListener class in its method onKernelController. Furthermore, this method receives the magic service_container object containing all configuration from other bundles and system.

A cleaner implementation would be to pass only the tokens (which is what is really requested and the only thing that really needs to be injected) but I prefer to pass the full object in case we ever may need something else from other bundles / configuration / whatever.

The interface approach

As I said in the previous post, I am developping an API. Some methods are public for everyone, some methods need to get an identification on the Agent requesting (being Iphone, Android, or the actual Ulabox website) and even some methods are exclusive for one of those Agents.

And in this case, we will take advantadge of the PHP interfaces to decorate the controllers and executing some code depending on the interface that the controller implements.

We will create 2 interfaces inside the Controller folder. One is NeedsValidTokenInterface.php and the other is NeedsIphoneTokenInterface.php. These interfaces will be empty, so nothing is defined inside it, but we could also define some methods and force the controllers implementing them to add some logic.

<?php
namespace Ulabox\ApiBundle\Controller;
 
interface NeedsValidTokenInterface
{
 
}
?>
<?php
namespace Ulabox\ApiBundle\Controller;
 
interface NeedsIphoneTokenInterface
{
 
}
?>

And when defining the controllers we can make them these tree ways:

Public Controllers
controller xxxxx extends Controller
This controller will be absolutely public to all requests

Tokenised Controllers
controller yyyyy extends Controller implements NeedsValidTokenInterface
This controller will only get executed if a valid token is supplied

Iphone exclusive Controllers
controller zzzzz extends Controller implements NeedsValidTokenInterface, NeedsIphoneTokenInterface
This controller will be exclusive for Iphone (thus meaning only gets executed if token is exactly Iphone Token)

Note that with Symfony2 routing system, our controller can be called any way and execute any URL (this is not the case in Kohana3 and other frameworks).

For instance we can have a Controller like this:

<?php
namespace Ulabox\ApiBundle\Controller;
 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 
use Symfony\Component\HttpKernel\Exception\HttpException;
 
class IphoneController extends Controller implements NeedsValidTokenInterface, NeedsIphoneTokenInterface
{
    /**
     * Gets customer cart detail
     *
     * @return array containing cart detail or Error information if there is a problem
     *
     * @Route("/mycart/getDetails.json")
     * @Template("here goes my template")
     */
    public function anyNameYouLike()
    {
 
    }
}
?>

And this method responds to mycart/getDetails.json calls, despite the Controller and the Action being called differently!

The Listener

And to end this, the Listener Code!

<?php
namespace Ulabox\ApiBundle\EventListener;
 
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
 
use Ulabox\ApiBundle\Controller\NeedsIphoneTokenInterface;
use Ulabox\ApiBundle\Controller\NeedsValidTokenInterface;
 
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;
 
        $tokens = $this->container->getParameter('tokens');
        $tokenpassed = $event->getRequest()->get('apitoken');
 
        /**
         * If controller implements NeedsValidTokenInterface, we need to check that the token supplied is valid
         */
        if($controller[0] instanceof NeedsValidTokenInterface)
        {
            if(!in_array($tokenpassed, $tokens))
                throw new AccessDeniedHttpException('Invalid API Token');
        }
 
        /**
         * If controller implements NeedsIphoneTokenInterface, we need to check that the token supplied is indeed the iphone one
         */
        if($controller[0] instanceof NeedsIphoneTokenInterface)
        {
            if($tokens['iphone'] !== $tokenpassed)
                throw new AccessDeniedHttpException('Exclusive Iphone URL');
        }
 
     }
}
?>

As you can see, we take advantage of the instanceof construct to identify the controller and check some things depending on that interface.

Remember that the instanceof construct checks for an object being that class but also a class extending the class checked or even a class implementing an interface!!!

Hope this makes a little bit more clear what was really hapenning, and how I solved it.

Final thoughts

As you can see, event listening opens a wide world of things that we can do in our code. And it can be done in a clean way, not just creating a bunch of abstract controllers to do those things.

Symfony2 is also extremely fast. Some people may think that all these event listeners, these code, these strange yml configurations can slow down your website performance, but trust me, as Fabien Potencier says: Symfony2 is fast as hell.

Hope you liked it!

You may also like...

2 Responses

  1. I’m no guru, but couldn’t you define your controllers as services and use service tags to determine which token to check?

  2. Ricard Clau says:

    Hi

    I am sorry I don’t understand what you mean.

    This is used in a controller in my API application, so defining this controllers as services has no sense from my point of view.

    I agree, however, that it would be better to inject just the array of tokens and not the container, but I don’t see how to achieve what you say. Could you please provide an example for that?