Hướng dẫn dùng php di trong PHP

Mô hình lập trình DI - Dependency Injection trong PHP

Dependency Injection là một mô hình lập trình, cách tổ chức code sao cho các đoạn code khác nhau, các module khác nhau, các lớp khác nhau không phụ thuộc nhau một cách cứng nhắc, mà cần có một cơ chế thay đổi các thành phần phụ thuộc cả ở thời điểm chạy và thời điểm biên dịch. Ví dụ dưới đây trình bày với lập trình PHP.

dependency : Giả sử bạn có một lớp classA, lớp này có sử dụng một chức năng từ đối tượng lớp classB (classA hoạt động dựa vào classB). Lúc đó classB gọi là phụ thuộc (dependency) (của classA)

Bằng cách sử dụng mô hình Dependency Injection ta dễ dàng bảo trì code và module hóa ứng dụng. Tất cả các project đều có các thành phần phụ thuộc vào nhau, dự án càng lớn thì càng nhiều thành phần phụ thuộc, thì cơ chế DI giúp cho quản lý các thành phần phụ thuộc này tốt nhất.

Giờ bạn tạo ra 2 lớp mà chúng không sử dụng cơ chế DI, sau đó viết lại có sử dụng DI để xem sự khác biệt:

Lớp thứ nhất là StockItem biểu diễn mặt hàng trong kho (số lượng, tình trạng). Lớp thứ 2 biểu diễn mặt hàng bán trên trang web, liên quan đến mặt hàng lưu trữ trong kho.

class StockItem {

    private $quantity;
    private $status;

    public function __construct($quantity, $status){
        $this->quantity = $quantity;
        $this->status   = $status;
    }

    public function getQuantity(){
        return $this->quantity;
    }

    public function getStatus(){
        return $this->status;
    }

}

Lớp product trình bày như sau:

class Product {
    private $stockItem;
    private $code;

    public function __construct($code, $stockQuantity, $stockStatus){
        $this->stockItem  = new StockItem($stockQuantity, $stockStatus);
        $this->code        = $code;
    }

    public function getStockItem(){
        return $this->stockItem;
    }

    public function getCode(){
        return $this->code;
    }
}
$product = new Product("101010", 50, "Áo Dài");
var_dump($product->getStockItem());

Code trên tạo ra lớp Product, khi khởi tạo Product thì cũng khởi tạo đối tượng StockItem trong hàm tạo của lớp Product bằng cách truyền các tham số $stockQuantity, $stockStatus

Với cách sử dụng code như trên, đó là đoạn code bình thường không có vấn đề gì về logic, nhiều khi đánh giá là đoạn code tốt. Tuy nhiên khi vận hành, bảo trì, mở rộng có thể phát sinh một số vấn đề:

  • StockItemProduct là một cặp cố định, vậy thì một lúc nào đó StockItem thay đổi tham số khởi tạo (thêm, bớt) thì sao? Vậy bạn cần phải viết lại hàm khởi tạo trong Product cũng như tất cả các lớp có sử dụng phiên bản cũ của StrockItem
  • Product biết quá nhiều, ở đây là số lượng và trạng thái của sản phẩm trong kho. Điều này giảm đi tính độc lập, đóng kín của lập trình hướng đối tượng.
  • Khi viết unit test cho code trên, vì Product khởi tạo stockItem trong hàm tạo, nên không thể tạo Unit test cho Product mà không tạo Unit test cho stockItem

Viết lại với Dependency Injection

Tiêm vào đối tượng các thành phần phụ thuộc cần thiết:

class Product {
    private $stockItem;
    private $code;

    public function __construct($code, StockItem $stockItem){
        $this->stockItem   = $stockItem;
        $this->code        = $code;
    }

    public function getStockItem(){
        return $this->stockItem;
    }

    public function getCode(){
        return $this->code;
    }
}

$stockItem = new StockItem(50, "Áo Dài");
$product = new Product("101010", $stockItem);
var_dump($product->getStockItem());

Với cách viết thứ 2 này, đối tượng StockItem không còn khởi tạo bên trong hàm tạo Product nữa, mà nó được truyền vào (tiêm) Product thông qua chính đối tượng StockItem, như vậy khi thay đổi cách khởi tạo StockItem thì lớp Product không phải thay đổi gì. Đó chính là khái niệm Dependency Injection

Các kiểu Dependency Injection

Việc đưa đối tượng phụ thuộc vào một đối tượng khác (tiêm - inject) được thực hiện qua mấy cách sau:

Constructor Injection - Inject qua hàm tạo

Ở ví dụ trên chính là sử dụng kiểu Constructor Injection, với cách này có một số đặc điểm

  • Khi một lớp phụ thuộc cần một lớp triển khai để hoạt động thì sẽ sử dụng cách này để đảm bảo lớp đó được cung cấp cho đối tượng phụ thuộc.
  • Do truyền đối tượng qua hàm tạo, nên cần đảm bảo thành phần phụ thuộc này không bị thay thế trong suốt quá trình tồn tại của đối tượng
  • Sử dụng cách này khi kế thừa các lớp việc xử lý hàm tạo khá phức tạp

Setter Injection - Inject thông qua hàm setter

Thành phần phụ thuộc được tiêm (truyền) vào đối tượng thông qua hàm setter. Ví dụ:

class Product {
    private $stockItem;
    private $code;

    public function __construct($code){
        $this->code        = $code;
    }

    public function getStockItem(){
        return $this->stockItem;
    }

    public function getCode(){
        return $this->code;
    }

    public function setStockItem(StockItem $stockItem){
        $this->stockItem = $stockItem;
    }
}

$stockItem = new StockItem(50, "Áo Dài");
$product = new Product("101010");
$product->setStockItem($stockItem);
var_dump($product->getStockItem());

Như vậy đối tượng phụ thuộc vào $stockItem được cài vào Product thông qua một hàm setter: setStockItem($stockItem). Với cách này:

  • Cho phép tùy chọn các phụ thuộc và lớp có thể tạo ra với các giá trị mặc định
  • Thêm các thành phần phụ thuộc một cách đơn giản thông qua hàm setter mà không làm hỏng logic của code.

Interface Injection - Inject qua giao diện lớp

Với cách này định nghĩa một giao diện sao cho các thành phần phụ thuộc được tích hợp vào mã triển khai giao diện:

interface ProductInterface {
    public function getStockItem();
    public function setStockItem(StockItem $stockItem);

Khi triển khai giao diện ProductInterface cần cung cấp StockItem thông qua định nghĩa hàm của giao diện

Dependency Injection Container - Trình chứa DI

Trên đây là toàn bộ cách inject một đối tượng này vào đối tượng khác sao cho chúng không phụ thuộc vào nhau cứng nhắc, với một đối tượng có một vài đối tượng phụ thuộc vấn đề sẽ không có gì, tuy nhiên khi lượng đối tượng phụ thuộc là lớn thì lại trở lên rất phức tạp để quản lý nó.

Lúc này giải pháp đưa ra cần tạo một trình chứa chuyên quản lý tất cả các đối tượng độc lập, từ autoload, khởi tạo, thiết lập và inject vào đối tượng khác. Đó là Dependency Injection Container (DI Container), chương trình quản lý - khởi tạo các đối tượng tập trung.

Trong PHP, công đồng lập trình thống nhất đưa ra một giao diện của DI Container, interface này được mô tả tại: psr-11 và code giao diện lưu trên github: php-fig/container. Nếu muốn phát triển DI Container đúng chuẩn, bạn nên triển khai từ giao diện trên. Các PHP Framework hiện nay có sử dụng DI hầu hết đều triển khai từ giao diện này.

Đây là danh sách các thư viện, framework PHP có triển khai DI Container trên: psr/container-implementation, bạn thấy có : lavarel, symfony, laminas (zend framework), joomla ...