PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Chain of Responsibility – walidacja wieloetapowa, pipeline, middleware

by Henryk Tews / środa, 17 czerwca 2026 / Opublikowano w Wzorce projektowe

Chain of Responsibility to wzorzec behawioralny w którym żądanie przechodzi przez łańcuch handlerów – każdy może je obsłużyć, zmodyfikować lub przekazać dalej. Idealny do walidacji wieloetapowej, pipeline przetwarzania danych i middleware. W Magento 2 ten wzorzec jest wszędzie – w pluginach, pipeline zamówień i obsłudze żądań HTTP. Pokażę implementację od podstaw i trzy różne warianty łańcucha.

Trzy warianty wzorca

Chain of Responsibility ma trzy odmiany:

  • Classic – handler obsługuje LUB przekazuje dalej (jedno z dwóch)
  • Pipeline – każdy handler przetwarza i przekazuje dalej (middleware)
  • Intercepting filter – handler może zatrzymać łańcuch przez zwrócenie błędu

Klasyczna implementacja – walidacja zamówienia

<?php

declare(strict_types=1);

// Interfejs handlera
interface OrderValidatorInterface
{
    public function setNext(OrderValidatorInterface $validator): OrderValidatorInterface;
    public function validate(array $orderData): ValidationResult;
}

// Wynik walidacji
final class ValidationResult
{
    private array $errors = [];

    public function addError(string $field, string $message): void
    {
        $this->errors[$field][] = $message;
    }

    public function isValid(): bool { return empty($this->errors); }
    public function getErrors(): array { return $this->errors; }
}

// Abstrakcyjna baza - obsługuje łańcuch
abstract class AbstractOrderValidator implements OrderValidatorInterface
{
    private ?OrderValidatorInterface $next = null;

    public function setNext(OrderValidatorInterface $validator): OrderValidatorInterface
    {
        $this->next = $validator;
        return $validator; // umożliwia chainowanie: $a->setNext($b)->setNext($c)
    }

    // Wywołaj następny handler jeśli istnieje
    protected function validateNext(array $orderData, ValidationResult $result): void
    {
        if ($this->next !== null) {
            $nextResult = $this->next->validate($orderData);
            foreach ($nextResult->getErrors() as $field => $messages) {
                foreach ($messages as $message) {
                    $result->addError($field, $message);
                }
            }
        }
    }

    abstract public function validate(array $orderData): ValidationResult;
}

// Konkretne handlery
class CustomerValidator extends AbstractOrderValidator
{
    public function validate(array $orderData): ValidationResult
    {
        $result = new ValidationResult();

        if (empty($orderData['customer_email'])) {
            $result->addError('customer_email', 'Email klienta jest wymagany');
        } elseif (!filter_var($orderData['customer_email'], FILTER_VALIDATE_EMAIL)) {
            $result->addError('customer_email', 'Nieprawidłowy format email');
        }

        if (empty($orderData['customer_firstname'])) {
            $result->addError('customer_firstname', 'Imię klienta jest wymagane');
        }

        $this->validateNext($orderData, $result);
        return $result;
    }
}

class AddressValidator extends AbstractOrderValidator
{
    public function validate(array $orderData): ValidationResult
    {
        $result = new ValidationResult();
        $address = $orderData['shipping_address'] ?? [];

        if (empty($address['street'])) {
            $result->addError('shipping_address.street', 'Ulica jest wymagana');
        }

        if (empty($address['postcode'])) {
            $result->addError('shipping_address.postcode', 'Kod pocztowy jest wymagany');
        } elseif (!preg_match('/^\d{2}-\d{3}$/', $address['postcode'])) {
            $result->addError('shipping_address.postcode', 'Nieprawidłowy format kodu pocztowego');
        }

        $this->validateNext($orderData, $result);
        return $result;
    }
}

class ItemsValidator extends AbstractOrderValidator
{
    public function validate(array $orderData): ValidationResult
    {
        $result = new ValidationResult();
        $items  = $orderData['items'] ?? [];

        if (empty($items)) {
            $result->addError('items', 'Zamówienie musi zawierać co najmniej jeden produkt');
        }

        foreach ($items as $index => $item) {
            if (empty($item['sku'])) {
                $result->addError("items.{$index}.sku", 'SKU produktu jest wymagane');
            }
            if (($item['qty'] ?? 0) <= 0) {
                $result->addError("items.{$index}.qty", 'Ilość musi być większa od 0');
            }
        }

        $this->validateNext($orderData, $result);
        return $result;
    }
}

class MinimumOrderAmountValidator extends AbstractOrderValidator
{
    public function __construct(private float $minimumAmount = 50.0) {}

    public function validate(array $orderData): ValidationResult
    {
        $result = new ValidationResult();
        $total  = $orderData['grand_total'] ?? 0.0;

        if ($total < $this->minimumAmount) {
            $result->addError(
                'grand_total',
                "Minimalna wartość zamówienia to {$this->minimumAmount} PLN"
            );
        }

        $this->validateNext($orderData, $result);
        return $result;
    }
}

// Budowanie łańcucha
$customerValidator = new CustomerValidator();
$customerValidator
    ->setNext(new AddressValidator())
    ->setNext(new ItemsValidator())
    ->setNext(new MinimumOrderAmountValidator(50.0));

// Użycie
$orderData = [
    'customer_email'    => 'jan@example.com',
    'customer_firstname'=> 'Jan',
    'shipping_address'  => ['street' => 'Testowa 1', 'postcode' => '30-001'],
    'items'             => [['sku' => 'SKU-001', 'qty' => 2]],
    'grand_total'       => 149.99,
];

$result = $customerValidator->validate($orderData);

if (!$result->isValid()) {
    foreach ($result->getErrors() as $field => $messages) {
        echo "{$field}: " . implode(', ', $messages) . "\n";
    }
}

Pipeline – middleware HTTP

<?php

declare(strict_types=1);

// Pipeline: każdy handler MUSI przekazać dalej (middleware pattern)
interface MiddlewareInterface
{
    public function process(Request $request, callable $next): Response;
}

class Request
{
    public array $attributes = [];
    public function __construct(public string $method, public string $path, public array $headers = []) {}
}

class Response
{
    public function __construct(public int $status, public string $body = '') {}
}

// Middleware: uwierzytelnianie
class AuthMiddleware implements MiddlewareInterface
{
    public function process(Request $request, callable $next): Response
    {
        $token = $request->headers['Authorization'] ?? '';

        if (empty($token)) {
            return new Response(401, 'Unauthorized');
        }

        // Dodaj dane użytkownika do requestu
        $request->attributes['user_id'] = $this->validateToken($token);
        return $next($request); // przekaż dalej
    }

    private function validateToken(string $token): int
    {
        return 42; // uproszczone
    }
}

// Middleware: rate limiting
class RateLimitMiddleware implements MiddlewareInterface
{
    public function process(Request $request, callable $next): Response
    {
        $userId = $request->attributes['user_id'] ?? 0;

        if ($this->isRateLimited($userId)) {
            return new Response(429, 'Too Many Requests');
        }

        return $next($request);
    }

    private function isRateLimited(int $userId): bool
    {
        return false; // uproszczone
    }
}

// Middleware: logowanie
class LoggingMiddleware implements MiddlewareInterface
{
    public function process(Request $request, callable $next): Response
    {
        $start    = microtime(true);
        $response = $next($request); // wykonaj resztę łańcucha
        $duration = round((microtime(true) - $start) * 1000, 2);

        error_log("{$request->method} {$request->path} {$response->status} {$duration}ms");
        return $response;
    }
}

// Pipeline runner
class Pipeline
{
    private array $middleware = [];

    public function pipe(MiddlewareInterface $middleware): static
    {
        $this->middleware[] = $middleware;
        return $this;
    }

    public function run(Request $request, callable $handler): Response
    {
        $chain = array_reduce(
            array_reverse($this->middleware),
            fn($next, $middleware) => fn($req) => $middleware->process($req, $next),
            $handler
        );

        return $chain($request);
    }
}

// Użycie
$pipeline = (new Pipeline())
    ->pipe(new LoggingMiddleware())
    ->pipe(new AuthMiddleware())
    ->pipe(new RateLimitMiddleware());

$response = $pipeline->run(
    new Request('GET', '/api/orders', ['Authorization' => 'Bearer token123']),
    fn($req) => new Response(200, json_encode(['orders' => []]))
);

Chain of Responsibility w Magento 2

<?php

// Magento 2 Total Collectors to przykład CoR
// Każdy collector dodaje swój element do totals

// Własny validator w checkout - rejestracja przez di.xml
// <type name="Magento\Checkout\Model\CompositeConfigProvider">
//   <arguments>
//     <argument name="configProviders" xsi:type="array">
//       <item name="my_validator" xsi:type="object">Vendor\Module\Model\MyValidator</item>
//     </argument>
//   </arguments>
// </type>

// Composite pattern + Chain of Responsibility
class CompositeOrderValidator implements OrderValidatorInterface
{
    /** @var OrderValidatorInterface[] */
    private array $validators;

    public function __construct(array $validators)
    {
        $this->validators = $validators;
    }

    public function validate(array $orderData): ValidationResult
    {
        $combined = new ValidationResult();

        foreach ($this->validators as $validator) {
            $result = $validator->validate($orderData);
            if (!$result->isValid()) {
                foreach ($result->getErrors() as $field => $messages) {
                    foreach ($messages as $message) {
                        $combined->addError($field, $message);
                    }
                }
            }
        }

        return $combined;
    }

    // Nie potrzebuje setNext() - to Composite, nie klasyczny CoR
    public function setNext(OrderValidatorInterface $v): OrderValidatorInterface { return $v; }
}

Podsumowanie

Chain of Responsibility rozwiązuje problem wieloetapowego przetwarzania bez sztywnego sprzężenia między krokami. Klasyczny wariant – handler obsługuje lub przekazuje. Pipeline – każdy handler przetwarza i musi przekazać dalej. Composite validator – wszystkie handlery wykonują się równolegle. W Magento 2 ten wzorzec pojawia się w total collectors, plugin pipeline (around plugins tworzą łańcuch), i obsłudze żądań. Następny wpis: Custom Payment Method – integracja własnej bramki płatności.

About Henryk Tews

Co możesz przeczytać następne

Wzorzec State – maszyna stanów dla zamówienia, serializacja, porównanie ze Strategy
Wzorzec Command – kolejka komend, undo/redo, Magento 2 MessageQueue
Observer i Strategy w PHP – wzorce behawioralne
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}