Apartado de Salas uses a custom-built router that provides clean URL routing without the overhead of a full framework. The router handles HTTP method matching, path normalization, and controller dispatching.
Router Architecture
The router is a simple PHP class located at app/core/Router.php that manages route registration and request dispatching.
Router Class
Here’s the complete router implementation:
<?php
class Router
{
private array $routes = [];
// Register routes
public function add(string $method, string $path, string $controller, string $action): void
{
$this->routes[] = [
'method' => strtoupper($method),
'path' => $path,
'controller' => $controller,
'action' => $action
];
}
// Get current request path
private function getCurrentPath(): string
{
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Current script path (e.g., /dashboard/Portafolio/Apartado-Salas/public/index.php)
$scriptName = $_SERVER['SCRIPT_NAME'];
// Base directory (e.g., /dashboard/Portafolio/Apartado-Salas/public)
$basePath = rtrim(str_replace('/index.php', '', $scriptName), '/');
// Remove base path from URI
if (strpos($uri, $basePath) === 0) {
$uri = substr($uri, strlen($basePath));
}
// Normalize the final path
$uri = '/' . trim($uri, '/');
return $uri === '/' ? '/' : $uri;
}
// Get HTTP request method
private function getRequestMethod(): string
{
return $_SERVER['REQUEST_METHOD'];
}
// Dispatch the request to the appropriate controller
public function dispatch(): void
{
$currentPath = $this->getCurrentPath();
$requestMethod = $this->getRequestMethod();
foreach ($this->routes as $route) {
if (
$route['path'] === $currentPath &&
$route['method'] === $requestMethod
) {
$this->callAction($route['controller'], $route['action']);
return;
}
}
$this->handleNotFound();
}
private function callAction(string $controllerName, string $action): void
{
$controllerFile = __DIR__ . '/../controllers/' . $controllerName . '.php';
if (!file_exists($controllerFile)) {
die('Controlador no encontrado');
}
require_once $controllerFile;
$controller = new $controllerName();
if (!method_exists($controller, $action)) {
die('Método no encontrado en el controlador');
}
$controller->$action();
}
// Handle 404 errors
private function handleNotFound(): void
{
http_response_code(404);
echo '404 - Página no encontrada';
}
}
Route Registration
Routes are registered in routes/web.php using the add() method:
<?php
// Authentication routes
$router->add('GET', '/login', 'AuthController', 'showLogin');
$router->add('POST', '/login', 'AuthController', 'login');
$router->add('GET', '/logout', 'AuthController', 'logout');
// Root route
$router->add('GET', '/', 'AuthController', 'showLogin');
// Dashboard
$router->add('GET', '/dashboard', 'DashboardController', 'index');
// Reservations
$router->add('GET', '/reservations/create', 'ReservationController', 'create');
$router->add('POST', '/reservations/store', 'ReservationController', 'store');
$router->add('GET', '/reservations', 'ReservationController', 'index');
$router->add('POST', '/reservations/approve', 'ReservationController', 'approve');
$router->add('POST', '/reservations/reject', 'ReservationController', 'reject');
$router->add('GET', '/reservations/show', 'ReservationController', 'show');
$router->add('GET', '/reservations/mine', 'ReservationController', 'mine');
// Internal API
$router->add('GET', '/api/materials', 'MaterialController', 'byRoom');
The add() method accepts four parameters:
$router->add(
'GET', // HTTP method
'/reservations/create', // URI path
'ReservationController', // Controller class name
'create' // Action method name
);
HTTP methods are automatically converted to uppercase for consistent matching. Both 'get' and 'GET' work identically.
Path Normalization
The router intelligently handles different installation paths, making it work in both root and subdirectory environments.
Base Path Detection
The getCurrentPath() method automatically detects the application’s base path:
// Example: App installed at /dashboard/Portafolio/Apartado-Salas/public/
$scriptName = $_SERVER['SCRIPT_NAME'];
// Result: /dashboard/Portafolio/Apartado-Salas/public/index.php
$basePath = rtrim(str_replace('/index.php', '', $scriptName), '/');
// Result: /dashboard/Portafolio/Apartado-Salas/public
URI Cleaning
The router strips the base path from incoming requests:
// Request: /dashboard/Portafolio/Apartado-Salas/public/login
// Base path: /dashboard/Portafolio/Apartado-Salas/public
// Clean path: /login
if (strpos($uri, $basePath) === 0) {
$uri = substr($uri, strlen($basePath));
}
When installed at domain root:Request: https://example.com/login
Script: /public/index.php
Base: /public
Path: /login ✓
When installed in a subdirectory:Request: https://example.com/app/public/login
Script: /app/public/index.php
Base: /app/public
Path: /login ✓
Request Dispatching
The dispatch() method is called from public/index.php to handle incoming requests:
$router = new Router();
require_once __DIR__ . '/../routes/web.php';
$router->dispatch();
Dispatch Process
-
Extract Request Details
$currentPath = $this->getCurrentPath(); // e.g., /login
$requestMethod = $this->getRequestMethod(); // e.g., POST
-
Match Against Registered Routes
foreach ($this->routes as $route) {
if (
$route['path'] === $currentPath &&
$route['method'] === $requestMethod
) {
$this->callAction($route['controller'], $route['action']);
return;
}
}
-
Call Controller Action or Return 404
Controller Loading
The callAction() method dynamically loads and instantiates controllers:
private function callAction(string $controllerName, string $action): void
{
// Build controller file path
$controllerFile = __DIR__ . '/../controllers/' . $controllerName . '.php';
// Check if controller file exists
if (!file_exists($controllerFile)) {
die('Controlador no encontrado');
}
// Load the controller file
require_once $controllerFile;
// Instantiate the controller
$controller = new $controllerName();
// Check if action method exists
if (!method_exists($controller, $action)) {
die('Método no encontrado en el controlador');
}
// Call the action method
$controller->$action();
}
The router uses dynamic method invocation to call controller actions. This allows for flexible routing without hardcoded controller references.
Error Handling
The router includes basic error handling for common scenarios:
404 Not Found
When no route matches the request:
private function handleNotFound(): void
{
http_response_code(404);
echo '404 - Página no encontrada';
}
Controller Not Found
When the controller file doesn’t exist:
if (!file_exists($controllerFile)) {
die('Controlador no encontrado');
}
Action Not Found
When the controller exists but the action method doesn’t:
if (!method_exists($controller, $action)) {
die('Método no encontrado en el controlador');
}
HTTP Method Support
The router currently supports standard HTTP methods:
Used for retrieving data and displaying pages:$router->add('GET', '/dashboard', 'DashboardController', 'index');
$router->add('GET', '/reservations', 'ReservationController', 'index');
Used for form submissions and data modifications:$router->add('POST', '/login', 'AuthController', 'login');
$router->add('POST', '/reservations/store', 'ReservationController', 'store');
$router->add('POST', '/reservations/approve', 'ReservationController', 'approve');
The router can be easily extended to support PUT, PATCH, and DELETE methods by adding them to the route matching logic.
Route Organization
Routes are organized by functionality in routes/web.php:
// Authentication
$router->add('GET', '/login', 'AuthController', 'showLogin');
$router->add('POST', '/login', 'AuthController', 'login');
$router->add('GET', '/logout', 'AuthController', 'logout');
// Dashboard
$router->add('GET', '/dashboard', 'DashboardController', 'index');
// Reservations - Create
$router->add('GET', '/reservations/create', 'ReservationController', 'create');
$router->add('POST', '/reservations/store', 'ReservationController', 'store');
// Reservations - List
$router->add('GET', '/reservations', 'ReservationController', 'index');
$router->add('GET', '/reservations/mine', 'ReservationController', 'mine');
// Reservations - Actions
$router->add('POST', '/reservations/approve', 'ReservationController', 'approve');
$router->add('POST', '/reservations/reject', 'ReservationController', 'reject');
Advantages of Custom Router
The custom router provides several benefits:
- Lightweight - No framework overhead or dependencies
- Flexible Installation - Works in root or subdirectories
- Simple to Understand - Clean, readable code
- Easy to Extend - Add features as needed
- Full Control - Complete control over routing logic
Limitations
The current implementation has some limitations:
- No Route Parameters - Cannot extract variables from URLs (e.g.,
/users/{id})
- No Named Routes - Routes are referenced by path strings
- No Route Groups - No middleware or prefix support
- Basic Error Handling - Simple error messages instead of custom error pages
These limitations are acceptable for the current application scope but could be addressed with future enhancements.
Future Enhancements
Potential improvements to the routing system:
// Route parameters
$router->add('GET', '/users/{id}', 'UserController', 'show');
// Named routes
$router->add('GET', '/dashboard', 'DashboardController', 'index', 'dashboard');
// Middleware
$router->add('GET', '/admin', 'AdminController', 'index')
->middleware('auth', 'admin');
// Route groups
$router->group(['prefix' => 'api', 'middleware' => 'api'], function($router) {
$router->add('GET', '/materials', 'MaterialController', 'index');
});
Next Steps