Hoang ND

web developer

Using custom annotation in Symfony controller

Consider the scenario when the website have to check if the user is allowed (including roles, subscription type and account balance check, etc) in several controller's actions, the user will be redirected to a page if they are not allowed. You could do something like this:

function someAction(){
    $isAllowed = $this->get('user_service')->check($user);
    if(!$isAllowed){
        $this->redirect($this->generateUrl('some_where'));
    }
}

Repeating these 3 lines of code is not a good idea. It would be much more elegant to use annotation for this:

/**
* @AllowedUser()
*/
function someAction(){    
}

If the controller consist of several actions that all required the same user check. Annotation can be even more useful:

/**
 * @AllowedUser()
 */
class UserController extends Controller
{
    // both actions required user check
    public function someAction(){        
    }
    
    public function someOtherAction(){
    }
}

Custom annotation like this can be created easily with the help from Doctrine's AnnotationReader and an Event Listener for controller. Now lets create a custom annotation.

Create custom annotation

1. Create annotation class is pretty straightforward:

#Acme\DemoBundle\Annotation\Demo
 
/**
 * @Annotation
 */
class Demo {
    public $message;
} 

More about annotation class here:  http://docs.doctrine-project.org/projects/doctrine-common/en/latest/reference/annotations.html#annotation-classes

2. Controller using custom annotation for demo:

#Acme\DemoBundle\Controller\AnnotationDemoController
 
/**
 * @Demo(message="This is class annotation")
 */
class AnnotationDemoController extends Controller
{
    /**
     * @Route("/annotation", name="annotation_demo")
     * @Demo(message="This is method annotation")
     */
    public function indexAction()
    {
        return new Response('This message won\'t appear');
    }

This controller is using Demo annotations that contain different messages for class and indexAction. The event listener will override the response of indexAction and print out 2 annotation messages.

3. Create event listener

<service id="acme.demo.annotation_listener" class="Acme\DemoBundle\EventListener\AnnotationListener">
    <argument type="service" id="annotation_reader" />
    <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController"/>
</service>
# Acme\DemoBundle\EventListener\AnnotationListener
class AnnotationListener
{
    protected $reader;
 
    public function __construct($reader)
    {
        /** @var AnnotationReader $reader */
        $this->reader = $reader;
    }
 
    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 happen.
         * If it is a class, it comes in array format
         *
         */
        if (!is_array($controller)) {
            return;
        }
        
        list($controllerObject, $methodName) = $controller;
 
        $demoAnnotation = 'Acme\DemoBundle\Annotation\Demo';
 
        $message = '';
 
        // Get class annotation
        // Using ClassUtils::getClass in case the controller is an proxy
        $classAnnotation = $this->reader->getClassAnnotation(
            new \ReflectionClass(ClassUtils::getClass($controllerObject)), $demoAnnotation
        );
        if($classAnnotation)
            $message .=  $classAnnotation->message.'<br>';
 
        // Get method annotation
        $controllerReflectionObject = new \ReflectionObject($controllerObject);
        $reflectionMethod = $controllerReflectionObject->getMethod($methodName);
        $methodAnnotation = $this->reader->getMethodAnnotation($reflectionMethod,$demoAnnotation);
        if($methodAnnotation)
            $message .=  $methodAnnotation->message.'<br>';
 
        // Override the response only if the annotation is used for method or class
        if($classAnnotation || $methodAnnotation)
            $event->setController(
                function() use ($message) {
                    return new Response($message);
                }
            );
 
    }
}

That's it. The output will be:

This is class annotation
This is method annotation

Grap the code for this demo here: https://github.com/hoangnd25/AnnotationDemo