PHPackages                             tobento/service-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. tobento/service-routing

ActiveLibrary[Framework](/categories/framework)

tobento/service-routing
=======================

A flexible PHP router.

2.0.2(3mo ago)02843MITPHPPHP &gt;=8.4

Since Sep 16Pushed 3mo ago1 watchersCompare

[ Source](https://github.com/tobento-ch/service-routing)[ Packagist](https://packagist.org/packages/tobento/service-routing)[ Docs](https://www.tobento.ch)[ RSS](/packages/tobento-service-routing/feed)WikiDiscussions 2.x Synced 1mo ago

READMEChangelog (10)Dependencies (15)Versions (22)Used By (3)

Routing Service
===============

[](#routing-service)

The Routing Service provides a flexible way to build routes for any PHP application.

Table of Contents
-----------------

[](#table-of-contents)

- [Getting started](#getting-started)
    - [Requirements](#requirements)
    - [Highlights](#highlights)
- [Documentation](#documentation)
    - [Router](#router)
    - [Basic Routing](#basic-routing)
        - [Routing Methods](#routing-methods)
        - [Uri Definitions](#uri-definitions)
        - [Handler Definitions](#handler-definitions)
        - [Parameters](#parameters)
        - [Url Generation](#url-generation)
        - [More Routes Methods](#more-routes-methods)
    - [Group Routing](#group-routing)
    - [Resource Routing](#resource-routing)
    - [Domain Routing](#domain-routing)
        - [Domain Routes](#domain-routes)
        - [Domain Url Generation](#domain-url-generation)
        - [Managing Domains](#managing-domains)
    - [Signed Routing](#signed-routing)
        - [Signed Routes](#signed-routes)
        - [Signed Url Generation](#signed-url-generation)
    - [Localization and Translation Routing](#localization-and-translation-routing)
        - [Localize Routes](#localize-routes)
        - [Translatable Routes](#translatable-routes)
    - [Matched Route Event](#matched-route-event)
    - [Constrainer](#constrainer)
    - [Dispatching Strategies](#dispatching-strategies)
        - [Simple](#simple)
        - [With PSR-7 Response](#with-psr-7-response)
        - [With PSR-15 Middleware](#with-psr-15-middleware)
- [Credits](#credits)

---

Getting started
===============

[](#getting-started)

Add the latest version of the routing service running this command.

```
composer require tobento/service-routing

```

Requirements
------------

[](#requirements)

- PHP 8.4 or greater

Highlights
----------

[](#highlights)

- Basic routing (GET, POST, PUT, PATCH, UPDATE, DELETE)
- Domain routing
- Group routing
- Resource routing
- Named routes
- Matched route handling
- Url and signed url generation
- PSR-15 middleware support
- Localization
- Autowiring
- Framework-agnostic, will work with any project
- Decoupled design
- Easily extendable or customizable

Documentation
=============

[](#documentation)

Router
------

[](#router)

```
use Tobento\Service\Routing\Router;
use Tobento\Service\Routing\RequestData;
use Tobento\Service\Routing\UrlGenerator;
use Tobento\Service\Routing\RouteFactory;
use Tobento\Service\Routing\RouteDispatcher;
use Tobento\Service\Routing\Constrainer\Constrainer;
use Tobento\Service\Routing\RouteHandler;
use Tobento\Service\Routing\MatchedRouteHandler;
use Tobento\Service\Routing\RouteResponseParser;

// Any PSR-11 container
$container = new \Tobento\Service\Container\Container();

$router = new Router(
    new RequestData(
        $_SERVER['REQUEST_METHOD'] ?? 'GET',
        rawurldecode($_SERVER['REQUEST_URI'] ?? ''),
        'example.com',
    ),
    new UrlGenerator(
        'https://example.com/basepath',
        'a-random-32-character-secret-signature-key',
    ),
    new RouteFactory(),
    new RouteDispatcher($container, new Constrainer()),
    new RouteHandler($container),
    new MatchedRouteHandler($container),
    new RouteResponseParser(),
);

$router->setBaseUri('/path/app/');
```

Basic Routing
-------------

[](#basic-routing)

### Routing methods

[](#routing-methods)

```
$router->get('blog', [Controller::class, 'method']);
$router->post('blog', [Controller::class, 'method']);
$router->put('blog', [Controller::class, 'method']);
$router->patch('blog', [Controller::class, 'method']);
$router->delete('blog', [Controller::class, 'method']);
$router->head('blog', [Controller::class, 'method']);
$router->options('blog', [Controller::class, 'method']);

// Route multiple
$router->route('GET|POST', 'blog', [Controller::class, 'method']);

// Route any
$router->route('*', 'blog', [Controller::class, 'method']);
```

### Uri definitions

[](#uri-definitions)

```
$router->get('blog/{slug}', 'Controller::method');

// you can define as many as you want:
$router->get('blog/{slug}/comment/{id}', 'Controller::method');

// using a question mark for optional parameters
$router->get('{?locale}/blog/{?id}', 'Controller::method');

// or using wildcard to allow any path
$router->get('blog/{path*}', 'Controller::method');
```

### Handler definitions

[](#handler-definitions)

The default RouteHandler supports autowiring and the following handler definitions.

```
// By providing class and method name:
$router->get('blog', [Controller::class, 'method']);
$router->get('blog', [new Controller(), 'method']);
$router->get('blog', Controller::class); // __invoke method called

// Using Class::method syntax:
$router->get('blog', 'Controller::method');

// Using closure:
$router->get('blog', function() {
    return 'welcome';
});

// You might provide data for build-in method parameters:
$router->get('blog', [Controller::class, 'method', ['name' => 'value']]);
```

### Parameters

[](#parameters)

**Name a route:**

The main purpose for named routes is the generation of URLs. But they might be useful for any other cases too.

```
$router->get('blog', 'Controller::method')->name('blog');
```

> ⚠️ Named routes should be unique, otherwise the route got overwritten.

**Adding middleware:** see also [With PSR-15 Middleware](#with-psr-15-middleware)

```
$router->get('blog', 'Controller::method')
       ->middleware(Middleware::class, Another::Middleware);
```

**Where constraint parameter:** see also [Constrainer](#constrainer)

```
$router->get('blog/{slug}', 'Controller::method')
       ->where('slug', '[a-z]+');

$router->get('{path*}', 'Controller::method')
       ->where('path', '[^?]+');
```

**Query constraint parameter:**

```
// do not allow any uri query parameters:
$router->get('blog/{slug}', 'Controller::method')->query(null);

// allow only certain query characters:
$router->get('blog/{slug}', 'Controller::method')->query('/^[a-zA-Z0-9=&\/,\[\]-]+?$/');
```

**Domain:** see also [Domain Routing](#domain-routing)

```
$router->get('blog', [Controller::class, 'method'])
       ->domain('sub.example.com');
```

**Signed:** see also [Signed Routing](#signed-routing)

```
$router->get('blog', [Controller::class, 'method'])
       ->signed('blog', validate: false); // default validate: true
```

**Matches:**

```
use Tobento\Service\Routing\RouteInterface;

$router->get('{slug}', 'BlogController::method')
       ->matches(function(SlugsRepo $slugs, RouteInterface $route): null|RouteInterface {
           // we would need call matches handler later on RouteDispatcher in order to have request data
           $slug = $slugs->find($route->getParameter('request_parameters')['slug']);

           if (!$slug || $slug->getResourceKey() !== 'blog') {
               return null;
           }

           $requestParams = $route->getParameter('request_parameters', []);
           $requestParams['id'] = $slug->getResourceId();
           unset($requestParams['slug']);
           $route->parameter('request_parameters', $requestParams);

           return $route;
       });

$router->get('{slug}', 'ProductsController::method')
       ->matches(function(SlugsRepo $slugs, RouteInterface $route): null|RouteInterface {

           $slug = $slugs->find($route->getParameter('request_parameters')['slug']);

           if (!$slug || $slug->getResourceKey() !== 'products') {
               return null;
           }

           $requestParams = $route->getParameter('request_parameters', []);
           $requestParams['id'] = $slug->getResourceId();
           unset($requestParams['slug']);
           $route->parameter('request_parameters', $requestParams);

           return $route;
       });
```

**BaseUrl:**

```
$router->get('blog', [Controller::class, 'method'])
       ->baseUrl('https:://sub.example.com/app');
```

**Adding custom parameters:**

```
$router->get('blog', [Controller::class, 'method'])
       ->parameter('name', 'value');
```

### Url Generation

[](#url-generation)

**Generating url from named routes:**

```
$router->get('blog', [Controller::class, 'method'])
       ->name('blog');

$blogUrl = $router->url('blog')->get();
$blogUrl = (string) $router->url('blog');

// if your route uri has parameters:
$router->get('blog/edit/{id}', [Controller::class, 'method'])
       ->name('blog.edit');

$blogUrl = $router->url('blog.edit', ['id' => 5])->get();
```

### More Routes methods

[](#more-routes-methods)

**Custom Routes:**

```
use Tobento\Service\Routing\RouteInterface;

// must implement RouteInterface
$router->addRoute(new CustomRoute());

$router->addRoutes([
    new CustomRoute(),
    new CustomRoute(),
]);
```

**Get All Routes:**

```
use Tobento\Service\Routing\RouteInterface;

$routes = $router->getRoutes();
// returns: array
```

**Get Named Route:**

```
use Tobento\Service\Routing\RouteInterface;

$route = $router->getRoute('name');
// returns: null|RouteInterface
```

**Get Matched Route:**

```
use Tobento\Service\Routing\RouteInterface;

$matchedRoute = $router->getMatchedRoute();
// returns: null|RouteInterface
```

Group Routing
-------------

[](#group-routing)

You might use groups to share parameters across routes:

```
use Tobento\Service\Routing\RouteGroupInterface;

$router->group('admin', function(RouteGroupInterface $group) {

    // supports any basic routing methods:
    $group->get('blog', [Controller::class, 'method'])->name('admin.blog');

    // resources:
    $group->resource('products', ProductsController::class);
    // The group uri 'admin' gets prepended to route names for resources only.
    // $router->getRoute('admin.products.index');
    // $router->url('admin.products.index');

    // group:
    $group->group('account', function(RouteGroupInterface $group) {
        // define routes.
    });

    // you might overwrite the group parameters by defining it:
    $group->get('blog', [Controller::class, 'method'])
          ->middleware(Middleware::class);

})->domain('sub.example.com')
  ->middleware(Middleware::class)
  ->baseUrl('sub.example.com')
  ->parameter('name', 'value')
  ->locale('de')
  ->locales(['de', 'en'])
  ->localeOmit('de')
  ->localeName('locale')
  ->localeFallbacks(['de' => 'en']);
```

If the group uri definition has parameters, they are available on the routes:

```
use Tobento\Service\Routing\RouteGroupInterface;

$router->group('{locale}', function(RouteGroupInterface $group) {

    // locale is available too.
    $group->get('blog/{id}', function($locale, $id) {
        // do something
    });

})->where('locale', ':in:de:fr');
```

Resource Routing
----------------

[](#resource-routing)

You may use resource routing for convenience:

```
$router->resource('products', ProductsController::class);
```

This will produce the following routes:

MethodUriAction / Controller methodRoute nameGETproductsindexproducts.indexGETproducts/createcreateproducts.createPOSTproductsstoreproducts.storeGETproducts/{id}showproducts.showGETproducts/{id}/editeditproducts.editPUT/PATCHproducts/{id}updateproducts.updateDELETEproducts/{id}deleteproducts.delete**You might route only specific actions:**

```
$router->resource('products', ProductsController::class)
       ->only(['index', 'show']);

$router->resource('products', ProductsController::class)
       ->except(['delete']);
```

**You might change the default where constraint:**

```
$router->resource('products', ProductsController::class)
       ->where('[a-z0-9]+'); // default is [0-9]+
```

**Adding new or overwriting existing actions:**

```
// creating new action:
$router->resource('products', ProductsController::class)
       ->action(
           action: 'display',
           method: 'GET',
           uri: '/display/{id}',
           parameters: ['constraints' => ['id' => '[0-9]+']],
       );
// GET, products/display/{id}, display, products.display

// overwriting index action:
$router->resource('products', ProductsController::class)
       ->action(
           action: 'index',
           method: 'GET',
           uri: '/index',
           parameters: [],
       );
// GET, products/index, index, products.index
```

**Middleware:**

```
$router->resource('products', ProductsController::class)
       ->middleware(
           ['show'], // empty array for all actions
           Middleware::class,
           AnotherMiddleware::class,
       );
```

**Adding additional route parameters for an action:**

```
$router->resource('products', ProductsController::class)
       ->parameter(
           action: 'index',
           name: 'foo',
           value: 'bar',
       );
```

**Adding additional route parameters for all actions:**

```
$router->resource('products', ProductsController::class)
       ->sharedParameter(
           name: 'foo',
           value: 'bar',
       );
```

**With localization and translation:**

```
$router->resource('{?locale}/{products}', ProductsResource::class)
    // specify the name as above we set the uri:
    ->name('products')

    // specify the locales:
    ->locales(['de', 'en'])
    ->localeOmit('en')
    ->localeFallbacks(['de' => 'en'])

    // specify the translations for the verbs:
    ->trans('create', ['de' => 'neu', 'en' => 'create'], action: 'create')
    ->trans('edit', ['de' => 'bearbeiten', 'en' => 'edit'], action: 'edit')

    // for products:
    ->trans('products', ['de' => 'produkte', 'en' => 'products']);

// Example with new action:
$router->resource('products', ProductsController::class)
    ->action(
        action: 'display',
        method: 'GET',
        uri: '/display/{id}', // set without {display}
        parameters: ['constraints' => ['id' => '[0-9]+']],
    )

    // specify the locales:
    ->locales(['de', 'en'])
    ->localeOmit('en')
    ->localeFallbacks(['de' => 'en'])

    // specify the translations for the display verb:
    ->trans('display', ['de' => 'ansicht', 'en' => 'display'], action: 'display');
```

Domain Routing
--------------

[](#domain-routing)

### Domain Routes

[](#domain-routes)

```
// route:
$router->get('blog', [Controller::class, 'method'])
       ->domain('example.com');

// group:
$router->group('api', function($group) {})
       ->domain('api.example.com');

// resource:
$router->resource('products', ProductsController::class)
       ->domain('example.com');
```

**multiple domains**

You may add a route for multiple domains:

```
$router->get('blog', [Controller::class, 'method'])
       ->domain('example.ch')
       ->domain('example.de');
```

**domain specific parameters**

You may set domain specific parameters for each domain:

```
use Tobento\Service\Routing\RouteInterface;

$router->get('{?locale}/{about}', function($locale, $about) {
    return [$locale, $about];
})
->name('about')

// default parameters will be used if not specified on domain:
->trans('about', ['de' => 'ueber-uns', 'en' => 'about', 'fr' => 'se-presente'])

->domain('example.ch', function(RouteInterface $route): void {
    $route->locales(['de', 'fr'])
        ->localeOmit('de')
        ->localeFallbacks(['fr' => 'de'])
        ->trans('about', ['de' => 'ueber-uns', 'fr' => 'se-presente']);
})->domain('example.de', function(RouteInterface $route): void {
    $route->locales(['de', 'en'])
        ->localeOmit('de')
        ->localeFallbacks(['en' => 'de']);
});
```

### Domain Url Generation

[](#domain-url-generation)

```
$router->get('blog', [Controller::class, 'method'])
       ->name('blog')
       ->domain('example.ch')
       ->domain('example.de');

// current domain url:
$url = $router->url('blog');

// get specific domain url:
$url = $router->url('blog')->domain('example.de');

// get all domained urls:
$urls = $router->url('blog')->domained();
/*[
    'example.ch' => 'https://example.ch/blog',
    'example.de' => 'https://example.de/blog',
]*/

// you may get all translated urls from current or specific domain:
$urls = $router->url('blog')->translated();
$urls = $router->url('blog')->domain('example.de')->translated();
```

### Managing Domains

[](#managing-domains)

You may specify the domains in order to managing them at one place.

```
use Tobento\Service\Routing\Router;
use Tobento\Service\Routing\RequestData;
use Tobento\Service\Routing\UrlGenerator;
use Tobento\Service\Routing\RouteFactory;
use Tobento\Service\Routing\RouteDispatcher;
use Tobento\Service\Routing\Constrainer\Constrainer;
use Tobento\Service\Routing\RouteHandler;
use Tobento\Service\Routing\MatchedRouteHandler;
use Tobento\Service\Routing\RouteResponseParser;
use Tobento\Service\Routing\Domains;
use Tobento\Service\Routing\Domain;

// Any PSR-11 container
$container = new \Tobento\Service\Container\Container();

// Domains
$domains = new Domains(
    new Domain(key: 'example.ch', domain: 'ch.localhost', uri: 'http://ch.localhost'),
    new Domain(key: 'example.de', domain: 'de.localhost', uri: 'http://de.localhost'),
);

$router = new Router(
    new RequestData(
        $_SERVER['REQUEST_METHOD'] ?? 'GET',
        rawurldecode($_SERVER['REQUEST_URI'] ?? ''),
        'ch.localhost',
    ),
    new UrlGenerator(
        'https://example.com/basepath',
        'a-random-32-character-secret-signature-key',
    ),
    new RouteFactory($domains), // pass domains
    new RouteDispatcher($container, new Constrainer()),
    new RouteHandler($container),
    new MatchedRouteHandler($container),
    new RouteResponseParser(),
);

// Adding Routes:
// Set the domain name as the key specified above:
$router->get('blog', [Controller::class, 'method'])
       ->domain('example.ch')
       ->domain('example.de');
```

Signed Routing
--------------

[](#signed-routing)

### Signed Routes

[](#signed-routes)

**Add a signed route:**

```
use Tobento\Service\Routing\InvalidSignatureException;

$router->get('unsubscribe/{user}', [Controller::class, 'method'])
       ->signed('unsubscribe');

try {
    $matchedRoute = $router->dispatch();
} catch (InvalidSignatureException $e) {
    // handle invalid signature
}
```

**Add a signed route with validating on handler for custom response:**

```
use Tobento\Service\Routing\RouterInterface;

$router->get('unsubscribe/{user}', function(RouterInterface $router, $user) {

    $matchedRoute = $router->getMatchedRoute();
    $requestUri = $router->getRequestData()->uri();

    if (! $router->getUrlGenerator()->hasValidSignature($matchedRoute->getUri(), $requestUri)) {
        // handle invalid signature.
    }
})
->signed('unsubscribe', validate: false);
```

### Signed Url Generation

[](#signed-url-generation)

```
use Tobento\Service\Dater\Dater;

$router->get('unsubscribe/{user}', [Controller::class, 'method'])
       ->signed('unsubscribe');

// generate a signed url with no expiring.
$url = (string) $router->url('unsubscribe', ['user' => 5])->sign();
// https://example.com/basepath/unsubscribe/5/a0df83344703b26cd1f9cdcb05196082a6a7799e84b4748a5610d3256b556c55

// generate a signed url which expires in 10 days.
$url = (string) $router->url('unsubscribe', ['user' => 5])->sign(new Dater()->addDays(10));
// https://example.com/basepath/unsubscribe/5/a0df83344703b26cd1f9cdcb05196082a6a7799e84b4748a5610d3256b556c55/1630752459

// generate a signed url with no expiring and add signature data as query parameters.
$url = (string) $router->url('unsubscribe', ['user' => 5])->sign(withQuery: true);
// https://example.com/basepath/unsubscribe/5?signature=6d632a4a8981b1fb017ad6f82067370d6c98ddcd8c6d18cb4fc30c1d44e0f67e

// generate a signed url which expires in 10 days and add signature data as query parameters.
$url = (string) $router->url('unsubscribe', ['user' => 5])->sign(new Dater()->addDays(10), true);
// https://example.com/basepath/unsubscribe/5?expires=1630752540&signature=6d632a4a8981b1fb017ad6f82067370d6c98ddcd8c6d18cb4fc30c1d44e0f67e
```

Localization and Translation Routing
------------------------------------

[](#localization-and-translation-routing)

### Localize Routes

[](#localize-routes)

```
$router->get('{?locale}/about', [Controller::class, 'method'])
       ->name('about');

// get current locale url:
$url = (string) $router->url('about');
// https://example.com/basepath/about

// get specific locale url:
$url = (string) $router->url('about')->locale('en');
// https://example.com/basepath/en/about

// get all translated urls:
$urls = $router->url('about')->translated();
/*[]*/

// get/create specific translated urls:
$urls = $router->url('about')->translated(['de', 'fr']);
/*[
    'de' => 'https://example.com/basepath/de/about',
    'fr' => 'https://example.com/basepath/fr/about',
]*/
```

**Support only specific locales:**

```
$router->get('{?locale}/about', [Controller::class, 'method'])
       ->name('about')
       ->locales(['de', 'en']); // the supported locales, MUST be called before any other locale methods.

// get current locale url:
$url = (string) $router->url('about');
// https://example.com/basepath/about

// get specific locale url:
$url = (string) $router->url('about')->locale('en');
// https://example.com/basepath/en/about

// get all translated urls:
$urls = $router->url('about')->translated();
/*[
    'de' => 'https://example.com/basepath/de/about',
    'en' => 'https://example.com/basepath/en/about',
]*/

// get/create specific translated urls:
$urls = $router->url('about')->translated(['de', 'fr']);
/*[
    'de' => 'https://example.com/basepath/about',
    'fr' => 'https://example.com/basepath/fr/about',
]*/
```

**Omit locale in request uri:**

```
$router->get('{?locale}/about', [Controller::class, 'method'])
       ->name('about')
       ->locales(['de', 'en'])
       ->localeOmit('en');

// get current locale url:
$url = (string) $router->url('about');
// https://example.com/basepath/about

// get specific locale url:
$url = (string) $router->url('about')->locale('en');
// https://example.com/basepath/about

// get all translated urls:
$urls = $router->url('about')->translated();
/*[
    'de' => 'https://example.com/basepath/de/about',
    'en' => 'https://example.com/basepath/about',
]*/
```

**Define current locale:**

```
$router->get('{locale}/about', [Controller::class, 'method'])
       ->name('about')
       ->locale('en');

// get current locale url:
$url = (string) $router->url('about');
// https://example.com/basepath/en/about

// get specific locale url:
$url = (string) $router->url('about')->locale('de');
// https://example.com/basepath/de/about

// get all translated urls:
$urls = $router->url('about')->translated();
/*[
    'en' => 'https://example.com/basepath/en/about',
]*/
```

**Rename locale uri definition:**

```
$router->get('{?loc}/about', [Controller::class, 'method'])
       ->name('about')
       ->localeName('loc'); // the locale uri definition name, 'locale' is the default name
```

### Translatable Routes

[](#translatable-routes)

#### Without locale uri definition

[](#without-locale-uri-definition)

```
$router->get('/{about}', [Controller::class, 'method'])
       ->name('about')
       ->locale('de') // set the current locale.
       ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']);

// get current locale url:
$url = (string) $router->url('about');
// https://example.com/basepath/ueber-uns

// get specific locale url:
$url = (string) $router->url('about')->locale('en');
// https://example.com/basepath/about

// get all translated urls:
$urls = $router->url('about')->translated();
/*[
    'de' => 'https://example.com/basepath/ueber-uns',
    'en' => 'https://example.com/basepath/about',
]*/
```

**Support only specific locales:**

```
$router->get('/{about}', [Controller::class, 'method'])
       ->name('about')
       ->locales(['en']) // the supported locales, MUST be called before any other locale methods.
       ->locale('de') // set the current locale.
       ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']);

// get current locale url:
$url = (string) $router->url('about');
// https://example.com/basepath/ueber-uns

// get specific locale url:
$url = (string) $router->url('about')->locale('en');
// https://example.com/basepath/about

// get all translated urls:
$urls = $router->url('about')->translated();
/*[
    'en' => 'https://example.com/basepath/about',
]*/
```

**Define locale fallbacks:**

```
$router->get('/{about}', [Controller::class, 'method'])
       ->name('about')
       ->locales(['en', 'de', 'fr'])
       ->localeFallbacks(['fr' => 'en'])
       ->locale('de') // set the current locale.
       ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']);

// get current locale url:
$url = (string) $router->url('about');
// https://example.com/basepath/ueber-uns

// get specific locale url:
$url = (string) $router->url('about')->locale('fr');
// https://example.com/basepath/about

// get all translated urls:
$urls = $router->url('about')->translated();
/*[
    'en' => 'https://example.com/basepath/about',
    'de' => 'https://example.com/basepath/ueber-uns',
    'fr' => 'https://example.com/basepath/about',
]*/
```

#### With locale uri definition:

[](#with-locale-uri-definition)

```
$router->get('{?locale}/{about}', [Controller::class, 'method'])
       ->name('about')
       ->localeOmit('en')
       ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']);

// get current locale url:
$url = (string) $router->url('about');
// https://example.com/basepath/about

// get specific locale url:
$url = (string) $router->url('about')->locale('de');
// https://example.com/basepath/de/ueber-uns

// get all translated urls:
$urls = $router->url('about')->translated();
/*[
    'de' => 'https://example.com/basepath/de/ueber-uns',
    'en' => 'https://example.com/basepath/about',
]*/
```

**Default parameters are always prioritized:**

```
$router->get('/{about}', [Controller::class, 'method'])
       ->name('about')
       ->locale('de') // set the current locale.
       ->trans('about', ['de' => 'ueber-uns', 'en' => 'about']);

$url = (string) $router->url('about', ['locale' => 'de', 'about' => 'ueberuns'])->locale('en');
// https://example.com/basepath/ueberuns
```

Matched Route Event
-------------------

[](#matched-route-event)

The default MatchedRouteHandler supports autowiring.

```
$router->get('blog/edit', [Controller::class, 'method'])->name('blog.edit');

$router->matched('blog.edit', function() {
    // do something after the route has been matched.
});
```

Constrainer
-----------

[](#constrainer)

**Add rule constraint to route:**

```
// instead of:
$router->get('blog/{word}', 'Controller::method')
       ->where('word', '(foo|bar)');

// you can use a rule:
$router->get('blog/{word}', 'Controller::method')
       ->where('word', ':in:foo:bar');

// using rule with array syntax.
$router->get('blog/{word}', 'Controller::method')
       ->where('word', ['in', 'foo', 'bar']);
```

**Available Rules:**

RuleRegexDescription:alpha\[a-zA-Z\]+:alpha:2\[a-zA-Z\]{2}n{x} Matches any string that contains a sequence of X n's:alpha:2:5\[a-zA-Z\]{2,5}n{x,y} Matches any string that contains a sequence of X to Y n's:alpha:2:\[a-zA-Z\]{2,}n{x,} Matches any string that contains a sequence of at least X n's:num\[0-9\]+:num:2\[0-9\]{2}n{x} Matches any string that contains a sequence of X n's:num:2:5\[0-9\]{2,5}n{x,y} Matches any string that contains a sequence of X to Y n's:num:2:\[0-9\]{2,}n{x,} Matches any string that contains a sequence of at least X n's:alphaNum\[a-zA-Z0-9\]+:alphaNum:2\[a-zA-Z0-9\]{2}n{x} Matches any string that contains a sequence of X n's:alphaNum:2:5\[a-zA-Z0-9\]{2,5}n{x,y} Matches any string that contains a sequence of X to Y n's:alphaNum:2:\[a-zA-Z0-9\]{2,}n{x,} Matches any string that contains a sequence of at least X n's🆔1:5🆔minNumber:maxLength:idUses the default parameters from the rule 🆔1:21:in:foo:bar:bazIf the value is is one of foo, bar, baz**Custom Rules:**

```
// rule with regex:
$router->getRouteDispatcher()
       ->rule('slug')
       ->regex('[a-z0-9-]+');

// rule with regex closure:
$router->getRouteDispatcher()
       ->rule('slug')
       ->regex(function(array $parameters): null|string {
           // build the regex based on the parameters
       });

// rule with matches:
$router->getRouteDispatcher()
       ->rule('slug')
       ->matches(function(string $value, array $parameters): bool {
           // handle
       });

// or by adding a rule.
$router->getRouteDispatcher()->addRule('slug', new SlugRule());
```

Dispatching Strategies
----------------------

[](#dispatching-strategies)

There are different ways of handling the matched route, depending on your needs.

### Simple

[](#simple)

No middleware support though.

```
use Tobento\Service\Routing\RouteNotFoundException;
use Tobento\Service\Routing\InvalidSignatureException;
use Tobento\Service\Routing\TranslationException;
use Tobento\Service\Routing\RouteInterface;

try {
    $matchedRoute = $router->dispatch();

    var_dump($matchedRoute instanceof RouteInterface);
    // bool(true)

} catch (RouteNotFoundException $e) {
    // handle exception
} catch (InvalidSignatureException $e) {
    // handle exception
} catch (TranslationException $e) {
    // handle exception
}

// call matched route handler for handling registered matched event actions.
$router->getMatchedRouteHandler()->handle($matchedRoute);

// handle the matched route.
$routeResponse = $router->getRouteHandler()->handle($matchedRoute);
```

### With PSR-7 Response

[](#with-psr-7-response)

No middleware support though.

```
use Tobento\Service\Routing\RouteNotFoundException;
use Tobento\Service\Routing\InvalidSignatureException;
use Tobento\Service\Routing\TranslationException;

try {
    $matchedRoute = $router->dispatch();
} catch (RouteNotFoundException $e) {
    // handle exception
} catch (InvalidSignatureException $e) {
    // handle exception
} catch (TranslationException $e) {
    // handle exception
}

// call matched route handler for handling registered matched event actions.
$router->getMatchedRouteHandler()->handle($matchedRoute);

// handle the matched route.
$routeResponse = $router->getRouteHandler()->handle($matchedRoute);

// create response.
$response = new \Nyholm\Psr7\Factory\Psr17Factory()->createResponse(200);

// parse the route response.
$response = $router->getRouteResponseParser()->parse($response, $routeResponse);

// emitting response.
new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter()->emit($response);
```

### With PSR-15 Middleware

[](#with-psr-15-middleware)

You will need to define your MiddlewareDispatcher implementation on the container. You might customize this behaviour by your own RouteHandler though.

```
use Tobento\Service\Middleware\MiddlewareDispatcherInterface;
use Tobento\Service\Middleware\MiddlewareDispatcher;
use Tobento\Service\Middleware\AutowiringMiddlewareFactory;
use Tobento\Service\Middleware\FallbackHandler;

// adjust route parameters passed to the request attributes if needed:
$router->setRequestAttributes(['uri', 'name', 'request_uri']);
// After PreRouting or Routing Middleware: $request->getAttribute('route.name');

// Middleware Handling:
$container->set(MiddlewareDispatcherInterface::class, function($container) {

    return new MiddlewareDispatcher(
        new FallbackHandler(new \Nyholm\Psr7\Factory\Psr17Factory()->createResponse(200)),
        new AutowiringMiddlewareFactory($container)
    );
});

$middlewareDispatcher = $container->get(MiddlewareDispatcherInterface::class);

// add MethodOverride middleware if needed.
$middlewareDispatcher->add(\Tobento\Service\Routing\Middleware\MethodOverride::class);
// add PreRouting middleware if needed.
$middlewareDispatcher->add(\Tobento\Service\Routing\Middleware\PreRouting::class);
// ... more middlewares
$middlewareDispatcher->add(\Tobento\Service\Routing\Middleware\Routing::class);

$request = new \Nyholm\Psr7\Factory\Psr17Factory()->createServerRequest('GET', 'https://example.com');

$response = $middlewareDispatcher->handle($request);

// emitting response.
new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter()->emit($response);
```

Credits
=======

[](#credits)

- [Tobias Strub](https://www.tobento.ch)
- [All Contributors](../../contributors)

###  Health Score

51

—

FairBetter than 95% of packages

Maintenance86

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity77

Established project with proven stability

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

Recently: every ~39 days

Total

22

Last Release

99d ago

Major Versions

1.x-dev → 2.02025-10-01

PHP version history (2 changes)1.0.0PHP &gt;=8.0

2.0PHP &gt;=8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/055d6a1b5c2384bb179c75ab0b55914231d898fdc4dffeb30770f81200e52206?d=identicon)[TOBENTOch](/maintainers/TOBENTOch)

---

Top Contributors

[![tobento-ch](https://avatars.githubusercontent.com/u/16684832?v=4)](https://github.com/tobento-ch "tobento-ch (70 commits)")

---

Tags

packagerouterroutingtobento

###  Code Quality

TestsPHPUnit

Static AnalysisPsalm

Type Coverage Yes

### Embed Badge

![Health badge](/badges/tobento-service-routing/health.svg)

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

###  Alternatives

[cakephp/cakephp

The CakePHP framework

8.8k18.5M1.6k](/packages/cakephp-cakephp)[slim/slim

Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs

12.2k49.9M1.3k](/packages/slim-slim)[cakephp/authentication

Authentication plugin for CakePHP

1153.6M67](/packages/cakephp-authentication)[neos/flow

Flow Application Framework

862.0M449](/packages/neos-flow)[cakephp/authorization

Authorization abstraction layer plugin for CakePHP

742.2M34](/packages/cakephp-authorization)[yiisoft/router

Yii router

62321.8k21](/packages/yiisoft-router)

PHPackages © 2026

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