Работаем с событиями Laravel-моделей

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

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

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

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 и заканчивается именем трейта. Вот пример:

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. Наблюдатели моделей — это основанный на классах подход к реагированию на события модели, где методы соответствуют конкретным запускаемым событиям.

namespace App\Observers;

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

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

Этот класс нужно будет зарегистрировать, либо в сервис-провайдере, либо в самой модели (рекомендую). Регистрация этого наблюдателя в модели обеспечивает его видимость и понимание, что изменяется поведение модели. В сервис-провайдере же проблема заключается в том, что нельзя сразу понять, что в нём находится что-то влияющее на модель.

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

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Models\Events\SetModelUuid;

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

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

namespace App\Models\Events;

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

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

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

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

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

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,
                      ]);
    }
}

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

Автор: JustSteveKing
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.