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.
