# HTTP request router for PHP

The HttpSoft\Router package provides convenient routing management HTTP requests.

This package supports PSR-7 and PSR-15 interfaces.

This package requires PHP version 7.4 or later.

Package installation:

composer require httpsoft/http-router

Source code on GitHub.

# API

Exceptions:

Middlewares:

# Usage

use HttpSoft\Router\RouteCollector;

/**
 * @var mixed $handler
 */

$router = new RouteCollector();

# Defining routes

$router->get('home', '/', $handler);
$router->post('logout', '/logout', $handler);
$router->add('login', '/login', $handler, ['GET', 'POST']);

$router->delete('post.delete', '/post/delete/{id}', $handler);
$router->get('post.view', '/post/{slug}{format}', $handler);

When adding the {token} placeholder parameter to the path, the regular expression [^/]+ will be used by default, which matches everything except the slash that points to the next segment of the path.

To specify custom regular expressions for placeholder parameter tokens, use the HttpSoft\Router\Route::tokens() method.

$router->delete('post.delete', '/post/delete/{id}', $handler)->tokens(['id' => '\d+']);

$router->get('post.view', '/post/{slug}{format}', $handler)
    ->tokens(['slug' => '[\w\-]+', 'format' => '\.[a-zA-z]{3,}'])
;

You can set the token to the default value using the HttpSoft\Router\Route::defaults() method.

$router->get('post.view', '/post/{slug}{format}', $handler)
    ->tokens(['slug' => '[\w\-]+', 'format' => '\.[a-zA-z]{3,}'])
    ->defaults(['format' => '.html'])
;

// '/post/post-slug.html'
$router->routes()->path('post.view', ['slug' => 'post-slug']);

# Optional parameters

Tokens of the route enclosed in [...] are considered optional.

$router->get('post.list', '/posts{[page]}', $handler)
    ->tokens(['page' =>  '\d+'])
;

// '/posts/33'
$router->routes()->path('post.list', ['page' => 33]);
// '/posts'
$router->routes()->path('post.list');

Optional parameters SHOULD only be located at the very end of the route.

Note that the leading slash separator does not need to be placed before the optional parameter token.

You can also set the default value for the optional parameter token.

$router->get('post.list', '/posts{[page]}', $handler)
    ->tokens(['page' =>  '\d+'])
    ->defaults(['page' =>  '1'])
;

// '/posts/1'
$router->routes()->path('post.list');
// '/posts'
$router->routes()->path('post.list', ['page' => null]);

Sometimes it is useful to have a route with several optional parameters in a row.

// `{[year/month/day]}` equivalently to `{[/year/month/day]}`
$router->get('post.archive', '/post/archive{[year/month/day]}', $handler)
    ->tokens(['year' => '\d{4}', 'month' => '\d{2}', 'day' => '\d{2}'])
;

// '/post/archive'
$router->routes()->path('post.archive', ['year' => null, 'month' => null, 'day' => null]);
// Or 
$router->routes()->path('post.archive');

// '/post/archive/2020/09/12'
$router->routes()->path('post.archive', ['year' => '2020', 'month' => '09', 'day' => '12']);
// '/post/archive/2020/09'
$router->routes()->path('post.archive', ['year' => '2020', 'month' => '09']);
// '/post/archive/2020'
$router->routes()->path('post.archive', ['year' => '2020']);

# Host Matching

Using the HttpSoft\Router\Route::host() method, you can specify a specific host for each route.

// Only for example.com
$router->get('page', '/page', $handler)
    ->host('example.com')
;

// Only for subdomain.example.com
$router->get('page', '/page', $handler)
    ->host('subdomain.example.com')
;

// Only for shop.example.com or blog.example.com
$router->get('page', '/page', $handler)
    ->host('(shop|blog).example.com')
;

// For any subdomain
$router->get('page', '/page', $handler)
    ->host('(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?).example.com')
;

Note that all dots in the regular expression will be automatically escaped.

# Route groups

You can specify routes inside of a group. All routes defined inside a group will have a common path prefix.

$router->group('/post', static function (RouteCollector $router) use ($handler): void {
    // '/post/post-slug'
    $router->get('post.view', '/{slug}', $handler)->tokens(['slug' => '[\w-]+']);
    // '/post' or '/post/page-14'
    $router->get('post.list', '{[page]}', $handler)->tokens(['page' =>  'page-\d+']);
    // '/post/delete/13'
    $router->delete('post.delete', '/delete/{id}', DeleteHandler::class)->tokens(['id' => '\d+']);
    // '/post/create/11' or '/post/update/12'
    $router->add('post.edit', '/(create|update)/{id}', EditHandler::class, ['GET', 'POST'])
        ->tokens(['id' => '\d+'])
    ;
});

The result will be equivalent to:

$router->get('post.view', '/post/{slug}', $handler)->tokens(['slug' => '[\w-]+']);
$router->get('post.list', '/post{[page]}', $handler)->tokens(['page' =>  'page-\d+']);
$router->delete('post.delete', '/post/delete/{id}', DeleteHandler::class)->tokens(['id' => '\d+']);
$router->add('post.edit', '/post/(create|update)/{id}', CreateHandler::class, ['GET', 'POST'])
    ->tokens(['id' => '\d+'])
;

Nested groups are also supported, in which case the prefixes of all the nested groups are combined.

For more information about matching routes, see the description the following classes:

# URI generation

To generate paths, use the HttpSoft\Router\RouteCollection::path() method.

$router->get('home', '/', $handler);
$router->routes()->path('home'); // '/'

$router->get('page', '/{slug}', $handler)->tokens(['slug' => '[\w\-]+']);
$router->routes()->path('page', ['slug' => 'page-slug']); // '/page-slug'

To generate URL, use the HttpSoft\Router\RouteCollection::url() method.

$router->get('home', '/', $handler)->host('(shop|blog).example.com');
$router->routes()->url('home', []); // '/'
$router->routes()->url('home', [], 'shop.example.com'); // '//shop.example.com'
$router->routes()->url('home', [], 'blog.example.com', true); // 'https://blog.example.com'
$router->routes()->url('home', [], 'shop.example.com', false); // 'http://shop.example.com'

$router->get('page', '/{slug}', $handler)->tokens(['slug' => '[\w\-]+']);
$router->routes()->url('page', ['slug' => 'page-slug']); // '/page-slug'
$router->routes()->url('page', ['slug' => 'page-slug'], 'any-host.com'); // '//any-host.com/page-slug'

For more information about matching routes, see the description the following classes:

# Matching routes

For matching routes, it is recommended to use middleware.

In the simplest case, your application can do something like the following:

/**
 * @var HttpSoft\Router\RouteCollector $router
 * @var Psr\Http\Message\ServerRequestInterface $request
 */

if (!$route = $router->routes()->match($request, false)) {
    // 404 Not Found
}

if (!$route->isAllowedMethod($request->getMethod())) {
    // 405 Method Not Allowed
}

// 200 OK
return $route;

For more information about matching routes, see the description the following classes: