Инициализируемые контроллеры в Symfony и работа с аннотациями

от автора

Давным давно передо мной встала задача реализовать механизм инициализации контроллеров в Symfony, т.е. выполнение неких дефолтных действий перед каждым вызовом экшна контроллера. Первое, что пришло на ум, — это добавить EventListener для события kernel.controller, в котором будет вызываться метод контроллера initialize, если он есть. Данным способом я пользуюсь уже на протяжении нескольких лет.

Буквально на днях я задумался: а что если необходимо перед экшном выполнить разные методы для разных контроллеров, несколько методов подряд, а некоторые из них даже несколько раз и с разными параметрами? В данной статье я хочу рассказать, как я решил эту проблему с помощью аннотаций. Думаю, эта статья будет полезна в том числе и тем, кто никогда не работал с аннотациями.

В первую очередь, я наглядно покажу, как реализовать механизм инициализации контроллеров.

Сначала создадим интерфейс, который поможет отлавливать те контроллеры, которым необходима инициализация:

<?php namespace MyBundle\Controller;  interface InitializableControllerInterface { } 

Затем создадим EventListener для события kernel.controller, который и будет осуществлять инициализацию:

<?php namespace MyBundle\EventListener;  use MyBundle\Controller\InitializableControllerInterface; use Symfony\Component\HttpKernel\Event\FilterControllerEvent;  class KernelControllerListener {     // Метод, вызываемый при событии kernel.controller     public function onKernelController(FilterControllerEvent $event)     {         $controller = $event->getController();          // Если контроллер реализует интерфейс InitializableControllerInterface         if (is_array($controller) && $controller[0] instanseof InitializableControllerInterface) {             // Вызов методов инициализации контроллера         }     } } 

И добавим для него конфигурацию сервиса (services.xml):

<?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">     <services>         <service id="my_bundle.kernel_controller_listener" class="MyBundle\EventListener\KernelControllerListener">             <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />         </service>     </services> </container> 

В принципе, этого уже достаточно, но мы же хотим сделать инициализацию контроллеров более гибкой, поэтому переходим к аннотациям.

На самом деле, с аннотациями работать очень просто, особенно если используешь ридер аннотаций от Doctrine. Начнем с того, что создадим класс-аннотацию, который следует применять к инициализирующим методам контроллера:

<?php namespace MyBundle\Annotation;  /**  * @Annotation  * @Target({"METHOD"})  *  * С помощью @Annotation мы указываем, что данный класс должен использоваться как аннотация,  * а @Target({"METHOD"}) - что данную аннотацию можно применять только к методам класса.  */ class Init {     // Параметры аннотации:      /**      * @var array      *      * Массив передаваемых аргументов      */     public $args = [];      /**      * @var int      *      * Приоритет вызова (чем больше, тем раньше будет вызов)      */     public $priority = 0; } 

Рекомендую указывать тип параметров аннотации для осуществления контроля Doctrine над типами входящих данных.

Теперь аннотацию можно использовать в контроллере:

<?php namespace MyBundle\Controller;  use MyBundle\Annotation\Init; use MyBundle\Controller\InitializableControllerInterface; use Symfony\Bundle\FrameworkBundle\Controller\Controller;  class MyController extends Controller implements InitializableControllerInterface {     /**      * @Init(args = {"test"}, priority = 200)      *      * Данный метод будет вызван (initialize("test");) перед вызовом экшна контроллера      */     public function initialize($value)     {         // ... какой-то код ...     } } 

Осталось только добавить обработку аннотаций в наш KernelControllerListener:

<?php namespace MyBundle\EventListener;  use Doctrine\Common\Annotations\Reader; use MyBundle\Annotation\Init; use MyBundle\Controller\InitializableControllerInterface; use Symfony\Component\HttpKernel\Event\FilterControllerEvent;  class KernelControllerListener {     protected $annotationReader;      // Передаем в конструктор ридер аннотаций     public function __construct(Reader $annotationReader)     {         $this->annotationReader = $annotationReader;     }      public function onKernelController(FilterControllerEvent $event)     {         $controller = $event->getController();          if (is_array($controller) && $controller[0] instanceof InitializableControllerInterface) {             // Получаем информацию о классе             $reflector = new \ReflectionClass($controller[0]);             // Получаем список всех публичных методов класса             $methods = $reflector->getMethods(\ReflectionMethod::IS_PUBLIC);             $initMethods = [];              // Сохраняем только те методы, у которых есть аннотация @Init             foreach ($methods as $method) {                 // Получаем все аннотации метода                 $annotations = $this->annotationReader->getMethodAnnotations($method);                  foreach ($annotations as $annotation) {                     // Если аннотация - наша, то сохраняем метод в отдельный список с параметрами приоритета и аргументов                     if ($annotation instanceof Init) {                         $initMethods[] = [                             'method' => $method,                             'args' => $annotation->args,                             'priority' => $annotation->priority                         ];                     }                 }             }              // Сортируем список сохраненных методов по порядку убывания приоритета             usort($initMethods, function($a, $b) { return $b['priority'] - $a['priority']; });              foreach ($initMethods as $initMethod) {                 $method = $initMethod['method'];                  // Осуществляем вызов метода с учетом, есть ли для него аргументы или нет                 if (count($initMethod['args'])) {                     $method->invokeArgs($controller[0], $initMethod['args']);                 } else {                     $method->invoke($controller[0]);                 }             }         }     } } 

И дополним конфигурацию сервиса:

<?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">     <services>         <service id="my_bundle.kernel_controller_listener" class="MyBundle\EventListener\KernelControllerListener">             <argument type="service" id="annotation_reader" /> <!-- Передача ридера аннотаций в конструктор -->             <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />         </service>     </services> </container> 

Вот и все. Весь мой код можно посмотреть на GitHub, буду рад объективной критике.

См. также:

ссылка на оригинал статьи https://habrahabr.ru/post/277465/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *