Laravel: События Модели

Источник: «Working with Laravel Model Events»
При работе с Моделями Eloquent обычно используют события, отправляемые в течении жизненного цикла Моделей. Есть несколько разных способов сделать это, и в этой статье я расскажу о них и объясню преимущества и недостатки каждого из них.

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

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

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class Office extends Model
{
public static function boot(): void
{
static::creating(fn (Model $model) =>
$model->uuid = Str::uuid(),
);
}
}

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

Это прекрасно подводит нас ко второму подходу использующему трейт. В Laravel модели могут наследовать трейты и автоматически загружать их, если вы создадите метод в трейте начинающийся с boot и заканчивающийся именем трейта. Вот пример:

declare(strict_types=1);

namespace App\Models\Concerns;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

trait HasUuid
{
public static function bootHasUuid(): void
{
static::creating(fn (Model $model) =>
$model->uuid = Str::uuid(),
);
}
}

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

Это приводит нас к следующему варианту, Model Observers — Наблюдатели Модели. Наблюдатели Модели — основанный на классах подход к реагированию на события модели, где методы соответствуют конкретным запускаемым событиям.

declare(strict_types=1);

namespace App\Observers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class OfficeObserver
{
public function creating(Model $model): void
{
$model->uuid = Str::uuid();
}
}

Этот класс нужно будет где-то зарегистрировать, в Сервис Провайдере или в самой модели (где я и рекомендую это сделать). Регистрация этого наблюдателя в модели обеспечивает видимость на уровне модели побочных эффектов изменяющих поведение Eloquent. Проблема с сокрытием наблюдателя в Сервис Провайдере заключается в том, что если не все знаю, что он там — об этом сложно узнать. На мой взгляд, этот подход является фантастическим при правильном использовании.

Ещё один способ решить эту проблему — воспользоваться свойством $dispatchesEvents в самой Модели Eloquent. Это свойство каждой Модели Eloquent позволяет перечислить события, которые вы хотите прослушивать, и класс вызываемый для этих событий.

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class Office extends Model
{
protected $dispatchesEvents = [
'creating' => SetModelUuid::class,
];
}

SetModelUuid будет создан в течении жизненного цикла модели Eloquent, и это ваш шанс добавить в модель поведение и свойство.

declare(strict_types=1);

namespace App\Models\Events;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class SetModelUuid
{
public function __construct(Model $model)
{
$model->uuid = Str::uuid();
}
}

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

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

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

declare(strict_types=1);

namespace App\Models\Pipelines;

use App\Models\Office

class OfficeCreatingPipeline
{
public function __construct(Office $model)
{
app(Pipeline::class)
->send($model)
->through([
ApplyUuidProperty::class,
TapCreatedBy::class,
]);
}
}

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

А как вы обрабатываете события моделей в своих Laravel проектах?

Дополнительные материалы

Предыдущая Статья

Laravel: Data Transfer Objects — Зачем и Как

Следующая Статья

Laravel: Сокращение дублирования кода