Giới thiệu
Hôm nay, tôi sẽ chỉ cho bạn cách tạo một ứng dụng PHP đơn giản theo mẫu MVC (Model-View-Controller). Tôi đã được truyền cảm hứng từ một khóa học PHP mà tôi đã dạy cách đây vài năm, và trong đó tôi đã xây dựng một trang web thương mại đơn giản với các học viên. Trang web thương mại này dựa trên một framework MVC đơn giản dựa trên PHP. Nhờ thế, những người đã tiếp tục với lập trình đã biết một chút về MVC trước khi bắt đầu làm việc với một framework thực sự.
Các framework MVC được sử dụng rộng rãi trong ngành công nghiệp vì chúng cung cấp nhiều lợi ích cho việc phát triển nhanh chóng và có cấu trúc. Có các framework MVC cho hầu hết các ngôn ngữ lập trình bạn có thể biết, từ DotNet đến PHP. Tuy nhiên, những framework đó có thể có một độ cong học cao. Điều này bởi vì mọi người cần phải học cách viết mã trong môi trường framework.
Chú ý cá nhân: vào năm 2010, tôi đã phát triển phần mềm trong hơn 5 năm và tìm kiếm một giải pháp tốt để xây dựng ứng dụng web cho sếp của tôi. Sau cuộc trao đổi với một đồng nghiệp cũ của tôi (cảm ơn Davide C.!), tôi bắt đầu sử dụng Symfony 1.4. Tôi đã sử dụng phương pháp "RTFM approach" (Read The Friendly Manual...) trước khi viết bất kỳ mã nào. Trong hai tháng, tôi đã hoàn thành một ứng dụng trung bình phức tạp (đăng ký, ACL, bảng điều khiển, giao diện người dùng, v.v.).
Sau đó, tôi đã làm việc với Zend Framework, Symfony 2.0 và 5, và Laravel (đang làm việc trên 5.8), cũng như các microframework như Silex (không còn được bảo trì) và Lumen. Không có nghi ngờ gì, framework ưa thích của tôi là Laravel. Mặc dù có một số "thứ kỳ diệu" có thể làm sợ mọi người, Laravel cung cấp nhiều tính năng sẵn có mà bạn có thể kích hoạt dễ dàng với các cấu hình phù hợp.
Ý nghĩa của MVC là gì?
MVC là một mẫu thiết kế được sử dụng để phân tách dữ liệu (Models), giao diện người dùng (Views) và logic ứng dụng (Controllers). Để có thể làm theo "Hướng dẫn này", bạn cần có kiến thức tốt về PHP và OOP (Lập trình Hướng đối tượng).
Xây dựng một framework PHP MVC đơn giản
Dù bạn sử dụng Docker, XAMPP, hoặc bất kỳ công cụ nào khác cho môi trường phát triển của bạn, hãy tạo một cấu trúc đơn giản cho framework PHP MVC đơn giản. Tôi thường có một thư mục gọi là "Solutions" cho tất cả các dự án của mình, sau đó truy cập vào thư mục của bạn, tạo một thư mục mới có tên "simple-php-mvc", sau đó truy cập vào thư mục đó. Hãy tạo các thư mục cơ bản cho MVC của bạn:
- app
- config
- public
- views
- routes
Bắt đầu một cách đơn giản, hãy tạo hai tệp quan trọng nhất của framework PHP MVC của chúng ta: index.php và htaccess.
Tệp cấu hình htaccess
Truy cập vào thư mục public và hãy tạo một tệp có tên là index.php.
Bây giờ, ở mức root của dự án của bạn, hãy tạo một tệp mới có tên là .htaccess, sau đó mở nó và đặt mã sau vào htaccess:
<IfModule mod_rewrite.c>
RewriteEngine On
# Stop processing if already in the /public directory
RewriteRule ^public/ - [L]
# Static resources if they exist
RewriteCond %{DOCUMENT_ROOT}/public/$1 -f
RewriteRule (.+) public/$1 [L]
# Route all other requests
RewriteRule (.*) public/index.php?route=$1 [L,QSA]
</IfModule>
htaccess là một tệp cấu hình cho máy chủ web Apache, và chỉ thị mod_rewrite cho biết cho Apache rằng mọi yêu cầu sẽ kết thúc tại index.php nằm trong thư mục có tên là public. Điều này có nghĩa là nếu bạn duyệt qua https://simple-php-mvc/page1, https://simple-php-mvc/page2 hoặc https://simple-php-mvc/page3, tất cả đều sẽ kết thúc ở index.php trong thư mục public, đó là điểm vào của framework PHP MVC của bạn. Điều này là một lợi thế lớn vì bạn có thể xử lý yêu cầu của mình ở một nơi duy nhất, hiểu được tài nguyên được yêu cầu và cung cấp phản hồi chính xác. Một điều khác: bằng cách sử dụng htaccess và định tuyến lưu lượng giao thông dưới thư mục public, phần còn lại của cấu trúc dự án của bạn sẽ bị ẩn không được công khai cho bất kỳ ai.
Đây là cách dự án của bạn trông như thế nào ngay bây giờ:
app
config
public
- index.php
views
routes
.htaccess
Khởi động framework PHP MVC
Bây giờ bạn cần một cách để khởi động ứng dụng của mình và tải mã bạn cần. Chúng ta đã nói rằng index.php ở trong thư mục public là điểm vào, vì vậy chúng ta sẽ bao gồm các tệp cần thiết từ đó.
Đầu tiên, chúng ta sẽ tải tệp cấu hình, dưới đây là nội dung của index.php:
// Load Config
require_once '../config/config.php';
Bây giờ chúng ta có thể tạo một tệp config.php trong thư mục config.
Trong tệp cấu hình, chúng ta có thể lưu trữ các thiết lập của framework, ví dụ: tên của ứng dụng của chúng ta, đường dẫn gốc và đường dẫn khối, cũng như các thông số kết nối cơ sở dữ liệu:
<?php
// Tên trang web
define('SITE_NAME', 'your-site-name');
// Đường dẫn gốc của ứng dụng
define('APP_ROOT', dirname(dirname(__FILE__)));
define('URL_ROOT', '/');
define('URL_SUBFOLDER', '');
// Thông số kết nối cơ sở dữ liệu
define('DB_HOST', 'your-host');
define('DB_USER', 'your-username');
define('DB_PASS', 'your-password');
define('DB_NAME', 'your-db-name');
Bộ tải tự động
Chúng ta muốn có khả năng tải các lớp tương lai một cách dễ dàng mà không có bất kỳ vấn đề nào (xem: hàng chục bao gồm hoặc yêu cầu), vì vậy chúng ta sẽ sử dụng bộ tải tự động PSR-4 với Composer.
Được báo cáo rằng một số dịch vụ lưu trữ yêu cầu chỉ thị classmap: autoload classmap sẽ đi qua các tệp .php và .inc trong các thư mục và tệp đã được chỉ định và tìm kiếm các lớp trong chúng.
Composer là một trình quản lý phụ thuộc cho PHP, cho phép bạn khai báo các thư viện mà dự án của bạn phụ thuộc vào và nó sẽ quản lý chúng cho bạn. Rất hữu ích!
Trước tiên, ở mức root, bạn phải tạo một tệp có tên là composer.json và thêm nội dung sau vào tệp đó:
{
"name": "gmaccario/simple-mvc-php-framework",
"description": "Simple MVC PHP framework: a demonstration of how to create a simple MVC framework in PHP",
"autoload": {
"psr-4": {
"App\\": "app/"
},
"classmap": [
"app/"
]
}
}
Sau đó, giả sử rằng bạn đã cài đặt Composer trên máy tính của bạn - hoặc container, thực thi lệnh sau tại mức root của dự án của bạn, tùy thuộc vào nơi bạn làm việc và nơi Composer của bạn được cài đặt:
composer install
Nếu bạn kiểm tra thư mục gốc của mình bây giờ, bạn có thể thấy một thư mục mới gọi là vendor chứa tệp autoload.php và thư mục composer. Mở file index.php trong thư mục public và đơn giản thêm mã sau vào đầu tệp:
require_once '../vendor/autoload.php';
Từ bây giờ, bạn có thể sử dụng App làm điểm bắt đầu của các không gian tên của bạn, như sau:
use App\Controllers\MyController;
Bây giờ, hãy tìm hiểu ý nghĩa của viết tắt MVC.
Model
Model là một đối tượng đại diện cho dữ liệu của bạn. Model sẽ được xây dựng dựa trên cấu trúc bảng cơ sở dữ liệu của bạn và nó sẽ tương tác với các hoạt động cơ sở dữ liệu (create, read, update và delete). Ví dụ, nếu bạn có một bảng Sản phẩm như sau:
CREATE TABLE IF NOT EXISTS products (
id int(10) NOT NULL auto_increment,
title varchar(255) collate utf8_unicode_ci NOT NULL,
description text collate utf8_unicode_ci,
price decimal(12,5) NOT NULL,
sku varchar(255) collate utf8_unicode_ci NOT NULL,
image varchar(255) collate utf8_unicode_ci NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1;
Đầu tiên hết, hãy tạo một thư mục mới gọi là Models trong thư mục app. Sau đó, hãy tạo một tệp mới có tên Product trong Models. Model Product của bạn sẽ như sau:
<?php
namespace App\Models;
class Product {
protected $id;
protected $title;
protected $description;
protected $price;
protected $sku;
protected $image;
// CÁC PHƯƠNG THỨC GET
public function getId() {
return $this->id;
}
public function getTitle() {
return $this->title;
}
public function getDescription() {
return $this->description;
}
public function getPrice() {
return $this->price;
}
public function getSku() {
return $this->sku;
}
public function getImage() {
return $this->image;
}
// CÁC PHƯƠNG THỨC SET
public function setTitle(string $title) {
$this->title = $title;
}
public function setDescription(string $description) {
$this->description = $description;
}
public function setPrice(string $price) {
$this->price = $price;
}
public function setSku(string $sku) {
$this->sku = $sku;
}
public function setImage(string $image) {
$this->image = $image;
}
// CÁC HOẠT ĐỘNG CRUD
public function create(array $data) {
// Hàm tạo
}
public function read(int $id) {
// Hàm đọc
}
public function update(int $id, array $data) {
// Hàm cập nhật
}
public function delete(int $id) {
// Hàm xóa
}
}
Và đó là tất cả. Với các phương thức này, bạn sẽ tạo ra các đối tượng mà bạn cần điền các giá trị thực tế dựa trên model.
View
View là trách nhiệm của nó để lấy dữ liệu từ controller và hiển thị các giá trị đó. Đó là tất cả. Có rất nhiều công cụ mẫu cho PHP, từ Twig đến Blade. Cho hướng dẫn MVC PHP này, chúng ta sẽ chỉ sử dụng HTML thuần túy để đơn giản hóa mọi thứ.
Để tạo một view mới, chúng ta phải tạo một tệp mới có tên là product.php trong thư mục views. Dựa trên các thuộc tính của sản phẩm, chúng ta có thể viết một HTML đơn giản như sau:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="favicon.png">
<title>Simple PHP MVC</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<section>
<h1>My Product:</h1>
<ul>
<li><?php echo $product->getTitle(); ?></li>
<li><?php echo $product->getDescription(); ?></li>
<li><?php echo $product->getPrice(); ?></li>
<li><?php echo $product->getSku(); ?></li>
<li><?php echo $product->getImage(); ?></li>
</ul>
<a href="<?php echo $routes->get('homepage')->getPath(); ?>">Back to homepage</a>
<section>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
View đã sẵn sàng để lấy đối tượng sản phẩm ($product) và hiển thị giá trị của nó.
Controller
Controller là trái tim của logic ứng dụng. Nó chịu trách nhiệm chấp nhận đầu vào và chuyển đổi nó thành các lệnh cho model hoặc view.
Hãy tạo một thư mục mới gọi là Controllers trong thư mục app, và tạo một tệp điều khiển mới có tên ProductController.php. Đây là nội dung của nó:
<?php
namespace App\Controllers;
use App\Models\Product;
use Symfony\Component\Routing\RouteCollection;
class ProductController {
// Hiển thị các thuộc tính của sản phẩm dựa trên id.
public function showAction(int $id, RouteCollection $routes) {
$product = new Product();
$product->read($id);
require_once APP_ROOT . '/views/product.php';
}
}
Rất đơn giản, phải không? Tất nhiên, mọi thứ có thể phức tạp hơn, chúng ta có thể tạo một lớp Controller cha, một phương thức view và các hàm trợ giúp khác. Nhưng với hiện tại, nó là đủ.
Hệ thống định tuyến
Bây giờ chúng ta cần một cơ chế để xử lý các URL. Chúng ta muốn sử dụng một URL thân thiện; nói cách khác, chúng ta muốn xử lý các địa chỉ web dễ đọc và bao gồm các từ mô tả nội dung của trang web. Chúng ta cần một hệ thống định tuyến.
Chúng ta có thể tạo ra hệ thống định tuyến riêng của mình, hoặc vì chúng ta đã sử dụng composer cho autoload, chúng ta có thể khai thác các gói phần mềm toàn diện Symfony và làm việc một cách thông minh! Vì vậy, hãy xem cách chúng ta có thể tận dụng các thành phần Định tuyến của Symfony. Dưới đây là tài liệu: https://symfony.com/doc/current/create_framework/routing.html
Trước hết, hãy cài đặt thành phần:
composer require symfony/routing
Nếu bạn kiểm tra vào thư mục vendor bây giờ, bạn có thể thấy rằng một thư mục mới được tạo có tên Symfony.
Hãy bắt đầu triển khai hệ thống định tuyến cho framework MVC của chúng ta. Mục tiêu là hiển thị các giá trị của sản phẩm với ID = 1 khi duyệt qua URL /product/1
Hãy tạo một tệp mới có tên là web.php trong thư mục routes. Tệp này sẽ chứa tất cả các định tuyến của ứng dụng của bạn.
<?php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
// Hệ thống định tuyến
$routes = new RouteCollection();
$routes->add('product', new Route(constant('URL_SUBFOLDER') . '/product/{id}', array('controller' => 'ProductController', 'method'=>'showAction'), array('id' => '[0-9]+')));
Chúng ta sử dụng lớp Route và RouteCollection từ thành phần Định tuyến Symfony để tạo và liệt kê tất cả các định tuyến chúng ta cần. Chúng ta bắt đầu với một trang sản phẩm đơn lẻ.
Chưa đủ: chúng ta cần phải cài đặt gói phần mềm này:
composer require symfony/http-foundation
Đây là một số giải thích từ Symfony: Trong PHP, yêu cầu được biểu diễn bằng một số biến toàn cục ($_GET, $_POST, $_FILES, $_COOKIE, $_SESSION, ...) và phản hồi được tạo ra bằng một số hàm (echo, header (), setcookie (), ...).
Thanh phần HttpFoundation của Symfony thay thế các biến toàn cục và chức năng mặc định của PHP này bằng một lớp hướng đối tượng.
OK rồi, hãy tạo bộ định tuyến. Thêm một tệp mới có tên là Router.php vào thư mục gốc của bạn và đặt mã sau đây vào nó:
<?php
namespace App;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
class Router {
public function __invoke(RouteCollection $routes) {
$context = new RequestContext();
$request = Request::createFromGlobals();
$context->fromRequest(Request::createFromGlobals());
// Định tuyến có thể khớp với các định tuyến với các yêu cầu đến
$matcher = new UrlMatcher($routes, $context);
try {
$arrayUri = explode('?', $_SERVER['REQUEST_URI']);
$matcher = $matcher->match($arrayUri[0]);
// Cast params to int if numeric
array_walk($matcher, function(&$param) {
if(is_numeric($param)) {
$param = (int) $param;
}
});
// Fix Non-static method ... should not be called statically
$className = '\App\Controllers\\' . $matcher['controller'];
$classInstance = new $className();
// Add routes as paramaters to the next class
$params = array_merge(array_slice($matcher, 2, -1), array('routes' => $routes));
call_user_func_array(array($classInstance, $matcher['method']), $params);
} catch (MethodNotAllowedException $e) {
echo 'Route method is not allowed.';
} catch (ResourceNotFoundException $e) {
echo 'Route does not exists.';
} catch (NoConfigurationException $e) {
echo 'Configuration does not exists.';
}
}
}
// Gọi Router từ một instance
$router = new Router();
$router($routes);
Mã khá đơn giản và nói lên tất cả, nhưng hãy giải thích một chút: trình phân giải URL lấy URI yêu cầu và kiểm tra xem có phù hợp với các định tuyến được xác định trong routes/web.php hay không. Nếu phù hợp, hàm call_user_func_array sẽ gọi đúng phương thức của đúng controller.
Hơn nữa, chúng ta đã sử dụng hàm array_walk để chuyển các giá trị số thành dạng số nguyên, vì trong các phương thức lớp của chúng ta, chúng ta sử dụng yêu cầu khai báo kiểu rõ ràng.
Trang chủ
Hãy chuẩn bị đường dẫn trang chủ. Mở routes/web.php và thêm đường dẫn mới:
$routes->add('homepage', new Route(constant('URL_SUBFOLDER') . '/', array('controller' => 'PageController', 'method'=>'indexAction'), array()));
Rõ ràng, chúng ta cần tạo một controller mới PageController:
<?php
namespace App\Controllers;
use App\Models\Product;
use Symfony\Component\Routing\RouteCollection;
class PageController {
// Hành động trang chủ
public function indexAction(RouteCollection $routes) {
$routeToProduct = str_replace('{id}', 1, $routes->get('product')->getPath());
require_once APP_ROOT . '/views/home.php';
}
}
Và view mới:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="favicon.png">
<title>Simple PHP MVC</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<section>
<h1>Homepage</h1>
<p>
<a href="<?php echo $routeToProduct; ?>">Check the first product</a>
</p>
<section>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
Vì chúng ta sử dụng cùng một mã cho header và footer, chúng ta có thể tạo một thư mục layout và phân tách mã cho những phần HTML đó.
Để điều hướng trang, mở trình duyệt của bạn và duyệt qua URL này:
URL chính xác phụ thuộc vào các cài đặt của bạn. Tôi sử dụng Docker cho môi trường local của tôi và tôi thường cài đặt cổng, sau đó URL của tôi có thể hơi khác so với của bạn, ví dụ: tôi sử dụng http://localhost:8887/
Lưu ý: Nếu bạn cài đặt dự án của mình vào một thư mục con như nhiều người dùng dường như thực hiện, bạn phải thiết lập hằng số URL_SUBFOLDER trong tệp cấu hình. Ví dụ, nếu bạn đã cài đặt dự án này trong một thư mục con được gọi là simple-mvc-php-framework, URL cuối cùng của bạn sẽ là:
http://localhost/simple-mvc-php-framework/
Cải thiện framework PHP MVC
Bạn có thể dễ dàng thêm kết nối cơ sở dữ liệu và ORM, session, cookies, trình điều khiển trang tốt hơn chấp nhận các thông số trang khác nhau hoặc bất kỳ tính năng nào khác, nhưng bài viết này chỉ muốn cho thấy cách xây dựng một framework PHP MVC đơn giản thực sự.
Hãy tải mã xuống tại đây: https://github.com/gmaccario/simple-mvc-php-framework
Kết luận
Khi bạn đã quen với mô hình MVC, tôi đề nghị bạn đọc tài liệu của Laravel (framework PHP MVC ưa thích của tôi) hoặc Symfony và bắt đầu thực hành! Bạn sẽ nhận thấy rằng quá trình phát triển trở nên nhanh hơn so với việc sử dụng giải pháp PHP thuần.
Cảm ơn bạn đã dành thời gian, tôi hy vọng bạn đã nhận được một số thông tin mới về mô hình MVC. Hãy tự do fork dự án trên GitHub và nếu bạn có bất kỳ vấn đề nào, hãy kiểm tra tab vấn đề hoặc mở một vấn đề mới (nếu đó là vấn đề thực sự mới!).
Anyway, bạn cũng có thể gửi cho tôi một tin nhắn nhưng tôi thích trả lời trên Github để chia sẻ kiến thức với mọi người khác. Tôi quyết định đóng phần bình luận trên trang này vì lượng spam rất lớn và số câu hỏi trùng lặp rất nhiều. Tôi hy vọng bạn có thể hiểu!
Lưu ý: dự án này là một bộ khởi động đơn giản và hoạt động tốt nếu môi trường của bạn đã được cấu hình đúng cách. Tôi không sử dụng XAMPP hoặc bất kỳ gói chồng đáp ứng nào khác. Trước đây, tôi đã tạo ra LAMP của mình bằng cách cài đặt phần mềm một cách riêng lẻ, bây giờ tôi đang sử dụng Docker. Vì vậy, nếu bạn gặp sự cố nào đó với môi trường của mình, bạn phải giải quyết vấn đề của mình.
Và một lần nữa: đây là một bộ khởi động đơn giản với mục tiêu giúp mọi người hiểu một số khái niệm đằng sau mô hình MVC. Nếu bạn muốn tạo một dự án chuyên nghiệp, hãy chọn Laravel hoặc Symfony.