
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.

Xây dựng lớp và đối tượng trong PHP với thuộc tính có kiểu, Constructor Property Promotion và readonly, thực hành gọi phương thức trực tiếp ngay khi khởi tạo.
Ở các bài học trước, chúng ta dùng mảng kết hợp như ['host' => 'web-01', 'cpu' => 88] để biểu diễn dữ liệu nhanh gọn. Tuy nhiên, khi dự án mở rộng quy mô, việc truyền mảng tự do qua nhiều tầng hàm sẽ bộc lộ ba điểm yếu lớn: trình thông dịch không phát hiện được lỗi gõ nhầm tên khóa 'cpu' thành 'cup', không thể kiểm tra kiểu dữ liệu của từng trường ngay tại chữ ký hàm, và không gắn kèm được quy tắc bảo toàn tính hợp lệ ngay lúc khởi tạo. Hậu quả là mọi hàm nhận mảng đều phải viết lại hàng loạt lệnh isset() phòng thủ.
Bài học mở đầu Chặng 3 này hướng dẫn bạn chuyển từ mảng tự do sang mô hình Lập trình hướng đối tượng (OOP) hiện đại trong PHP 8.4 với class, thuộc tính định kiểu (typed properties), cú pháp rút gọn Constructor Property Promotion, từ khóa bất biến readonly và cú pháp khởi tạo chuỗi không cần ngoặc phụ new MyClass()->method() mới trên PHP 8.4. Bài viết kế thừa kiến thức từ PHP EP 10: Mảng PHP và các hàm array_find, array_any, array_all và làm tiền đề cho PHP EP 12: Property Hooks và Asymmetric Visibility trong PHP 8.4. Tài liệu tham chiếu gồm PHP Manual: Classes and Objects, Readonly classes và PHP RFC: new MyClass()->method() without parentheses.
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.uninitialized của thuộc tính định kiểu, lớp readonly và cú pháp new ...()->... của PHP 8.4; chưa đi vào Property Hooks (EP 12) hay Autoloading PSR-4 (EP 14).Để thiết kế lớp gọn gàng và chặt chẽ, bạn cần nắm rõ sự khác biệt giữa thuộc tính định kiểu, Constructor Promotion, readonly và quy tắc khởi tạo của PHP 8.4:
| Cơ chế OOP | Cú pháp tiêu biểu | Hành vi thực tế trong Zend Engine | Điều kiện / Không nên giả định |
|---|---|---|---|
| Thuộc tính định kiểu (Typed Property) | public int $weight; |
Khi chưa được gán giá trị, thuộc tính mang trạng thái nội bộ IS_UNDEF (uninitialized), không phải null. |
Đọc thuộc tính định kiểu trước khi gán giá trị sẽ ném Error: Typed property ... must not be accessed before initialization. |
| Constructor Property Promotion | public function __construct(public string $id) {} |
Trình biên dịch tự động khai báo thuộc tính $this->id và gán giá trị tham số ngay đầu hàm __construct(). |
Không dùng được với kiểu callable; vẫn có thể viết thêm logic kiểm tra trong thân __construct(). |
Thuộc tính / Lớp readonly |
readonly class Invoice { ... } |
Mọi thuộc tính chỉ được phép gán đúng 1 lần duy nhất; mọi nỗ lực ghi đè sau đó đều ném Error. |
readonly class bắt buộc 100% thuộc tính phải có khai báo kiểu tường minh và cấm tạo thuộc tính động (dynamic properties). |
| Khởi tạo gọi chuỗi trực tiếp (PHP 8.4) | new ReleaseTag('api', 42)->label() |
Truy cập phương thức, thuộc tính hoặc hằng số ngay sau new ClassName(...) mà không cần bọc ngoặc đơn (new ...). |
Phải luôn có cặp ngoặc tròn () sau tên lớp (new Foo()->bar() hợp lệ, nhưng new Foo->bar() là lỗi cú pháp). |
Khi một lớp định nghĩa thuộc tính có kiểu tường minh (public int $weight;) mà không có giá trị mặc định, Zend Engine không tự gán null mà đánh dấu ô nhớ bằng cờ IS_UNDEF (uninitialized). Thiết kế này bảo toàn lời hứa kiểu dữ liệu: nếu thuộc tính khai báo là int (không phải ?int), bất kỳ lúc nào đọc thành công thuộc tính đó, kết quả chắc chắn là một số nguyên int. Để tránh quên khởi tạo, Constructor Property Promotion (PHP Manual) gộp bước khai báo thuộc tính, tham số hàm tạo và lệnh gán $this->prop = $prop vào ngay danh sách tham số của __construct().
Khi kết hợp Constructor Promotion với readonly class, toàn bộ thuộc tính được khóa cứng chỉ-ghi-một-lần (write-once) ngay sau khi khởi tạo, rất lý tưởng cho đối tượng truyền dữ liệu (DTO) và đối tượng giá trị (Value Object).
Đặc biệt trên PHP 8.4, đặc tả PHP RFC: new MyClass()->method() without parentheses cho phép gọi chuỗi phương thức hoặc đọc thuộc tính trực tiếp ngay sau new ClassName($args)->method(), loại bỏ cặp ngoặc đơn dư thừa (new ClassName($args))->method() của các phiên bản cũ.

Chúng ta sẽ xây dựng lớp readonly class ReleaseTag sử dụng Constructor Property Promotion, kiểm tra điều kiện hợp lệ trong hàm tạo và gọi phương thức trực tiếp bằng cú pháp new ReleaseTag(...)->label() của PHP 8.4.
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-ep11-XXXXXX)"
cd "$WORKDIR"
pwd
Bước 2: Tạo kịch bản release_tag.php với readonly class và cú pháp new ...()->... của PHP 8.4
Mục đích: Định nghĩa đối tượng giá trị bất biến biểu diễn thẻ phát hành phần mềm và gọi phương thức label() ngay trên biểu thức new.
cat <<'EOF' > "$WORKDIR/release_tag.php"
<?php
declare(strict_types=1);
readonly class ReleaseTag
{
public function __construct(
public string $service,
public int $buildNumber,
) {
if ($this->buildNumber <= 0) {
throw new InvalidArgumentException('Build number must be positive.');
}
}
public function label(): string
{
return "{$this->service}#b{$this->buildNumber}";
}
}
echo "Label=" . new ReleaseTag('billing-api', 42)->label() . "\n";
EOF
php "$WORKDIR/release_tag.php"
Kết quả quan sát được:
Label=billing-api#b42.$this->service và $this->buildNumber trước khi chạy thân hàm { ... } của __construct(), nên câu lệnh if ($this->buildNumber <= 0) kiểm tra được ngay giá trị vừa truyền vào.Cảnh báo an toàn: Trong một readonly class, nếu thuộc tính giữ một đối tượng khả biến (như DateTime), từ khóa readonly chỉ ngăn gán đối tượng mới đè lên thuộc tính đó chứ không ngăn sửa nội dung bên trong đối tượng con. Hãy luôn dùng lớp bất biến như DateTimeImmutable.
Khi đối tượng đã khóa bằng readonly, muốn tạo phiên bản cập nhật (ví dụ tăng số thứ tự bản dựng thêm 1) mà không phá vỡ tính bất biến, chúng ta cung cấp phương thức with*() trả về một thực thể mới.
Bước 3: Tạo kịch bản immutable_step.php minh họa phương thức withNextRevision()
Mục đích: Giữ nguyên thực thể gốc $v1 và tạo ra thực thể mới $v2 với số thứ tự bản dựng tăng thêm 1.
cat <<'EOF' > "$WORKDIR/immutable_step.php"
<?php
declare(strict_types=1);
readonly class BuildArtifact
{
public function __construct(
public string $name,
public int $revision,
) {}
public function withNextRevision(): self
{
return new self($this->name, $this->revision + 1);
}
}
$v1 = new BuildArtifact('core-worker', 10);
$v2 = $v1->withNextRevision();
echo "V1={$v1->revision} | V2={$v2->revision} | SameObject=" . ($v1 === $v2 ? 'true' : 'false') . "\n";
EOF
php "$WORKDIR/immutable_step.php"
Kết quả quan sát được:
V1=10 | V2=11 | SameObject=false.=== trên hai biến đối tượng chỉ trả về true khi cả hai cùng trỏ tới đúng một thực thể trong bộ nhớ. Ở đây $v1 và $v2 độc lập hoàn toàn.Cảnh báo an toàn: Không dùng thuộc tính static để lưu trạng thái thay đổi theo từng request của người dùng vì dễ gây rò rỉ dữ liệu giữa các tác vụ trên môi trường worker chạy dài hạn.
Nếu khai báo public int $weight; bên trong lớp nhưng quên gán giá trị trước khi đọc, Zend Engine sẽ chặn lại bằng lỗi nghiêm trọng thay vì âm thầm trả về null.
Bước 4: Đọc thuộc tính định kiểu trước khi khởi tạo và kiểm tra mã thoát
Mục đích: Xác nhận PHP 8.4 ném Error khi đọc thuộc tính đang ở trạng thái uninitialized.
set +e
php -r '
class Box { public int $weight; }
$b = new Box();
echo $b->weight;
'
RC=$?
set -e
echo "RC=$RC"
Kết quả quan sát được:
RC=255 kèm thông báo: Fatal error: Uncaught Error: Typed property Box::$weight must not be accessed before initialization.public ?int $weight; (có dấu ?) nhưng không viết = null, thuộc tính lúc mới tạo vẫn ở trạng thái uninitialized chứ chưa phải null.
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"
Trong kiến trúc thực tế, cần phân loại đối tượng theo vai trò:
readonly class kết hợp Constructor Property Promotion để khóa trạng thái.readonly class, mà cần kết hợp Property Hooks và Asymmetric Visibility (public private(set)) ở bài học kế tiếp.Hãy tự trả lời 4 câu hỏi tình huống dưới đây:
class Order { public ?string $note; } (không có = null) thì ngay sau $o = new Order(), biểu thức $o->note === null trả về true hay gây lỗi? -> Gây lỗi Error (Uninitialized property). Bạn phải viết public ?string $note = null; nếu muốn nó mặc định mang giá trị null.new ReleaseTag('api', 1)->label() có cần bọc thêm ngoặc đơn (new ...)->label() không? -> Không cần. PHP 8.4 hỗ trợ gọi chuỗi trực tiếp sau new ClassName(...).__construct(public int $age) { ... }, thân hàm { ... } chạy trước hay sau khi thuộc tính $this->age được gán giá trị? -> Chạy sau. Phép gán được trình biên dịch tự động chèn vào đầu hàm tạo.readonly class có cho phép thêm thuộc tính động ($obj->extra = 1) lúc chạy không? -> Không. Nó cấm hoàn toàn thuộc tính động và ném Error ngay lập tức.Bạn đã nắm vững cách đóng gói dữ liệu bằng Class, Constructor Promotion và readonly trong PHP 8.4. Hãy tiếp tục sang PHP EP 12: Property Hooks và Asymmetric Visibility trong PHP 8.4 để làm chủ hai tính năng hướng đối tượng đột phá nhất của PHP 8.4.
Ôn bài trước: PHP EP 10: Mảng PHP và các hàm array_find, array_any, array_all. Học tiếp: PHP EP 12: Property Hooks và Asymmetric Visibility trong PHP 8.4.

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.