Ланцюжок обовʼязків і конвеєр виглядають однаково: кожен обробник отримує запит і посилання на наступного. Відрізняє їх, скільки обробників мають попрацювати. У Chain of Responsibility запит шукає того, хто за нього візьметься: правило повернення коштів для дрібних замовлень, правило для старих замовлень, ручний розгляд в кінці. Ланка, яка взялася, повертає результат і далі нікого не кличе. У Pipeline працюють усі кроки: імпорт фіду проходить очищення, дедуплікацію й теги, і кожен крок отримує результат попереднього.
Middleware у Laravel - це саме Pipeline. HTTP-ядро проганяє запит через Illuminate\Routing\Pipeline, нащадка загального класу, який ще й перетворює виняток зі шару на відповідь через обробник винятків. Кожен шар отримує запит і $next: код до виклику бачить запит, код після бачить відповідь, яка повертається назовні тим самим шляхом. Найчастіше шар передає запит далі, як крок конвеєра. Але auth, що повернув 401, або перевірка підпису URL, що повернула 403, поводяться як ланка ланцюжка: вони відповіли самі, і внутрішні шари з контролером цього запиту не побачать.
Свій конвеєр збирають через Pipeline::send()->through([...])->then() або thenReturn(). Кроки Laravel резолвить з контейнера, тож у них можна інжектувати репозиторії, а порядок видно в одному масиві. Пакетна перевірка дублікатів одним запитом на весь фід замість запиту на кожну вакансію зазвичай і є головною вигодою від такого розкладу. У Symfony окремого класу конвеєра немає: кроки позначають тегом, а контейнер збирає їх у #[AutowireIterator] з порядком за priority. У Messenger своя версія того самого: стек middleware на шині, де шар викликає $stack->next()->handle().
Межі видно з обох боків. Два правила, які не множитимуться, читаються легше як match, ніж як базовий клас з двома нащадками. Кроки, які пишуть у базу, роблять конвеєр крихким: падіння посередині лишає частину змін. Найпростіше, щоб кроки лише перетворювали дані, а запис відбувався один раз після всіх кроків; якщо так не виходить, прогін загортають у транзакцію, у Laravel для цього є withinTransaction().
use Illuminate\Support\Facades\Pipeline;
// Chain of Responsibility: запит бере перша ланка, яка вміє, і далі він не йде
final class AutoRefundSmallOrders
{
public function handle(Refund $refund, Closure $next): Decision
{
if ($refund->amount <= 500_00 && $refund->days <= 14) {
return Decision::approve('автоматично'); // ланцюжок зупинився тут
}
return $next($refund); // не моє: передаю наступній ланці
}
}
$decision = Pipeline::send($refund)
->through([RejectAfter30Days::class, AutoRefundSmallOrders::class])
->then(fn (Refund $refund) => Decision::toManager()); // ніхто не взявся
// Pipeline: кожен крок обробляє дані й обовʼязково передає результат далі
final class DropDuplicates
{
public function handle(array $items, Closure $next): array
{
return $next(array_values(array_column($items, null, 'external_id')));
}
}
$items = Pipeline::send($feed)
->through([NormalizeTitles::class, DropDuplicates::class, AttachTags::class])
->thenReturn(); // запис у базу один раз, уже після всіх кроків
Сформулюйте одним реченням: «у ланцюжку відповідає один, у конвеєрі працюють усі». Потім покажіть, що middleware - це та сама функція `handle($request, $next)`, яка може працювати і як ланка, і як крок, залежно від того, чи викликає вона `$next`.