
PHP EP 20: Transaction PDO, khóa ngoại và Repository trong PHP
Quản lý giao dịch cơ sở dữ liệu bằng beginTransaction, commit và rollBack trong PHP PDO, thực hành bật ràng buộc khóa ngoại và đóng gói truy vấn vào Repository.

Thiết kế kiến trúc hướng đối tượng bằng Interface, Abstract Class, Trait và Enum trong PHP, thực hành kiểm tra hợp đồng kiểu dữ liệu và xử lý tryFrom an toàn.
Khi mới học lập trình hướng đối tượng, nhiều người thường gom mọi đoạn mã dùng chung vào một lớp cha khổng lồ (BaseModel, BaseService) rồi cho hàng chục lớp con kế thừa (extends) nối tiếp qua 3 đến 4 tầng. Kiến trúc kế thừa sâu tạo ra sự phụ thuộc chặt (tight coupling): chỉ cần sửa một phương thức protected ở lớp cha, hàng loạt lớp con có thể bị lỗi dây chuyền. Đồng thời, việc dùng hằng số chuỗi rời rạc như const STATUS_OPEN = 'open' vẫn cho phép truyền bất kỳ chuỗi rác nào (như 'opened') vào tham số hàm có kiểu string.
Bài học này hướng dẫn bạn thiết kế mô hình miền (Domain Modeling) sạch trong PHP 8.4 bằng cách ưu tiên giao diện hợp đồng (interface kết hợp Property Hooks), chia sẻ hành vi ngang bằng trait, khóa kế thừa bằng final, và dùng Backed Enum (enum Name: string) để giới hạn tập giá trị hợp lệ ngay ở mức kiểu dữ liệu. Bài viết nối tiếp PHP EP 12: Property Hooks và Asymmetric Visibility trong PHP 8.4 và chuẩn bị cho việc tổ chức thư mục chuẩn PSR-4 tại PHP EP 14: Namespace, PSR-4 và Composer trong PHP. Tài liệu tham chiếu gồm PHP Manual: Object Interfaces, Traits và Enumerations.
Hồ sơ kiểm chứng & Môi trường Lab (Verification Profile):
php-course-qa:8.4 (--network none, --read-only, --tmpfs /tmp:rw,exec,nosuid,size=64m, user phplab UID 1001).PHP 8.4.27 (cli) trên Alpine Linux 3.21 (aarch64), Zend Engine v4.4.27.interface (kèm hợp đồng thuộc tính PHP 8.4), trait, abstract class, final và Backed Enum trong tiến trình CLI; chưa đi vào ánh xạ tự động xuống cơ sở dữ liệu PDO (Chặng 5).Để chọn đúng công cụ mô hình hóa cho từng bài toán mà không biến mọi thứ thành lớp cha extends, bảng dưới đây đối chiếu vai trò và ranh giới kỹ thuật của 4 cấu trúc:
| Cấu trúc ngôn ngữ | Mối quan hệ biểu diễn | Khả năng chứa trạng thái & Hành vi trong PHP 8.4 | Điều kiện / Không nên giả định |
|---|---|---|---|
interface |
Cam kết năng lực (Can-do / Contract); một lớp có thể implements nhiều interface. |
Chứa chữ ký phương thức public, hằng số, và từ PHP 8.4 có thể khai báo hợp đồng thuộc tính public Type $prop { get; set; }. |
Interface không chứa thân phương thức thực thi và không cấp phát ô nhớ thuộc tính. |
trait |
Tái sử dụng mã nguồn theo chiều ngang (Horizontal code reuse) qua từ khóa use. |
Chứa phương thức, thuộc tính và hằng số; trình biên dịch sao chép phẳng vào lớp sử dụng. | Một trait không phải là một kiểu dữ liệu (type): không thể dùng làm kiểu tham số hàm hay kiểm tra instanceof. |
abstract class / extends |
Phân loại bản chất (Is-a) và chia sẻ khung thuật toán chung; đơn kế thừa. | Chứa cả phương thức đã cài đặt lẫn phương thức trừu tượng abstract; lớp con ghi đè nên gắn #[\Override]. |
Mặc định nên đánh dấu final class cho các lớp cụ thể để ngăn kế thừa tùy tiện. |
enum (Unit & Backed Enum) |
Tập hợp hữu hạn các trạng thái cố định (Closed set of cases). | Mỗi case là thực thể đơn nhất (singleton); có thể gắn giá trị gốc int|string, cài đặt phương thức và implements interface. |
Enum không được chứa thuộc tính biến thiên (properties), không được extends và không thể khởi tạo bằng new. |
Nguyên tắc Ưu tiên hợp thành hơn kế thừa (Composition over Inheritance) giúp hệ thống linh hoạt trước thay đổi. Thay vì bắt SupportTicket và PaymentOrder kế thừa chung một lớp cha chỉ để xuất chuỗi kiểm toán, bạn định nghĩa interface Auditable làm kiểu đầu vào chung. Trên PHP 8.4, interface còn khai báo được hợp đồng thuộc tính public int $id { get; } trực tiếp. Khi cần chia sẻ một đoạn logic kỹ thuật giữa các lớp không cùng nhánh kế thừa, trait cho phép ghép đoạn mã đó vào lớp lúc biên dịch.
Đối với các trường trạng thái có tập giá trị cố định (như trạng thái phiếu Open, Closed), Backed Enum (enum TicketStatus: string) biến mỗi trạng thái thành hằng đối tượng có kiểu riêng TicketStatus. Khi lưu DB hoặc xuất JSON, bạn đọc $status->value để lấy chuỗi 'open'. Khi nhận chuỗi từ bên ngoài, PHP cung cấp hai phương thức tĩnh:
TicketStatus::from($input): Trả về thực thể Enum tương ứng, hoặc ném ValueError ngay nếu $input không khớp với bất kỳ case nào.TicketStatus::tryFrom($input): Trả về thực thể Enum hoặc trả về null êm thấm nếu không khớp.
Chúng ta sẽ xây dựng mô hình SupportTicket kết hợp Backed Enum TicketStatus, interface Auditable và trait HasAuditLine, đồng thời kiểm chứng sự khác biệt giữa tryFrom() và from().
Bước 1: Tạo vùng làm việc tạm an toàn
Mục đích: Khởi tạo thư mục thực hành tách biệt trong /tmp.
WORKDIR="$(mktemp -d /tmp/php-ep13-XXXXXX)"
cd "$WORKDIR"
pwd
Bước 2: Tạo kịch bản ticket_domain.php kết hợp Enum, Interface và Trait
Mục đích: Định nghĩa enum TicketStatus: string, giao diện Auditable, trait HasAuditLine và lớp bất biến final readonly class SupportTicket.
cat <<'EOF' > "$WORKDIR/ticket_domain.php"
<?php
declare(strict_types=1);
enum TicketStatus: string
{
case Open = 'open';
case Closed = 'closed';
public function isMutable(): bool
{
return match ($this) {
self::Open => true,
self::Closed => false,
};
}
}
interface Auditable
{
public function auditDescribe(): string;
}
trait HasAuditLine
{
public function formatAudit(string $entity, int $id, string $state): string
{
return "{$entity}#{$id}[{$state}]";
}
}
final readonly class SupportTicket implements Auditable
{
use HasAuditLine;
public function __construct(
public int $id,
public TicketStatus $status,
) {}
public function auditDescribe(): string
{
return $this->formatAudit('Ticket', $this->id, $this->status->value);
}
}
$t = new SupportTicket(7, TicketStatus::from('open'));
$unknown = TicketStatus::tryFrom('archived');
echo "Desc={$t->auditDescribe()} | UnknownIsNull=" . ($unknown === null ? 'true' : 'false') . "\n";
EOF
php "$WORKDIR/ticket_domain.php"
Kết quả quan sát được:
Desc=Ticket#7[open] | UnknownIsNull=true.TicketStatus::from('open') ánh xạ chuỗi 'open' thành thực thể TicketStatus::Open hợp lệ, còn TicketStatus::tryFrom('archived') trả về null an toàn.Cảnh báo an toàn: Không dùng $status->name (tên biến định danh trong mã nguồn) để lưu xuống cơ sở dữ liệu; hãy luôn dùng giá trị gắn kết $status->value của Backed Enum để tránh gãy dữ liệu khi đổi tên định danh.
Thuộc tính #[\Override] bắt buộc trình biên dịch kiểm tra xem phương thức ở lớp con có thực sự ghi đè một phương thức của lớp cha hoặc thực thi phương thức của interface hay không, giúp phát hiện sớm lỗi gõ sai tên hàm.
Bước 3: Tạo kịch bản interface_hook_override.php kiểm chứng Interface Property Hook và #[\Override]
Mục đích: Khai báo interface chứa cả hợp đồng thuộc tính { get; } của PHP 8.4 lẫn phương thức, và gắn #[\Override] trên lớp thực thi.
cat <<'EOF' > "$WORKDIR/interface_hook_override.php"
<?php
declare(strict_types=1);
interface IdentifiableResource
{
public string $resourceKey { get; }
public function summary(): string;
}
final readonly class ServerNode implements IdentifiableResource
{
public function __construct(
public string $resourceKey,
public int $cores,
) {}
#[\Override]
public function summary(): string
{
return "{$this->resourceKey}:{$this->cores}c";
}
}
function printResource(IdentifiableResource $res): void
{
echo "Key={$res->resourceKey} | Summary={$res->summary()}\n";
}
printResource(new ServerNode('node-ap-01', 16));
EOF
php "$WORKDIR/interface_hook_override.php"
Kết quả quan sát được:
Key=node-ap-01 | Summary=node-ap-01:16c. Lớp readonly class ServerNode với thuộc tính public string $resourceKey thỏa mãn trọn vẹn hợp đồng { get; } của interface.Cảnh báo an toàn: Khi tham số hàm nhận kiểu IdentifiableResource chỉ khai báo { get; }, hàm đó chỉ được phép đọc $res->resourceKey chứ không được gán giá trị mới.
Hãy kiểm chứng cách PHP 8.4 phản ứng khi một chuỗi không hợp lệ lọt vào TicketStatus::from().
Bước 4: Gọi TicketStatus::from('invalid_state') và kiểm tra mã thoát
Mục đích: Xác nhận BackedEnum::from() ném ngoại lệ ValueError ngay tại biên chuyển đổi dữ liệu.
set +e
php -r '
require "'"$WORKDIR"'/ticket_domain.php";
TicketStatus::from("invalid_state");
'
RC=$?
set -e
echo "RC=$RC"
Kết quả quan sát được:
RC=255 kèm thông báo: Fatal error: Uncaught ValueError: "invalid_state" is not a valid backing value for enum TicketStatus.
Bước 5: Dọn dẹp vùng làm việc tạm
Mục đích: Xóa thư mục thực hành trong /tmp.
rm -rf "$WORKDIR"
Khi áp dụng Enum và kế thừa vào dự án thực tế, cần lưu ý hai ranh giới:
Enum cho danh mục dữ liệu động do quản trị viên cấu hình: Enum dành cho tập trạng thái cố định gắn liền với nhánh logic trong mã nguồn. Với danh mục có thể thêm/sửa từ trang quản trị (như danh mục sản phẩm, đối tác vận chuyển), hãy dùng bảng cơ sở dữ liệu kết hợp Entity.final class cho các lớp dịch vụ và thực thể cụ thể: Giúp ngăn kế thừa tùy tiện làm rò rỉ chi tiết cài đặt nội bộ.Hãy tự trả lời 4 câu hỏi tình huống dưới đây:
trait Loggable, bạn có thể khai báo hàm function run(Loggable $task): void được không? -> Không thể. trait chỉ sao chép mã nguồn lúc biên dịch chứ không tạo ra một kiểu dữ liệu (type). Bạn phải tạo thêm interface.BackedEnum::tryFrom($input) và khi nào dùng BackedEnum::from($input)? -> Dùng tryFrom() cho dữ liệu chưa tin cậy khi muốn xử lý nhánh null nhẹ nhàng; dùng from() khi giá trị sai là vi phạm hợp đồng cần ném ngay ValueError.enum trong PHP 8.4 có thể khai báo thuộc tính lưu trữ trạng thái hoặc khởi tạo bằng new không? -> Hoàn toàn không. Enum cấm khai báo properties và cấm khởi tạo bằng new.#[\Override] đặt trên phương thức lớp con mang lại lợi ích gì? -> Buộc PHP kiểm tra lúc biên dịch xem phương thức đó có thực sự tồn tại trên lớp cha hoặc interface hay không, tránh lỗi gõ sai tên hàm.Bạn đã nắm vững bộ công cụ mô hình hóa hướng đối tượng của PHP 8.4. Hãy bước sang bài cuối của Chặng 3 tại PHP EP 14: Namespace, PSR-4 và Composer trong PHP để tổ chức dự án đa tệp chuẩn công nghiệp.
Ôn bài trước: PHP EP 12: Property Hooks và Asymmetric Visibility trong PHP 8.4. Học tiếp: PHP EP 14: Namespace, PSR-4 và Composer trong PHP.

Quản lý giao dịch cơ sở dữ liệu bằng beginTransaction, commit và rollBack trong PHP PDO, thực hành bật ràng buộc khóa ngoại và đóng gói truy vấn vào Repository.

Thiết kế cấu trúc thư mục dự án PHP tách biệt nghiệp vụ và giao diện, thực hành viết kịch bản kiểm thử tự động xác minh nhánh lỗi và đọc Stack Trace để gỡ lỗi.

Tổng hợp kiến thức toàn khóa qua dự án ứng dụng quản lý Ticket bằng PHP và PDO: kết hợp Property Hooks, Enum, Transaction, bảo mật biểu mẫu HTTP và bộ kiểm thử.

Phân tích vòng đời HTTP Request/Response trong PHP qua $_GET, $_POST, $_SERVER, thực hành kiểm tra dữ liệu bằng filter_var và mã hóa đầu ra chống tấn công XSS.