承接 besnovatyj/yii2-cms-smart-domain-events 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

besnovatyj/yii2-cms-smart-domain-events

Composer 安装命令:

composer require besnovatyj/yii2-cms-smart-domain-events

包简介

Реализация паттерна Domain Events с поддержкой асинхронности и транзакций для Yii2 CMS

README 文档

README

Архитектурная реализация паттерна Domain Events с поддержкой асинхронности и транзакционной безопасности для Yii2.

Содержание

Обзор архитектуры

┌─────────────────────────────────────────────────────────────────────────────┐
│  1. ENTITY (Aggregate Root)                                                 │
│     Contact::create() → recordEvent(new ContactMessageSent($contact))       │
│     Событие записывается в приватный массив $events                         │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  2. REPOSITORY                                                              │
│     ContactRepository::save($contact)                                       │
│        → $contact->save()                                                   │
│        → $dispatcher->dispatchAll($contact->releaseEvents())                │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  3. DISPATCHER CHAIN (Decorator Pattern)                                    │
│                                                                             │
│     EventDispatcher (interface)                                             │
│            │                                                                │
│            ├── DeferredEventDispatcher (откладывает события)                │
│            │            │                                                   │
│            │            └── AsyncEventDispatcher (помещает в Queue)         │
│            │                        │                                       │
│            │                        └── AsyncEventJob → Queue               │
│            │                                                                │
│            └── SimpleEventDispatcher (вызывает listeners синхронно)         │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  4. QUEUE WORKER (отдельный процесс)                                        │
│     AsyncEventJob::execute()                                                │
│        → AsyncEventJobHandler::handle()                                     │
│        → SimpleEventDispatcher::dispatch(event)                             │
│        → [Listener1::handle(), Listener2::handle(), ...]                    │
└─────────────────────────────────────────────────────────────────────────────┘

Компоненты системы

Интерфейсы и базовые классы

Файл Назначение
AggregateRoot.php Интерфейс для сущностей, генерирующих события
EventTrait.php Трейт с реализацией записи и освобождения событий
EntityEvent.php Базовый класс событий с безопасной сериализацией
TransactionManager.php Обёртка для транзакций с отложенными событиями

Диспетчеры (dispatchers/)

Файл Назначение
EventDispatcher.php Интерфейс диспетчера
SimpleEventDispatcher.php Синхронный вызов слушателей по карте событий
DeferredEventDispatcher.php Декоратор для отложенной отправки (транзакции)
AsyncEventDispatcher.php Помещает события в очередь для асинхронной обработки

Задачи очереди (jobs/)

Файл Назначение
Job.php Базовый класс задач с автоматическим разрешением обработчика
AsyncEventJob.php Задача очереди, содержащая сериализованное событие
AsyncEventJobHandler.php Обработчик задачи, вызывающий SimpleEventDispatcher

Поток данных

Пример: отправка контактной формы

// 1. Контроллер вызывает сервис
$contactService->send($form);

// 2. Сервис создаёт сущность
$contact = Contact::create($name, $email, $phone, $body, $notify_flag = true);
// Внутри: $this->events[] = new ContactMessageSent($contact);

// 3. Репозиторий сохраняет и диспатчит события
$this->repo->save($contact);
// Внутри:
//   $contact->save()                              → запись в БД, получение ID
//   $dispatcher->dispatchAll($contact->releaseEvents()) → отправка событий

// 4. DeferredEventDispatcher
// defer=false → сразу передаёт в AsyncEventDispatcher
// defer=true  → копит в очередь до вызова release()

// 5. AsyncEventDispatcher
$queue->push(new AsyncEventJob($event));
// Событие сериализуется и попадает в yii2-queue

// 6. Queue Worker (./yii queue/listen)
// AsyncEventJob::execute()
//   → AsyncEventJobHandler::handle($job)
//   → SimpleEventDispatcher::dispatch($job->event)
//   → ContactMessageEmailSentListener::handle($event)
//   → ContactMessageTGSentListener::handle($event)

Создание событий

Базовый класс EntityEvent

Для безопасной сериализации событий, содержащих ActiveRecord сущности, используйте базовый класс EntityEvent:

<?php

namespace Besnovatyj\Contact\entities\events;

use Besnovatyj\DomainEvents\EntityEvent;
use Besnovatyj\Contact\entities\Contact;

class ContactMessageSent extends EntityEvent
{
    public function __construct(Contact $contact)
    {
        parent::__construct($contact);
    }

    protected function findEntity(int $id): ?Contact
    {
        return Contact::findOne($id);
    }

    public function getContact(): Contact
    {
        return $this->getEntity();
    }
}

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

  1. При создании события передаётся объект сущности (ID может быть ещё null)
  2. При сериализации (помещении в очередь) сохраняется только $entity->id
  3. При десериализации (в воркере) сущность загружается из БД по ID

Сущность с событиями (Aggregate Root)

<?php

namespace Besnovatyj\Contact\entities;

use Besnovatyj\DomainEvents\AggregateRoot;
use Besnovatyj\DomainEvents\EventTrait;
use Besnovatyj\Contact\entities\events\ContactMessageSent;
use yii\db\ActiveRecord;

class Contact extends ActiveRecord implements AggregateRoot
{
    use EventTrait;

    public static function create($name, $email, $phone, $body, $notify): self
    {
        $contact = new static();
        $contact->name = $name;
        $contact->email = $email;
        $contact->phone = $phone;
        $contact->body = $body;
        $contact->date = time();

        if ($notify) {
            $contact->recordEvent(new ContactMessageSent($contact)); // TODO: Вынос в сервис `$entity->recordEvent()`. Там после этого `$repo->save();`
        }

        return $contact;
    }
}

Создание слушателей

Слушатель — это класс с методом handle(), принимающим событие:

<?php

namespace Besnovatyj\Contact\listeners;

use Besnovatyj\Contact\entities\events\ContactMessageSent;
use yii\mail\MailerInterface;

class ContactMessageEmailSentListener
{
    private MailerInterface $mailer;

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

    public function handle(ContactMessageSent $event): void
    {
        $contact = $event->getContact();

        $this->mailer
            ->compose(['html' => 'contact/body-html'], ['contact' => $contact])
            ->setTo('admin@example.com')
            ->setSubject('Новое сообщение с сайта')
            ->send();
    }
}
<?php

namespace Besnovatyj\Contact\listeners;

use Besnovatyj\Contact\entities\events\ContactMessageSent;

class ContactMessageTGSentListener
{
    public function handle(ContactMessageSent $event): void
    {
        $contact = $event->getContact();
        // Отправка в Telegram...
    }
}

Конфигурация

Регистрация в DI-контейнере (\Besnovatyj\DomainEvents\Bootstrap)

<?php

use Besnovatyj\DomainEvents\dispatchers\AsyncEventDispatcher;
use Besnovatyj\DomainEvents\dispatchers\DeferredEventDispatcher;
use Besnovatyj\DomainEvents\dispatchers\EventDispatcher;
use Besnovatyj\DomainEvents\dispatchers\SimpleEventDispatcher;
use Besnovatyj\DomainEvents\jobs\AsyncEventJobHandler;
use Besnovatyj\Contact\entities\events\ContactMessageSent;
use Besnovatyj\Contact\listeners\ContactMessageEmailSentListener;
use Besnovatyj\Contact\listeners\ContactMessageTGSentListener;
use yii\di\Container;
use yii\di\Instance;
use yii\queue\Queue;

class SetUp implements BootstrapInterface
{
    public function bootstrap($app): void
    {
        $container = \Yii::$container;

        // 1. По умолчанию используется DeferredEventDispatcher
        $container->setSingleton(EventDispatcher::class, DeferredEventDispatcher::class);

        // 2. DeferredEventDispatcher оборачивает AsyncEventDispatcher (Decorator)
        $container->setSingleton(DeferredEventDispatcher::class, function (Container $container) {
            $queue = $container->get(Queue::class);
            return new DeferredEventDispatcher(new AsyncEventDispatcher($queue));
        });

        // 3. Карта событий → слушатели
        $container->setSingleton(SimpleEventDispatcher::class, function (Container $container) {
            return new SimpleEventDispatcher($container, [
                ContactMessageSent::class => [
                    ContactMessageEmailSentListener::class,
                    ContactMessageTGSentListener::class,
                ],
                // Другие события...
            ]);
        });

        // 4. Обработчик задач получает SimpleEventDispatcher
        $container->setSingleton(AsyncEventJobHandler::class, [], [
            Instance::of(SimpleEventDispatcher::class)
        ]);
    }
}

Добавление нового слушателя

Чтобы добавить слушателя к событию, достаточно добавить класс в массив:

ContactMessageSent::class => [
    ContactMessageEmailSentListener::class,
    ContactMessageTGSentListener::class,    // ← добавлено
    ContactMessageSlackListener::class,     // ← ещё один
],

Работа с транзакциями

Проблема

При сохранении нескольких связанных сущностей в одной транзакции события могут быть отправлены до успешного коммита:

// Плохо: событие уходит, а транзакция может откатиться
$order->save();
$dispatcher->dispatchAll($order->releaseEvents()); // Email "Заказ создан" уже ушёл!
$payment->save(); // Ошибка! Rollback! А email уже отправлен...

Решение: TransactionManager

<?php

use Besnovatyj\DomainEvents\TransactionManager;

class OrderService
{
    private TransactionManager $transaction;
    private OrderRepository $orders;
    private PaymentRepository $payments;

    public function __construct(
        TransactionManager $transaction,
        OrderRepository $orders,
        PaymentRepository $payments
    ) {
        $this->transaction = $transaction;
        $this->orders = $orders;
        $this->payments = $payments;
    }

    public function createOrder(OrderForm $form): void
    {
        $this->transaction->wrap(function () use ($form) {
            $order = Order::create($form);
            $this->orders->save($order);      // События откладываются

            $payment = Payment::create($order);
            $this->payments->save($payment);  // События откладываются
        });
        // После успешного commit() все события отправляются
        // При rollback() события очищаются
    }
}

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

public function wrap(callable $function): void
{
    $transaction = Yii::$app->db->beginTransaction();
    try {
        $this->dispatcher->defer();   // Включить режим накопления
        $function();
        $transaction->commit();
        $this->dispatcher->release(); // Отправить все накопленные события
    } catch (Exception $e) {
        $transaction->rollBack();
        $this->dispatcher->clean();   // Очистить события без отправки
        throw $e;
    }
}

Преимущества системы

Преимущество Описание
Domain Events События генерируются в бизнес-логике (Entity), а не в инфраструктуре
Транзакционная безопасность События отправляются только после успешного commit
Асинхронность Тяжёлые операции (email, Telegram) не блокируют HTTP-запрос
Расширяемость Добавить слушателя = одна строка в конфигурации
Decorator Pattern Можно менять поведение (sync/async/deferred) без изменения кода
Безопасная сериализация EntityEvent решает проблему сериализации ActiveRecord
Testability Легко мокать EventDispatcher в тестах
Слабая связанность Сущности не знают о слушателях, слушатели не зависят друг от друга

Запуск воркера очереди

# Непрерывное прослушивание (для production с supervisor)
./yii queue/listen

# Однократная обработка (для cron)
./yii queue/run

# С указанием количества воркеров
./yii queue/listen --isolate=1

Отладка

Проверка, что события создаются

$contact = Contact::create(...);
var_dump($contact->releaseEvents()); // Должен быть массив с событием

Проверка очереди

# Посмотреть статус очереди (зависит от драйвера)
./yii queue/info

Логирование в слушателе

public function handle(ContactMessageSent $event): void
{
    Yii::info('Processing ContactMessageSent for ID: ' . $event->getContact()->id);
    // ...
}

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 1
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-06

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固