π·πΊ Π ΡΡΡΠΊΠ°Ρ Π²Π΅ΡΡΠΈΡ
Flexible and performant routing library for PHP 8.4+ with route caching support.
- Installation
- Quick Start
- Creating Routes
- Route Parameters
- Route Groups
- URL Generation
- PSR-15 Middleware Integration
- Accessing Route Data in Controllers
- Route Locators
- PHP Attribute-based Route Location
- Accessing Route Handler
- Route Caching
- Error Handling
composer require bermudaphp/routerRequirements: PHP 8.4+
use Bermuda\Router\{Routes, Router, RouteRecord};
// Create routes collection
$routes = new Routes();
$router = Router::fromDnf($routes);
// Add route
$routes->addRoute(
RouteRecord::get('hello', '/hello/[name]', function(string $name): string {
return "Hello, $name!";
})
);
// Match request
$route = $router->match('/hello/John', 'GET');
if ($route) {
$name = $route->parameters->get('name');
echo call_user_func($route->handler, $name);
}| Method | HTTP Methods | Description | Usage Example |
|---|---|---|---|
get() |
GET | Retrieve data | RouteRecord::get('users.index', '/users', 'UsersController') |
post() |
POST | Create new resources | RouteRecord::post('users.store', '/users', 'UsersController::store') |
put() |
PUT | Full resource update | RouteRecord::put('users.update', '/users/[id]', 'UsersController::update') |
patch() |
PATCH | Partial resource update | RouteRecord::patch('users.patch', '/users/[id]', 'UsersController::patch') |
delete() |
DELETE | Delete resource | RouteRecord::delete('users.destroy', '/users/[id]', 'UsersController::destroy') |
head() |
HEAD | Retrieve headers | RouteRecord::head('users.check', '/users/[id]', 'UsersController::head') |
options() |
OPTIONS | Get available methods | RouteRecord::options('users.options', '/users', 'UsersController::options') |
any() |
Custom | Multiple HTTP methods | RouteRecord::any('users.resource', '/users/[id]', 'UsersController', ['GET', 'PUT', 'DELETE']) |
// GET route for user listing
$routes->addRoute(RouteRecord::get('users.index', '/users', UsersController::class));
// POST route for creating new user
$routes->addRoute(RouteRecord::post('users.store', '/users', 'UsersController::store'));
// PUT route for full user update
$routes->addRoute(RouteRecord::put('users.update', '/users/[id]', 'UsersController::update'));
// PATCH route for partial user update
$routes->addRoute(RouteRecord::patch('users.patch', '/users/[id]', 'UsersController::patch'));
// DELETE route for user deletion
$routes->addRoute(RouteRecord::delete('users.destroy', '/users/[id]', 'UsersController::destroy'));
// Multiple methods for single route
$routes->addRoute(RouteRecord::any('users.resource', '/users/[id]', UsersController::class,
['GET', 'PUT', 'PATCH', 'DELETE']
));
// All HTTP methods (catch-all route)
$routes->addRoute(RouteRecord::any('api.catchall', '/api/[path:.*]', ApiController::class));
// Closure as handler
$routes->addRoute(RouteRecord::get('hello', '/hello/[name]', function(string $name) {
return "Hello, $name!";
}));use Bermuda\Router\RouteBuilder;
$route = RouteBuilder::create('users.show', '/users/[id]')
->handler(UsersController::class)
->get()
->middleware([AuthMiddleware::class, ValidationMiddleware::class])
->tokens(['id' => '\d+'])
->defaults(['format' => 'json'])
->build();
$routes->addRoute($route);// Required parameter
$routes->addRoute(RouteRecord::get('user.show', '/users/[id]', 'showUser'));
// Optional parameter
$routes->addRoute(RouteRecord::get('posts.index', '/posts/[?page]', 'listPosts'));
// Multiple parameters
$routes->addRoute(RouteRecord::get('post.show', '/blog/[year]/[month]/[slug]', 'showPost'));| Name | Pattern | Description | Examples |
|---|---|---|---|
id |
\d+ |
Numeric ID | 1, 123, 999 |
slug |
[a-z0-9-]+ |
URL-compatible string | hello-world, my-post |
uuid |
[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} |
UUID v4 format | 550e8400-e29b-41d4-a716-446655440000 |
any |
.+ |
Any characters including slashes | any/path/here |
alpha |
[a-zA-Z]+ |
Letters only | Hello, ABC |
alnum |
[a-zA-Z0-9]+ |
Letters and digits | Hello123, ABC789 |
year |
[12]\d{3} |
4-digit year (1900-2999) | 2024, 1995 |
month |
0[1-9]|1[0-2] |
Month (01-12) | 01, 12 |
day |
0[1-9]|[12]\d|3[01] |
Day of month (01-31) | 01, 15, 31 |
locale |
[a-z]{2}(_[A-Z]{2})? |
Locale code | en, en_US, fr_FR |
version |
v?\d+(\.\d+)* |
Version string | 1.0, v2.1.3 |
date |
\d{4}-\d{2}-\d{2} |
ISO date (YYYY-MM-DD) | 2024-12-25 |
Inline patterns allow defining regex patterns directly in route definition. Syntax: [parameter_name:regular_expression]
// Inline pattern - numeric ID only
$routes->addRoute(RouteRecord::get('user.show', '/users/[id:\d+]', 'showUser'));
// Inline pattern - API version
$routes->addRoute(RouteRecord::get('api.version', '/api/[version:v\d+]/users', 'apiUsers'));
// Inline pattern - product SKU (3 letters, dash, 4 digits)
$routes->addRoute(RouteRecord::get('product.show', '/products/[sku:[A-Z]{3}-\d{4}]', 'showProduct'));
// Inline pattern - file format (only specific extensions)
$routes->addRoute(RouteRecord::get('download', '/files/[name]/[format:pdf|doc|txt]', 'downloadFile'));
// Optional inline pattern
$routes->addRoute(RouteRecord::get('posts.category', '/posts/[?category:tech|news|blog]', 'showCategory'));
// Complex inline pattern - date in YYYY-MM-DD format
$routes->addRoute(RouteRecord::get('archive.date', '/archive/[date:\d{4}-\d{2}-\d{2}]', 'showArchive'));// Set pattern via method
$route = RouteRecord::get('product.show', '/products/[sku]', 'showProduct')
->withToken('sku', '[A-Z]{3}-\d{4}');
// Multiple patterns
$route = RouteRecord::get('complex.route', '/app/[locale]/[category]/[item]', 'handler')
->withTokens([
'locale' => '[a-z]{2}(_[A-Z]{2})?',
'category' => '[a-z0-9-]+',
'item' => '\d+'
]);Patterns are applied in the following priority order (highest to lowest):
- Inline patterns in route:
[id:\d+] - Method patterns:
->withToken('id', '\d+') - Group patterns:
$group->setTokens(['id' => '\d+']) - Predefined patterns: from table above
- Default pattern:
[^\/]+(any characters except slash)
$route = RouteRecord::get('posts', '/posts/[?page]', 'listPosts')
->withDefaults([
'page' => '1'
]);
// Single value
$route = $route->withDefaultValue('page', '1');Groups allow organizing related routes with common settings:
// Create group
$apiGroup = $routes->group('api', '/api/v1');
// Add routes to group
$apiGroup->get('users.index', '/users', UsersController::class);
$apiGroup->post('users.store', '/users', 'UsersController::store');
$apiGroup->get('users.show', '/users/[id]', 'UsersController::show');
// Resulting routes:
// api.users.index -> GET /api/v1/users
// api.users.store -> POST /api/v1/users
// api.users.show -> GET /api/v1/users/[id]// Middleware for entire group
$apiGroup->addMiddleware(AuthMiddleware::class)
->addMiddleware(RateLimitMiddleware::class);
// Patterns for entire group
$apiGroup->setTokens([
'id' => '\d+',
'slug' => '[a-z0-9-]+',
'locale' => '[a-z]{2}'
]);
// Replace entire middleware stack
$apiGroup->setMiddleware([
CorsMiddleware::class,
AuthMiddleware::class,
LoggingMiddleware::class
]);// Simple generation
echo $router->generate('users.show', ['id' => 123]);
// Result: /users/123
// With optional parameters
echo $router->generate('posts.index', ['page' => 2]);
// Result: /posts/2
echo $router->generate('posts.index'); // optional parameter omitted
// Result: /posts
// Complex parameters
echo $router->generate('blog.post', [
'year' => 2024,
'month' => 3,
'slug' => 'new-article'
]);
// Result: /blog/2024/3/new-articleuse Bermuda\Router\Middleware\{MatchRouteMiddleware, DispatchRouteMiddleware, RouteNotFoundHandler};
use Bermuda\Pipeline\Pipeline;
use Bermuda\MiddlewareFactory\MiddlewareFactory;
$pipeline = new Pipeline();
$factory = new MiddlewareFactory($container, $responseFactory);
// Middleware for route matching
$pipeline->pipe($factory->makeMiddleware(MatchRouteMiddleware::class));
// Create 404 handler
$notFoundHandler = new RouteNotFoundHandler($responseFactory);
// Middleware for route execution with fallback handler
$pipeline->pipe(new DispatchRouteMiddleware($notFoundHandler));
$response = $pipeline->handle($request);RouteNotFoundHandler handles requests for non-existent routes and can work in two modes:
use Bermuda\Router\Middleware\RouteNotFoundHandler;
// JSON response mode (default)
$notFoundHandler = new RouteNotFoundHandler(
$responseFactory,
exceptionMode: false,
customMessage: 'Requested resource not found'
);
// Example JSON response:
// {
// "error": "Not Found",
// "code": 404,
// "message": "Requested resource not found",
// "path": "/api/users/999",
// "method": "GET",
// "timestamp": "2024-12-25T10:30:00+00:00"
// }
// Exception mode
$notFoundHandler = new RouteNotFoundHandler(
$responseFactory,
exceptionMode: true // will throw RouteNotFoundException
);
// Dynamic mode switching via request attributes
$request = $notFoundHandler->withExceptionModeAttribute($request, true);
// Check current mode
$isExceptionMode = $notFoundHandler->getExceptionMode($request);use Bermuda\Router\Middleware\{MatchRouteMiddleware, DispatchRouteMiddleware, RouteNotFoundHandler};
$pipeline = new Pipeline();
// 1. Try to find route
$pipeline->pipe(new MatchRouteMiddleware($middlewareFactory, $router));
// 2. Create 404 handler
$notFoundHandler = new RouteNotFoundHandler(
$responseFactory,
exceptionMode: false,
customMessage: 'API endpoint not found'
);
// 3. Execute found route or handle 404
$pipeline->pipe(new DispatchRouteMiddleware($notFoundHandler));
// Process request
$response = $pipeline->handle($request);
// With exceptionMode: true - exception handling
$notFoundHandler = new RouteNotFoundHandler($responseFactory, exceptionMode: true);
$pipeline->pipe(new DispatchRouteMiddleware($notFoundHandler));
try {
$response = $pipeline->handle($request);
} catch (RouteNotFoundException $e) {
// Custom exception handling (only works with exceptionMode: true)
$response = new JsonResponse([
'error' => 'Route not found',
'path' => $e->path,
'method' => $e->requestMethod
], 404);
}
// With exceptionMode: false (default) - automatic JSON response
$notFoundHandler = new RouteNotFoundHandler($responseFactory, exceptionMode: false);
$pipeline->pipe(new DispatchRouteMiddleware($notFoundHandler));
$response = $pipeline->handle($request);
// If route not found, RouteNotFoundHandler automatically returns JSON:
// HTTP 404 Not Found
// Content-Type: application/json; charset=utf-8
// {
// "error": "Not Found",
// "code": 404,
// "message": "The requested endpoint was not found.",
// "path": "/api/users/999",
// "method": "GET",
// "timestamp": "2024-12-25T10:30:00+00:00"
// }use Bermuda\Router\Middleware\RouteMiddleware;
class UserController
{
public function show(ServerRequestInterface $request): ResponseInterface
{
// Get route data
$routeMiddleware = RouteMiddleware::fromRequest($request);
$route = $routeMiddleware->route;
// Access parameters
$userId = $request->getAttribute('id');
// or
$userId = $route->parameters->get('id');
// Route information
$routeName = $route->name;
$routePath = $route->path;
return new JsonResponse(['user_id' => $userId]);
}
}For loading routes from configuration files:
use Bermuda\Router\Locator\RouteLocator;
$locator = new RouteLocator(
filename: '/app/config/routes.php',
context: [
'app' => $application,
'container' => $container,
'config' => $config
],
useCache: $_ENV['APP_ENV'] === 'production'
);
$routes = $locator->getRoutes();// /app/config/routes.php
/** @var Routes $routes */
/** @var Application $app */
/** @var ContainerInterface $container */
// Simple routes
$routes->addRoute(RouteRecord::get('home', '/', HomeController::class));
// Groups
$apiGroup = $routes->group('api', '/api/v1');
$apiGroup->addMiddleware(CorsMiddleware::class);
$apiGroup->get('users.index', '/users', function() use ($app) {
return $app->getUsers();
});
$apiGroup->post('users.store', '/users', function($request) use ($container) {
$service = $container->get(UserService::class);
return $service->create($request->getParsedBody());
});The library supports automatic route discovery through PHP attributes on controller methods. This allows defining routes declaratively, right next to handlers.
Attribute support requires an additional package:
composer require bermudaphp/attribute-locatorThe #[Route] attribute allows defining routes directly on controller methods:
use Bermuda\Router\Attribute\Route;
class UserController
{
#[Route('users.index', '/users', 'GET')]
public function index(): ResponseInterface
{
// Get user list
return new JsonResponse($this->userService->getAll());
}
#[Route('users.show', '/users/[id]', 'GET')]
public function show(ServerRequestInterface $request): ResponseInterface
{
$id = $request->getAttribute('id');
return new JsonResponse($this->userService->getById($id));
}
#[Route('users.store', '/users', 'POST', middleware: ['auth', 'validation'])]
public function store(ServerRequestInterface $request): ResponseInterface
{
$data = $request->getParsedBody();
$user = $this->userService->create($data);
return new JsonResponse($user, 201);
}
#[Route('users.update', '/users/[id]', 'PUT|PATCH', group: 'api')]
public function update(ServerRequestInterface $request): ResponseInterface
{
$id = $request->getAttribute('id');
$data = $request->getParsedBody();
$user = $this->userService->update($id, $data);
return new JsonResponse($user);
}
#[Route('users.destroy', '/users/[id]', 'DELETE', priority: 10)]
public function destroy(ServerRequestInterface $request): ResponseInterface
{
$id = $request->getAttribute('id');
$this->userService->delete($id);
return new JsonResponse(null, 204);
}
}| Parameter | Type | Description | Example |
|---|---|---|---|
name |
string |
Unique route name | 'users.show' |
path |
string |
URL pattern with parameters | '/users/[id]' |
methods |
string|array |
HTTP methods (string or array) | 'GET', 'PUT|PATCH', ['GET', 'POST'] |
middleware |
array |
Middleware array for route | ['auth', 'validation'] |
group |
string |
Route group name | 'api' |
priority |
int |
Route priority (higher = earlier) | 10 |
defaults |
array |
Default parameter values | ['format' => 'json'] |
AttributeRouteLocator works as a decorator for existing route locators:
use Bermuda\Router\Locator\{RouteLocator, AttributeRouteLocator};
// Base locator (file-based or any other)
$baseLocator = new RouteLocator('/app/config/routes.php');
// Decorate with attribute locator
$attributeLocator = new AttributeRouteLocator($baseLocator);
// Pass context (if needed)
$attributeLocator->setContext([
'container' => $container,
'config' => $config
]);
// Get all routes (file + attributes)
$routes = $attributeLocator->getRoutes();
$router = Router::fromDnf($routes);For automatic controller discovery with attributes, ClassFinder is used (already included in dependencies):
π Detailed documentation: bermudaphp/finder | Russian Guide
use Bermuda\ClassFinder\{ClassFinder, ClassNotifier};
use Bermuda\Router\Locator\AttributeRouteLocator;
use Bermuda\Router\Attribute\Route;
// Create locator
$baseLocator = new RouteLocator('/app/config/routes.php');
$attributeLocator = new AttributeRouteLocator($baseLocator);
// Find all classes in controllers directory
$finder = new ClassFinder();
$classes = $finder->find('src/Controllers/');
// Notify locator about found classes (locator filters classes with Route attributes)
$notifier = new ClassNotifier([$attributeLocator]);
$notifier->notify($classes);
// Get all routes
$routes = $attributeLocator->getRoutes();
$router = Router::fromDnf($routes);use Bermuda\ClassFinder\{ClassFinder, ClassNotifier};
use Bermuda\Router\Locator\{RouteLocator, AttributeRouteLocator};
use Bermuda\Router\Attribute\Route;
// 1. Create base locator
$baseLocator = new RouteLocator(
filename: '/app/config/routes.php',
useCache: $_ENV['APP_ENV'] === 'production'
);
// 2. Create attribute locator as decorator
$attributeLocator = new AttributeRouteLocator($baseLocator);
$attributeLocator->setContext(['app' => $app, 'container' => $container]);
// 3. Find classes in various directories with exclusions
$finder = new ClassFinder();
$controllerClasses = $finder->find(
paths: [
'src/Controllers/', // Main controllers
'src/Api/', // API controllers
'app/Http/Controllers/' // Legacy controllers
],
exclude: ['src/Api/products'] // Exclude specific directory
);
// 4. Notify locator about found classes
// ClassFinder finds all classes in specified directories,
// then AttributeRouteLocator scans class methods for Route attributes
// and registers found methods as route handlers
$notifier = new ClassNotifier([$attributeLocator]);
$notifier->notify($controllerClasses);
// 5. Get all routes and create router
$routes = $attributeLocator->getRoutes();
$router = Router::fromDnf($routes);
// 6. Use in middleware pipeline
$pipeline = new Pipeline();
$pipeline->pipe(new MatchRouteMiddleware($middlewareFactory, $router));
$pipeline->pipe(new DispatchRouteMiddleware($notFoundHandler));
$response = $pipeline->handle($request);RouterException will be thrown.
// First define groups in base locator's routes.php file
/** @var Routes $routes */
$apiGroup = $routes->group('api', '/api');
$adminGroup = $routes->group('admin', '/admin');
// Now you can use these groups in attributes
class ApiController
{
#[Route('api.users.index', '/users', 'GET', group: 'api')]
public function getUsers(): ResponseInterface
{
return new JsonResponse($this->userService->getAll());
}
#[Route('api.users.store', '/users', 'POST', group: 'api', middleware: ['auth'])]
public function createUser(ServerRequestInterface $request): ResponseInterface
{
$data = $request->getParsedBody();
$user = $this->userService->create($data);
return new JsonResponse($user, 201);
}
}
// Group configuration can be added after route loading
$routes = $attributeLocator->getRoutes();
// Configure 'api' group after loading routes
$apiGroup = $routes->group('api');
$apiGroup->addMiddleware(CorsMiddleware::class);
$apiGroup->setTokens(['id' => '\d+']);Priorities determine the order of route checking when matching requests. Routes with higher priority are checked first.
Priority Rules:
- Default priority =
0 - Higher number = higher priority
- Routes are sorted by descending priority (100, 50, 10, 0, -10)
- Order not guaranteed for same priority
When to use priorities:
- Special routes should be checked before general ones
- Specific patterns β before wide catch-all routes
- API versioning with fallback to older versions
class RouteController
{
// Highest priority - special handling
#[Route('admin.special', '/admin/special/action', 'POST', priority: 100)]
public function specialAdminAction(): ResponseInterface
{
return new JsonResponse(['action' => 'special']);
}
// High priority - specific route
#[Route('user.profile', '/users/profile', 'GET', priority: 50)]
public function userProfile(): ResponseInterface
{
return new JsonResponse(['page' => 'profile']);
}
// Medium priority - route with parameter
#[Route('user.show', '/users/[id]', 'GET', priority: 10)]
public function showUser(): ResponseInterface
{
return new JsonResponse(['type' => 'user']);
}
// Normal priority - general route
#[Route('users.list', '/users', 'GET', priority: 0)]
public function listUsers(): ResponseInterface
{
return new JsonResponse(['type' => 'list']);
}
// Low priority - catch-all route (checked last)
#[Route('catch.all', '/[path:.*]', 'GET', priority: -10)]
public function catchAll(): ResponseInterface
{
return new JsonResponse(['type' => 'fallback']);
}
}Priority example for API versioning:
class ApiController
{
// v2 API - high priority
#[Route('api.v2.users', '/api/v2/users/[id]', 'GET', priority: 20)]
public function getUserV2(): ResponseInterface
{
return new JsonResponse(['version' => 'v2', 'features' => ['new_field']]);
}
// v1 API - medium priority
#[Route('api.v1.users', '/api/v1/users/[id]', 'GET', priority: 10)]
public function getUserV1(): ResponseInterface
{
return new JsonResponse(['version' => 'v1']);
}
// Fallback to v1 for requests without version - low priority
#[Route('api.users.fallback', '/api/users/[id]', 'GET', priority: 0)]
public function getUserFallback(): ResponseInterface
{
// Redirect to v1
return new JsonResponse(['version' => 'v1', 'deprecated' => true]);
}
}Regular routes vs attribute routes:
- Attribute routes: priority determined by
priorityparameter - Regular routes: priority determined by addition order (first added = highest priority)
// Regular routes - addition order determines priority
$routes->addRoute(RouteRecord::get('special', '/api/special', 'handler1')); // Checked first
$routes->addRoute(RouteRecord::get('generic', '/api/[path:.*]', 'handler2')); // Checked second
// Attribute routes - use priority parameter
#[Route('high', '/api/high', 'GET', priority: 100)] // Checked first
#[Route('low', '/api/low', 'GET', priority: 0)] // Checked secondRouteRecord provides convenient access to various route components:
// Create route with middleware
$route = RouteRecord::get('users.show', '/users/[id]', UserController::class)
->withMiddlewares([AuthMiddleware::class, ValidationMiddleware::class]);
// Access full pipeline (middleware + handler)
$fullPipeline = $route->pipeline; // [AuthMiddleware::class, ValidationMiddleware::class, UserController::class]
// Access middleware only
$middleware = $route->middleware; // [AuthMiddleware::class, ValidationMiddleware::class]
// Access main handler
$handler = $route->handler; // UserController::classFor improved performance in production:
use Bermuda\Router\CacheFileProvider;
// Setup routes
$routes = new Routes();
$routes->addRoute(RouteRecord::get('home', '/', 'HomeController'));
$routes->addRoute(RouteRecord::get('users.show', '/users/[id]', 'UsersController::show'));
// Create cache
$cacheProvider = new CacheFileProvider('/path/to/cache');
$routeData = $routes->toArray();
$cacheProvider->writeFile('routes', $routeData);use Bermuda\Router\RoutesCache;
// Load cached routes
$cacheProvider = new CacheFileProvider('/path/to/cache');
$cachedData = $cacheProvider->readFile('routes');
$routes = new RoutesCache($cachedData);
$router = Router::fromDnf($routes);When routes use closures with external variables (via use), these variables must be available when loading cached routes. Context allows passing necessary objects and data to the cached file scope.
// When creating routes with closures
$app = new Application();
$db = new Database();
$routes->addRoute(RouteRecord::get('users.index', '/users',
function() use ($app, $db) {
return $app->respond($db->users()->all());
}
));
// Save cache with context
$cacheProvider->writeFile('routes', $routes->toArray(), [
'app' => $app,
'db' => $db
]);
// Load with context - variables $app and $db will be available in cached file
$cachedData = $cacheProvider->readFile('routes');
$routes = new RoutesCache($cachedData);The caching system has the following limitations:
// 1. Objects as handlers
$controller = new UserController();
$routes->addRoute(RouteRecord::get('users', '/users', $controller)); // Not cached
// 2. Closures with objects in use (without context)
$service = new UserService();
$routes->addRoute(RouteRecord::get('users', '/users',
function() use ($service) {
return $service->getUsers();
}
)); // Handler will be cached, but error will occur due to missing context // Undefined variable $service
// 3. Anonymous classes
$routes->addRoute(RouteRecord::get('test', '/test', new class {
public function handle() { return 'test'; }
})); // Anonymous class not cached// 1. Strings (class and method names)
$routes->addRoute(RouteRecord::get('users', '/users', 'UserController'));
$routes->addRoute(RouteRecord::get('posts', '/posts', 'PostController::index'));
// 2. Arrays with class/method names
$routes->addRoute(RouteRecord::get('api', '/api', ['ApiController', 'handle']));
// 3. Scalar values in context
$routes->addRoute(RouteRecord::get('config', '/config',
function() use ($appName, $version) { // $appName and $version - strings/numbers
return ['app' => $appName, 'version' => $version];
}
));- Use string handlers in production for maximum cache compatibility
- Pass contextual data if using
usein closure handlers
Most preferred handler type - class name MyHandler::class or class and method combination MyHandler::handle
use Bermuda\Router\Exception\{
RouterException,
RouteNotFoundException,
RouteNotRegisteredException,
GeneratorException,
MatchException
};
try {
$route = $router->match($uri, $method);
$url = $router->generate('nonexistent.route', ['id' => 123]);
} catch (RouteNotFoundException $e) {
// 404 - route not found
echo "Path not found: $e->path [$e->requestMethod]";
} catch (RouteNotRegisteredException $e) {
// 500 - route not registered
echo "Route '$e->routeName' not registered";
} catch (GeneratorException $e) {
// 400 - URL generation error
echo "URL generation error: " . $e->getMessage();
} catch (MatchException $e) {
// Pattern matching error
echo "Matching error: $e->pattern for $e->path";
} catch (RouterException $e) {
// General router errors
echo "Router error: " . $e->getMessage();
}