Search by

wilycode / router-attribute

szymonpietrek-design

PHP 8.4 attribute-based route discovery and caching for WilyCode Router.

Package info

github.com/wilycode/router-attribute

pkg:composer/wilycode/router-attribute

Statistics

Installs: 1

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 0

v0.1.0 2026-10-11 23:43 UTC

This package is auto-updated.

Last update: 2026-10-12 00:02:07 UTC


README

PHP 8.4+ License: MIT Tests

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, and string arguments.
  • 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

  1. Pass one or more controller directories to AttributeRouteCollection.
  2. On first load, it recursively scans files whose names end in Controller.php.
  3. It reflects the discovered classes and their route attributes.
  4. It creates native WilyCode\Router\Route objects 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.