Middleware в приложении Symfony

Middleware в приложении Symfony

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

Создадим интерфейс MiddlewareInterface, будем реализовывать его в контроллерах, в которые мы хотим внедрить middleware:

use Symfony\Component\HttpFoundation\Response;
interface MiddlewareInterface
{
	public function before();
    public function after(Response $response);
}

Создадим контроллер, который реализует этот интерфейс:

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class PageController extends AbstractController implements MiddlewareInterface {

   protected string $title = 'Заголовок по умолчанию';

   public function before()
   {
      $this->title = 'Заголовок, установленный в before';
   }

   #[Route('/', name: 'app_demo_page')]
   public function index(): Response
   {
      return $this->render('demo_page/index.html.twig', [
         'controller_name' => $this->title,
      ]);
   }

   public function after(Response $response): Response
   {
      $responseContent = $response->getContent();
      $responseContent = str_replace('color: red;', 'color: blue;', $responseContent);
      $response->setContent($responseContent);
      return $response;
   }
}

В методе before изменим заголовок нашей страницы. В шаблоне он стилизован красным цветом через инлайн стиль:

...
<h1 style="color: red;">{{ controller_name }}</h1>
...

Теперь, если мы перейдем на наш сайт, вы увидим на странице "Заголовок по умолчанию", выделенный красным цветом.

Добавим слушатель событий для запуска наших before/after. Before мы должны запустить после создания контроллера, но перед вызовом экшена. В Symfony для этого подходят события kernel.controller и kernel.controller_arguments. Первое создается после создания контроллера, второе, непосредственно перед вызовом экшена, после разрешения аргументов.

Для метода after удобно использовать событие kernel.response. Оно происходит сразу после выполнения экшена.

Создадим слушатель этих событий:

use App\Controller\MiddlewareInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;

class Middleware
{
	public null | MiddlewareInterface $currentController = null;
	
	public function onKernelController(ControllerEvent $event)
	{
		$controller = $event->getController();
		if (is_array($controller))
		{
			$controller = $controller[0];
		}
		
		if ($controller instanceof MiddlewareInterface)
		{
			$this->currentController = $controller;
			$this->currentController->before();
		}
	}
	
	public function onKernelResponse(ResponseEvent $event)
	{
		if ($this->currentController instanceof MiddlewareInterface)
			$event->setResponse($this->currentController->after($event->getResponse()));
	}
}

В функции onKernelController (слушатель события kerenl.controller) мы проверим, реализует ли наш контроллер интерфейс MiddlewareInterface, сохраним ссылку на наш контроллер и вызовем метод before(). Во второй функции onKernelResponse (слушатель события kerenl.response) мы вызовем наш метод after, передав туда объект Response, который мы получили из экшена. After вернет обработанный Response, и его мы установим в качестве ответа.

Для того, чтобы наш слушатель подхватился, нам нужно объявить его, как сервис. Для этого добавим в config/servises.yaml соответствующий код:

services:
    ...
    App\EventListener\Middleware:
        tags:
            - { name: kernel.event_listener, event: kernel.controller }
            - { name: kernel.event_listener, event: kernel.response }
   ...

Запустив теперь наше приложение, мы увидим, что заголовок "Заголовок, установленный в before" стал синим (был изменен в after).

Приведенные выше примеры скорее для наглядности, и не несут большой смысловой нагрузки. На самом деле в before/after можно размещать какую угодно логику, проверять уровень доступа пользователя, работать с конфигурацией, внедрять дополнительные модули на страницу, реализовывать защиту парсеров данных и многое-многое другое.

Приведенный способ - не единственный. Наряду со солушателем событий можно использовать подсписчиков, но об этом как-нибудь в другой раз.