Đến hẹn lại lên, Hôm nay mình sẽ tiếp tục seri Laravel core. Đầu tiên phải khẳng định rằng Service container là 1 khái niệm cực kì quan trọng trong laravel, nó được xem là trung tâm của framework, là trái tim của Laravel. Service container được dùng ở khắp mọi nơi trong framework mà không phải ai cũng biết.
Ở các bài viết trước mình đã tào lao bí đao về các kiến thức quan trọng cần phải biết trong laravel, Nếu anh em nào chưa đọc có thể tham khảo:
- N+1 trong laravel – Nguyên nhân và cách khắc phục.
- Phân biệt Method và Properties trong laravel, 4 trường hợp không nên sử dụng Properties.
- Phân biệt Eloquent và Query Builder, Nên sử dụng cái nào?
- Dependency Injection là gì? 3 phương pháp DI thường gặp
- Repository Design Pattern là gì? 3 Bước tối ưu code trong pattern này.
Lịch sử ra đời của Service Container
Mình tình cờ biết được khái niệm Inversion of Control Container (IoC Container) trong laravel khi search IoC của spring. IoC xuất hiện trong version 4 của laravel. Hay nói cách khác tiền thân của Service container chính là Inversion of Control Container. Từ version 5 trở đi thì Inversion of Control Container được đổi hẳn sang cái tên Service container cho nó dễ nhớ :v.
Vậy đầu tiên hãy tìm hiểu Inversion of Control trước đã nhé, khái niệm này có lẽ sẽ quen thuộc với mấy anh em code spring hơn :v.
DI và Inversion of Control
Đố anh em Dependence và DI thằng nào sinh ra thằng nào đấy =)), nói thật là lúc đầu đọc khái niệm về Inversion of Control mình mếu hiểu cái đếch gì :v
Vì trên wikipedia nó định nghĩa Ioc như này:
In software engineering, inversion of control (IoC) is a design pattern in which custom-written portions of a computer program receive the flow of control from a generic framework. The term “inversion” is historical: a software architecture with this design “inverts” control as compared to procedural programming
Theo wikipedia.
Đệch mợ mình đang đọc cái méo gì thế này? Ngày ấy mình đã quen code với tư duy “code truyền thống” nên khi đọc đoạn trên mình không có hiểu. Mãi sau đọc tới khái niệm DI mình mới hiểu nó chút ít :v.
Hiểu đơn giản thì Ioc là một kỹ thuật trong lập trình làm “đảo ngược” luồng xử lý của phương pháp truyền thống. Bình thường thì đoạn xử lý logic của ta sẽ gọi đến những class, library mà nó cần dùng, thế nhưng với IoC thì ta sẽ gửi cho những đoạn xử lý logic đó những thứ mà nó cần =)) .
Nghe mông vãi chưởng =)) Có phải thông thường code tới logic nào đó cần tới class A thì lúc đó ta mới khởi tạo class A bằng lênh new A() đúng không? Nhưng với Ioc thì Class A đã được 1 thằng khác khởi tạo và quản lý từ trước rồi, khi code tới logic cần tới class A thì chỉ cần bảo thằng đó đưa class A cho mình và sử dụng thôi. Với IoC thì các dependence sẽ được inject vào các đối tượng tương ứng tại thời điểm run time thay vì compile time.
Nếu bạn chưa hiểu Dependence hay inject là gì thì đọc lại bài viết trước của mình tại đây nhé
Nếu vẫn chưa hiểu nữa thì mình cùng đi vào ví dụ nhé, vẫn bài cũ dùng lại Class Computer có 2 sự phụ thuộc là Keyboard và Monitor.
// Tư duy thông thường, khi code logic mà cần tới class Computer thì lúc đó ta mới khởi tạo chúng ...logic trước... $keyboard = new Keyboard(); $monitor = new Monitor(); $computer = new Computer($monitor, $keyboard); ...khi này mới bắt đầu sử dụng thằng $computer này... ------------------------------ // với phương pháp Ioc // Đầu tiên, ta đăng ký với IoC Container IoC::register('computer', function() { $keyboard = new Keyboard(); $monitor = new Monitor(); $computer = new Computer($monitor, $keyboard); return $computer; }); // tới đây thì class Computer đã được tạo ra rồi và được thằng Ioc quản lý trong Ioc Container // 1 tỉ logic được thực hiện ở dòng này ... // Khi cần tới thì chỉ việc lấy nó ra từ Ioc Container $computer = IoC::resolve('computer');
Như ta thấy thì khi cần khởi tạo một instance của class Computer, thay vì khởi tạo một cách truyền thống bằng từ khóa new rồi truyền các dependency thông qua constructor, thì với IoC ta sẽ lấy ra khỏi Container bằng hàm resolve (các xử lý tạo dependency cũng như đưa dependency vào trong class cần khởi tạo đều được thực hiện trong callback function mà ta đã đăng kí với IoC Container)
Với cách này thì ta không cần phải nhớ xem Computer nó cần những dependency gì mà vẫn có thể gọi nó ra ở bất cứ đâu. Khi có thay đổi về các dependency của class Computer ta chỉ cần sửa phần đăng ký với IoC Container là xong. Tiện quá trời quá đất luôn.
Laravel Service Container
Mặc dù từ phiên bản 5 đã đổi tên thành Service Container nhưng nhìn chung thì tư tưởng không thay đổi. Service Container vẫn là một nơi quản lý các class dependency và thực hiện inject chúng với nhau. Tuy nhiên nếu Service container chỉ có vậy thì chẳng có gì đáng để nói, các ngôn ngữ khác cũng làm được. Để hiểu hơn về sức mạnh của thằng này thì ta sẽ tìm hiểu thêm về cách bind và resolve của nó.
Bind và Resolve
Laravel sử dụng bind để chỉ việc đăng ký một class hay interface với Container, và resolve để lấy ra instance từ Container. Ta có một ví dụ về cách bind cũng như cách resolve như sau:
// Thực hiện đăng kí 1 class với Container bằng hàm bind()
\App::bind('computer', function() {
$keyboard = new Keyboard();
$monitor = new Monitor();
$computer = new Computer($monitor, $keyboard);
return $computer;
}
// Lấy 1 instance ra khỏi Container
\App::make('computer');
app('computer');
app()->make('computer');
app()['computer']
Phần bind thì giống như phần trước, ta đăng kí với Container bằng một callback function, rồi dùng hàm make() để lấy 1 instance đã đăng kí ra. Đó là một cách ta thường dùng. Tuy nhiên bạn có thể thấy là ta có khá nhiều cách để lấy 1 instance của Computer (mình đã tô đỏ trong ví dụ), đó cũng là điểm rất đặc sắc của laravel.
Tiếp tục phân tích ví dụ:
class Computer
{
public $monitor;
public $keyboard;
public function __construct(Monitor $monitor, Keyboard $keyboard)
{
$this->monitor = $monitor;
$this->keyboard = $keyboard;
}
}
app()->bind('computer', 'Computer');
$computer = app('computer');
//hoặc
$computer = app('Computer');
Bạn thấy đó, ta không cần phải truyền bất cứ callback function nào vào hàm bind cả. Thậm chí ta còn có thể gọi thẳng app(‘Computer’) để khởi tạo ra một instance của Computer mà không cần bind bủng gì hết, không cần khởi tạo dependency, không cần inject.
Khi này laravel sẽ thực hiện như sau:
Đầu tiên các bạn hãy để ý tới Construction của class Computer ta đã đăng ký, chúng ta có truyền vào 2 đối tượng của thằng Monitor và thằng Keyboard.
Phân tích cú pháp: app()->bind(‘computer’, ‘Computer’) thì tức là ta đã đăng ký cho một cái instance của class Computer với cái tên computer (chú ý 1 cái chữ C viết hoa và 1 cái viết thường chữ c). Computer trong lệnh bind trên chính là tên của class, còn computer là tên ta đăng kí với Service Container. Và ta có thể lấy ra instance của Class Computer bằng app(‘computer’) – tham số truyền vào tên đã đăng kí với Service Container.
Phân tích cú pháp: app(‘Computer’) thì bước 1 Laravel sẽ kiểm tra xem đã có cái gì được bind vào Container dưới tên Computer hay chưa. Nếu chưa thì nó sẽ coi Computer là tên class và tiến hành lấy ra instance từ class Computer. Có nghĩa là thậm chí ta không cần đăng ký gì với Service Container thì quá trình inject vẫn được Laravel thực hiện 1 cách tự động luôn :v
Nếu ai đã từng sử dụng 1 vài framework khác như spring chẳng hạn thì sẽ thấy là thằng spring loằng ngoằng hơn, ta phải đăng ký thằng nào là bean, thằng nào cần inject… Nhưng với Laravel thì ta còn chả cần động vào, Service Container đã hỗ trợ tận răng rồi :v
Quá trình tạo ra instance được diễn ra như thế nào?
Quá trình tạo ra instance được Laravel thực hiện như sau:
Đầu tiên Container sẽ kiểm tra thấy xem thằng Computer cần những dependency nào, như ví dụ trên thì là Monitor và Keyboard .
Sau đó Service Container tiến hành resolve ra 2 instance của 2 class Monitor và Keyboard ra và inject vào Class Computer thông qua constructor injection. Nếu mà 2 class Monitor và Keyboard có sự phụ thuộc tiếp thì chúng lại thực hiện lại các bước như với thằng Computer. Vậy là sau khi truy vết tới class không còn sự phụ thuộc nữa thì nó inject tất cả vào nhau và cuối cùng là ra được 1 instance của Computer.
Và đương nhiên tại lần đầu tiên tạo instance của thằng computer này nó cũng đã tạo được các instance của thằng Mornitor, Keyboard và các class liên quan tới chúng :v. Sau này có 1 thằng khác cũng có Dependence là Mornitor thì nó không cần truy vết thằng này nữa.
Có 1 lưu ý là thằng Service Container sẽ ưu tiên những gì được bind vào nó hơn. Tức là nếu có câu lệnh app()->bind(‘computer’, function(){return ‘ITFORBABY’;}) thì khi resolve Computer nó sẽ ưu tiên logic trong callback function trước thay vì ra instance của class Computer.
Nếu các bạn đã theo dõi bài viết về Dependence Injection của mình thì cũng đã biết rằng DI có 1 điểm yếu chí mạng là nếu các dependency có phụ thuộc tầng tầng lớp lớp với nhau thì việc inject ở code sẽ rất cực khổ. Tuy nhiên như phần trên mình giải thích thì Laravel đã tự động inject hộ chúng ta rồi, việc của chúng ta chỉ là resolve ra và dùng thôi :v.
Laravel sử dụng Service Container dùng ở đâu?
Tới đây các bạn đã thấy sự lợi hại của Service Container chưa =)). Chính vì Service Container mạnh mẽ đến vậy nên hầu như tất cả tính năng của Laravel đều sử dụng thằng này. Bạn có thể thấy từ Router, Middleware, Controller, Listener, Queue tới Model… Đều lấy instance ra từ Service Container.
Ví dụ trong Controller
class AuthorController extends Controller { public function update(\Illuminate\Http\Request $request, $id) { $all = $request->all(); } }
Chúng ta chỉ cần khai báo truyền \Illuminate\Http\Request vào trong hàm update, và bên dưới chỉ cần gọi $request ra và dùng thôi, Khum cần phải lo lắng gì hết :v
Các phương pháp binding vào Service Container
Không chỉ hỗ trợ bind theo closure, hay tên class như ví dụ bên trên, Service Container còn hỗ trợ nhiều phương pháp binding khác.
Singleton binding: Như tên gọi của nó thì instance sẽ chỉ được resolve một lần, những lần gọi tiếp theo sẽ không tạo ra instance mới mà chỉ trả về instance đã được resolve từ trước. (Cái này giống thằng spring ghê, chắc coppy của nhau :v)
app()->singleton('now', function() {return time();}); app('now') === app('now'); // true
Instance binding: Khứa này cũng gần giống với Singleton Binding.
$now = time(); app()->instance('now', $now); $now === app('now'); // true
Interface binding: Điều gì sẽ xảy ra nếu ta thay app(‘Computer’) thành app(‘IComputer’) với IComputer là interface?
class Computer implements IComputer {} // Binding Interface với một Implementation của nó app()->bind('IComputer', 'Computer'); // Trong constructor của một class nào đó ta có thể truyền vào theo interface public function __construct(IComputer $computer) { $this->computer = $computer; }
Tới đây thì chắc các bạn đã nhớ lại bài trước mình đã nói lý do tại sao chúng ta nên truyền vào 1 Interface thay vì 1 class rồi chứ. Nếu ai chưa biết thì có thể xem lại nha.
Contextual Binding: 1 Vấn đề đặt ra là nếu có đến 2, 3 class là implementation của một Interface thì sao?. Và trong một trường hợp bạn cần inject implementation này và trong trường hợp khác bạn lại cần implementation khác, khi đó Contextual Binding sẽ là lựa chọn tốt nhất.
Ngoài ra bạn còn có thể sử dụng tag để gom một lúc nhiều thứ vào làm một, để lúc resolve được dễ dàng hơn.
// Tag app()->tag(['Foo', 'Bar'], 'example'); // Resolve ra một mảng gồm các instance của Foo và Bar app()->tagged('example');
Tổng kết
Service container là 1 khái niệm cực cực cực cực cực cực cực cực cực kì quan trọng mà bất cứ Laravel Programer nào cũng cần phải nắm được. Ở bài viết sau mình sẽ giới thiệu những khái niệm xoay quanh thằng Service container này, các bạn hãy chờ đón ở tập sau nhé…!
Link tham khảo:
- https://laravel.com/docs/5.6/container
- https://viblo.asia/p/laravel-beauty-tim-hieu-ve-service-container-3KbvZ1wLGmWB
- https://toidicodedao.com/2015/11/03/dependency-injection-va-inversion-of-control-phan-1-dinh-nghia/