При работе с 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.