Tập 6: Service container Laravel
This post hasn't been updated for 2 years
Chào mừng các bạn đã quay trở lại với mình cùng series "Hành trình chinh phục Laravel Framework". Tập hôm nay mình sẽ nói về một trong những khái niệm kiến trúc (Architecture concepts) của Laravel đó chính là "Service container".
Achitecture concepts của Laravel là những khái niệm hoàn toàn mới, có thể bạn chưa tiếp xúc bao giờ nên sẽ rất khó hiểu. Thông thường bài này là ở Laravel nâng cao nhưng mình vẫn để ở tập này vì một phần nào đó nó sẽ giúp Laravel gần hơn với bạn. Việc hiểu biết về service container sẽ là một thế mạnh giúp bạn có thể xây dựng một ứng dụng tuyệt vời.
Khuyến cáo: Đây là một trong những phần Laravel nâng cao, chính vì thế sẽ gây khó hiểu, mất nghị lực, quyết tâm khi học. Mình khuyên bạn hãy đọc một lần, nếu thấy không thể tiếp thu thì có thể bỏ qua.
I. Khái niệm service container (Service container conception)
Trước tiên tìm hiểu về service container thì các bạn hãy hiểu hai thuật ngữ "dependency" và "dependency injection" trước đã. Mới đọc xong câu này chắc các bạn rất hụt hẫng khi chẳng biết nó là cái quái gì cả đúng không? Vì đây là các thuật ngữ trong lập trình nên dù có Google translate cũng vô cùng khó hiểu. "Dependency" nghĩa là "phụ thuộc", "dependency injection" nghĩa là "tiêm phụ thuộc", nghe xong lại càng mơ hồ. Chính điều đó, để giải thích hai thuật ngữ này mình sẽ làm example sau để các bạn có thể hình dung được nghĩa của nó.
Chẳng hạn mình có một file PHP với nội dung sau:
<?php
class Health {}
class Money {}
class Happy {}
class SuccessfulPeople
{
public function __construct()
{
new Health();
new Money();
new Happy();
}
}
new SuccessfulPeople();
Để trở thành một người thành công, bạn cần phải có sức khỏe, tiền bạc và hạnh phúc. Dựa vào đoạn code trên, class SuccessfulPeople
sẽ phụ thuộc vào các class Health
, Money
và Happy
. Nhưng thay vì khởi tạo các class đó bên trong People
thì ta sẽ "tiêm" chúng thông qua phương thức __construct
hoặc setter
.
<?php
// ...
class SuccessfulPeople
{
public $health, $money, $happy;
public function __construct(Health $health, Money $money, Happy $happy)
{
$this->health = $health;
$this->money = $money;
$this->happy = $happy;
}
}
new SuccessfulPeople(new Health(), new Money(), new Happy());
Các khởi tạo class Health
, Money
và Happy
là điều kiện để class People
hoạt động, đó chính là các "class dependency". Và phương thức truyền cái object đó thông qua __construct
hay setter
thì gọi là "dependency injection".
Trước khi tiếp tục mình xin nói về một thuật ngữ nữa, đó là là "type-hint". Hiểu đơn giản nó chính là từ khóa đứng trước biến khi inject dependency vào các method. Như ở đoạn code trên, tại method __construct
chúng ta nhận ba tham số, mỗi tham số đều có các "type-hint" đứng trước, đại diện cho việc hiểu tham số đó chính là class tương ứng với tên của "type-hint".
Nhưng tại sao lại dùng cơ chế dependency injection? Mình sẽ sử dụng lại example trên để nêu ra lợi ích của cơ chế này. Bây giờ mình muốn lấy thông tin về tài sản mà người đó có được. Thì nếu như chưa áp dụng dependency injection thì đoạn code sẽ như thế này:
<?php
// ...
class SuccessfulPeople
{
public function __construct()
{
new Health();
new Money();
new Happy();
}
public function getMoney()
{
return new Money();
}
}
$people = new SuccessfulPeople();
$people->getMoney();
Như quan sát, để đáp ứng yêu cầu trên ta phải khởi tạo object Money
hai lần. Nhưng với dependency injection thì lại khác:
<?php
// ...
class SuccessfulPeople
{
public $health, $money, $happy;
public function __construct(Health $health, Money $money, Happy $happy)
{
$this->health = $health;
$this->money = $money;
$this->happy = $happy;
}
public function getMoney()
{
return $this->money;
}
}
$people = new SuccessfulPeople(new Health(), new Money(), new Happy());
$people->getMoney();
Ta chỉ khởi tạo Money
một lần duy nhất và lưu nó vào trong public $money
, chính vì thế nó vẫn đáp ứng được yêu cầu.
Tóm lại, cơ chế dependency injection chúng ta phải tuân thủ 2 điều kiện thiết yếu:
- Các class dependency được truyền qua phương thức
__construct
hoặcsetter
.- Trong class chính phải có biến lưu trữ các class dependency để dễ dàng gọi trong các method.
Vậy service container trong Laravel chính là công cụ hữu ích để quản lý các class dependency và depedency injection.
II. Binding
Đây là một thuật ngữ trong Laravel, "binding" được hiểu là dùng để đăng ký một class hay interface trong service container. Việc binding này thường được code ở các file service provider trong thư mục app/Provider
tại method register
.
Chú ý: Bắt đầu từ đây với mỗi code example, mình không sử dụng lại nội dung phía trước nên mỗi khi qua một ví dụ mới, bạn nên đưa các file đã thay đổi ở ví dụ trước đó về trạng thái ban đầu để tránh lỗi code. Chân thành cảm ơn!
1. Khái niệm cơ bản binding (Bindings basic)
Trong một file service provider, bạn luôn có quyền truy cập đến container thông qua thuộc tính $this->app
.
a. Binding simple
Chúng ta có thể bind đơn giản bằng cách sử dụng method bind
, truyền tên class hoặc interface chúng ta mong muốn đăng ký cùng với một Closure trả về lớp được khởi tạo.
Các bạn mở file app/Providers/AppServiceProvider.php
lên và viết đoạn code này trong method register
:
$this->app->bind('MyUser', function($app) {
return new \App\User;
});
Chú ý: Tại Closure, có thể nhận tham số
$app
để có thể truy cập các thuộc tính của objectApp
.
Trong đó: MyUser
chính là tên class thay thế mình mong muốn cho \App\User
(đây là namespace class của file app/User.php
).
Để kiểm chúng cho việc đã binding thành công chưa, các bạn mở file routes/web.php
và sửa lại code như thế này:
Route::get('/', function () {
dd(app()); // Dòng code để kiểm chứng việc đã bind thành công
});
Về dòng code kiểm chứng, đại loại nó sẽ dump object App
để ta có thể quan sát các thuộc tính của nó.
Bây giờ các bạn nạp server và chạy đường dẫn http://localhost:8000, tại đây các bạn quan sát tại thuộc tính bindings
trong object App
:
Như vậy là ta đã bind thành công rồi đấy.
b. Binding a singleton
Giống như tên của nó, phương thức bind này khai báo với Laravel rằng khi khởi tạo class này thì chỉ một lần duy nhất, những yêu cầu khởi tạo kế tiếp trong cùng một request chỉ trả lại object đã tạo lần đầu.
Các bạn sẽ hiểu rõ hơn khi quan sát mô hình so sánh này:
Để thực hiện bind singleton cũng rất đơn giản, bạn chỉ việc thay thế bind
thành singleton
như thế này:
$this->app->singleton('MyUser', function($app) {
return new \App\User;
});
Bây giờ để kiểm tra xem có đúng là singleton chỉ tạo được duy nhất một object dù cho có yêu cầu khởi tạo nhiều lần hay không thì bạn chỉnh sửa code ở file routes/web.php
:
Route::get('/', function () {
dd([
app()->make('MyUser'),
app()->make('MyUser')
]);
});
Dòng app()->make('MyUser')
dùng để khởi tạo class MyUser
vừa mới được bind ở phía trên theo phương thức singleton
, cái này chỉ dùng để test nên bạn không cần phải hiểu bây giờ dòng này đâu nhé! Ở đây mình gọi hai lần để xem nó có trả về cùng một object hay không.
Nạp server chạy lại và đây là kết quả:
Dù gọi hai lần như nó vẫn trả về cùng một object, đó chính là singleton
.
c. Binding instances
Đây chẳng qua là một biến tấu của singleton
. Nó cũng sẽ trả về một object duy nhất dù có gọi bao nhiêu lần, nhưng object khởi tạo đầu tiên không phải ở trong Closure mà là ở ngoài, nằm ở trước bind instance
.
$user = new \App\User; // Khởi tạo object lần đầu
$this->app->instance('MyUser', $user);
Khác với bind
và singleton
, phương thức instance
chỉ nhận tham số thứ hai là một object chứ không phải là Closure nữa.
Bây giờ vẫn giữ nguyên đoạn code kiểm chứng và chạy lại:
Nó vẫn trả về cùng một object giống như singleton
. Việc sử dụng instance
sẽ giúp bạn linh hoạt hơn trong việc binding.
d. Binding primitives
Có thể nói đây là cách mà ta có thể tự động inject các dependency vào trong class. Nhìn vào đoạn code dưới đây:
$this->app->when('App\User')
->needs('$id')
->give(1);
Trong đó:
when
chứa tham số namespace class nhận dependency, có thể nhận mảng. Ở đây là classApp\User
của fileapp/User.php
needs
chứa tên biến hoặc classgive
chứa giá trị củaneeds
, có thể là Closure (trong trường hợp muốn gán giá trị là một object class...)
Tiếp theo các bạn mở file app/User.php
và thêm đoạn code này vào dưới cùng:
public function __construct($id)
{
echo $id;
}
Ngay tại __construct
, chúng ta đã thêm tham số $id
để nhận dependency từ phía binding primitives.
Để kiếm chứng, tại routes/web.php
, các bạn sửa code thành như này:
Route::get('/', function() {
app()->make('App\User');
});
Bây giờ nạp server và xem kết quả:
Một ví dụ thực tế cho phương thức này đó chính là giả sử bạn đang làm một dự án chia sẻ hình ảnh, video trực tuyến cho người dùng. Bạn muốn ảnh đại diện của người dùng thì được lưu tại máy chủ, còn video và ảnh khác thì lưu tại Amazon S3. Đây chính là lúc binding primitives thể hiện khả năng của mình.
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\AvatarController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
$this->app->when(AvatarController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when([PhotoController::class, VideoController::class])
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});
Bạn không cần hiểu quá chi tiết về đoạn code trên, vì chúng ta chỉ mới bắt đầu. Bạn chỉ cần hiểu cho mình thể này:
- Khi class
AvatarController
được gọi thì nó sẽ được inject một dependency với giá trị là một objectStorage::disk('local')
tại type-hint làFilesystem
để có thể thao tác lưu trữ tại server. - Khi class
PhotoController
hoặcVideoController
được gọi thì nó sẽ được inject một dependency với giá trị là một objectStorage::disk('s3')
tại type-hint làFilesystem
để có thể thao tác lưu trữ tại Amazon S3.
2. Binding interfaces to implementations.
Đây là một công cụ mạnh mẽ trong service container. Bạn còn nhớ nội dung file bootstrap/app.php
hôm trước chứ. Tại nhiệm vụ bind các interface quan trọng, các binding dưới đây là một ví dụ cụ thể cho cơ chế này:
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
Nói dễ hiểu, cơ chế này có thể bind một interface cho một implementation cụ thể.
Chẳng hạn chúng ta có một interface Logger
và một implementation ErrorLogger
.
$app->singleton(
Interface\Logger::class,
ErrorLogger::class
);
Đoạn code trên nói cho container biết rằng nó phải inject interface Logger
như một dependency cho implementation ErrorLogger
. Bây giờ chúng ta có thể type-hint interface Logger
tại __construct
của implementation ErrorLogger
:
use Interface\Logger;
class ErrorLogger
{
public function __construct(Logger $logger)
{
//
}
}
3. Tagging
Có thể từ này đã quá quen thuộc với rất nhiều bạn rồi. Nó là từ đại điện cho một nhóm có nội dung, đặc điểm hay tính chất chung. Chẳng hạn ErrorReport
, MemoryReport
, ActiveReport
đều có thẻ chung là Report
.
Mình sẽ lấy dẫn chứng ở trên để đưa vào code. Chúng ta sẽ sử dụng method tag
để nhóm các binding lại với nhau.
$this->app->bind('ErrorReport', function () {
//
});
$this->app->bind('MemoryReport', function () {
//
});
$this->app->bind('ActiveReport', function () {
//
});
$this->app->tag(['ErrorReport', 'MemoryReport', 'ActiveReport'], 'reports');
Trong đó:
- Tham số thứ nhất là các thành phần binding.
- Tham số thứ hai là tên thẻ.
Bây giờ chúng ta sẽ inject chúng vào class ReportAggregator
:
$this->app->bind('ReportAggregator', function ($app) {
return new ReportAggregator($app->tagged('reports'));
});
Để lấy các dependency từ tagging, chúng ta sử dụng method tagged
với tham số duy nhất là tên của thẻ.
4. Extending binding
Đây là một cơ chế cho phép thay đổi một service đã được khởi tạo. Method extend
có thể nhận Closure làm tham số cho việc thêm code.
Bây giờ chúng ta tiến hành thử nghiệm. Mình sẽ sử dụng file app/User.php
có sẵn và tạo thêm một model DecoratedUser
nữa bằng lệnh Artisan:
php artisan make:model DecoratedUser
Các bạn mở file app/DecoratedUser.php
vừa tạo lên và thêm đoạn code này vào dưới cùng:
protected $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function decoratedTest()
{
return 'Decorated: ' . $this->user->test();
}
Tại file app/User.php
, bạn viết thêm method user
như sau:
public function test()
{
return "test() method of User class";
}
Giờ mới là phần chính, mở file app/Providers/AppServiceProvider.php
và code đoạn extend tại method register
:
$this->app->extend('App\User', function($user) {
return new \App\DecoratedUser($user);
});
Cuối cùng để kiểm chứng, tại routes/web.php
các bạn sửa thành đoạn code sau:
Route::get('/', function() {
return app()->make('App\User')->decoratedTest();
});
Chúng ta sẽ có kết quả như thế này:
Với kết quả như thế này các bạn thử đoán xem
app()->make('App\User')
sẽ trả về objectApp\User
hayApp\DecoratedUser
?
Đáp án đó chính là object App\DecoratedUser
. Nếu bạn không tin có thể thay app()->make('App\User')->decoratedTest()
thành app()->make('App\User')->test()
xem, chắc chắn sẽ xuất hiện một lỗi như này:
Đây là bằng chứng cho việc app()->make('App\User')
trả về object App\DecoratedUser
. Vậy nếu ứng dụng bạn đòi hỏi app()->make('App\User')
vừa có thể gọi method decoratedTest
trong App\DecoratedUser
vừa có thể gọi test()
trong App\User
thì phải giài quyết như thế nào?
Rất đơn giản, bạn chỉ cần cho class App\DecoratedUser
extend với class App\User
là ok.
// ...
use App\User;
class DecoratedUser extends User
{
// ...
Bây giờ chắc chắn chúng ta đã có thể thực thi câu lệnh app()->make('App\User')->test()
rồi đấy.
Ngoài ra, trong tham số Closure của method extend
, ngoài nhận biến chứa binding thì nó còn nhận biến thứ hai đó là $app
giúp chúng ta có thể linh hoạt trong quá trình code.
$this->app->extend('App\User', function($user, $app) {
//
});
Lưu ý: Trong trường hợp binding mà không thể tham chiếu tới
$app
, bạn có thể sử dụng hàm app() (một trong những global helper function, mình sẽ nói vấn đề này ở những tập sau) để thay thế. Ta có thể áp dụng cách này cho cả binding và resolve.
Chẳng hạn thay vì:
$this->app->bind('MyUser', function() {
//
});
Ta có thể sử dụng app()
để thay thế nếu không thể tham chiếu tới $app
:
app()->bind('MyUser', function() {
//
});
III. Resolving
Phương thức này dùng để khởi tạo các class đã được bind hoặc lấy các object đã được khởi tạo từ binding a singleton hoặc binding intances.
1. Một số cách resovle (Some resolve ways)
Bạn có thể sử dụng method make
để resolve một class trong container:
$this->app->make('App\User');
hoặc dùng global helper function app()
để resolve:
app()->make('App\User');
Cách này giống như dòng code kiểm chứng mà mình để cập ở phần trên.
Ngoài ra bạn có thể sử dụng một global helper function nữa là resolve()
để làm thao tác này.
resolve('App\User');
Trong quá trình code, đôi khi bạn không thể tham chiếu đến $app
, nên hãy tùy biến trong ngữ cảnh mà sử dụng các cách khác nhau.
Laravel cung cấp cho chúng ta method makeWith
để truyền tham số cho class khi resolve.
$this->app->makeWith('App\User', ['id' => 1])
Trong đó:
- Tham số thứ nhất là class cần resolve
- Tham số thứ hai là một mảng với các dữ liệu cần inject (không thể truyền object)
Tại file app/User.php
để nhận tham số, ta chỉ cần thêm biến ở method __construct
:
public function __construct($id)
{
//
}
2. Automatic injection
Đây mà một cơ chế hữu ích và quan trọng trong quá trình code, vì hầu như bạn luôn gặp nó khi code ở bất cứ phần nào, bao gồm controller, middleware, event listener...
Với cơ chế tự động inject này, bạn có thể inject một class dependency bất kỳ ở tất cả các method bằng cách truyền tham số biến cùng với "type-hint" namespace class.
Dưới đây là đoạn code mẫu của một file controller có sử dụng automatic injection:
<?php
namespace App\Http\Controllers;
use App\Users\Repository as UserRepository;
class UserController extends Controller
{
protected $users;
public function __construct(UserRepository $users)
{
$this->users = $users;
}
}
Như vậy khi route thực thi controller thì container sẽ tự động inject class dependency UserRepository
cho method __construct
của UserController
.
Ta có thể áp dụng cơ chế này cho cả method handle
của queued jobs (sẽ tìm hiểu ở các tập sau).
IV. Container events
Service container sẽ kích hoạt một sự kiễn mỗi khi ta resovle một object. Để có thể bắt sự kiện đó, ta chỉ cần sử dụng method resolving
:
$this->app->resolving(function ($object, $app) {
// Được gọi khi resolve bất kỳ object nào
});
$this->app->resolving(App\User::class, function ($user, $app) {
// Được gọi khi resolve object với class là "App\User"
});
Cơ chế này giúp bạn tùy biến thêm cho ứng dụng, cho phép bạn bổ sung thêm thuộc tính gì đó trước khi trả về object cần resolve.
Cảm ơn các bạn đã quan tâm theo dõi. Cùng đồng hành với mình qua những tập tiếp theo tại series "Hành trình chinh phục Laravel Framework" nhé! Chúc may mắn và hẹn gặp lại.
Mình đang xây dựng blog riêng là lechihuy.dev , mong các bạn ghé sang ủng hộ, mình cảm ơn rất nhiều ạ
All Rights Reserved