Приложение позволяет настраивать динамический роутинг HTTP-запросов согласно выставленным настройкам. Пользователь посредством API может создать свой роут и желаемое количество правил, которые будут применены для маршрутизации. То есть это набор условий, которые будут проверяться, каждая ветвью этих условий должна заканчиваться редиректом на тот или иной веб-адрес.
Единицей здесь является сам роут, имеющий уникальный URL-паттерн. Он представляет собой параметризованный URL или маску. Вы можете строго задать определенный адрес, который будет отслеживаться, например: /homepage, но можете использовать параметры, например /posts/{id}, где id это содержащийся в адресе параметр, его имя вы также можете затем использовать для формирования адреса, на который будет произведен редирект. И третий вариант, это использовать маску которая принимает любой текст вместо звездочки, например /shop/catalogue/*.
Вот небольшой пример создания настройки:
Настройка создается запросом POST /api/v1/route
{
"urlPattern": "/blog/posts/{id}",
"priority": 0,
"isActive": true,
"initialStep": {
"type": "condition",
"schemeType": "datetime_range",
"schemeProps": {
"from": "2021-06-10",
"to": "2025-10-14"
},
"onPassStep": {
"type": "redirect",
"schemeType": "redirect",
"schemeProps": {
"url": "https://old-site.com/blog/posts/{id}"
}
},
"onDeclineStep": {
"type": "redirect",
"schemeType": "redirect",
"schemeProps": {
"url": "https://new-site.com/blog/posts/{id}"
}
}
}
}
Здесь создается настройка, которая с 14 октября 2025 года перенаправляет запросы на чтение постов на новый сайт, до этого же запросы идут на сайт старый. Ключи onPassStep и onDeclineStep могут также содержать неограниченное количество вложенных условий. Как видно, id поста будет передан в итоговый адрес, куда произойдет редирект.
Для добавления нового условия (не обязательно условия, в принципе также вы можете реализовать механизм каких-либо действий, например отправки нужных вам метрик), достаточно реализовать свою схему шага роутинга и стратегию для его обработки.
Схема описывает то, какие поля должна содержать настройка шага. Это должен быть класс имплементирующий App\Application\Scheme\RoutingStepSchemeInterface, а также помеченный атрибутом App\Application\Attribute\RoutingStepScheme содержащим тип шага (из коробки поддерживаются типы "условие" и "редирект"), а также алиас используемый для именования данной схемы.
Вот пример схемы, которую вы бы могли захотеть добавить в проект для определения, содержит ли запрос пользователя определенный заголовок.
<?php
declare(strict_types=1);
namespace Plugin\HasHeader;
use App\Application\Attribute\RoutingStepScheme;
use App\Application\Scheme\RoutingStepSchemeInterface;
use App\Domain\Enum\RoutingStepType;
#[RoutingStepScheme(type: RoutingStepType::CONDITION->value, alias: 'has_header')]
final class HasHeaderScheme implements RoutingStepSchemeInterface
{
public string $headerName;
}
Далее вам следует добавить стратегию, которая будет обрабатывать шаг роутинга, содержащий вашу схему. Она должна имплементировать App\Application\Service\Strategy\RoutingStepStrategyInterface и быть помеченной атрибутом App\Application\Attribute\SupportedRoutingStepScheme с указанием в последнем того класса схемы, которую ваша стратегия обрабатывает.
Рекомендуется не писать с нуля класс, а расширить один из существующих, например App\Infrastructure\Service\Strategy\ConditionCheckerStrategy. Если вы хотите создавать блоки, которые не являются ни условиями, ни редиректами (например хотите писать метрики), расширьте базовый класс App\Infrastructure\Service\Strategy\RoutingStepStrategy.
Вот пример стратегии для работы с приведенной выше кастомной схемой.
<?php
declare(strict_types=1);
namespace Plugin\HasHeader;
use App\Application\Attribute\SupportedRoutingStepScheme;
use App\Application\Dto\HttpRequestDto;
use App\Application\Scheme\RoutingStepSchemeInterface;
use App\Application\Service\Routing\RedirectionContextInterface;
use App\Infrastructure\Service\Strategy\ConditionCheckerStrategy;
#[SupportedRoutingStepScheme(class: HasHeaderScheme::class)]
final class HasHeaderCheckerStrategy extends ConditionCheckerStrategy
{
/**
* @param HasHeaderScheme $routingStepScheme
*/
protected function meetsCondtion(
RoutingStepSchemeInterface $routingStepScheme,
HttpRequestDto $httpRequestDto,
RedirectionContextInterface $context,
): bool {
return isset($httpRequestDto->headers[$routingStepScheme->headerName]);
}
protected function isRouteStepSchemeSupported(RoutingStepSchemeInterface $routingStepScheme): bool
{
return $routingStepScheme instanceof HasHeaderScheme;
}
}
Подобных двух классов достаточно, чтобы реализовать свой плагин. Поместите свои файлы в директорию plugin проекта и они автоматически будут подключены.
-
Для сопоставления запроса пользователя с определенным роутом сейчас запрашиваются все роуты, затем каждый URL-паттерн разбивается на части и строится дерево маршрутизации. Это очень неоптимальный способ, требующий много накладных расходов, а также имеющий существенные ограничения (начиная с определенного количества роутов мы столкнемся с проблемами производительности). Для того, чтобы понимать, как эту проблему следует решать, нужно понять, с каким объемом данных предстоит работать. Если речь идет о десятках - сотнях роутов, то нам будет достаточно кэшировать итоговое дерево маршрутизации. Если же данных будет существенно больше, возможно следует использовать несколько деревьев, которые по очереди будут загружаться в память, кроме этого, можно подумать над более умными и быстрыми алгоритмами поиска в дереве нужного маршрута. Также само это дерево можно сделать существенно компактнее.
-
Для того, чтобы можно было реализовать фронтенд приложения, который не будет требовать вносить изменения в него, когда мы расширяем функционал бэкенда (а мы делаем это с помощью плагинов, то есть вообще вне релизов как таковых), нам следует предоставлять через API способ получить описание формата схем, которые имеются в приложении. В том числе для определенных в кастомных плагинах. Делать это можно через механизм рефлексии этих схем, но также желательно предоставить точку расширения, которая будет позволять переопределить то, как будет описана конкретная схема для фронтенда. Также такое расширение должно быть доступно для механизма плагинов.
-
Определенные проблемы может привнести взаимодействие бэкенда с сервисом IP-геолокации. Взаимодействие между ними должно быть очень быстрым, к тому же вероятно мы не сможем задействовать здесь кэширование, так как с одного IP не будет большого количества повторяющихся запросов. Следует подумать о хорошей сетевой связанности этих двух приложений, а также предусмотреть возможности масштабирования (в том числе автоматического) и балансировки нагрузки, предусмотреть observability с уровнем достаточным для того, чтобы вовремя реагировать на проблемы.
-
Также возможно возникновение проблем с производительностью самого бэкенда при высокой нагрузке на него (проблем на разном уровне, от кода до базы данных). В целом, если мы решим проблему из п.1, мы можем довольно легко масштабировать наше приложение. Оно предполагает редкую запись/обновление/удаление данных из БД и частое чтение. Это хорошо ложится на схему с несколькими инстансами приложения, каждый из которых общается с репликой на чтение, а запросы от администратора, который настраивает схемы, направляет в мастер. Перед этими инстансами стоит балансировщик, который распределяет запросы пользователей.