phpsoftbox / router
Router component for the PhpSoftBox framework
Requires
- php: ^8.4
- psr/container: ^2.0
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- psr/simple-cache: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.93
- phpsoftbox/cli-app: dev-master
- phpsoftbox/cs-fixer: ^1.1.0
- phpsoftbox/http-message: dev-master
- phpsoftbox/request: dev-master
- phpsoftbox/validator: dev-master
- phpunit/phpunit: ^11.2
This package is auto-updated.
Last update: 2026-04-02 13:55:59 UTC
README
Лёгкий роутер PhpSoftBox позволяет регистрировать маршруты, группировать их, навешивать middleware, валидировать параметры и генерировать URL по имени маршрута.
Состав пакета:
PhpSoftBox\Router\RouteCollector— регистрация маршрутов, групп, ресурсных маршрутов, глобальных middleware.PhpSoftBox\Router\RouteResolver— поиск подходящего маршрута по PSR-7 запросу.PhpSoftBox\Router\Dispatcher— исполнение обработчика маршрута с учётом middleware.PhpSoftBox\Router\Router— PSR-15 RequestHandler (resolve + dispatch).PhpSoftBox\Router\UrlGenerator— генерация URL по имени маршрута.PhpSoftBox\Router\RequestContext— контекст запроса для генерации абсолютных URL.PhpSoftBox\Router\Handler\ContainerHandlerResolver— резолвер обработчиков через DI-контейнер (PSR-11).PhpSoftBox\Router\ParamTypesEnum— встроенные валидаторы параметров маршрута.
Мы используем собственные реализации PSR-7/PSR-17 (PhpSoftBox\Http\Message\*).
Быстрый старт
use PhpSoftBox\Http\Message\Response; use PhpSoftBox\Http\Message\ServerRequest; use PhpSoftBox\Http\Message\Uri; use PhpSoftBox\Router\{RouteCollector, RouteResolver, Dispatcher, Router, UrlGenerator}; $routes = new RouteCollector(); $routes->get('/hello', fn($r) => new Response(200, [], 'OK'))->name('hello'); $routes->get('/users/{id}/{extra?}', [UserController::class, 'show'])->name('user.show'); $router = new Router(new RouteResolver($routes), new Dispatcher(), $routes); // Обработка входящего запроса $request = new ServerRequest('GET', new Uri('https://example.com/hello')); $response = $router->handle($request); // 200 OK // Генерация URL по имени $urlGenerator = new UrlGenerator($routes, $request); $url = $urlGenerator->generate('user.show', ['id' => 42]); // "/users/42"
Регистрация маршрутов
get(post|put|delete|patch|head|options|any)(string $path, callable|array|string $handler): RouteBuildergroup(callable $callback): RouteGroupBuilderimport(string $pathWithoutExtension): callableimportGroup(string $pathWithoutExtension): RouteGroupBuilderresource(string $path, string $controller): ResourceBuilderaddControllerMiddleware(string $controller, array $middlewares, array $only = [], array $except = [])
В middleware можно передавать как экземпляры, так и строки (alias, группа или class‑string).
Middleware для контроллеров
$routes->addControllerMiddleware(UserController::class, ['auth']); $routes->addControllerMiddleware(UserController::class, ['admin'], only: ['store', 'update']);
Рекомендуется вешать middleware на маршруты или группы; контроллеры/экшены — для точечных случаев.
Примеры
$rc = new RouteCollector(); // Обычные маршруты $rc->get('/posts', [PostController::class, 'index'])->name('posts.index'); $rc->get('/posts/{slug}', [PostController::class, 'show'])->name('posts.show'); $rc->post('/posts', [PostController::class, 'store'])->name('posts.store'); // Invokable-класс $rc->get('/ping', PingAction::class); // Опциональные параметры: сегмент "/{slug?}" можно опустить $rc->get('/blog/{slug?}', [BlogController::class, 'show']); // Ограничение по хосту и метод ANY $rc->any('/internal/ping', [SysController::class, 'ping'])->host('api.example.com'); // Именованные маршруты $rc->get('/u/{id}', [UserController::class, 'show'])->name('users.show');
Группы и middleware
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Message\ServerRequestInterface; use PhpSoftBox\Http\Message\Response; $rc = new RouteCollector(); // Глобальный middleware $rc->addMiddleware(new class implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): \Psr\Http\Message\ResponseInterface { return $handler->handle($request); } }); // Группа с префиксом и своим набором middleware $rc->group(function (RouteCollector $r) { $r->get('/users', fn($r) => new Response(200)); }) ->prefix('/api') ->middlewares([new class implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): \Psr\Http\Message\ResponseInterface { return $handler->handle($request); } }]) ->apply();
Resource (CRUD) маршруты
use PhpSoftBox\Auth\Middleware\AuthMiddleware; use PhpSoftBox\Session\CsrfMiddleware; $rc->resource('/users', UserController::class) ->except([]) ->middlewares([new AuthMiddleware()]) ->routeMiddlewares([ 'store' => [new CsrfMiddleware()], 'update' => [new CsrfMiddleware()], ]) ->namePrefix('users') ->appendRestoreMethod() ->routeParameter('user') ->apply(); // Сгенерирует: GET /users (users.index), GET /users/{user} (users.show), POST /users (users.store), // GET /users/create (users.create), GET /users/{user}/edit (users.edit), // PUT /users/{user} (users.update), DELETE /users/{user} (users.destroy), // POST /users/{user}/restore (users.restore)
Автонейминг маршрутов
Если имя не задано явно, оно генерируется автоматически по пути и методу.
Примеры:
GET / -> root.index
GET /health -> health.index
GET /status/ping -> status.ping.index
POST /status/ping -> status.ping.store
GET /users -> users.index
POST /users -> users.store
GET /users/{id} -> users.show
PUT /users/{id} -> users.update
DELETE /users/{id} -> users.destroy
POST /api/accounts/{id}/refresh -> api.accounts.by-id.refresh.store
GET /api/users -> api.users.index
GET /api/crm/orders/{id} -> api.crm.orders.show
Конфликты
- Если два маршрута получают одинаковое имя, выбрасывается исключение.
- Например:
GET /usersиGET /users/indexоба дадутusers.index.
Как избежать конфликтов
- Используйте
group(...)->prefix('/api')->apply()— префикс группы попадёт в имя (api.users.index). - Задавайте
nameявно для нестандартных маршрутов (health,metrics). - Для
resource(...)задавайтеnamePrefix, чтобы контролировать неймспейс.
Можно передавать строки вместо экземпляров middleware, если диспетчер умеет их резолвить:
use PhpSoftBox\Application\Middleware\KernelRouteMiddlewareResolver; use PhpSoftBox\Router\Dispatcher; $dispatcher = new Dispatcher( handlerResolver: null, middlewareResolver: new KernelRouteMiddlewareResolver($kernel->middlewareManager(), $container), );
Валидация параметров
use PhpSoftBox\Router\ParamTypesEnum as T; $rc->get('/users/{id}', [UserController::class, 'show'])->validators(['id' => T::INT]); $rc->get('/posts/{slug}', [PostController::class, 'show'])->validators([ 'slug' => fn(string $v) => preg_match('~^[a-z0-9-]+$~', $v) === 1, ]);
Поведение
- Несоответствие валидатору бросает
InvalidRouteParameterException(сообщение включает имя параметра). - Несоответствие пути/метода/хоста ведёт к отсутствию маршрута (404) или
MethodNotAllowedException(405).
Генерация URL: UrlGenerator::generate()
$urlGenerator->generate('users.show', ['id' => 10]); // "/u/10" $urlGenerator->generate('user.show', ['id' => 42]); // "/users/42" $urlGenerator->generate('user.show', ['id' => 42, 'x' => 1]); // лишние параметры игнорируются $urlGenerator->generate('user.show', ['id' => 42, 'extra' => 'q']); // для "/users/{id}/{extra?}" => "/users/42/q" $urlGenerator->generate('user.show', ['id' => 42], true); // "https://example.com/users/42" // Можно передавать ORM-сущность (EntityInterface): будет подставлен primary key (id()). $urlGenerator->generate('user.show', ['user' => $userEntity]); // "/users/42"
Настройка контекста запроса:
use PhpSoftBox\Router\RequestContext; use PhpSoftBox\Router\UrlGenerator; $context = RequestContext::fromRequest($request); $context->setHost('tenant.example.com'); $context->setScheme('https'); $urlGenerator = new UrlGenerator($routes, context: $context);
Правила
- Обязательные плейсхолдеры
{param}должны быть предоставлены — иначеRouteNotFoundException. - Опциональные сегменты
/{param?}удаляются, если параметр не передан. - В пути нормализуются повторяющиеся слеши, завершающий слеш отбрасывается (кроме корня).
- Третий аргумент
bool $shouldAbsolute = falseвключает абсолютный URL. - Источник host/scheme:
RequestContext. - Если
RequestContextпустой, используетсяhostмаршрута (fallback).
Данные маршрута в request attributes
id/slugи прочие параметры маршрута кладутся вattributes.- Дополнительно доступны
_route(имя или путь маршрута) и_route_params(все параметры).
Авто‑резолв сущностей (EntityInterface)
Если параметр контроллера типизирован сущностью ORM (EntityInterface), контейнер автоматически подгружает сущность по параметру маршрута, имя которого совпадает с именем аргумента:
use App\Entity\User\User; // маршрут: GET /users/{user} public function show(User $user): ResponseInterface { // $user уже загружен из ORM }
По умолчанию используется EntityManagerInterface::find(). Чтобы разрешать soft‑deleted записи, можно отметить параметр атрибутом #[WithDeleted]:
use PhpSoftBox\Router\Attributes\WithDeleted; public function show(#[WithDeleted] User $user): ResponseInterface { }
Scoped bindings (проверка связей между сущностями)
Чтобы автоматически проверять связь между вложенными сущностями, используйте scopeBindings():
$routes->scopeBindings(function (RouteCollector $routes): void { $routes->get('/users/{user}/companies/{company}', [UserCompaniesController::class, 'show']); });
При включённом scopeBindings сначала резолвятся все сущности, затем проверяется их связь по цепочке (parent → child). Если связь не найдена — выбрасывается InvalidRouteParameterException (404).
Поддерживаемые типы связей: many_to_one, has_one, has_many, belongs_to_many, has_many_through, morph_many, morph_to.
По умолчанию проверка пытается использовать ORM‑метаданные связей. Для кастомной логики можно зарегистрировать свой resolver:
use PhpSoftBox\Router\Binding\ScopedBindingsResolverInterface; final class AppScopedBindingsResolver implements ScopedBindingsResolverInterface { public function supports(object $parent, object $child, array $context = []): bool { // определить, поддерживается ли пара сущностей } public function isScoped(object $parent, object $child, array $context = []): bool { // вернуть true, если сущности действительно связаны } }
Интеграция
Routerреализует PSR-15RequestHandlerInterfaceи работает с нашими PSR-7/17 реализациями (PhpSoftBox\Http\Message\*).- Для DI можно прокидывать
RouteCollectorвRouteResolver, затем собратьRouter:
$routes = new RouteCollector(); $resolver = new RouteResolver($routes); $router = new Router($resolver, new Dispatcher(), $routes);
Пример DI-резолвера обработчиков (PSR-11)
use PhpSoftBox\Router\Handler\ContainerHandlerResolver; use PhpSoftBox\Router\Dispatcher; $dispatcher = new Dispatcher(new ContainerHandlerResolver($container)); $router = new Router(new RouteResolver($routes), $dispatcher, $routes);
Если контейнер поддерживает call() (например PHP-DI), он будет использован для инъекций в методы.
CLI
router:list router:cache router:cache-clear
Кеш маршрутов
Кеш сохраняется через CacheInterface. В кеш попадают только обработчики
в виде Class::method или invokable‑класс, а middleware должны быть строками
(alias, группа или class‑string). Замыкания и кастомные валидаторы не поддерживаются.
use PhpSoftBox\Router\Cache\RouteCache; use PhpSoftBox\Router\RouteCollector; $collector = new RouteCollector(); $collector->get('/users/{id}', [UserController::class, 'show'])->validators(['id' => ParamTypesEnum::INT]); $cache = new RouteCache($cacheStorage); $cache->dump($collector, 'dev'); $routes = $cache->load('dev');