События моделей и наблюдатели
Перед сохранением статьи нужно создать адрес из заголовка, после удаления — стереть картинку, при изменении цены — записать её в историю. Вместо того чтобы помнить об этом в каждом контроллере, действие привязывают к событию модели.
События
Модель сообщает о каждом шаге своей жизни:
| Событие | Когда происходит |
|---|---|
creating, created | До и после добавления новой записи |
updating, updated | До и после изменения существующей |
saving, saved | До и после любого сохранения |
deleting, deleted | До и после удаления |
restoring, restored | До и после восстановления мягко удалённой |
События с окончанием -ing происходят до запроса к базе: в них ещё можно изменить данные или отменить действие.
Обработчик в модели
Простые обработчики записывают в методе booted():
use Illuminate\Support\Str;
class Post extends Model
{
protected static function booted(): void
{
static::creating(function (Post $post) {
$post->slug = Str::slug($post->title);
});
static::deleting(function (Post $post) {
$post->comments()->delete();
});
}
}Теперь адрес появляется у статьи всегда — откуда бы её ни создавали.
Отмена действия
Если обработчик события с окончанием -ing вернёт false, действие не выполнится:
static::deleting(function (Post $post) {
if ($post->is_pinned) {
return false;
}
});Наблюдатель
Когда обработчиков много, их выносят в отдельный класс:
php artisan make:observer PostObserver --model=Postnamespace App\Observers;
use App\Models\Post;
use Illuminate\Support\Facades\Storage;
class PostObserver
{
public function updating(Post $post): void
{
if ($post->isDirty('price')) {
$post->priceHistory()->create(['price' => $post->getOriginal('price')]);
}
}
public function deleted(Post $post): void
{
Storage::disk('public')->delete($post->cover);
}
}isDirty() сообщает, изменилось ли поле, getOriginal() возвращает прежнее значение.
Подключение наблюдателя
use App\Observers\PostObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy([PostObserver::class])]
class Post extends Model
{
// …
}Сохранение без событий
Иногда обработчики мешают — например, при загрузке тестовых данных:
$post->saveQuietly();
Post::withoutEvents(function () {
Post::factory()->count(100)->create();
});Частая ошибка
События срабатывают, только когда действие идёт через модель. Массовые запросы минуют их:
Post::where('user_id', 5)->delete(); // событий нет
Post::where('user_id', 5)->get()->each->delete(); // события естьПервый запрос удалит строки одним DELETE, не вызвав deleting — картинки останутся на диске. Если от события зависит целостность данных, удаляйте и изменяйте записи по одной или выполняйте нужную работу рядом с массовым запросом.