Senior: питання на співбесіді з теми «Події»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
Eloquent сам розсилає події життєвого циклу: creating, created, updating, updated, saving, saved, deleting, deleted, restored, replicating.
Зручно для дрібниць, які мають виконуватися завжди:
class Post extends Model
{
protected static function booted(): void
{
static::creating(function (Post $post) {
$post->slug ??= Str::slug($post->title);
});
}
}
Для повного набору краще окремий observer - інакше booted() розростається:
#[ObservedBy(PostObserver::class)]
class Post extends Model
{
}
Чим небезпечні:
- Масові операції їх не викликають.
Post::where(...)->update([...]),->delete()таinsert()йдуть повз Eloquent, тож жоден слухач не спрацює. Дані тихо розходяться з тим, що гарантував observer. - Логіка стає невидимою.
$post->save()у контролері виглядає простим рядком, а насправді тягне за собою запис в інші таблиці й виклик API. - Тести сповільнюються. Кожна фабрика в кожному тесті тягне весь ланцюжок подій, включно з тими, що ходять у мережу.
- Помилка в слухачі ламає збереження. Виняток у
creatingскасовує операцію - зокрема там, де про це ніхто не думав. - Рекурсія. Слухач
saved, який оновлює ту саму модель, викликає сам себе; рятуєsaveQuietly().
Правило: модельні події - для інваріантів самої моделі (слаг, нормалізація поля, службові звʼязки). Бізнес-процес - оформлення замовлення, розсилка, інтеграції - належить явному сервісу чи доменній події, які видно в місці виклику.
Типовий випадок: імпорт оновлює товар 20 разів за хвилину, і кожне ProductUpdated перебудовує пошуковий індекс.
Дебаунс (Laravel 13) - обробити лише останню подію за проміжок:
#[DebounceFor(30)]
class UpdateProductSearchIndex implements ShouldQueue
{
public function debounceId(ProductUpdated $event): string
{
return (string) $event->product->getKey();
}
public function handle(ProductUpdated $event): void { /* ... */ }
}
Кожна нова подія з тим самим debounceId скидає таймер; виконується лише остання, через 30 секунд тиші.
Унікальний слухач - не ставити в чергу дубль, поки попередній не оброблений:
class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
public function uniqueId(LicenseSaved $event): string
{
return (string) $event->license->id;
}
}
Різниця: дебаунс чекає, доки події вщухнуть, і обробляє останню. Унікальність обробляє першу, а повтори відкидає, поки вона в черзі.
Інші підходи:
- подію на рівні пачки:
ProductsImportedодин раз замість тисячіProductUpdated; Event::defer()чиModel::withoutEvents()навколо масових операцій.
Обидва механізми потребують драйвера кешу з атомарними блокуваннями.
Event::defer() відкладає всі події, що виникли в замиканні, до його завершення.
Event::defer(function () {
$user = User::create(['name' => 'Victoria']);
$user->posts()->create(['title' => 'Перший допис']);
});
Без defer слухач created користувача спрацює одразу після User::create() - ще до створення поста. Якщо слухач розраховує на пов'язані записи (надіслати вітання з посиланням на перший допис), він їх не знайде.
Особливості:
- якщо в замиканні виняток, відкладені події не диспетчеризуються зовсім;
- другим аргументом можна відкласти лише певні події:
['eloquent.created: '.User::class].
Чим відрізняється від after-commit:
Event::defer() |
ShouldDispatchAfterCommit |
|
|---|---|---|
| Чекає | кінця замикання | коміту транзакції бази |
| Транзакція потрібна | ні | так (без неї - одразу) |
| Мета | цілісність набору записів у коді | щоб слухачі не бачили незакомічених даних |
Їх можна поєднувати: defer усередині DB::transaction() збирає події, а after-commit гарантує, що слухачі в черзі побачать закомічені дані.