Если вы когда‑нибудь пытались настроить бизнес‑логику в своём проекте так, чтобы она не выглядела как свалка if-else и работала хорошо, то этот материал для вас. Сегодня мы разберём один из самых приятных паттернов — Chain of Responsibility, или «Цепочка обязанностей».
Вместо кучи условий, которые как минимум трудно читать, а как максимум невозможно поддерживать, мы строим гибкую архитектуру. Каждый этап обработки запроса становится отдельным модулем. А благодаря возможности менять порядок этих модулей или добавлять новые, система легко масштабируется.
Используйте Chain of Responsibility, когда:
<?php
interface HandlerInterface
{
public function setNext(HandlerInterface $handler): HandlerInterface;
public function handle(array $request): ?array;
}
abstract class AbstractHandler implements HandlerInterface
{
private ?HandlerInterface $nextHandler = null;
public function setNext(HandlerInterface $handler): HandlerInterface
{
$this->nextHandler = $handler;
return $handler;
}
public function handle(array $request): ?array
{
if ($this->nextHandler) {
return $this->nextHandler->handle($request);
}
return $request;
}
}
Интерфейс HandlerInterface определяет контракт для всех обработчиков, а базовый класс AbstractHandler реализует передачу запроса следующему обработчику.
<?php
class StockHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
if ($request['stock'] <= 0) {
throw new RuntimeException('Товара нет в наличии.');
}
error_log("Товар в наличии: {$request['stock']} единиц.");
return parent::handle($request);
}
}
Теперь реализуем проверку возраста покупателя:
<?php
class AgeVerificationHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
if ($request['age'] < 18) {
throw new RuntimeException('Покупатель слишком молод.');
}
error_log("Возраст покупателя ({$request['age']}) прошёл проверку.");
return parent::handle($request);
}
}
Проверка оплаты:
<?php
class PaymentHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
if (empty($request['payment']) || !$request['payment']) {
throw new RuntimeException('Оплата не прошла.');
}
error_log("Оплата успешно завершена: {$request['payment_id']}.");
return parent::handle($request);
}
}
Упаковка и подготовка к доставке:
<?php
class PackagingHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
error_log("Товар упакован и готов к доставке.");
$request['status'] = 'ready_for_delivery';
return parent::handle($request);
}
}
<?php
$request = [
'stock' => 5,
'age' => 25,
'payment' => true,
'payment_id' => 'PAY12345',
];
$stockHandler = new StockHandler();
$ageHandler = new AgeVerificationHandler();
$paymentHandler = new PaymentHandler();
$packagingHandler = new PackagingHandler();
$stockHandler->setNext($ageHandler)
->setNext($paymentHandler)
->setNext($packagingHandler);
try {
$result = $stockHandler->handle($request);
echo "Заказ успешно обработан: " . json_encode($result, JSON_PRETTY_PRINT);
} catch (RuntimeException $e) {
error_log("Ошибка обработки заказа: " . $e->getMessage());
echo "Ошибка: " . $e->getMessage();
}
Если что‑то идёт не так, выбрасываем RuntimeException, а все важные этапы логируются через error_log (или можно заменить на тот же Monolog).
Не забываем покрыть код тестами:
<?php
use PHPUnit\Framework\TestCase;
class ChainTest extends TestCase
{
public function testStockHandlerFailsWhenOutOfStock()
{
$handler = new StockHandler();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Товара нет в наличии.');
$handler->handle(['stock' => 0]);
}
public function testChainProcessesRequestSuccessfully()
{
$request = [
'stock' => 5,
'age' => 25,
'payment' => true,
'payment_id' => 'PAY12345',
];
$stockHandler = new StockHandler();
$ageHandler = new AgeVerificationHandler();
$paymentHandler = new PaymentHandler();
$packagingHandler = new PackagingHandler();
$stockHandler->setNext($ageHandler)
->setNext($paymentHandler)
->setNext($packagingHandler);
$result = $stockHandler->handle($request);
$this->assertEquals('ready_for_delivery', $result['status']);
}
}
Вместо кучи условий, которые как минимум трудно читать, а как максимум невозможно поддерживать, мы строим гибкую архитектуру. Каждый этап обработки запроса становится отдельным модулем. А благодаря возможности менять порядок этих модулей или добавлять новые, система легко масштабируется.
Используйте Chain of Responsibility, когда:
- Логика обработки запроса должна быть модульной.
- Нужно динамически менять последовательность обработки.
- Вы хотите облегчить добавление новых обработчиков.
Реализация паттерна на примере магазина котиков
Архитектура магазина котиков
Вот как будет выглядеть наш процесс:- Проверка наличия товара.
- Проверка возраста покупателя.
- Проверка оплаты.
- Упаковка заказа.
Интерфейс обработчика
Начнём с базового интерфейса, который будут реализовывать наши обработчики.<?php
interface HandlerInterface
{
public function setNext(HandlerInterface $handler): HandlerInterface;
public function handle(array $request): ?array;
}
abstract class AbstractHandler implements HandlerInterface
{
private ?HandlerInterface $nextHandler = null;
public function setNext(HandlerInterface $handler): HandlerInterface
{
$this->nextHandler = $handler;
return $handler;
}
public function handle(array $request): ?array
{
if ($this->nextHandler) {
return $this->nextHandler->handle($request);
}
return $request;
}
}
Интерфейс HandlerInterface определяет контракт для всех обработчиков, а базовый класс AbstractHandler реализует передачу запроса следующему обработчику.
Обработчики
Теперь создадим обработчики для проверки заказа. Начнем с проверки наличия заказа:<?php
class StockHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
if ($request['stock'] <= 0) {
throw new RuntimeException('Товара нет в наличии.');
}
error_log("Товар в наличии: {$request['stock']} единиц.");
return parent::handle($request);
}
}
Теперь реализуем проверку возраста покупателя:
<?php
class AgeVerificationHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
if ($request['age'] < 18) {
throw new RuntimeException('Покупатель слишком молод.');
}
error_log("Возраст покупателя ({$request['age']}) прошёл проверку.");
return parent::handle($request);
}
}
Проверка оплаты:
<?php
class PaymentHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
if (empty($request['payment']) || !$request['payment']) {
throw new RuntimeException('Оплата не прошла.');
}
error_log("Оплата успешно завершена: {$request['payment_id']}.");
return parent::handle($request);
}
}
Упаковка и подготовка к доставке:
<?php
class PackagingHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
error_log("Товар упакован и готов к доставке.");
$request['status'] = 'ready_for_delivery';
return parent::handle($request);
}
}
Сборка цепочки
Теперь объединим все обработчики в цепочку.<?php
$request = [
'stock' => 5,
'age' => 25,
'payment' => true,
'payment_id' => 'PAY12345',
];
$stockHandler = new StockHandler();
$ageHandler = new AgeVerificationHandler();
$paymentHandler = new PaymentHandler();
$packagingHandler = new PackagingHandler();
$stockHandler->setNext($ageHandler)
->setNext($paymentHandler)
->setNext($packagingHandler);
try {
$result = $stockHandler->handle($request);
echo "Заказ успешно обработан: " . json_encode($result, JSON_PRETTY_PRINT);
} catch (RuntimeException $e) {
error_log("Ошибка обработки заказа: " . $e->getMessage());
echo "Ошибка: " . $e->getMessage();
}
Если что‑то идёт не так, выбрасываем RuntimeException, а все важные этапы логируются через error_log (или можно заменить на тот же Monolog).
Не забываем покрыть код тестами:
<?php
use PHPUnit\Framework\TestCase;
class ChainTest extends TestCase
{
public function testStockHandlerFailsWhenOutOfStock()
{
$handler = new StockHandler();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Товара нет в наличии.');
$handler->handle(['stock' => 0]);
}
public function testChainProcessesRequestSuccessfully()
{
$request = [
'stock' => 5,
'age' => 25,
'payment' => true,
'payment_id' => 'PAY12345',
];
$stockHandler = new StockHandler();
$ageHandler = new AgeVerificationHandler();
$paymentHandler = new PaymentHandler();
$packagingHandler = new PackagingHandler();
$stockHandler->setNext($ageHandler)
->setNext($paymentHandler)
->setNext($packagingHandler);
$result = $stockHandler->handle($request);
$this->assertEquals('ready_for_delivery', $result['status']);
}
}
Что ещё можно улучшить?
- Динамическая конфигурация цепочки.
Например, настраивать последовательность обработчиков через тот же JSON или YAML. - Производительность.
Для больших цепочек можно добавить кэширование результатов, чтобы не проходить одни и те же проверки повторно. - Логирование.
Подключаем Monolog для более подробного логирования.
Реализация паттерна Chain of Responsibility на примере котиков в PHP
Привет, Хабр! Если вы когда‑нибудь пытались настроить бизнес‑логику в своём проекте так, чтобы она не выглядела как свалка if-else и работала хорошо, то этот материал для вас....
habr.com