Chuyển đến nội dung chính (Skip to content)
Mục lục bài viết
PHP

PHP EP 13: Interface, Trait và Enum trong PHP

PHP EP 13: Interface, Trait và Enum trong PHP

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.

Chọn hợp đồng kiểu thay cho kế thừa quá sâu

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):

  • Ngày kiểm chứng: 2026-10-26.
  • Môi trường thực thi: Container cô lập php-course-qa:8.4 (--network none, --read-only, --tmpfs /tmp:rw,exec,nosuid,size=64m, user phplab UID 1001).
  • Phiên bản runtime: PHP 8.4.27 (cli) trên Alpine Linux 3.21 (aarch64), Zend Engine v4.4.27.
  • Giới hạn kiểm chứng: Kiểm chứng 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).

Phân biệt Abstract Class, Interface, Trait và Enum

Để 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.

Interface, Trait và Backed Enum có vai trò khác nhau

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.
Quy trình chọn công cụ mô hình hóa giữa kế thừa dọc, hợp đồng Interface và Enum.
Hình 1. Quy trình chọn công cụ mô hình hóa giữa kế thừa dọc, hợp đồng Interface và Enum. Sơ đồ minh họa khái niệm; không phải ảnh chụp hay bản ghi kiểm thử. Mở hình để xem chi tiết.

Mô hình hóa phiếu hỗ trợ bằng Interface và Enum

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:

  • Đầu ra hiển thị: 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.

Kiểm tra Override và Property Hook trên Interface

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:

  • Màn hình in ra: 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.

Kiểm tra ValueError của Enum và dọn dẹp

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:

  • Tiến trình kết thúc với RC=255 kèm thông báo: Fatal error: Uncaught ValueError: "invalid_state" is not a valid backing value for enum TicketStatus.
Đối chiếu phạm vi sử dụng của Interface, Abstract Class, Trait và Backed Enum.
Hình 2. Đối chiếu phạm vi sử dụng của Interface, Abstract Class, Trait và Backed Enum. Sơ đồ minh họa khái niệm; không phải ảnh chụp hay bản ghi kiểm thử. Mở hình để xem chi tiết.

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 nào nên dùng Enum và lớp final

Khi áp dụng Enum và kế thừa vào dự án thực tế, cần lưu ý hai ranh giới:

  1. Không dùng 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.
  2. Mặc định dùng 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ộ.

Tự kiểm tra và giới hạn

Hãy tự trả lời 4 câu hỏi tình huống dưới đây:

  • Câu hỏi 1: Giả sử có 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.
  • Câu hỏi 2: Khi nào dùng 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.
  • Câu hỏi 3: Một 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.
  • Câu hỏi 4: Thuộc tính #[\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.

Bài liên quan trong bộ PHP

Tìm hiểu thêm