Thời gian đọc: 22 phút
Để xây dựng một ứng dụng theo mô hình MVC (Model-View-Controller) trong PHP thuần, bạn cần nắm rõ cách các thành phần chính tương tác với nhau. Tôi sẽ hướng dẫn bạn triển khai các chức năng như bài viết, chuyên mục bài viết, trang chủ theo mô hình MVC
Trước hết, chúng ta cần tổ chức cấu trúc thư mục cho dự án:
/project-root
/app
/Controllers
/Models
/Views
/Middlewares
/Helpers
/Libraries
/Logs
/Cache
/config
config.php
database.php
routes.php
/public
index.php
/resources
/lang
/storage
.htaccess
composer.json
/config/database.php
)Bạn cần cấu hình kết nối cơ sở dữ liệu trong file này:
return [
'host' => 'localhost',
'dbname' => 'your_database_name',
'username' => 'your_username',
'password' => 'your_password',
'charset' => 'utf8mb4',
];
/config/routes.php
)Định nghĩa các tuyến đường (routes) trong ứng dụng:
return [
'/' => 'HomeController@index',
'/posts' => 'PostController@index',
'/post/{id}' => 'PostController@show',
'/category/{id}' => 'CategoryController@show',
];
Trong ví dụ này, {id}
là placeholder cho tham số sẽ được truyền vào phương thức của controller.
index.php
để xử lý tham số từ URLDưới đây là phiên bản cập nhật của file index.php
để hỗ trợ truyền tham số vào các phương thức của controller:
<?php
// Bật báo cáo lỗi trong quá trình phát triển
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
// Autoload các class sử dụng PSR-4 autoloading
spl_autoload_register(function ($class) {
$prefix = 'App\\';
$baseDir = __DIR__ . '/../app/';
$len = strlen($prefix);
// có vai trò kiểm tra xem tên lớp ($class) có bắt đầu với tiền tố ($prefix) được xác định hay không
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relativeClass = substr($class, $len);
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
// Nạp các tệp cấu hình
$routes = include __DIR__ . '/../config/routes.php';
// Lấy URL hiện tại và xử lý nó
$requestUri = trim($_SERVER['REQUEST_URI'], '/');
$requestUri = parse_url($requestUri, PHP_URL_PATH);
$found = false;
// Duyệt qua các route để tìm khớp
foreach ($routes as $route => $target) {
// Chuyển đổi route thành regex
$routePattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([a-zA-Z0-9_]+)', $route);
$routePattern = str_replace('/', '\/', $routePattern);
// Kiểm tra nếu URL khớp với route pattern
if (preg_match('/^' . $routePattern . '$/', $requestUri, $matches)) {
array_shift($matches); // Bỏ phần tử đầu tiên vì đó là toàn bộ khớp
// Tách controller và method
$targetParts = explode('@', $target);
$controllerName = $targetParts[0];
$methodName = $targetParts[1];
$controllerClass = "App\\Controllers\\$controllerName";
if (class_exists($controllerClass)) {
$controller = new $controllerClass();
if (method_exists($controller, $methodName)) {
// Gọi phương thức của controller với các tham số
call_user_func_array([$controller, $methodName], $matches);
$found = true;
break;
} else {
http_response_code(404);
echo "Method $methodName not found in controller $controllerName.";
$found = true;
break;
}
} else {
http_response_code(404);
echo "Controller $controllerName not found.";
$found = true;
break;
}
}
}
if (!$found) {
http_response_code(404);
echo "Route not found.";
}
Giải thích
routes.php
, các route có thể chứa tham số dưới dạng {id}
, {slug}
, v.v. Ví dụ /post/{id}
.{id}
, {slug}
thành các regex có thể khớp với các phần URL tương ứng. Cụ thể, {id}
trở thành ([a-zA-Z0-9_]+)
.$matches
.call_user_func_array
, chúng ta truyền các tham số từ $matches
vào phương thức của controller.Ví dụ thực tế
Với cấu hình trên, nếu bạn truy cập vào URL /post/1
, hệ thống sẽ:
/post/{id}
.1
từ URL và truyền nó vào phương thức show
của PostController
.Phương thức show
trong PostController
sẽ nhận tham số $id
và có thể sử dụng nó như sau:
Việc truyền tham số vào phương thức của controller có thể được thực hiện trực tiếp trong index.php
, như tôi đã trình bày ở trên. Tuy nhiên, nếu ứng dụng của bạn phức tạp hoặc bạn muốn tổ chức mã nguồn một cách rõ ràng và dễ bảo trì hơn, bạn có thể tách việc xử lý logic đó ra một hàm hoặc lớp riêng biệt. Dưới đây, tôi sẽ so sánh hai cách tiếp cận này để bạn có thể lựa chọn giải pháp phù hợp nhất.
index.php
Ưu điểm:
index.php
giúp giảm thiểu số lượng tệp tin và lớp cần phải quản lý.Nhược điểm:
index.php
có thể trở nên phức tạp và khó bảo trì nếu chứa quá nhiều logic.Ưu điểm:
Nhược điểm:
Dưới đây là một cách tiếp cận sử dụng lớp Router
riêng để xử lý route và truyền tham số:
Router
namespace App\Core;
class Router {
private $routes;
public function __construct($routes) {
$this->routes = $routes;
}
public function direct($requestUri) {
foreach ($this->routes as $route => $target) {
$routePattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([a-zA-Z0-9_]+)', $route);
$routePattern = str_replace('/', '\/', $routePattern);
if (preg_match('/^' . $routePattern . '$/', $requestUri, $matches)) {
array_shift($matches);
list($controller, $method) = explode('@', $target);
$controllerClass = "App\\Controllers\\$controller";
if (class_exists($controllerClass)) {
$controllerObject = new $controllerClass();
if (method_exists($controllerObject, $method)) {
return call_user_func_array([$controllerObject, $method], $matches);
}
}
}
}
http_response_code(404);
echo "Route not found.";
}
}
index.php
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
spl_autoload_register(function ($class) {
$prefix = 'App\\';
$baseDir = __DIR__ . '/../app/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relativeClass = substr($class, $len);
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
$routes = include __DIR__ . '/../config/routes.php';
$requestUri = trim($_SERVER['REQUEST_URI'], '/');
$requestUri = parse_url($requestUri, PHP_URL_PATH);
$router = new \App\Core\Router($routes);
$router->direct($requestUri);
Dự án nhỏ hoặc đơn giản: Nếu bạn đang phát triển một ứng dụng nhỏ hoặc đơn giản, việc xử lý trực tiếp trong index.php
có thể là đủ.
Dự án lớn hoặc cần bảo trì lâu dài: Nếu dự án của bạn có khả năng mở rộng hoặc cần bảo trì trong thời gian dài, tách logic xử lý routing và truyền tham số vào một lớp hoặc hàm riêng biệt sẽ là lựa chọn tốt hơn. Điều này giúp tổ chức mã nguồn tốt hơn, dễ bảo trì và mở rộng trong tương lai.
/app/Controllers/BaseController.php
)BaseController là lớp cha mà tất cả các Controller khác sẽ kế thừa:
namespace App\Controllers;
class BaseController {
public function render($view, $data = []) {
extract($data);
include __DIR__ . '/../Views/' . $view . '.php';
}
}
/app/Controllers/PostController.php
)Controller quản lý bài viết:
namespace App\Controllers;
use App\Models\PostModel;
class PostController extends BaseController {
private $postModel;
public function __construct() {
$this->postModel = new PostModel();
}
public function index() {
$posts = $this->postModel->getAllPosts();
$this->render('posts/index', ['posts' => $posts]);
}
public function show($id) {
$post = $this->postModel->getPostById($id);
$this->render('posts/show', ['post' => $post]);
}
}
/app/Models/BaseModel.php
)Lớp BaseModel quản lý kết nối cơ sở dữ liệu và các chức năng chung:
namespace App\Models;
use PDO;
class BaseModel {
protected $db;
public function __construct() {
$config = include __DIR__ . '/../../config/database.php';
$dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
$this->db = new PDO($dsn, $config['username'], $config['password']);
}
}
/app/Models/PostModel.php
)Model quản lý dữ liệu bài viết:
namespace App\Models;
use PDO;
class BaseModel {
protected $db;
public function __construct() {
$config = include __DIR__ . '/../../config/database.php';
$dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
$this->db = new PDO($dsn, $config['username'], $config['password']);
}
}
/app/Views/posts/index.php
)Views sẽ hiển thị dữ liệu từ Controller:
<!DOCTYPE html>
<html>
<head>
<title>Posts</title>
</head>
<body>
<h1>All Posts</h1>
<ul>
<?php foreach($posts as $post): ?>
<li>
<a href="/post/<?= $post['id'] ?>"><?= $post['title'] ?></a>
</li>
<?php endforeach; ?>
</ul>
</body>
</html>
/app/Middlewares/CheckAuth.php
)Middleware là một lớp hoặc thành phần trong ứng dụng PHP MVC, được sử dụng để xử lý logic trước khi yêu cầu HTTP được gửi đến controller hoặc sau khi controller đã xử lý yêu cầu. Middleware rất hữu ích để xử lý các tác vụ như xác thực (authentication), phân quyền (authorization), log request, hoặc thực hiện các thao tác tiền xử lý khác.
Dưới đây là chi tiết về cách tạo và sử dụng middleware, cụ thể là middleware kiểm tra xác thực người dùng (CheckAuth
), và cách nó được nạp và xử lý trong ứng dụng.
CheckAuth
Đầu tiên, chúng ta sẽ tạo một middleware có tên CheckAuth
để kiểm tra xem người dùng đã đăng nhập hay chưa. Nếu người dùng chưa đăng nhập, middleware này sẽ chuyển hướng họ đến trang đăng nhập.
Middleware thường được đặt trong thư mục /app/Middlewares/
để tổ chức mã nguồn rõ ràng. Dưới đây là cấu trúc thư mục:
/app
/Controllers
/Models
/Views
/Middlewares
- CheckAuth.php
/Core
...
/public
- index.php
/config
- routes.php
- config.php
CheckAuth.php
Dưới đây là ví dụ về middleware CheckAuth
:
<?php
namespace App\Middlewares;
class CheckAuth {
public function handle() {
// Kiểm tra xem người dùng đã đăng nhập hay chưa
if (!isset($_SESSION['user'])) {
// Nếu chưa đăng nhập, chuyển hướng đến trang đăng nhập
header('Location: /login');
exit;
}
// Nếu đã đăng nhập, middleware này không làm gì cả và cho phép tiếp tục xử lý
}
}
Để tổ chức và quản lý middleware một cách hiệu quả hơn, bạn có thể cấu hình middleware trong file routes.php
và xử lý chúng trong lớp Router
. Dưới đây là hướng dẫn chi tiết cách làm:
Cấu hình Middleware trong routes.php
Trong file routes.php
, bạn có thể cấu hình middleware cho từng route hoặc nhóm route cụ thể. Ví dụ:
return [
'/' => [
'uses' => 'HomeController@index',
],
'/dashboard' => [
'uses' => 'DashboardController@index',
'middleware' => ['auth'], // Middleware kiểm tra xác thực
],
'/profile' => [
'uses' => 'ProfileController@show',
'middleware' => ['auth'], // Middleware kiểm tra xác thực
],
'/posts/create' => [
'uses' => 'PostController@create',
'middleware' => ['auth'], // Middleware kiểm tra xác thực
],
'/post/{id}' => [
'uses' => 'PostController@show',
],
'/login' => [
'uses' => 'AuthController@login',
],
// Thêm các route khác ở đây
];
Router
để xử lý MiddlewareLớp Router
sẽ được cập nhật để xử lý middleware trước khi gọi controller. Dưới đây là cách bạn có thể triển khai:
namespace App\Core;
use App\Middlewares\CheckAuth;
class Router {
private $routes;
public function __construct($routes) {
$this->routes = $routes;
}
public function direct($requestUri) {
foreach ($this->routes as $route => $options) {
$routePattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([a-zA-Z0-9_]+)', $route);
$routePattern = str_replace('/', '\/', $routePattern);
if (preg_match('/^' . $routePattern . '$/', $requestUri, $matches)) {
array_shift($matches);
// Lấy controller và phương thức từ cấu hình
$target = $options['uses'];
list($controller, $method) = explode('@', $target);
$controllerClass = "App\\Controllers\\$controller";
// Xử lý middleware nếu có
if (isset($options['middleware'])) {
foreach ($options['middleware'] as $middleware) {
$this->handleMiddleware($middleware);
}
}
if (class_exists($controllerClass)) {
$controllerObject = new $controllerClass();
if (method_exists($controllerObject, $method)) {
return call_user_func_array([$controllerObject, $method], $matches);
}
}
}
}
http_response_code(404);
echo "Route not found.";
}
private function handleMiddleware($middleware) {
switch ($middleware) {
case 'auth':
$authMiddleware = new CheckAuth();
$authMiddleware->handle();
break;
// Bạn có thể thêm các middleware khác tại đây
default:
throw new \Exception("Middleware $middleware not found.");
}
}
}
Router
'middleware'
trong cấu hình route để chỉ định middleware cần chạy trước khi truy cập vào controller. Ví dụ, 'middleware' => ['auth']
chỉ định rằng route này cần phải chạy middleware CheckAuth
.handleMiddleware
để xử lý các middleware đã được cấu hình.handleMiddleware
:
auth
được ánh xạ đến lớp CheckAuth
.Middleware là một công cụ mạnh mẽ giúp bạn kiểm soát luồng yêu cầu trong ứng dụng PHP MVC. Bằng cách sử dụng middleware, bạn có thể tách biệt rõ ràng logic xử lý phụ trợ (như kiểm tra xác thực) khỏi các controller, giúp mã nguồn dễ bảo trì và mở rộng hơn. Việc nạp và xử lý middleware có thể thực hiện trực tiếp trong index.php
, nhưng việc tổ chức tốt hơn là thông qua một lớp Router để quản lý các middleware một cách linh hoạt hơn.
/resources/lang/en.php
, vn.php
)Để triển khai chức năng đa ngôn ngữ trong ứng dụng PHP MVC, bạn có thể sử dụng các tệp ngôn ngữ để lưu trữ các chuỗi văn bản và nạp chúng dựa trên ngôn ngữ mà người dùng đã chọn. Dưới đây là hướng dẫn chi tiết về cách nạp và xử lý đa ngôn ngữ trong ứng dụng của bạn.
Bạn sẽ cần tạo thư mục resources/lang/
và đặt các tệp ngôn ngữ tương ứng cho từng ngôn ngữ mà bạn muốn hỗ trợ. Ví dụ:
/resources
/lang
- en.php
- vn.php
/app
/Controllers
/Models
/Views
...
/public
- index.php
/config
- config.php
Mỗi tệp ngôn ngữ sẽ chứa một mảng các chuỗi văn bản được sử dụng trong ứng dụng.
Tệp en.php
<?php
return [
'welcome' => 'Welcome to our website!',
'login' => 'Login',
'register' => 'Register',
'dashboard' => 'Dashboard',
// Thêm các chuỗi văn bản khác
];
Tệp vn.php
<?php
return [
'welcome' => 'Chào mừng bạn đến với trang web của chúng tôi!',
'login' => 'Đăng nhập',
'register' => 'Đăng ký',
'dashboard' => 'Bảng điều khiển',
// Thêm các chuỗi văn bản khác
];
Bạn cần một helper để nạp các tệp ngôn ngữ và lấy chuỗi văn bản tương ứng. Tạo một helper trong app/Helpers/Localization.php
:
<?php
namespace App\Helpers;
class Localization {
private $language;
private $translations = [];
public function __construct($language = 'en') {
$this->language = $language;
$this->loadTranslations();
}
private function loadTranslations() {
$file = __DIR__ . '/../../resources/lang/' . $this->language . '.php';
if (file_exists($file)) {
$this->translations = include $file;
}
}
public function get($key) {
return $this->translations[$key] ?? $key;
}
public static function translate($key, $language = 'en') {
$localization = new self($language);
return $localization->get($key);
}
}
index.php
Trong file index.php
, bạn có thể nạp Helper và thiết lập ngôn ngữ mặc định hoặc dựa trên lựa chọn của người dùng:
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
// Nạp autoload các class
spl_autoload_register(function ($class) {
$prefix = 'App\\';
$baseDir = __DIR__ . '/../app/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relativeClass = substr($class, $len);
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
// Xác định ngôn ngữ dựa trên session hoặc query string
$language = isset($_GET['lang']) ? $_GET['lang'] : (isset($_SESSION['lang']) ? $_SESSION['lang'] : 'en');
$_SESSION['lang'] = $language;
// Khởi tạo Localization Helper
use App\Helpers\Localization;
$localization = new Localization($language);
// Ví dụ: sử dụng helper để dịch chuỗi văn bản
echo $localization->get('welcome');
// Tiếp tục xử lý routing và các phần khác
$routes = include __DIR__ . '/../config/routes.php';
$requestUri = trim($_SERVER['REQUEST_URI'], '/');
$requestUri = parse_url($requestUri, PHP_URL_PATH);
$router = new \App\Core\Router($routes);
$router->direct($requestUri);
Bạn có thể sử dụng Helper để dịch các chuỗi văn bản trong Views và Controllers.
<?php
namespace App\Controllers;
use App\Helpers\Localization;
class HomeController {
public function index() {
$language = $_SESSION['lang'] ?? 'en';
$welcomeMessage = Localization::translate('welcome', $language);
// Truyền thông điệp chào mừng tới view
$this->render('home/index', ['welcomeMessage' => $welcomeMessage]);
}
}
<?php
echo $welcomeMessage;
?>
/app/Helpers/url_helper.php
)Để nạp các Helper trong ứng dụng PHP MVC của bạn thông qua cấu hình mảng, bạn có thể thiết lập một hệ thống quản lý việc nạp Helper một cách tự động dựa trên cấu hình này. Dưới đây là hướng dẫn chi tiết:
Trước tiên, tạo một file cấu hình để liệt kê các Helper mà bạn muốn nạp tự động. Tạo file config/helpers.php
với nội dung như sau:
<?php
return [
'url_helper',
'string_helper',
'array_helper',
// Thêm các helper khác nếu có
];
Giả sử bạn đã có các helper tương ứng trong thư mục /app/Helpers/
như url_helper.php
, string_helper.php
, và array_helper.php
. Ví dụ:
url_helper.php
string_helper.php
array_helper.php
index.php
để Nạp HelperTrong file index.php
, bạn sẽ cần thêm logic để tự động nạp các Helper được liệt kê trong file cấu hình.
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
// Nạp autoload các class
spl_autoload_register(function ($class) {
$prefix = 'App\\';
$baseDir = __DIR__ . '/../app/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relativeClass = substr($class, $len);
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
// Nạp các helper từ file cấu hình
$helpers = include __DIR__ . '/../config/helpers.php';
foreach ($helpers as $helper) {
$helperPath = __DIR__ . '/../app/Helpers/' . $helper . '.php';
if (file_exists($helperPath)) {
require_once $helperPath;
} else {
throw new Exception("Helper file not found: " . $helperPath);
}
}
// Tiếp tục với phần xử lý routing và controller
$routes = include __DIR__ . '/../config/routes.php';
$requestUri = trim($_SERVER['REQUEST_URI'], '/');
$requestUri = parse_url($requestUri, PHP_URL_PATH);
$router = new \App\Core\Router($routes);
$router->direct($requestUri);
Giải Thích Hoạt Động
config/helpers.php
: Liệt kê các helper mà bạn muốn nạp tự động dưới dạng mảng. Bạn có thể thêm hoặc bớt các helper tại đây mà không cần chỉnh sửa logic nạp trong index.php
.index.php
, sau khi lấy danh sách các helper từ file cấu hình, một vòng lặp foreach
được sử dụng để nạp từng helper. Điều này giúp bạn dễ dàng quản lý và mở rộng danh sách helper mà không phải nạp từng helper thủ công.Ví Dụ Cụ Thể
Giả sử bạn có các hàm trong các helper như sau:
url_helper.php
<?php
if (!function_exists('base_url')) {
function base_url($path = '') {
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'];
$baseUrl = $protocol . $host;
if ($path) {
return rtrim($baseUrl, '/') . '/' . ltrim($path, '/');
}
return $baseUrl;
}
}
string_helper.php
<?php
if (!function_exists('str_contains')) {
function str_contains($haystack, $needle) {
return strpos($haystack, $needle) !== false;
}
}
array_helper.php
<?php
if (!function_exists('array_get')) {
function array_get($array, $key, $default = null) {
return isset($array[$key]) ? $array[$key] : $default;
}
}
Khi bạn đã cấu hình và nạp các helper, bạn có thể sử dụng các hàm như base_url()
, str_contains()
, và array_get()
ở bất kỳ đâu trong ứng dụng mà không cần phải nạp lại chúng.
Bằng cách sử dụng một cấu hình mảng để quản lý các helper, bạn tạo ra một hệ thống linh hoạt và dễ bảo trì. Khi bạn cần thêm helper mới, bạn chỉ cần cập nhật file cấu hình config/helpers.php
, giúp mã nguồn của bạn dễ dàng mở rộng và sạch sẽ hơn.
Bạn có thể sử dụng Composer để cài đặt các thư viện bên thứ 3:
composer require phpmailer/phpmailer
Trong code:
use PHPMailer\PHPMailer\PHPMailer;
$mail = new PHPMailer();
/app/Helpers/log_helper.php
)Tạo một helper để ghi log:
function write_log($message) {
$logFile = __DIR__ . '/../Logs/log.txt';
file_put_contents($logFile, date('Y-m-d H:i:s') . ": " . $message . "\n", FILE_APPEND);
}
/app/Cache/Cache.php
)Sử dụng file caching:
namespace App\Cache;
class Cache {
public static function set($key, $value, $expiration = 3600) {
$file = __DIR__ . "/$key.cache";
$data = ['expiration' => time() + $expiration, 'value' => $value];
file_put_contents($file, serialize($data));
}
public static function get($key) {
$file = __DIR__ . "/$key.cache";
if (file_exists($file)) {
$data = unserialize(file_get_contents($file));
if ($data['expiration'] >= time()) {
return $data['value'];
}
unlink($file); // Xóa cache nếu đã hết hạn
}
return null;
}
}
Hooks có thể được sử dụng để thực hiện một số hành động trước hoặc sau khi các sự kiện nhất định xảy ra. Ví dụ, bạn có thể sử dụng hooks trước khi một controller xử lý:
class BaseController {
protected function before() {
// Code chạy trước khi phương thức controller thực thi
}
protected function after() {
// Code chạy sau khi phương thức controller thực thi
}
public function __call($name, $arguments) {
$this->before();
call_user_func_array([$this, $name], $arguments);
$this->after();
}
}
Trong ví dụ này, các phương thức before
và after
sẽ tự động được gọi trước và sau khi controller thực thi các phương thức khác.
Trên đây là hướng dẫn chi tiết cách thiết kế một ứng dụng PHP theo mô hình MVC với các chức năng yêu cầu. Bạn có thể tùy chỉnh và mở rộng theo nhu cầu thực tế của dự án.