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

в 9:17, , рубрики: php, symfony, аннотации

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

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

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

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

<?php
namespace MyBundleController;

interface InitializableControllerInterface
{
}

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

<?php
namespace MyBundleEventListener;

use MyBundleControllerInitializableControllerInterface;
use SymfonyComponentHttpKernelEventFilterControllerEvent;

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="MyBundleEventListenerKernelControllerListener">
            <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
        </service>
    </services>
</container>

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

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

<?php
namespace MyBundleAnnotation;

/**
 * @Annotation
 * @Target({"METHOD"})
 *
 * С помощью @Annotation мы указываем, что данный класс должен использоваться как аннотация,
 * а @Target({"METHOD"}) - что данную аннотацию можно применять только к методам класса.
 */
class Init
{
    // Параметры аннотации:

    /**
     * @var array
     *
     * Массив передаваемых аргументов
     */
    public $args = [];

    /**
     * @var int
     *
     * Приоритет вызова (чем больше, тем раньше будет вызов)
     */
    public $priority = 0;
}

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

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

<?php
namespace MyBundleController;

use MyBundleAnnotationInit;
use MyBundleControllerInitializableControllerInterface;
use SymfonyBundleFrameworkBundleControllerController;

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

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

<?php
namespace MyBundleEventListener;

use DoctrineCommonAnnotationsReader;
use MyBundleAnnotationInit;
use MyBundleControllerInitializableControllerInterface;
use SymfonyComponentHttpKernelEventFilterControllerEvent;

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="MyBundleEventListenerKernelControllerListener">
            <argument type="service" id="annotation_reader" /> <!-- Передача ридера аннотаций в конструктор -->
            <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
        </service>
    </services>
</container>

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

См. также:

Автор: const_seoff

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js