События жизненного цикла в Doctrine 2

Общим требованием при создании приложений является понимание того, что в результате какого-либо события в системе активируются определенные действия.

Это могло бы быть так же просто, как обновление поля updated_at каждый раз при изменении записи в базе данных, или отправка электронного письма каждый раз, когда в вашем приложении регистрируется новый пользователь.

Doctrine 2 имеет систему событий, которая позволяет вам реализовывать события в рамках своего проекта.

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

Что такое события?

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

Никто не делает так, чтобы код регистрировал нового пользователя и еще плюс к этому отправлял ему новое электронное письмо.

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

Если вы хотите узнать больше о событиях и о том, как они реализуются с технической точки зрения, почитайте следующие две статьи из этой серии:

  • Using Events in Laravel 4 (Использование событий в Laravel 4)
  • Creating Registration Events in Laravel 4 (Создание событий регистрации в Laravel 4)

Система событий в Doctrine

Doctrine активирует ряд событий, к которым вы можете привязаться или которые вы можете перехватить. Ниже приведены доступные события в Doctrine:

  • preRemove – событие preRemove сущности происходит до выполнения Менеджером сущностей соответствующей операции по удалению этой сущности. Оно не вызывается для оператора DQL DELETE.
  • postRemove – событие postRemove сущности происходит после удаления сущности. Оно активируется после операций базы данных по удалению. Оно не вызывается для оператора DQL DELETE.
  • prePersist – событие prePersist сущности происходит до выполнения Менеджером сущностей соответствующей операции сохранения этой сущности. Следует иметь в виду, что это событие активируется только при первом сохранении объекта (т.е. оно не активируется при последующих обновлениях).
  • postPersist – событие postPersist сущности происходит после сохранения сущности. Оно активируется после операции базы данных по вставке. Первичные значения ключей доступны в событии postPersist.
  • preUpdate – событие preUpdate происходит до операций базы данных по обновлению данных сущности. Оно не вызывается для оператора DQL UPDATE.
  • postUpdate – событие postUpdate происходит после операций базы данных по обновлению данных сущности. Оно не вызывается для оператора DQL UPDATE.
  • postLoad – событие postLoad происходит после загрузки события в текущий Менеджер сущностей из базы данных или после применения к нему операции обновления (refresh operation).
  • loadClassMetadata – событие loadClassMetadata происходит после того, как отображаемые метаданные для класса загружены из источника отображений (аннотации/xml/yaml). Это событие не является обратным вызовом жизненного цикла.
  • preFlush – событие preFlush происходит в самом начале операции сброса (очистки).
  • onFlush – событие onFlush происходит после вычисления наборов изменений всех управляемых сущностей. Это событие не является обратным вызовом жизненного цикла.
  • postFlush – событие postFlush происходит в конце операции сброса (очистки). Это событие не является обратным вызовом жизненного цикла.
  • onClear – событие onClear происходит при вызове операции #clear(), после того как из единицы работы были удалены все ссылки на сущности.

Примечание: Я взял эти описания прямиком из документации Doctrine .

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

Методы обратного вызова жизненного цикла (Lifecycle Callbacks)

Основной и самый простой способ привязки к событиям жизненного цикла это обратные вызовы жизненного цикла. Это методы, реализуемые на вашей сущности.

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

Мы начнем с базового класса-сущности:

use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity
 * @ORM\Table(name="users")
 */
class User {
  /**
   * @ORM\Id
   * @ORM\GeneratedValue
   * @ORM\Column(type="integer")
   */
  private $id;
  /**
   * @ORM\Column(type="string")
   */
  private $name;
}

Сначала нам нужно определить свойство класса $updatedAt :

/**
 * @ORM\Column(name="updated_at", type="datetime")
 * @var \DateTime
 */
private $updatedAt;

Затем мы определяем метод setUpdatedAt() , который задаст свойство для нового экземпляра DateTime:

public function setUpdatedAt()
{
  $this->updatedAt = new DataTime;
}

Теперь, теоретически, мы могли бы вызывать метод setUpdatedAt() в нашей кодовой базе каждый раз, когда хотели бы задать свойство $updateAt.

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

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

Сначала добавьте к сущности следующую аннотацию:

/**
 * @ORM\HasLifecycleCallbacks()
 */

Затем добавьте следующую аннотацию к методу setUpdatedAt():

/**
 * @ORM\PreUpdate
 */
public function setUpdatedAt()
{
  $this->updatedAt = new DataTime;
}

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

@ORM\PreUpdate говорит Doctrine автоматически вызывать этот метод при событии PreUpdate. Каждый раз, когда вы захотите обновить сущность User , столбец таблицы updated_at, соответствующий этой записи, будет задаваться автоматически! Неплохо, правда?

Для часто используемого функционала (например, для поддержания свойств updated_at или created_at) вместо реализации этого кода для каждой из ваших сущностей, лучше создать трейт, который вы просто включаете во все сущности, которые этого требуют.

Есть вариант даже получше: если вы используете пакет Doctrine 2 for Laravel от Митчелла ван Вингаардена, вы можете воспользоваться уже готовым трейтом Timestamps.

Слушатели событий жизненного цикла (Lifecycle Event Listeners)

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

Но вместе с тем, обратные вызовы жизненного цикла могут раздуть ваши классы-сущности за счет дополнительных методов, которым, на самом деле, там не место.

В вышеприведенном примере использование setUpdatedAt() для задания свойства $updateAtобоснованно, поскольку вы лишь автоматически задаете свойство для сущности.

А что если вы захотите отправить электронное письмо? Мы ведь определенно не хотим сочетать нашу сущность со способностью отправлять электронные письма!

Вместо этого мы можем воспользоваться слушателями событий жизненного цикла. Слушатель событий жизненного цикла представляет собой инкапсулированный объект, который перехватывает триггер (активатор) события. Это означает, что вы можете создать эти небольшие фрагменты кода, которые будут выполнять внешние действия, активируемые системой событий Doctrine.

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

В Doctrine предусмотрены два способа реализации этой функции – при помощи Слушателя или Подписчика.

Использование Слушателя выглядит следующим образом:

use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
class SendWelcomeEmailListener
{
  public function postPersist(LifecycleEventArgs $event)
  {
    // Send welcome email
  }
}

Затем вы регистрируете событие при помощи менеджера событий Doctrine:

// $em is an instance of the Event Manager
$em->getEventManager()->addEventListener(
  [Event::postPersist],
  new SendWelcomeEmailListener
);

Или же вы можете создать Подписчика событий:

use Doctrine\ORM\Events;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
class SendWelcomeEmailSubscriber implements EventSubscriber
{
  public function postPersist(LifecycleEventArgs $event)
  {
    // Send welcome email
  }
  public function getSubscribedEvents()
  {
    return [Events::postPersist];
  }
}

Затем вы зарегистрируете Подписчика событий:

// $em is an instance of the Event Manager
$em->getEventManager()->addEventSubscriber(
  new SendWelcomeEmailSubscriber
);

Как видите, Подписчик событий реализует интерфейс EventSubscriber и имеет getSubscribedEvents(), который возвращает массив событий, на которые необходимо подписаться. В этом его отличие от Слушателя событий – там вы подписываетесь на события, когда регистрируете слушателя в менеджере событий.

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

Теперь, когда у нас есть класс для работы с логикой события, мы можем внедрить зависимости в класс для последующей отправки электронного письма:

public function __construct(Mailer $mailer)
{
  $this->mailer = $mailer;
}

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

Затем мы можем написать логику, чтобы отправить электронное письмо в методе postPersist():

public function postPersist(LifecycleEventArgs $event)
{
  $em = $event->getEntityManager();
  $entity = $event->getEntity();
}

Сначала мы можем получить Менеджера сущностей и экземлпяр сущности из LifecycleEventArgs, введенного как аргумент.

Затем мы можем отправить электронное письмо при помощи объекта Mailer, введенного как зависимость:

$mailer->send('welcome', $data, function($msg) use ($entity)
{
  $message->to($entity->email, $entity->name)->subject('Welcome!');
});

Однако Подписчик событий будет активироваться для каждой сущности, сохраняемой Doctrine. Это означает, что если мы бы только захотели отправить электронные письма новым сущностям User, нам нужно бы было включать if () в качестве оператора:

if ( ! $entity instanceOf User ) { return; }

Если вы хотите отправить приветственные письма множеству разных типов сущностей, вы можете указать подсказку для интерфейса:

if ( ! $entity->instanceOf Welcomeable ) { return; }

Если вы используете Слушателя событий или Подписчика для обновления свойств сущности, то это немного усложняет дело:

Для этого нам нужно взять экземпляр UnitOfWork из Doctrine. UnitOfWork это то, как менеджер сущностей обрабатывает изменения, вносимые в ваши сущности.

$uow = $event->getEntityManager()->getUnitOfWork();
$uow->recomputeSingleEntityChangeSet(
  $em->getClassMetaData(get_class($entity)),
  $entity
);

По сути, мы словно сообщаем Doctrine , что экземпляр, который на данный момент хранится в менеджере сущностей, нужно заменить на экземпляр, вводимый с обновленными свойствами.

Unit Of Work представляет собой огромное преимущество Doctrine. Я не буду вдаваться в технические подробности работы Unit Of Work , потому что это не тема этой статьи и, к тому же, выше моего понимания. Я уверен, что мы разберем, как работает Unit Of Work, в одной из будущих статей.

Заключение

Doctrine имеет очень хорошую систему событий, которая позволяет вам с легкостью привязываться и выполнять определенные действия при наступлении определенных событий базы данных.
Однако события Doctrine будут активироваться для всех сущностей в вашем проекте. Это и хорошо, и плохо.

Это хорошо, потому что так легче поддерживать свойства сущностей (такие , как updated_at или created_at) для всех ваших сущностей. Мы можете с легкостью создать трейт, и все ваши сущности автоматически примут этот функционал.

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

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