devsrealm / tonics-router-system
An Imaginary PHP Router System For Tonics Projects
Installs: 1 677
Dependents: 0
Suggesters: 0
Security: 0
Stars: 20
Watchers: 1
Forks: 1
Open Issues: 0
pkg:composer/devsrealm/tonics-router-system
Requires
- php: ^8.0
- ext-mbstring: *
- nyholm/psr7: ^1.8
- psr/http-factory: ^1.0
- psr/http-message: ^1.0 || ^2.0
README
A Trie based PHP Router System For Tonics Projects.
This would serve as a base router for tonics web apps, the router is different from most PHP Router in the sense that it doesn't use regex for matching urls, it instead uses a tree data structure where every path is hierarchically organized making it faster for finding both static or dynamic url.
Additionally, I came up with a concept called Node Teleporting which can further enhance and speed up searching dynamic routes, in the best case, dynamic route would be matched directly just like the static routes, and in the worse case, it would teleport a couple of times which is also faster than mere traversing.
You can learn more about the teleporting in the part 2 of how the router works.
Features
- Fast Trie-based routing - Uses a tree data structure instead of regex for faster URL matching
- Node Teleporting - Advanced optimization for dynamic routes
- PSR-7 Support - Full PSR-7 HTTP message interface compatibility
- Backward Compatible - Works with traditional PHP globals or PSR-7 objects
- Request Interceptors - Middleware-like functionality for request processing
- Route Groups - Organize routes hierarchically with shared attributes
- Dependency Injection - Built-in container for automatic dependency resolution
Requirements
- PHP 8.0 and above
- PHP mbstring extension enabled.
Installation
composer require devsrealm/tonics-router-system
If you don't want to use composer, go-to the release section and download the zip file that has a postfix of composer-no-required e.g tonics-router-system-v1.0.0-composer-no-required.zip
Unzip it and require it like so:
require 'path/to/tonics-router-system/vendor/autoload.php';
How The Router Works
- A Faster Router System in PHP (Part 1)
- A Faster Router System in PHP (Part 2) (Improvement & Benchmarks)
Documentation
Before you get started, wire up the Router dependencies:
use Devsrealm\TonicsRouterSystem\Container\Container; use Devsrealm\TonicsRouterSystem\Events\OnRequestProcess; use Devsrealm\TonicsRouterSystem\Handler\Router; use Devsrealm\TonicsRouterSystem\RequestInput; use Devsrealm\TonicsRouterSystem\Resolver\RouteResolver; use Devsrealm\TonicsRouterSystem\Response; use Devsrealm\TonicsRouterSystem\Route; use Devsrealm\TonicsRouterSystem\RouteNode; use Devsrealm\TonicsRouterSystem\RouteTreeGenerator; use Devsrealm\TonicsRouterSystem\State\RouteTreeGeneratorState; $onRequestProcess = new OnRequestProcess( new RouteResolver( new Container() ), new Route( new RouteTreeGenerator( new RouteTreeGeneratorState(), new RouteNode() ) ) ); $router = new Router( $onRequestProcess, $onRequestProcess->getRouteObject(), new Response( $onRequestProcess, new RequestInput() ) );
Basic routing
First parameter is the url paths which you want the route to match, and the second parameter could be a closure or a callback function that the route would call once the route matches.
$route = $router->getRoute(); $route->get('/', function() { return 'Welcome To My Home Page'; }); // Once your route is set up, dispatch it (don't forget to do this once all your route is set-up, otherwise, it won't work): try { $router->dispatchRequestURL(); } catch (Exception $e) { // handle error or 404 }
If you want to keep things organized, you can also resolve through a class method, like so:
$route->get('/', [HomePage::class, 'methodName']);
Request Interceptors
Some call it middleware, requestInterceptor sounds plain and simple to me.
RequestInterceptors can be used to intercept a request before it moves to the next life cycle or to other request interceptors.
For example, if you have an admin url path: /admin, and you want to check if a user is logged in before processing the request, you use the request interceptor. Let's see an example:
$route->get('admin', [AdminController::class, 'adminDashboard'], [IsAuthenticated::class]);
in isAuthenticated() class you can have something as such:
class Authenticated implements TonicsRouterRequestInterceptorInterface { /** * @inheritDoc */ public function handle(OnRequestProcess $request): void { if (UserData::isAuthenticated() === false){ # If this is for admin, then redirect to admin login if (str_starts_with($request->getRequestURL(), '/admin')){ redirect(route('admin.login')); } # If this is for customer, then redirect to customer login if (str_starts_with($request->getRequestURL(), '/customer')){ redirect(route('customer.login')); } # Else... SimpleState::displayUnauthorizedErrorMessage(); } } }
We implemented the TonicsRouterRequestInterceptorInterface (it is a must to implement the interface to use the request interceptor) which provides a handle method with the $request object.
Inside the handle method, I am checking if user is not authenticated, and thus redirecting them to their proper destination.
However, if user is authenticated, the interceptor would move to the next life cycle in the route state, the next life cycle could be a new request interceptor or a class method or a callback delegation.
To add more request interceptors, simply do:
$route->get('admin', [AdminController::class, 'adminDashboard'], [IsAuthenticated::class, MoreInterceptor::class, EvenMoreInterceptor::class] );
Route Required parameters
To match a dynamic url parameter you do:
$route->get('posts/:slug', function($slug) { return "Post with slug: $slug"; });
where you capture the slug from the url, for example, if user visits /posts/blog-post-title, you get access
to blog-post-title.
Alternatively you can do
$route->get('/posts/:slug', [PostsController::class, 'viewPost']);
where `PostsController could look like:
class PostsController { viewPost($slug) { return "Post with slug: $slug"; } }
Route Groups
With the route group you could organize route in a tree like fashion, the good thing about this approach is
you can share route attributes, such as route interceptors, parent url paths, etc. across a large number of routes without needing to define those attributes on each individual route.
instead of doing this:
$route->get('admin/login', [LoginController::class, 'showLoginForm'], [SpecialInterceptor::class, RedirectAuthenticated::class]); $route->post('admin/login', [LoginController::class, 'login'], [SpecialInterceptor::class]); $route->post('admin/logout', [LoginController::class, 'logout'], [SpecialInterceptor::class]);
do this:
$route->group('admin', function (Route $route){ $route->get('login', [LoginController::class, 'showLoginForm'], [RedirectAuthenticated::class]); $route->post('login', [LoginController::class, 'login']); $route->post('logout', [LoginController::class, 'logout']); }, [SpecialInterceptor::class]);
The end goal is identical to the above one but this is better organized.
You could also nest a group:
$route->group('/admin/posts', function (Route $route){ #--------------------------------- # POST CATEGORIES... #--------------------------------- $route->group('/category', function (Route $route){ $route->get('', [PostCategoryController::class, 'index']); $route->get(':category/edit', [PostCategoryController::class, 'edit']); $route->get('create', [PostCategoryController::class, 'create']); $route->post('store', [PostCategoryController::class, 'store']); $route->post(':category/trash', [PostCategoryController::class, 'trash']); $route->post( '/trash/multiple', [PostCategoryController::class, 'trashMultiple']); $route->match(['post', 'put', 'patch'], ':category/update', [PostCategoryController::class, 'update']); $route->match(['post', 'delete'], ':category/delete', [PostCategoryController::class, 'delete']); }, alias: 'category'); }, [StartSession::class, CSRFGuard::class, Authenticated::class, PostAccess::class]);
Route HTTP Verbs
$route->get(string $url, array|Closure $callback, array $requestInterceptor = [])$route->post(string $url, array|Closure $callback, array $requestInterceptor = [])$route->put(string $url, array|Closure $callback, array $requestInterceptor = [])$route->patch(string $url, array|Closure $callback, array $requestInterceptor = [])$route->delete(string $url, array|Closure $callback, array $requestInterceptor = [])$route->match(array $method, string $url, \Closure|array $callback, array $requestInterceptor = [])
With match, you can match multiple HTTP verbs in one fell swoop.
PSR-7 Support
The Tonics Router System now has full PSR-7 support while maintaining 100% backward compatibility. You can choose to use PSR-7 HTTP message interfaces or continue using the traditional approach.
Why Use PSR-7?
PSR-7 provides standardized HTTP message interfaces that:
- Work seamlessly with modern PHP frameworks and libraries
- Enable better testing with mock request/response objects
- Provide immutable request/response objects for safer code
- Follow PHP-FIG standards for better interoperability
Using PSR-7 Router (Recommended for new projects)
The easiest way to use PSR-7 is with the Psr7Router class:
use Devsrealm\TonicsRouterSystem\Handler\Psr7Router; // Create router from PHP globals $router = Psr7Router::create(); // Define your routes $router->getRoute()->get('/', function() { return 'Welcome to PSR-7!'; }); $router->getRoute()->get('/user/:id', function($id) { return "User ID: $id"; }); // Handle request and emit response $router->run();
Important PSR-7 Best Practice: Controllers should return content, not echo it:
// ✅ GOOD: Return content (PSR-7 compliant) $router->getRoute()->get('/api/users', function() { return json_encode(['users' => []]); }); // ❌ BAD: Echo content (not PSR-7 compliant) $router->getRoute()->get('/api/users', function() { echo json_encode(['users' => []]); // Don't do this! });
If you have legacy code that uses echo, you can use handleWithOutputBuffering() temporarily:
// For legacy code only (not recommended) $response = $router->handleWithOutputBuffering($request); $router->emit($response);
However, refactoring to return values is strongly recommended for proper PSR-7 compliance.
Manual PSR-7 Usage (Advanced)
For more control, you can manually create PSR-7 objects:
use Devsrealm\TonicsRouterSystem\Handler\Psr7Router; use Devsrealm\TonicsRouterSystem\Psr7Factory; // Create a PSR-7 request from globals $psrRequest = Psr7Factory::createServerRequestFromGlobals(); // Create the router with the PSR-7 request $router = new Psr7Router($psrRequest); // Define routes $router->getRoute()->get('/api/data', function() { return json_encode(['status' => 'success', 'data' => []]); }); // Handle the request $psrResponse = $router->handle($psrRequest); // Emit the response $router->emit($psrResponse);
Creating Router with Custom Container
use Devsrealm\TonicsRouterSystem\Handler\Psr7Router; use Devsrealm\TonicsRouterSystem\Container\Container; use Devsrealm\TonicsRouterSystem\Resolver\RouteResolver; use Devsrealm\TonicsRouterSystem\Psr7Factory; // Create and configure your container $container = new Container(); // Register all your dependencies $container->set(DatabaseInterface::class, fn() => new MySQLDatabase()); $container->set(UserRepository::class, fn($c) => new UserRepository($c->get(DatabaseInterface::class))); // ... more registrations // Create route resolver with your configured container $routeResolver = new RouteResolver($container); // Create PSR-7 request $psrRequest = Psr7Factory::createServerRequestFromGlobals(); // Create router with custom resolver $router = new Psr7Router($psrRequest, $routeResolver); // Now define routes $router->getRoute()->get('/', [HomeController::class, 'index']); $router->run();
Auto-wiring Example
The container can automatically resolve dependencies if they're type-hinted:
$router = Psr7Router::create(); // Register only what can't be auto-resolved (primitives, interfaces, etc.) $router->getContainer()->set(DatabaseInterface::class, function() { return new MySQLDatabase('localhost', 'mydb', 'user', 'pass'); }); // These classes will be auto-resolved class EmailService { // No dependencies - will be auto-created } class UserRepository { // DatabaseInterface must be registered (interface) public function __construct(private DatabaseInterface $db) {} } class UserService { // UserRepository will be auto-created, EmailService will be auto-created public function __construct( private UserRepository $repo, private EmailService $email ) {} } class UserController { // UserService and all its dependencies will be auto-resolved! public function __construct(private UserService $service) {} public function show($id) { return json_encode($this->service->findById($id)); } } // Just register the route - everything else is automatic! $router->getRoute()->get('/users/:id', [UserController::class, 'show']);
Using PSR-7 Request Adapter
You can also use PSR-7 requests with individual components:
use Devsrealm\TonicsRouterSystem\Adapter\Psr7RequestAdapter; use Devsrealm\TonicsRouterSystem\Psr7Factory; $psrRequest = Psr7Factory::createServerRequestFromGlobals(); $requestAdapter = new Psr7RequestAdapter($psrRequest); // Use it like the traditional RequestInput $postData = $requestAdapter->fromPost()->all(); $userId = $requestAdapter->fromGet()->retrieve('user_id');
Using PSR-7 Response Adapter
For PSR-7 compliant responses:
use Devsrealm\TonicsRouterSystem\Adapter\Psr7ResponseAdapter; use Devsrealm\TonicsRouterSystem\Psr7Factory; $psrResponse = Psr7Factory::createResponse(200); $responseAdapter = new Psr7ResponseAdapter($psrResponse); // Use Tonics-style methods $responseAdapter->json(['status' => 'success']); // or $responseAdapter->redirect('/dashboard');
Backward Compatibility
All existing code continues to work! The traditional approach still works exactly as before:
use Devsrealm\TonicsRouterSystem\Container\Container; use Devsrealm\TonicsRouterSystem\Events\OnRequestProcess; use Devsrealm\TonicsRouterSystem\Handler\Router; use Devsrealm\TonicsRouterSystem\RequestInput; use Devsrealm\TonicsRouterSystem\Resolver\RouteResolver; use Devsrealm\TonicsRouterSystem\Response; use Devsrealm\TonicsRouterSystem\Route; use Devsrealm\TonicsRouterSystem\RouteNode; use Devsrealm\TonicsRouterSystem\RouteTreeGenerator; use Devsrealm\TonicsRouterSystem\State\RouteTreeGeneratorState; $onRequestProcess = new OnRequestProcess( new RouteResolver(new Container()), new Route( new RouteTreeGenerator( new RouteTreeGeneratorState(), new RouteNode() ) ) ); $router = new Router( $onRequestProcess, $onRequestProcess->getRouteObject(), new Response($onRequestProcess, new RequestInput()) ); // Traditional usage works as always $route = $router->getRoute(); $route->get('/', function() { return 'Hello World'; }); $router->dispatchRequestURL();
PSR-7 in Controllers
When using PSR-7, you can type-hint PSR-7 interfaces in your controllers:
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; class UserController { public function show(ServerRequestInterface $request, string $id) { // Access PSR-7 request $queryParams = $request->getQueryParams(); $headers = $request->getHeaders(); return "User: $id"; } }
Testing with PSR-7
PSR-7 makes testing much easier:
use Devsrealm\TonicsRouterSystem\Handler\Psr7Router; use Nyholm\Psr7\Factory\Psr17Factory; // Create a test request $factory = new Psr17Factory(); $testRequest = $factory->createServerRequest('GET', '/api/user/123'); // Create router and handle $router = new Psr7Router($testRequest); $router->getRoute()->get('/api/user/:id', function($id) { return json_encode(['id' => $id]); }); $response = $router->handle($testRequest); // Assert response assert($response->getStatusCode() === 200); assert($response->getHeaderLine('Content-Type') === 'application/json');
Working with Controllers
Controllers help organize your application logic. Here are practical examples:
Basic Controller Example
namespace App\Controllers; class HomeController { public function index() { return 'Welcome to the homepage!'; } public function about() { return 'About us page'; } } // Register routes $route->get('/', [HomeController::class, 'index']); $route->get('/about', [HomeController::class, 'about']);
Controller with Route Parameters
namespace App\Controllers; class PostController { public function show($slug) { // Fetch post from database $post = Post::findBySlug($slug); if (!$post) { http_response_code(404); return 'Post not found'; } return json_encode($post); } public function showById($id) { $post = Post::find($id); return json_encode($post); } } // Register routes $route->get('/posts/:slug', [PostController::class, 'show']); $route->get('/posts/id/:id', [PostController::class, 'showById']);
Controller with Dependency Injection
The router automatically resolves dependencies through the container:
namespace App\Controllers; use App\Services\UserService; use App\Services\EmailService; class UserController { private UserService $userService; private EmailService $emailService; // Dependencies are automatically injected public function __construct(UserService $userService, EmailService $emailService) { $this->userService = $userService; $this->emailService = $emailService; } public function show($id) { $user = $this->userService->findById($id); return json_encode($user); } public function sendWelcomeEmail($id) { $user = $this->userService->findById($id); $this->emailService->sendWelcome($user->email); return json_encode(['message' => 'Email sent']); } } // Register routes $route->get('/user/:id', [UserController::class, 'show']); $route->post('/user/:id/welcome', [UserController::class, 'sendWelcomeEmail']);
RESTful Controller Example
namespace App\Controllers; use Devsrealm\TonicsRouterSystem\RequestInput; class ApiUserController { private RequestInput $input; public function __construct(RequestInput $input) { $this->input = $input; } // GET /api/users public function index() { $users = User::all(); return json_encode(['users' => $users]); } // GET /api/users/:id public function show($id) { $user = User::find($id); return json_encode(['user' => $user]); } // POST /api/users public function store() { $data = $this->input->fromPost(); $name = $data->retrieve('name'); $email = $data->retrieve('email'); $user = User::create(['name' => $name, 'email' => $email]); http_response_code(201); return json_encode(['user' => $user]); } // PUT /api/users/:id public function update($id) { $data = $this->input->fromPost(); $user = User::find($id); $user->name = $data->retrieve('name', $user->name); $user->email = $data->retrieve('email', $user->email); $user->save(); return json_encode(['user' => $user]); } // DELETE /api/users/:id public function destroy($id) { User::find($id)->delete(); return json_encode(['message' => 'User deleted']); } } // Register RESTful routes $route->get('/api/users', [ApiUserController::class, 'index']); $route->get('/api/users/:id', [ApiUserController::class, 'show']); $route->post('/api/users', [ApiUserController::class, 'store']); $route->put('/api/users/:id', [ApiUserController::class, 'update']); $route->delete('/api/users/:id', [ApiUserController::class, 'destroy']);
PSR-7 Controller Example
namespace App\Controllers; use Psr\Http\Message\ServerRequestInterface; use Devsrealm\TonicsRouterSystem\Adapter\Psr7ResponseAdapter; class Psr7UserController { // Type-hint PSR-7 interfaces public function show(ServerRequestInterface $request, $id) { // Access query parameters $queryParams = $request->getQueryParams(); $includeEmail = $queryParams['include_email'] ?? false; // Access headers $authToken = $request->getHeaderLine('Authorization'); // Get request body $body = $request->getBody()->getContents(); $user = User::find($id); if ($includeEmail === 'true') { return json_encode(['id' => $user->id, 'name' => $user->name, 'email' => $user->email]); } return json_encode(['id' => $user->id, 'name' => $user->name]); } }
Complete Example with Container + PSR-7
Here's a full real-world example showing how everything works together:
use Devsrealm\TonicsRouterSystem\Handler\Psr7Router; // 1. Create router $router = Psr7Router::create(); // 2. Configure container with your dependencies $container = $router->getContainer(); // Register database $container->singleton(PDO::class, function() { return new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass'); }); // Register repositories $container->set(UserRepository::class, function($c) { return new UserRepository($c->get(PDO::class)); }); $container->set(PostRepository::class, function($c) { return new PostRepository($c->get(PDO::class)); }); // Register services $container->set(UserService::class, function($c) { return new UserService( $c->get(UserRepository::class), $c->get(EmailService::class) ); }); $container->set(EmailService::class, function() { return new EmailService(getenv('SMTP_HOST'), getenv('SMTP_PORT')); }); // 3. Define your routes $router->getRoute()->get('/', [HomeController::class, 'index']); $router->getRoute()->get('/users/:id', [UserController::class, 'show']); $router->getRoute()->post('/users', [UserController::class, 'store']); $router->getRoute()->get('/posts/:slug', [PostController::class, 'show']); // 4. Run the application $router->run(); // Controller examples class HomeController { public function index() { return json_encode(['message' => 'Welcome to our API']); } } class UserController { // Dependencies auto-injected via container public function __construct( private UserService $userService, private UserRepository $userRepo ) {} public function show($id) { try { $user = $this->userService->findById($id); return json_encode(['user' => $user]); } catch (NotFoundException $e) { http_response_code(404); return json_encode(['error' => 'User not found']); } } public function store() { // Using PSR-7 request in constructor $data = json_decode(file_get_contents('php://input'), true); $user = $this->userService->createUser( $data['name'] ?? '', $data['email'] ?? '' ); http_response_code(201); return json_encode(['user' => $user]); } } class PostController { public function __construct(private PostRepository $postRepo) {} public function show($slug) { $post = $this->postRepo->findBySlug($slug); if (!$post) { http_response_code(404); return json_encode(['error' => 'Post not found']); } return json_encode(['post' => $post]); } } // Service layer class UserService { public function __construct( private UserRepository $userRepo, private EmailService $emailService ) {} public function findById($id) { return $this->userRepo->find($id); } public function createUser(string $name, string $email) { $user = $this->userRepo->create(['name' => $name, 'email' => $email]); $this->emailService->sendWelcome($user->email); return $user; } } // Repository layer class UserRepository { public function __construct(private PDO $db) {} public function find($id) { $stmt = $this->db->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]); return $stmt->fetch(PDO::FETCH_OBJ); } public function create(array $data) { $stmt = $this->db->prepare('INSERT INTO users (name, email) VALUES (?, ?)'); $stmt->execute([$data['name'], $data['email']]); return $this->find($this->db->lastInsertId()); } }
Request Interceptors (Middleware) Examples
Request Interceptors act as middleware to process requests before they reach your controllers.
Authentication Interceptor
namespace App\Middleware; use Devsrealm\TonicsRouterSystem\Events\OnRequestProcess; use Devsrealm\TonicsRouterSystem\Interfaces\TonicsRouterRequestInterceptorInterface; class AuthenticationMiddleware implements TonicsRouterRequestInterceptorInterface { public function handle(OnRequestProcess $request): void { // Check if user is authenticated if (!isset($_SESSION['user_id'])) { // Redirect to login page http_response_code(401); header('Location: /login'); exit; } // If authenticated, continue to next interceptor or controller } } // Usage $route->get('/dashboard', [DashboardController::class, 'index'], [AuthenticationMiddleware::class]);
CORS Interceptor
namespace App\Middleware; use Devsrealm\TonicsRouterSystem\Events\OnRequestProcess; use Devsrealm\TonicsRouterSystem\Interfaces\TonicsRouterRequestInterceptorInterface; class CorsMiddleware implements TonicsRouterRequestInterceptorInterface { public function handle(OnRequestProcess $request): void { // Add CORS headers header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); // Handle preflight requests if ($request->getRequestMethod() === 'OPTIONS') { http_response_code(200); exit; } // Continue to next interceptor or controller } } // Usage on API routes $route->group('/api', function (Route $route) { $route->get('/users', [ApiUserController::class, 'index']); $route->post('/users', [ApiUserController::class, 'store']); }, [CorsMiddleware::class]);
JSON Content-Type Validator
namespace App\Middleware; use Devsrealm\TonicsRouterSystem\Events\OnRequestProcess; use Devsrealm\TonicsRouterSystem\Interfaces\TonicsRouterRequestInterceptorInterface; class JsonContentTypeMiddleware implements TonicsRouterRequestInterceptorInterface { public function handle(OnRequestProcess $request): void { $method = $request->getRequestMethod(); // Check Content-Type for POST, PUT, PATCH requests if (in_array($method, ['POST', 'PUT', 'PATCH'])) { $contentType = $request->getHeaderByKey('CONTENT_TYPE'); if (strpos($contentType, 'application/json') === false) { http_response_code(415); echo json_encode(['error' => 'Content-Type must be application/json']); exit; } } // Continue to next interceptor or controller } }
Rate Limiting Interceptor
namespace App\Middleware; use Devsrealm\TonicsRouterSystem\Events\OnRequestProcess; use Devsrealm\TonicsRouterSystem\Interfaces\TonicsRouterRequestInterceptorInterface; class RateLimitMiddleware implements TonicsRouterRequestInterceptorInterface { private int $maxRequests = 100; private int $perMinutes = 1; public function handle(OnRequestProcess $request): void { $ip = $request->getHeaderByKey('REMOTE_ADDR'); $key = "rate_limit:$ip"; // Get current count from cache (Redis, Memcached, etc.) $count = Cache::get($key, 0); if ($count >= $this->maxRequests) { http_response_code(429); echo json_encode(['error' => 'Too many requests. Please try again later.']); exit; } // Increment counter Cache::increment($key); Cache::expire($key, $this->perMinutes * 60); // Continue to next interceptor or controller } }
Logging Interceptor
namespace App\Middleware; use Devsrealm\TonicsRouterSystem\Events\OnRequestProcess; use Devsrealm\TonicsRouterSystem\Interfaces\TonicsRouterRequestInterceptorInterface; class LoggingMiddleware implements TonicsRouterRequestInterceptorInterface { public function handle(OnRequestProcess $request): void { $method = $request->getRequestMethod(); $url = $request->getRequestURL(); $ip = $request->getHeaderByKey('REMOTE_ADDR'); $userAgent = $request->getUserAgent(); // Log the request error_log(sprintf( "[%s] %s %s from %s - %s", date('Y-m-d H:i:s'), $method, $url, $ip, $userAgent )); // Continue to next interceptor or controller } } // Apply to all routes $route->group('/', function (Route $route) { // All your routes here }, [LoggingMiddleware::class]);
Multiple Interceptors Example
namespace App\Middleware; // Chain multiple interceptors $route->group('/admin', function (Route $route) { $route->get('/dashboard', [AdminController::class, 'dashboard']); $route->get('/users', [AdminController::class, 'users']); $route->post('/users', [AdminController::class, 'createUser']); }, [ LoggingMiddleware::class, // First: Log the request AuthenticationMiddleware::class, // Second: Check if user is logged in AdminAuthorizationMiddleware::class, // Third: Check if user is admin CsrfMiddleware::class // Fourth: Validate CSRF token ]);
Best Practices
1. Controller Organization
✅ DO: Keep controllers focused and single-purpose
// Good: Focused controller class UserController { public function show($id) { /* ... */ } public function update($id) { /* ... */ } } class UserProfileController { public function show($id) { /* ... */ } public function updateAvatar($id) { /* ... */ } }
❌ DON'T: Create god controllers
// Bad: Too many responsibilities class UserController { public function show($id) { /* ... */ } public function updateProfile($id) { /* ... */ } public function uploadAvatar($id) { /* ... */ } public function sendEmail($id) { /* ... */ } public function generateReport($id) { /* ... */ } // ... 50 more methods }
2. Return Values vs Echo
✅ DO: Return values from controllers (especially for PSR-7)
public function show($id) { $user = User::find($id); return json_encode($user); // ✅ Return }
❌ DON'T: Echo directly in controllers
public function show($id) { $user = User::find($id); echo json_encode($user); // ❌ Echo (not PSR-7 compliant) }
3. Use Dependency Injection
✅ DO: Inject dependencies via constructor
class UserController { private UserRepository $userRepo; public function __construct(UserRepository $userRepo) { $this->userRepo = $userRepo; // ✅ Injected } public function show($id) { return json_encode($this->userRepo->find($id)); } }
❌ DON'T: Create dependencies inside methods
class UserController { public function show($id) { $userRepo = new UserRepository(); // ❌ Tight coupling return json_encode($userRepo->find($id)); } }
4. Request Interceptor Best Practices
✅ DO: Keep interceptors focused on one concern
// Good: Single responsibility class AuthenticationMiddleware implements TonicsRouterRequestInterceptorInterface { public function handle(OnRequestProcess $request): void { // Only handles authentication if (!$this->isAuthenticated()) { $this->redirectToLogin(); } } }
✅ DO: Chain interceptors for multiple checks
$route->group('/admin', function (Route $route) { // Routes here }, [ AuthenticationMiddleware::class, // Check if logged in AdminAuthorizationMiddleware::class, // Check if admin CsrfMiddleware::class // Validate CSRF ]);
❌ DON'T: Create monolithic interceptors
// Bad: Does too much class MegaMiddleware implements TonicsRouterRequestInterceptorInterface { public function handle(OnRequestProcess $request): void { // Authentication // Authorization // CSRF validation // Rate limiting // Logging // ... everything in one class } }
5. Route Organization
✅ DO: Group related routes
// Good: Organized by feature $route->group('/api/v1', function (Route $route) { $route->group('/users', function (Route $route) { $route->get('', [UserController::class, 'index']); $route->get(':id', [UserController::class, 'show']); $route->post('', [UserController::class, 'store']); }); $route->group('/posts', function (Route $route) { $route->get('', [PostController::class, 'index']); $route->get(':id', [PostController::class, 'show']); }); }, [CorsMiddleware::class, AuthMiddleware::class]);
❌ DON'T: Mix unrelated routes
// Bad: No organization $route->get('/api/v1/users', [UserController::class, 'index']); $route->get('/admin/dashboard', [AdminController::class, 'dashboard']); $route->get('/api/v1/posts', [PostController::class, 'index']); $route->get('/public/about', [PageController::class, 'about']);
6. Error Handling
✅ DO: Handle errors gracefully
public function show($id) { try { $user = User::findOrFail($id); return json_encode(['user' => $user]); } catch (NotFoundException $e) { http_response_code(404); return json_encode(['error' => 'User not found']); } catch (Exception $e) { http_response_code(500); return json_encode(['error' => 'Internal server error']); } }
✅ DO: Use proper HTTP status codes
// 200 - OK return json_encode(['message' => 'Success']); // 201 - Created http_response_code(201); return json_encode(['user' => $newUser]); // 400 - Bad Request http_response_code(400); return json_encode(['error' => 'Invalid input']); // 401 - Unauthorized http_response_code(401); return json_encode(['error' => 'Authentication required']); // 403 - Forbidden http_response_code(403); return json_encode(['error' => 'Access denied']); // 404 - Not Found http_response_code(404); return json_encode(['error' => 'Resource not found']); // 500 - Internal Server Error http_response_code(500); return json_encode(['error' => 'Server error']);
7. Input Validation
✅ DO: Validate input data
public function store(RequestInput $input) { $data = $input->fromPost(); // Validate required fields if (!$data->hasValue('name')) { http_response_code(400); return json_encode(['error' => 'Name is required']); } if (!$data->hasValue('email')) { http_response_code(400); return json_encode(['error' => 'Email is required']); } // Validate email format $email = $data->retrieve('email'); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { http_response_code(400); return json_encode(['error' => 'Invalid email format']); } // Create user $user = User::create([ 'name' => $data->retrieve('name'), 'email' => $email ]); return json_encode(['user' => $user]); }
8. Use Response Helper (PSR-7)
✅ DO: Use response adapter for clean code
use Devsrealm\TonicsRouterSystem\Adapter\Psr7ResponseAdapter; public function show(Psr7ResponseAdapter $response, $id) { $user = User::find($id); if (!$user) { return $response->httpResponseCode(404) ->json(['error' => 'User not found']); } return $response->json(['user' => $user]); }
9. Security Best Practices
✅ DO: Sanitize user input
public function search(RequestInput $input) { $query = $input->fromGet()->retrieve('q', ''); // Sanitize input $query = htmlspecialchars($query, ENT_QUOTES, 'UTF-8'); $results = Search::query($query); return json_encode(['results' => $results]); }
✅ DO: Use HTTPS for sensitive operations
class SecureMiddleware implements TonicsRouterRequestInterceptorInterface { public function handle(OnRequestProcess $request): void { if (!$request->isSecure()) { header('Location: https://' . $request->getHost() . $request->getRequestURL()); exit; } } }
✅ DO: Validate CSRF tokens
class CsrfMiddleware implements TonicsRouterRequestInterceptorInterface { public function handle(OnRequestProcess $request): void { if (in_array($request->getRequestMethod(), ['POST', 'PUT', 'DELETE'])) { $token = $_POST['csrf_token'] ?? ''; if (!$this->validateCsrfToken($token)) { http_response_code(403); echo json_encode(['error' => 'Invalid CSRF token']); exit; } } } private function validateCsrfToken(string $token): bool { return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); } }