Các chuẩn autoload trong PHP, ứng dụng autoload với Composer – Blog Web Hoàn Hảo

Như chúng ta đã biết, PSR (PHP Standards Recommendation) là chuẩn coding cho PHP được phát triển bởi FIG (Framework Interoperability Group), mặc dù không phải là chuẩn chính thức của PHP nhưng nó đã và đang được khuyến khích sử dụng. Nhiều thư viện, framework và các sản phẩm PHP lớn cũng đang áp dụng chuẩn này.

Ở thời điểm hiện tại (12/2017) thì các PSR sau đây đã được ban hành và đang dùng:

  • PSR-1: Basic Coding Standard (Tiêu chuẩn cơ bản khi viết code PHP)
  • PSR-2: Coding Style Guide (Tiêu chuẩn trình bày code PHP)
  • PSR-3: Logger Interface (Giao diện logger)
  • PSR-4: Autoloading Standard
  • PSR-6: Caching Interface (Giao diện về Caching)
  • PSR-7: HTTP Message Interface (Tiêu chuẩn Giao diện thông điệp HTTP)
  • PSR-11: Container Interface (Giao diện về trình chứa DIC)
  • PSR-13: Hypermedia Links (Tiêu chuẩn về biểu diễn liên kết)
  • PSR-16: Simple Cache (Tạo Cache đơn giản từ PSR-6)

Ngoài ra, còn có PSR-0Autoloading Standard đã bị loại bỏ.
Bài viết này sẽ nói về 2 chuẩn autoload là PSR-0 (đã bỏ) và PSR-4 (đang dùng).

PSR-0: chuẩn Autoload cũ

Trước đây trong PHP, việc nạp các file thư viện – mã dùng lại vào một file PHP khác thường dùng các lệnh includerequire. Điều này thực sự mất thời gian và rắc rối. Từ phiên bản PHP5 đã có các hàm trợ giúp tự động hóa việc gọi thư viện bằng các hàm như: spl_autoload_register, spl_autoload_functions, spl_autoload_extensions… Từ các hàm này cùng với khái niệm namespace trong PHP, các lập trình viên xây dựng cho mình một bộ code tự động nạp. Tuy nhiên, để dễ dàng chia sẻ – dùng lại code giữa các framework, giữa các dự án…, cộng đồng PHP thống nhất một cách thức tự động nạp thư viện theo một chuẩn bố trí thư viện. Việc thống nhất đó hình thành một tiêu chuẩn nên tuân theo đó là PSR-0.
PSR-0 là tiêu chuẩn autoload kiểu cũ được dùng cho tới 21-10-2014. Hiện nay, chuẩn PSR-0 không được chấp nhận rộng rãi, chỉ còn tồn tại trong một số framework sử dụng từ trước đó và khó có thể thay đổi. Chuẩn PSR-0 vì không còn phổ dụng nên chuẩn PSR-4 được đề nghị sử dụng như là một chuẩn thay thế – mạnh mẽ hơn.
Chuẩn PSR-0 yêu cầu bắt buộc phải tuân thủ các điều kiện để tương tác Autoloader như sau:

Nội dung

Một namespace và một Class phải có cấu trúc sau:

<VendorName>(<Namespace>)*<ClassName>
  • – Với mỗi namespace thì đều phải chứa namespace cấp cao nhất: (‘VendorName’).
  • – Các namespace có thể có nhiều sub-namespace tùy ý.
  • – Mỗi dấu ngăn cách namespace (dấu ) đều được convert thành DIRECTORY_SEPARATOR khi tương tác với file system.
  • – Kí tự _ trong tên class (Class Name) đều được convert thành DIRECTORY_SEPARATOR, và dấu _ không có nghĩa trong tên namespace (namespace không được có kí tự _).
  • – Một fully-qualified namespace kèm theo tên class phía sau sẽ được thêm đuôi (hậu tố) .php khi tương tác với file system.
  • – Một tên namespace hoặc tên class chỉ được chứa các kí tự alphabetic, bao gồm viết hoa và viết thường.

Ví dụ:

DoctrineCommonIsolatedClassLoader => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
SymfonyCoreRequest => /path/to/project/lib/vendor/Symfony/Core/Request.php
ZendAcl => /path/to/project/lib/vendor/Zend/Acl.php

Dấu gạch dưới trong tên Class sẽ biến thành dấu phân tách thư mục (DIRECTORY_SEPARATOR):
Ví dụ:

namespacepackageClass_Name => /path/to/project/lib/vendor/namespace/package/Class/Name.php
namespacepackage_nameClass_Name => /path/to/project/lib/vendor/namespace/package_name/Class/Name.php

Chú ý: PSR-0 tương thích với kiểu đặt tên class dạng PEAR

PSR-4: chuẩn Autoload đang được khuyên dùng

Chuẩn PSR-0 lúc mới ra đời đã giải quyết được vấn đề autoload tương đối hiệu quả. Tuy nhiên, khi công cụ quản lý gói PHP có tên là Composer xuất hiện thì mọi thứ đã thay đổi. Cấu trúc thư mục theo chuẩn PSR-0 trở nên lạc hậu vì nó có số lượng gấp đôi số thư mục cần thiết cho một dự án và cấu trúc thư mục được phân tầng càng sâu, càng phức tạp. Sau đây là ví dụ cấu trúc thư mục theo chuẩn PSR-0 được Composer tạo ra:

vendor/
    vendor_name/
        package_name/
            src/
                Vendor_Name/
                    Package_Name/
                        ClassName.php # Vendor_NamePackage_NameClassName
            tests/
                Vendor_Name/
                    Package_Name/
                        ClassNameTest.php # Vendor_NamePackage_NameClassNameTest

Nhìn vào cấu trúc thư mục trên ta thấy chúng lặp lại như hai thư mục “src” và “tests” phải lặp lại cấu trúc thư mục có cả tên vendor và tên package. Đây chỉ mới là một ví dụ đơn giản, nếu một dự án lớn thì vấn đề càng nghiêm trọng hơn. Chính vì vậy, PSR-4 tối ưu hơn đã ra đời vào năm 2014.
Nội dung của chuẩn PSR-4 như sau:
A/ Thuật ngữ “class” (lớp) dùng để chỉ các class, các interface, các trait và các cấu trúc tương tự khác.
B/ Fully-qualified class name của 1 class có dạng:

<NamespaceName>(<SubNamespaceNames>)*<ClassName>

1- Một fully-qualified class name PHẢI có một tên namespace gốc, được biết như là “vendor namespace”.
2- Một fully-qualified class name CÓ THỂ có một hoặc nhiều namespace con.
3- Một fully-qualified class name PHẢI được kết thúc bằng một tên lớp (class name).
4- Dấu gạch dưới (dấu _) không có ý nghĩa đặc biệt trong bất kỳ phần nào của fully qualified class name.
5- Các ký tự alphabet trong fully qualified class name có thể là chữ hoa hoặc chữ thường.
6- Các tên lớp phải phân biệt chữ hoa chữ thường (cùng tên nhưng chữ hoa và chữ thường khác nhau).
C/ Khi load một file tương ứng với một fully qualified class name …
1- Trong một fully qualified class name, một chuỗi liên tục của một hoặc nhiều leading namespace và sub-namespace, không bao gồm dấu phân cách của leading namespace (có thể gọi là “namespace prefix”) tương ứng với ít nhất 1 “thư mục cơ sở” (base directory).
2- Một loạt liên tiếp các sub-namespace sau “namespace prefix” tương ứng với 1 thư mục con trong “thư mục cơ sở” (base directory), mỗi namespace riêng biệt tương ứng với thư mục riêng biệt. Tên thư mục con PHẢI tuân theo cách đặt tên sub-namespace.
3- Tên class kết thúc tương ứng với tên file <ClassName>.php
D/ Mã để triển khai Autoloader PHẢI KHÔNG quăng ra exceptions, PHẢI KHÔNG gây ra lỗi ở bất kì level, và KHÔNG NÊN trả về giá trị.
Ví dụ:

FULLY QUALIFIED CLASS NAME
NAMESPACE PREFIX
BASE DIRECTORY
Đường dẫn thực

\Acme\Log\Writer\File_Writer
Acme\Log\Writer
./acme-log-writer/lib/
./acme-log-writer/lib/File_Writer.php

\Aura\Web\Response\Status
Aura\Web
/path/to/aura-web/src/
/path/to/aura-web/src/Response/Status.php

\Symfony\Core\Request
Symfony\Core
./vendor/Symfony/Core/
./vendor/Symfony/Core/Request.php

\Zend\Acl
Zend
/usr/includes/Zend/
/usr/includes/Zend/Acl.php

Tuy nhiên, mỗi phương pháp autoload mang một số ưu và khuyết điểm: PSR-4 sẽ cho phép các cấu trúc thư mục đơn giản hơn, nhưng sẽ gây khó khăn chúng ta biết đường dẫn chính xác của một class khi chỉ bằng cách nhìn trực quan vào fully qualified name. PSR-0 thì tạo ra sự hỗn loạn trong vấn đề cấu trúc thư mục trên ổ cứng, nhưng lại hỗ trợ các lập trình viên bị kẹt trong các dự án cũ có dùng cách đặt tên gạch dưới. PSR-0 còn giúp chúng ta dễ dàng xác định vị trí của một class bằng cách nhìn vào fully qualified name của class. Chính vì vậy, PSR-0 vẫn tồn tại bên cạnh PSR-4 trong quá trình chuyển đổi sang chuẩn autoload mới, nhưng đối với các dự án mới sau khi PSR-4 ra đời thì được khuyên dùng PSR-4 vì nó tiên tiến hơn PSR-0. Chuẩn autoload cũ vẫn được hỗ trợ bởi các công cụ quản lý thư viện PHP như Composer, dành cho những trường hợp cần đến nó.

Sự khác nhau giữa PSR-4 và PSR-0

PSR-0PSR-4 khác nhau ở hai vấn đề chính sau đây:
1/ PSR-0 vẫn tương thích với cách đặt tên kiểu gạch dưới PEAR. Trong khi đó, PSR-4 loại bỏ hẳn, nó chỉ còn hỗ trợ namespace.
2/ PSR-0 buộc toàn bộ namespace là một cấu trúc thư mục. Trong khi đó, PSR-4 chỉ yêu cầu namespace là cấu trúc thư mục từ phần sau điểm neo.
Ví dụ: Nếu bạn xác định rằng namespace có tên AcmeFoo được neo trong src/
– Đối với PSR-0 có nghĩa là autoloader sẽ tìm AcmeFooBar trong src/Acme/Foo/Bar.php.
– Đối với PSR-4, autoloader sẽ tìm trong src/Bar.php.
Như vậy, PSR-4 cho phép cấu trúc thư mục ngắn hơn.
Ngoài ra, PSR-4 còn cho phép autoloader đăng ký sử dụng nhiều thư mục trong một namespace duy nhất. Điều này cũng đã góp phần làm đơn giản hóa cấu trúc thư mục.

Ứng dụng Composer để autoload

Composer hỗ trợ 4 cách autoload gồm: PSR-4, PSR-0, classmap, files. Trong đó, PSR-4 là cách được khuyến nghị vì nó dễ dàng sử dụng hơn (không cần phải tạo lại autoloader khi thêm class). Bạn sử dụng cấu trúc khai báo trong file composer.json để định nghĩa các cách autoload theo yêu cầu của dự án.

Sử dụng PSR-4 trong Composer

Bên trong từ khóa psr-4 bạn định nghĩa các namespace ánh xạ với các đường dẫn, vị trí tương đối so với thư mục gốc của package. Khi autoloading một class như Foo\Bar\Baz với namespace prefix là Foo\ trỏ vào thư mục src/ thì autoloader sẽ tìm một file có đường dẫn src/Bar/Baz.php và nạp nó nếu tồn tại. Chú ý, trường hợp này khác với dùng PSR-0 ở chổ: tiền tố Foo\ không xuất hiện trong đường dẫn file.
Các namespace prefix phải được kết thúc bằng 2 ký tự gạch chéo ngược (\) để tránh xung đột với các prefix gần giống với nó. Ví dụ như namespace Foo sẽ so khớp với các class trong namespace tên là FooBar, do đó dấu chéo ngược ở cuối sẽ giải quyết được vấn đề: Foo\FooBar\ là khác nhau.
Các tham chiếu PSR-4 (trong quá trình cài đặt/cập nhật) được tổng hợp vào một mảng theo dạng key => value và chứa trong file được Composer tạo ra có đường dẫn vendor/composer/autoload_psr4.php.

Ví dụ:

{
    "autoload": {
        "psr-4": {
            "Monolog\": "src/",
            "Vendor\Namespace\": ""
        }
    }
}

Nếu cần gán cùng một namespace prefix cho nhiều thư mục chứa các class thì bạn cần chỉ định chúng trong một mảng như sau:

{
    "autoload": {
        "psr-4": { "Monolog\": ["src/", "lib/"] }
    }
}

Nếu bạn muốn có một thư mục dự phòng cho bất kỳ namespace nào được tìm kiếm thì bạn có thể sử dụng namespace prefix rỗng như sau:

{
   "autoload": {
   "psr-4": { "": "src/" }
   }
}

Sử dụng PSR-0 trong Composer

Bên trong từ khóa psr-0 bạn định nghĩa các namespace ánh xạ với các đường dẫn, vị trí tương đối so với thư mục gốc của package. Chú ý, chuẩn này có hỗ trợ kiểu quy ước đặt tên không có namespace tên là PEAR.
Các namespace prefix phải được kết thúc bằng 2 ký tự gạch chéo ngược (\) để chắn chắn autoloader xử lý chính xác. Ví dụ như namespace Foo sẽ so khớp với các class trong namespace tên là FooBar, do đó dấu chéo ngược ở cuối sẽ giải quyết được vấn đề: Foo\FooBar\ là khác nhau.
Các tham chiếu PSR-0 (trong quá trình cài đặt/cập nhật) được tổng hợp vào một mảng theo dạng key => value và chứa trong file được Composer tạo ra có đường dẫn vendor/composer/autoload_namespaces.php.
Ví dụ:

{
    "autoload": {
        "psr-0": {
            "Monolog\": "src/",
            "Vendor\Namespace\": "src/",
            "Vendor_Namespace_": "src/"
        }
    }
}

Trong PSR-0 cũng hỗ trợ nhiều thư mục trong một namespace prefix, bạn có thể khai báo nó như một mảng:

{
    "autoload": {
        "psr-0": { "Monolog\": ["src/", "lib/"] }
    }
}

Kiểu PSR-0 không chỉ giới hạn ở các khai báo namespace mà còn có thể khai báo đến cấp class. Tính năng này hữu ích cho các thư viện chỉ với một class trong namespace toàn cục. Nếu file nguồn php cũng nằm trong thư mục gốc của gói, nó được khai báo như sau:

{
    "autoload": {
        "psr-0": { "UniqueGlobalClass": "" }
    }
}

Nếu bạn muốn có một thư mục dự phòng cho bất kỳ namespace nào được tìm kiếm thì bạn có thể sử dụng prefix rỗng như sau:

{
    "autoload": {
        "psr-0": { "": "src/" }
    }
}

Sử dụng Classmap trong Composer

Các tham chiếu classmap (trong quá trình cài đặt/cập nhật) được tổng hợp vào một mảng theo dạng key => value và chứa trong file được Composer tạo ra có đường dẫn vendor/composer/autoload_classmap.php. Map này được xây dựng bởi việc quét các class trong tất cả các file .php.inc trong các thư mục/file đã cho.
Bạn có thể sử dụng công cụ hỗ trợ tạo classmap để xác định autoloading cho tất cả các thư viện không theo chuẩn PSR-0/4. Cấu hình tính năng này cần chỉ định các thư mục hoặc file để tìm kiếm các class.
Ví dụ:

{
    "autoload": {
        "classmap": ["src/", "lib/", "Something.php"]
    }
}

Sử dụng Files trong Composer

Nếu bạn muốn luôn load sẵn một số file đã tồn tại một cách tường minh thì bạn có thể sử dụng cơ chế autoloading 'files'. Điều này rất hữu ích nếu package của bạn có các hàm PHP cần thiết nhưng không được autoload bởi PHP.
Ví dụ:

{
    "autoload": {
        "files": ["src/MyLibrary/functions.php"]
    }
}

Loại trừ các file khỏi classmap

Nếu bạn muốn loại trừ một số file hoặc folder khỏi classmap, bạn có thể sử dụng thuộc tính 'exclude-from-classmap'. Tính năng này hữu ích để loại trừ các class dùng để test sau khi đưa dự án vào chạy thật, ví dụ như các loại trừ này sẽ được bỏ qua khi build một optimized autoloader.
Trình tạo classmap sẽ bỏ qua tất cả các file trong các đường dẫn được cấu hình. Các đường dẫn là tuyệt đối từ thư mục gốc package (ví dụ: vị trí file composer.json) và ký hiệu * có thể khớp bất kỳ ký tự nào trừ dấu gạch chéo, còn ký hiệu ** đại diện cho bất kỳ ký tự nào. Ký hiệu ** được thêm không tường minh vào cuối đường dẫn.
Ví dụ:

{
    "autoload": {
        "exclude-from-classmap": ["/Tests/", "/test/", "/tests/"]
    }
}

Thực hiện optimizing autoloader

Autoloader có thể có tác động khá lớn đến thời gian chạy ứng dụng (50-100ms mỗi yêu cầu trong các framework lớn sử dụng nhiều class). Xem các bài viết về optimizing the autoloader để biết thêm chi tiết về làm thế nào để giảm tác động này.

Autoload ở chế độ phát triển ứng dụng (development)

Phần này cho phép định nghĩa các rule cho autoload nhằm mục đích phát triển ứng dụng.
Các class cần chạy bộ test không nên khai báo nó trong các luật của autoload chính để tránh chạy ngay khi dự án đã chạy chính thức và khi người khác sử dụng package của bạn như là thư viện.
Vì vậy, nên đưa đường dẫn dành riêng cho các unit test vào mục autoload-dev.
Ví dụ:

{
    "autoload": {
        "psr-4": { "MyLibrary\": "src/" }
    },
    "autoload-dev": {
        "psr-4": { "MyLibrary\Tests\": "tests/" }
    }
}

Kết luận

– Hãy sử dụng PSR-4 trong Composer để làm autoload: vừa dễ dùng, vừa hỗ trợ tự động tạo autoloader. Nếu muốn tăng tốc load thì dùng lệnh dump-autoload để chuyển đổi sang classmap.
– Nếu sử dụng PSR-4 thì lưu ý có namespace prefix trỏ vào một thư mục nào đó, phần tiếp theo của namespace chính là cấu trúc thư mục trong thư mục được trỏ vào.
– Có thể khai báo một namespace prefix vào nhiều thư mục khác nhau, vậy một namespace có thể có nhiều file .php ở trong nhiều thư mục khác nhau.
– Ký hiệu 2 dấu gạch chéo ngược \ trong namespace prefix thì một gạch chéo chính là dấu phân tách namespace trong fully-qualified class name, còn dấu gạch chéo còn lại là ký hiệu escape cho dấu gạch chéo kia trong file .json.