Ініціалізовані контролери в 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, буду радий конструктивній критиці.

См. також:

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.