wilycode / router-attribute
PHP 8.4 attribute-based route discovery and caching for WilyCode Router.
Requires
- php: ^8.4
- psr/simple-cache: ^3.0
- wilycode/router: ^0.1
Requires (Dev)
- phpunit/phpunit: ^11.0
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-12 00:02:07 UTC
README
Define HTTP routes directly on controller methods using PHP attributes.
WilyCode Router Attribute discovers your controllers, reads attributes such as
#[Get] and #[Post], and creates route definitions for
WilyCode Router.
This package is an optional extension. The core Router works without it, and neither package requires a profiler.
Features
- HTTP method attributes:
#[Get],#[Post],#[Put],#[Patch],#[Delete],#[Head],#[Options], and#[Any]. - Generic
#[Route]for registering one or more HTTP methods. - Controller-level
#[RouteGroup]for shared paths, route names, and middleware. - Named routes, path parameters (
{id}), and optional segments ([/{page}]). - Automatic parameter rules for
int,float,bool, andstringarguments. - Explicit parameter rules, defaults, middleware, and route priority.
- Optional PSR-16 discovery cache, with configurable source checking.
Requirements
- PHP 8.4+.
wilycode/router^0.1.psr/simple-cache^3.0 (PSR-16 interfaces). A cache implementation is not required unless you enable caching.- A PSR-11 container to resolve controller handlers, plus your application's PSR-7 request/response implementation.
Installation
Once this package is available on Packagist:
composer require wilycode/router-attribute:^0.1
Composer also installs the required core wilycode/router package. For local
library development before Packagist publication, you can use a Composer path
repository or a VCS repository.
Quick start
1. Add attributes to a controller
Create src/Controller/UserController.php:
<?php declare(strict_types=1); namespace App\Controller; use Nyholm\Psr7\Response; use Psr\Http\Message\ResponseInterface; use WilyCode\RouterAttribute\Attribute\Get; use WilyCode\RouterAttribute\Attribute\RouteGroup; #[RouteGroup(prefix: '/api', namePrefix: 'api.')] final class UserController { #[Get(path: '/users/{id}', name: 'users.show')] public function show(int $id): ResponseInterface { return new Response(200, ['Content-Type' => 'text/plain'], 'User ' . $id); } }
Here, int $id tells the attribute collection to apply the core Router's
integer parameter rule. The example uses nyholm/psr7 to create a response;
install it with composer require nyholm/psr7 or use another PSR-7 response
implementation.
2. Register controller discovery
use WilyCode\Router\Router; use WilyCode\RouterAttribute\AttributeRouteCollection; // $container is your application's PSR-11 container. // Controller classes must be loadable and resolvable by your application. $routes = new AttributeRouteCollection([ __DIR__ . '/src/Controller', ]); $router = new Router($container); $router->addCollection($routes);
3. Dispatch a request
// $request is your application's PSR-7 ServerRequestInterface instance. $response = $router->dispatch($request);
The controller above registers:
| HTTP method | Path | Route name |
|---|---|---|
GET |
/api/users/{id} |
api.users.show |
For GET /api/users/42, the Router matches the route and passes the validated
integer 42 to UserController::show(). Your application remains responsible
for creating the request and emitting the response.
Route attributes
Use a method-specific attribute for the common case:
#[Get(path: '/articles', name: 'articles.list')] #[Post(path: '/articles', name: 'articles.create')]
For multiple methods using one route definition, use #[Route]:
use WilyCode\Router\HttpMethod; use WilyCode\RouterAttribute\Attribute\Route; #[Route(methods: [HttpMethod::Get, HttpMethod::Post], path: '/search', name: 'search')]
#[Any] registers a route for every HTTP method supported by the core Router.
A method may have multiple route attributes.
Parameters and defaults
The collection infers rules from typed controller parameters when those names appear in the route path:
#[Get(path: '/articles[/{page}]', name: 'articles.list')] public function list(int $page = 1): ResponseInterface { return new \Nyholm\Psr7\Response(200, [], 'Page ' . $page); }
This declares /articles and /articles/2, with page = 1 as the default.
You can override inferred rules and defaults explicitly:
use WilyCode\Router\Rule\IntRule; #[Get( path: '/articles/{id}', name: 'articles.show', rules: ['id' => IntRule::class], )]
Route parameters are validated and cast by the core Router. Route handlers must
return a Psr\Http\Message\ResponseInterface when they are executed.
Route groups and middleware
Use #[RouteGroup] to share a path prefix, route-name prefix, and middleware
across controller methods:
use WilyCode\RouterAttribute\Attribute\Get; use WilyCode\RouterAttribute\Attribute\RouteGroup; #[RouteGroup( prefix: '/admin', namePrefix: 'admin.', middlewares: [AuthMiddleware::class], )] final class ArticleController { #[Get(path: '/articles/{id}', name: 'articles.show')] public function show(int $id): ResponseInterface { return new \Nyholm\Psr7\Response(200, [], 'Article ' . $id); } }
This registers GET /admin/articles/{id} as admin.articles.show.
AuthMiddleware is a PSR-15 middleware class from your application. Group
middleware is placed before route-specific middleware. You can also set a
collection-level prefix with $routes->prefix('/v1'), yielding
/v1/admin/articles/{id} in this example.
Each attribute also supports middlewares, rules, defaults, and priority
where applicable.
How discovery works
- Pass one or more controller directories to
AttributeRouteCollection. - On first load, it recursively scans files whose names end in
Controller.php. - It reflects the discovered classes and their route attributes.
- It creates native
WilyCode\Router\Routeobjects and hands them to the core Router for matching, middleware execution, and dispatch.
Controllers should be available through Composer autoloading or loadable from
the scanned PHP files. Discovery is lazy: it runs when the route collection is
loaded, not when you construct AttributeRouteCollection.
Optional route cache
Discovery works without any cache implementation. If you want to cache the resulting route collection, supply a PSR-16 cache:
use WilyCode\RouterAttribute\AttributeRouteCollection; $routes = new AttributeRouteCollection( paths: [__DIR__ . '/src/Controller'], cache: $cache, // Your Psr\SimpleCache\CacheInterface implementation. cacheKey: 'app.routes', // Optional; defaults to a hash of the paths. cacheTtl: null, // Optional: int, DateInterval, or null. autoReload: true, );
| Setting | Behavior when loading a new collection |
|---|---|
No cache |
Discovers routes without reading or writing a cache. |
autoReload: true |
Checks the cached file/directory snapshot and rediscover routes if it changed. Useful during development. |
autoReload: false |
Reuses a valid cached collection without checking source files. Useful in production with controlled invalidation. |
Production note: deploy with a fresh cache namespace or explicitly clear the route cache whenever controller files or attributes change. The cache contains route objects, so choose a cache adapter that can store and restore those objects correctly. Once an individual collection instance has loaded routes, it does not rescan them on every request.
Related packages
| Package | Role |
|---|---|
wilycode/router |
Required core router: matching, middleware, dispatch, URL generation. |
wilycode/router-profiler |
Optional timing and diagnostics for the core Router. |
router-attribute does not depend on router-profiler.
Development
composer install
composer validate --strict
composer test
PHPUnit tests run in GitHub Actions on pushes and pull requests. Test sources
are in the Git repository but omitted from standard Composer distribution
archives. The repository does not commit vendor/ or composer.lock.
Contributing
Bug reports and pull requests are welcome. See CONTRIBUTING.md and GitHub Issues.
License
MIT — copyright © 2026 Simon Piter. See LICENSE.