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

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 10/10/2026 by Anonymous