Overview

Description

Caldera Router is a complete routing system with route-groups and controllers.

As with the other Caldera components it has been built to be swappable and modular.

Installation

The easisest way to install it is to use Composer:

composer require vecode/caldera-router

Requires

  • php: >=8.1
  • ext-mbstring: *
  • psr/container: ^2.0
  • psr/http-message: ^1.0
  • psr/http-factory: ^1.0

Basic usage

Getting started

You will need a Router instance, and for that you will need to pass an instance of a ContainerInterface and a ResponseFactoryInterface implementations:

use Caldera\Router\Router;

$router = new Router($container, $response_factory);

For that you may use any PSR-11 implementation for the ContainerInterface and any PSR-17 implementation for the ResponseFactoryInterface.

Then, to create a Route for an specific HTTP method you may use one of the following methods:

$router->get(string $slug, $handler, bool $insert = false);
$router->post(string $slug, $handler, bool $insert = false);
$router->put(string $slug, $handler, bool $insert = false);
$router->patch(string $slug, $handler, bool $insert = false);
$router->options(string $slug, $handler, bool $insert = false);
$router->delete(string $slug, $handler, bool $insert = false);
$router->head(string $slug, $handler, bool $insert = false);

Each one takes a $slug that identifies the route and can contain parameters (more on that below), a $handler that can be a Closure or an array with a callable from a Controller and an $insert argument that specified whether to add the route to the beginning of the list or at the end:

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

$router->get('/about', function(ServerRequestInterface $request, ResponseInterface $response)) {
    return $response;
});

Controllers are supported too:

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

use App\Controllers\ServicesController;

$router->get('/services/{service}', [ServicesController::class, 'service']);

In this last example the service method on ServicesController will be called with the $request and $response arguments but also with a third one, $service as it has been specified in $slug when creating the route: /services/{service}.

There are other two methods that you may use, any which will match any HTTP method and match in which you can specifiy the methods that will be matched:

any(string $slug, $handler, bool $insert = false);
match(array $methods, string $slug, $handler, bool $insert = false);

For example:

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

$router->any('/contact', function(ServerRequestInterface $request, ResponseInterface $response)) {
    return $response;
});

Or better yet:

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

$router->match(['get', 'post'], '/contact', function(ServerRequestInterface $request, ResponseInterface $response)) {
    return $response;
});
Router dispatching

To do the actual routing just call the dispatch method:

$router->dispatch($request); // $request is a ServerRequestInterface instance

And it will return a ResponseInterface instance or throw a NotFoundException if no routes match the request.

You can specify the default route with the setDefault method and the path with the setDirectory method:

$router->setDefault('/home'); // Defaults to /home if not path is specified in the URL
$router->setDirectory('contosso/landings/november'); // The app is installed in http://example.com/contosso/landings/november
Named routes and reverse routing

When you add a route, you get a Route object back, and you can use it to name the route:

use App\Controllers\ServicesController;

$router->get('/services', [ServicesController::class, 'services'])
    ->setName('services.all');

$router->get('/services/{service}', [ServicesController::class, 'service'])
    ->setName('services.single');

Once you've set a name, you can use it for reverse routing:

$route = $router->route('services.all'); // $route will equal to '/services'
$route = $router->route('services.single'); // A RuntimeException will be thrown

If the route has required (non-optional) parameters you must pass them in an array:

$route = $router->route('services.single', ['service' => 'consulting']); // $route will equal to '/services/consulting'
Route parameters

Routes can have parameters, to specify a parameter just use the notation {parameter_name} on the $slug argument:

use App\Controllers\ServicesController;

$router->get('/services/{service}', [ServicesController::class, 'services']);

You may specify several parameters, also you can make them optional by appending the ? token:

use App\Controllers\ServicesController;

$router->get('/services/{service}/{subservice?}', [ServicesController::class, 'service']);

Also, you may specify custom constraints for each parameter with the setConstraint method:

use App\Controllers\UsersController;

$router->get('/users', [UsersController::class, 'all'])
    ->setName('users.all');

$router->get('/users/edit/{id}', [UsersController::class, 'edit'])
    ->setName('users.edit')
    ->setConstraint('id', '\d+');

In this case the \d+ expression restricts the id parameter to integers.

Route groups

You may create route groups to keep them organized and to make it easier to configure them, for example:

use App\Controllers\UsersController;

$router->group('users', function(Group $group) {
    $router->get('/', [UsersController::class, 'all'])
        ->setName('all');
    $router->match(['get', 'post'], '/new', [UsersController::class, 'new'])
        ->setName('new');
    $router->match(['get', 'post'], '/edit/{id}', [UsersController::class, 'edit'])
        ->setName('edit')
        ->setConstraint('id', '\d+');
    $router->match(['get', 'post'], '/delete/{id}', [UsersController::class, 'delete'])
        ->setName('delete')
        ->setConstraint('id', '\d+');
})->setName('users');

This code will create a CRUD-style route group where /users will show all the users, /users/new will create a new user, /users/edit/12 will edit an user with id=12 and so on.

Note the $router->group('users' part, as it is used as the prefix for all the child routes.

Also each route has a name, and the group too, so for reverse routing you must use dot-notation to get a child route, for example:

$route = $router->route('users/new'); // Will throw a RuntimeException
$route = $router->route('users.new'); // Will return /users/new
Controllers

Controllers allow for a better project structure and logic separation.

A Controller is just a class with methods that are bound to certain routes and that will be called when a user visits that route.

To get started, just create a class that extends Controller:

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

use Caldera\Router\Controller;

class UsersController extends Controller {

    public function index(ServerRequestInterface $request, ResponseInterface $response) {
        $response->getBody()->write('USERS.INDEX');
    }
    public function new(ServerRequestInterface $request, ResponseInterface $response) {
        $response->getBody()->write('USERS.NEW');
    }
    public function edit(ServerRequestInterface $request, ResponseInterface $response, int $id) {
        $response->getBody()->write( sprintf('USERS.EDIT:%d', $id) );
    }
    public function delete(ServerRequestInterface $request, ResponseInterface $response, int $id) {
        $response->getBody()->write( sprintf('USERS.DELETE:%d', $id) );
    }
}

As simple as that. Then bind the routes to the controller:

use App\Controllers\UsersController;

$router->group('users', function(Group $group) {
    $router->get('/', [UsersController::class, 'all'])
        ->setName('all');
    $router->match(['get', 'post'], '/new', [UsersController::class, 'new'])
        ->setName('new');
    $router->match(['get', 'post'], '/edit/{id}', [UsersController::class, 'edit'])
        ->setName('edit')
        ->setConstraint('id', '\d+');
    $router->match(['get', 'post'], '/delete/{id}', [UsersController::class, 'delete'])
        ->setName('delete')
        ->setConstraint('id', '\d+');
})->setName('users');

And you're set. Just call dispatch on your Router instance and each method will be called when required.

The controller instance will be through from the ContainerInterface instance, so you'll either have to register it as a service OR make sure that the container has class visibility (either by using a use clause or by specifying the FQN of the class) AND that the container can resolve classes by its FQN (it is not part of the PSR-11 specification but most implementations support it).

The good news is that if the selected container implementation supports dependency injection on the constructor you'll be able to add dependencies as constructor arguments on the controller, but also on the handler methods as the Router tries to resolve each argument that is specified (starting with $request and $response):

use App\Forms\ContactForm;

$router->get('/contact', function(ServerRequestInterface $request, ResponseInterface $response, ContactForm $form) {
    $response->getBody()->write( $form->toHtml() );
})->setName('contact');

If you try to specify a method that doesn't exist you will get a BadMethodCallException.