Фрек ван дер Хертен (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.