PHPackages                             hengeb/router - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. hengeb/router

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

hengeb/router
=============

a PHP router that makes use of attributes

v0.11(1w ago)0246↓71.9%MITPHPPHP &gt;=8.4.0

Since Dec 1Pushed 1w ago1 watchersCompare

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

READMEChangelog (10)Dependencies (2)Versions (22)Used By (0)

Hengeb\\Router
==============

[](#hengebrouter)

a PHP router that makes use of attributes and provides autowiring

example usage
-------------

[](#example-usage)

Make sure your HTTP server redirects alle requests to `index.php` (the front controller).

example nginx directive:

```
...
location / {
    try_files $uri $uri/ /index.php$is_args$args;
}
...

```

In `index.php`, create the Router object and dispatch the request:

```
use App\Service\Router\Router;
use Symfony\Component\HttpFoundation\Request;

require_once '../vendor/autoload.php';

// create the Router object and pass it the directory with your controllers (will also look in subdirectories)
$router = new Router(__DIR__ . '/../Classes/Controller');
$request = Request::createFromGlobals();
$router->dispatch($request)->send();
```

The router will look for a controller with a Route attribute that matches the router. The router will cache the routes and only re-analyze the controllers when a file has changed.

```
use Hengeb\Router\Attribute\Route;
use Hengeb\Router\Attribute\AllowIf;
use Hengeb\Router\Attribute\Inject;
use Hengeb\Router\Attribute\PublicAccess;
use Hengeb\Router\Attribute\RequestValue;
use Hengeb\Router\Attribute\RequireLogin;
use Hengeb\Router\Interface\CurrentUserInterface;
use Hengeb\Db\Db;
use Symfony\Component\HttpFoundation\Response;

class MyController extends Controller {
    // the router will inject the TemplateEngine service directly after it created the object
    #[Inject]
    public TemplateEngine $templates;

    // the router will inject the Logger service when it creates the object
    public function __construct(
        private Logger $logger,
    ) {}

    // the route matcher starts with the HTTP method, followed by the path. You can use some regular expressions to some extend
    // do not use the ? symbol because this separates the query from the path)
    // this will match /search and / and also /search/ and also /search?foo=bar but not /search?q=theQuery because of the next route
    // note: one of the Access attributes is mandatory
    #[Route('GET /(search|)'), PublicAccess]
    public function form(): Response {
        return $this->render('SearchController/search');
    }

    // you can use identifiers in the path or in the query, they will be passed as arguments and casted to the desired type
    // the Db object will be injected (in this case via the Db::getInstance() method)
    #[Route('GET /(search|)?q={query}', PublicAccess]
    public function search(string $query, Db $db): Response {
        $sql = $this->buildQuery($query);
        $results = $db->query($sql);
        return $this->showResults($ids);
    }

    // access control: only allow logged-in users
    // the dispatch method needs a second argument that implements the CurrentUserInterface
    #[Route('GET /users'), RequireLogin]
    public function users(): Response {
        ...
    }

    #[Route('GET /group/{group}'), RequireLogin]
    // the group object will be fetched from the database and injected
    public function show(Group $group): Response {
        ...
    }

    // you can have multiple route matchers, make sure they fit together
    // \d+ regex for number, get the user object by its id
    #[Route('GET /user/{\d+:id=>user}'), RequireLogin]
    // if the above matcher does not fit, get the user by its username
    #[Route('GET /user/{username=>user}'), RequireLogin]
    // the user object will be fetched from the database and injected
    public function show(User $user): Response {
        ...
    }

    // access control: only give access if the current user has the admin role OR is the user himself/herself
    // this will look for a method like hasRole, getRole, isRole, role, get("role") and so on
    // the '$user->get("id")' is a template string that will be evaluated. This does only allow simple function calls because the string will be parsed.
    #[Route('GET /user/{username=>user}/edit'), AllowIf(role: 'admin'), AllowIf(id: '$user->get("id")')]
    public function edit(User $user): Response {
        ...
    }

    // this route will only match if the method is POST
    // this will also check for a POST variable _csrfToken and validate it to prevent CSRF attacks
    // you can override this setting the CheckCsrfToken(false) attribute
    // generate the CSRF token with $router->createCsrfToken()
    // $username and $password will be injected from the request body
    #[Route('POST /login'), PublicAccess]
    public function login(#[RequestValue] string $username, #[RequestValue] string $password) {

    }

    // you can inject the router object, the request, the current user
    #[Route('GET /foo'), PublicAccess]
    public function foo(Request $request, CurrentUserInterface $user, Router $router) {
        ...
    }
}
```

Model injection
---------------

[](#model-injection)

You can tell the Router object how to retrieve an object of a given type:

```
$router->addType(User::class, fn($id) => User::find($id), 'id')
```

The third parameter is optional. Use it if there are multiple ways to find the target.

Alternatively your model can implement the RetrievabelModel interface that is part of this package.

```
class User implements RetrievableModel {
    public function retrieveModel($userId): ?static {
        return UserRepository::getInstance()->findOneById((int)$userId);
    }
}
```

Service injection
-----------------

[](#service-injection)

You can tell the Router how to retrieve a service object:

```
$templateEngine = new TemplateEngine();
$router->addService(TemplateEngine::class, $templateEngine);
```

Or using a closure so objects will only be created when they are needed:

```
$router->addService(TemplateEngine::class, fn() => new TemplateEngine());

```

Exception handling
------------------

[](#exception-handling)

If something goes wrong a default error page will be shown with a short description of the error and the according HTTP status code.

You can add a method `handleException(\Exception $e, ...)` (other dependcies will be injected) to your controller to handle the exception.

However, this will not work if no controller could be determined because no route matches request. To cover this case you can add a custom exception handler:

```
$router->addExceptionHandler(InvalidRouteException::class, [Controller::class, 'handleException']);
```

The object will be created with service injection.

Alternatively you can use a closure:

```
$router->addExceptionHandler(InvalidRouteException::class, fn(\Exception $e) => die($e->getMessage()));
```

###  Health Score

47

—

FairBetter than 93% of packages

Maintenance98

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity56

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 ~28 days

Recently: every ~33 days

Total

21

Last Release

13d ago

PHP version history (2 changes)v0.1PHP &gt;=8.0.0

v0.8PHP &gt;=8.4.0

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/23295473?v=4)[Henrik Gebauer](/maintainers/hengeb)[@hengeb](https://github.com/hengeb)

---

Top Contributors

[![hengeb](https://avatars.githubusercontent.com/u/23295473?v=4)](https://github.com/hengeb "hengeb (25 commits)")

### Embed Badge

![Health badge](/badges/hengeb-router/health.svg)

```
[![Health](https://phpackages.com/badges/hengeb-router/health.svg)](https://phpackages.com/packages/hengeb-router)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M387](/packages/easycorp-easyadmin-bundle)[illuminate/session

The Illuminate Session package.

9939.3M850](/packages/illuminate-session)[spatie/laravel-export

Create a static site bundle from a Laravel app

674146.0k6](/packages/spatie-laravel-export)[symfony/ux-autocomplete

JavaScript Autocomplete functionality for Symfony

645.9M39](/packages/symfony-ux-autocomplete)[illuminate/cookie

The Illuminate Cookie package.

244.6M136](/packages/illuminate-cookie)[codefog/contao-news_categories

News Categories bundle for Contao Open Source CMS

3189.0k6](/packages/codefog-contao-news-categories)

PHPackages © 2026

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