Ở bài trước mình đã giới thiệu về Service container. Ở bài này mình sẽ phân tích xem sức mạnh của Service Container là gì nhé!
1. Auto injection
Ví dụ ta có một sự phụ thuộc class rườm rà như thế này. Class A phụ thuộc vào Class B, Class B lại phụ thuộc vào Class C, Class C lại phụ thuộc vào class D. Nếu muốn khởi tạo 1 instance của class A ta cần viết như thế này:
$instanceOfA = new ClassA(new ClassB(new ClassC(new ClassD())));
Khi làm thực tế không ít lần chúng ta gặp phải trường hợp tương tự, nhìn thấy rắc rối phải không nào 😂😂😂 Nhưng nếu chúng ta dùng Service Container của Laravel thì chúng ta chỉ cần viết thế này
// binding
$container = new Illuminate\Container\Container();
$container->bind('ClassA');
// resolve
$instanceOfA = $container->make('ClassA');
Đơn giản hơn rất nhiều đúng ko. Chúng ta còn chẳng cần quan tâm đến Class A phụ thuộc vào Class Cha nào rồi Class Cha này có phụ thuộc vào Class Ông nào ko, ..... Service Container đã lo cho chúng ta hết. Chúng ta gọi việc này là Auto Injection. Tức là Service Container tự nó đi tìm những class phụ thuộc vào class được bind vào Service Container và tự injection vào. Vậy làm sao Service Container lại làm được điều này, đó chính là nhờ vào Php Reflection , tức là class sẵn có này của Php cung cấp khả năng phân tích cấu trúc bên trong của 1 class. Từ đó Service Container biết để khởi tạo instance của Class A thì cần Class B. Service Container lại tiếp tục phân tích để khi tạo instance của Class B thì cần Class C, ... cứ tiếp tục đệ quy như thế này cho đến hết.
2. Cung cấp đa dạng các cách binding
Cấu trúc chung của binding
/** * Register a binding with the container. * * @param string $abstract * @param \Closure|string|null $concrete * @param bool $shared * @return void * * @throws \TypeError */ public function bind($abstract, $concrete = null, $shared = false)
Giải thích $abstract: định dạng string. Là tên của service mà mình đặt trên khi binding vào Service Container $concrete: định dạng Closure|string|null. Đại diện cho class muốn binding vào Nếu là Closure thì sẽ là 1 1 function Closure, class returen trong function Closure này chính là Class dc binding vào. Đây là 1 cách viết tùy biến cao nhất so với định dạng string|null Nếu là string thì chính là tên của Class muốn binding vào Nếu là null thì sẽ lấy $concrete = $abstract $shared: định dạng kiểu boolean, giá trị mặc định là false. Nó sẽ liên quan tới cách binding singleton sẽ nói ở mục sau. Các loại binding
2.1 Binding Basics
Binding simple
Binding tronmg Service Providers
$this->app->bind('ClassA', function ($app) { return new ClassA();
});
$this->app ở đây chính là Service Container được tạo ra khi khởi động ứng dụng Laravel Các này sẽ tương đương với cách ta dùng App facade.
use Illuminate\Support\Facades\App; App::bind(ClassA::class, function () { return new ClassA();
});
Binding A Singleton
Cách binding này sử dụng thuật ngữ "Signleton" trong design patterm, tức là chỉ có 1 instance duy nhất dù ta có tạo mới instance nhiều lần. Khi ta binding 1 class vào Service Container qua cách Binding A Singleton thì các lần resolve class này ra dùng thì ta chỉ resolve 1 instance duy nhất. Hãy theo dõi ví dụ sau để hiểu hơn nhé! Ví dụ ta có 1 class TestClass muốn binding và Service Container app\Support\TestClass.php
<?php namespace App\Support; class TestClass
{ protected $value = 0; public function increase() { $this->value++; return $this->value; }
}
Trong AppServiceProvider.php ta sẽ binding theo 2 cách
- là singleton
- là binding thông thường
...
public function register()
{ $this->app->singleton( 'test1', \App\Support\TestClass::class ); $this->app->bind( 'test2', \App\Support\TestClass::class );
}
...
Và đây là test kết quả
>>> app('test1')->increase()
=> 1
>>> app('test1')->increase()
=> 2
>>> app('test1')->increase()
=> 3 >>> app('test2')->increase()
=> 1
>>> app('test2')->increase()
=> 1
>>> app('test2')->increase()
=> 1
Bạn thấy sự khác biệt chưa. Cách 1 do ta binding theo kiểu singleton nên khi resolve app('test1') lần đầu tiên tạo instance , các lần sau sử dụng lại instance ở lần , nên các giá trị chạy hàm increase() sẽ tăng 1, 2, 3 Cách 2 ta binding theo kiểu thông thường nên mỗi lần resolve app('test1') sẽ tạo 1 instance khác nhau, nên các giá trị chạy hàm increase() chỉ là 1
Binding Instances
Như nói ở phần cấu trúc chung, binding này rất linh hoạt nên nếu thích ta có thể binding cả 1 instance vào.
$sendGrid = new SendGrid("xxx-yyy-zzz");
$this->app->instance('SendGrid', $sendGrid);
Trong thực tế đôi khi 1 class phức tạp, ta không chắc nếu binding class này vào Service Container liệu Service Container sẽ auto injection đủ những class depency ta cần vào hay chưa, hoặc việc injection các class depency cần 1 vài thủ tục rườm rà khác. Nên Laravel cung cấp cả cách thức giúp ta binding cả 1 instance vào Service Container. Như ví dụ trên, muốn sử dụng 1 class SendGrid( 1 dịch vụ gửi email) ta cần khởi tạo class kèm theo secret key "xxx-yyy-zzz") của nó, vì vậy ta sẽ lựa chọn dùng Binding Instance
Binding Primitives
Thỉnh thoảng bạn có một class nhật một vài injected class khác, nhưng cũng cần một inject giá trị nguyên thủy như một số nguyên. Bạn có thể dễ dàng sử dụng binding để inject bất kỳ giá trị nào vào trong class nếu cần:
$this->app->when('ClassA') ->needs('$variableName') ->give($value);
2.2 Binding Interfaces To Implementations
Service Container cung cấp 1 cách thức cho phép ta binding 1 interface và kèm theo 1 implementation. Hãy xem xét ví dụ sau. Ta có 1 interface là FileStorageInterface có 2 việc chính là upload và delete file. Có 1 vài class con implement interface FileStorageInterface ví dụ như: S3FileStorage , LocalFileStorage, ...
<?php interface FileStorageInterface
{ public function upload(); public function delete();
} class S3FileStorage implements FileStorageInterface { public function upload() { //Amazon S3 specific upload code here. } public function delete() { //Amazon S3 specific delete code here. }
} class LocalFileStorage implements FileStorageInterface { public function upload() { //Local file specific upload code here. } public function delete() { //Local file specific delete code here. }
}
Ví dụ ta muốn dùng FileStorageInterface với triển khai là S3FileStorage, ta sẽ binding vào Service Container như sau
$this->app->bind('FileStorageInterface', 'S3FileStorage');
Rồi ở trong FileStorageController chẳng hạn, ta muốn dùng function upload(), ta sẽ dùng như sau
$this->app('FileStorageInterface')->upload();
Nếu đến thế này thôi thì ta chưa thấy tác dụng của việc Binding Interfaces To Implementations, nhưng nếu một ngày đẹp trời ta ko muốn lưu file trên s3 mà muốn lưu file ở luôn server thì ta chỉ cần sửa đoạn code binding
$this->app->bind('FileStorageInterface', 'S3FileStorage'); -> $this->app->bind('FileStorageInterface', 'LocalFileStorage');
Mà không cần sửa ở controller hay bất cứ chỗ nào dùng 2 function upload() hay delete() của FileStorageInterface, thật nhàn phải ko nào. Ta nói dùng Binding Interfaces To Implementations giúp code của ta: Ít phụ thuộc(less coupled), dễ bảo trì(maintainable) và dễ test hơn(testable).
2.3 Contextual Binding
Lấy ví dụ ở 2.3 trên. Nếu chúng ta thêm 1 yêu cầu như sau: Ta có 2 controller là PhotoController và VideoController, PhotoController dùng S3FileStorage, VideoController dùng LocalFileStorage thì trong Laravel sẽ có thể viết theo ngữ cảnh như thế này
$this->app->when(PhotoController::class) ->needs(FileStorageInterface::class) ->give(function () { return S3FileStorage:class; $this->app->when(VideoController::class) ->needs(FileStorageInterface::class) ->give(function () { return LocalFileStorage:class;
Ta thấy Contextual Binding là 1 kiểu mở rộng của Binding Interfaces To Implementations khi dùng thêm điều kiện ngữ cảnh ->when. Với cách dùng này cách dùng của ta sẽ linh hoạt hơn trong từng ngữ cảnh.
2.4 Tagging
Thỉnh thoảng, bạn cần giải quyết tất cả các "category" của binding. Ví dụ, có lẽ bạn đang xây dụng một tập hợp báo cáo mà sẽ nhận một mảng danh sách các implementations khác nhau của interface Report. Sau khi đăng ký Report implementations, bạn có thể gán chúng vào một tag sử dụng phương thức tag:
$this->app->bind(CpuReport::class, function () { // ...
}); $this->app->bind(MemoryReport::class, function () { // ...
}); $this->app->tag([CpuReport::class, MemoryReport::class], 'reports');
Khi service đã được tag, bạn có thể dễ dàng resolve chúng qua phương thức tagged:
3. Resolving
3.1 Make
Bạn có thể sử dụng phương thức make để resolve một class instance ra khỏi container. Phương thức make nhận tên class hay interface bạn muốn thực hiện resolve
$instanceOfClassA = $this->app->make('ClassA');
3.2 MakeWith
Ví dụ bạn dùng make mà ko thể resolve được instance của ClassA từ Service Container bởi vì ClassA này khi khởi tạo cần truyền thêm tham số vào, ví dụ là tham số id. Khi đó ta sẽ dùng makeWith
$instanceOfClassA = $this->app->makeWith('ClassA', ['id' => 1]);
4. Code của bạn nhìn đẹp hơn, module hơn và dễ maintain hơn
Rõ ràng là qua các ví dụ ở phần 2.2, 2.3 chúng ta thấy code của chúng ta đẹp hơn, module hơn và dễ maintain hơn. Đây chính là 1 mục đích của tạo ra Service Container