Create Routes Using PHP Attributes
PHP 8 attributes are a modern way to define routes in your code directly. This approach makes your code cleaner and easier to understand.
Basic Setup
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use ByJG\RestServer\Route\RouteList;
use ByJG\RestServer\HttpRequestHandler;
$routeDefinition = new RouteList();
$routeDefinition->addClass(\My\ClassName::class);
$restServer = new HttpRequestHandler();
$restServer->handle($routeDefinition);
Class with Route Attributes
The class will handle the routes defined with the RouteDefinition attribute:
<?php
namespace My;
use ByJG\RestServer\Attributes\RouteDefinition;
use ByJG\RestServer\HttpRequest;
use ByJG\RestServer\HttpResponse;
use ByJG\RestServer\OutputProcessor\JsonOutputProcessor;
use ByJG\RestServer\OutputProcessor\XmlOutputProcessor;
class ClassName
{
//...
#[RouteDefinition('GET', '/route1')]
public function someMethod(HttpResponse $response, HttpRequest $request)
{
$response->write(['result' => 'ok']);
}
#[RouteDefinition('PUT', '/route2', XmlOutputProcessor::class)]
public function anotherMethod(HttpResponse $response, HttpRequest $request)
{
$response->write(['result' => 'ok']);
}
//...
}
RouteDefinition Parameters
The RouteDefinition attribute accepts the following parameters:
#[RouteDefinition(
string $method = 'GET', // HTTP method (GET, POST, PUT, DELETE, etc.)
string $path = '/', // URL path pattern
string $outputProcessor = JsonOutputProcessor::class // Output processor class
)]
By default, the output processor is set to JsonOutputProcessor. You can change it to any class that implements the
OutputProcessorInterface.
Route Interceptors
Route interceptors are a powerful feature that allow you to execute code before or after a route handler, using PHP 8 attributes.
Creating Interceptors
Before Route Interceptor
<?php
namespace My\Interceptors;
use Attribute;
use ByJG\RestServer\Attributes\BeforeRouteInterface;
use ByJG\RestServer\HttpRequest;
use ByJG\RestServer\HttpResponse;
#[Attribute(Attribute::TARGET_METHOD)]
class ValidateUserAccess implements BeforeRouteInterface
{
private array $requiredRoles;
public function __construct(array $requiredRoles = ['user'])
{
$this->requiredRoles = $requiredRoles;
}
public function processBefore(HttpResponse $response, HttpRequest $request)
{
$userRoles = $this->getUserRoles($request);
// Check if user has required roles
foreach ($this->requiredRoles as $role) {
if (!in_array($role, $userRoles)) {
throw new \ByJG\RestServer\Exception\Error403Exception('Insufficient permissions');
}
}
}
private function getUserRoles(HttpRequest $request): array
{
// Implementation to get user roles from request
// For example, from JWT token
return $request->param('jwt.roles') ?? [];
}
}
After Route Interceptor
<?php
namespace My\Interceptors;
use Attribute;
use ByJG\RestServer\Attributes\AfterRouteInterface;
use ByJG\RestServer\HttpRequest;
use ByJG\RestServer\HttpResponse;
#[Attribute(Attribute::TARGET_METHOD)]
class LogApiCall implements AfterRouteInterface
{
private string $logLevel;
public function __construct(string $logLevel = 'info')
{
$this->logLevel = $logLevel;
}
public function processAfter(HttpResponse $response, HttpRequest $request)
{
// Implementation to log the API call
$logger = /* Get your logger instance */;
$logger->{$this->logLevel}('API call', [
'path' => $request->getRequestPath(),
'method' => $request->getMethod(),
'status' => $response->getResponseCode(),
'user' => $request->param('jwt.sub') ?? 'anonymous'
]);
}
}
Using Interceptors with Routes
You can apply interceptors to route methods by adding the attribute:
<?php
namespace My\Controllers;
use ByJG\RestServer\Attributes\RouteDefinition;
use ByJG\RestServer\HttpRequest;
use ByJG\RestServer\HttpResponse;
use My\Interceptors\ValidateUserAccess;
use My\Interceptors\LogApiCall;
class UserController
{
#[RouteDefinition('GET', '/users')]
#[ValidateUserAccess(['admin'])]
#[LogApiCall('debug')]
public function listUsers(HttpResponse $response, HttpRequest $request)
{
// Only admin users can get here due to ValidateUserAccess interceptor
$response->write(['users' => $this->getUserList()]);
// After response is sent, LogApiCall will log this request
}
#[RouteDefinition('POST', '/users')]
#[ValidateUserAccess(['admin'])]
public function createUser(HttpResponse $response, HttpRequest $request)
{
// Only admins can create users
$userData = $request->body();
$userId = $this->createUserInDatabase($userData);
$response->write(['id' => $userId]);
}
#[RouteDefinition('GET', '/users/profile')]
#[ValidateUserAccess(['user', 'admin'])]
public function getUserProfile(HttpResponse $response, HttpRequest $request)
{
// Both users and admins can access profiles
$userId = $request->param('jwt.sub');
$profile = $this->getUserProfile($userId);
$response->write($profile);
}
}
Multiple Interceptors
You can apply multiple interceptors to a single route. They will be executed in the order they are declared:
#[RouteDefinition('POST', '/sensitive-operation')]
#[ValidateUserAccess(['admin'])]
#[ValidateCSRFToken]
#[RateLimitOperation(10)]
#[LogApiCall('warning')]
public function performSensitiveOperation(HttpResponse $response, HttpRequest $request)
{
// Implementation...
}
Common Use Cases for Interceptors
-
Authentication and Authorization
- Validate user roles and permissions
- Check API keys
- Verify tokens
-
Input Validation
- Validate request parameters
- Check required fields
- Sanitize inputs
-
Logging and Monitoring
- Log API calls
- Track performance metrics
- Record audit trails
-
Rate Limiting
- Limit frequency of requests
- Prevent abuse
-
Data Transformation
- Transform request data before processing
- Format response data after processing
-
Caching
- Implement response caching
- Cache invalidation
-
Cross-cutting Concerns
- Transaction management
- Error handling
Interceptors help keep your route handler methods focused on business logic by moving cross-cutting concerns into reusable attributes.
Built-in Route Attributes
RestServer provides built-in attributes for common authentication and authorization scenarios.
RequireAuthenticated
The RequireAuthenticated attribute ensures that a route can only be accessed by authenticated users. It works in
conjunction with the JWT middleware.
<?php
namespace My\Controllers;
use ByJG\RestServer\Attributes\RouteDefinition;
use ByJG\RestServer\Attributes\RequireAuthenticated;
use ByJG\RestServer\HttpRequest;
use ByJG\RestServer\HttpResponse;
class SecureController
{
#[RouteDefinition('GET', '/profile')]
#[RequireAuthenticated]
public function getProfile(HttpResponse $response, HttpRequest $request)
{
// Only authenticated users can access this endpoint
$userId = $request->param('jwt.sub');
$response->write(['user_id' => $userId]);
}
}
If the user is not authenticated (JWT token is missing or invalid), the attribute will throw a Error401Exception with
the message from the JWT middleware.
RequireRole
The RequireRole attribute ensures that a route can only be accessed by users with a specific role. It also checks for
authentication first.
<?php
namespace My\Controllers;
use ByJG\RestServer\Attributes\RouteDefinition;
use ByJG\RestServer\Attributes\RequireRole;
use ByJG\RestServer\HttpRequest;
use ByJG\RestServer\HttpResponse;
class AdminController
{
#[RouteDefinition('GET', '/admin/users')]
#[RequireRole('admin')]
public function listUsers(HttpResponse $response, HttpRequest $request)
{
// Only users with 'admin' role can access this endpoint
$response->write(['users' => $this->getAllUsers()]);
}
#[RouteDefinition('GET', '/moderator/reports')]
#[RequireRole('moderator', 'jwt.role')]
public function viewReports(HttpResponse $response, HttpRequest $request)
{
// Check the role from a custom JWT parameter
$response->write(['reports' => $this->getReports()]);
}
}
Constructor parameters:
$role(string, required): The required role value$roleParam(string, optional, default: 'role'): The parameter path where the role is stored$roleKey(string|null, optional): Optional key to extract from the parameter if it's an array or object
Examples:
// Basic usage - checks if $request->param('role') === 'admin'
#[RequireRole('admin')]
// Custom parameter - checks if $request->param('jwt.role') === 'moderator'
#[RequireRole('moderator', 'jwt.role')]
// Extract from array - if $request->param('user') returns ['role' => 'admin'],
// checks if $user['role'] === 'admin'
#[RequireRole('admin', 'user', 'role')]
// Extract from object - if $request->param('user') returns object with role property,
// checks if $user->role === 'editor'
#[RequireRole('editor', 'user', 'role')]
Exceptions:
- Throws
Error401Exceptionif the user is not authenticated - Throws
Error403Exceptionif the user doesn't have the required role
Combining Authentication Attributes
You can use both attributes together, though RequireRole already checks authentication:
#[RouteDefinition('POST', '/admin/settings')]
#[RequireAuthenticated] // Optional - RequireRole already checks this
#[RequireRole('admin')]
public function updateSettings(HttpResponse $response, HttpRequest $request)
{
// Implementation...
}