Complete Usage Guide

Derafu Routing is a flexible PHP routing library that uses a parser-based architecture. Instead of having a monolithic router, it separates routing logic into specialized parsers, each handling different types of routes.

Installation

composer require derafu/routing

Dependency Injection

Import routing-services.yaml to register a shared RouterInterface with static and dynamic parsers. UrlGeneratorInterface, RouteRegistrarInterface and RouteMatcherInterface resolve to that same router.

imports:
    - { resource: '../vendor/derafu/routing/resources/config/routing-services.yaml' }

parameters:
    derafu.routing.routes:
        article:
            path: '/articles/{slug}'
            handler: 'App\Controller\ArticleController::show'

The routes parameter defaults to an empty array. Applications using Derafu Kernel’s route loader can set derafu.routing.routes: '%routes%' to reuse the routes it loads. Routes added through RouteRegistrarInterface are immediately available for matching and URL generation.

Additional parsers join the derafu_routing.parser tagged collection. Higher priority runs first; built-in static and dynamic parsers have priorities 300 and 200. For example, this application parser runs before both:

services:
    App\Routing\CustomParser:
        tags:
            - { name: derafu_routing.parser, priority: 400 }

To enable file-based routes, import routing-filesystem-services.yaml instead. It includes the base configuration and adds the filesystem parser at priority 100:

imports:
    - { resource: '../vendor/derafu/routing/resources/config/routing-filesystem-services.yaml' }

parameters:
    derafu.routing.filesystem.directories:
        - '%kernel.project_dir%/templates/pages'
    derafu.routing.filesystem.extensions: ['.html.twig', '.md']

Directories default to an empty array; the package assumes no project directory layout. The default extensions are .html.twig and .md. Static and dynamic routes take precedence over files.

The router explicitly uses its internal URL generator over the same route collection. Wiring its $urlGenerator argument to the public generator alias would create a circular dependency. Integrations using an external generator or registrar, such as Symfony adapters, should supply their own composition instead of importing this standalone router configuration.

Basic Concepts

Parser Architecture

The routing system is built around specialized parsers:

  1. StaticParser: Handles exact route matches.
  2. DynamicParser: Processes routes with parameters.
  3. FileSystemParser: Maps URLs to physical files.

Each parser implements the ParserInterface:

interface ParserInterface {
    public function parse(string $uri, array $routes): ?RouteMatchInterface;
    public function supports(RouteInterface $route): bool;
}

Route Types

Static Routes

The simplest form of routing, handled by StaticParser:

$router = new Router([new StaticParser()]);
$router->addRoute('about', '/about', 'PagesController::action');
$router->addRoute('contact', '/contact', 'ContactController::show');

Dynamic Routes

Handled by DynamicParser, supporting various parameter types:

$router->addParser(new DynamicParser());

// Basic parameter.
$router->addRoute('user.show', '/users/{id}', 'UserController::show');

// Validation with regular expressions.
$router->addRoute('user.show', '/users/{id:\d+}', 'UserController::show');

// Optional parameters.
$router->addRoute('blog.index', '/blog/{year?}', 'BlogController::index');

// Multiple parameters.
$router->addRoute('blog.archive', '/blog/{year}/{month?}', 'BlogController::archive');

// Complex patterns.
$router->addRoute('user.profile', '/users/{username:[a-z0-9_-]+}', 'UserController::profile');

Filesystem Routes

The FileSystemParser maps URLs to actual files:

$parser = new FileSystemParser(
    directories: [__DIR__ . '/pages'],
    extensions: ['.html.twig', '.md']
);
$router->addParser($parser);

Directory structure:

pages/
├── about.md           # Matches /about
├── contact.twig       # Matches /contact
└── blog/
    ├── post-1.md     # Matches /blog/post-1
    └── post-2.md     # Matches /blog/post-2

Using the Router

Basic Configuration

use Derafu\Routing\Router;
use Derafu\Routing\Parser\StaticParser;
use Derafu\Routing\Parser\DynamicParser;

$router = new Router([
    new StaticParser(),
    new DynamicParser(),
]);

Adding Routes

// String handler (Controller@action).
$router->addRoute('users.index', '/users', 'UserController::index');

// Closure handler.
$router->addRoute('api.data', '/api/data', function($params) {
    return ['data' => 'value'];
});

// Array handler.
$router->addRoute('blog.index', '/blog', [
    'controller' => 'BlogController',
    'action' => 'list'
]);

// A route with default values for its parameters.
$router->addRoute(
    name: 'user.show',
    path: '/users/{id}',
    handler: 'UserController::show',
    defaults: ['active' => true]
);

Route Matching

try {
    $match = $router->match('/users/123');
    // $match->getHandler(): Returns the route handler.
    // $match->getParameters(): Returns the route parameters.
    // $match->getName(): Returns the route name if defined.
} catch (RouteNotFoundException $e) {
    // Handle 404.
}

The Path Is Matched in Its Canonical Form

A path can be written in many ways that mean the same thing: /api//index, /api/./index, /api/%69ndex and /api/index/ are /api/index. The router matches the canonical form of the path (see Url), so every way of writing it gives the same match, and the parsers and the match never see the other forms. A dynamic route that takes everything below /api (/api/{resource:.+}) gets resource as index for all of them, not /index or %69ndex.

$router->addRoute('index', '/api/index', 'IndexController::action');

$router->match('/api//index')->getName();     // "index"
$router->match('/api/./index')->getName();    // "index"
$router->match('/api/%69ndex')->getName();    // "index"
$router->match('')->getName();                // The route of "/", an empty path is the root.

A path that has no safe canonical form is not matched: match() throws an InvalidPathException (its code is 400) instead of a RouteNotFoundException. They are the paths that have a .. segment (also as %2e%2e), a separator hidden in an escape (%2F, %5C) or a backslash, a control character (the null byte among them) or an escape that is not valid. Their meaning depends on who reads them, so none is served. The message does not have the path, which is what a client sent.

try {
    $router->match('/api/../admin');
} catch (InvalidPathException $e) {
    // A bad request: $e->getCode() is 400. $e->getUri() has the path.
}

This matters to whoever decides by the text of the path: a rule that protects /api/index and a router that reads its meaning must be about the same path, or /api//index would get to the route without being under the rule. RouterMiddleware of derafu/http also gives the rest of the pipeline the canonical path.

Roles of a Route

A route can declare the roles that it is for. The router only keeps them, as data of the route: it does not decide who can access. That is up to whoever uses the router, for example Derafu Auth.

reports:
    path: /dashboard/reports
    handler: ReportsController::index
    roles: [analyst, admin]
$router->addRoute('reports', '/dashboard/reports', 'ReportsController::index', roles: ['analyst', 'admin']);

$match = $router->match('/dashboard/reports');

$match->getRoles();                        // ['analyst', 'admin']
$match->hasRole('admin');                  // true
$match->hasAnyRole(['guest', 'analyst']);  // true
$match->hasAnyRole([]);                    // false

A route that declares no roles has none (getRoles() is []): the router does not say it is open or closed, it is the caller that decides what that means.

URL Generation

The router allows generating URLs from named routes:

// Set request context (needed for absolute URLs).
$router->setContext(new RequestContext(
    baseUrl: '/myapp',
    scheme: 'https',
    host: 'example.com'
));

// Generate relative URLs.
$url = $router->generate('user.show', ['id' => 123]); // /myapp/users/123
$url = $router->generate('blog.archive', [
    'year' => '2024',
    'month' => '03'
]); // /myapp/blog/2024/03

// Generate URL without optional parameter.
$url = $router->generate('blog.archive', [
    'year' => '2024'
]); // /myapp/blog/2024

// Generate absolute URL.
$url = $router->generate('about', [], UrlReferenceType::ABSOLUTE_URL);
// https://example.com/myapp/about

// Generate network path URL.
$url = $router->generate('about', [], UrlReferenceType::NETWORK_PATH);
// //example.com/myapp/about

Available reference types are:

  • ABSOLUTE_PATH: Absolute path from root (default).
  • ABSOLUTE_URL: Complete URL with scheme and host.
  • NETWORK_PATH: URL without scheme (useful for resources that work on both HTTP and HTTPS).

The Dispatcher

The dispatcher handles the execution of matching routes:

$dispatcher = new Dispatcher([
    'md' => function($file, $params) {
        // Render markdown file.
        return parseMarkdown(file_get_contents($file));
    },
    'twig' => function($file, $params) {
        // Render Twig template.
        return $twig->render($file, $params);
    }
]);

$result = $dispatcher->dispatch($match);

Note: This is a very basic dispatcher, you should implement your own.

Advanced Usage

Custom Parser Example

class RegexParser implements ParserInterface
{
    public function parse(string $uri, array $routes): ?RouteMatchInterface
    {
        foreach ($routes as $route) {
            if (!$this->supports($route)) {
                continue;
            }

            // Custom regex matching logic
            if (preg_match($route->getPath(), $uri, $matches)) {
                return new RouteMatch(
                    $route->getHandler(),
                    $matches
                );
            }
        }
        return null;
    }

    public function supports(RouteInterface $route): bool
    {
        // Define what routes this parser can handle
        return str_starts_with($route->getPath(), '#');
    }
}

Best Practices

  1. Parser Order: Add parsers in order of specificity.

    • StaticParser first (faster, more specific).
    • DynamicParser next.
    • FileSystemParser last (more flexible but slower).
  2. Route Organization: Group related routes.

    // User management
    $router->addRoute('users.index', '/users', 'UserController::index');
    $router->addRoute('users.show', '/users/{id}', 'UserController::show');
    
    // Blog system
    $router->addRoute('blog.index', '/blog', 'BlogController::index');
    $router->addRoute('blog.show', '/blog/{slug}', 'BlogController::show');
    
  3. Parameter Validation: Use regex constraints for better security.

    // Ensure ID is numeric.
    $router->addRoute('users.show', '/users/{id:\d+}', 'UserController::show');
    
    // Validate username format.
    $router->addRoute('users.profile', '/users/{username:[a-z0-9_-]+}', 'UserController::profile');
    
  4. Error Handling: Always wrap matches in try-catch.

    try {
        $match = $router->match($uri);
        $result = $dispatcher->dispatch($match);
    } catch (RouteNotFoundException $e) {
        // Handle 404.
    } catch (DispatcherException $e) {
        // Handle dispatcher errors.
    }
    
  5. URL Generation: Always use route names instead of hardcoded URLs.

    // Bad
    $url = '/users/' . $id;
    
    // Good
    $url = $router->generate('users.show', ['id' => $id]);
    
  6. Request Context: Configure context if absolute URLs are needed.

    $router->setContext(new RequestContext(
        baseUrl: '/myapp',
        scheme: 'https',
        host: 'example.com',
        httpPort: 80,
        httpsPort: 443
    ));
    
On this page

Last updated on 30/10/2026 by Anonymous