PHP
May 17

Событие при сохранении/обновлении базы данных через Доктрину

Как-то раз озадачился каким образом организовать клиент-серверное взаимодействие, таким образом, чтобы фронтенд на Vue обращался к данным, которые он сохранил в LocalStorage, а когда на бекенде Symfony + Doctrine происходило обновление или добавление данных в базу, отправлялся бы сигнал на фронтенд, для того чтобы послать запрос на бекенд и обновить данные в LocalStorage.

Решение было таким:

// src/EventListener/UpdatedAtListener.php

namespace App\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;

class UpdatedAtListener
{
    public function preUpdate(PreUpdateEventArgs $event)
    {
        $entity = $event->getEntity();
        $entity->setLastUpdated(new \DateTime());
    }
}

Класс UpdatedAtListener служит для автоматического обновления даты и времени последнего изменения сущности в базе данных. Он "подключается" к событиям Doctrine и реагирует на них. В данном случае, он реагирует на событие preUpdate, которое происходит непосредственно перед тем, как Doctrine выполнит обновление сущности в базе данных.

Когда происходит сохранение или обновляете сущности с помощью Doctrine, обычно вызывается метод EntityManager::persist() и EntityManager::flush(). Между этими вызовами Doctrine отслеживает изменения в сущностях и, когда вызывается flush(), оно генерирует и выполняет соответствующие SQL-запросы для базы данных.

В момент, когда Doctrine готовится к выполнению обновления (но до того, как фактический SQL-запрос будет сформирован и отправлен базе данных), оно генерирует событие preUpdate. Слушатель UpdatedAtListener "подписан" на это событие и его метод preUpdate автоматически вызывается Doctrine. Внутри этого метода код получает сущность, которая находится в процессе обновления, и устанавливает для неё новое значение даты и времени в специальном поле lastUpdated (или аналогичном по названию). Это гарантирует, что каждый раз, когда сущность изменяется и сохраняется в базе данных, её поле lastUpdated будет обновлено на текущие дату и время.

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

Чтобы использовать этот слушатель, необходимо также зарегистрировать его в конфигурации Doctrine, обычно это делается в файле конфигурации сервисов вашего приложения (например, в services.yaml для Symfony), где надо указать, что этот класс должен быть вызван при событии preUpdate. Doctrine автоматически вызовет этот слушатель всякий раз, когда будет обновлять сущности, и нет нужды в ручном вызове этого класса из кода сущностей.

Как это работает

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

  1. Создание слушателя событий: Вы создаете класс, который будет слушать события Doctrine. В вашем случае это UpdatedAtListener.
  2. Подписка на события: Вы подписываете свой слушатель на конкретное событие, в данном случае preUpdate. Это событие возникает перед тем, как Doctrine обновит сущность в базе данных.
  3. Регистрация слушателя: Вы регистрируете свой слушатель событий в конфигурации Doctrine или Symfony, чтобы Doctrine знал о его существовании и мог вызвать его при наступлении события.
  4. Обработка события: Когда событие происходит, Doctrine автоматически вызывает ваш слушатель, и вы можете выполнить необходимую логику.

Вот как можно подключить UpdatedAtListener к событию preUpdate:

1. Создайть класс UpdatedAtListener:

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;

class UpdatedAtListener
{
    public function preUpdate(PreUpdateEventArgs $event)
    {
        $entity = $event->getObject();
        $entity->setLastUpdated(new \DateTime());
    }
}

2. Зарегистрировать UpdatedAtListener как сервис: В Symfony надо зарегистрировать свой слушатель как сервис и подписать его на событие preUpdate. Это делается в файле конфигурации сервисов, обычно services.yaml:

services:
    App\EventListener\UpdatedAtListener:
        tags:
            - { name: doctrine.orm.entity_listener, event: preUpdate }

3. Убедиться, что сущности имеют setLastUpdated(): Все сущности, для которых хотим автоматически обновлять lastUpdated, должны иметь метод setLastUpdated().

Здесь речь идет о:

#[Column(name: 'last_updated', type: 'datetime')]
private \DateTime $lastUpdated;

Если у сущности есть поле даты recordDate, которое фактически выполняет функцию lastUpdated. Не нужно создавать отдельный setLastUpdated(), если хотим обновлять recordDate при каждом изменении сущности.

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

  1. Переименовать поле:
    Изменить название поля recordDate на lastUpdated, чтобы отразить его истинное назначение.
  2. Обновлять lastUpdated при каждом изменении:
    Нужно будет добавить логику обновления lastUpdated в каждый сеттер сущности.
#[Entity(repositoryClass: FeedbackRecordRepository::class)]
#[Table(name: 'feedback_records')]
class FeedbackRecord
{
    // ... другие поля ...

    #[Column(name: 'last_updated', type: 'datetime')]
    private \DateTime $lastUpdated;

    public function __construct() {
        $this->lastUpdated = new \DateTime();
    }

    // ... другие геттеры ...

    public function setProduct(Product $product): void {
        $this->product = $product;
        $this->updateLastUpdated(); // Обновляем lastUpdated
    }

    public function setFeedbackCount(int $feedbackCount): void {
        $this->feedbackCount = $feedbackCount;
        $this->updateLastUpdated(); // Обновляем lastUpdated
    }

    // ... другие сеттеры ...

    public function getLastUpdated(): \DateTime {
        return $this->lastUpdated;
    }

    private function updateLastUpdated(): void {
        $this->lastUpdated = new \DateTime();
    }
}

Теперь при каждом изменении product или feedbackCount будет обновляться и поле lastUpdated. Можно применить эту логику и к другим сеттерам, если необходимо.

4. Протестировать: После настройки, когда обновляем сущность и вызываем $entityManager->flush(), Doctrine автоматически вызовет UpdatedAtListener перед обновлением сущности в базе данных, и lastUpdated будет установлен на текущую дату и время.

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

Если хотим увидеть, когда UpdatedAtListener вызывается, можем добавить логирование или использовать dd($entity) внутри метода preUpdate для отладки:

use Psr\Log\LoggerInterface;

class UpdatedAtListener
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function preUpdate(PreUpdateEventArgs $event)
    {
        $entity = $event->getObject();
        $this->logger->info('PreUpdate event fired for entity: ' . get_class($entity));
        // Или для отладки: dd($entity);

        $entity->setLastUpdated(new \DateTime());
    }
}

И не забыть подключить LoggerInterface к слушателю в конфигурации сервисов.

После этих шагов, при каждом обновлении сущности, вы должны видеть в логах, что UpdatedAtListener был вызван, или dd($entity) остановит выполнение и покажет сущность, для которой вызывается preUpdate.