Laravel: Model Events
Glossary overview

Laravel: Model Events

Що це таке

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()ObserverDispatchable Events
Де кодВ моделіВ окремому класіВ слухачах (Listeners)
Коли використовувати1-2 прості подіїБагато подій, складна логікаКілька незалежних реакцій на одну подію
СкладністьПростаСередняНайскладніша
ТестуванняРазом з моделлюОкремо від моделіКожен listener окремо
ПрикладГенерація slugCRUD-логіка: slug + кеш + сповіщення + лічильникиЗамовлення створено → email + аналітика + склад + SMS

Коли що обирати

booted() — проста логіка, одна-дві події: генерація slug, UUID, дефолтні значення.

Observer — багато логіки при CRUD: slug + кеш + сповіщення + лічильники + видалення файлів.

Dispatchable Events — одна подія має кілька незалежних наслідків, які можуть виконуватись асинхронно: замовлення створено → email + аналітика + склад + SMS.