Routing
Table of Contents
- Introduction
- Basic Routing
- Route Parameters
- Route Groups
- Middleware
- Attribute-Based Routing
- Field Selection
- Advanced Features
- Performance Optimization
- Best Practices
Introduction
The Glueful Framework features a high-performance router designed for enterprise applications with O(1) static route lookup, intelligent route bucketing for dynamic routes, comprehensive middleware pipeline, and advanced features like GraphQL-style field selection.
Key Features
- O(1) Static Route Performance: Hash table lookup for static routes
- Intelligent Route Bucketing: Dynamic routes grouped by first path segment
- Route Caching: Compiled route cache with opcache integration
- Middleware Pipeline: Enhanced middleware system with runtime parameters
- Attribute-Based Routing: PHP 8 attributes for declarative route definitions
- Field Selection: GraphQL-style field selection for REST APIs
- CORS Support: Built-in configurable CORS handling
Basic Routing
Defining Routes
Routes are defined in the routes/ directory and loaded automatically by the framework.
Note on context:
- In route files (e.g.,
routes/api.php), the router instance$routeris in scope (provided by the framework’s manifest loader), so you can call$router->get(...)directly. - In other contexts (e.g., bootstrapping/tests), resolve it from the container:
$router = container()->get(Glueful\\Routing\\Router::class);
use Glueful\Routing\Router;
use Glueful\Http\Response;
// Basic GET route
$router->get('/users', function() {
return new Response(['users' => []]);
});
// POST route with controller
$router->post('/users', [UserController::class, 'store']);
// PUT route
$router->put('/users/{id}', [UserController::class, 'update']);
// DELETE route
$router->delete('/users/{id}', [UserController::class, 'destroy']);
// HEAD route (Router also maps HEAD -> GET automatically)
$router->head('/users', [UserController::class, 'check']);
Response Types
The router supports various response types:
use Glueful\Http\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
// JSON response (default)
$router->get('/api/data', fn() => new Response(['data' => 'value']));
// Explicit JSON response
$router->get('/api/json', fn() => new JsonResponse(['json' => true]));
// Streamed response
$router->get('/stream', fn() => new StreamedResponse(function() {
echo "Streaming data...";
}));
// File download
$router->get('/download/{file}', fn($file) => new BinaryFileResponse("/path/to/{$file}"));
Route Parameters
Basic Parameters
Route parameters are defined using curly braces:
// Single parameter
$router->get('/users/{id}', function(int $id) {
return new Response(['user_id' => $id]);
});
// Multiple parameters
$router->get('/posts/{post}/comments/{comment}', function(int $post, int $comment) {
return new Response([
'post_id' => $post,
'comment_id' => $comment
]);
});
// Optional segments are not supported in paths. Define both forms:
$router->get('/search', fn() => new Response(['searching' => 'all']));
$router->get('/search/{query}', fn(string $query) => new Response(['searching' => $query]));
Parameter Constraints
Apply regex constraints to route parameters:
// Numeric constraint
$router->get('/users/{id}', [UserController::class, 'show'])
->where('id', '\d+');
// Multiple constraints
$router->get('/posts/{year}/{month}/{slug}', [PostController::class, 'show'])
->where([
'year' => '\d{4}',
'month' => '\d{2}',
'slug' => '[a-z0-9-]+'
]);
// UUID constraint
$router->get('/api/resources/{uuid}', [ResourceController::class, 'show'])
->where('uuid', '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}');
Dependency Injection in Route Handlers
The router automatically resolves dependencies from the container:
use Psr\Log\LoggerInterface;
use App\Services\UserService;
$router->get('/users/{id}', function(
int $id,
UserService $service,
LoggerInterface $logger
) {
$logger->info("Fetching user", ['id' => $id]);
$user = $service->find($id);
return new Response(['user' => $user]);
});
Route Groups
Group routes to share common attributes like prefixes and middleware:
Basic Groups
// API version grouping
$router->group(['prefix' => '/api/v1'], function(Router $router) {
$router->get('/users', [UserController::class, 'index']);
$router->get('/posts', [PostController::class, 'index']);
});
// Result: /api/v1/users and /api/v1/posts
Nested Groups
$router->group(['prefix' => '/api'], function(Router $router) {
// Version 1
$router->group(['prefix' => '/v1'], function(Router $router) {
$router->get('/users', [V1\UserController::class, 'index']);
});
// Version 2 with middleware
$router->group(['prefix' => '/v2', 'middleware' => ['auth']], function(Router $router) {
$router->get('/users', [V2\UserController::class, 'index']);
});
});
Group Middleware
// Apply middleware to all routes in group
$router->group(['middleware' => ['auth', 'rate_limit:60,60']], function(Router $router) {
$router->get('/dashboard', [DashboardController::class, 'index']);
$router->get('/profile', [ProfileController::class, 'show']);
$router->put('/profile', [ProfileController::class, 'update']);
});
// Admin routes with multiple middleware
$router->group([
'prefix' => '/admin',
'middleware' => ['auth', 'admin', 'log_activity']
], function(Router $router) {
$router->get('/users', [AdminController::class, 'users']);
$router->get('/settings', [AdminController::class, 'settings']);
});
Middleware
Implementing Middleware
All middleware must implement the RouteMiddleware interface:
use Glueful\Routing\Middleware\RouteMiddleware;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class CustomMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next, ...$params): Response
{
// Pre-processing
$request->attributes->set('custom_data', 'value');
// Call next middleware or handler
$response = $next($request);
// Post-processing
$response->headers->set('X-Custom-Header', 'Value');
return $response;
}
}
Middleware with Parameters
class RateLimitMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next, ...$params): Response
{
$maxAttempts = (int) ($params[0] ?? 60);
$windowSeconds = (int) ($params[1] ?? 60);
// Rate limiting logic
if ($this->tooManyAttempts($request, $maxAttempts, $windowSeconds)) {
return new Response(['error' => 'Too many requests'], 429);
}
return $next($request);
}
}
// Usage
$router->get('/api/resource', $handler)
->middleware('rate_limit:100,60'); // 100 requests per 60 seconds
Global vs Route Middleware
// Register middleware in service provider
$container->set('auth', AuthMiddleware::class);
$container->set('cors', CorsMiddleware::class);
$container->set('rate_limit', RateLimitMiddleware::class);
// Apply globally to all routes
$router->group(['middleware' => ['cors']], function($router) {
// All application routes
});
// Apply to specific routes
$router->get('/public', $handler); // No middleware
$router->get('/private', $handler)->middleware(['auth']); // Auth required
Attribute-Based Routing
Use PHP 8 attributes for cleaner controller-based routing:
Controller Attributes
use Glueful\Routing\Attributes\{Controller, Get, Post, Put, Delete};
use Glueful\Routing\Attributes\Fields;
use Glueful\Http\Response;
#[Controller(prefix: '/api/users', middleware: ['auth'])]
class UserController
{
#[Get('/', name: 'users.index')]
public function index(): Response
{
return new Response(['users' => []]);
}
#[Get('/{id}', where: ['id' => '\d+'], name: 'users.show')]
#[Fields(allowed: ['id', 'name', 'email', 'posts', 'posts.comments'])]
public function show(int $id): Response
{
return new Response(['user' => ['id' => $id]]);
}
#[Post('/', middleware: ['validate:user'])]
public function store(Request $request): Response
{
// Create user
return new Response(['created' => true], 201);
}
#[Put('/{id}', where: ['id' => '\d+'])]
public function update(int $id, Request $request): Response
{
// Update user
return new Response(['updated' => true]);
}
#[Delete('/{id}', where: ['id' => '\d+'])]
public function destroy(int $id): Response
{
// Delete user
return new Response(null, 204);
}
}
Loading Attribute Routes
// Discover controllers under a directory
$router->discover(base_path('app/Controllers'));
// Or register a specific controller class
$router->controller(App\\Controllers\\UserController::class);
Field Selection
See the feature overview and API in Features → Field Selection
Enable GraphQL-style field selection for REST APIs to prevent over-fetching and N+1 queries:
Basic Field Selection
use Glueful\Support\FieldSelection\FieldSelector;
use Glueful\Routing\Attributes\{Get, Fields};
class UserController
{
#[Get('/users/{id}')]
#[Fields(allowed: ['id', 'name', 'email', 'posts', 'posts.comments'], strict: true)]
public function show(int $id, FieldSelector $selector): array
{
$user = $this->userRepo->findAsArray($id);
// Conditional loading based on requested fields
if ($selector->requested('posts')) {
$user['posts'] = $this->postRepo->findByUser($id);
if ($selector->requested('posts.comments')) {
// Batch load comments to prevent N+1
$postIds = array_column($user['posts'], 'id');
$comments = $this->commentRepo->findByPosts($postIds);
// Attach comments to posts...
}
}
return $user; // Middleware applies projection automatically
}
}
Request Formats
# REST-style syntax
GET /users/123?fields=id,name,email&expand=posts.title,posts.comments.text
# GraphQL-style syntax
GET /users/123?fields=user(id,name,posts(title,comments(text)))
# Wildcard selection
GET /users/123?fields=*&expand=posts.comments
Field Selection Middleware
The FieldSelectionMiddleware automatically handles field projection:
// Enable for specific routes
$router->group(['middleware' => ['field_selection']], function($router) {
$router->get('/users/{id}', [UserController::class, 'show']);
$router->get('/posts', [PostController::class, 'index']);
});
Preventing N+1 Queries with Expanders
use Glueful\Support\FieldSelection\Projector;
// Register expanders for batch loading
$projector = new Projector();
$projector->register('posts', function($context, $node, $data) {
$userIds = array_column($data, 'id');
return $this->postRepo->findByUsers($userIds);
});
$projector->register('comments', function($context, $node, $data) {
$postIds = array_column($data, 'id');
return $this->commentRepo->findByPosts($postIds);
});
Advanced Features
Named Routes
// Define named routes
$router->get('/users/{id}', [UserController::class, 'show'])
->name('users.show');
// Generate URLs from names
$url = $router->url('users.show', ['id' => 123]); // /users/123
// With query parameters
$url = $router->url('users.show', ['id' => 123], ['tab' => 'posts']); // /users/123?tab=posts
Route Model Binding
Route model binding is not provided by the router. Resolve models explicitly in handlers or via services/repositories. php use App\Models\User;
// Automatic model resolution $router->get('/users/{user}', function(User $user) { return new Response('user' => $user->toArray()); });
// Custom binding logic $router->bind('user', function($value) { return User::where('slug', $value)->firstOrFail(); });
### CORS Configuration
- Configure the `cors` settings (e.g., in a `config/cors.php` file or under the `cors` key in your app config). Allowed origins, headers, methods, max age, and credentials are supported.
- The router handles OPTIONS preflight automatically for matched paths and returns the appropriate Allow header.
- For additional policies, you can add a CORS middleware (PSR‑15 adapter or custom) and apply it via route groups.
### Route Caching
Routes are automatically cached in production. In development, route cache uses a short TTL and auto‑invalidates when route files change (including framework routes). No CLI is required to manage route cache.
### Not Found / Method Not Allowed
The router standardizes JSON error responses for 404 Not Found and 405 Method Not Allowed (405 responses include an `Allow` header). No explicit fallback route is needed.
## Performance Optimization
### Route Organization
The router uses several optimization techniques:
1. **Static Route Hash Table**: O(1) lookup for routes without parameters
2. **Route Bucketing**: Dynamic routes grouped by first segment
3. **Compiled Patterns**: Regex patterns pre-compiled and cached
4. **Reflection Caching**: Method/parameter reflection cached
### Optimization Tips
```php
// 1. Place static routes before dynamic ones (automatic with bucketing)
$router->get('/users/popular', $handler); // Checked first
$router->get('/users/{id}', $handler); // Checked second
// 2. Use specific constraints to fail fast
$router->get('/users/{id}', $handler)->where('id', '\d+');
// 3. Group related routes
$router->group(['prefix' => '/api/v1'], function($router) {
// All v1 routes share the prefix
});
// 4. Use route caching in production
// Routes are compiled to native PHP arrays
Benchmarks
The router achieves:
- Static routes: O(1) hash table lookup (~0.001ms)
- Dynamic routes: Optimized regex matching (~0.01ms)
- Route compilation: One-time cost, cached indefinitely
- Middleware pipeline: Lazy resolution, minimal overhead
Best Practices
1. Route Organization
// Organize routes by feature
routes/
├── api.php # API routes
├── web.php # Web routes
├── admin.php # Admin routes
└── webhooks.php # Webhook endpoints
// Framework loads routes via a centralized manifest
use Glueful\\Routing\\RouteManifest;
RouteManifest::load($router);
2. RESTful Conventions
// Follow REST conventions
$router->get('/users', [UserController::class, 'index']); // List
$router->get('/users/{id}', [UserController::class, 'show']); // Show
$router->post('/users', [UserController::class, 'store']); // Create
$router->put('/users/{id}', [UserController::class, 'update']); // Update
$router->delete('/users/{id}', [UserController::class, 'destroy']); // Delete
3. API Versioning
// Version via URL path
$router->group(['prefix' => '/api/v1'], function($router) {
// Version 1 routes
});
$router->group(['prefix' => '/api/v2'], function($router) {
// Version 2 routes
});
// Version via header (in middleware)
class ApiVersionMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next): Response
{
$version = $request->headers->get('API-Version', 'v1');
$request->attributes->set('api_version', $version);
return $next($request);
}
}
4. Security Best Practices
// Always authenticate sensitive routes
$router->group(['middleware' => ['auth']], function($router) {
$router->get('/profile', [ProfileController::class, 'show']);
$router->put('/profile', [ProfileController::class, 'update']);
});
// Rate limit public endpoints
$router->get('/api/search', $handler)
->middleware('rate_limit:10,60'); // 10 requests per minute
// Validate input
$router->post('/api/users', $handler)
->middleware('validate:user_create');
// CSRF protection for state-changing operations
$router->post('/actions/delete', $handler)
->middleware('csrf');
5. Error Handling
The router standardizes errors without extra hooks:
- 404 Not Found: Glueful\Http\Response::error('Not Found', 404)
- 405 Method Not Allowed: error with Allow header automatically set
6. Testing Routes
use PHPUnit\Framework\TestCase;
use Glueful\Framework;
use Symfony\Component\HttpFoundation\Request;
class RouteTest extends TestCase
{
public function test_user_route(): void
{
$app = Framework::create(getcwd())->boot();
$router = $app->getContainer()->get(Router::class);
$response = $router->dispatch(
Request::create('/api/users/123', 'GET')
);
$this->assertEquals(200, $response->getStatusCode());
}
}
Troubleshooting
Common Issues
Route not found (404)
- Check route registration order
- Verify path prefixes in groups
- In development, route cache auto‑expires (5s). To force clear: delete
storage/cache/routes_dev.phpor call:
app(Glueful\Routing\RouteCache::class)->clear();
Method not allowed (405)
- Verify HTTP method matches route definition
- Check if OPTIONS is needed for CORS
Middleware not executing
- Ensure middleware is registered in container
- Check middleware parameter syntax
- Verify group middleware inheritance
Parameter injection failing
- Type-hint parameters correctly
- Register bindings in container
- Check parameter names match route placeholders
Inspecting Routes Programmatically
$routes = $router->getAllRoutes(); // method, path, handler, middleware, name
Framework Endpoints Reference
The Glueful Framework includes several built-in endpoint groups that demonstrate real-world routing patterns:
Health Monitoring Routes (routes/health.php)
// System health with monitoring middleware
$router->group(['prefix' => '/health'], function (Router $router) {
// Main health check - high rate limit for monitoring tools
$router->get('/', function (Request $request) {
$healthController = container()->get(HealthController::class);
return $healthController->index();
})->middleware('rate_limit:60,60'); // 60 requests per minute
// Specific component checks with lower limits
$router->get('/database', function (Request $request) {
$healthController = container()->get(HealthController::class);
return $healthController->database();
})->middleware('rate_limit:30,60'); // 30 requests per minute
$router->get('/cache', [HealthController::class, 'cache'])
->middleware('rate_limit:30,60');
$router->get('/extensions', [HealthController::class, 'extensions'])
->middleware('rate_limit:20,60');
});
Authentication Routes (routes/auth.php)
// Authentication endpoints with varying rate limits
$router->group(['prefix' => '/auth'], function (Router $router) {
// Login with strict rate limiting
$router->post('/login', function (Request $request) {
$authController = container()->get(AuthController::class);
return $authController->login();
})->middleware('rate_limit:5,60'); // 5 attempts per minute
// Email verification
$router->post('/verify-email', [AuthController::class, 'verifyEmail']);
// OTP verification with additional security
$router->post('/verify-otp', [AuthController::class, 'verifyOtp'])
->middleware('rate_limit:3,60'); // 3 attempts per minute
// Token refresh and logout
$router->post('/refresh', [AuthController::class, 'refresh'])
->middleware(['auth', 'rate_limit:10,60']);
$router->post('/logout', [AuthController::class, 'logout'])
->middleware('auth');
});
Generic Resource Routes (routes/resource.php)
// RESTful resource endpoints with authentication
// List resources with pagination
$router->get('/{resource}', function (Request $request) {
$resourceController = container()->get(ResourceController::class);
$pathInfo = $request->getPathInfo();
$segments = explode('/', trim($pathInfo, '/'));
$params = ['resource' => $segments[0]];
$queryParams = $request->query->all();
return $resourceController->get($params, $queryParams);
})->middleware(['auth', 'rate_limit:100,60']); // 100 requests per minute
// Get single resource by UUID
$router->get('/{resource}/{uuid}', function (Request $request) {
$resourceController = container()->get(ResourceController::class);
$pathInfo = $request->getPathInfo();
$segments = explode('/', trim($pathInfo, '/'));
$params = ['resource' => $segments[0], 'uuid' => $segments[1]];
$queryParams = $request->query->all();
return $resourceController->getSingle($params, $queryParams);
})->middleware(['auth', 'rate_limit:200,60']); // Higher limit for single reads
// Create resource
$router->post('/{resource}', function (Request $request) {
$resourceController = container()->get(ResourceController::class);
// Implementation...
})->middleware(['auth', 'rate_limit:20,60']); // 20 creates per minute
// Update resource
$router->put('/{resource}/{uuid}', [ResourceController::class, 'update'])
->middleware(['auth', 'rate_limit:20,60']);
// Delete resource
$router->delete('/{resource}/{uuid}', [ResourceController::class, 'delete'])
->middleware(['auth', 'rate_limit:10,60']); // Lower limit for destructive operations
Key Patterns from Framework Routes
Rate Limiting Strategy:
- Authentication endpoints: Very restrictive (3-5 requests/minute)
- Health checks: Generous for monitoring (60 requests/minute)
- Read operations: High limits (100-200 requests/minute)
- Write operations: Moderate limits (10-20 requests/minute)
Security Patterns:
- All resource operations require authentication
- Destructive operations have the lowest rate limits
- OTP and login attempts are heavily restricted
- Health endpoints allow higher volume for monitoring
URL Structure:
- Grouped by feature (
/auth,/health) - RESTful patterns for resources (
/{resource},/{resource}/{id}) - Clear hierarchy and predictable paths
Conclusion
The Glueful routing system provides a powerful, performant foundation for building enterprise APIs and web applications. With features like O(1) static route matching, intelligent middleware pipelines, and GraphQL-style field selection, it handles both simple and complex routing requirements efficiently.
The framework's built-in endpoints demonstrate best practices for authentication, health monitoring, and resource management that you can use as patterns for your own applications.