Pierre de LESPINAY Pierre de LESPINAY - 6 months ago 14
PHP Question

Redirect if requirement is not satisfied

I have many controller actions that require some context in the session so they can be executed.

/**
* @Route("/some/route", name="some_route")
*/
public function oneOfMyAction(Request $request)
{
if (!$request->getSession()->get('some_required_variable')) {
$this->redirectToRoute('some_other_route');
}

return $this->render('AppBundle::protected-content.html.twig');
}


Is there a way to factorize this requirement in an annotation or something so I can easily use it in my controllers ?

/**
* @Route("/some/route", name="some_route")
* @SomeRequiredVariable()
*/
public function oneOfMyAction(Request $request)
{
return $this->render('AppBundle::protected-content.html.twig');
}


If I can implement a
SomeRequiredVariable
class, how would I do it ?

Or would there be another know way ?

Answer

I've always found the documentation on custom annotations a bit lacking. But hopefully the following will help you.

Prerequisites:

  • You need a class annotated as @Annotation. This will act as a container for all the parameters used with the annotion.
  • You need an appropriate event listener which finds and reads the annotation and acts accordinglingly.

The annotation class

For this to work, you need to define all properties, which can be used for your custom annotation (here route, route_params, required), as class properties.

<?php
// src/AppBundle/Annotation/RedirectOnMissing.php
namespace AppBundle\Annotation;

use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * @Annotation
 */
class RedirectOnMissing
{
    /**
     * @var string
     */
    private $route;

    /**
     * @var array
     */
    private $route_params = [];

    /**
     * @var array
     */
    private $required = [];

    /**
     * @param array $options
     */
    public function __construct(array $options)
    {
        $options = $this->configureOptions(new OptionsResolver())->resolve($options);

        $this->route = $options['route'];
        $this->route_params = $options['route_params'];
        $this->required = $options['required'];
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @return OptionsResolver
     */
    private function configureOptions(OptionsResolver $resolver)
    {
        return $resolver
            ->setRequired(['route', 'required'])
            ->setDefaults([
                'route_params' => []
            ])
            ->setAllowedTypes('route', 'string')
            ->setAllowedTypes('required', 'array')
            ->setAllowedTypes('route_params', 'array')
        ;
    }

    /**
     * Get `route`
     *
     * @return string
     */
    public function getRoute()
    {
        return $this->route;
    }

    /**
     * Get `route_params`
     *
     * @return array
     */
    public function getRouteParams()
    {
        return $this->route_params;
    }

    /**
     * Get `required`
     *
     * @return string[]
     */
    public function getRequired()
    {
        return $this->required;
    }
}

The listener

You need to listen to the kernel.controller event, for otherwise you won't have access to the active controller.

<?php
// src/AppBundle/EventListener/FilterControllerListener.php
namespace AppBundle\EventListener;

use AppBundle\Annotation\RedirectOnMissing;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpFoundation as Http;

class FilterControllerListener
{
    /**
     * @var Reader
     */
    private $reader;

    /**
     * @var UrlGeneratorInterface
     */
    private $urlGenerator;

    /**
     * @var SessionInterface
     */
    private $session;

    /**
     * @param Reader                $reader
     * @param UrlGeneratorInterface $urlGenerator
     * @param SessionInterface      $session
     */
    public function __construct(Reader $reader, UrlGeneratorInterface $urlGenerator, SessionInterface $session)
    {
        $this->reader = $reader;
        $this->urlGenerator = $urlGenerator;
        $this->session = $session;
    }

    /**
     * @param FilterControllerEvent $event
     */
    public function onKernelController(FilterControllerEvent $event)
    {
        $controller = $event->getController();
        if (!is_array($controller)) {
            return;
        }

        /** @var $methodAnnotation RedirectOnMissing */
        $methodAnnotation = $this->reader->getMethodAnnotation(
            new \ReflectionMethod($controller[0], $controller[1]),
            RedirectOnMissing::class
        );

        if (null !== $methodAnnotation) {
            foreach ($methodAnnotation->getRequired() as $key) {
                if (!$this->session->has($key)) {
                    $event->setController(function () use($methodAnnotation) {
                        return new Http\RedirectResponse($this->urlGenerator->generate($methodAnnotation->getRoute(), $methodAnnotation->getRouteParams()));
                    });
                    break;
                }
            }
        }
    }
}

The configuration

// src/AppBundle/Resources/config/services.yml

services:
    // ...

    app.event_listner.controller_listener:
        class: AppBundle\EventListener\FilterControllerListener
        arguments:
            - "@annotation_reader"
            - "@router"
            - "@session"
        tags:
            - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }

Usage

<?php

/**
 * @Cfg\Route("/test")
 *
 * @RedirectOnMissing(route="home", required={"foo", "bar"})
 */
public function testAction()
{
    return new Http\Response('no redirect');
}

Note: When dealing with custom annotations, it might be that you need to clear the cache often to see changes.