chevere/router

Chevere router package

0.8.0 2025-07-17 14:24 UTC

README

Chevere

Build Code size Apache-2.0 PHPStan Mutation testing badge

Quality Gate Status Maintainability Rating Reliability Rating Security Rating Coverage Technical Debt CodeFactor

Summary

Router is a library for creating routing systems for chevere/http. It is compatible with the following PHP-FIG PSR:

  • PSR-7: HTTP message interfaces
  • PSR-11: Container interface
  • PSR-15: HTTP Server Request Handlers
  • PSR-17: HTTP Factories

Installing

Router is available through Packagist and the repository source is at chevere/router.

composer require chevere/router

Quick start

// Define routes
$routes = routes(
    route('/hello', GET: 'hello.twig'),
    route('/api/users',
        GET: UserListController::class,
        POST: headless(UserCreateController::class, CsrfMiddleware::class)
    ),
    route('/products/{id}',
        GET: bind(ProductGetController::class, 'product.twig'),
        PUT: ProductUpdateController::class,
        DELETE: ProductDeleteController::class
    )
);
// Create router
$router = router($routes);
// Handle request
$routed = routed($serverRequest, $router, $responseFactory, $container);
$response = $routed->response();
$return = $routed->return(); // Controller return value

What it does

The Router library allows you to bind paths to HTTP methods and logic. It maps paths to their corresponding HTTP controller actions, views, and middleware pipelines. It also collects and validates the views and dependencies used in the routing process.

For example, to resolve this:

GET /product/123
    -> ProductGet::main(123):context
    -> product.twig

DELETE /product/123
    -> ProductDelete::main(123)

You need to write the following route code:

$route = route(
    '/product/{id}',
    GET: bind(ProductGet::class, 'product.twig'),
    DELETE: ProductDelete::class,
);

And the HTTP controllers may look like this:

class ProductGet extends Controller
{
    protected function main(string $id): array
    {
        // ...
        return $context;
    }
}

class ProductDelete extends Controller
{
    protected function main(string $id): void
    {
        // ...
    }
}

Bind

A Bind is the conjunction of a controller, its middleware pipeline and a view. Use helper function bind($controller, $view, ...$middleware) to explicitly create a binding.

$bind = bind(
    controller: ProductGet::class,
    view: 'product.twig',
    ...$middleware // PSR-15
)

Use method controllerName() to access the ControllerName API.

$bind->controllerName(); // ProductGet

Use method view() to access the view string.

$bind->view(); // product.twig

Use method middlewares() to access the Middlewares collection API.

$bind->middlewares();

Headless

Use helper function headless($controller, ...$middleware) to define a bind without a view, only the controller and middleware.

headless(ProductGet::class, ...$middleware)

Route

Use the helper function route($path, ...) to define path endpoints. An endpoint is defined as the conjunction of a path, an HTTP method and a controller.

route('/hello-world', GET: ..., POST: ...)

View route

When no controller is needed you can bind the HTTP method to the target view, without indicating a controller.

route('/', GET: 'home.twig')

Headless route

When no view is needed you can bind the HTTP method to the target controller, without indicating a view.

route(
    '/product/{id}',
    DELETE: ProductDelete::class,
)

Dynamic route

Dynamic routes use variable wildcards ({variable} syntax) to denote variable path components.

route('/products/{id}', GET: MyController::class);

Where MyController::main method parameters must match the defined wildcards:

protected function main(string $id) {...}

Path variables implicit match against [^/]+. To customize use StringAttr on main’s function parameters.

use Chevere\Parameter\Attributes\StringAttr;

protected function main(
    #[StringAttr('/\d+/')]
    string $id
) {
    // $id is digits only
}

The view argument

Use the view argument to define the same view for all endpoints.

route(
    '/login',
    view: 'login.twig',
    GET: LoginGet::class,
    POST: LoginPost::class,
)

Use helpers like bind() or headless() to override the base view argument definition.

The middleware argument

Use the middleware argument to define the same middleware pipeline for all endpoints.

route(
    '/signup',
    view: 'signup.twig',
    middleware: middlewares(
        RedirectIfLogged::class, // PSR-15
        TurnstileVerify::class, // PSR-15
    ),
    GET: SignUpGet::class,
    POST: SignUpPost::class,
)

Use helpers like bind() or headless() to override the base middleware argument definition.

The exclude argument

The exclude argument enables to define middleware names that should be excluded from the pipeline.

route(
    '/logout',
    exclude: middlewares(
        TurnstileVerify::class, // PSR-15
        ChallengeTwoFactor::class, // PSR-15
    ),
    POST: LogoutPost::class,
)

Middlewares

The Middlewares API enables you to organize PSR-15 middleware for your routes. Use the helper function middlewares(...$middleware) to create a Middlewares collection.

$middlewares = middlewares(
    SessionMiddleware::class,
    AuthMiddleware::class
);

You can use this collection in route definitions to apply middleware to specific routes.

Routes

The Routes API enables to collect, assert, inspect and organize Route objects.

Use helper function routes(...$route|$routes) to create a Routes object.

$routes = routes(
    route(...),
    routes(...),
);

Managing routes

Use method withRoute(...$route) to add Route objects, use method withRoutes(...$routes) to add Routes objects.

$routes = $routes->withRoute($route);
$routes = $routes->withRoutes($moreRoutes);

Use method has(...$path) to tell if the path is already routed. Use get($path) to retrieve the route for a given path.

$routes = $routes->has('/pricing'); // bool
$home = $routes->get('/pricing'); // RouteInterface

Middleware pipelines

Use method withPrependMiddleware($midlewares) to prepend middleware to the begin of the pipeline. Use this for middleware that must resolve early in execution order.

$routes = $routes->withPrependMiddleware(
    middlewares(
        SessionSetUpCSRFToken::class, // PSR-15
    )
)

Use method withAppendMiddleware($middlewares) to append middleware to the end of the pipeline. Use this for middleware that must resolve last in execution order.

$routes = $routes->withAppendMiddleware(
    middlewares(
        SessionCheckCSRFToken::class, // PSR-15
    )
)

Views

The Views API enables to assert the views collected for all routes. Use assert($viewsDir) method to assert that the views directory contains the view names defined in routes.

$router->views()->assert($viewsDir);

Container

Use new Container(...) to create the dependency container by passing entries you may need to manually create.

$container = new Container(
    database: $database,
    // other entries
);

Adding entries

Use method with(...$entries) to add one or more named entries to the container.

$container = $container->with(
    status: 'challenged',
    challenge: '2fa',
);

Accessing entries

Use method has($name) to tell if the container has an entry by name. Use method get($name) to retrieve the entry value.

$container->has('session'); // bool true
$session = $container->get('session'); // SessionInterface

Automatic dependency injection

Use method withAutoInject($deps, ...$ignore) to automatically inject missing dependencies recursively.

$container = $container->withAutoInject($deps, ...$ignore);

The ignore argument allows you to define dependencies that should be ignored, which is useful for dependencies that must be late injected after the middleware pipeline resolves.

Dependencies

The Dependencies API enables to interact with the dependencies detected for all routing participants. The Router will collect every __construct on both controllers and middleware, to provide a collection where you can use your own dependency injection logic.

$deps = $router->dependencies();

Assert dependencies

Use method assert($container) to assert that the PSR-11 $container meets the required dependencies.

$deps->assert($container);

This is an additional guard that can be used to static detect missing dependencies. On runtime, the system will throw an exception before even reach the middleware layer.

Routed

The Routed API enables to interact with the outcome result of the routing process. Use helper function routed(...) to resolve routing.

$routed = routed(
    $serverRequest,   // PSR-7
    $router,
    $responseFactory, // PSR-17
    $container,       // PSR-11
    $callback
);

Late dependency injection

The $callback argument enables to pass logic that will resolve after the middleware pipeline and before the controller layer.

use Chevere\Router\Interfaces\ContainerInterface;

$callback = function (ContainerInterface $container): ContainerInterface {
    $session = $container->get('sessionFactory')->newSession(
        $container->get('requestUser')->sessionId
    );

    return $container->with(
        session: $session,
        user: $session->getOrDefault('user')
    );
};

Routed outcome

Use method hasThrowable() to tell if routing throws an exception. Use method throwable() to access the exception (if any).

$hasThrowable = $routed->hasThrowable(); // bool
$throwable = $routed->throwable();

The system is flexible as it enables to define catch-all strategies. For example, you may want to catch ControllerException objects and pass-by everything else.

if ($routed->hasThrowable()
    && ! ($routed->throwable() instanceof ControllerException)
) {
    throw $routed->throwable();
}

Use method response() to access the routed PSR-7 response object.

$psr7Response = $routed->response();

Use method bind() to access the routed Bind API, which enables to tell the controller, view and middleware.

$bind = $routed->bind();

Use method return() to access to the controller return value. This is the value after Action I/O guard layer.

$controllerReturn = $routed->return();

Documentation

Documentation is available at chevere.org.

License

Copyright Rodolfo Berrios A.

Chevere is licensed under the Apache License, Version 2.0. See LICENSE for the full license text.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.