Laravel Event Projector и концепция порождения событий

Фрек ван дер Хертен (Freek Van der Herten) и команда Spatie долго трудились над Laravel Event Projector, пакетом, позволяющим применять концепцию порождения событий (Event Sourcing) во фреймворке Laravel. И вот наконец доступна первая стабильная версия (v1.0.0)!

Вы можете установить Event Projector в свой проект при помощи composer и благодаря автоматическому обнаружению пакетов в Laravel приступить к работе сразу же после публикации миграций пакета и конфигурирования!

composer require spatie/laravel-event-projector:^1.0.0

Event Projector требует наличия PHP 7.2, поэтому ваше приложение должно поддерживать последнюю версию PHP, хотя самому фреймворку Laravel достаточно PHP версии не ниже 7.1.3.

Что такое «порождение событий»

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

Вместо того, чтобы хранить текущее состояние данных в предметной области (domain), используйте для записи всей последовательности действий, производимых над этими данными, хранилище, работающее в режиме append-only — только добавление данных. Хранилище выступает в роли системы записей (system of record) и может использоваться для материализации объектов предметной области. Это позволяет упростить задачи в сложных предметных областях, поскольку отпадает необходимость в синхронизации модели данных и предметной области, что приводит к улучшению масштабируемости, повышению производительности и оперативности работы. Подобный подход обеспечивает согласованность транзакционных данных, а также позволяет вести полноценные журнал изменений и историю, благодаря чему становится возможным осуществление компенсирующих действий.

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

Мне также нравится объяснение концепции порождения событий, которое дано во введении к документации по Event Projector:

Порождение событий является по отношению к данным тем же самым, чем Git является по отношению к коду. Большинство приложений хранят в базе данных только свое текущее состояние. Значительный объем полезной информации теряется — вы не знаете, каким образом приложение достигло этого состояния.
Концепция порождения событий — это попытка решить эту проблему путем сохранения всех событий, которые происходят в вашем приложении. Состояние приложения формируется в результате прослушивания этих событий.
Для облегчения понимания рассмотрим небольшой пример из жизни. Представьте, что вы банк. У ваших клиентов имеются счета. Хранить только информацию об остатке на счете недостаточно. Необходимо также запоминать все транзакции. При использовании шаблона порождения событий остаток на счете будет не просто отдельным полем в базе данных, а значением, рассчитанным на основании сохраненной информации о транзакциях. Это только одно из множества преимуществ, которое предлагает концепция порождения событий.
Данный пакет призван познакомить пользователей фреймворка Laravel с концепцией порождения событий и облегчить ее использование на практике.

Похоже, что Event Projector помогает упростить работу с шаблоном порождения событий. Документация также помогла мне разобраться, каким образом следует использовать этот подход в рамках концепций фреймворка Laravel, с которым я уже знаком.

Основы порождения событий в Laravel

Чтобы разобраться, как работает порождение событий в пакете Event Projector и какие компоненты задействованы в этом процессе, вам следует ознакомиться с разделом Создание первого проектора.

На высоком уровне (простите, если не совсем корректен в своей оценке, поскольку порождение событий — это новая для меня концепция) порождение событий с помощью Event Projector включает в себя:

  • Модели Eloquent
  • События, реализующие интерфейс ShouldBeStored
  • Классы проектора

Пример класса модели со статическим методом createWithAttributes, приводимый в документации:

namespace App;
use App\Events\AccountCreated;
use App\Events\AccountDeleted;
use App\Events\MoneyAdded;
use App\Events\MoneySubtracted;
use Illuminate\Database\Eloquent\Model;
use Ramsey\Uuid\Uuid;
class Account extends Model
{
    protected $guarded = [];
    protected $casts = [
        'broke_mail_send' => 'bool',
    ];
    public static function createWithAttributes(array $attributes): Account
    {
        /*
         * Давайте сгенерируем универсальный уникальный идентификатор (uuid).
         */
        $attributes['uuid'] = (string) Uuid::uuid4();
        /*
         * Счет будет создан в этом событии на основе сгенерированного uuid.
         */
        event(new AccountCreated($attributes));
        /*
         * Обращение к созданному счету будет происходить по этому uuid.
         */
        return static::uuid($attributes['uuid']);
    }
    public function addMoney(int $amount)
    {
        event(new MoneyAdded($this->uuid, $amount));
    }
    public function subtractMoney(int $amount)
    {
        event(new MoneySubtracted($this->uuid, $amount));
    }
    public function delete()
    {
        event(new AccountDeleted($this->uuid));
    }
    /*
     * Вспомогательный метод для быстрого обращения к счету по uuid.
     */
    public static function uuid(string $uuid): ?Account
    {
        return static::where('uuid', $uuid)->first();
    }
}

Здесь необходимо обратить внимание на события AccountCreated, MoneyAdded, MoneySubtracted и AccountDeleted, инициируемые соответственно для открытия счета, добавления денег на счет, снятия денег со счета и удаления счета.

Ниже приведен пример события MoneyAdded (добавление денег на счет). Интерфейс ShouldBeStored указывает пакету Event Projector, что это событие должно быть сохранено:

namespace App\Events;
use Spatie\EventProjector\ShouldBeStored;
class MoneyAdded implements ShouldBeStored
{
    /** @var string */
    public $accountUuid;
    /** @var int */
    public $amount;
    public function __construct(string $accountUuid, int $amount)
    {
        $this->accountUuid = $accountUuid;
        $this->amount = $amount;
    }
}

И наконец, частичный пример обработчика событий внутри класса проектора для пополнения средств на счете (мой любимый тип банковского события):

public function onMoneyAdded(MoneyAdded $event)
{
    $account = Account::uuid($event->accountUuid);
    $account->balance += $event->amount;
    $account->save();
}

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

Account::createWithAttributes(['name' => 'Luke']);
Account::createWithAttributes(['name' => 'Leia']);
$account = Account::where(['name' => 'Luke'])->first();
$anotherAccount = Account::where(['name' => 'Leia'])->first();
$account->addMoney(1000);
$anotherAccount->addMoney(500);
$account->subtractMoney(50);

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

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

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

Сама идея создания новых проекторов по завершении событий представляется весьма перспективной. Получается, что вам не обязательно получать все «правильные» данные заранее, а можно приходить к результату итеративно.

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

Хотите узнать больше?

Для начала я рекомендую ознакомиться с документацией по Event Projector и убедиться, что у вас установлена свежая версия PHP 7.2. Команда Spatie также выложила пример приложения на Laravel, демонстрирующий возможности пакета Event Projector.

Вы можете загрузить код, отметить репозиторий звездочкой, а также внести свой вклад в развитие проекта Event Projector на GitHub. На момент написания статьи над проектом Event Projector работают уже 15 участников, усилиями которых и стал возможен стабильный релиз 1.0. Отличная работа, Spatie! Уверен, что Event Projector станет еще одним великолепным пакетом, который будет по достоинству оценен сообществом Laravel.