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.
