EzCode # Bài 6 — Lập trình hướng đối tượng
Học xong bài này bạn sẽ:
- Viết được
classvới thuộc tính, phương thức và hàm khởi tạo- Hiểu
$this,private,publicvàinclude_once- Đọc hiểu trọn vẹn một file Model của EzCode
1. Vấn đề đặt ra
EzCode có sáu file trong thư mục Models/: User.php, Courses.php, Category.php, Lession.php, Enrollment.php, VNPay.php.
Cả sáu đều cần làm cùng một việc: nói chuyện với cơ sở dữ liệu. Mỗi file đều phải mở kết nối, gửi câu truy vấn, nhận kết quả về.
Nếu viết bằng hàm thuần, bạn sẽ có sáu bản sao của cùng một đoạn code kết nối. Đổi mật khẩu database là phải sửa sáu chỗ. Sót một chỗ là lỗi.
Vấn đề thứ hai: mỗi Model cần giữ kết nối của riêng nó trong suốt thời gian làm việc. Hàm thuần không giữ được gì — chạy xong là quên hết, đúng như bạn đã thấy ở Bài 5.
Lập trình hướng đối tượng giải quyết cả hai. Nó cho phép bạn gói dữ liệu và các hàm xử lý dữ liệu đó vào chung một chỗ, rồi tạo ra bao nhiêu bản sao cũng được.
2. Kiến thức mới
2.1. Class và object
Class là khuôn. Object là sản phẩm đúc ra từ khuôn.
Hãy nghĩ tới khuôn làm bánh trung thu. Cái khuôn có hình dạng, có hoa văn — nhưng bạn không ăn được cái khuôn. Bạn dùng nó để đúc ra nhiều chiếc bánh. Mỗi chiếc bánh có cùng hình dạng nhưng nhân khác nhau: cái nhân đậu xanh, cái nhân thập cẩm.
Trong lập trình:
- Class
Categorymô tả "một Model quản lý danh mục thì có gì và làm được gì". - Object là một
Categorycụ thể đang tồn tại trong bộ nhớ, đang giữ một kết nối database thật.
<?php
// Định nghĩa khuôn
class KhoaHoc
{
public $ten;
public $gia;
}
// Đúc ra hai sản phẩm
$khoa1 = new KhoaHoc();
$khoa1->ten = "HTML CSS cơ bản";
$khoa1->gia = 300000;
$khoa2 = new KhoaHoc();
$khoa2->ten = "Flutter cho người mới";
$khoa2->gia = 500000;
echo $khoa1->ten; // HTML CSS cơ bản
echo $khoa2->ten; // Flutter cho người mới
?>
Hai điểm cú pháp cần nhớ:
newtạo object từ class.->truy cập vào bên trong object. Đọc là "của".$khoa1->tenlà "ten của khoa1".
Cảnh báo cú pháp: sau dấu
->không có dấu$. Viết$khoa1->$tenlà sai hoàn toàn — nó có nghĩa khác hẳn. Đây là lỗi phổ biến nhất khi mới học.
2.2. Thuộc tính và phương thức
- Thuộc tính (property) là biến nằm trong class. Nó giữ dữ liệu.
- Phương thức (method) là hàm nằm trong class. Nó xử lý dữ liệu.
<?php
class KhoaHoc
{
public $ten; // thuộc tính
public $gia; // thuộc tính
function hienThi() // phương thức
{
return $this->ten . " — " . number_format($this->gia) . "đ";
}
}
$khoa = new KhoaHoc();
$khoa->ten = "Python cơ bản";
$khoa->gia = 300000;
echo $khoa->hienThi(); // Python cơ bản — 300,000đ
?>
2.3. $this — chính object này
Bên trong một phương thức, $this trỏ tới chính object đang gọi phương thức đó.
Điều này cần thiết vì một class có thể có hàng trăm object. Khi bạn gọi $khoa1->hienThi(), phương thức phải biết nó đang làm việc với $khoa1 chứ không phải $khoa2. $this chính là câu trả lời.
<?php
$khoa1 = new KhoaHoc();
$khoa1->ten = "Python";
$khoa2 = new KhoaHoc();
$khoa2->ten = "Java";
echo $khoa1->hienThi(); // bên trong, $this là $khoa1 -> in "Python"
echo $khoa2->hienThi(); // bên trong, $this là $khoa2 -> in "Java"
?>
Quy tắc: trong class, muốn dùng thuộc tính hay phương thức của chính mình thì luôn viết $this->. Viết $ten trần là PHP hiểu thành một biến cục bộ hoàn toàn khác.
2.4. public và private
Mỗi thuộc tính và phương thức có một phạm vi truy cập:
| Từ khóa | Nghĩa |
|---|---|
public |
Ai cũng dùng được, cả từ bên ngoài class |
private |
Chỉ dùng được bên trong class |
<?php
class Category
{
private $db; // chỉ bên trong class dùng được
public function getAll() // ai cũng gọi được
{
return $this->db->query("SELECT * FROM categories");
}
}
$model = new Category();
$model->getAll(); // Được — getAll() là public
$model->db; // LỖI — db là private
?>
Cố truy cập thuộc tính private từ bên ngoài sẽ nhận lỗi:
Fatal error: Uncaught Error: Cannot access private property Category::$db
Vì sao phải để $db là private?
Vì kết nối database là chuyện nội bộ của Model. Người dùng Model chỉ cần biết "gọi getAll() thì nhận được danh sách danh mục". Họ không cần và không nên chạm vào kết nối.
Nếu để public, ai đó có thể viết $model->db = null; từ bên ngoài và làm hỏng Model. Hoặc tệ hơn, họ viết truy vấn SQL trực tiếp qua $model->db, bỏ qua toàn bộ logic bạn đã đặt trong Model.
Nguyên tắc: để private mặc định, chỉ mở public những gì bên ngoài thực sự cần. Cả sáu Model của EzCode đều khai báo private $db;.
2.5. __construct() — hàm khởi tạo
Đây là phương thức đặc biệt, chạy tự động ngay khi bạn new.
<?php
class Category
{
private $db;
function __construct()
{
$this->db = new Database();
echo "Đã tạo kết nối!";
}
}
$model = new Category(); // in ra: Đã tạo kết nối!
?>
Bạn không bao giờ gọi __construct() bằng tay. new gọi nó hộ bạn.
Tên có hai dấu gạch dưới ở đầu. PHP quy định như vậy để phân biệt các phương thức đặc biệt (gọi là "magic method") với phương thức thường.
Constructor có tham số:
<?php
class KhoaHoc
{
private $ten;
private $gia;
function __construct($ten, $gia)
{
$this->ten = $ten;
$this->gia = $gia;
}
function hienThi()
{
return $this->ten . " — " . number_format($this->gia) . "đ";
}
}
$khoa = new KhoaHoc("Python cơ bản", 300000);
echo $khoa->hienThi();
?>
Cách này gọn hơn hẳn: tạo object và gán dữ liệu chỉ trong một dòng. Và vì $ten, $gia là private, không ai có thể sửa chúng sau khi đã tạo.
Hãy để ý dòng $this->ten = $ten; — bên trái là thuộc tính của object, bên phải là tham số truyền vào. Chúng cùng tên nhưng là hai thứ khác nhau, $this-> phân biệt chúng.
2.6. __destruct() — hàm hủy
Ngược lại với constructor, __destruct() chạy tự động khi object bị hủy — thường là khi trang chạy xong.
<?php
class Database
{
private $conn;
function __construct()
{
$this->conn = new PDO(/* ... */);
}
function __destruct()
{
$this->conn = null; // đóng kết nối
}
}
?>
Dùng để dọn dẹp: đóng kết nối, đóng file, giải phóng tài nguyên. Models/Database.php của EzCode có đúng phương thức này, và Bài 8 sẽ viết nó.
2.7. include_once — nạp file chứa class
Class Category cần dùng class Database. Nhưng hai class nằm ở hai file khác nhau. PHP phải được bảo cho biết Database ở đâu.
<?php
include_once "./Models/Database.php";
class Category
{
// giờ mới dùng được new Database()
}
?>
PHP có bốn lệnh nạp file:
| Lệnh | Nạp lại nếu đã nạp | Thiếu file thì |
|---|---|---|
include |
Có | Cảnh báo, chạy tiếp |
include_once |
Không | Cảnh báo, chạy tiếp |
require |
Có | Lỗi nghiêm trọng, dừng hẳn |
require_once |
Không | Lỗi nghiêm trọng, dừng hẳn |
Vì sao class phải dùng bản _once?
Vì PHP không cho định nghĩa một class hai lần. Thử tưởng tượng:
Controllers/CourseController.phpnạpModels/Courses.php, file này nạpModels/Database.php.- Cùng controller đó nạp tiếp
Models/User.php, file này cũng nạpModels/Database.php.
Nếu dùng include thường, lần thứ hai PHP gặp lại class Database và báo:
Fatal error: Cannot declare class Database, because the name is already in use
include_once ghi nhớ những file đã nạp và bỏ qua lần sau. Đây là lý do cả sáu Model của EzCode đều mở đầu bằng include_once.
Về đường dẫn "./Models/Database.php": dấu chấm đầu nghĩa là "thư mục hiện tại". Nhưng "hiện tại" ở đây là thư mục của file đang chạy chính, tức index.php ở gốc project — không phải thư mục chứa Category.php. Đây là lý do mọi đường dẫn include trong EzCode đều viết từ gốc project.
2.8. Gọi phương thức bằng tên nằm trong biến
Đây là kỹ thuật nâng cao, học trước để chuẩn bị cho Bài 9.
Bình thường bạn gọi phương thức bằng tên viết thẳng:
$model->getAll();
Nhưng PHP cho phép đặt tên phương thức vào một biến rồi gọi qua biến đó:
<?php
$tenPhuongThuc = "getAll";
$model->$tenPhuongThuc(); // tương đương $model->getAll();
?>
Tương tự, bạn tạo được object từ tên class nằm trong chuỗi:
<?php
$tenClass = "Category";
$model = new $tenClass(); // tương đương new Category();
?>
Nghe có vẻ vô dụng, nhưng chính kỹ thuật này khiến index.php của EzCode chỉ dài 31 dòng mà điều phối được toàn bộ 27 chức năng của website. Bài 9 sẽ cho bạn thấy.
3. Áp dụng vào EzCode
Mở Models/Category.php. Đây là file ngắn nhất trong toàn bộ project — chỉ 16 dòng — và nó chứa đúng khuôn mẫu chung của cả sáu Model.
📁
Models/Category.php— đọc trước
<?php
include_once "./Models/Database.php";
class Category
{
private $db;
function __construct()
{
$this->db = new Database();
}
function getAll()
{
$sql = "SELECT * FROM categories";
return $this->db->query($sql);
}
}
Ta đọc từng phần.
Dòng 2 — include_once "./Models/Database.php";
Nạp file chứa class Database. Bắt buộc phải có, vì dòng 8 sắp dùng new Database(). Dùng bản _once vì lý do ở mục 2.7.
Dòng 5 — private $db;
Khai báo thuộc tính giữ kết nối database. private nên chỉ code bên trong class Category chạm được.
Dòng 6 đến 9 — hàm khởi tạo
function __construct()
{
$this->db = new Database();
}
Ngay khi ai đó viết new Category(), dòng này chạy và tạo một kết nối database mới. Từ lúc đó trở đi, mọi phương thức của object đều dùng chung $this->db.
Dòng 11 đến 15 — phương thức getAll()
function getAll()
{
$sql = "SELECT * FROM categories";
return $this->db->query($sql);
}
Ba việc: viết câu SQL vào biến, gọi query() của object $db, trả kết quả về.
Chú ý $this->db->query($sql) có hai dấu -> nối tiếp. Đọc từ trái sang: lấy db của object này, rồi gọi query() của cái db đó. Đây là chuỗi gọi lồng nhau, bạn sẽ gặp liên tục trong mọi Model.
Kết quả trả về là một mảng lồng nhau đúng như bạn học ở Bài 3: 33 danh mục, mỗi danh mục là mảng kết hợp có hai khóa id và name.
Cách dùng class này từ bên ngoài — trích từ Controllers/CourseController.php:
include_once "./Models/Category.php";
$categoryModel = new Category();
$categoriesList = $categoryModel->getAll();
Ba dòng: nạp file, tạo object, gọi phương thức. Cùng một khuôn mẫu này lặp lại ở cả 27 phương thức Controller của EzCode.
Khuôn mẫu chung của sáu Model. Hãy nhớ hình dạng sau, vì bạn sẽ gõ lại nó năm lần nữa:
<?php
include_once "./Models/Database.php";
class TenModel
{
private $db;
function __construct()
{
$this->db = new Database();
}
// các phương thức truy vấn ở đây
}
Điểm khác biệt duy nhất giữa các Model là danh sách phương thức bên trong. Category có 1 phương thức, Lession có 6, User có 10, Courses có 13.
⚠️ Lưu ý: Mỗi Model tự tạo một
new Database()riêng trong constructor của mình.Hậu quả: một trang như danh sách khóa học dùng ba Model là
Courses,User,Category, nên nó mở ba kết nối MySQL riêng biệt cho cùng một lần tải trang. Trang chi tiết khóa học dùng hai Model, mở hai kết nối.Với vài người dùng thì không sao. Nhưng MySQL có giới hạn số kết nối đồng thời (mặc định của XAMPP là 151), nên cách làm này lãng phí và không mở rộng được.
Đây là lỗi số 16 trong Bài 22, và ở đó bạn sẽ học mẫu Singleton để cả website dùng chung đúng một kết nối. Bây giờ cứ gõ y hệt để khớp với repo.
4. Chạy thử
Tạo hoc/bai06.php:
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
class Calculator
{
private $a;
private $b;
function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
echo "<p>Đã tạo Calculator với $a và $b</p>";
}
function cong()
{
return $this->a + $this->b;
}
function tru()
{
return $this->a - $this->b;
}
function nhan()
{
return $this->a * $this->b;
}
function __destruct()
{
echo "<p><i>Calculator đã bị hủy</i></p>";
}
}
$mayTinh1 = new Calculator(10, 3);
echo "<p>10 + 3 = " . $mayTinh1->cong() . "</p>";
echo "<p>10 - 3 = " . $mayTinh1->tru() . "</p>";
echo "<p>10 x 3 = " . $mayTinh1->nhan() . "</p>";
echo "<hr>";
$mayTinh2 = new Calculator(100, 25);
echo "<p>100 + 25 = " . $mayTinh2->cong() . "</p>";
echo "<hr><p>Hết phần chính. Bên dưới là lúc PHP hủy các object:</p>";
?>
Mở http://localhost/EZCode_khoahoc/hoc/bai06.php.
Kết quả đúng:
Đã tạo Calculator với 10 và 3
10 + 3 = 13
10 - 3 = 7
10 x 3 = 30
─────────────
Đã tạo Calculator với 100 và 25
100 + 25 = 125
─────────────
Hết phần chính. Bên dưới là lúc PHP hủy các object:
Calculator đã bị hủy
Calculator đã bị hủy
Ba điều đáng quan sát:
- Dòng "Đã tạo" xuất hiện ngay khi
newchạy — đó là__construct()tự động. - Hai object hoàn toàn độc lập.
$mayTinh1giữ 10 và 3,$mayTinh2giữ 100 và 25. Cùng một class nhưng dữ liệu riêng. - Hai dòng "đã bị hủy" nằm ở cuối cùng —
__destruct()chỉ chạy khi trang sắp kết thúc.
Bây giờ thử thêm dòng này vào cuối file rồi tải lại:
echo $mayTinh1->a;
Bạn sẽ nhận:
Fatal error: Uncaught Error: Cannot access private property Calculator::$a
Đó là private đang làm việc.
5. Bài tập
Bài tập 1
Tạo hoc/bai06_bt1.php với class KhoaHoc gồm:
- Hai thuộc tính
private:$titlevà$price - Constructor nhận hai giá trị và gán vào hai thuộc tính đó
- Phương thức
hienThi()trả về chuỗi dạngTên khóa học — 300.000đ, giá đã định dạng kiểu Việt Nam
Tạo ba object với ba khóa học khác nhau và in cả ba ra.
Bài tập 2
Thêm vào class trên phương thức giamGia($phanTram) trả về giá sau khi giảm.
Yêu cầu quan trọng: phương thức này không được thay đổi giá gốc. Sau khi gọi giamGia(20), gọi lại hienThi() vẫn phải ra giá cũ.
Kiểm chứng bằng cách in theo thứ tự: giá gốc, giá sau giảm 20%, rồi lại giá gốc.
Bài tập 3
Tách class KhoaHoc ra file riêng hoc/KhoaHoc.php, rồi tạo hoc/bai06_bt3.php nạp nó vào bằng include_once.
Sau khi chạy được, làm ba thí nghiệm:
- Viết
include_once "KhoaHoc.php";hai lần liên tiếp. Quan sát kết quả. - Đổi cả hai thành
include "KhoaHoc.php";. Quan sát kết quả và chép lại thông báo lỗi. - Đổi tên file thành
KhoaHocSai.phptrong lệnh include, một lần dùngincludevà một lần dùngrequire. So sánh hai thông báo và cho biết khác nhau ở chỗ nào.
6. Đáp án
Đáp án bài tập 1 và 2
📄 hoc/bai06_bt1.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
class KhoaHoc
{
private $title;
private $price;
function __construct($title, $price)
{
$this->title = $title;
$this->price = $price;
}
function hienThi()
{
return $this->title . " — " . number_format($this->price, 0, ',', '.') . "đ";
}
function giamGia($phanTram)
{
$soTienGiam = $this->price * $phanTram / 100;
return $this->price - $soTienGiam;
}
}
$khoa1 = new KhoaHoc("Khóa học HTML CSS cơ bản", 300000);
$khoa2 = new KhoaHoc("Flutter cho người mới bắt đầu", 500000);
$khoa3 = new KhoaHoc("Python cơ bản", 300000);
echo "<h3>Danh sách khóa học</h3>";
echo "<p>" . $khoa1->hienThi() . "</p>";
echo "<p>" . $khoa2->hienThi() . "</p>";
echo "<p>" . $khoa3->hienThi() . "</p>";
echo "<h3>Thử giảm giá khóa 2</h3>";
echo "<p>Giá gốc: " . $khoa2->hienThi() . "</p>";
echo "<p>Giảm 20% còn: " . number_format($khoa2->giamGia(20), 0, ',', '.') . "đ</p>";
echo "<p>Giá gốc vẫn là: " . $khoa2->hienThi() . "</p>";
?>
Vì sao giá gốc không đổi? Vì giamGia() dùng return để trả về một giá trị mới, chứ không viết $this->price = .... Nó tính toán chứ không thay đổi.
Nếu bạn viết nhầm thành:
function giamGia($phanTram)
{
$this->price = $this->price - ($this->price * $phanTram / 100);
return $this->price;
}
thì gọi giamGia(20) hai lần liên tiếp sẽ giảm giá hai lần: 500.000 xuống 400.000 rồi xuống 320.000. Đây là một loại lỗi rất khó tìm vì nó chỉ xuất hiện khi phương thức được gọi nhiều lần.
Quy tắc rút ra: phương thức chỉ nên sửa thuộc tính khi tên của nó nói rõ điều đó, ví dụ datGia() hay capNhatGia(). Phương thức tên giamGia(), tinh...(), lay...() thì chỉ nên trả về giá trị.
Đáp án bài tập 3
📄 hoc/KhoaHoc.php
<?php
class KhoaHoc
{
private $title;
private $price;
function __construct($title, $price)
{
$this->title = $title;
$this->price = $price;
}
function hienThi()
{
return $this->title . " — " . number_format($this->price, 0, ',', '.') . "đ";
}
function giamGia($phanTram)
{
$soTienGiam = $this->price * $phanTram / 100;
return $this->price - $soTienGiam;
}
}
Chú ý file này không có thẻ đóng ?>. Đây là quy ước cho các file chỉ chứa PHP, và lý do đã được giải thích ở Bài 4 mục 2.9: tránh ký tự thừa lọt ra sau thẻ đóng gây lỗi "headers already sent". Hãy mở Models/Category.php của EzCode và kiểm chứng — nó cũng kết thúc bằng } chứ không có ?>.
📄 hoc/bai06_bt3.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
include_once "KhoaHoc.php";
$khoa = new KhoaHoc("Khóa học nạp từ file riêng", 450000);
echo "<p>" . $khoa->hienThi() . "</p>";
?>
Thí nghiệm 1 — include_once hai lần:
include_once "KhoaHoc.php";
include_once "KhoaHoc.php";
Kết quả: chạy bình thường, không lỗi, không cảnh báo. Lần thứ hai bị PHP bỏ qua vì file đã nạp rồi.
Thí nghiệm 2 — include hai lần:
include "KhoaHoc.php";
include "KhoaHoc.php";
Kết quả:
Fatal error: Cannot declare class KhoaHoc, because the name is already in use in C:\xampp\htdocs\EZCode_khoahoc\hoc\KhoaHoc.php on line 2
PHP nạp file hai lần, gặp class KhoaHoc hai lần, và không cho phép trùng tên class.
Đây chính xác là lý do sáu Model của EzCode dùng include_once. Trang danh sách khóa học nạp cả Courses.php, User.php và Category.php; cả ba đều nạp Database.php. Không có _once là trang chết ngay.
Thí nghiệm 3 — file không tồn tại:
Với include "KhoaHocSai.php";:
Warning: include(KhoaHocSai.php): Failed to open stream: No such file or directory in ... on line 5
Warning: include(): Failed opening 'KhoaHocSai.php' for inclusion ... in ... on line 5
Fatal error: Uncaught Error: Class "KhoaHoc" not found in ... on line 7
Với require "KhoaHocSai.php";:
Warning: require(KhoaHocSai.php): Failed to open stream: No such file or directory in ... on line 5
Fatal error: Uncaught Error: Failed opening required 'KhoaHocSai.php' in ... on line 5
Khác nhau ở chỗ nào:
includechỉ cảnh báo rồi chạy tiếp. Chương trình chạy tới dòng 7, gặpnew KhoaHoc()mà class không tồn tại, mới chết ở đó.requiredừng ngay tại dòng 5. Nó coi file đó là bắt buộc, thiếu là không đi tiếp được.
Nên dùng cái nào? Với file chứa class thì require_once an toàn hơn: lỗi hiện ra đúng chỗ thiếu file, dễ tìm hơn nhiều so với việc mò xem tại sao class không tồn tại.
EzCode dùng include_once ở mọi nơi. Đây không phải lỗi, chỉ là lựa chọn kém an toàn hơn một chút. Bạn cứ gõ theo repo, và ghi nhớ điều này cho project của riêng bạn sau này.
🎉 Bạn vừa hoàn thành Phần I. Từ Bài 7 trở đi, bạn bắt đầu gõ vào đúng cấu trúc thư mục của project thật.
➡️ Bài tiếp theo: Bài 7 — Thiết kế cơ sở dữ liệu EzCode
⬅️ Về mục lục · Bài trước
All rights reserved