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:
- StaticParser: Handles exact route matches.
- DynamicParser: Processes routes with parameters.
- 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
-
Parser Order: Add parsers in order of specificity.
- StaticParser first (faster, more specific).
- DynamicParser next.
- FileSystemParser last (more flexible but slower).
-
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'); -
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'); -
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. } -
URL Generation: Always use route names instead of hardcoded URLs.
// Bad $url = '/users/' . $id; // Good $url = $router->generate('users.show', ['id' => $id]); -
Request Context: Configure context if absolute URLs are needed.
$router->setContext(new RequestContext( baseUrl: '/myapp', scheme: 'https', host: 'example.com', httpPort: 80, httpsPort: 443 ));