PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Chain of Responsibility – łańcuch walidatorów, konfiguracja przez di.xml z sortOrder

by Henryk Tews / wtorek, 08 lutego 2022 / Opublikowano w Wzorce projektowe

Chain of Responsibility pozwala przekazywać żądanie przez łańcuch handlerów, gdzie każdy może je obsłużyć lub przekazać dalej. Eliminuje rozbudowane if-else i switch decydujące „kto powinien to obsłużyć”. W Magento 2 ten wzorzec pojawia się w pipeline przetwarzania płatności, middleware HTTP i systemie reguł cenowych. Pokazuję implementację od zera i praktyczne zastosowania.

Problem bez wzorca

<?php

// Walidacja zamówienia bez CoR - jedna klasa wie o wszystkim
class OrderValidator
{
    public function validate(Order $order): array
    {
        $errors = [];

        // Walidacja adresu
        if (empty($order->getShippingAddress())) {
            $errors[] = 'Brak adresu wysyłki';
        }

        // Walidacja produktów
        foreach ($order->getItems() as $item) {
            if ($item->getQty() <= 0) {
                $errors[] = "Nieprawidłowa ilość dla {$item->getSku()}";
            }
        }

        // Walidacja płatności
        if (!$order->getPaymentMethod()) {
            $errors[] = 'Brak metody płatności';
        }

        // Walidacja limitu kredytowego
        if ($order->getGrandTotal() > $this->getCreditLimit($order->getCustomerId())) {
            $errors[] = 'Przekroczony limit kredytowy';
        }

        // ...kolejne 10 walidacji...

        return $errors;
    }
}

Klasa rośnie, trudno testować poszczególne walidacje, dodanie nowej walidacji wymaga modyfikacji istniejącej klasy. Chain of Responsibility rozdziela każdą walidację na osobny handler.

Implementacja – interfejs i handler abstrakcyjny

<?php

declare(strict_types=1);

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

// Abstrakcyjny handler - obsługuje łańcuchowanie
abstract class AbstractOrderValidator implements OrderValidatorInterface
{
    private ?OrderValidatorInterface $next = null;

    public function setNext(OrderValidatorInterface $validator): OrderValidatorInterface
    {
        $this->next = $validator;
        return $validator; // zwróć następny - umożliwia fluent chaining
    }

    public function validate(Order $order): array
    {
        if ($this->next !== null) {
            return array_merge($this->doValidate($order), $this->next->validate($order));
        }

        return $this->doValidate($order);
    }

    // Każdy handler implementuje tylko swoją walidację
    abstract protected function doValidate(Order $order): array;
}

Konkretne handlery

<?php

declare(strict_types=1);

class ShippingAddressValidator extends AbstractOrderValidator
{
    protected function doValidate(Order $order): array
    {
        $errors = [];
        $address = $order->getShippingAddress();

        if (empty($address)) {
            $errors[] = 'Brak adresu wysyłki';
            return $errors;
        }

        if (empty($address->getStreet())) {
            $errors[] = 'Brak ulicy w adresie wysyłki';
        }

        if (empty($address->getPostcode())) {
            $errors[] = 'Brak kodu pocztowego';
        }

        return $errors;
    }
}

class ItemQuantityValidator extends AbstractOrderValidator
{
    protected function doValidate(Order $order): array
    {
        $errors = [];

        foreach ($order->getItems() as $item) {
            if ($item->getQty() <= 0) {
                $errors[] = "Nieprawidłowa ilość dla produktu: {$item->getSku()}";
            }
        }

        return $errors;
    }
}

class PaymentMethodValidator extends AbstractOrderValidator
{
    protected function doValidate(Order $order): array
    {
        if (!$order->getPaymentMethod()) {
            return ['Brak wybranej metody płatności'];
        }

        return [];
    }
}

class CreditLimitValidator extends AbstractOrderValidator
{
    public function __construct(
        private CustomerCreditService $creditService
    ) {}

    protected function doValidate(Order $order): array
    {
        $limit = $this->creditService->getLimit($order->getCustomerId());

        if ($order->getGrandTotal() > $limit) {
            return [sprintf(
                'Kwota zamówienia (%.2f PLN) przekracza limit kredytowy (%.2f PLN)',
                $order->getGrandTotal(),
                $limit
            )];
        }

        return [];
    }
}

class StockAvailabilityValidator extends AbstractOrderValidator
{
    public function __construct(
        private StockRegistry $stockRegistry
    ) {}

    protected function doValidate(Order $order): array
    {
        $errors = [];

        foreach ($order->getItems() as $item) {
            $stockItem = $this->stockRegistry->getStockItemBySku($item->getSku());

            if (!$stockItem->getIsInStock()) {
                $errors[] = "Produkt {$item->getSku()} jest niedostępny";
            } elseif ($stockItem->getQty() < $item->getQty()) {
                $errors[] = sprintf(
                    'Niewystarczający stan magazynowy dla %s (dostępne: %d, zamówione: %d)',
                    $item->getSku(),
                    $stockItem->getQty(),
                    $item->getQty()
                );
            }
        }

        return $errors;
    }
}

Budowanie i używanie łańcucha

<?php

// Budowanie łańcucha - fluent interface dzięki zwracaniu $next z setNext()
$validator = new ShippingAddressValidator();

$validator
    ->setNext(new ItemQuantityValidator())
    ->setNext(new PaymentMethodValidator())
    ->setNext(new CreditLimitValidator($creditService))
    ->setNext(new StockAvailabilityValidator($stockRegistry));

$errors = $validator->validate($order);

if (!empty($errors)) {
    throw new \Magento\Framework\Exception\LocalizedException(
        __('Zamówienie zawiera błędy: %1', implode(', ', $errors))
    );
}

Wariant z wczesnym zatrzymaniem

Czasem chcesz zatrzymać łańcuch przy pierwszym błędzie zamiast zbierać wszystkie:

<?php

abstract class AbstractStrictValidator implements OrderValidatorInterface
{
    private ?OrderValidatorInterface $next = null;

    public function setNext(OrderValidatorInterface $validator): OrderValidatorInterface
    {
        $this->next = $validator;
        return $validator;
    }

    public function validate(Order $order): array
    {
        $errors = $this->doValidate($order);

        // Zatrzymaj łańcuch jeśli są błędy
        if (!empty($errors)) {
            return $errors;
        }

        if ($this->next !== null) {
            return $this->next->validate($order);
        }

        return [];
    }

    abstract protected function doValidate(Order $order): array;
}

Konfiguracja łańcucha przez di.xml w Magento 2

<?xml version="1.0"?>
<config>
    <type name="Vendor\Module\Model\OrderValidationChain">
        <arguments>
            <argument name="validators" xsi:type="array">
                <item name="shipping_address" xsi:type="object" sortOrder="10">
                    Vendor\Module\Model\Validator\ShippingAddressValidator
                </item>
                <item name="item_quantity" xsi:type="object" sortOrder="20">
                    Vendor\Module\Model\Validator\ItemQuantityValidator
                </item>
                <item name="payment_method" xsi:type="object" sortOrder="30">
                    Vendor\Module\Model\Validator\PaymentMethodValidator
                </item>
                <item name="stock" xsi:type="object" sortOrder="40">
                    Vendor\Module\Model\Validator\StockAvailabilityValidator
                </item>
            </argument>
        </arguments>
    </type>
</config>
<?php

// Klasa budująca łańcuch z tablicy wstrzykniętej przez DI
class OrderValidationChain
{
    private ?OrderValidatorInterface $chain = null;

    public function __construct(array $validators = [])
    {
        // Posortuj po sortOrder
        usort($validators, fn($a, $b) => ($a['sortOrder'] ?? 0) <=> ($b['sortOrder'] ?? 0));

        // Zbuduj łańcuch
        $current = null;
        foreach (array_reverse($validators) as $validator) {
            $instance = $validator['instance'];
            if ($current !== null) {
                $instance->setNext($current);
            }
            $current = $instance;
        }

        $this->chain = $current;
    }

    public function validate(Order $order): array
    {
        if ($this->chain === null) {
            return [];
        }

        return $this->chain->validate($order);
    }
}

Podsumowanie

Chain of Responsibility świetnie sprawdza się wszędzie tam gdzie masz wieloetapowe przetwarzanie z możliwością zatrzymania lub modyfikacji na każdym etapie. W Magento 2 wzorzec pojawia się naturalnie w pipeline’ach płatności, middleware HTTP i systemie reguł. Konfiguracja łańcucha przez di.xml z sortOrder to eleganckie rozwiązanie które pozwala zewnętrznym modułom dodawać własne handlery bez modyfikacji istniejącego kodu.

About Henryk Tews

Co możesz przeczytać następne

Wzorzec Visitor – double dispatch, eksport CSV/PDF, walidacja bez modyfikacji klas
Wzorzec Factory Method – Simple Factory, GoF, auto-generowane fabryki Magento 2
Command i Chain of Responsibility 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}