julienlinard / php-router
Un routeur PHP moderne et complet avec support des routes dynamiques, middlewares, groupes de routes et gΓ©nΓ©ration d'URL. Compatible PHP 8+ avec Attributes.
Installs: 204
Dependents: 2
Suggesters: 0
Security: 0
Stars: 13
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/julienlinard/php-router
Requires
- php: >=8.0
Requires (Dev)
- phpunit/phpunit: ^11.5
README
π«π· Read in French | π¬π§ Read in English
π Support the project
If this bundle is useful to you, consider becoming a sponsor to support the development and maintenance of this open source project.
A modern and complete PHP router for managing your application routes with support for dynamic routes, middlewares, and all essential features.
π Table of Contents
- Installation
- Quick Start
- Route Definition
- Dynamic Routes
- Route Groups
- URL Generation
- Request
- Response
- Middlewares
- Error Handling
- API Reference
- Complete Examples
π Installation
Use Composer to install the package:
composer require julienlinard/php-router
Requirements: PHP 8.0 or higher
β‘ Quick Start
<?php require_once __DIR__ . '/vendor/autoload.php'; use JulienLinard\Router\Router; use JulienLinard\Router\Request; use JulienLinard\Router\Response; use JulienLinard\Router\Attributes\Route; // Create a router instance $router = new Router(); // Define a controller with routes class HomeController { #[Route(path: '/', methods: ['GET'], name: 'home')] public function index(): Response { return new Response(200, 'Welcome!'); } } // Register routes $router->registerRoutes(HomeController::class); // Handle the request $request = new Request(); $response = $router->handle($request); // Send the response $response->send();
π£οΈ Route Definition
Routes are defined in your controllers using the Route attribute (PHP 8).
Simple Route
<?php namespace App\Controller; use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Request; use JulienLinard\Router\Response; class HomeController { #[Route(path: '/', methods: ['GET'], name: 'home')] public function index(): Response { return new Response(200, 'Homepage'); } }
Routes with Multiple HTTP Methods
class ApiController { #[Route(path: '/api/users', methods: ['GET'], name: 'api.users.index')] public function index(): Response { return Response::json(['users' => []]); } #[Route(path: '/api/users', methods: ['POST'], name: 'api.users.store')] public function store(Request $request): Response { $data = $request->getBody(); // Process data... return Response::json(['message' => 'User created'], 201); } }
Route Registration
$router = new Router(); $router->registerRoutes(HomeController::class); $router->registerRoutes(ApiController::class);
Route Groups
Route groups allow you to organize your routes with a common prefix and shared middlewares.
use JulienLinard\Router\Middlewares\AuthMiddleware; // Group with prefix only $router->group('/api', [], function($router) { $router->registerRoutes(ApiController::class); // All routes will have the /api prefix }); // Group with prefix and middlewares $router->group('/admin', [AuthMiddleware::class], function($router) { $router->registerRoutes(AdminController::class); // All routes will have the /admin prefix AND the AuthMiddleware }); // Nested groups $router->group('/api', [], function($router) { $router->group('/v1', [], function($router) { $router->registerRoutes(ApiV1Controller::class); // Routes with /api/v1 prefix }); $router->group('/v2', [], function($router) { $router->registerRoutes(ApiV2Controller::class); // Routes with /api/v2 prefix }); });
Complete Example:
class ApiController { // Path defined in controller: '/users' #[Route(path: '/users', methods: ['GET'], name: 'api.users.index')] public function index(): Response { return Response::json(['users' => []]); } } // Registration with group $router->group('/api', [], function($router) { $router->registerRoutes(ApiController::class); }); // The route will be accessible at: /api/users
π Dynamic Routes
The router supports dynamic routes with parameters automatically extracted from the URL.
Route with One Parameter
class UserController { #[Route(path: '/user/{id}', methods: ['GET'], name: 'user.show')] public function show(Request $request): Response { $userId = $request->getRouteParam('id'); return Response::json([ 'user_id' => $userId, 'message' => "Displaying user {$userId}" ]); } }
Example URL: /user/123 β $userId = '123'
Route with Multiple Parameters
class PostController { #[Route(path: '/user/{userId}/post/{slug}', methods: ['GET'], name: 'post.show')] public function show(Request $request): Response { $userId = $request->getRouteParam('userId'); $slug = $request->getRouteParam('slug'); return Response::json([ 'user_id' => $userId, 'slug' => $slug ]); } }
Example URL: /user/123/post/my-article β $userId = '123', $slug = 'my-article'
Accessing Parameters
// Get a specific parameter $id = $request->getRouteParam('id'); $id = $request->getRouteParam('id', 'default'); // with default value // Get all parameters $params = $request->getRouteParams(); // ['id' => '123', 'slug' => 'my-article']
π₯ Request
The Request class provides complete access to HTTP request data.
Path and Method
$request = new Request(); $path = $request->getPath(); // '/user/123' $method = $request->getMethod(); // 'GET', 'POST', etc.
Query Parameters
// URL: /search?q=php&page=2 $query = $request->getQueryParam('q'); // 'php' $page = $request->getQueryParam('page', 1); // '2' or 1 as default $allParams = $request->getQueryParams(); // ['q' => 'php', 'page' => '2']
HTTP Headers
$contentType = $request->getHeader('content-type'); $allHeaders = $request->getHeaders(); $customHeader = $request->getHeader('x-custom-header', 'default');
Cookies
$token = $request->getCookie('auth_token'); $allCookies = $request->getCookies();
Body (POST/PUT/PATCH)
// For JSON $data = $request->getBody(); // ['name' => 'John', 'email' => '...'] $name = $request->getBodyParam('name'); // 'John' $rawBody = $request->getRawBody(); // Raw string // For form-urlencoded $data = $request->getBody(); // ['field1' => 'value1', ...]
Utility Methods
if ($request->isAjax()) { // AJAX request } if ($request->wantsJson()) { // Client accepts JSON }
Customization for Tests
// Create a custom request for tests $request = new Request('/user/123', 'GET');
π€ Response
The Response class allows you to create and send HTTP responses.
Simple Response
$response = new Response(200, 'Response content'); $response->send();
JSON Response
$data = ['message' => 'Success', 'data' => []]; $response = Response::json($data, 200); $response->send();
Custom Headers
$response = new Response(200, 'Content'); $response->setHeader('X-Custom-Header', 'value'); $response->setHeader('Content-Type', 'application/xml'); $response->send();
Available Methods
$statusCode = $response->getStatusCode(); // 200 $content = $response->getContent(); // 'Content' $headers = $response->getHeaders(); // ['content-type' => 'application/json']
π Dependency Injection
The Router now supports dependency injection via a Container. This allows automatic injection of dependencies into controllers and middlewares.
Configuration with Container
use JulienLinard\Router\Router; use JulienLinard\Core\Container\Container; $router = new Router(); $container = new Container(); // Pass the Container to the Router $router->setContainer($container); // The Router will automatically use the Container to instantiate controllers
Controllers with Dependencies
use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Response; use JulienLinard\Doctrine\EntityManager; use JulienLinard\Auth\AuthManager; class UserController { private EntityManager $em; private AuthManager $auth; // Dependencies are automatically injected via the Container public function __construct(EntityManager $em, AuthManager $auth) { $this->em = $em; $this->auth = $auth; } #[Route(path: '/users', methods: ['GET'], name: 'users.index')] public function index(): Response { $users = $this->em->getRepository(User::class)->findAll(); return Response::json($users); } }
Note: If no Container is set, the Router instantiates controllers directly with new.
π‘οΈ Middlewares
Middlewares allow you to execute code before request processing.
Important : The Middleware interface has been improved. The handle() method now returns ?Response instead of void. If a middleware returns a Response, execution stops and that response is returned. If it returns null, execution continues with the next middleware.
Global Middlewares
use JulienLinard\Router\Middlewares\CorsMiddleware; use JulienLinard\Router\Middlewares\LoggingMiddleware; $router = new Router(); // Add a global middleware $router->addMiddleware(new CorsMiddleware()); $router->addMiddleware(new LoggingMiddleware());
Route-Specific Middlewares
use JulienLinard\Router\Middlewares\AuthMiddleware; use JulienLinard\Router\Middlewares\RoleMiddleware; class AdminController { #[Route( path: '/admin/dashboard', methods: ['GET'], name: 'admin.dashboard', middleware: [AuthMiddleware::class, RoleMiddleware::class] )] public function dashboard(): Response { return new Response(200, 'Admin dashboard'); } }
Available Middlewares
CorsMiddleware
use JulienLinard\Router\Middlewares\CorsMiddleware; // Default configuration (all origins) $cors = new CorsMiddleware(); // Custom configuration $cors = new CorsMiddleware( allowedOrigins: ['https://example.com', 'https://app.example.com'], allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], allowCredentials: true ); $router->addMiddleware($cors);
AuthMiddleware
use JulienLinard\Router\Middlewares\AuthMiddleware; class ProtectedController { #[Route( path: '/profile', methods: ['GET'], middleware: [AuthMiddleware::class] )] public function profile(): Response { // User is authenticated return Response::json(['user' => $_SESSION['user']]); } }
RoleMiddleware
use JulienLinard\Router\Middlewares\RoleMiddleware; class AdminController { #[Route( path: '/admin/users', methods: ['GET'], middleware: [AuthMiddleware::class, RoleMiddleware::class] )] public function users(): Response { // User is authenticated AND has admin role return Response::json(['users' => []]); } } // In your bootstrap $router->addMiddleware(new RoleMiddleware('admin'));
LoggingMiddleware
use JulienLinard\Router\Middlewares\LoggingMiddleware; $router->addMiddleware(new LoggingMiddleware()); // Log all requests to error_log
Create a Custom Middleware
<?php namespace App\Middlewares; use JulienLinard\Router\Middleware; use JulienLinard\Router\Request; use JulienLinard\Router\Response; class CustomMiddleware implements Middleware { public function handle(Request $request): void { // Your logic here // For example, check a condition if (/* condition not met */) { Response::json(['error' => 'Access denied'], 403)->send(); exit; } // Otherwise, continue execution } }
β οΈ Error Handling
The router automatically handles common errors:
- 404 Not Found: Route not found
- 405 Method Not Allowed: HTTP method not supported for this route
- 500 Internal Server Error: Server error (exceptions)
Customize Error Handling
use JulienLinard\Router\ErrorHandler; // Errors are automatically handled by the router // You can use ErrorHandler directly if needed $response = ErrorHandler::handleNotFound(); // 404 $response = ErrorHandler::handleServerError($e); // 500
π API Reference
Router
registerRoutes(string $controller): void
Registers all routes of a controller.
$router->registerRoutes(HomeController::class);
addMiddleware(Middleware $middleware): void
Adds a global middleware.
$router->addMiddleware(new CorsMiddleware());
handle(Request $request): Response
Processes a request and returns the response.
$response = $router->handle($request);
getRoutes(): array
Returns all registered routes (debug).
$routes = $router->getRoutes(); // ['static' => [...], 'dynamic' => [...]]
getRouteByName(string $name): ?array
Returns a route by its name.
$route = $router->getRouteByName('home'); // ['path' => '/', 'method' => 'GET', 'route' => [...]]
url(string $name, array $params = [], array $queryParams = []): ?string
Generates a URL from a route name and its parameters.
// Static route $url = $router->url('home'); // Returns: '/' // Dynamic route with one parameter $url = $router->url('user.show', ['id' => '123']); // Returns: '/user/123' // Dynamic route with multiple parameters $url = $router->url('post.show', ['userId' => '123', 'slug' => 'my-article']); // Returns: '/user/123/post/my-article' // With query parameters $url = $router->url('user.show', ['id' => '123'], ['page' => '2', 'sort' => 'name']); // Returns: '/user/123?page=2&sort=name' // Returns null if route doesn't exist $url = $router->url('non-existent'); // Returns: null // Throws exception if required parameter is missing try { $url = $router->url('user.show', []); // Missing 'id' parameter } catch (\InvalidArgumentException $e) { // "The parameter 'id' is required for route 'user.show'." }
group(string $prefix, array $middlewares, callable $callback): void
Creates a route group with a prefix and common middlewares.
$router->group('/api', [AuthMiddleware::class], function($router) { $router->registerRoutes(ApiController::class); });
Request
Main Methods
getPath(): string- Request pathgetMethod(): string- HTTP methodgetQueryParams(): array- All query parametersgetQueryParam(string $key, $default = null)- A query parametergetHeaders(): array- All headersgetHeader(string $name, ?string $default = null): ?string- A headergetCookies(): array- All cookiesgetCookie(string $name, $default = null)- A cookiegetBody(): ?array- Parsed bodygetBodyParam(string $key, $default = null)- A body parametergetRawBody(): string- Raw bodygetRouteParams(): array- All route parametersgetRouteParam(string $key, $default = null)- A route parameterisAjax(): bool- Checks if it's an AJAX requestwantsJson(): bool- Checks if client accepts JSON
Response
Constructor
new Response(int $statusCode = 200, string $content = '')
Static Methods
Response::json($data, int $statusCode = 200): self- Creates a JSON response
Instance Methods
setHeader(string $name, string $value): void- Sets a headersend(): void- Sends the HTTP responsegetStatusCode(): int- Status codegetContent(): string- ContentgetHeaders(): array- All headers
π Integration with Other Packages
Integration with core-php
core-php automatically includes php-router. The router is accessible via Application::getRouter().
<?php use JulienLinard\Core\Application; use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Response; $app = Application::create(__DIR__); $router = $app->getRouter(); class HomeController { #[Route(path: '/', methods: ['GET'], name: 'home')] public function index(): Response { return new Response(200, '<h1>Home</h1>'); } } $router->registerRoutes(HomeController::class); $app->start();
Integration with auth-php
Use authentication middlewares with php-router.
<?php use JulienLinard\Router\Router; use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Response; use JulienLinard\Auth\AuthManager; use JulienLinard\Auth\Middleware\AuthMiddleware; use JulienLinard\Auth\Middleware\RoleMiddleware; $router = new Router(); $auth = new AuthManager($authConfig); class DashboardController { #[Route( path: '/dashboard', methods: ['GET'], name: 'dashboard', middleware: [new AuthMiddleware($auth)] )] public function index(): Response { return new Response(200, '<h1>Dashboard</h1>'); } } class AdminController { #[Route( path: '/admin', methods: ['GET'], name: 'admin', middleware: [ new AuthMiddleware($auth), new RoleMiddleware('admin', $auth) ] )] public function index(): Response { return new Response(200, '<h1>Admin</h1>'); } } $router->registerRoutes(DashboardController::class); $router->registerRoutes(AdminController::class);
Standalone Usage
php-router can be used independently of all other packages.
<?php require_once __DIR__ . '/vendor/autoload.php'; use JulienLinard\Router\Router; use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Request; use JulienLinard\Router\Response; $router = new Router(); class HomeController { #[Route(path: '/', methods: ['GET'], name: 'home')] public function index(): Response { return new Response(200, 'Hello World'); } } $router->registerRoutes(HomeController::class); $request = new Request(); $response = $router->handle($request); $response->send();
π URL Generation
The router allows you to generate URLs from route names, which facilitates maintenance and avoids hardcoded URLs.
Simple URL Generation
// In your views or controllers $homeUrl = $router->url('home'); // Returns: '/' $userUrl = $router->url('user.show', ['id' => '123']); // Returns: '/user/123'
URL Generation with Query Parameters
$url = $router->url('user.show', ['id' => '123'], ['page' => '2', 'sort' => 'name']); // Returns: '/user/123?page=2&sort=name'
Usage in Responses
class UserController { #[Route(path: '/user/{id}', methods: ['GET'], name: 'user.show')] public function show(Request $request, Router $router): Response { $id = $request->getRouteParam('id'); // Generate next user URL $nextUserId = (int)$id + 1; $nextUrl = $router->url('user.show', ['id' => $nextUserId]); return Response::json([ 'user_id' => $id, 'next_user_url' => $nextUrl ]); } }
Note: To use $router in your controllers, you can inject it via a dependency container or pass it as a parameter.
π‘ Complete Examples
Example 1: Complete REST API with Groups
<?php require_once __DIR__ . '/vendor/autoload.php'; use JulienLinard\Router\Router; use JulienLinard\Router\Request; use JulienLinard\Router\Response; use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Middlewares\CorsMiddleware; class UserController { // Paths are defined without the /api prefix (added by group) #[Route(path: '/users', methods: ['GET'], name: 'users.index')] public function index(): Response { return Response::json(['users' => []]); } #[Route(path: '/users/{id}', methods: ['GET'], name: 'users.show')] public function show(Request $request): Response { $id = $request->getRouteParam('id'); return Response::json(['user' => ['id' => $id]]); } #[Route(path: '/users', methods: ['POST'], name: 'users.store')] public function store(Request $request): Response { $data = $request->getBody(); // Create user... return Response::json(['message' => 'User created'], 201); } #[Route(path: '/users/{id}', methods: ['PUT'], name: 'users.update')] public function update(Request $request): Response { $id = $request->getRouteParam('id'); $data = $request->getBody(); // Update user... return Response::json(['message' => 'User updated']); } #[Route(path: '/users/{id}', methods: ['DELETE'], name: 'users.delete')] public function delete(Request $request): Response { $id = $request->getRouteParam('id'); // Delete user... return Response::json(['message' => 'User deleted'], 204); } } // Configuration with groups $router = new Router(); $router->addMiddleware(new CorsMiddleware()); // API group with /api prefix $router->group('/api', [], function($router) { $router->registerRoutes(UserController::class); }); // Processing $request = new Request(); $response = $router->handle($request); $response->send(); // URL generation $usersUrl = $router->url('users.index'); // '/api/users' $userUrl = $router->url('users.show', ['id' => 5]); // '/api/users/5'
Example 2: Web Application with Authentication and Groups
<?php require_once __DIR__ . '/vendor/autoload.php'; use JulienLinard\Router\Router; use JulienLinard\Router\Request; use JulienLinard\Router\Response; use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Middlewares\AuthMiddleware; use JulienLinard\Router\Middlewares\RoleMiddleware; class HomeController { #[Route(path: '/', methods: ['GET'], name: 'home')] public function index(): Response { return new Response(200, '<h1>Welcome</h1>'); } } class AuthController { #[Route(path: '/login', methods: ['GET', 'POST'], name: 'login')] public function login(Request $request): Response { if ($request->getMethod() === 'POST') { // Process login $_SESSION['user'] = ['id' => 1, 'role' => 'user']; return new Response(302, '', ['Location' => '/dashboard']); } return new Response(200, '<form>...</form>'); } } class DashboardController { #[Route( path: '/dashboard', methods: ['GET'], name: 'dashboard', middleware: [AuthMiddleware::class] )] public function index(): Response { return new Response(200, '<h1>Dashboard</h1>'); } } class AdminController { #[Route( path: '/admin', methods: ['GET'], name: 'admin', middleware: [AuthMiddleware::class, RoleMiddleware::class] )] public function index(): Response { return new Response(200, '<h1>Admin</h1>'); } } // Configuration with groups $router = new Router(); // Public routes $router->registerRoutes(HomeController::class); $router->registerRoutes(AuthController::class); // Dashboard group with authentication $router->group('/dashboard', [AuthMiddleware::class], function($router) { $router->registerRoutes(DashboardController::class); }); // Admin group with authentication and role $router->group('/admin', [AuthMiddleware::class, new RoleMiddleware('admin')], function($router) { $router->registerRoutes(AdminController::class); }); // Processing session_start(); $request = new Request(); $response = $router->handle($request); $response->send();
π§ͺ Tests
The package includes a complete test suite. To run the tests:
composer test # or vendor/bin/phpunit tests/
π License
MIT License - See the LICENSE file for more details.
π€ Contributing
Contributions are welcome! Feel free to open an issue or a pull request.
π§ Support
For any questions or issues, please open an issue on GitHub.
π Support the project
If this bundle is useful to you, consider becoming a sponsor to support the development and maintenance of this open source project.
Developed with β€οΈ by Julien Linard