Laravel Service Container in Depth & Tips to Customize Your Application
Bài đăng này đã không được cập nhật trong 3 năm
Introduction
Service Container không phải là một khái niệm xa lạ trong Laravel nói riêng và một số framework khác nói chung (ví dụ như Symfony hay Zend). Tuy nhiên có thể nói, Laravel Service Container ở thời điểm hiện tại là hoàn chỉnh, đầy đủ và mềm dẻo nhất. Trong quá trình làm việc với Laravel, bạn có thể sử dụng nó mà không cần biết đến sự tồn tại của nó. Có thể bạn đã từng gọi và sử dụng một số phương thức trong Service Container tuy nhiên chưa có thời gian để tìm hiểu tại sao nó lại hoạt động như vậy. Trong bài viết này, mình sẽ đi qua một vòng Service Container trong Laravel và tìm hiểu một số vấn đề quan trọng về nó. Tất nhiên mình không thể giải thích từng phương thức trong Service Container được thì như vậy bài viết này sẽ kéo dài mãi mất
Sau khi đã có những kiến thức cơ bản, chúng ta hay cùng bàn luận về cách customize Laravel. Những quy tắc hay quy ước (conventions) trong Laravel có phải là bắt buộc không? Làm sao để mở rộng framework? Cũng như những lưu ý khi làm việc với nó.
Have fun! :crazy_face:
The Service Container
A gentle overview
Trước khi tìm hiểu về Service Container, chúng ta cần một cái nhìn tổng quan về những gì mà Service Container cung cấp. Hầu hết tất cả các components (services) trong Laravel đều triển khai (implements) một hoặc nhiều interfaces (hay contracts). Service Container cũng không phải là một ngoại lệ. Bạn chỉ cần truy cập vào trang https://github.com/laravel/framework, nhấn T và gõ Container vào ô tìm kiếm. Bạn hãy tìm và mở file src/Illuminate/Contracts/Container/Container.php
và quan sát nội dung của nó. Đây là danh sách các phương thức (signature only) bên trong interface đó:
- bound
- alias
- tag
- tagged
- bind
- bindIf
- singleton
- extend
- instance
- when
- factory
- make
- call
- resolved
- resolving
- afterResolving
Cảm giác đầu tiên của mình là vừa quen, vừa lạ. Một số phương thức mình sử dụng rất nhiều còn một số thì chỉ nghe qua hoặc hoàn toàn không biết. Tuy nhiên, bạn cũng không cần phải nắm rõ tất cả những phương thức trên, khi làm việc 5 hoặc 6 phương thức là đủ.
Khi tìm hiểu về Service Container, trên thực tế bạn nên tự đặt câu hỏi: "Laravel là gì?" Service Container có thể được coi là sương sống của Laravel Framework, hiểu một cách đơn giản hơn Service Container giống như một chất keo gắn mọi thứ bên trong framework lại với nhau (các components, packages,...). Hiểu theo một cách khác, Service Container giống như một kho lưu trữ trung tâm mà bạn có thể lưu và truy xuất mọi thứ (!?).
Hầu hết các components trong Laravel đều phụ thuộc ít nhiều vào Service Container, ví dụ như:
- Routing
- Middleware
- Request
- Facades
Components và Services không hoàn toàn giống nhau, tuy nhiên, trong bài viết này chúng ta sẽ coi chúng như một. Tất nhiên Services sẽ đúng hơn do mọi thứ trong Laravel đều xoay quanh từ đó
Service Container có thể được biết đến dưới một số tên gọi khác như:
- The Container (cách rút gọn)
- Application (Laravel Application class là một sub-class của Container class)
- IOC Container
- DI Container
Service Container, The Container hay Application không cho chúng ta thấy được những chức năng cơ bản của Service Container. Tuy nhiên hai tên gọi còn lại: IOC Container và DI Container tiết lộ cho chúng ta một phần nào đó về chức năng và cách hoạt động của Service Container. Trong phần tiếp theo, chúng ta sẽ cùng điểm qua hai khái niệm: IOC và DI, do chúng ta cần nắm vững hai khái niệm này nếu muốn hiểu hơn về Service Container.
IOC & DI
Hai khái niệm này không còn xa lạ và cũng có khá nhiều bài viết về chúng. Tuy nhiên, trong khuôn khổ của bài viết này, mình sẽ tìm cách rút gọn và đơn giản hóa chúng sao cho gần gũi với thứ mà chúng ta đang quan tâm ở đây - Service Container.
Bạn có thể từng nghe nói rằng Service Container trong Laravel khá mềm dẻo và linh hoạt do nó triển khai IOC - Inversion of Control design pattern. Như vậy chẳng khác gì nói rằng ngôi nhà nào đó đặc biệt vì nó có ống khói nếu dừng lại ở đó.
Trước khi tìm hiểu về IOC và DI, đây là những điều bạn cần nhớ. Bạn còn nhớ trong nguyên tắc SOLID, nguyên tắc cuối cùng D hay Dependency Inversion với nội dung ngắn gọn là: "Module cấp cao không nên phụ thuộc vào các module cấp thấp, cả hai nên phụ thuộc vào một abstraction nào đó". Hay đơn giản hơn là abstraction không nên phụ thuộc vào concrete implementation, chúng nên giao tiếp qua các interfaces. Lý do đơn giản là nếu module cấp cao phụ thuộc vào các module cấp thấp hơn thì khi có sự thay đổi ở các module cấp thấp, các module cấp cao cũng sẽ phải thay đổi theo. Kết quả là code sẽ khó bảo trì và mở rộng hơn.
Chúng ta cũng cần phân biệt giữa ba khá niệm Dependency Inversion, Inversion of Control và Dependency Injection, chúng có nhiều điểm tương tự nhưng không hoàn toàn giống nhau. Cụ thể như sau:
- Dependency Inversion: là một trong 5 nguyên tắc thiết kế SOLID.
- Inversion of Control: là một design pattern được tạo ra với mục đích tuân thủ nguyên lý trên.
- Dependency Injection: là một thể hiện (implementation) của pattern trên (nó cũng có thể được coi là một design pattern riêng nếu cấn thiết). Ngoài Dependency Injection, Inversion of Control còn có rất nhiều implementation khác như: Service Locator, Event, Delegation.
IOC - Inversion of Control
IOC - Inversion of Control là một design pattern trong đó việc định nghĩa sự thể hiện (implementation) của một một feature hay code path nào đó được thực hiện một lần duy nhất ở mức framework thay vì được định nghĩa nhiều lần trong codebase (định nghĩa một lần và sử dụng nhiều lần).
Định nghĩa trên đã được đơn giản hóa rất nhiều thay vì những định nghĩa khá trừu tượng và tổng quát. Để hiểu rõ hơn định nghĩa trên, chúng ta sẽ cùng đi qua một ví dụ đơn giản sau. Trong hầu hết các ứng dụng web hiện nay, chúng sẽ có chức năng cho phép gửi mail đến người dùng (có thể là newsletter hay unread notification). Giả sử chúng ta có một class tên Mailer
với nhiệm vụ tạo và gửi email. Trong nhiều trường hợp việc gửi mail có thể được thực hiện ở nhiều nơi khác nhau trong codebase (10 lần chẳng hạn). Giả sử chúng ta không sử dụng IOC, công việc chúng ta sẽ là tạo 10 instance khác nhau của Mailer
sử dụng cùng một logic. Câu hỏi đặt ra là nếu chúng ta có thay đổi về API của Mailer
chúng ta sẽ phải tìm và chỉnh sửa ở cả 10 vị trí khác nhau, cũng như việc tạo 10 instance khác nhau có cần thiết không khi chúng hoàn toàn giống nhau? Điều gì sẽ xảy ra nếu chúng ta muốn thay đổi mail driver? Việc tạo mới một instance của Mailer
có đơn giản không khi nó có nhiều dependencies khác nhau?
In a nutshell, tất cả các công việc liên quan đến định nghĩa các feature nên được thực hiện (điều khiển) ở tầng framework (top level) thay vì bên trong codebase (bottom level). Bottom level sẽ yêu cầu các feature và top level có nhiệm vụ khởi tạo và trả về các feature đó. Đó là lý do tại sao pattern này có tên Inversion of Control.
DI - Dependency Injection
DI - Dependency Injection là một implementation của Inversion of Control design pattern. Ý tưởng của nó là khi một đối tượng cần các dependencies là một hoặc nhiều đối tượng khác, thay vì khởi tạo các dependencies đó bên trong class của object ban đầu, các dependencies đó sẽ được truyền từ bên ngoài một cách gián tiếp.
Hãy cùng xét một ví dụ đơn giản, giả sử class của bạn cần một instance của một Logger implementation nào đó. Thông thường chúng ta sẽ tạo mới instance của Logger class đó sử dụng từ khóa new
và gán giá trị đó cho một property bên trong class. Việc làm này mang lại khá nhiều hệ quả không tốt như: Logger trong trường hợp này sẽ coupling với class của bạn (khi bạn muốn thay đổi Logger thì class của bạn cũng sẽ phải thay đổi theo). Khi bạn muốn thay đổi Logger sang một implementation khác (có thể là một logging driver khác chẳng hạn), class của bạn cũng sẽ thay đổi. Một hệ quả không tốt nữa là class của bạn sẽ khó test hơn. Sẽ tốt hơn nếu chúng ta chỉ cần typehinting Logger class (có thể là một concrete class, hay một interface) bên trong constructor của của class ban đầu và chúng ta có thể (magically ) truy cập được đến instance của Logger. Đây là một trong những nhiệm vụ chính của Service Container, đó là lý do tại sao nó còn có tên gọi là DI Container.
Nếu định nghĩa một cách tổng quát hơn, ý tưởng của Dependency Injection sẽ là:
- Các module không giao tiếp một cách trực tiếp với nhau mà thông qua các interfaces. Module cấp thấp sẽ triển khai các interfaces, module cấp cao sẽ gọi đến các module cấp thấp.
- Việc khởi tạo các module cấp thấp sẽ do DI Container đảm nhiệm, thay vì khởi tạo trực tiếp trong codebase.
- DI Container cho phép chúng ta định nghĩa quan hệ giữa các modules và các interfaces.
- DI có tác dụng làm giảm sự phụ thuộc giữa các modules, giúp cho việc thay đổi module, maintain codebase và testing dễ dàng hơn.
Các dạng cơ bản của Dependency Injection là:
- Constructor Injection (được sử dụng khá nhiều trong Laravel)
- Setter Injection
- Interface Injection
Service Container cho phép chúng ta cấu trúc các components bên trong framework cũng nhưng các custom components sử dụng DI qua đó gián tiếp thực hiện IOC pattern và đảm bảo nguyên tắc thiết kế Dependency Inversion trong SOLID.
Formal Definition
Trong phần trước của bài viết chúng ta đã tìm hiểu qua các khái niệm IOC và DI. Những lợi ích mà chúng mang lại cũng là nhưng lợi ích mà Service Container đảm bảo đạt được. Vậy Service Container là gì, dưới đây là một định nghĩa tổng quát:
Service Container là một công cụ có nhiệm vụ quản lý các dependencies của các class cũng như hiện thực hóa quá trình Dependency Injection.
Service Container trong Laravel có hai nhiệm vụ chính là Binding và Resolving, chúng ta sẽ cùng tìm hiểu hai chức năng chính này trong phần sau của bài viết.
Binding and Resolving
Hãy tưởng tượng Service Container như một center storage mà chúng ta có thể lưu trữ và truy xuất các key-value từ đó. Từ đó hai khái niệm Binding và Resolving có thể hiểu đơn giản như sau :upside_down:
- Binding: quá trình lưu một key vào bên trong container (nó có thể đơn giản là một string, classname hoặc interface).
- Resolving: quá trình truy xuất giá trị của một key nào đó từ container.
Trước khi tìm hiểu về Service Container trong Laravel, chúng ta cần có một instance của container. Có rất nhiều cách để thực hiện việc này (sẽ được đề cập đến trong những phần sau), nhưng cách đơn giản nhất đó là sử dụng app()
global function.
$container = app();
Chúng ta có thể resolve instance của một class sử dụng phương thức make()
của Service Container:
$logger = $container->make(Illuminate\Log\Writer::class);
Sử dụng app()
function một cách trực tiếp:
$logger = app()->make(Illuminate\Log\Writer::class);
Sử dụng app()
function một cách trực tiếp mà không cần gọi tới phương thức make()
:
$logger = app(Illuminate\Log\Writer::class);
Tại sao ở đây chúng ta sử dụng Service Container để lấy ra một instance của Illuminate\Log\Writer
class thay vì sử dụng new
keyword - new Illuminate\Log\Writer(...)
?
Giả sử Illuminate\Log\Writer
không có một dependency nào cả, khi đó việc sử dụng phương thức make()
và new
keyword không có quá nhiều sự khác biệt. Tuy nhiên, nếu class của bạn có một danh sách các dependencies, và mỗi dependency trong danh sách đó lại chứa một danh sách các dependencies khác (recursively ). Nếu bạn không sử dụng Service Container, đây có thể là logic mà bạn cần thực hiện:
$instance = new A(new B);
$instance = new A(new B(new C(new D, new E, new F)));
Đoạn logic trên có vẻ khá nhàm chán do new
keyword được sử dụng khá nhiều. Mỗi khi chúng ta cần một instance của class A, quá trình này sẽ được lặp lại khá nhiều lần Nếu chúng ta sử dụng Service Container chúng ta sẽ không cần phải sử dụng new
keyword và trong hầu hết các trường hợp, Service Container sẽ thực hiện việc instantiate các dependencies (recursively) một cách tự động (tất nhiên trong một vài trường hợp, Service Container không thể thực hiện việc resolve thành công nhưng vì những lý do đúng đắn).
Vậy tại sao Service Container có thể thực hiện được công việc trên? Câu trả lời có liên quan đến một khái niệm mang tên Autowiring mà chúng ta sẽ cùng tìm hiểu ngay sau đây.
Autowiring
Autowiring là một mô hình trong đó framework/packages có khả năng instantiate một class mà không cần những chỉ dẫn trực tiếp từ bên ngoài về cấu trúc của class đó. Nếu class đó có danh sách các dependencies, mô hình sẽ sử dụng Reflection để xác định và khởi tạo các dependencies một cách tự động.
Reflection là một khái niệm khá fancy . Một cách ngắn gọn, Reflection là một công cụ cho phép bạn hiểu hơn về cấu trúc bên trong của một class: các phương thức, properties, constructor và quan trọng nhất là các dependencies của class đó. Khi chúng ta sử dụng Service Container để instantiate một class nào đó, Service Container đầu tiên sẽ sử dụng Reflection API để đọc các dependencies của class đó. Service Container sau đó có thể tự đặt ra câu hỏi: "Class này có một số dependencies, liệu mình có thể khởi tạo các dependencies này trước khi khởi tạo class đang cần không?" Quá trình này diễn ra một cách đệ quy và câu hỏi trên sẽ được lặp đi lặp lại nhiều lần cho tới khi tất cả các nested dependencies được resolved hoặc khi một dependency nào đó không thể resolve nếu không có định nghĩa cụ thể về cách mà nó sẽ được instantiate.
Tưởng tượng phương thức make()
của chúng ta sẽ đơn giản như sau:
public function make($key)
{
$dependencies = $this->getConstructorDependencies($key);
return new $key(...$dependencies);
}
Điều gì sẽ xảy ra nếu một trong những dependency không được typehinted. Xét ví dụ sau đây:
class Mailer
{
protected $apiKey;
public function __construct(Queue $queue, $apiKey)
{
$this->queue = $queue;
$this->apiKey = $apiKey;
}
}
Nếu chúng ta sử dụng app(Mailer::class)
để resolve một instance của Mailer
class, chúng ta sẽ nhận được một exception với nội dung đơn giản hóa là "Unresolvable dependency". Service Container đã cố gắng hết sức để hoàn thành công việc của nó, tuy nhiên nó không phải là một magician. Trong ví dụ trên $apiKey
có thể là bất kì thứ gì bạn muốn (một string, một object) do không có một kiểu xác định cho $apiKey
. Do đó không có cơ sở nào để xác định được giá trị của $apiKey
, Service Container không biết làm gì hơn ngoài việc throwing exception
Service Container không thể tự động khởi tạo instance của một class khi một trong những dependency của class đó (hoặc các nested dependencies) không có kiểu xác định hoặc ở dạng primitive như
string
,array
,callable
Vậy chúng ta sẽ quay lại sử dụng new
keyword do Service Container bó tay trong những trường hợp như thế này. Câu trả lời là không. Việc chúng ta cần làm là dạy cho Service Container cách mà Mailer
class sẽ được khởi tạo. Đó là nội dung mà chúng ta sẽ đề cập đến trong phần tiếp theo của bài viết - Manual Binding
Bạn cũng có thể resolve instance của một class sử dụng
resolve()
function -$foo = resolve(Foo::class)
Nếu class của bạn cần một số giá trị primitive hoặc không xác định kiểu bạn có thể dùng phương thức
makeWith()
và truyền các giá trị cần thiết -$mailer = app(Mailer::class, ['apiKey' => 'supersecret'])
. Đây là một cách rút gọn của explicit binding mà chúng ta sẽ bàn ở phần sau.
Manual Binding (Explicit Binding)
Trong Laravel có 4 cách để thực hiện Manual Binding:
- bind
- singleton
- instance
- alias
bind
Đây có lẽ là một trong những phương thức phổ biến nhất khi nhắc đến Service Container. Trên thực tế, các phương thức còn lại đều được xây dựng dựa trên phương thức này. Vậy mục đích của phương thức bind()
ở đây là gì?
Phương thức này cho phép bạn đưa ra chỉ dẫn cho Service Container về cách khởi tạo object của một class nào đó. Tất nhiên nếu class đó có thể được khởi tạo bằng Autowiring thì việc sử dụng phương thức này là không cần thiết. Giả sử với Mailer
class nói trên, chúng ta sẽ sử dụng phương thức bind()
để dạy Service Container về giá trị mà $apiKey
sẽ nhận:
app()->bind(Mailer::class, function () {
// In reality, we should store the API key in configuration file. Just for demo here.
return new Mailer('xlTBWj1qasAYJu0Wbtw2NhYUxFgREWoHwdTjymX5aJ3lrLi8p3');
});
$mailer = app(Mailer::class); // It works!!
Trong ví dụ trên, chúng ta đã bind (gán) classname của Mailer
class (key) với một closure (value). Mỗi khi Service Container cần resolve một instance của Mailer
, closure trên sẽ được thực thi và giá trị mà closure trả về sẽ là giá trị khởi tạo của Mailer
. Tất nhiên sau khi đã định nghĩa cách khởi tạo của Mailer
class (thông thường trong Service Provider), chúng ta hoàn toàn có thể sử dụng app()
function để resolve một instance mới của Mailer
Hiện tại Mailer
của chúng ta chỉ nhận vào một tham số duy nhất là $apiKey
trong constructor. Giả sử chúng ta có nhiều hơn một tham số và một hoặc nhiều trong số đó có thể được khởi tạo sử dụng Autowiring. Tất nhiên chúng ta phải sử dụng Manual Binding ở đây do $apiKey
là unresolvable. Trên thực tế một instance của Laravel Application sẽ được truyền cho closure ở trên. Vì vậy chúng ta có thể sử dụng nó để resolve các dependencies khác của Mailer
.
app()->bind(Mailer::class, function ($app) {
return new Mailer(
$app->make(Queue::class), // Here Queue is a resolvable class or interface
'xlTBWj1qasAYJu0Wbtw2NhYUxFgREWoHwdTjymX5aJ3lrLi8p3'
);
})
Ở đây chúng ta đã sử dụng cả Autowiring và Manual Binding để định nghĩa cách khởi tạo của Mailer
class.
Phương thức
bind()
sẽ khởi tạo một instance mới của class mỗi lần nó được thực thi.
Xét một ví dụ đơn giản sau:
class Foo
{
public $rand;
public function __construct()
{
$this->rand = str_random();
}
}
dump(app(Foo::class)->rand); //xlTBWj1qasAYJu0W
dump(app(Foo::class)->rand); //Ju0Wbtw2NhYUxFg
Mỗi lần chúng ta resolve một instance của Foo
chúng ta sẽ nhận được một random string khác nhau. Điều này có thể xảy ra ở cả Autowiring và Manual Binding. Trong hầu hết các trường hợp, điều này khá bình thường; tuy nhiên trong một số trường hợp chúng ta chỉ muốn có một instance duy nhất của một class nào đó và share nó trong toàn bộ application. Mỗi khi chúng ta cần một instance của class đó, một instance duy nhất sẽ được trả về cho tất cả các lần gọi. Đó cũng là mục đích của phương pháp binding tiếp theo - singleton
singleton
Phương thức này có cách hoạt động gần như tương đồng với phương thức bind()
nói trên. Tuy nhiên khi closure tương ứng với key được thực thi lần đầu tiên, kết quả sẽ được cached lại. Trong những lần resolve kế tiếp instance đã được cached sẽ được trả về.
Hãy xét một ví dụ đơn giản sau. Giả sử bạn đang sử dụng Chatwork SDK để thực hiện việc gửi thông báo đến một room nào đó. Ngoài việc cài đặt API key cho SDK bạn cũng nên tạo một singleton cho ChatworkRoom
(với tham số đầu vào là room ID, giả sử ở đây bạn chỉ muốn gửi thông báo đến một room duy nhất). Sau khi đã tạo binding xong, bạn chỉ cần typehinting ChatworkRoom
hoặc resolve một instance của class đó... and you're good to go!
protected function configureChatworkSDK()
{
ChatworkSDK::setApiKey(config('services.chatwork.api_key'));
$this->app->bind(ChatworkRoom::class, function () {
return new ChatworkRoom(config('services.chatwork.room_id'));
});
}
instance
Phương thức này gần như giống hoàn toàn phương thức singleton()
nói trên. Tuy nhiên có một chút khác biệt là thay vì sử dụng closure để định nghĩa cách instantiate một object nào đó, phương thức này yêu cầu bạn phải có một object có sẵn trước đó và truyền trực tiếp object đó vào phương thức.
Phương thức này đặc biệt hữu ích khi viết Unit Test, bạn có thể sử dụng nó để thay thế implementation thực sự của một class bằng một mocked object.
alias
Có lẽ phương thức này ít được biết đến và ít được sử dụng nhất khi làm việc với Laravel. Tuy nhiên, nó khá hữu dụng khi viết package cho framework hay đơn giản là xây dựng một component cho ứng dụng của bạn. Bạn có thể hiểu đơn giản alias ở đây giống như trong Linux system, bạn gán cho một command một cái tên ngắn, dễ nhớ hơn và sử dụng tên đó để thực thi command.
alias trong Laravel cho phép bạn bind một key nào đó đến một hoặc nhiều key khác. Bạn có thể sử dụng các key đó để resolve instance cần thiết. Lưu ý rằng key ở đây không nhất thiết phải là tên của một class nào đó, nó có thể chỉ đơn giản là một string.
Trên thực tế hầu hết các component trong Laravel đều sử dụng alias. Bạn có thể tham khảo trong Documentation về Facade ở đây: https://laravel.com/docs/5.5/facades#facade-class-reference. Để ý đến cột cuối cùng trong bảng - Service Container Binding, đó chính là alias cho các class trong cột thứ hai của bảng. Một số alias thường dùng có thể kể ra là: log
, hash
, db
, auth
, và config
. Thay vì phải nhớ cả namespace của một class hay interface nào đó, bạn có thể sử dụng alias để resolve instance của một component nào đó:
$logger = app('logger');
$hasher = app('hash');
Một câu hỏi đặt ra là tại sao Laravel lại sử dụng alias cho các component của nó?
Trên thực tế các alias trong framework không chỉ ánh xạ đến một class nào đó. Thông thường mỗi alias sẽ ánh xạ đến một concrete class và một hoặc nhiều interfaces.
OK, hãy mở source code của framework và tìm đến file src/Illuminate/Foundation/Application.php
. Đây là một subclass của src/Illuminate/Container/Container.php
, và được tạo ra để phục vụ nhu cầu của framework nói riêng (chú ý Container component của Laravel có thể được sử dụng độc lập và không phụ thuộc vào framework). Trong constructor của class này, sau khi đã đăng ký một số bindings và service providers chính, framework sẽ tiến hành đăng ký các core container aliases thông quá phương thức registerCoreContainerAliases()
Nếu quan sát nội dung của phương thức này chúng ta có thể thấy mỗi alias sẽ được ánh xạ đến một mảng các concrete classes và các interfaces.
public function registerCoreContainerAliases()
{
foreach ([
'app' => [
\Illuminate\Foundation\Application::class,
\Illuminate\Contracts\Container\Container::class,
\Illuminate\Contracts\Foundation\Application::class,
\Psr\Container\ContainerInterface::class
],
// more keys below
] as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}
Lần đầu tiên quan sát phương thức này bạn có thể sẽ hơi confused một chút. Bạn có thể nghĩ các classes và interfaces sẽ được alias đến key bên trái. Trên thực tế nó là ngược lại, mỗi key sẽ được alias đến các classes và interfaces. Có hai câu hỏi có thể đặt ra ở đây:
- Tại sao chúng ta có thể thực hiện được việc alias này?
- Tác dụng của việc tạo các alias ở đây là gì?
Để trả lời cho câu hỏi đầu tiên, chúng ta cần quan sát kĩ hơn constructor của src/Illuminate/Foundation/Application.php
class:
public function __construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();
}
Chúng ta sẽ chỉ quan tâm đến hai dòng cuối trong phương thức này. Chú ý rằng các service providers sẽ được đăng ký trước khi các container aliases được đăng ký. Nếu bạn còn nhớ về service providers thì nó là một công cụ cho phép bạn plug in các service từ bên ngoài vào core của framework (tất cả các core component của Laravel đều có một service provider tương ứng). Trong mỗi service provider, chúng ta có thể thực hiện đăng ký các bindings với Service Container. Dó đó trong ví dụ trên app
đã được đăng ký trước khi các aliases được tiến hành đăng ký, hay nói cách khác app
ở đây đã là resolvable. Ở đây app
có hơi đặc biệt một chút so với các key khác do nó được đăng ký trong phương thức registerBaseBindings()
ngay bên trong Application
class. Đối với các key của các component khác, nó sẽ được đăng ký trong service provider tương ứng. OK, vậy chúng ta chắc chắn rằng mỗi service container binding key là resolvable, do đó việc alias chúng đến một thứ gì đó khác là hoàn toàn khả thi.
Đối với câu hỏi thứ hai, mục đích của quá trình này là tăng thêm tính mềm dẻo cho framework. Tất cả các classes và interfaces đều được ánh xạ đến một key duy nhất (trong ví dụ là app
). Dó đó bạn có thể sử dụng các classes và interfaces đó để resolve một instance của Laravel application. Bạn có thể kiểm tra việc này đơn giản sử dụng Tinker:
// Get an instance of Illuminate\Foundation\Application
$app1 = app(\Illuminate\Foundation\Application::class)
// Get another instance of Illuminate\Foundation\Application
$app2 = app(\Illuminate\Contracts\Container\Container::class)
$app1 == $app2 // true - because the instance bound to the `app` key is singleton
Khi tìm hiểu về Service Container có hai phần khá quan trong liên quan đó là Service Providers và Facades, các bạn có thể tìm hiểu trên Documentation hoặc tìm kiếm các bài viết và series khá hay trên Viblo liên quan đến hai chủ đề đó.
Extra Credits
Calling Methods
Bạn có biết rằng bạn có thể gọi một phương thức sử dụng phương thức call()
của Service Container? Bạn đã từng bao giờ đặt câu hỏi tại sao các dependencies của các phương thức bên trong controller đều được tự động resolved (method injection)? Lý do là các phương thức trong controller sẽ được gọi bởi Service Container sử dụng phương thức call()
, từ đó các dependencies được typehinted sẽ tự động được resolved tương tự như constructor injection.
Có hai cách chính bạn có thể gọi một phương thức sử dụng Service Container:
app()->call([$object, $method])
app()->call('ClassName@method', [$parameters])
Bạn có thể sẽ quen thuộc với cách thứ hai hơn cách đầu tiên. Một ví dụ thực tế là khi bạn sử dụng event subscriber (một cách ngắn gọn để đăng ký các events và listeners tương ứng) bạn sẽ thấy cấu trúc này khá quen thuộc. Bạn có thể đã từng thắc mắc tại sao ta có thể map events và các listeners sử dụng một cấu trúc khá dị như vậy. Trên thực tế, class@method
string sẽ được chuyển thành argument cho phương thức call()
và... things just work!
Binding Arbitrary Values
Trên thực tế bạn có thể bind một giá trị bất kỳ với Service Container - giá trị trả về của binding closure không nhất thiết phải là instance của một class nào đó, bạn có thể trả về một string, một array hay bất ký giá trị nào bạn muốn. Xét ví dụ sau đây:
app()->bind('numbers', function () {
return ['one', 'two', 'three'];
});
dd(app('numbers')); // ['one', 'two', 'three']
Using Array Syntax
Illuminate\Container\Container
class triển khai ArrayAccess
interface, do đó bạn có thể sử dụng container (application) object giống như một array. Tuy nhiên khi làm việc bạn nên chọn một trong hai cách: sử dụng array syntax và sử dụng method calls (thường hay được dùng hơn) và tuân thủ theo chúng.
$app = app();
$logger = $app['log']; // Illuminate\Log\Writer
$mailer = $app['mailer']; // Illuminate\Mail\Mailer
$numbers = $app['numbers']; // ['one', 'two', 'three']
Binding Interfaces to Implementations
Một phương pháp thường được sử dụng đó là bind một interface với một concrete class. Một ví dụ khá quen thuộc đó là khi bạn sử dụng repository pattern (tuy nhiên khi sử dụng Active Record, pattern này có vẻ không còn phù hợp), với mỗi repository class tương ứng với một model bạn sẽ có một interface tương ứng. Công việc của bạn là bind hai thứ đó lại với nhau và sử dụng (injecting) interface thay vì sử dụng concrete class khi cần truy cập instance của một repository nào đó. Tất nhiên phụ thuộc vào một interface sẽ mềm dẻo hơn là một concrete class.
// Inside a service provider
$this->app->bind(
App\Repositories\Contracts\UserRepository::class,
App\Repositories\UserRepository::class
);
// Inside a controller
use App\Repositories\Contracts\UserRepository;
class UsersController extends Controller
{
/**
* Create a new UsersController instance.
*
* @param UserRepository $users
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
}
Tagging
Trong nhiều trường hợp, bạn có thể muốn resolve một danh sách các binding trong cùng một nhóm thay vì resolve từng binding một. Ví dụ bạn có một danh sách các plugin thường được sử dụng cùng nhau, thay vì resolve từng plugin một khi cần, bạn có thể nhóm chúng lại sử dụng một tag nào đó và resolve chúng cùng một lúc khi cần. Thay vì resolve một binding đã được bound trong Service Container, chúng ta sẽ tag các bindings đó và resolve chúng cùng một lần.
$this->app->bind('FooPlugin', function () {
//
});
$this->app->bind('BarPlugin', function () {
//
});
$this->app->tag(['FooPlugin', 'BarPlugin'], 'plugins');
$plugins = app()->tagged('plugins'); // return an array of plugins
Container Events
Service Container sẽ fire một số event khi nó thực hiện quá trình resolve một instance nào đó. Có hai event chính là resolving
và afterResolving
. Tham số đầu vào sẽ là một abstract (container binding key, class hay interface) và một closure nhận vào giá trị là object đang được resolving và một instance của Laravel Application:
// This callback is executed every time something is resolved from the Service Container
// There is no abstract provided as the first argument for the closure.
$this->app->resolving(function ($object, $app) {
dump($object);
});
$this->app->afterResolving(function ($object, $app) {
dump($object);
});
// You can register event listener for a specific container binding
$this->app->resolving('log', function ($object, $app) {
dd(get_class($object));
});
app('log'); // "Illuminate\Log\Writer"
Rebounds and Rebinding
Khi bạn bind một key với Service Container nhiều hơn một lần, ta sẽ gọi quá trình đó là rebinding. Quá trình trên sẽ kích hoạt một quá trình nhỏ hơn - rebound. Rebounding đơn giản là tạo một instance mới của một abstract nào đó và thực thi các rebounding callbacks trên instance mới đó. Xét một ví dụ phương thức instance()
của Service Container:
public function instance($abstract, $instance)
{
$this->removeAbstractAlias($abstract);
$isBound = $this->bound($abstract);
unset($this->aliases[$abstract]);
$this->instances[$abstract] = $instance;
if ($isBound) {
$this->rebound($abstract);
}
return $instance;
}
Chúng ta không cần hiểu rõ từng dòng trong phương thức này. Nhìn qua ta có thể đoán rằng phương thức này sẽ xóa một số alias liên quan đến $abstract
và thực hiện gán một instance mới cho nó. $this->bound($abstract)
có nhiệm vụ kiểm tra xem $abstract
đã được bound với Service Container hay chưa (nó là một binding thông thường, một instance binding hay một alias?). Nếu $abstract
đã được bounded thì chúng ta sẽ thực thi các rebounding callbacks tương ứng đã đăng ký với Service Container.
Ok vậy tác dụng của Rebound và Rebinding là gì? Nếu bạn đang phát triển một Laravel package, bạn muốn người dùng package được phép mở rộng nó bằng cách thực hiện rebinding. Cách đơn giản để mở rộng một class là sử dụng setter injection. Giả sử chúng ta có một abstract class là Car
và một concrete class là BugattiChiron
(extends Car
). Khi khởi tạo một loại car nào đó chúng ta sẽ cần nhiên liệu cho nó - Fuel
(một interface). Thêm một giả thiết nữa là chúng ta đang có hai binding là fuel
và car
và những logic bên dưới nằm trong một package nào đó bên ngoài.
interface Fuel {}
class Petrol implements Fuel {}
abstract class Car
{
protected $fuel;
public function __construct(Fuel $fuel)
{
$this->fuel = $fuel;
}
public function setFuel(Fuel $fuel)
{
$this->fuel = $fuel;
}
}
class BugattiChiron extends Car {}
// Inside package service provider
$this->app->singleton('fuel', function () {
return new Petrol;
});
$this->app->singleton('car', function ($app) {
return new BugattiChiron($app->make('fuel'));
});
Nếu một developer nào đó đang sử dụng package trên của chúng ta và muốn sử dụng một loại nhiên liệu nào đó khác thay vì Petrol
, SuperUnleadedPetrol
chẳng hạn, developer đó có thể làm như sau:
$this->app->make('car')->setFuel(new SuperUnleadedPetrol)
Đôi khi đó là tất cả những gì mà developer kia cần làm nếu anh ta chỉ resolve car
ở một hoặc hai nơi nào đó. Tuy nhiên nếu car
binding được sử dụng ở, ví dụ, 10 nơi khác nhau chẳng hạn, developer sẽ phải thực hiện setter kia lặp lại nhiều lần. Liệu có cách nào khác đơn giản và DRY hơn không? Chúng ta có thể sử dụng rebinding ở đây, trong package chúng ta thay vì đăng ký binding car
như ở trên chúng ta sẽ làm như sau:
$this->app->singleton('car', function ($app) {
return new BugattiChiron($app->rebinding('fuel', function ($app, $fuel) {
$app->make('car')->setFuel($fuel);
}));
});
Phương thức rebinding()
sẽ trả về cho chúng ta instance tương ứng nếu key đã được bound trong Service Container, do đó chúng ta có thể sử dụng nó cho constructor của BugattiChiron
. Phương thức rebinding()
nhận vào hai parameters là Laravel application instance - $app
và binding mới - $fuel
. Chúng ta sau đó sẽ sử dụng setter - setFuel()
để thay đổi dạng nhiên liệu cần thiết.
OK, bây giờ nếu developer kia muốn thay đổi dạng nhiên liệu, anh ta chỉ cần rebinding fuel
một lần là xong:
$this->app->singletion('fuel', function () {
retur new SuperUnleadedPetrol;
});
Khi fule
được rebinded Service Container sẽ kích hoạt các rebounding callbacks tương ứng (được đăng ký trong phương thức rebinding()
phía trên) và SuperUnleadedPetrol
sẽ được sử dụng thay vì giá trị mặc định là Petrol
.
Extending
Một cách đơn giản hơn để thực hiện công việc trong ví dụ trên là sử dụng extending. Phương thức extend()
trong Service Container nhận vào một binding và một closure tương ứng. Closure đó sẽ nhận vào resolved instance của binding kia và ở đây chúng ta có thể thay đổi nó theo ý muốn.
$this->app->extend('car', function ($app, $car) {
$car->setFuel(new SuperUnleadedPetrol);
});
Lưu ý việc thay đổi sử dụng extend()
chỉ được thực hiện trên một binding duy nhất khác với rebinding khi chúng ta thay đổi một binding dẫn đến một binding khác cũng thay đổi theo :slight_smile:
Conditional Binding
Trong nhiều trường hợp bạn chỉ muốn bind một key với Service Container khi key đó chưa được bounded (tránh rebinding... !?), phương thức bindIf()
sẽ giúp chúng ta thực hiện việc đó, phương thức này sẽ kiểm tra xem key đã được bounded với Service Container hay chưa sử dụng phương thức bound()
đã đề cập trong phần trước, sau đó mới thực hiện việc binding. Nếu binding của bạn ở dạng singletion, bạn cần đặt parameter thứ ba của bindIf()
- $shared
thành true
:
$this->app->bindIf('foo', function ($app) {
return new Bar;
});
Contextual Binding
Giả sử chúng ta có một mẫu ô tô khác là BugattiVeyron
sử dụng dạng nhiên liệu là PremiumUnleadedPetrol
. Chúng ta có thể sử dụng contextual binding để sử dụng loại nhiên liệu khác nhau cho từng loại car, khá đơn giản như sau (các phương thức đã quá rõ ràng quan tên gọi của nó):
$this->app->when('BugattiChiron')->needs('Fuel')->give('SuperUnleadedPetrol');
$this->app->when('BugattiVeyron')->needs('Fuel')->give('PremiumUnleadedPetrol');
Tips to Make Customizations
Trong những phần trước của bài viết, chúng ta đã có những cái nhìn tổng quát cũng như chi tiết về Service Container trong Laravel. Trong phần này của bài viết, chúng ta sẽ đi qua một số cách để biến framework thành của riêng bạn (nhiều cách có thể bạn đã khá quen thuộc), và một trong những cách đó cần một chút lý thuyết về Service Container. Laravel có khá nhiều conventions, tuy nhiên chúng không phải bắt buộc. Nếu bạn không happy với những cái được cung cấp sẵn, bạn luôn có quyền thay đổi chúng. Tất cả những gì bên trong thư mục app
của framework chỉ giống như template, nó được tạo ra để bạn thay đổi. Những file trong thư mục đó luôn có đi kèm với dấu comment - //
để nhắc bạn rằng bạn có thể làm nhiều thứ hơn với class hoặc file đó. Conventions luôn đi kèm với flexibility. Let's start!
Notes about Configuration
- Bạn có thể thêm hoặc bớt các key trong các file configuration trong thư mục
config
. Ví dụ, bạn sử dụng Chatwork SDK và muốn lưu thông tin về secret keys của Chatwork, hãy tạo một key với tênchatwork
trong fileconfig/services.php
và lưu thông tin ở đây. - Nếu muốn bạn luôn có thể tạo một file configuration hoàn mới trong thư mục
config
. - Lưu những thông tin quan trọng trong file
.env
. - Không sử dụng
env()
function trực tiếp trong code do nó khá chậm và không được tận dụng được configuration caching. Tốt nhất nên sử dụng nó trong file configuration và dùngconfig()
function để reference giá trị của một key nào đó trong code của bạn.
Scaffolding Authentication
Laravel cung cấp một hệ thống authentication mặc định, bạn có thể enable nó sử dụng php artisan make:auth
console command. Tất cả các controller liên quan như LoginController
, RegisterController
, PasswordController
, các views liên quan và các routes có thể được custom lại theo ý muốn của bạn.
Default Service Providers
Các Service Provider class bên trong thư mục app/Providers
giống như một template cho bạn. Bạn có thể thay đổi, chỉnh sửa hoặc thêm và xóa các Service Provider theo ý muốn (nhớ đăng ký chúng đúng cách là ok). AppServiceProvider
dành cho những ai mới làm quen với framework, trên thực tế bạn có thể xóa nó ngay khi khởi tạo project :slight_smile:
Cách làm tương tự sẽ được áp dụng cho Middlewares
Exceptions
- Bạn có thể tạo custom exception và handle chúng trong
app/Exceptions/Handler.php
- Bạn có thể customize exception sử dụng phương thức
report()
bên trongapp/Exceptions/Handler.php
. Trong phiên bản 5.5 trở lên bạn có thể định nghĩa phương thứcreport()
trực tiếp bên trong exception class.
Include Things
Bạn có thể tách file route thành các file nhỏ hơn và include
chúng, tương tự cho việc scheduling các command, và nhiều phần khác trong cấu trúc của project.
Testing
- Bạn có thể thêm các custom assertion bên trong
TestCase
class (hoặc bất cứ thứ gì bạn muốn) - Bạn có thể bind hoặc replace instance, hoăc tạo seeder của một class trong phương thức
setUp
của test class (nếu tất cả các case đều cần chung một điều kiện ban đầu) - Bạn luôn có thể thay đổi nội dung file
phpunit.xml
- Nếu bạn cần thông tin environment riêng cho việc testing, hãy tạo một file với tên
.env.testing
trong thư mục gốc. - Nên sử dụng
RefreshDatabase
trait khi viết test. - Swappable Facades
Cache::shouldReceive('get')
->once()
->with('key')
->andReturn('value')
Helpers File
An old school technique :slight_smile:
- Tạo một file với tên
helpers.php
(hoặc bất cứ tên gì bạn muốn) chứa các helper functions. - Load file đó trong
composer.json
file
{
"autoload": {
"files": ["helpers.php"]
}
}
Extend Default Laravel Application
Refer to this article: https://mattstauffer.com/blog/extending-laravels-application
Custom Validation
- Pre 5.5, bạn có thể sử dụng
Validator::extend()
- Từ 5.5 bạn có thể tạo một custom validation class (subclass của
Illuminate\Validation\Rule
)
Custom Request Object
class CustomRequest extends \Illuminate\Http\Request
{
// Add custom method and properties.
}
// Use it in a route
public function index(CustomRequest $request) {}
// Bind globally in public/index.php if necessary
$response = $kernel->handle(
$request = CustomRequest::capture()
);
Custom Response Object
class CustomResponse extends \Illuminate\Http\Response
{
// Add custom method and properties.
}
public function index()
{
return CustomResponse::create(...);
}
// Response macro
Response::macro('custom', function ($user) {
return CustomResponse::forUser($user);
});
return response()->custom($user);
// Using Responsable interface
class FooBar implements Responsable
{
public function toResponse()
{
return new JsonResponse(); // Just for demo
}
}
Custom Eloquent Collection
class ItemCollection extends \Illuminate\Database\Eloquent\Collection
{
public function sumPrices()
{
//
}
}
// Overide default collection
class Item
{
public function newCollection(array $models = [])
{
return new ItemCollection($models);
}
}
Other Stuffs
- Response macro sử dụng
Response::macro()
- Sử dụng
Blade::directive()
- Sử dụng
Blade::if()
(5.5) - Sử dụng View Composers -
View::composer()
- Sử dụng Route Model Binding
- Sử dụng Form Request
- Manual route key binding sử dụng
Route::bind()
Conclusion
Trong bài viết khá dài này, mình đã cùng đi qua một vòng Service Container trong Laravel cách hoạt động của nó đặc biệt là hai quá trình Binding và Resolving. Thông thường bạn cũng không yêu cầu phải nắm rõ tất cả những thông tin trên (chúng ta có thể xem lại khi cần) khi làm việc. Tuy nhiên nếu bạn đang muốn tìm hiểu về các services cung cấp sẵn của framework hay muốn tự tay mình xây dựng một service, một package, thì những thông tin trên mình nghĩ sẽ cần thiết. Một điều nữa cần nhớ là Laravel không cứng nhắc vì một loạt các conventions, điều quan trọng là bạn muốn thay đổi nó ở mức độ nào mà thôi
Mong bài viết sẽ giúp ích được phần nào cho các bạn!
References
- Laravel Official Documentation
- Laracon Conference Talks
- Laracasts
All rights reserved