Middle: питання на співбесіді з теми «Події»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Події дають слабке зв'язування: одна частина застосунку «оголошує», що щось сталося, інші - реагують, нічого не знаючи одна про одну.
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 на завданні.
Правило: починайте з прямого виклику. Переходьте на подію, коли реакцій стало більше однієї або вони належать іншій частині системи.
Бо подію диспетчеризували всередині транзакції, а воркер узяв завдання раніше, ніж транзакція закомітилася.
DB::transaction(function () use ($data) {
$order = Order::create($data);
OrderPlaced::dispatch($order); // слухач у черзі вже може стартувати
$order->items()->createMany($data['items']);
});
Воркер читає замовлення з бази - а там його ще немає, або немає позицій. Якщо транзакція відкотиться, слухач узагалі обробить те, чого не існує.
Рішення:
ShouldQueueAfterCommitу слухачі - стати в чергу лише після коміту;ShouldDispatchAfterCommitу події - диспетчеризувати саму подію після коміту (не дістануть і синхронні слухачі);after_commit => trueу конфігурації підключення черги - для всіх завдань;- для спостерігачів -
ShouldHandleEventsAfterCommit.
Якщо транзакції немає, усе це спрацьовує одразу, тож поведінка поза транзакцією не змінюється.
Те саме стосується листів і сповіщень у черзі - у них є afterCommit().
Два різні питання - «чи відправлено подію» і «чи правильно працює слухач» - тестують окремо.
Чи відправлено:
Event::fake([OrderShipped::class]);
$this->post("/orders/{$order->id}/ship")->assertOk();
Event::assertDispatched(OrderShipped::class, fn ($e) => $e->order->is($order));
Передавати список подій важливо: Event::fake() без аргументів підмінить усі події, зокрема події моделей. Тоді зламається все, що тримається на creating - генерація UUID, slug, спостерігачі - і тест впаде з дивною помилкою.
Інші інструменти:
Event::fakeExcept([...])- підмінити все, крім переліченого;Event::fakeFor(fn () => ...)- лише для частини тесту;Event::assertListening(OrderShipped::class, SendShipmentNotification::class)- що слухач підписаний.
Чи правильно працює слухач - окремий тест, де слухач викликають напряму:
(new SendShipmentNotification)->handle(new OrderShipped($order));
Notification::assertSentTo($order->customer, ShipmentSent::class);
Варто мати й кілька тестів без підмін, де ланцюжок «дія → подія → слухач» проходить цілком: саме там ламаються інтеграції, які фейки приховують.