PHPackages                             waffle-commons/routing - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. waffle-commons/routing

ActiveLibrary[Framework](/categories/framework)

waffle-commons/routing
======================

Routing component for Waffle framework.

0.1.0-beta4(2w ago)1351MITPHPPHP ^8.5CI passing

Since Dec 16Pushed 2w agoCompare

[ Source](https://github.com/waffle-commons/routing)[ Packagist](https://packagist.org/packages/waffle-commons/routing)[ RSS](/packages/waffle-commons-routing/feed)WikiDiscussions main Synced today

READMEChangelog (9)Dependencies (47)Versions (26)Used By (1)

[![Discord](https://camo.githubusercontent.com/b30f41baece56d71f7f496f7e39fd33a2a096221c66c648b350dd4fe14276c2e/68747470733a2f2f696d672e736869656c64732e696f2f646973636f72642f3735353238383030313539323033333339313f6c6f676f3d646973636f7264)](https://discord.gg/eKgywnfXr2)[![PHP Version Require](https://camo.githubusercontent.com/48ec01022c6457fc3fe2359dc03617999bfbcbd0365055f328e34f404fddd4f9/687474703a2f2f706f7365722e707567782e6f72672f776166666c652d636f6d6d6f6e732f726f7574696e672f726571756972652f706870)](https://packagist.org/packages/waffle-commons/routing)[![PHP CI](https://github.com/waffle-commons/routing/actions/workflows/main.yml/badge.svg)](https://github.com/waffle-commons/routing/actions/workflows/main.yml)[![codecov](https://camo.githubusercontent.com/dc1ad96d4a7cb1d0d3702e8a3e30f85bdfbee09932a178aa3103fe8a4e878cb5/68747470733a2f2f636f6465636f762e696f2f67682f776166666c652d636f6d6d6f6e732f726f7574696e672f67726170682f62616467652e7376673f746f6b656e3d64373461633632612d373837322d343033352d386238622d626363336166313939316530)](https://codecov.io/gh/waffle-commons/routing)[![Latest Stable Version](https://camo.githubusercontent.com/77557448235117509a0cb1aaa048d6f902698d08e911a0dc8a142abd8c969b9f/687474703a2f2f706f7365722e707567782e6f72672f776166666c652d636f6d6d6f6e732f726f7574696e672f76)](https://packagist.org/packages/waffle-commons/routing)[![Latest Unstable Version](https://camo.githubusercontent.com/ce6c6f96731eab3bf9e26d4aec09ef61cd178cf1df4406353fdc15c492b800c3/687474703a2f2f706f7365722e707567782e6f72672f776166666c652d636f6d6d6f6e732f726f7574696e672f762f756e737461626c65)](https://packagist.org/packages/waffle-commons/routing)[![Total Downloads](https://camo.githubusercontent.com/f05847cbff98807036fa87b28d283005d7dcf80b3bbfb07dcfac6ae35a8bc246/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f776166666c652d636f6d6d6f6e732f726f7574696e672e737667)](https://packagist.org/packages/waffle-commons/routing)[![Packagist License](https://camo.githubusercontent.com/edddea98527c3afd8129fca9f1327b2826ffa791290b7e2a8c7c38495e576e01/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f776166666c652d636f6d6d6f6e732f726f7574696e67)](https://github.com/waffle-commons/routing/blob/main/LICENSE.md)

Waffle Routing Component
========================

[](#waffle-routing-component)

> **Release:** `0.1.0-beta4` | [`CHANGELOG.md`](./CHANGELOG.md)

Attribute-driven router. No YAML, no XML — routes live next to the controller code via the `#[Route]` attribute and are discovered by scanning the configured controller directory at boot time. The compiled route table is then cached.

🆕 Beta-2 highlights — HTTP method correctness
---------------------------------------------

[](#-beta-2-highlights--http-method-correctness)

- **HTTP method filtering &amp; route overloading.** `#[Route(methods: ['GET', 'POST'])]` constrains a route to specific verbs. Multiple controller actions may share one path provided their methods arrays don't intersect — e.g. a `GET` and `POST` handler for `/articles`. Method names are canonicalised (upper-case) and de-duplicated at discovery, so typos like `methods: ['get']` are caught at boot, not at runtime.
- **`HEAD ⇒ GET` fallback** (RFC 7231 §4.3.2). A request with method `HEAD` matches a `GET` route automatically.
- **`OPTIONS` auto-answer.** When `Waffle\Commons\Pipeline\CoreRoutingMiddleware` is wired with a PSR-17 `ResponseFactoryInterface`, an `OPTIONS` request to a known path is answered with `204 No Content` + `Allow` header — no controller dispatch required.
- **Deterministic `Allow` header.** When raising `MethodNotAllowedException` (HTTP `405`), the router merges declared methods, auto-augments with `HEAD` (if `GET` is allowed) and `OPTIONS`, deduplicates, and **alphabetically sorts** the resulting list — e.g. `Allow: GET, HEAD, OPTIONS, POST`. The error renderer (`waffle-commons/error-handler`) copies this verbatim.
- **`#[Route]` attribute relocation.** The canonical attribute now lives at `Waffle\Commons\Contracts\Routing\Attribute\Route` (in the `contracts` package). The old `Waffle\Commons\Routing\Attribute\Route` has been removed — `use Waffle\Commons\Contracts\Routing\Attribute\Route;` everywhere.
- **Worker-safe PCRE cache.** PCRE patterns compiled from route templates are memoised in resident-worker memory and survive across requests for the worker's lifetime.

Beta-1 inheritance
------------------

[](#beta-1-inheritance)

- **Priority routing &amp; catch-all.** `#[Route]` takes an `int $priority = 0`. At boot the router sorts the compiled table by **descending** priority, so high-priority specific routes are tried before low-priority ones. Negative priorities (e.g. `-1000`) flag catch-all routes like `/{path:.*}` that should only match once every specific route has failed — the foundation for Strangler-Fig / API-gateway proxying.
- **`MatchedRoute` DTO.** `matchRequest()` returns a typed `Waffle\Commons\Contracts\Routing\MatchedRoute` (or `null`) instead of a loose array.
- **Reflection cleanup.** Discovery reads `#[Route]` via native `ReflectionClass`/`ReflectionMethod::getAttributes()` — the old `ReflectionTrait` was removed in Beta-1.

📦 Installation
--------------

[](#-installation)

```
composer require waffle-commons/routing
```

🧱 Surface
---------

[](#-surface)

ClassRole`Waffle\Commons\Routing\Router``RouterInterface` implementation. Boots from a `ContainerInterface`, sorts the table by descending `priority`, and returns a `?MatchedRoute` from `matchRequest()`.`Waffle\Commons\Routing\RouteDiscoverer`Walks the controller directory and builds a `list` from each controller's `#[Route]` attributes.`Waffle\Commons\Routing\ControllerFinder`Filesystem traversal of `*.php` controller files.`Waffle\Commons\Routing\RouteParser`Builds a `MatchedRoute` from a `Route` attribute + native reflection metadata (`ReflectionClass`/`ReflectionMethod`). A method-level `priority` overrides the class-level default.`Waffle\Commons\Contracts\Routing\Attribute\Route`The `#[Route(path, methods, name, arguments, priority)]` attribute (defined in `contracts`).`Waffle\Commons\Routing\Attribute\Argument`The `#[Argument(classType, paramName, required)]` attribute for routes that auto-resolve container services.`Waffle\Commons\Routing\Trait\RequestTrait`Helpers for extracting routing data from a PSR-7 request (`_controller`, `_route_params`).🚀 `#[Route]` attribute — exact signature
----------------------------------------

[](#-route-attribute--exact-signature)

From `Waffle\Commons\Contracts\Routing\Attribute\Route` (the attribute lives in `contracts`, so every component reads the same definition):

```
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final readonly class Route
{
    /**
     * @param array        $methods   Allowed HTTP methods. Default ['GET']; empty [] accepts any.
     * @param array|null $arguments
     * @param int                  $priority Higher matches first. Use negative
     *                                       values (e.g. -1000) for catch-all routes.
     */
    public function __construct(
        public string $path,
        public array $methods = ['GET'],
        public ?string $name = null,
        public ?array $arguments = null,
        public int $priority = 0,
    ) {}
}
```

`Argument` (from `src/Attribute/Argument.php`):

```
#[Attribute]
final class Argument
{
    public function __construct(
        public string $classType,
        public string $paramName,
        public bool $required = true,
    ) {}
}
```

🚀 Declaring routes on a controller
----------------------------------

[](#-declaring-routes-on-a-controller)

```
use Waffle\Commons\Contracts\Routing\Attribute\Route;
use Waffle\Commons\Routing\Attribute\Argument;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class UserController
{
    #[Route(path: '/users/{id}', name: 'users.show')]
    public function show(ServerRequestInterface $request): ResponseInterface
    {
        // The {id} placeholder is exposed via $request->getAttribute('_route_params')['id']
    }

    #[Route(
        path: '/users',
        name: 'users.create',
        arguments: [
            new Argument(classType: UserRepository::class, paramName: 'repo'),
        ],
    )]
    public function create(ServerRequestInterface $request, UserRepository $repo): ResponseInterface
    {
        // $repo is auto-injected via the container.
    }
}
```

🔎 Matching a request
--------------------

[](#-matching-a-request)

```
use Waffle\Commons\Routing\Router;
use Waffle\Commons\Container\Container;

$router = new Router(/* …discoverer, cache… */);
$router->boot($container);

$match = $router->matchRequest($psr7Request);
/* @var \Waffle\Commons\Contracts\Routing\MatchedRoute|null $match
 *
 * MatchedRoute exposes: className, method, arguments, path, name, params, priority.
 * Use $match->withParams([...]) to attach the captured path parameters immutably.
 */
```

`null` means no route matched — the kernel converts that into `RouteNotFoundException` (which the error handler renders as RFC 7807 `404`). Routes are evaluated in descending `priority` order, so a catch-all (`priority: -1000`) is only reached after every specific route has failed.

🔀 HTTP method filtering
-----------------------

[](#-http-method-filtering)

```
use Waffle\Commons\Contracts\Routing\Attribute\Route;

final class ArticleController
{
    #[Route(path: '/articles', methods: ['GET'], name: 'articles.list')]
    public function list(): ResponseInterface { /* … */ }

    #[Route(path: '/articles', methods: ['POST'], name: 'articles.create')]
    public function create(): ResponseInterface { /* … */ }
}
```

- Matching is **case-insensitive**; methods are upper-cased + de-duplicated at discovery.
- A `GET` route also answers `HEAD` (RFC 7231 §4.3.2).
- `OPTIONS` to a known path is auto-answered `204` + `Allow` when `CoreRoutingMiddleware` is wired with a PSR-17 response factory.
- A method mismatch throws `MethodNotAllowedException` (`405`); its `Allow` header is merged across candidates, augmented (`HEAD` if `GET`, always `OPTIONS`), de-duplicated, and **sorted** — e.g. `Allow: GET, HEAD, OPTIONS, POST`.

🐘 PHP 8.5 features used
-----------------------

[](#-php-85-features-used)

- `#[Route]`, `#[Argument]` — PHP 8 attribute syntax with PHP 8.5 typed properties.
- `final` classes throughout.
- Strongly-typed `MatchedRoute` DTO return from `RouterInterface::matchRequest()` (no loose arrays).
- Native reflection (`ReflectionClass` / `ReflectionMethod::getAttributes()`) to read `#[Route]` — the old `ReflectionTrait` was removed in Beta-1.

🧭 Architectural boundary (`mago guard`)
---------------------------------------

[](#-architectural-boundary-mago-guard)

An active dependency **perimeter** is enforced on every CI run by `vendor/bin/mago guard` (bundled into `composer mago`; zero baselines). The rules live in [`mago.toml`](./mago.toml) under `[guard.perimeter]` — a forbidden `use` statement fails the build, not a reviewer.

Production code under `Waffle\Commons\Routing` may depend **only** on:

- `Waffle\Commons\Routing\**` — itself
- `Waffle\Commons\Contracts\**` — the shared contracts package (where `#[Route]`, `MatchedRoute`, and `MethodNotAllowedException` now live)
- `Waffle\Commons\Utils\**` — the `ClassParser` controller-discovery helper
- `Psr\**` — PSR interfaces (PSR-7)
- `@global` + `Psl\**` — PHP core and the PHP Standard Library

Test code under `WaffleTests\Commons\Routing` is unrestricted (`@all`). Structural rules are guarded too: interfaces must be named `*Interface`, `Exception\**` classes must end in `*Exception`, and any `Enum\**` namespace may hold only `enum` declarations.

Contract-first, component-agnostic by construction: components compose through `waffle-commons/contracts` (plus the explicitly-permitted `utils`), never ad-hoc through one another.

🧪 Testing
---------

[](#-testing)

```
docker exec -w /waffle-commons/routing waffle-dev composer tests
```

📄 License
---------

[](#-license)

MIT — see [LICENSE.md](./LICENSE.md).

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance96

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community13

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~22 days

Recently: every ~5 days

Total

9

Last Release

20d ago

PHP version history (2 changes)0.1.0-alpha3PHP ^8.4

0.1.0-alpha4PHP ^8.5

### Community

Maintainers

![](https://www.gravatar.com/avatar/34a7557a3fb23aaf788ca3892b9b7efdf96e753264bafd0599153c9e8a921316?d=identicon)[LesliePetrimaux](/maintainers/LesliePetrimaux)

---

Top Contributors

[![supa-chayajin](https://avatars.githubusercontent.com/u/695448?v=4)](https://github.com/supa-chayajin "supa-chayajin (74 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPsalm

Type Coverage Yes

### Embed Badge

![Health badge](/badges/waffle-commons-routing/health.svg)

```
[![Health](https://phpackages.com/badges/waffle-commons-routing/health.svg)](https://phpackages.com/packages/waffle-commons-routing)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k87.2M2.2k](/packages/symfony-symfony)[aws/aws-sdk-php

AWS SDK for PHP - Use Amazon Web Services in your PHP project

6.3k543.5M2.6k](/packages/aws-aws-sdk-php)[neuron-core/neuron-ai

The PHP Agentic Framework.

2.0k656.1k38](/packages/neuron-core-neuron-ai)[bref/bref

Bref is a framework to write and deploy serverless PHP applications on AWS Lambda.

3.4k10.6M67](/packages/bref-bref)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k15](/packages/tempest-framework)[aimeos/aimeos-core

Full-featured e-commerce components for high performance online shops

4.5k361.1k75](/packages/aimeos-aimeos-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
