Питання на співбесіді: Events
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
3 питання
Події дають слабке зв'язування: одна частина застосунку «оголошує», що щось сталося, інші - реагують, нічого не знаючи одна про одну.
event(new OrderShipped($order)); // диспатч
// слухач
class SendShipmentNotification
{
public function handle(OrderShipped $event): void
{
// ...
}
}
- Слухача, що реалізує
ShouldQueue, обробляють асинхронно в черзі. - У сучасному Laravel слухачі автоматично виявляються за type-hint у методі
handle- ручна реєстрація не обов'язкова.
Приклад: подія UserRegistered → слухачі «надіслати лист», «нарахувати бонус», «оновити статистику».
Подія повідомляє, що щось сталося, не знаючи, хто на це зреагує. Це розвʼязує звʼязок між тим, хто діє, і тим, хто відповідає.
OrderPlaced::dispatch($order);
Слухачі - лист, нарахування бонусів, повідомлення на склад - підписані окремо, і додати ще одного можна, не чіпаючи оформлення замовлення.
Коли події доречні:
- На одну дію треба кілька незалежних реакцій, і їхній перелік з часом росте.
- Реакції належать іншим доменам - розсилка, аналітика, інтеграції.
- Реакція має піти у фонову роботу (
ShouldQueueна слухачі). - Реагувати мусить пакет чи модуль, який про ваш код не знає.
Коли краще виклик напряму:
- Реакція єдина й завжди та сама.
OrderPlacedз одним слухачем - це виклик методу, записаний у два файли. - Порядок і результат важливі: подія не повертає значення й не гарантує послідовності.
- Дія - частина тієї самої операції, яка мусить відкотитися разом із нею.
Головна пастка - невидимість. Через рік ніхто не скаже, що саме відбувається під час оформлення замовлення: у коді видно один рядок, а реально виконуються сім слухачів у різних файлах. Налагодження зводиться до пошуку по всьому проєкту.
Друга пастка - транзакції. Подія, відправлена всередині DB::transaction(), може дійти до черги раніше, ніж транзакція завершиться, - і воркер не знайде запису. Для цього є ShouldDispatchAfterCommit на події або afterCommit на завданні.
Правило: починайте з прямого виклику. Переходьте на подію, коли реакцій стало більше однієї або вони належать іншій частині системи.
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().
Правило: модельні події - для інваріантів самої моделі (слаг, нормалізація поля, службові звʼязки). Бізнес-процес - оформлення замовлення, розсилка, інтеграції - належить явному сервісу чи доменній події, які видно в місці виклику.