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

PHP EP 14: Namespace, PSR-4 và Composer trong PHP

PHP EP 14: Namespace, PSR-4 và Composer trong PHP

Tổ chức mã nguồn PHP theo Namespace và quy chuẩn PSR-4, thực hành cấu hình composer.json, tạo bảng ánh xạ autoload và xử lý lỗi sai lệch tên tệp trên hệ thống.

Tổ chức dự án nhiều tệp bằng autoload

Trong 13 bài học đầu tiên, chúng ta đặt toàn bộ lớp và hàm vào chung một tệp kịch bản đơn lẻ để tiện kiểm chứng cơ chế. Tuy nhiên, một ứng dụng thực tế thường có hàng trăm lớp đối tượng. Nếu tổ chức dự án bằng cách đặt hàng chục lệnh require_once '../../models/User.php'; ở đầu mỗi tệp, dự án sẽ nhanh chóng vấp phải ba điểm nghẽn: đường dẫn tương đối bị gãy ngay khi di chuyển tệp; đụng độ tên lớp (Cannot declare class User) khi tích hợp thư viện bên thứ ba cũng có lớp User; và lãng phí bộ nhớ khi phải nạp trước hàng trăm tệp dù request hiện tại chỉ dùng vài lớp.

Bài học khép lại Chặng 3 này hướng dẫn bạn giải quyết triệt để cả ba vấn đề trên bằng không gian tên (namespace), cơ chế nạp lớp tự động theo yêu cầu (spl_autoload_register) tuân thủ chuẩn PSR-4, và công cụ quản lý gói Composer 2.10. Bạn sẽ hiểu rõ ánh xạ 1-1 giữa thư mục vật lý với tên lớp đầy đủ (FQCN) cũng như sự khác biệt giữa composer install và composer update. Bài viết nối tiếp PHP EP 13: Interface, Trait và Enum trong PHP và mở đường cho Chặng 4 tại PHP EP 15: Xử lý ngoại lệ PHP với Throwable và try/catch. Tài liệu tham chiếu gồm PHP Manual: Namespaces, đặc tả PHP-FIG PSR-4 và Composer Documentation.

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) và Composer version 2.10.3 trên Alpine Linux 3.21 (aarch64).
  • Giới hạn kiểm chứng: Kiểm chứng cấu hình composer.json, sinh bộ nạp vendor/autoload.php theo chuẩn PSR-4 hoàn toàn ngoại tuyến (--network none) và kiểm tra phân biệt chữ hoa/thường trên hệ thống tệp Linux; không tuyên bố kiểm chứng tải gói từ kho Packagist qua Internet.

Hợp đồng của Namespace, PSR-4 và Composer

Để vận hành chuẩn xác hệ thống nạp lớp tự động và quản lý gói trong dự án PHP 8.4, bảng dưới đây tổng hợp hợp đồng kỹ thuật của từng thành phần:

Thành phần / Lệnh Cú pháp / Cấu hình chuẩn Hành vi thực tế trong PHP & Composer Điều kiện / Không nên giả định
Khai báo namespace & use namespace App\Support;
use App\Domain\Ticket;
Biên dịch tại thời điểm phân tích tệp: ghép tiền tố namespace vào tên lớp để tạo FQCN (như App\Support\Slugger). Lệnh use chỉ là bí danh rút gọn tên lớp trong tệp hiện tại, hoàn toàn không nạp tệp vật lý vào bộ nhớ.
Ánh xạ PSR-4 trong composer.json "psr-4": { "App\\": "src/" } Thay tiền tố App\ bằng thư mục src/, đổi dấu \ thành / và nối đuôi .php (App\Support\Slugger -> src/Support/Slugger.php). Tên tệp và thư mục phải khớp chính xác 100% chữ hoa/chữ thường với tên lớp và namespace trên hệ điều hành Linux.
composer dump-autoload -o Chạy khi thêm/đổi cấu hình autoload hoặc khi đóng gói Production (`-o`) Sinh ra tệp vendor/autoload.php và quét thư mục để tạo bảng tra cứu tĩnh autoload_classmap.php tăng tốc độ nạp. Nếu bật cờ --classmap-authoritative, lớp mới tạo thêm mà chưa chạy lại dump-autoload sẽ không được tìm thấy.
composer install so với composer update composer install --no-dev --optimize-autoloader (Production) install cài chính xác phiên bản đã khóa trong composer.lock; update tính lại phiên bản mới nhất và ghi đè composer.lock. Tuyệt đối không chạy composer update trên máy chủ Production vì có thể kéo bản cập nhật phụ gây lỗi bất ngờ.

Tên lớp được ánh xạ đến tệp như thế nào

Trong kiến trúc Zend Engine (PHP Manual: Aliasing/Importing), từ khóa namespace và use chỉ hoạt động ở bước biên dịch văn bản: khi thấy new Slugger(), trình biên dịch tra bảng use ở đầu tệp để biết tên đầy đủ (FQCN) là App\Support\Slugger. Tại dòng lệnh use, chưa hề có bất kỳ thao tác đọc ổ đĩa nào diễn ra.

Chỉ đến khi dòng thực thi new App\Support\Slugger() chạy (runtime) và Zend Engine thấy lớp chưa có trong bộ nhớ, PHP mới gọi các hàm callback đã đăng ký qua spl_autoload_register(). Bộ nạp của Composer (vendor/autoload.php) thực thi quy tắc PSR-4:

  1. Kiểm tra chuỗi App\Support\Slugger khớp với tiền tố "App\\": "src/" trong composer.json.
  2. Cắt bỏ tiền tố App\, đổi phần còn lại Support\Slugger thành đường dẫn src/Support/Slugger.php.
  3. Nạp tệp đó đúng một lần ngay tại thời điểm lớp được cần đến (lazy loading).
Luồng phân giải tên lớp đầy đủ (FQCN) thành đường dẫn tệp vật lý qua chuẩn PSR-4.
Hình 1. Luồng phân giải tên lớp đầy đủ (FQCN) thành đường dẫn tệp vật lý qua chuẩn PSR-4. 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.

Tạo dự án PSR-4 và sinh Composer autoload

Chúng ta sẽ tạo một dự án PHP chuẩn mực: viết composer.json khai báo ánh xạ PSR-4 "App\\": "src/", tạo lớp App\Support\Slugger tại src/Support/Slugger.php, chạy composer dump-autoload và gọi lớp qua vendor/autoload.php.

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 dự án mẫu trong /tmp.

WORKDIR="$(mktemp -d /tmp/php-ep14-XXXXXX)"
cd "$WORKDIR"
mkdir -p "$WORKDIR/src/Support"
pwd

Bước 2: Tạo cấu hình composer.json và lớp src/Support/Slugger.php chuẩn PSR-4

Mục đích: Thiết lập ánh xạ tiền tố App\ vào src/ và định nghĩa lớp Slugger thuộc namespace App\Support.

cat <<'EOF' > "$WORKDIR/composer.json"
{
    "name": "thanhnh/php-ep14-lab",
    "type": "project",
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}
EOF

cat <<'EOF' > "$WORKDIR/src/Support/Slugger.php"
<?php
declare(strict_types=1);

namespace App\Support;

final class Slugger
{
    public function make(string $text): string
    {
        $lower = strtolower(trim($text));
        return trim((string) preg_replace('/[^a-z0-9]+/', '-', $lower), '-');
    }
}
EOF

Bước 3: Sinh bộ nạp tự động bằng Composer và chạy kịch bản run.php

Mục đích: Chạy composer dump-autoload --optimize ngoại tuyến để tạo vendor/autoload.php và kiểm chứng nạp lớp tự động.

composer dump-autoload --working-dir="$WORKDIR" --optimize --quiet

cat <<'EOF' > "$WORKDIR/run.php"
<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use App\Support\Slugger;

echo "Slug=" . new Slugger()->make('PHP EP 14 Composer') . "\n";
EOF
php "$WORKDIR/run.php"

Kết quả quan sát được:

  • Đầu ra hiển thị: Slug=php-ep-14-composer. Chỉ cần đúng một dòng require __DIR__ . '/vendor/autoload.php'; duy nhất ở điểm vào ứng dụng.

Cảnh báo an toàn: Bên trong tệp lớp chuẩn PSR-4 (src/Support/Slugger.php), chỉ được định nghĩa lớp/interface/trait/enum chứ không được đặt các câu lệnh tạo tác dụng phụ (như echo hay kết nối DB ngoài lớp).

Tối ưu Classmap và tách môi trường phát triển

Khi chạy với cờ --optimize (-o), Composer quét toàn bộ thư mục src/ để biên dịch sẵn mảng kết hợp tĩnh trong vendor/composer/autoload_classmap.php ánh xạ trực tiếp tên lớp tới đường dẫn tệp, giúp Production không tốn thao tác kiểm tra file_exists() trên đĩa.

Bước 3b: Kiểm tra tệp autoload_classmap.php do Composer sinh ra

Mục đích: Xác nhận lớp App\Support\Slugger đã nằm trong bảng tra cứu tĩnh.

grep "App\\\\\\\\Support\\\\\\\\Slugger" "$WORKDIR/vendor/composer/autoload_classmap.php"

Kết quả quan sát được:

  • Đầu ra hiển thị dòng ánh xạ trực tiếp từ 'App\\Support\\Slugger' tới /src/Support/Slugger.php.

Kiểm tra lỗi chữ hoa, chữ thường và dọn dẹp

Một sự cố kinh điển là viết mã trên macOS/Windows (hệ thống tệp mặc định không phân biệt chữ hoa/thường), đặt tên tệp là slugger.php (chữ s thường) cho lớp Slugger (chữ S hoa). Khi đưa lên máy chủ hoặc container Linux (phân biệt tuyệt đối chữ hoa/thường), ứng dụng lập tức sập.

Bước 4: Đổi tên tệp thành chữ thường slugger.php trong container Linux và kiểm tra lỗi

Mục đích: Chứng minh sự lệch pha chữ hoa/thường giữa tên lớp Slugger và tệp slugger.php làm gãy cơ chế nạp PSR-4 trên Linux.

mv "$WORKDIR/src/Support/Slugger.php" "$WORKDIR/src/Support/slugger.php"
composer dump-autoload --working-dir="$WORKDIR" --quiet

set +e
php "$WORKDIR/run.php"
RC=$?
set -e
echo "RC=$RC"

Kết quả quan sát được:

  • Tiến trình dừng với RC=255 kèm thông báo: Fatal error: Uncaught Error: Class "App\Support\Slugger" not found in /tmp/php-ep14-.../run.php:8.
Ranh giới giữa khai báo phụ thuộc trong composer.json và khóa phiên bản composer.lock.
Hình 2. Ranh giới giữa khai báo phụ thuộc trong composer.json và khóa phiên bản composer.lock. 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 dự án thử nghiệm trong /tmp.

rm -rf "$WORKDIR"

Quản lý composer.lock và phiên bản phụ thuộc

Khi làm việc với Composer trong quy trình CI/CD thực tế, hãy tuân thủ 3 nguyên tắc:

  1. Đưa cả composer.json và composer.lock vào Git: composer.lock ghi lại chính xác phiên bản từng gói đã kiểm thử, đảm bảo mọi máy chủ chạy composer install đều nhận bộ thư viện giống hệt nhau.
  2. Tách biệt require và require-dev: Trên Production, luôn chạy composer install --no-dev --prefer-dist --optimize-autoloader.
  3. Gọi lớp ở Global Namespace: Khi đứng trong namespace App\Support;, hãy thêm use DateTimeImmutable; hoặc viết \DateTimeImmutable có dấu \ ở đầu.

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

Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:

  • Câu hỏi 1: Câu lệnh use App\Support\Slugger; ở đầu tệp có đọc và nạp tệp Slugger.php từ ổ đĩa ngay tại dòng đó không? -> Không. use chỉ tạo bí danh tên lớp lúc biên dịch; tệp chỉ được nạp khi chạm tới lệnh đầu tiên dùng lớp Slugger lúc chạy.
  • Câu hỏi 2: Với cấu hình PSR-4 "App\\": "src/", lớp App\Billing\Gateway\StripeClient phải đặt tại đường dẫn nào? -> Tại src/Billing/Gateway/StripeClient.php (khớp chính xác chữ hoa/thường).
  • Câu hỏi 3: Vì sao khi triển khai lên Production phải chạy composer install --no-dev chứ không chạy composer update? -> Vì composer update sẽ bỏ qua phiên bản đã khóa trong composer.lock để tải bản mới nhất, có thể gây lỗi chưa từng kiểm thử.
  • *Câu hỏi 4: Khi đang ở trong namespace App\Service;, gọi new DateTimeImmutable() không có dấu \ ở đầu và chưa khai báo use thì điều gì xảy ra?* -> Báo lỗi Class "App\Service\DateTimeImmutable" not found, vì tên lớp không tự động lùi về Global Namespace như hàm.

Khép lại Chặng 3, bạn đã hoàn thiện kỹ năng OOP và cấu trúc dự án chuẩn PSR-4. Hãy bước sang Chặng 4 tại PHP EP 15: Xử lý ngoại lệ PHP với Throwable và try/catch để làm chủ kiến trúc xử lý lỗi của PHP 8.4.

Ôn bài trước: PHP EP 13: Interface, Trait và Enum trong PHP. Học tiếp: PHP EP 15: Xử lý ngoại lệ PHP với Throwable và try/catch.

Bài liên quan trong bộ PHP

Tìm hiểu thêm