Bài 23 đã xây dựng MVC cơ bản với router đơn giản. Bài này nâng cấp thành Router chuyên nghiệp — hỗ trợ HTTP methods (GET/POST/PUT/DELETE), dynamic parameters, route groups, middleware và named routes như Laravel. Router tốt giúp code dễ bảo trì, URL đẹp và RESTful.
1. Vấn đề với Router đơn giản
<?php
// Router đơn giản từ bài 23:
$routes = [
'GET /' => 'SinhVienController@index',
'GET /sinhvien/:id' => 'SinhVienController@show',
'POST /sinhvien/:id/delete' => 'SinhVienController@delete',
];
// Vấn đề:
// ❌ Phải viết 'GET ', 'POST ' trước mỗi pattern — dễ sai
// ❌ Không có route groups — lặp '/admin' trước mỗi route admin
// ❌ Không có middleware — không kiểm tra auth trước khi vào controller
// ❌ Không có named routes — thay đổi URL phải sửa khắp nơi
// ❌ Không hỗ trợ PUT, PATCH, DELETE thật sự (chỉ fake qua POST)
?>2. Thiết kế Router Class
<?php
// File: app/Core/Router.php
namespace App\Core;
class Router {
protected array $routes = [];
protected array $namedRoutes = [];
protected array $groupStack = [];
/**
* Register GET route
*/
public function get(string $uri, $action): self {
return $this->addRoute('GET', $uri, $action);
}
/**
* Register POST route
*/
public function post(string $uri, $action): self {
return $this->addRoute('POST', $uri, $action);
}
/**
* Register PUT route
*/
public function put(string $uri, $action): self {
return $this->addRoute('PUT', $uri, $action);
}
/**
* Register PATCH route
*/
public function patch(string $uri, $action): self {
return $this->addRoute('PATCH', $uri, $action);
}
/**
* Register DELETE route
*/
public function delete(string $uri, $action): self {
return $this->addRoute('DELETE', $uri, $action);
}
/**
* Register route for multiple methods
*/
public function match(array $methods, string $uri, $action): self {
foreach ($methods as $method) {
$this->addRoute(strtoupper($method), $uri, $action);
}
return $this;
}
/**
* Register route for all HTTP methods
*/
public function any(string $uri, $action): self {
return $this->match(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], $uri, $action);
}
/**
* Add route to collection
*/
protected function addRoute(string $method, string $uri, $action): self {
$uri = $this->applyGroupPrefix($uri);
$route = [
'method' => $method,
'uri' => $uri,
'action' => $action,
'middleware' => $this->gatherMiddleware(),
];
$this->routes[] = $route;
return $this;
}
/**
* Set name for last registered route
*/
public function name(string $name): self {
if (empty($this->routes)) {
throw new \RuntimeException("Cannot name route: no routes defined");
}
$lastIndex = count($this->routes) - 1;
$this->namedRoutes[$name] = $lastIndex;
return $this;
}
/**
* Define route group with shared attributes
*/
public function group(array $attributes, callable $callback): void {
$this->groupStack[] = $attributes;
call_user_func($callback, $this);
array_pop($this->groupStack);
}
/**
* Apply group prefix to URI
*/
protected function applyGroupPrefix(string $uri): string {
$prefix = '';
foreach ($this->groupStack as $group) {
if (isset($group['prefix'])) {
$prefix .= '/' . trim($group['prefix'], '/');
}
}
$uri = '/' . trim($uri, '/');
return $prefix ? rtrim($prefix . $uri, '/') : $uri;
}
/**
* Gather middleware from group stack
*/
protected function gatherMiddleware(): array {
$middleware = [];
foreach ($this->groupStack as $group) {
if (isset($group['middleware'])) {
$mw = is_array($group['middleware']) ? $group['middleware'] : [$group['middleware']];
$middleware = array_merge($middleware, $mw);
}
}
return $middleware;
}
/**
* Dispatch request to matched route
*/
public function dispatch(string $method, string $uri): mixed {
// Method spoofing for PUT, PATCH, DELETE
if ($method === 'POST' && isset($_POST['_method'])) {
$method = strtoupper($_POST['_method']);
}
// Find matching route
foreach ($this->routes as $route) {
if ($route['method'] !== $method) {
continue;
}
// Convert route pattern to regex
$pattern = $this->convertToRegex($route['uri']);
if (preg_match($pattern, $uri, $matches)) {
// Extract parameters
$params = array_filter($matches, fn($k) => !is_int($k), ARRAY_FILTER_USE_KEY);
// Run middleware
foreach ($route['middleware'] as $middleware) {
$this->runMiddleware($middleware, $params);
}
// Execute action
return $this->executeAction($route['action'], $params);
}
}
// No route matched
throw new \RuntimeException("Route not found: {$method} {$uri}", 404);
}
/**
* Convert route URI to regex pattern
*/
protected function convertToRegex(string $uri): string {
// Replace :param with named capture group
$pattern = preg_replace('/:(\w+)/', '(?P<$1>[^/]+)', $uri);
// Escape forward slashes
$pattern = str_replace('/', '\/', $pattern);
return '/^' . $pattern . '$/';
}
/**
* Execute controller action
*/
protected function executeAction($action, array $params): mixed {
// Closure
if ($action instanceof \Closure) {
return call_user_func_array($action, [$params]);
}
// Controller@method string
if (is_string($action) && str_contains($action, '@')) {
[$controller, $method] = explode('@', $action);
$controllerClass = "App\\Controllers\\{$controller}";
if (!class_exists($controllerClass)) {
throw new \RuntimeException("Controller not found: {$controllerClass}");
}
$instance = new $controllerClass();
if (!method_exists($instance, $method)) {
throw new \RuntimeException("Method not found: {$controllerClass}::{$method}");
}
return call_user_func_array([$instance, $method], [$params]);
}
throw new \RuntimeException("Invalid route action");
}
/**
* Run middleware
*/
protected function runMiddleware(string $middleware, array $params): void {
$middlewareClass = "App\\Middleware\\{$middleware}";
if (!class_exists($middlewareClass)) {
throw new \RuntimeException("Middleware not found: {$middlewareClass}");
}
$instance = new $middlewareClass();
if (!method_exists($instance, 'handle')) {
throw new \RuntimeException("Middleware must have handle() method");
}
$instance->handle($params);
}
/**
* Generate URL for named route
*/
public function route(string $name, array $params = []): string {
if (!isset($this->namedRoutes[$name])) {
throw new \RuntimeException("Named route not found: {$name}");
}
$route = $this->routes[$this->namedRoutes[$name]];
$uri = $route['uri'];
// Replace parameters
foreach ($params as $key => $value) {
$uri = str_replace(":{$key}", $value, $uri);
}
return $uri;
}
/**
* Get all registered routes
*/
public function getRoutes(): array {
return $this->routes;
}
}
?>3. Route Definition — Web Routes
<?php
// File: config/routes.php
use App\Core\Router;
$router = new Router();
// Simple routes
$router->get('/', 'HomeController@index')->name('home');
// Routes with parameters
$router->get('/sinhvien/:id', 'SinhVienController@show')->name('sinhvien.show');
// Multiple HTTP methods
$router->match(['GET', 'POST'], '/contact', 'ContactController@handle');
// Resource routes (RESTful)
$router->get('/sinhvien', 'SinhVienController@index')->name('sinhvien.index');
$router->get('/sinhvien/create', 'SinhVienController@create')->name('sinhvien.create');
$router->post('/sinhvien', 'SinhVienController@store')->name('sinhvien.store');
$router->get('/sinhvien/:id/edit', 'SinhVienController@edit')->name('sinhvien.edit');
$router->put('/sinhvien/:id', 'SinhVienController@update')->name('sinhvien.update');
$router->delete('/sinhvien/:id', 'SinhVienController@destroy')->name('sinhvien.destroy');
// Route groups with prefix
$router->group(['prefix' => 'admin'], function($router) {
$router->get('/dashboard', 'Admin\DashboardController@index')->name('admin.dashboard');
$router->get('/users', 'Admin\UserController@index')->name('admin.users');
});
// Route groups with middleware
$router->group(['middleware' => 'Auth'], function($router) {
$router->get('/profile', 'ProfileController@show')->name('profile');
$router->post('/profile', 'ProfileController@update')->name('profile.update');
});
// Nested groups
$router->group(['prefix' => 'admin', 'middleware' => 'Auth'], function($router) {
$router->get('/settings', 'Admin\SettingsController@index');
$router->group(['prefix' => 'users'], function($router) {
$router->get('/', 'Admin\UserController@index');
$router->get('/:id', 'Admin\UserController@show');
});
});
// Closure routes (for simple logic)
$router->get('/test', function($params) {
return view('test', ['message' => 'Hello from closure!']);
});
return $router;
?>4. Middleware
<?php
// File: app/Middleware/Auth.php
namespace App\Middleware;
class Auth {
public function handle(array $params): void {
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
flash('error', 'Vui lòng đăng nhập để tiếp tục');
redirect('/login');
}
}
}
// ---
// File: app/Middleware/Guest.php
namespace App\Middleware;
class Guest {
public function handle(array $params): void {
// Redirect logged-in users away from login/register
if (isset($_SESSION['user_id'])) {
redirect('/');
}
}
}
// ---
// File: app/Middleware/Admin.php
namespace App\Middleware;
class Admin {
public function handle(array $params): void {
if (!isset($_SESSION['user_id'])) {
redirect('/login');
}
// Check if user is admin
if (!isset($_SESSION['is_admin']) || !$_SESSION['is_admin']) {
http_response_code(403);
echo view('errors.403');
exit;
}
}
}
?>5. Method Spoofing — PUT/DELETE qua Form
<?php
// HTML Form với method spoofing
?>
<!-- Form DELETE -->
<form method="POST" action="/sinhvien/<?php echo $id; ?>">
<input type="hidden" name="_method" value="DELETE">
<button type="submit">Xóa</button>
</form>
<!-- Form PUT -->
<form method="POST" action="/sinhvien/<?php echo $id; ?>">
<input type="hidden" name="_method" value="PUT">
<input type="text" name="ho_ten" value="<?php echo e($sv['ho_ten']); ?>">
<button type="submit">Cập nhật</button>
</form>6. Named Routes — Tạo URL
<?php
// File: app/Helpers/functions.php (bổ sung)
/**
* Generate URL from named route
*/
function route(string $name, array $params = []): string {
global $router;
if (!$router) {
throw new \RuntimeException("Router not initialized");
}
$uri = $router->route($name, $params);
$baseUrl = rtrim(config('app.url'), '/');
return $baseUrl . $uri;
}
// ---
// Sử dụng trong view:
?>
<a href="<?php echo route('sinhvien.show', ['id' => $sv['id']]); ?>">
Xem chi tiết
</a>
<a href="<?php echo route('sinhvien.edit', ['id' => $sv['id']]); ?>">
Chỉnh sửa
</a>
<form method="POST" action="<?php echo route('sinhvien.destroy', ['id' => $sv['id']]); ?>">
<input type="hidden" name="_method" value="DELETE">
<button>Xóa</button>
</form>7. RESTful Resource Routes
<?php
// File: app/Core/Router.php (thêm method)
/**
* Register RESTful resource routes
*/
public function resource(string $name, string $controller): void {
$this->get("/{$name}", "{$controller}@index")->name("{$name}.index");
$this->get("/{$name}/create", "{$controller}@create")->name("{$name}.create");
$this->post("/{$name}", "{$controller}@store")->name("{$name}.store");
$this->get("/{$name}/:id", "{$controller}@show")->name("{$name}.show");
$this->get("/{$name}/:id/edit", "{$controller}@edit")->name("{$name}.edit");
$this->put("/{$name}/:id", "{$controller}@update")->name("{$name}.update");
$this->delete("/{$name}/:id", "{$controller}@destroy")->name("{$name}.destroy");
}
// Sử dụng:
$router->resource('sinhvien', 'SinhVienController');
$router->resource('monhoc', 'MonHocController');
// Tự động tạo 7 routes cho mỗi resource
?>8. Update Entry Point
<?php
// File: public/index.php (cập nhật)
// Initialize application
$configs = require __DIR__ . '/../bootstrap/init.php';
// Load router
$router = require CONFIG_PATH . '/routes.php';
// Make router available globally
$GLOBALS['router'] = $router;
// Get request info
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Remove base path
$basePath = dirname($_SERVER['SCRIPT_NAME']);
if ($basePath !== '/') {
$uri = substr($uri, strlen($basePath));
}
$uri = '/' . trim($uri, '/');
// Dispatch
try {
$response = $router->dispatch($method, $uri);
// Output response
if (is_string($response)) {
echo $response;
} elseif (is_array($response)) {
header('Content-Type: application/json');
echo json_encode($response, JSON_UNESCAPED_UNICODE);
}
} catch (\RuntimeException $e) {
if ($e->getCode() === 404) {
http_response_code(404);
echo view('errors.404');
} else {
if (config('app.debug')) {
throw $e;
}
logMessage('ERROR', $e->getMessage());
http_response_code(500);
echo view('errors.500');
}
} catch (\Exception $e) {
if (config('app.debug')) {
throw $e;
}
logMessage('ERROR', $e->getMessage());
http_response_code(500);
echo view('errors.500');
}
?>9. Route List Command — Debug Routes
<?php
// File: routes-list.php (root directory)
require __DIR__ . '/bootstrap/init.php';
$router = require __DIR__ . '/config/routes.php';
$routes = $router->getRoutes();
echo "Registered Routes:\n";
echo str_repeat('-', 80) . "\n";
printf("%-8s %-30s %-30s %-20s\n", "METHOD", "URI", "ACTION", "MIDDLEWARE");
echo str_repeat('-', 80) . "\n";
foreach ($routes as $route) {
$action = is_string($route['action']) ? $route['action'] : 'Closure';
$middleware = implode(', ', $route['middleware']) ?: '-';
printf(
"%-8s %-30s %-30s %-20s\n",
$route['method'],
$route['uri'],
$action,
$middleware
);
}
echo str_repeat('-', 80) . "\n";
echo "Total: " . count($routes) . " routes\n";
// Chạy: php routes-list.php
?>10. Ví dụ Thực Tế — API Routes
<?php
// File: config/api-routes.php
use App\Core\Router;
$router = new Router();
// API prefix
$router->group(['prefix' => 'api/v1'], function($router) {
// Public endpoints
$router->get('/health', function() {
return ['status' => 'ok', 'timestamp' => time()];
});
// Protected endpoints
$router->group(['middleware' => 'ApiAuth'], function($router) {
// Students API
$router->get('/students', 'Api\StudentController@index');
$router->get('/students/:id', 'Api\StudentController@show');
$router->post('/students', 'Api\StudentController@store');
$router->put('/students/:id', 'Api\StudentController@update');
$router->delete('/students/:id', 'Api\StudentController@destroy');
// Stats
$router->get('/stats', 'Api\StatsController@index');
});
});
return $router;
?>Tóm tắt
- ✅ Router class với fluent API:
$router->get()->name() - ✅ HTTP methods: GET, POST, PUT, PATCH, DELETE
- ✅ Dynamic parameters:
:id,:slugtự động extract - ✅ Route groups: prefix, middleware tự động áp dụng
- ✅ Middleware: Auth, Guest, Admin — chạy trước controller
- ✅ Named routes:
route('sinhvien.show', ['id' => 1]) - ✅ Method spoofing: PUT/DELETE qua form POST với
_method - ✅ Resource routes:
resource()tạo 7 routes RESTful - ✅ Closure routes: logic đơn giản không cần controller
🎯 Bài tập thực hành
- Tích hợp Router vào dự án: Thay thế router đơn giản từ bài 23 bằng Router class mới. Chuyển tất cả routes sang cú pháp mới với named routes. Test CRUD sinh viên hoạt động bình thường.
-
Xây dựng Admin Panel: Tạo route group
/adminvới middlewareAdmin. ThêmAdminControllervới dashboard, quản lý users. Kiểm tra middleware chặn người dùng thường, chỉ admin vào được. -
RESTful API: Tạo file
config/api-routes.phpvới prefix/api/v1. ImplementApi\SinhVienControllertrả về JSON. Test với Postman hoặc curl: GET /api/v1/students, POST /api/v1/students, PUT /api/v1/students/1, DELETE /api/v1/students/1.