Чат Telegram
Группа ВКонтакте
Знакомство с Event и EventSubsriber в Symfony

Знакомство с Event и EventSubsriber в Symfony

В этом уроке мы закончим регистрацию в нашем приложении, познакомившись и применив на практике такой компонент Symfony как события. Если простым языком, события - это любые действия, которые происходят в системе вашего приложения по "вине" пользователя (чаще всего) или чего-то другого. Этими действиями могут быть регистрация, авторизация, новый комментарий, новый лайк, создание поста и проч.

Конечно, по большей части, можно написать какой-нибудь приватный метод или сервис, который применить в конкретном экшене, реагируя на заранее известное вам событие (та же регистрация пользователя), но компонент Symfony, называемый EventDispatcher, позволяет реагировать на абсолютно любые события, выполнять их в определенном порядке (по приоритету) и даже несколько сразу.

События работают следующим образом: у нас есть Event - в нашем случае это зарегистрированный пользователь; и есть EventSubscriber, который подписывается на это событие и отрабатывает, когда оно произошло. Таким образом, когда у нас появится новый зарегистрированный пользователь, подписчик на данное событие отправит письмо на почту для подтверждения регистрации. Давайте приступим!

События, или Events, практически всегда выглядят крайне просто. Например, вот так:

<?php

declare(strict_types=1);

namespace App\Event;

use App\Entity\User;
use Symfony\Component\EventDispatcher\Event;

class RegisteredUserEvent extends Event
{
    public const NAME = 'user.register';

    /**
     * @var User
     */
    private $registeredUser;

    /**
     * @param User $registeredUser
     */
    public function __construct(User $registeredUser)
    {
        $this->registeredUser = $registeredUser;
    }

    /**
     * @return User
     */
    public function getRegisteredUser(): User
    {
        return $this->registeredUser;
    }
}

Создайте папку Event и положите этот класс туда. Итак, разберем его подробнее:

  1. Событие должно наследоваться от класса Event;
  2. У события есть имя в константе NAME: обычно оно составляется по имени класса (сущность User) и конкретного события (register, deleted, loggedIn, etc);
  3. Событие принимает в конструктор имя сущности и возвращает ее через геттер.

Как видите, ничего сложного. Теперь давайте напишем подписчик на событие. Для этого создайте папку EventSubscriber и создайте там класс UserSubscriber. Данный класс подписывается на любые события, совершенные пользователем, но пока он отслеживает только его регистрацию. Вы не поверите, но выглядит он даже еще проще (пока, по крайней мере), чем само событие:

<?php

namespace App\EventSubscriber;

use App\Event\RegisteredUserEvent;
use App\Service\Mailer;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Twig_Error_Loader;
use Twig_Error_Runtime;
use Twig_Error_Syntax;

class UserSubscriber implements EventSubscriberInterface
{
    /**
     * @var Mailer
     */
    private $mailer;

    /**
     * @param Mailer $mailer
     */
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    /**
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return [
            RegisteredUserEvent::NAME => 'onUserRegister'
        ];
    }

    /**
     * @param RegisteredUserEvent $registeredUserEvent
     * @throws Twig_Error_Loader
     * @throws Twig_Error_Runtime
     * @throws Twig_Error_Syntax
     */
    public function onUserRegister(RegisteredUserEvent $registeredUserEvent)
    {
        $this->mailer->sendConfirmationMessage($registeredUserEvent->getRegisteredUser());
    }
}

Подписчик должен имплементить интерфейс EventSubscriberInterface, который по контракту требует реализовать метод getSubscribedEvents, в комментарии к которому (если у вас IDE PhpStorm, вы можете легко найти этот интерфейс и сами убедиться) можно увидеть следующее:

  array('eventName' => 'methodName')
  array('eventName' => array('methodName', $priority))
  array('eventName' => array(array('methodName1', $priority), array('methodName2')))

Так создатели Symfony предлагают реализовать этот метод: он должен возвращать простой массив, ключами которого являются имена событий, а значения - реализованные в этом подписчике методы. Обычно они должны формироваться как "on" + имя события. Именем у нас является "user.register", а значит, метод будет называться onUserRegister. Напомню код еще раз:

public static function getSubscribedEvents()
    {
        return [
            RegisteredUserEvent::NAME => 'onUserRegister'
        ];
    }

А метод, собственно, всего лишь вызывает наш сервис по отправке сообщений:

public function onUserRegister(RegisteredUserEvent $registeredUserEvent)
    {
        $this->mailer->sendConfirmationMessage($registeredUserEvent->getRegisteredUser());
    }

Скажите, просто? Осталось совсем немного: запустить событие в контроллере. На прошлом уроке мы написали следующий экшен:

   /**
     * @Route("/register", name="register")
     */
    public function register(
        UserPasswordEncoderInterface $passwordEncoder,
        Request $request,
        CodeGenerator $codeGenerator,
        Mailer $mailer
    ) {
        $user = new User();
        $form = $this->createForm(
            UserType::class,
            $user
        );

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $user = $form->getData();

            $password = $passwordEncoder->encodePassword(
                $user,
                $user->getPlainPassword()
            );
            $user->setPassword($password);
            $user->setConfirmationCode($codeGenerator->getConfirmationCode());

            $em = $this->getDoctrine()->getManager();

            $em->persist($user);
            $em->flush();

            $mailer->sendConfirmationMessage($user);
        }

          return $this->render('security/register.html.twig', [
            'form' => $form->createView()
            ]);
    }

Нам надо его переписать, используя события. Для начала удаляем сервис Mailer из аргументов метода: он нам больше не нужен. Также удаляем строку

$mailer->sendConfirmationMessage($user);

Вместо Mailer инжектим следующее EventDispatcherInterface $eventDispatcher и добавляем следующие две строки в конец:

$userRegisteredEvent = new RegisteredUserEvent($user);
$eventDispatcher->dispatch(RegisteredUserEvent::NAME, $userRegisteredEvent);

Создаем эвент, который принимает пользователя, потом вызываем метод dispatch встроенного класса EventDispatcher, принимающий имя события и само событие. Вот так выглядит весь код:

   /**
     * @Route("/register", name="register")
     */
    public function register(
        UserPasswordEncoderInterface $passwordEncoder,
        Request $request,
        CodeGenerator $codeGenerator,
        EventDispatcherInterface $eventDispatcher
    ) {
        $user = new User();
        $form = $this->createForm(
            UserType::class,
            $user
        );

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $user = $form->getData();

            $password = $passwordEncoder->encodePassword(
                $user,
                $user->getPlainPassword()
            );
            $user->setPassword($password);
            $user->setConfirmationCode($codeGenerator->getConfirmationCode());

            $em = $this->getDoctrine()->getManager();

            $em->persist($user);
            $em->flush();

            $userRegisteredEvent = new RegisteredUserEvent($user);
            $eventDispatcher->dispatch(RegisteredUserEvent::NAME, $userRegisteredEvent);
        }

          return $this->render('security/register.html.twig', [
            'form' => $form->createView()
            ]);
    }

Некоторые правки с прошлого урока

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

<!doctype html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Подтверждение регистрации!</title>
</head>
<body>
<div class="container">
    <p>{{ user.username }}, добро пожаловать!</p>
    <p>Чтобы завершить регистрацию, подтвердите <a href="{{ path('email_confirmation', {'code': user.confirmationCode }) }}">электронный адрес</a></p>
</div>
</body>
</html>

Строку со ссылкой на подтверждение надо заменить на следующую:

<p>Чтобы завершить регистрацию, подтвердите <a href="{{ app.request.schemeAndHttpHost }}{{ path('email_confirmation', {'code': user.confirmationCode }) }}">электронный адрес</a></p>

app.request.schemeAndHttpHost - в этой переменной хранится полное имя вашего сайта (http://127.0.0.1:8000).

Итого

Теперь, когда все готово, зарегистрируйтесь в вашем приложении. В tool-bar вы увидите письмо, вы можете нажать на него и выбрать Rendered Content. Собственно, вот и все. Мы познакомились с новым компонентом фреймворка, который может быть необычайно полезным и обязательно будет! В следующем уроке мы сделаем формы входы и выхода.

loader
Логические задачи с собеседований