
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.

Làm chủ Property Hooks (get/set) và Asymmetric Visibility (public private(set)) trong PHP, thực hành kiểm tra dữ liệu gán vào thuộc tính và phân biệt với __get.
Trong các lớp thực thể nghiệp vụ (Domain Entities) như UserAccount hay Invoice, chúng ta thường có hai nhu cầu thiết thực đối với thuộc tính: thứ nhất là tự động chuẩn hóa hoặc tính toán giá trị mỗi khi đọc/ghi (như chuẩn hóa email về chữ thường đã cắt khoảng trắng, hoặc trích xuất tên miền từ email); thứ hai là cho phép bên ngoài đọc trực tiếp thuộc tính (như số lần đăng nhập sai $user->loginAttempts) nhưng chỉ cho phép các phương thức nội bộ trong lớp thay đổi nó. Trước PHP 8.4, lập trình viên buộc phải biến mọi thuộc tính thành private rồi viết hàng loạt hàm getEmail(), setEmail(), getDomain() lặp đi lặp lại, hoặc dùng __get()/__set() làm mất khả năng kiểm tra kiểu tĩnh.
Bài học này giúp bạn làm chủ hai tính năng hướng đối tượng quan trọng bậc nhất của PHP 8.4: Property Hooks (get / set) và Asymmetric Visibility (public private(set), public protected(set)). Bạn sẽ phân biệt được thuộc tính có lưu trữ (backed property) với thuộc tính ảo (virtual property) và bảo vệ trạng thái đối tượng gọn gàng. Bài viết kế thừa kiến thức từ PHP EP 11: Class, Object và Constructor Promotion trong PHP và chuẩn bị cho PHP EP 13: Interface, Trait và Enum trong PHP. Tài liệu tham chiếu gồm PHP RFC: Property Hooks, PHP RFC: Asymmetric Visibility v2 và PHP Manual: Property Hooks.
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.Mỗi cơ chế can thiệp vào thuộc tính trong PHP 8.4 có cách cấp phát bộ nhớ và giới hạn thao tác khác nhau. Bảng dưới đây tổng hợp hợp đồng kỹ thuật chi tiết:
| Cơ chế PHP 8.4 | Cú pháp khai báo | Cấp phát bộ nhớ & Hành vi thực tế | Điều kiện / Không nên giả định |
|---|---|---|---|
| Backed Property Hook (Thuộc tính có lưu trữ) | public string $email { set => strtolower(trim($value)); } |
Có ô nhớ lưu trữ thực (`backing store`) trong đối tượng; ít nhất một nhánh get/set tham chiếu tới $this->email. |
Với biểu thức rút gọn set => expr, kết quả của biểu thức tự động được gán vào ô nhớ thuộc tính. |
| Virtual Property Hook (Thuộc tính ảo) | public string $domain { get => substr(strrchr($this->email, '@'), 1); } |
Không tốn ô nhớ lưu trữ trên đối tượng; giá trị được tính toán động mỗi lần đọc vì không nhánh nào dùng $this->domain. |
Nếu chỉ khai báo nhánh get cho thuộc tính ảo, mọi nỗ lực gán giá trị vào thuộc tính đó sẽ ném lỗi Error. |
| Asymmetric Visibility (Phạm vi bất đối xứng) | public private(set) int $loginAttempts = 0; |
Cho phép đọc ở phạm vi public nhưng chỉ cho phép gán/thay đổi ở phạm vi private (bên trong chính lớp đó). |
Phạm vi set bắt buộc hẹp hơn hoặc bằng phạm vi get (không thể khai báo private public(set)) và phải có kiểu tường minh. |
Tương tác với Tham chiếu (&$obj->prop) |
Lấy tham chiếu tới thuộc tính có set hook hoặc private(set) |
Bị chặn từ bên ngoài để ngăn việc lách qua set hook hoặc sửa lén thuộc tính private(set). |
Không thể sửa mảng trực tiếp bằng $obj->items[] = $x từ bên ngoài nếu $items có set hook hoặc private(set). |
Theo PHP RFC: Property Hooks, Zend Engine cho phép gắn trực tiếp hai nhánh get và set vào khai báo thuộc tính. Trình biên dịch tự động phân biệt giữa Backed Property và Virtual Property:
get hoặc set có truy cập $this->propertyName (hoặc dùng cú pháp rút gọn set => ...), Zend Engine cấp phát một ô nhớ thực (backing value) cho thuộc tính. Trong nhánh set, biến mặc định $value chứa giá trị đang truyền vào (đã kiểm tra khớp kiểu).get và set đều không nhắc tới $this->propertyName (như public string $domain { get => ... }), đó là Virtual Property không chiếm bộ nhớ lưu trữ và không xuất hiện khi var_dump().Song hành cùng Property Hooks, PHP RFC: Asymmetric Visibility v2 giải quyết bài toán "đọc công khai, ghi nội bộ" mà readonly không đáp ứng được (do readonly cấm cả chính lớp đó cập nhật lần hai). Với public private(set) int $loginAttempts = 0;, bên ngoài đọc được $user->loginAttempts trực tiếp, nhưng chỉ các phương thức bên trong lớp mới được phép thực hiện $this->loginAttempts++.

Chúng ta sẽ xây dựng lớp UserAccount kết hợp cả ba kỹ thuật của PHP 8.4: Backed Property Hook chuẩn hóa email khi gán, Virtual Property Hook trích xuất tên miền, và public private(set) bảo vệ bộ đếm đăng nhập sai.
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 riêng trong /tmp.
WORKDIR="$(mktemp -d /tmp/php-ep12-XXXXXX)"
cd "$WORKDIR"
pwd
Bước 2: Tạo kịch bản user_account.php kiểm chứng Property Hooks và private(set)
Mục đích: Xác nhận nhánh set tự động làm sạch chuỗi email, nhánh get ảo tính tên miền chính xác và phương thức nội bộ tăng được biến đếm private(set).
cat <<'EOF' > "$WORKDIR/user_account.php"
<?php
declare(strict_types=1);
class UserAccount
{
public string $email {
set(string $value) {
$clean = strtolower(trim($value));
if (!str_contains($clean, '@')) {
throw new InvalidArgumentException('Invalid email');
}
$this->email = $clean;
}
}
public string $domain {
get => substr((string) strrchr($this->email, '@'), 1);
}
public private(set) int $loginAttempts = 0;
public function __construct(string $email)
{
$this->email = $email;
}
public function recordFailedLogin(): void
{
$this->loginAttempts++;
}
}
$u = new UserAccount(' Admin@ThanhNH.id.vn ');
$u->recordFailedLogin();
echo "Email={$u->email} | Domain={$u->domain} | Attempts={$u->loginAttempts}\n";
EOF
php "$WORKDIR/user_account.php"
Kết quả quan sát được:
Email=admin@thanhnh.id.vn | Domain=thanhnh.id.vn | Attempts=1.new UserAccount(...) gán $this->email = $email, giá trị đi qua nhánh set, cắt khoảng trắng và đổi về chữ thường admin@thanhnh.id.vn.$u->domain, nhánh get ảo tính toán tức thời trả về thanhnh.id.vn.Cảnh báo an toàn: Không đặt các tác vụ tốn tài nguyên (như truy vấn SQL hay gọi HTTP API) bên trong nhánh get của Property Hook, vì người đọc mã luôn kỳ vọng truy cập thuộc tính $u->domain là thao tác bộ nhớ tức thời.
Khi chỉ cần biến đổi giá trị bằng một biểu thức đơn giản, PHP 8.4 cho phép viết gọn set => biểu_thức ngay trong danh sách tham số của __construct().
Bước 3: Tạo kịch bản compact_hooks.php sử dụng biểu thức set => rút gọn
Mục đích: Viết lớp ProductSku dùng Constructor Property Promotion kết hợp public protected(set) và biểu thức set => tự động in hoa mã sản phẩm.
cat <<'EOF' > "$WORKDIR/compact_hooks.php"
<?php
declare(strict_types=1);
class ProductSku
{
public function __construct(
public string $code {
set => strtoupper(trim($value));
},
public protected(set) int $stock = 0,
) {}
public function restock(int $added): void
{
if ($added <= 0) {
throw new InvalidArgumentException('Added stock must be positive.');
}
$this->stock += $added;
}
}
$sku = new ProductSku(' vn-srv-01 ', 15);
$sku->restock(5);
echo "SKU={$sku->code} | Stock={$sku->stock}\n";
EOF
php "$WORKDIR/compact_hooks.php"
Kết quả quan sát được:
SKU=VN-SRV-01 | Stock=20. Biến $value trong set => strtoupper(trim($value)) tự động nhận đối số truyền vào và ghi kết quả vào ô nhớ của $this->code.Cảnh báo an toàn: Khi đọc $this->prop trực tiếp bên trong thân get của chính $prop, PHP 8.4 đọc ô nhớ lưu trữ gốc (backing store), nhưng nếu bạn gọi qua một phương thức khác quay lại đọc $this->prop thì có thể gây đệ quy vô hạn.
Chúng ta sẽ thử gán trực tiếp $u->loginAttempts = 0 từ phạm vi toàn cục bên ngoài lớp UserAccount để kiểm chứng cơ chế bảo vệ của public private(set).
Bước 4: Thực thi phép gán trái phép vào thuộc tính private(set) và kiểm tra mã thoát
Mục đích: Xác nhận Zend Engine chặn đứng hành vi sửa đổi thuộc tính private(set) từ bên ngoài lớp bằng lỗi Error.
set +e
php -r '
require "'"$WORKDIR"'/user_account.php";
$u->loginAttempts = 0;
'
RC=$?
set -e
echo "RC=$RC"
Kết quả quan sát được:
$u->loginAttempts = 0; ở phạm vi toàn cục làm tiến trình dừng ngay lập tức với RC=255 kèm lỗi: Fatal error: Uncaught Error: Cannot modify private(set) property UserAccount::$loginAttempts from global scope.
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 đưa Property Hooks và Asymmetric Visibility vào dự án thực tế, cần nắm vững hai quy tắc:
interface (PHP 8.4): Giờ đây interface có thể khai báo trực tiếp interface Identifiable { public string $id { get; } }. Lớp thực thi có thể thỏa mãn { get; } bằng thuộc tính public thường, readonly, public private(set) hoặc thuộc tính ảo có nhánh get.virtual property) không có ô nhớ lưu trữ nên không xuất hiện trong kết quả của serialize() hay var_export().Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:
$this->tenThuocTinh hay không. Nếu có đọc/ghi $this->tenThuocTinh (hoặc dùng set => expr), đó là Backed Property; nếu không nhắc tới, đó là Virtual Property.private public(set) string $name; có hợp lệ trong PHP 8.4 không? -> Không hợp lệ (Fatal error). Quyền set bắt buộc phải bằng hoặc hẹp hơn quyền get (public -> protected -> private).public readonly int $count và public private(set) int $count là gì? -> Khả năng cập nhật nhiều lần từ bên trong lớp. readonly chỉ cho gán đúng 1 lần duy nhất, còn public private(set) khóa việc ghi từ bên ngoài nhưng cho phép các phương thức nội bộ cập nhật giá trị nhiều lần.static) hoặc thuộc tính readonly không? -> Không. Property Hooks chỉ áp dụng cho thuộc tính thực thể (instance properties) và không kết hợp trực tiếp với readonly.Hoàn thành bài học này, bạn đã nắm vững cách đóng gói trạng thái đối tượng hiện đại nhất của PHP 8.4. Hãy tiếp tục sang PHP EP 13: Interface, Trait và Enum trong PHP để thiết kế hợp đồng giao diện và kiểu Enum an toàn.
Ôn bài trước: PHP EP 11: Class, Object và Constructor Promotion trong PHP. Học tiếp: PHP EP 13: Interface, Trait và Enum 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.