PHP hiện đại với Property Hooks và Asymmetric Visibility trong PHP 8.4+: OOP gọn gàng không còn code thừa

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Vấn đề: Cái giá phải trả cho boilerplate truyền thống trong PHP

Viết domain model gọn gàng trong PHP từ lâu đã là một sự đánh đổi gây ức chế giữa tính đóng gói (encapsulation) và sự tỉnh táo của lập trình viên. Mỗi lần tạo một entity hay một data transfer object (DTO), bạn lại phải viết hàng tá dòng code phòng thủ chỉ để làm nhiệm vụ chuyển tiếp biến qua ranh giới của class.

Để ngăn chặn bên ngoài can thiệp vào trạng thái nội bộ, giải pháp thông thường là khóa các thuộc tính lại bằng private hoặc protected, sau đó tạo ra hàng loạt cặp getter và setter:

<?php

class UserProfile
{
    private string $username;
    private int $reputationScore = 0;

    public function __construct(string $username)
    {
        $this->setUsername($username);
    }

    public function getUsername(): string
    {
        return $this->username;
    }

    public function setUsername(string $username): void
    {
        $cleaned = trim($username);
        if (strlen($cleaned) < 3) {
            throw new InvalidArgumentException('Tên người dùng phải có ít nhất 3 ký tự.');
        }
        $this->username = $cleaned;
    }

    public function getReputationScore(): int
    {
        return $this->reputationScore;
    }

    public function increaseReputation(int $points): void
    {
        if ($points > 0) {
            $this->reputationScore += $points;
        }
    }
}

Tại sao lại xảy ra tình trạng này? PHP cổ điển áp dụng visibility theo kiểu nhị phân: một thuộc tính hoặc là hoàn toàn công khai, hoặc là bị khóa chặt. Khai báo public string $username cho phép bên ngoài đọc dữ liệu, nhưng nó cũng tước bỏ mọi cơ chế định dạng và kiểm tra hợp lệ (validation). Trong một codebase production với 40 domain entity, mỗi entity trung bình có 8 thuộc tính, chỉ riêng hạn chế này đã dễ dàng làm phình to dự án của bạn thêm 1.500 đến 2.000 dòng code accessor vô vị.

Quá trình phát triển của quản lý state trong PHP

Việc quản lý state đã được cải thiện liên tục qua các bản phát hành lớn gần đây. Dưới đây là so sánh giữa các phương pháp trước đây và PHP 8.4 hiện đại:

Phương pháp Quyền truy cập Đọc/Ghi Validation nội dòng (Inline) Chi phí code boilerplate
PHP 7.4 Typed Properties Đồng nhất (public/protected/private) Đòi hỏi các method setter rõ ràng Cao (hàng tá getter/setter thủ công)
PHP 8.1 Readonly Properties Bất biến sau khi khởi tạo Không có khi cập nhật (không thể thay đổi giá trị) Thấp, nhưng không dùng được cho mutable state
PHP 8.2+ Magic Methods (__get / __set) Mô phỏng truy cập động Khả thi, nhưng bỏ qua static typing Trung bình; làm giảm khả năng autocompletion của IDE và phân tích tĩnh
PHP 8.4+ Hooks & Asymmetric Visibility Phân quyền chi tiết (ví dụ: public private(set)) Gắn trực tiếp thông qua các hook get và set Cực kỳ thấp; đảm bảo trọn vẹn type safety và hỗ trợ IDE

Vì sao các giải pháp tình thế trước đây chưa đủ tốt?

Các nhóm lập trình thường dựa vào các magic method __get() và __set() để mô phỏng các thuộc tính tính toán (computed) hoặc bị hạn chế quyền. Cách tiếp cận này đi kèm với những nhược điểm lớn. Việc chặn truy cập thuộc tính động chạy chậm hơn khoảng 3 đến 5 lần trong các bài microbenchmark so với truy cập trực tiếp, làm hỏng khả năng điều hướng code của IDE, và buộc bạn phải viết các docblock annotation tùy chỉnh để PHPStan hoặc Psalm có thể hiểu được kiểu dữ liệu.

PHP 8.1 mang đến các thuộc tính readonly, giải quyết triệt để vấn đề cho các DTO tĩnh. Tuy nhiên, các domain entity biến đổi trong suốt vòng đời của chúng—như giỏ hàng, gói đăng ký và hồ sơ người dùng—vẫn tiếp tục phải gánh chịu sự rườm rà của getter/setter truyền thống.

PHP 8.4 trực tiếp giải quyết vấn đề này với hai tính năng ngôn ngữ trọng tâm:

  • Asymmetric Visibility: Kiểm soát quyền đọc và ghi độc lập với nhau (chẳng hạn, cho phép đọc public nhưng giới hạn quyền ghi chỉ trong nội bộ class).
  • Property Hooks: Nhúng logic đọc (get) và ghi (set) tùy chỉnh trực tiếp vào ngay phần khai báo thuộc tính.

Ưu và nhược điểm: Đánh giá các tính năng property trong PHP 8.4

Lợi ích chính

  • Giảm đáng kể lượng code cần bảo trì: Một domain model thông thường giảm từ 40% đến 60% lượng code. File của bạn sẽ tập trung vào các quy tắc nghiệp vụ (invariants) thay vì các method accessor lặp đi lặp lại.
  • Refactor an toàn và linh hoạt: Bạn có thể bắt đầu với một thuộc tính public thông thường và bổ sung hook sau đó. Phía gọi code vẫn tiếp tục truy cập $user->username mà không làm thay đổi hay phá vỡ public API.
  • Hỗ trợ công cụ native: Các công cụ phân tích tĩnh như PHPStan 2.0+ và các IDE như PhpStorm đều hiểu cả hai tính năng này mà không cần plugin bổ sung hay mẹo docblock.
  • Cú pháp tự nhiên: Phía gọi code đọc và ghi các thuộc tính đối tượng chuẩn bằng phép gán trực tiếp, giúp việc sử dụng trở nên rất trực quan.

Những điểm đánh đổi trong thực tế

  • Nguy cơ phình to hook (Hook Bloat): Các hook nên duy trì tốc độ nhanh và gọn nhẹ. Việc gọi các request HTTP, query cơ sở dữ liệu hoặc kích hoạt các domain event nặng nề bên trong hook set sẽ vi phạm nguyên tắc phân tách mối quan tâm (separation of concerns) và dẫn đến những lỗi rất khó debug.
  • Giới hạn với Constructor Promotion: Mặc dù asymmetric visibility hoạt động mượt mà trong constructor parameter promotion, property hooks lại không thể khai báo trực tiếp bên trong chữ ký constructor.
  • Thói quen của đội ngũ phát triển: Các lập trình viên đã quen với quy ước getter kiểu PSR cũ có thể cần thời gian để thích nghi với việc đọc thuộc tính trực tiếp thay vì gọi $order->getStatus().

Cấu hình môi trường

Cả hai tính năng đều yêu cầu PHP 8.4 trở lên.

1. Kiểm tra phiên bản PHP của bạn

php -v
# Đảm bảo kết quả hiển thị PHP 8.4.0 hoặc cao hơn

2. Cấu hình Composer và công cụ kiểm soát chất lượng

Khóa môi trường của bạn ở PHP 8.4 và cài đặt các thư viện phân tích tĩnh cũng như testing tương thích:

{
    "require": {
        "php": ">=8.4.0"
    },
    "require-dev": {
        "phpstan/phpstan": "^2.0",
        "phpunit/phpunit": "^11.0"
    }
}

Việc refactor các entity thanh toán cốt lõi của chúng tôi bằng các tính năng này đã giúp cắt giảm hàng trăm dòng code diff trong pull request. Người review có thể dành thời gian kiểm tra các quy tắc nghiệp vụ thay vì phải lướt qua hàng loạt boilerplate getter giống hệt nhau.

Các bước refactor thực tế theo từng bước

Bước 1: Thiết lập Asymmetric Visibility

Hãy xem xét một model xử lý đơn hàng. Định danh (ID), tổng tiền đơn hàng và trạng thái cần được các template và tác vụ báo cáo đọc ra, nhưng thao tác ghi phải hoàn toàn nằm trong nội bộ model. Asymmetric visibility xử lý việc này trực tiếp ngay tại vị trí khai báo:

<?php

declare(strict_types=1);

class Order
{
    // Cho phép đọc tự do, nhưng chỉ có thể chỉnh sửa từ bên trong Order
    public private(set) string $status = 'pending';

    // Có thể đọc ở bất kỳ đâu, nhưng cập nhật bị giới hạn trong class và các subclass
    public protected(set) DateTimeImmutable $updatedAt;

    public function __construct(
        public private(set) int $id,
        public private(set) float $amount
    ) {
        $this->updatedAt = new DateTimeImmutable();
    }

    public function markAsCompleted(): void
    {
        $this->status = 'completed';
        $this->updatedAt = new DateTimeImmutable();
    }
}

$order = new Order(id: 1042, amount: 89.95);
echo $order->status; // In ra 'pending' (Hợp lệ: cho phép đọc public)

// $order->status = 'cancelled'; 
// Fatal Error: Cannot modify private(set) property Order::$status from global scope

Bước 2: Thêm Validation và chuẩn hóa bằng Hooks

Khi dữ liệu đầu vào cần được cắt tỉa khoảng trắng hoặc kiểm tra định dạng, hãy gắn trực tiếp các hook set và get vào thuộc tính. Các backed property sử dụng đối số $value ngầm định, và việc trả về một giá trị sẽ thiết lập trạng thái của thuộc tính bên dưới.

<?php

declare(strict_types=1);

class CustomerAccount
{
    public string $email {
        set {
            $sanitized = filter_var(trim($value), FILTER_VALIDATE_EMAIL);
            if ($sanitized === false) {
                throw new InvalidArgumentException("Địa chỉ email không hợp lệ: {$value}");
            }
            return strtolower($sanitized);
        }
    }

    // Virtual property: được tính toán khi truy cập mà không cần vùng lưu trữ riêng
    public string $displayName {
        get => ucfirst(explode('@', $this->email)[0]);
    }

    public function __construct(string $email)
    {
        $this->email = $email; // Tự động kích hoạt hook set
    }
}

$account = new CustomerAccount('  [email protected] ');
echo $account->email;       // '[email protected]'
echo $account->displayName; // 'Alex.dev'

Bước 3: Kết hợp Hooks với Asymmetric Visibility

Kết hợp cả hai tính năng sẽ tạo ra các domain model ngắn gọn và có khả năng tự bảo vệ dữ liệu. Dưới đây là widget bình chọn, nơi số lượt bình chọn được hiển thị công khai, nhưng người gọi bên ngoài không thể tự ý can thiệp hoặc giảm số lượng:

<?php

declare(strict_types=1);

class ProductReview
{
    public private(set) int $helpfulVotes = 0 {
        set {
            if ($value < 0) {
                throw new LogicException('Số lượt bình chọn không thể là số âm.');
            }
            return $value;
        }
    }

    public function __construct(public readonly string $content) {}

    public function upvote(): void
    {
        $this->helpfulVotes++;
    }
}

$review = new ProductReview('Giao hàng nhanh và chất lượng hoàn thiện chắc chắn.');
$review->upvote();
echo $review->helpfulVotes; // 1

// $review->helpfulVotes = 10; 
// Fatal Error: Cannot modify private(set) property ProductReview::$helpfulVotes from global scope

Với PHP 8.4, các class entity của bạn sẽ luôn tinh gọn, giàu tính biểu đạt và được bảo vệ chặt chẽ mà không cần đến hàng ngàn dòng code boilerplate.

Share: