<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
АРХІТЕКТУРА · MIDDLE

Chain of Responsibility, Pipeline і middleware: як вони повʼязані й чим відрізняються?

Обидва патерни передають запит послідовністю обробників, але з різним наміром: у Chain of Responsibility запит обробляє перша ланка, яка за нього взялася, і далі він не йде, а в Pipeline працює кожен крок і передає свій результат наступному. Middleware в Laravel і Symfony Messenger поєднує обидва: зазвичай шар передає запит далі й може змінити відповідь на зворотному шляху, але будь-який шар може зупинити обробку, повернувши відповідь сам.

Middleware в Laravel - це який патерн?
Чим ланцюжок обовʼязків відрізняється від конвеєра, якщо в обох є `$next`?
Імпорт фіду вакансій - це один цикл на триста рядків, де очищення, дедуплікація й теги перемішані. Як би ви його розклали?
Логувальний middleware не бачить запитів, які отримали 401. Чому?
design patterns Chain of Responsibility Pipeline middleware Laravel Symfony Messenger

Ланцюжок обовʼязків і конвеєр виглядають однаково: кожен обробник отримує запит і посилання на наступного. Відрізняє їх, скільки обробників мають попрацювати. У 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 як цибулю: код до `$next($request)` бачить запит, код після бачить відповідь, а шар без виклику `$next` відрізає всі внутрішні шари й контролер.
Що знає `Illuminate\Pipeline\Pipeline`: `send()`, `through()`, `via()`, `then()` і `thenReturn()`, кроки з контейнера, і що HTTP-запит іде через нащадка `Illuminate\Routing\Pipeline`.
Що порядок ланок задається в одному місці, де збирають ланцюжок, а не всередині ланок.
Що для кроків з побічними ефектами є стратегія: кроки лише перетворюють дані, запис один раз у кінці, або все в транзакції.
Писати в кожній ланці ланцюжка безумовний `return $next($request)`: спрацьовують усі обробники, і правило «запит бере перша ланка» зникає, тобто вийшов конвеєр з неочікуваною поведінкою.
Ставити логування запитів у список після `auth`, а потім шукати в логах 401: шар, що зупинив запит, не пускає його до внутрішніх шарів, тож їхній код не виконується.
Вважати, що колбек `then()` у `Pipeline` виконується завжди: якщо крок не викликав `$next`, `then()` не запуститься, а результатом стане те, що повернув цей крок.
Писати в базу в кожному кроці імпорту без транзакції: падіння посередині лишає половину змін, а повторний запуск дублює записи.
Ховати порядок у самих кроках (крок сам створює наступний клас за імʼям): переставити чи вимкнути крок в одному місці вже неможливо.
ПОРАДА

Сформулюйте одним реченням: «у ланцюжку відповідає один, у конвеєрі працюють усі». Потім покажіть, що middleware - це та сама функція `handle($request, $next)`, яка може працювати і як ланка, і як крок, залежно від того, чи викликає вона `$next`.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Обидва патерни послідовні й можуть бути зібрані будь-яким способом, тож відрізняє їх, хто обробляє і чи йде запит далі. Middleware може зупинити запит, наприклад повернувши 403 без виклику `$next`, тому в Laravel він поводиться і як крок конвеєра, і як ланка ланцюжка.