Проектная работа на тему "Умные ссылки" для курса "Архитектура и шаблоны проектирования" OTUS
- Цепочка обработчиков запросов (middleware)
- Ядро «умных ссылок» (Strategy → Condition → Action)
Все HTTP-запросы проходят через цепочку классов, реализующих RequestHandlerInterface
.
Каждый обработчик:
- Реализует метод
public function handle(Request $request): Response
- В случае невозможности собственно обработать запрос вызывает
чтобы передать управление следующему звену цепочки.
$this->next($request)
- Создать класс, имплементирующий
RequestHandlerInterface
. - Пометить его атрибутом
#[AsTaggedItem('app.request_handler', priority: <число>)]
- Symfony DI автоматически соберёт все обработчики и выстроит их по убыванию
priority
.
MethodNotAllowedHandler
(priority 900): сразу возвращает 405 для всех не-GET-запросов.SmartRedirectHandler
(priority 500): запускает логику «умных ссылок» и редиректит по первой успешно прошедшей стратегии.NotFoundHandler
(priority 0): всегда отдаёт 404, если никто ранее не обработал запрос.
Все настройки «умных ссылок» хранятся в БД как три сущности:
-
Strategy
-
path
— точное совпадение URL-path
-
priority
— порядок проверки (DESC)
-
conditions
→Condition
(0…N)
-
action
→Action
(0‒1)
-
Condition
-
handlerTag
— код условного обработчика
-
parameters
— массив доп. аргументов
-
Action
-
handlerTag
— код обработчика действия
-
parameters
— массив доп. аргументов
-
В
SmartRedirectHandler
поpath
из$request
вызываем$strategies = $repo->fetchForPath($path);
— получаем массив
Strategy
, уже сconditions
иaction
черезLEFT JOIN
. -
Для каждой
Strategy
по-порядку:- Проверяем все её условия:
— если нет условий, считаем их пройденными.
foreach ($conditions as $conditionEntity) { $tag = $conditionEntity->getHandlerTag(); $params = $conditionEntity->getParameters() ?? []; $checker = $this->conditionResolver->getCondition($tag); if (!$checker->check($request, $params)) { return false; } }
- Выполняем действие:
$actionEntity = $strategy->getAction(); $tag = $actionEntity->getHandlerTag(); $params = $actionEntity->getParameters() ?? []; return $this->actionResolver->getAction($tag)->handle($request, $params);
- Проверяем все её условия:
-
Если ни одна стратегия не сработала, передаём управление следующему middleware.
-
ConditionStrategy (интерфейс
ConditionStrategyInterface
)Метод
public function check(Request $request, array $params): bool
Пример:
#[AsTaggedItem('before_timestamp')] class BeforeTimestampStrategy implements ConditionStrategyInterface { public function check(Request $request, array $params): bool { /*…*/ } }
-
ActionStrategy (интерфейс
ActionStrategyInterface
)Метод:
public function handle(Request $request, array $params): Response
Пример:
#[AsTaggedItem('redirect')] class RedirectStrategy implements ActionStrategyInterface { public function handle(Request $request, array $params): Response { /*…*/ } }
- Имплементировать соответствующий интерфейс.
- Пометить атрибутом
#[AsTaggedItem('<handlerTag>')]
- DI автоматически загрузит класс в локатор — дальше он доступен по
getCondition(tag)
илиgetAction(tag)
.
flowchart TD
A[Incoming HTTP Request] --> B[RequestHandlerChainService handle]
B --> C[First Middleware handle]
C -->|cannot handle| D[SmartRedirectHandler handle]
D --> E[StrategyProcessor process]
E --> F{Strategies for path?}
F -->|none| P[Next Middleware handle]
F -->|one or more| G[Loop through strategies]
subgraph StrategyLoop direction TD
G --> H[Take next Strategy]
H --> I{Conditions exist?}
I -->|no| K[Execute Action]
I -->|yes| J[Loop through Conditions]
subgraph ConditionLoop direction TD
J --> L[Take next Condition]
L --> M[ConditionStrategy check]
M -->|false| Q[Skip to next Strategy]
M -->|true| N{More Conditions?}
N -->|yes| L
N -->|no| K
end
K --> O[ActionStrategy handle]
O --> R[Return Response]
Q --> H
end
P --> Z[Fallback Response]
Пояснение:
- Запрос сначала проходит через цепочку middleware — вплоть до SmartRedirectHandler.
- Внутри SmartRedirectHandler запускается StrategyProcessor.
- Из БД получают все стратегии по path.
- Для каждой стратегии последовательно перебираются её условия; при первом false переходят к следующей стратегии.
- Если все условия вернули true (или их не было), выполняется соответствующее действие и возвращается HTTP-ответ.
- Если ни одна стратегия не сработала, управление возвращается следующему middleware и отдаётся «fallback»-ответ.
Проблема: каждый запрос последовательно проходит все обработчики, и с ростом их числа время ответа растёт.
Решение: добавить префильтрацию по условию и кешировать ее, чтобы сразу вызывать только релевантные звенья цепочки.
Проблема: при большом количестве стратегий трудно визуально контролировать порядок и логику.
Решение: создать веб-UI с drag-and-drop приоритетов, фильтрами и симулятором прохождения запроса.
Проблема: исключение в любом ConditionStrategy
или ActionStrategy
обрывает цепочку.
Решение:
- Внедрить обёртки-спасатели (
try/catch
) вокруг каждого вызова стратегии. - Логировать контекст через Monolog + Sentry.
- При ошибке переходить к «дефолтному» действию или отдавать заранее настроенный fallback-ответ.