Давным давно передо мной встала задача реализовать механизм инициализации контроллеров в 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