Skip to content

Kissous/attribute-router

Repository files navigation

attribute-router

The compiled PHP 8 attribute router.

Declare routes with #[Route] attributes on controller methods. They are scanned once and compiled into a static PHP dispatch table — zero runtime reflection, zero external dependencies, PSR-7 compatible.

A lightweight option for small projects that want attribute routing without pulling in a framework or a larger routing component.

  • Compiled — scanning and regex assembly happen once, at build time.
  • Zero runtime reflection — dispatch is array lookups plus preg_match.
  • Zero dependencies — only the PSR HTTP message interfaces.
  • Minimal API — three classes you actually touch: the attribute, the CLI, the dispatcher.

Requirements

  • PHP 8.3+
  • Any PSR-7 implementation for the request object (e.g. nyholm/psr7).

Install

composer require kissous/attribute-router

Quick start

1. Declare routes

use Kissous\AttributeRouter\Attribute\Route;

final class UserController
{
    #[Route('/users', method: 'GET', name: 'user.index')]
    public function index(): string
    {
        return 'all users';
    }

    #[Route('/users/{id}', method: 'GET', name: 'user.show')]
    public function show(string $id): string
    {
        return "user #{$id}";
    }
}

A parameter {id} matches a single path segment ([^/]+) and is passed to the method by name.

2. Compile once (build step)

vendor/bin/attribute-router compile --scan=src/Controller --out=var/routes.compiled.php

Pass --scan more than once to scan several directories. Run this in your build / deploy pipeline (and locally after changing routes); the generated file is a plain return [...] array that the dispatcher loads with require.

3. Dispatch at runtime

use Kissous\AttributeRouter\Dispatcher\Dispatcher;

$dispatcher = Dispatcher::fromCompiledFile(__DIR__ . '/var/routes.compiled.php');

$result = $dispatcher->dispatch($request); // any PSR-7 ServerRequestInterface

if ($result->isNotFound()) {
    http_response_code(404);
    return;
}

if ($result->isMethodNotAllowed()) {
    http_response_code(405);
    header('Allow: ' . implode(', ', $result->allowedMethods));
    return;
}

// Matched: $result->controller, $result->action, $result->parameters, $result->name
$controller = new ($result->controller)();
echo $controller->{$result->action}(...$result->parameters);

$result->parameters is keyed by parameter name, so spreading it (...) passes them as named arguments — order-independent and matching the method signature. Swap the new (...) line for your DI container when you have one.

The DispatchResult

Dispatcher::dispatch() returns an immutable DispatchResult:

Property Type Meaning
status DispatchStatus Found / NotFound / MethodNotAllowed
controller ?string matched controller FQCN (match only)
action ?string matched method name (match only)
parameters array<string,string> captured route parameters (match only)
name ?string route name, if declared (match only)
allowedMethods list<string> allowed methods (method-not-allowed only)

Helpers: isFound(), isNotFound(), isMethodNotAllowed().

How it works

scan  ──►  compile  ──►  dispatch

ClassScanner reads #[Route] attributes via reflection and emits ScannedRoute DTOs. Compiler turns them into a readable, closure-free PHP file (static paths in a direct-lookup map, dynamic paths as anchored regexes). Dispatcher loads that file and resolves a request with array lookups and preg_match only. All reflection is confined to the scanner and runs at build time. See docs/architecture.md.

FAQ

Does it use reflection at runtime? No. Reflection is confined to the scanner and runs only when you compile.

What dependencies does it pull in? Only the PSR HTTP message interfaces.

Do I have to recompile after changing routes? Yes — re-run the compile command. Wire it into your build step; in development, run it whenever routes change.

Can I commit the compiled file? You can, but it is typically a build artifact written to var/ and gitignored.

Is it a full framework / does it call my controller? No. It resolves a request to a controller, action and parameters; you instantiate and invoke (directly or through your container), keeping it framework-agnostic.

What about route constraints, #[Get] aliases, URL generation, middleware? Planned for later minor versions — see the roadmap in docs/roadmap/.

Lightweight by design

The design trades a build step for a cheap runtime: no reflection, no attribute parsing, no object graph — just a map lookup and, for dynamic routes, a single preg_match. The aim is a small, dependency-free router that is easy to drop into a small project, not a replacement for full-featured routers.

Development

composer install
composer test          # unit + integration
composer stan          # PHPStan (max level)
composer cs:check      # PHP-CS-Fixer dry-run

License

MIT — see LICENSE.

About

Lightweight compiled PHP 8 attribute router. Declare routes with #[Route], compile once, dispatch with zero runtime reflection and zero dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages