Digitalagentur für Business Software aus Kiel

Controller Annotationen leicht gemacht

Veröffentlicht von Patrick Rathje am 09.10.2015

Patrick Rathje

Annotationen können zur Lesbarkeit und guter Strukturierung eines Projektes beitragen. Ganz nebenbei lassen sie sich einfach erstellen.

Durch Annotationen können Logiken und Konfigurationen direkt im Quellcode angegeben werden.
Es hat sich in unserer Agentur gezeigt, dass dies vor allem in Controllern die Arbeit erleichtert.
Ein gutes Beispiel hierfür ist die "@Security"-Annotation, die Symfony2 von Haus aus mitbringt. 

Wir wollen nun beispielhaft eine eigene Annotation implementieren: Dafür nehmen wir den DefaultController als Grundlage. Dieser begrüßt den Nutzer ausgehend von dem Namens-Parameter in der Route. Unsere Annotation soll den Zugriff nur einem bestimmten Namen gewähren.

Annotations-Objekt

Symfony2 nutzt intern den "Doctrine AnnotationsReader". Dieser wandelt die Textbasierten Annotationen in PHP-Objekte um, zumindest, wenn wir die dazugehörige Klasse erstellt haben:

Dabei ist gibt es für die Klasse (außer eine Annotation vor der Klassendefinition) keine weiteren Voraussetzungen.

Jedoch implementieren wir das ConfigurationInterface des SensioFrameWorkExtraBundle. Dies wird sich gleich als äußerst nützlich erweisen.

<?php
namespace BrauneDigital\DemoBundle\Annotation;

//src/BrauneDigital/DemoBundle/Annotation/Example.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface;

/**
 * @Annotation
 */
class Example implements ConfigurationInterface
{
    protected $name;

    public function __construct($options)
    {
        if (isset($options['value'])) {
            $this->name = $options['value'];
        }
    }

    //FrameWorkExtraBundle: Parametername im späteren Request
    public function getAliasName()
    {
        return 'example';
    }

    //FrameWorkExtraBundle: sind mehrere Annotation des gleichen Typs erlaubt?
    public function allowArray()
    {
        return false;
    }

    /**
     * @return mixed
     */
    public function getName()
    {
        return $this->name;
    }
}
src/BrauneDigital/DemoBundle/Annotation/Example.php

Hinweis: Die Annotation "@Annotation" signalisiert dem AnnotationReader, dass es sich um eine Annotations-Klasse handelt. Der Klassenname entspricht dem Annotationsnamen.

Für den Moment beschränken wir uns auf eine einzelne Annotation pro Controller-Action. Durch die Funktion allowArray kann jedoch bestimmt werden, dass man auch mehrere Annotationen eines Typs erlaubt.

Annotationen im Controller

Nun wollen wir unsere neu erstellte Annotation gleich verwenden. Dafür übergeben wir der Annotation den Namen, dem wir den Zugriff erlauben wollen:

<?php
namespace BrauneDigital\DemoBundle\Controller;
//src/BrauneDigital/DemoBundle/Controller/DefaultController.php


use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use BrauneDigital\DemoBundle\Annotation\Example;

use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends Controller
{
    /**
     * @Example("test_name")
     * @Route("/hello/{name}")
     */
    public function indexAction($name)
    {
        return $this->render('BrauneDigitalDemoBundle:Default:index.html.twig', array('name' => $name));
    }
}
src/BrauneDigital/DemoBundle/Controller/DefaultController.php

Annotations-Logik

Zu diesem Zeitpunkt wird die Annotation bereits geladen.

Dies haben wir dem ConfigurationInterface zu verdanken, da das SensioFrameworkExtraBundle automatisch alle Annotationen lädt, die eben dieses Interface implementieren.

Unsere Logik fehlt jedoch immer noch:

Da wir eine Controller Annotation erstellen, liegt es nahe, dass wir das Event kernel.controller verwenden.

Aber wie bekommt man nun Zugriff auf die Annotation?

Das SensioFrameworkExtraBundle setzt beim Laden der Annotationen im request Attribute. Der Name des Attributs hängt von der Funktion getAliasName ab.

In unserem Fall gibt die Funktion "example" zurück. Der resultierende Name ist dann "_example".

Das SensioFrameworkExtraBundle nutzt einen Subscriber, um die Annotationen bei einem kernel.controller Event zu laden.
Wir gehen genauso vor und erstellen einen einfachen AnnotationSubscriber:

<?php

namespace BrauneDigital\DemoBundle\EventListener;
//src/BrauneDigital/DemoBundle/EventListener/AnnotationSubscriber.php

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;


class AnnotationSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(KernelEvents::CONTROLLER => 'onKernelController');
    }

    public function onKernelController(FilterControllerEvent $event)
    {
        $request = $event->getRequest();

        //hole die exampleAnnotation aus dem Request (injected durch Sensio\Bundle\FrameworkExtraBundle\EventListener\ControllerListener)
        if (!$exampleAnnotation = $request->attributes->get('_example')) {
            return;
        }

        //prüfe ob der Parameter "name" identisch ist mit dem erlaubten Namen
        if($exampleAnnotation->getName() && $request->get('name') != $exampleAnnotation->getName()) {
            throw new AccessDeniedException('The annotation Example denied access');
        }
    }
}
src/BrauneDigital/DemoBundle/EventListener/AnnotationSubscriber.php

Nun müssen wir unseren AnnotationSubscriber nur noch als Service eintragen:

services:
    bd_demo.event_listener.annotation_subscriber:
        class: BrauneDigital\DemoBundle\EventListener\AnnotationSubscriber
        tags:
            - {name: kernel.event_subscriber}
src/BrauneDigital/DemoBundle/Resources/config/services.yml

Falls das SensioFrameworkBundle erst später geladen wird, müssen wir die Priorität des Subscribers unter die vom ConfigurationSubscriber setzen:

services:
    bd_demo.event_listener.annotation_subscriber:
        class: BrauneDigital\DemoBundle\EventListener\AnnotationSubscriber
        tags:
            - {name: kernel.event_subscriber, priority: -1 }
src/BrauneDigital/DemoBundle/Resources/config/services.yml

Fertig

Das wars, nun sollte nur noch die Route /hello/test_name funktionieren, bei anderen Namen wirft unser AnnotationSubscriber eine AccessDeniedException.

© 2020, Braune Digital GmbH, Niemannsweg 69, 24105 Kiel, +49 (0) 431 55 68 63 59