There's a much more detailed chapter about this subject in my book A Year With Symfony.

Symfony2 provides multiple ways of blocking, providing or modifying the response. You can:

  1. Intercept each request by listening to the kernel.request event and set the response directly on the event (which will effectively skip execution of a controller)

  2. Modify the controller or its arguments by listening to the kernel.controller event, then calling setController on the event object and modifying the attributes of the Request object.

  3. Change the response rendered by a controller, by listening to the kernel.response event.

I have struggled many times with providing an answer to the following seemingly easy question: what if I want to wait until just before the controller gets called, then do some checks and prevent the user from executing the current controller, and instead, render an entirely different response. How should I proceed? At first thought, using option two would be good. But not really:

use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

class ControllerListener
{
    public function onFilterController(FilterControllerEvent $event)
    {
        // do some checks

        $response = new Response('...');

        // ... no way to set the response right now
    }
}

Option 3 would also be a bad option, since changing the response would be too late: the controller is already executed, so all heavy-weight processes have finished. It does not make sense to completely change the response this late in the process.

More drastic would be to choose option 1: listen to every request and return a custom response when needed. In some cases this could be just fine (though a bit "too much" I think), but in some other cases (the ones I am thinking of) setting the response as a reaction to the kernel.request event is too early. We need everything to be in place, before running our controller-specific checks. This way all the possible execution paths have been narrowed down to just one: we know what action the user wishes to do and we are assured he has the rights to do so, based on authentication and authorization.

The inspiration for the solution I found came from learning about the way the Security Component redirects users. It throws an exception, which will be caught by the default exception handler, which dispatches a kernel.exception event. Each listener is allowed to set a response. For instance a RedirectResponse to redirect the user to the login page. But in fact, exception handlers could return any response they want:

use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;

class ExceptionListener
{
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        // totally custom response
        $response = new Response('...');

        $event->setResponse($response);
    }
}

Since most of the times preventing the user from executing a controller, would be something of an exception (the normal workflow being simply the unhindered execution of the controller) it does make sense to use the "throw exception-set response" workflow in these kind of situations. This means: we should have a controller listener, listening to the kernel.controller event, which does some "just-in-time" checks to verify that the user is allowed to execute the controller. When he is not, the listener should throw a specific type of exception, something like a ControllerNotAvailableException. By extending the exception we could give it all kinds of attributes for later reference, but the exception message itself could also simply describe why the controller was not available for the user.

use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

class ControllerListener
{
    public function onFilterController(FilterControllerEvent $event)
    {
        if (...) {
            // the user is allowed to be execute the controller, so do nothing
            return;
        }

        // the user is not allowed to be here, so
        throw new ControllerNotAvailableException('You are not allowed to execute this controller');
    }
}

On the other end we have the exception listener, which would of course only respond when the exception is of the specified type:

use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;

class ExceptionListener
{
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        $exception = $event->getException();

        if (!($exception instanceof ControllerNotAvailableException)) {
            return;
        }

        $response = new Response($exception->getMessage());

        $event->setResponse($response);
    }
}

Don't forget to hook this all up (see the relevant cookbook article on creating event listeners) using service definitions and kernel.event_listener tags.

Making a subrequest in the exception handler

Though you may prefer to generate the Response object by hand, in most cases you would be happier to work with another controller, some Twig template, etc. This can be done by letting the kernel make a subrequest. You don't have to inject the HttpKernel as a service, since you can retrieve it by calling the getKernel() method on the GetResponseForExceptionEvent object. Then you can make the appropriate subrequest, for instance render a page showing the user some options for upgrading his account from free to premium, etc.:

class ExceptionListener
{
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        // ...

        $kernel = $event->getKernel();

        /** @var \Symfony\Bundle\FrameworkBundle\HttpKernel $kernel */

        // $exception will be available as a controller argument
        $response  = $kernel->forward('MatthiasAccountBundle:Account:upgrade', array(
            'exception' => $exception,
        ));

        $event->setResponse($response); // this will stop event propagation
    }
}

You may also immediately call a controller, instead of letting the kernel nicely handle the forwarding:

use Symfony\Component\HttpKernel\HttpKernelInterface;

class ExceptionListener
{
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        // ...

        $attributes = array(
            '_controller' => 'MatthiasAccountBundle:Account:upgrade',
        );
        $request = $event->getRequest()->duplicate(null, null, $attributes);
        $response = $kernel->handle($request, HttpKernelInterface::SUB_REQUEST, false);
    }
}

Make something nice of it: use annotations

It is very well possible to use annotations to mark controllers that should be checked specifically. It would be nice to have something like:

use Matthias\CreditBundle\Annotation\RequiresCredits;

class SomeController
{
    /**
     * @RequiresCredits(100)
     */
    public function buyShieldAction()
    {
        // only executable when the current user has at least 100 credits
    }
}

As can be read in full detail in the Doctrine Common documentation, creating annotations is very easy. Just create a class, and mark it as an annotation:

namespace Matthias\CreditBundle\Annotation;

use Doctrine\Common\Annotations\Annotation;

class RequiresCredits extends Annotation
{
}

Make sure the controller listener gets the annotation_reader injected. Now you are ready to read the metadata specified by the controller annotation:

use Doctrine\Common\Annotations\Reader;
use Matthias\CreditBundle\Annotation\RequiresCredits;
use Doctrine\Common\Util\ClassUtils;

class ControllerListener
{
    private $annotationReader;

    public function __construct(Reader $annotationReader)
    {
        $this->annotationReader = $annotationReader;
    }

    public function onFilterController(FilterControllerEvent $event)
    {
        $controller = $event->getController();

        list($object, $method) = $controller;

        // the controller could be a proxy, e.g. when using the JMSSecuriyExtraBundle or JMSDiExtraBundle
        $className = ClassUtils::getClass($object);

        $reflectionClass = new \ReflectionClass($className);
        $reflectionMethod = $reflectionClass->getMethod($method);

        $allAnnotations = $this->annotationReader->getMethodAnnotations($reflectionMethod);

        $creditAnnotations = array_filter($allAnnotations, function($annotation) {
            return $annotation instanceof RequiresCredits;
        });

        foreach ($creditAnnotations as $creditAnnotation) {
            $requiredAmountOfCredits = $creditAnnotation->value;

            // ... verify if the user has the required amount

            // throw an exception, catch it using an exception listener (see above)
            // then in the exception listener, create a RedirectResponse or something 
            // and set it on the $event object
        }
    }
}

As you can imagine, using annotations will make your controller code much cleaner, since all pre-execute logic you would normally put inside your controller, can be moved to the docblock before the controller.

PHP Symfony2 Security annotations controller events
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).
Phil

You can actually set the controller to be a closure that returns a redirecResponse object. This gets around the fact that you can't set the response for the controller.

$event->setController(function () use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});

See this example: http://ifdattic.com/symfony...

edit: I see someone pointed this out further down the comments.

Shairyar Baig

Nice detailed article, I was thinking about placing a check with in the controller to check if the user has access to the controller but what you described is also a nice way to check. I am still new to the concept of event listener and may be I did not understand it right so my concern is would this check run every time a request is made and if yes that would mean the check will be running for no reason most of the time and is it then possible to make sure this check runs only when a user tries to access the controller?

Aurelien Fredouelle

Did you measure the performance impact of using reflection on every request to get the annotations associated with the controller?
If this perf hit is significant, have you tried doing this offline and generating a metadata cache for your controllers?

Tjorriemorrie

This is awesome, I hope you can write more similar articles about annotations. What do you do for unit tests?

Matthias Noback

Thanks - maybe I will :)
The tests would consists of unit tests and functional tests using for instance WebTestCase.

bertram

hey ho. indeed nice post. but how do you set a RedirectResponse directly on a FilterControllerEvent? i guess that's not possible.

Miliooo

Actually this works:

$event->setController(
function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});

Matthias Noback

You are definitely right - this is impossible. What I meant is that the behavior described above should be replicated here: onFilterController throws an exception, onKernelException inspects the exception and when applicable sets a Response object using $event->setResponse(). This is the way it can and should be done. I will change the comment in the post.

Patt

Amazing post, I've been looking for this for so long. Many thanks!!
In the last part of your post, I think there is a typo in the method name of the ControllerListener.
It is public function onKernelException(GetResponseForExceptionEvent $event)
at the moment, but shouldn't it be public function onFilterController(FilterControllerEvent $event) instead? Cheers. I really enjoy reading your posts, please keep blogging !! :)

Matthias Noback

That's right! Great catch. Funny that nobody has noticed this problem until now. I will keep blogging, thanks for reading!