Table of Contents
Що це таке
Laravel автоматично запускає події коли з моделлю щось відбувається: створення, оновлення, видалення. Ти можеш “підписатися” на ці події і виконати додаткову логіку.
Які події існують
| Подія | Коли спрацьовує |
|---|---|
creating | Перед створенням (INSERT ще не виконаний) |
created | Після створення (INSERT виконаний, є id) |
updating | Перед оновленням (UPDATE ще не виконаний) |
updated | Після оновлення (UPDATE виконаний) |
saving | Перед створенням АБО оновленням |
saved | Після створення АБО оновлення |
deleting | Перед видаленням |
deleted | Після видалення |
restoring | Перед відновленням (SoftDeletes) |
restored | Після відновлення (SoftDeletes) |
trashed | Після мʼякого видалення (SoftDeletes) |
replicating | При клонуванні моделі |
retrieved | Після отримання з БД |
Порядок виконання
При створенні (Model::create())
saving → creating → INSERT в БД → created → saved
При оновленні ($model->save(), $model->update())
saving → updating → UPDATE в БД → updated → saved
При видаленні ($model->delete())
deleting → DELETE з БД → deleted
Спосіб 1 — booted() в моделі
Найпростіший спосіб, логіка прямо в моделі:
class Post extends Model
{
protected static function booted(): void
{
// Перед створенням — згенерувати slug
static::creating(function (Post $post) {
$post->slug = Str::slug($post->title);
});
// Після створення — відправити сповіщення
static::created(function (Post $post) {
$post->author->notify(new PostPublished($post));
});
// Перед оновленням — зберегти стару версію
static::updating(function (Post $post) {
if ($post->isDirty('title')) {
$post->slug = Str::slug($post->title);
}
});
// Перед видаленням — видалити повʼязані дані
static::deleting(function (Post $post) {
$post->comments()->delete();
$post->tags()->detach();
if ($post->image) {
Storage::delete($post->image);
}
});
}
}
Спосіб 2 — Observer (окремий клас)
Коли логіки багато — виносимо в окремий клас:
php artisan make:observer PostObserver --model=Post
// app/Observers/PostObserver.php
class PostObserver
{
public function creating(Post $post): void
{
$post->slug = Str::slug($post->title);
$post->user_id = auth()->id();
$post->reading_time = $this->calculateReadingTime($post->body);
}
public function created(Post $post): void
{
// Відправити сповіщення підписникам
$post->author->followers->each(function ($follower) use ($post) {
$follower->notify(new NewPostFromAuthor($post));
});
// Оновити лічильник постів автора
$post->author->increment('posts_count');
}
public function updating(Post $post): void
{
if ($post->isDirty('title')) {
$post->slug = Str::slug($post->title);
}
if ($post->isDirty('body')) {
$post->reading_time = $this->calculateReadingTime($post->body);
}
}
public function deleting(Post $post): void
{
$post->comments()->delete();
$post->tags()->detach();
if ($post->image) {
Storage::delete($post->image);
}
}
public function deleted(Post $post): void
{
$post->author->decrement('posts_count');
Cache::forget("post:{$post->id}");
Cache::forget('popular_posts');
}
public function restored(Post $post): void
{
$post->author->increment('posts_count');
}
private function calculateReadingTime(string $body): int
{
$words = str_word_count(strip_tags($body));
return max(1, (int) ceil($words / 200));
}
}
Реєстрація Observer
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Post::observe(PostObserver::class);
}
Або через атрибут (Laravel 10+):
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy(PostObserver::class)]
class Post extends Model
{
// ...
}
Спосіб 3 — Dispatchable Events (для складної логіки)
Модель запускає подію, слухачі реагують:
// Модель
class Order extends Model
{
protected $dispatchesEvents = [
'created' => OrderCreated::class,
'updated' => OrderUpdated::class,
];
}
// Подія
class OrderCreated
{
public function __construct(
public Order $order
) {}
}
// Слухач 1 — відправити email
class SendOrderConfirmation
{
public function handle(OrderCreated $event): void
{
Mail::to($event->order->user)->send(new OrderConfirmationMail($event->order));
}
}
// Слухач 2 — оновити аналітику
class UpdateAnalytics
{
public function handle(OrderCreated $event): void
{
Analytics::track('order_created', [
'total' => $event->order->total,
]);
}
}
// Слухач 3 — зменшити залишки
class DecrementStock
{
public function handle(OrderCreated $event): void
{
foreach ($event->order->items as $item) {
$item->product->decrement('stock', $item->quantity);
}
}
}
// Реєстрація в EventServiceProvider
protected $listen = [
OrderCreated::class => [
SendOrderConfirmation::class,
UpdateAnalytics::class,
DecrementStock::class,
],
];
Корисні методи всередині подій
isDirty() — чи змінилось поле
static::updating(function (User $user) {
// Перевірити чи email змінився
if ($user->isDirty('email')) {
$user->email_verified_at = null;
$user->sendEmailVerificationNotification();
}
// Чи змінилось хоча б одне з полів
if ($user->isDirty(['name', 'avatar'])) {
Cache::forget("user_profile:{$user->id}");
}
});
getOriginal() — старе значення
static::updated(function (Order $order) {
$oldStatus = $order->getOriginal('status');
$newStatus = $order->status;
if ($oldStatus !== 'shipped' && $newStatus === 'shipped') {
$order->user->notify(new OrderShipped($order));
}
});
wasChanged() — чи було змінено (після save)
static::saved(function (Product $product) {
if ($product->wasChanged('price')) {
// Сповістити підписників про зміну ціни
$product->watchers->each(function ($user) use ($product) {
$user->notify(new PriceChanged($product));
});
}
});
Практичні приклади
Автоматичний slug
protected static function booted(): void
{
static::creating(function (Post $post) {
$post->slug = Str::slug($post->title);
// Перевірити унікальність
$count = Post::where('slug', $post->slug)->count();
if ($count > 0) {
$post->slug .= '-' . ($count + 1);
}
});
}
Автоматичний UUID
protected static function booted(): void
{
static::creating(function (Order $order) {
$order->uuid = Str::uuid()->toString();
});
}
Очищення кешу
protected static function booted(): void
{
static::saved(function (Product $product) {
Cache::forget("product:{$product->id}");
Cache::forget('popular_products');
Cache::forget("category:{$product->category_id}:products");
});
static::deleted(function (Product $product) {
Cache::forget("product:{$product->id}");
Cache::forget('popular_products');
});
}
Логування змін (Audit Log)
class AuditObserver
{
public function updated(Model $model): void
{
foreach ($model->getChanges() as $field => $newValue) {
if ($field === 'updated_at') continue;
AuditLog::create([
'model_type' => get_class($model),
'model_id' => $model->id,
'field' => $field,
'old_value' => $model->getOriginal($field),
'new_value' => $newValue,
'user_id' => auth()->id(),
]);
}
}
}
// Підключити до будь-якої моделі
User::observe(AuditObserver::class);
Order::observe(AuditObserver::class);
Product::observe(AuditObserver::class);
Лічильник коментарів
class CommentObserver
{
public function created(Comment $comment): void
{
$comment->post->increment('comments_count');
}
public function deleted(Comment $comment): void
{
$comment->post->decrement('comments_count');
}
public function restored(Comment $comment): void
{
$comment->post->increment('comments_count');
}
}
Скасування операції
Повернути false з creating/updating/deleting — операція скасується:
protected static function booted(): void
{
static::deleting(function (User $user) {
if ($user->is_admin) {
return false; // не дозволяємо видалити адміна
}
});
static::creating(function (Order $order) {
if ($order->total <= 0) {
return false; // не створюємо замовлення з нульовою сумою
}
});
}
Коли події НЕ спрацьовують
Масові операції обходять модель — події не викликаються:
// ✗ Події НЕ спрацюють — це SQL напряму
Post::where('user_id', 5)->update(['status' => 'archived']);
Post::where('created_at', '<', now()->subYear())->delete();
// ✓ Події спрацюють — через модель
Post::where('user_id', 5)->get()->each(function ($post) {
$post->update(['status' => 'archived']);
});
$post = Post::find(1);
$post->update(['status' => 'archived']); // updating + updated спрацюють
$post->delete(); // deleting + deleted спрацюють
Порівняння підходів
| booted() | Observer | Dispatchable Events | |
|---|---|---|---|
| Де код | В моделі | В окремому класі | В слухачах (Listeners) |
| Коли використовувати | 1-2 прості події | Багато подій, складна логіка | Кілька незалежних реакцій на одну подію |
| Складність | Проста | Середня | Найскладніша |
| Тестування | Разом з моделлю | Окремо від моделі | Кожен listener окремо |
| Приклад | Генерація slug | CRUD-логіка: slug + кеш + сповіщення + лічильники | Замовлення створено → email + аналітика + склад + SMS |
Коли що обирати
booted() — проста логіка, одна-дві події: генерація slug, UUID, дефолтні значення.
Observer — багато логіки при CRUD: slug + кеш + сповіщення + лічильники + видалення файлів.
Dispatchable Events — одна подія має кілька незалежних наслідків, які можуть виконуватись асинхронно: замовлення створено → email + аналітика + склад + SMS.