Học Laravel: Service Container

I. Lời mở đầu

"Em làm với Laravel được khá nhiều rồi, nhưng sao càng đọc càng không hiểu..." - Câu này mình nghe nhiều rồi =)). Cách bạn bắt đầu học một ngôn ngữ ảnh hưởng rất nhiều tới bước tiến của các bạn sau này.

Học Laravel là series bài viết mình viết dành cho các bạn bắt đầu học Laravel. Với mục đích là đặt mình ở vị trí một beginner, làm rõ sự kì diệu và tuyệt vời của Laravel. Khi thấy được vẻ đẹp và sức mạnh thực sự của Framework này, mong rằng các bạn sẽ có hứng thú tìm hiểu sâu hơn vào core của nó.

Về Laravel có lẽ mình không cần giải thích gì nhiều hơn nữa. Chỉ cần bảng thống kê độ phổ biến của các PHP Web Application Framework Popularity at Work dưới đây là đủ.

php_framework_popularity_at_work_-_sitepoint2c_2015.png

Trở lại với chủ đề chính ngày hôm nay là Service Container. Trong Laravel Document 5.2 có câu:

A deep understanding of the Laravel service container is essential to building a powerful, large application, as well as for contributing to the Laravel core itself.

Sự hiểu biết sâu về Laravel service container là điều kiện cốt yếu để xây dựng nên một Application lớn và mạnh mẽ cũng như để đóng góp xây dựng core của Laravel.

Nghe quan trọng là thế, nhưng nếu các bạn tìm kiếm trên Laravel Documentation 4.2 sẽ không thấy khái niệm đó. Vậy có phải là từ Laravel 5 mới có Service Container hay không? Câu trả lời là không nhé. Thực chất Service Container đã có từ trước đó, với một cái tên khác là IoC Container. Vậy IoC là cái gì? (?)

Nào, chúng ta hãy bắt đầu tìm hiểu nhé!

II. IoC và DI

IoC là viết tắt của Inversion of Control. DI là viết tắt của Dependencty Injection. Hai khái niệm này thực sự rất dễ khiến các lập trình viên nhầm lẫn. Các bạn hãy nhìn vào sơ đồ sau để thấy mối quan hệ giữa 2 khái niệm.

ioc-and-mapper-in-c-6-638.jpg

Khái niệm cao nhất là Dependency Inversion: Đây là một nguyên lý để thiết kế và viết code.

Inversion of Control: Đây là một design pattern được tạo ra để code có thể tuân thủ nguyên lý Dependency Inversion. Có nhiều cách hiện thực pattern này: ServiceLocator, Event, Delegate, … Dependency Injection là một trong các cách đó.

Dependency Injection: Đây là một cách để hiện thực Inversion of Control Pattern (Có thể coi nó là một design pattern riêng cũng được). Các module phụ thuộc (dependency) sẽ được inject vào module cấp cao.

DI là một cách để thể hiện IoC nên mình sẽ lấy một ví dụ sử dụng DI, đó cũng sẽ là ví dụ cho IoC luôn. (

//Cách viết thông thường
// class Engine {}
class Car
{
    protected $engine;
    public function __construct()
    {
        $this->engine = new Engine();
    }
}
$car = new Car();

// Cách viết sử dụng DI
// class Engine {}
class Car
{
    protected $engine;
    public function __construct($engine)
    {
        $this->engine = $engine;
    }
}
$car = new Car(new Engine());

Như các bạn thấy ở đây, tư tưởng chung của IoC sẽ là làm "đảo ngược" flow của những xử lý truyền thống. Trong cách viết thông thường, đoạn xử lý logic của ta sẽ gọi đến những class, library (Engine) mà nó cần dùng. Với IoC, ta gửi cho những đoạn xử lý logic đó những thứ mà nó cần. Ở đây với DI thì cụ thể là ta "tiêm" instance $engine của class Engine vào constructor của Car.

Cụ thể hơn một chút nữa đi! Ở Laravel 4.2 trở về trước thì Laravel hiện thực hoá tư tưởng IoC trong code như thế nào? Dưới đây sẽ là một ví dụ để làm rõ điều đó.

// Bước 1: Đăng ký với IoC Container
IoC::register('car', function() {
    $engine = new Engine();
    $car = new Car($engine);
    return $car;
});

// Bước 2: Tạo ra một instance để sử dụng
$car = IoC::resolve('car');

Như các bạn thấy, so với ví dụ trước phải tạo instance bằng từ khoá new và inject một instance của class Engine() vào; thì với Laravel, ta chỉ cần gọi hàm resolve, thậm chí còn chả cần quan tâm xem car cần inject cái gì để khởi tạo. (baiphuc)

Laravel đã làm điều đó bằng cách nào? Hãy cùng tôi đi tới với Service Container!

III. Service Container

3.1 Bind và Resolve

Bind: Là thao tác đăng ký một class hay interface với Container (vật chứa).

Resolve: Là thao tác lấy ra một instance từ trong Container.

Ở trên mình đã đưa ra một ví dụ cho bind và resolve với IoC, dưới đây sẽ là ví dụ với Service Container.

// Bind
$this->app->bind('car', function() {
    $engine = new Engine();
    $car = new Car($engine);
    return $car;
}

// Resolve
// Cách 1
$this->app->make('car');
// Cách 2
$this->app()['car'];

Có nhiều cách để bind và cũng có nhiều cách để resolve. Sau đây chúng ta sẽ điểm qua một lượt.

3.2 Binding

3.2.1 Singleton Binding

Singleton binding được sử dụng khi bạn chỉ muốn class hay interface mình đăng ký với Container được resolve 1 lần duy nhất. Những lần gọi tiếp theo sẽ chỉ trả về instance đã được resolve trước đó.

$this->app->singleton('Car', function ($app) {
    return new Car($app['engine']);
});

3.2.2 Instances Binding

Đơn giản là bạn đã có sẵn instance, bạn bind nó vào Container và mỗi lần resolve bạn sẽ nhận lại được chính instance đó.

$car = new Car(new Engind());

$this->app->instance('Car', $car);

3.2.3 Interface Binding

Đây có lẽ là một trong những feature mạnh mẽ nhất mà Service Container mang lại. Giả sử bạn có EventPusher interfacevà một implementation của nó là RedisEventPusher. Khi đó bạn có thể register với container như sau.

$this->app->bind('App\Contracts\EventPusher', 'App\Services\RedisEventPusher');

Service Container sẽ hiểu rằng, mỗi khi người dùng type-hint EventPusher vào contructor của một class (hay bất kì một nơi nào khác) thì nó phải tự động inject RedisEventPusher vào.

use App\Contracts\EventPusher;

public function __construct(EventPusher $pusher) // Ở đây Service Container sẽ "tiêm" instance của RedisEventPusher vào
{
    $this->pusher = $pusher;
}

Lý do mà mình cho rằng đó là một trong những feature mạnh mẽ nhất của Service Container là bởi vì với cách làm này ta có thể dễ dàng thay đổi Implementation mà mình mong muốn. Giả sử bây giờ bạn chán, hay RedisEventPusher không còn đáp ứng được yêu cầu Khách hàng đặt ra nữa, mà bây giờ phải dùng một Implementation mới là NewEventPusher chẳn hạn. Nếu mà bạn tiêm trực tiếp RedisEventPusher từ trước theo cách làm thông thường thì nguy rồi, class của bạn hoàn toàn phục thuộc vào RedisEventPusher; việc sửa code vừa dài lại vô cùng phức tạp, dễ dẫn đến nhầm lẫn. Nhưng trong trường hợp này vì bạn type-hint Interface vào rồi nên công việc thật đơn giản, bạn chỉ cần register Implementations mới vào Interface là xong. =))

$this->app->bind('App\Contracts\EventPusher', 'App\Services\NewEventPusher');

3.2.4 Contextual Binding

Là binding theo bối cảnh, khi bạn có 2 class dùng cùng Interface, nhưng bạn lại muốn mỗi class được inject một implementation của Interface đó. Với Service Container, đơn giản là bạn trỏ thẳng trường hợp nào bạn muốn dùng implementation nào.

$this->app->when('App\Handlers\Commands\CreateOrderHandler')
          ->needs('App\Contracts\EventPusher')
          ->give('App\Services\PubNubEventPusher');

3.3 Resolving

Có 2 cách chính để resolve

3.3.1 Dùng method make

$car = $this->app->make('Car');

3.3.2 Access vào Container như access vào một array

$car = $this->app['Car'];

IV. Service Container mạnh cỡ nào?

Đến với mục IV này có lẽ các bạn cũng thấy là Service Container khá là mạnh rồi. Nhưng thực ra nó còn mạnh thế rất nhiều!

Hãy cùng nhau xem ví dụ dưới đây.

class Car
{
    protected $engine;
    protected $wheel;
    public function __construct(Engine $engine, Wheel $wheel)
    {
        $this->engine = $engine;
        $this->wheel = $wheel;
    }
}
// Cách 1
$this->app()->bind('car', 'Car');
$car = $this->app('car');
// Cách 2 thậm chí còn ngắn gọn hơn
$car = app('Car');

Không phải truyền callback vào hàm bind, thậm chí cách 2 còn chả cần bind, chả cần inject bất cứ một cái gì. =))

Tại sao Service Container vẫn có thể hiểu được?

Bởi vì Service Container của Laravel sẽ tự động resolve tất cả các Controller cho chúng ta. Không chỉ có Controller mà hầu hết các thành phần quan trọng khác của project như Event Listeners, Queue Jobs, Middleware... cũng sẽ được Container tự động resolve và inject các dependency cần thiết vào. Nói chung là các bạn rất là nhàn khi thực hiện DI (Dependency Injection). Tưởng tượng nếu Container không làm điều ấy cho bạn thì sao? Để tạo instance của Car, mình phải có instance của Engine, giả sử mà Engine lại cần thêm 2 cái instance gì gì đấy, thêm mấy cấp nữa chắc là "vỡ mồm". Còn với Service Container, bạn thậm chí còn chả cần nhớ Car nó cần những dependency gì =))

V. Người hùng thầm lặng

Từ đầu tới giờ, có lẽ các bạn đã thấy được sức mạnh thực sự của Service Container.

Nhưng một thắc mắc được giải đáp lại sẽ khiến các bạn nảy sinh thêm một thắc mắc khác. Vậy điều gì khiến cho những chức năng kì diệu mà Service Container mang lại trở thành hiện thực?

Vâng, đó chính là PHP Reflection API. Để biết PHP Reflection API là gì mời các bạn đọc tài liệu cùng link, có thể mình sẽ viết về nó trong một ngày đẹp trời gần đây. Hôm nay mình sẽ chỉ lấy ví dụ về hoạt động của nó để các bạn hiểu được là nó đã làm gì cho Service Container.

Quay trở lại với ví dụ class Car ở trên.

class Car
{
    protected $engine;
    protected $wheel;
    public function __construct(Engine $engine, Wheel $wheel)
    {
        $this->engine = $engine;
        $this->wheel = $wheel;
    }
}

$reflection = new ReflectionClass('Car');

Bây giờ ta đã có thể dùng instance $reflection để lấy toàn bộ thông tin của Class Car.

// Lấy ra tên của class
echo $reflection->getName();
// Car

// Lấy ra 2 parameters của constructor
echo $reflection->getConstructor()->getParameters()[0]->getClass()->getName();
// Engine
echo $reflection->getConstructor()->getParameters()[1]->getClass()->getName();
// Wheel

// Thậm chí ta có thể lôi ra cả constructor
var_dump($reflection->getConstructor());
// object(ReflectionMethod)#2 (2) { ["name"]=> string(11) "__construct" ["class"]=> string(3) "Car" }

Khi bạn đã có thể lấy tất cả thông tin của một class ra như vậy thì có gì khó để tạo một instance? =))

Lời kết

Laravel thực sự rất "ngon". Hãy học và tận hưởng những điều thú vị mà Framework tuyệt vời này mang lại! (love)

Tài liệu tham khảo

https://laravel.com/docs/5.2/container

http://php.net/manual/en/book.reflection.php