Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

События моделей и наблюдатели

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

События

Модель сообщает о каждом шаге своей жизни:

СобытиеКогда происходит
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=Post
namespace 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 — картинки останутся на диске. Если от события зависит целостность данных, удаляйте и изменяйте записи по одной или выполняйте нужную работу рядом с массовым запросом.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.