willy68 / pg-router
A fast, flexible, and PSR-7 compatible router for PHP.
Requires
- php: ~8.1 || ~8.2 || ~8.3 || ~8.4
- fig/http-message-util: 1.1.5
- psr/cache: ^3.0
- psr/container: ^1.0 || ^2.0
- psr/http-message: ^1.0.1
- psr/http-server-middleware: ^1.0
- symfony/cache: ^7.2
Requires (Dev)
- guzzlehttp/psr7: ^2.2
- phpbench/phpbench: ^1.4
- phpunit/phpunit: ^10.1
- squizlabs/php_codesniffer: ^3.6
- symfony/var-dumper: ^7.0
- willy68/response-sender: ^1.0
README
A fast, flexible, and PSR-7 compatible HTTP router for PHP applications. Built for performance with support for advanced routing patterns, middleware stacking, and route caching.
Table of Contents
- Features
- Credits
- Requirements
- Installation
- Quick Start
- Advanced Usage
- Route Caching
- Performance
- Testing
- Contributing
- License
Features
- ✅ PSR-7 Compatible: Full support for PSR-7 request interfaces
- 🚀 High Performance: Optimized route matching with caching support
- 🎯 Flexible Routing: Support for complex route patterns and constraints
- 🔧 Middleware Support: Route-level and group-level middleware stacking
- 📦 Route Grouping: Organize routes with shared prefixes and middlewares
- 🎨 Named Routes: Easy URL generation with named route support
- ⚡ CRUD Helpers: Quick REST resource route generation
- 🔀 Optional Segments: Advanced optional route segment support
- 🏗️ Extensible: Custom matchers and collectors for advanced use cases
Credits
The regex pattern implementation and benchmarking are inspired by nikic/Fast-Route.
For a detailed explanation of how this approach works and why it is fast, please read nikic's blog post.
This router's implementation is original and may differ in speed or structure compared to Fast-Route.
Some parts do not strictly follow the same patterns as nikic/Fast-Route.
Requirements
- PHP 8.1 or higher
- Composer
Installation
Install via Composer:
composer require willy68/pg-router
Quick Start
use Pg\Router\Router; use guzzlehttp\Psr7\Response; use guzzlehttp\Psr7\ServerRequest; $request = ServerRequest::fromGlobals(); // Create a PSR-7 request from global variables $router = new Router(); $router->route('/hello/{name: \w+}', function ($request): ResponseInterface { $name = $request->getAttribute('name'); (return new Response())->getBody()->write("Hello, $name!"); }, 'hello', ['GET']); $res = $router->match($request); if ($res->isSuccess()) { // add route attributes to the request foreach ($res->getMatchedAttributes() as $key => $val) { $request = $request->withAttribute($key, $val); } $callback = $res->getMatchedRoute()->getCallback(); $response = $callback($request);
Advanced Usage
Route Parameters
// Simple parameter, id matches any alphanumeric string $router->route('/user/{id}','handler', 'user.show', ['GET']); //Define route parameters with custom patterns: // Parameter with regex constraint $router->route('/user/{id:\d+}','handler', 'user.show', ['GET']); // Multiple parameters $router->route('/blog/{year:\d{4}}/{month:\d{2}}/{slug}','handler', 'blog.post', ['GET']);
It is possible to define default tokens for parameters:
// The id matches a numeric string for this route $route = $router->route('/user/{id}','handler', 'user.show', ['GET']) ->setTokens(['id' => '\d+']); // Default token for all routes before adding a new one $router->setTokens(['id' => '\d+']); // Or with The Configuration in the constructor $router = new Router( null, null, [Router::CONFIG_DEFAULT_TOKENS => ['id' => '[0-9]+', 'slug' => '[a-zA-Z-]+[a-zA-Z0-9_-]+']] ); // Update and/or add tokens given by the Router $route->updateTokens(['id' => '[0-9]+', 'slug' => '[a-zA-Z0-9_-]+']);
Optional Segments
Breaking Change: Optional segments now use a new syntax with [!...;...]
pattern:
// Example route with optional segments $router->route('/article[!/{id: \d+};/{slug: [\w-]+}]', function ($request) { $id = $request->getAttribute('id', null); $slug = $request->getAttribute('slug', null); // ... }, 'article.show', ['GET']);
This route matches:
/article
(no parameters)/article/123
(id parameter only)/article/123/my-article-title
(both id and slug parameters)
Parameters will be available in the request based on the provided segments.
Route Groups
Organize related routes with shared prefixes and middlewares:
$router->group('/api/v1', function ($group) { $group->route('/users', 'UserController::index', 'api.users.index', ['GET']); $group->route('/users/{id:\d+}', 'UserController::show', 'api.users.show', ['GET']); $group->route('/users', 'UserController::store', 'api.users.store', ['POST']); })->middlewares([AuthMiddleware::class, ApiMiddleware::class]);
Middleware
Apply middleware to individual routes or groups:
// Route-level middleware $router->route('/admin/dashboard','handler', 'admin.dashboard', ['GET']) ->middlewares([AuthMiddleware::class, AdminMiddleware::class]); // Group-level middleware $router->group('/admin', function ($group) { $group->route('/users', 'AdminController::users', 'admin.users', ['GET']); $group->route('/settings', 'AdminController::settings', 'admin.settings', ['GET']); })->middlewares([AuthMiddleware::class, AdminMiddleware::class]);
Named Routes
Generate URLs using named routes:
// Define a named route $router->route('/user/{id:\d+}','handler', 'user.profile', ['GET']); // Generate URL $url = $router->generateUri('user.profile', ['id' => 123]); // Result: /user/123 // Generate URL with query parameters $url = $router->generateUri('user.profile', ['id' => 123], ['tab' => 'settings']); // Result: /user/123?tab=settings
CRUD Helper
$router->crud('/posts', PostController::class, 'posts');
This creates the following routes:
GET /posts
→PostController::index
(posts.index)GET /posts/new
→PostController::create
(posts.create)POST /posts/new
→PostController::create
(posts.create.post)GET /posts/{id:\d+}
→PostController::edit
(posts.edit)POST /posts/{id:\d+}
→PostController::edit
(posts.edit.post)DELETE /posts/{id:\d+}
→PostController::delete
(posts.delete)
URL Generation
// Basic URL generation $url = $router->generateUri('hello', ['name' => 'Alice']); // With query parameters $url = $router->generateUri('user.profile', ['id' => 123], ['tab' => 'settings']);
Route Caching
Enable route caching for production environments:
// Enable caching with a cache file $router = new Router ( null, null, [ Router::CONFIG_CACHE_ENABLED => ($env === 'prod'), Router::CONFIG_CACHE_DIR => '/tmp/cache', Router::CONFIG_CACHE_POOL_FACTORY => function (): CacheItemPoolInterface {...}, ] ) // In production, routes are cached and loaded from the cache file // In development, disable caching or clear cache when routes change $router->clearCache();
Router::CONFIG_CACHE_POOL_FACTORY
allows you to use a custom PSR-6 compatible cache pool implementation,
but this parameter is optional.
Using FileCache with Regex Collector
The FileCache
class provides a simple file-based caching solution that works well with all regex collectors.
Here's a practical example of using FileCache
with route collectors:
use Pg\Router\Cache\FileCache; use Pg\Router\RegexCollector\MarkRegexCollector; use Pg\Router\RegexCollector\NamedRegexCollector; use Pg\Router\RegexParser\FastMarkParser; // Initialize the cache with a custom directory $fileCache = new FileCache( cacheDir: __DIR__ . '../tmp/cache/router', // Directory to store cache files useCache: true // Enable caching ); // Example of using the cache with a route collection $collectors = [ 'MarkRegexCollector' => new MarkRegexCollector(), 'NamedCollector' => new NamedRegexCollector(), 'FastMarkCollector' => new MarkRegexCollector(new FastMarkParser()), // Add other collectors if needed ]; // Sample routes $allRoutes = [ // Your route definitions here ]; foreach ($collectors as $name => $collector) { $collector->addRoutes($allRoutes); $hasCache = $fileCache->has($name); // Fetch data with caching // The callback will only execute if the cache is empty $startTime = microtime(true); $routesData = $fileCache->fetch($name, [$collector, 'getData']); $duration = microtime(true) - $startTime; echo "<h2>Collector: $name</h2>"; echo "<div style='color:green;'>"; echo "Execution time: " . number_format($duration * 1000, 4) . " ms<br>"; echo "Cached: " . ($hasCache ? 'Yes' : 'No') . "<br>"; echo "</div>"; } // Clear specific cache if needed // $fileCache->delete('MarkRegexCollector'); // Or clear all caches // $fileCache->clear();
In this example:
- We create a
FileCache
instance pointing to a cache directory - We set up all available Collectors for route collection
- We use
fetch()
which will return cached data if available, or execute the callback to generate and cache the data - We measure and display the execution time to demonstrate the performance benefit of caching
The fetch()
method is particularly useful as it handles both the cache check and the callback execution in one call, making your code cleaner and more efficient.
Using the returned data directly in a Matcher like:
$data = $fileCache->fetch($name, [$collector, 'getData']); $matcher = new \Pg\Router\Matcher\MarkDataMatcher($data); $matches = $matcher->match('/post/{id: \d+}', 'GET');
Performance
pg-router is designed for high performance:
- Optimized Route Matching: Uses efficient algorithms for route compilation and matching
- Route Caching: Cache compiled routes for production use
- Minimal Memory Footprint: Efficient memory usage for large route tables
- Fast Parameter Extraction: Optimized parameter extraction from matched routes
Testing
This project uses PHPUnit for testing. To run the tests, ensure you have PHPUnit installed via Composer:
# Run all tests ./vendor/bin/phpunit # Run tests with coverage php -dxdebug.mode=coverage ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage # or composer run coverage # Run specific test file ./vendor/bin/phpunit tests/RouterTest.php
Contributing
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
Please ensure your code follows PSR-12 coding standards and includes appropriate tests.
License
This project is licensed under the MIT License. See the LICENSE file for details.
Author: William Lety Maintainer: willy68 Repository: pg-router