Предыстория: зачем ещё один роутер?

Каждый 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 (префиксное дерево).

Идея:

  1. Каждый маршрут разбивается на сегменты по /

  2. Статические сегменты — ключи в hash-map (O(1) lookup)

  3. Динамические сегменты ({id}, {slug:\w+}) — проверяются regex только когда нужно

  4. Если паттерн не совместим с 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/405

  • RouteTrieTest — тесты prefix-trie: статические/динамические сегменты, бэктрекинг

  • AttributeRouteLoaderTest — загрузка атрибутов, классовые префиксы

  • MiddlewarePipelineTest — порядок выполнения middleware, FIFO

  • RouteHandlerTest — 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 не прощает небрежности.

Что нужно от разработчика

  1. Чёткое видение — ИИ отлично выполняет задачи, но плохо ставит их сам себе

  2. Архитектурное мышление — декомпозиция, выбор паттернов, trade-offs

  3. Code review — ИИ может ошибаться, и его код нужно проверять с тем же пристрастием, что и код коллеги

  4. Знание предметной области — PSR-стандарты, особенности PHP 8.4, best practices

Когда это работает лучше всего

  • Библиотеки с чёткой спецификацией (как роутер)

  • Проекты, где много шаблонного кода

  • Задачи с хорошо определёнными интерфейсами (PSR)

  • Написание тестов и документации

Когда стоит быть осторожным

  • Уникальная бизнес-логика

  • Код, зависящий от специфического контекста

  • Оптимизации производительности (нужны бенчмарки, а не интуиция ИИ)

  • Безопасность (всегда проверяйте вручную)


Попробовать

composer require ascetic-soft/waypoint

Код на GitHub: ascetic-soft/Waypoint


Вопросы, критика, идеи — welcome в комментариях. Если вы тоже создавали проекты с помощью ИИ — делитесь опытом, интересно сравнить подходы.

Комментарии (14)


  1. dali
    15.02.2026 14:21

    Посмотреть бы сколько исходного кода совпадает с теми же FastRoute, Symfony Routing, Laravel Router


    1. kotafey Автор
      15.02.2026 14:21

      Это наверное последнее, что меня волнует. Уверен что там даже если и найти где-то что-то схожее, всё равно не будет иметь никакого значения. Я этот роутер сделал исключительно ради алгоритма, которого нет больше нигде. По крайней мере в пхпшных роутерах.


      1. karrakoliko
        15.02.2026 14:21

        ради алгоритма, которого нет больше нигде. По крайней мере в пхпшных роутерах.

        ну, главное самому в это верить :)

        и в symfony и в fastroute группировка идет по trie-like структурам


        1. kotafey Автор
          15.02.2026 14:21

          Алгоритмы принципиально разные.


      1. wii
        15.02.2026 14:21

        Давайте попросим (извините, я уже попросил, это заняло у меня ~5.5 минут) ИИ проанализировать ваш код? Дальше я копирую ответы:

        ИИ смог сгенерировать такой код потому что:

        1. Это стандартный алгоритм.

        2. Архитектура маршрутизатора хорошо формализуема.

        3. Паттерн повторяется во множестве проектов.

        Это не «уникальный» алгоритм, такой подход используется десятилетиями в:

        1. HTTP-серверах

        2. веб-фреймворках

        3. сетевых маршрутизаторах

        4. парсерах языков

        Это общеизвестный паттерн. Он описан в книгах по:

        1. компиляторам

        2. структурам данных

        3. high-performance routing


        1. wii
          15.02.2026 14:21

          Именно эта часть скопирована из промпта:

          Проанализируй репозиторий и скажи, на каком фреймворке (или фреймворках) основан этот код.
          Как ИИ смог написать такой код, не опираясь на код из фреймворков?

          На что ответ был:

          Короткий ответ: потому что подобный роутер — это типовая архитектурная задача, а не уникальное решение конкретного фреймворка.

          Разберём профессионально и без мифологии.

          И дальше много текста, часть из которого в комментарии выше.


          1. kotafey Автор
            15.02.2026 14:21

            Уже зарекся не отвечать в комментариях, но на этот коммент отвечу. Как работает ИИ и как он отвечает на промпты - это можно узнать в свободном доступе, на ютюбе и много где. В кратце, ответы можно получить почти на всё, и вопросы с наведением будут выдавать результаты прямо по контексту в самом вопросе. Вы потрудитесь поставить вопрос иначе, чтобы получить именно нужный ответ. Вот Вам пример промпта:
            Что уникального в алгоритме роутинга этого проекта по сравнению с другими аналогичными роутерами?


  1. karrakoliko
    15.02.2026 14:21

    открыл routeCollection.

    ~5 публичных, ~15 приватных методов, без интерфейса, какие то hydrateIfNeeded(), getCachedCompactRoute()...

    очень плохо, классика php инженерии.

    написали быстро, вы молодец, а обслуживать это как?


    1. kotafey Автор
      15.02.2026 14:21

      Это не автомобиль, масло там менять не нужно. Вот по бенчмаркам я немного заморочился с оптимизацией, и то всё через разные модели. Наверное эту оптимизацию можно назвать "обслуживанием". Всё так-же, через ИИ. Сам я ни строчки кода не написал.


      1. karrakoliko
        15.02.2026 14:21

        Это не автомобиль, масло там менять не нужно.

        Всё так-же, через ИИ.

        production-ready компоненты которые мы заслужили :(


        1. kotafey Автор
          15.02.2026 14:21

          Пора уже смириться с новой реальностью.


  1. Dhwtj
    15.02.2026 14:21

    Надоели статьи "Как я написал <micro utility/lib name> за один вечер с помощью ИИ".

    Когда стоит быть осторожным

    • Уникальная бизнес-логика

    • Код, зависящий от специфического контекста

    • Оптимизации производительности (нужны бенчмарки, а не интуиция ИИ)

    • Безопасность (всегда проверяйте вручную)

    А остальные и не ценились никогда. А сейчас тем более


    1. kotafey Автор
      15.02.2026 14:21

      я там еще несколько либ сделал. Горшочек не вари...


  1. wii
    15.02.2026 14:21

    Ходил я как-то на собеседования, был там вопрос: как вы поймете, где проблема? Вопрос про то, как какой-то условный сайт начал выдавать 500. А ответ про то, куда нужно лезть в первую очередь, во вторую, и с чем это в целом может быть связано, и как этого не допустить в будущем.

    А как поступит автор, когда его код, допустим, начнет выдавать 500?