Предыстория: зачем ещё один роутер?
Каждый PHP-разработчик хотя бы раз задавался вопросом: «А не написать ли свой роутер?» Обычно ответ — «не надо, возьми готовый». И это правильный совет. FastRoute, Symfony Routing, Laravel Router — все они проверены временем и боем.
Но у меня была другая цель. Я хотел проверить гипотезу: можно ли с помощью современных ИИ-инструментов создать production-ready библиотеку, которая не стыдно выложить на Packagist, за один вечер?
Не прототип. Не «MVP, который потом допилим». А полноценную библиотеку с:
Строгой типизацией (PHP 8.4,
strict_types)PHPStan level 9
Полным покрытием тестами
CI/CD пайплайном
Документацией
Публикацией на Packagist
Спойлер: получилось.
Что получилось: Waypoint
ascetic-soft/waypoint — легковесный PSR-15 роутер со следующими возможностями:
PSR-15 совместимость — реализует
RequestHandlerInterface, работает с любым PSR-7/PSR-15 стекомАтрибутная маршрутизация — объявление маршрутов через PHP 8
#[Route]атрибуты прямо на контроллерахБыстрый prefix-trie матчинг — статические сегменты за O(1), динамические — только когда нужно
Middleware-пайплайн — глобальные и per-route PSR-15 middleware
Группы маршрутов — общие префиксы и middleware
Кеширование — компиляция маршрутов в PHP-файл для OPcache
Автоматический DI — параметры маршрута,
ServerRequestInterface, сервисы из контейнераURL-генерация — обратная маршрутизация по именам
Диагностика — обнаружение конфликтов, дубликатов, затенённых маршрутов
Масштаб проекта
Метрика | Значение |
|---|---|
Строк кода (src/) | ~2 100 |
Строк тестов (tests/) | ~2 650 |
Файлов в src/ | 18 |
Тестовых файлов | 16 |
Коммитов | 8 |
Время разработки | ~5.5 часов |
PHPStan | Level 9 |
Зависимости | Только PSR-интерфейсы |
Как проходил процесс
Инструмент: Cursor IDE
Я использовал Cursor — IDE на базе VS Code с глубокой интеграцией ИИ. Cursor позволяет вести диалог с ИИ прямо в контексте кодовой базы: он видит файлы проекта, понимает структуру, может читать и редактировать код.
Хронология (по git log)
Весь проект был создан 12 февраля 2026 года. Вот хронология коммитов:
17:16 init project — каркас проекта, composer.json, базовые классы 17:22 phpstan level 9 — настройка статического анализа, фикс типов 17:25 makefile fixer — Makefile для удобной разработки 17:44 tree match — реализация prefix-trie для быстрого матчинга 17:57 docs && ci — README, GitHub Actions CI/CD 18:14 tests — полный набор юнит-тестов 22:44 Url Generator — обратная маршрутизация 22:56 Url Generator base url — поддержка абсолютных URL
Между первым и последним коммитом прошло 5 часов 40 минут. Это включая перерывы, обдумывание архитектуры и ревью сгенерированного кода.
Этап 1: Архитектура и каркас (17:16)
Я начал с описания того, что хочу получить. Примерно так:
«Мне нужен PSR-15 совместимый PHP-роутер. PHP 8.4, strict types. Поддержка атрибутов #[Route] на контроллерах. FastRoute-style плейсхолдеры {name} и {name:regex}. Middleware pipeline. Группы маршрутов. Минимум зависимостей — только PSR-интерфейсы.»
ИИ предложил архитектуру:
Router (PSR-15 RequestHandlerInterface) ├── RouteCollection │ ├── RouteTrie — prefix-tree для быстрого матчинга │ └── Route[] — линейный fallback для сложных паттернов ├── AttributeRouteLoader — чтение #[Route] атрибутов через Reflection ├── MiddlewarePipeline — FIFO PSR-15 middleware ├── RouteHandler — вызов контроллера с DI ├── RouteCompiler — компиляция/загрузка кеша └── RouteDiagnostics — обнаружение конфликтов
Я одобрил структуру, и за несколько итераций диалога были созданы все базовые файлы: composer.json, атрибут #[Route], value-объекты Route и RouteMatchResult, коллекция маршрутов, загрузчик атрибутов.
Что важно: я не просто жал «принять всё». На каждом шаге я проверял:
Соответствует ли код PSR-стандартам
Правильно ли используются readonly-свойства PHP 8.4
Нет ли лишних зависимостей
Логична ли декомпозиция
Этап 2: Prefix-Trie — сердце роутера (17:44)
Самая интересная часть — алгоритм матчинга маршрутов. Вместо простого перебора всех регулярок (как в большинстве роутеров), Waypoint использует prefix-trie (префиксное дерево).
Идея:
Каждый маршрут разбивается на сегменты по
/Статические сегменты — ключи в hash-map (O(1) lookup)
Динамические сегменты (
{id},{slug:\w+}) — проверяются regex только когда нужноЕсли паттерн не совместим с trie (например,
prefix-{name}.txt), маршрут попадает в линейный fallback
Вот ключевой метод матчинга:
public function match( string $method, array $segments, int $depth = 0, array $params = [], array &$allowedMethods = [], ): ?array { if ($depth === count($segments)) { foreach ($this->routes as $route) { if ($route->allowsMethod($method)) { return ['route' => $route, 'params' => $params]; } foreach ($route->getMethods() as $m) { $allowedMethods[$m] = true; } } return null; } $segment = $segments[$depth]; // 1. Статический потомок — O(1) hash-map lookup if (isset($this->staticChildren[$segment])) { $result = $this->staticChildren[$segment]->match( $method, $segments, $depth + 1, $params, $allowedMethods, ); if ($result !== null) { return $result; } } // 2. Динамические потомки — в порядке приоритета foreach ($this->paramChildren as $child) { if (preg_match($child['regex'], $segment)) { $childParams = $params; $childParams[$child['paramName']] = $segment; $result = $child['node']->match( $method, $segments, $depth + 1, $childParams, $allowedMethods, ); if ($result !== null) { return $result; } } } return null; }
ИИ предложил эту структуру, но я попросил доработать несколько моментов:
Добавить бэктрекинг (если статическая ветка не нашла — пробуем динамическую)
Собирать
allowedMethodsдля корректного 405-ответаПроверку совместимости паттерна с trie (
isCompatible())
Этап 3: Атрибутная маршрутизация
PHP 8 атрибуты — мощный инструмент для декларативного описания маршрутов:
#[Route('/api/users', middleware: [AuthMiddleware::class])] class UserController { #[Route('/', methods: ['GET'], name: 'users.list')] public function list(): ResponseInterface { /* ... */ } #[Route('/{id:\d+}', methods: ['GET'], name: 'users.show')] public function show(int $id): ResponseInterface { /* ... */ } #[Route('/{id:\d+}', methods: ['PUT'], name: 'users.update')] public function update(int $id, ServerRequestInterface $request): ResponseInterface { /* ... */ } }
Атрибут #[Route] работает на двух уровнях:
На классе — задаёт префикс пути и общие middleware
На методе — определяет конкретный маршрут
Атрибут — IS_REPEATABLE, что позволяет одному методу обслуживать несколько маршрутов. AttributeRouteLoader использует Reflection API для извлечения метаданных.
Этап 4: Тесты (18:14)
Вот тут ИИ показал себя во всей красе. После формулировки «Напиши полный набор тестов для всех компонентов» я получил 2 650 строк тестового кода, покрывающего:
RouterTest— интеграционные тесты роутераRouteCollectionTest— матчинг маршрутов, приоритеты, 404/405RouteTrieTest— тесты prefix-trie: статические/динамические сегменты, бэктрекингAttributeRouteLoaderTest— загрузка атрибутов, классовые префиксыMiddlewarePipelineTest— порядок выполнения middleware, FIFORouteHandlerTest— DI-инъекция параметров, приведение типовRouteDiagnosticsTest— обнаружение конфликтовRouteCompilerTest— компиляция и загрузка кешаUrlGeneratorTest— генерация URL, query-параметры
При этом тесты были не «для галочки» — в них проверялись граничные случаи: trailing slashes, пустые пути, конфликтующие маршруты, nullable-параметры, приведение типов.
Конечно, я проверял тесты и при необходимости просил доработать покрытие для edge cases.
Этап 5: CI/CD и документация (17:57)
GitHub Actions pipeline с тремя параллельными job'ами:
jobs: code-style: # PHP CS Fixer (dry-run) static-analysis: # PHPStan level 9 tests: # PHPUnit + coverage → Codecov
README получился обширный, с примерами для каждой фичи, таблицами параметров, диаграммой архитектуры, бейджами CI, покрытия и версий.
Этап 6: URL Generator (22:44–22:56)
Последний штрих — обратная маршрутизация. Именованные маршруты можно использовать для генерации URL:
$router->get('/users/{id:\d+}', $handler, name: 'users.show'); $url = $router->generate('users.show', ['id' => 42]); // => /users/42 $url = $router->generate('users.show', ['id' => 42], absolute: true); // => https://example.com/users/42
Что ИИ делает хорошо
1. Шаблонный код
Роутер — это много однотипного кода: методы get(), post(), put(), delete() отличаются одним параметром. ИИ генерирует такой код мгновенно и безошибочно.
2. PHPDoc и типизация
Все @param, @return, @throws, generic-типы вроде list<array{type: 'static'|'param', value: string}> — ИИ выдаёт их правильно и полно. Это критично для PHPStan level 9.
3. Тесты
Написание тестов — рутина, от которой большинство разработчиков отлынивают. ИИ генерирует тесты с удовольствием и покрывает сценарии, о которых можно забыть.
4. Конфигурационные файлы
composer.json, phpunit.xml.dist, phpstan.neon.dist, .php-cs-fixer.dist.php, Makefile, .github/workflows/ci.yml — всё это ИИ создал правильно с первого-второго раза.
5. README
Документация получилась подробная, с примерами кода для каждой фичи, таблицами параметров, ASCII-диаграммой архитектуры.
Где ИИ нуждается в контроле
1. Архитектурные решения
ИИ предлагает решения, но окончательный выбор — за разработчиком. Например, решение разделить маршруты на trie-совместимые и линейный fallback — это архитектурное решение, которое нужно было осознанно принять.
2. Edge cases
ИИ может пропустить нетривиальные граничные случаи. Например, что происходит, когда regex параметра может матчить / (кросс-сегментный захват)? Или когда сегмент содержит смесь статического текста и параметра (prefix-{name}.txt)? Эти случаи нужно было явно продумать и указать ИИ.
3. Качество кода
Иногда ИИ генерирует «рабочий, но некрасивый» код. Нужно не стесняться просить рефакторинг: «Сделай это через readonly-свойства», «Используй named arguments», «Разбей на более мелкие методы».
4. Консистентность
При генерации большого объёма кода ИИ может забыть о решениях, принятых ранее. Важно следить за единообразием именования, обработки ошибок, порядка параметров.
Технические решения, которыми горжусь
Двухуровневый матчинг
Не все маршруты помещаются в trie. Паттерн /files/{path:.+} (где regex матчит /) или /report-{year}.pdf (смесь статики и параметра в одном сегменте) — не совместимы с посегментным поиском. Waypoint автоматически определяет это через RouteTrie::isCompatible() и отправляет такие маршруты в линейный fallback.
Lazy-инициализация
Trie строится только при первом запросе match(). Индекс имён для URL-генерации — только при первом вызове generate(). Это значит, что если вы загружаете 500 маршрутов, но обрабатываете только один запрос — вы не платите за построение всех индексов.
OPcache-дружественный кеш
Маршруты компилируются в обычный PHP-массив:
// cache/routes.php — загружается мгновенно через OPcache return [ ['pattern' => '/users/{id:\d+}', 'methods' => ['GET'], 'handler' => [...], ...], // ... ];
Никакого serialize()/unserialize(), никакого JSON. Просто include + OPcache = нулевые накладные расходы.
DI с приведением типов
RouteHandler анализирует сигнатуру метода контроллера и автоматически приводит параметры маршрута к нужному типу:
// Маршрут: /users/{id:\d+} // Параметр из URL: string "42" public function show(int $id) { /* $id === 42 (int) */ }
Порядок разрешения: ServerRequestInterface → route-параметры → контейнер → default values → nullable.
Статистика и цифры
8 коммитов за 5 часов 40 минут
18 файлов в
src/, 16 тестовых файлов~4 800 строк PHP-кода суммарно
0 внешних зависимостей (только PSR-интерфейсы)
PHPStan level 9 — максимальный уровень строгости
3 параллельных CI job'а: code style, static analysis, tests + coverage
Выводы
ИИ — это мультипликатор, а не замена
ИИ не написал этот проект за меня. Я принимал архитектурные решения, ревьюил каждый файл, указывал на проблемы, просил доработки. Но ИИ колоссально ускорил процесс.
Без ИИ этот проект занял бы у меня 3–5 рабочих дней. С ИИ — один вечер. При этом качество кода не пострадало: PHPStan level 9 не прощает небрежности.
Что нужно от разработчика
Чёткое видение — ИИ отлично выполняет задачи, но плохо ставит их сам себе
Архитектурное мышление — декомпозиция, выбор паттернов, trade-offs
Code review — ИИ может ошибаться, и его код нужно проверять с тем же пристрастием, что и код коллеги
Знание предметной област�� — PSR-стандарты, особенности PHP 8.4, best practices
Когда это работает лучше всего
Библиотеки с чёткой спецификацией (как роутер)
Проекты, где много шаблонного кода
Задачи с хорошо определёнными интерфейсами (PSR)
Написание тестов и документации
Когда стоит быть осторожным
Уникальная бизнес-логика
Код, зависящий от специфического контекста
Оптимизации производительности (нужны бенчмарки, а не интуиция ИИ)
Безопасность (всегда проверяйте вручную)
Попробовать
composer require ascetic-soft/waypoint
Код на GitHub: ascetic-soft/Waypoint
Вопросы, критика, идеи — welcome в комментариях. Если вы тоже создавали проекты с помощью ИИ — делитесь опытом, интересно сравнить подходы.
