Giới thiệu Service container trong Laravel:
Service container là tool mạnh mẽ để quản lý các class dependency và thực thi Dependency injection. Dependency injection nghĩa là 1 class phụ thuộc (dependency) được tiêm (inject) vào class khác thông qua constructor hoặc setter method. Để sử dụng Service Container thì có 2 khái niệm đi liền với nó:
- Binding: là thao tác đăng ký 1 class hay interface với Container.
- Resolve: là thao tác lấy ra 1 instance từ trong Container.
Binding & Resolve:
Hầu hết việc binding vào service container sẽ do các service provider thực hiện. Không cần phải bind class vào container nếu như nó không phụ thuộc vào interface nào. Trong service provider luôn có quyền truy cập vào container thông qua $this->app.
Ví dụ: chúng ta có 1 class FooService cung cấp 1 số chức năng cụ thể:
<?php
namespace App\Services;
class FooService
{
public function __construct()
{
// ...
}
public function doSomething()
{
// ...
}
}Thông thường để sử dụng FooService ta làm như sau:
$fooService = new \App\Service\FooService();
$fooService->doSomething();Để bind vào container ta làm như sau:
$this->app->bind('FooService', \App\Service\FooService::class);Tuy nhiên cũng có những cách khác để bind vào container tùy vào cách sử dụng:
- Binding Singleton: instance sẽ được resolve 1 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:
$this->app->singleton('now', function() {
return time();
});- Binding Instance: chúng ta có 1 instance đang tồn tại và chúng ta bind nó vào Service Container. Mỗi lần lấy ra chúng ta sẽ nhận được đúng instance này:
$now = time();
$this->app->instance('now', $now);Lưu ý binding phải được thực hiện trong hàm register của service provider.
Làm thế nào để Resolve:
Khi class được register vào container, có thể truy xuất ở bất kỳ vị trí nào trong ứng dụng:
$fooService = $this->app->make('FooService');
$fooService->doSomething();
// ở vị trí mà không truy cập được $app thì sử dụng helper resolve
$fooService = resolve('FooService');
// combine:
app()->make('FooService')->doSomething();Chúng ta sẽ đi vào 1 ví dụ cụ thể hơn:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\Models\User;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected UserRepository $users,
) {}
/**
* Show the profile for the given user.
*/
public function show(string $id): View
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}Trong ví dụ này, UserController cần truy xuất users từ data. Vì vậy chúng ta inject 1 service có chức năng đó (UserRepository). Tuy nhiên vì repository được inject, chúng ta dễ dàng hoán đổi nó ra ngoài với implement khác. Chúng ta cũng có thể dễ dàng “mock”, hoặc tạo 1 dummy implement của UserRepository khi test ứng dụng.
Giải pháp không cần cấu hình:
Nếu 1 class không có dependencies hoặc chỉ phụ thuộc vào class cụ thể khác (không phải là interface), container không cần hướng dẫn cách resolve class đó. Ví dụ, bạn có thể đặt đoạn code dưới đây trong routes/web.php:
class Service
{
// do something
}
Route::get('/', function(Service $service) {
die(get_class($service));
});Trong ví dụ này, ứng dụng tự động resolve class Service và inject nó vào route handler. Điều này có nghĩa là bạn có thể xây dựng ứng dụng và có được lợi thế của dependency injection mà không phải lo lắng về việc cấu hình.
Nhiều class bạn sẽ viết khi xây dựng ứng dụng có thể tự động nhận dependency thông qua container, bao gồm controller, event listener, middleware và nhiều class khác … Ngoài ra bạn có thể type-hint dependency trong method handle của queued jobs.
Khi nào sử dụng container:
Bạn có thể type-hint dependency trên route, controller, event listener … mà không cần xử lý bằng tay với container. Ví dụ bạn có thể type-hint đối tượng Illuminate\Http\Request trong route, vì vậy bạn dễ dàng tiếp cận request. Mặc dù chúng ta không bao giờ phải làm việc với container để viết code này, nó vẫn quản lý injection của các dependency ở phía sau:
use Illuminate\Http\Request;
Route::get('/', function(Request $request) {
// code here
});Trong nhiều trường hợp nhờ có auto dependency injection và facades, bạn có thể build ứng dụng mà không cần bind bằng tay hoặc resolve bất kỳ gì từ container. Những trường hợp mà bạn cần xử lý bằng tay với container:
1 là nếu bạn viết 1 class implement 1 interface và bạn muốn type-hint interface đó vào route hoặc hàm constructor của 1 class, bạn phải cho container cách để resolve interface đó. 2 là nếu bạn viết 1 Laravel package, bạn cần bind các service của package đó vào container.
Binding:
Binding basic:
Hầu hết service container binding sẽ được đăng ký trong service provider.
Với Service Provider, bạn có thể tiếp cận tới container thông qua $this->app. Bạn có thể đăng ký 1 binding sử dụng phương thức bind, truyền vào tên class hoặc interface mà bạn muốn đăng ký cùng với 1 closure mà trả về 1 instance của class:
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
$this->app->bind(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});Lưu ý chúng ta có thể nhận bản thân container như 1 tham số đến resolver. Chúng ta sau đó có thể sử dụng container để resolve sub-dependency của đối tượng chúng ta đang build.
Như đã đề cập, bạn có thể tương tác với container ở trong service provider, tuy nhiên nếu bạn muốn tương tác với container ở ngoài service provider, bạn có thể thông qua facade App:
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;
App::bind(Transistor::class, function (Application $app) {
// ...
});Bạn có thể sử dụng method bindIf để register 1 container binding chỉ nếu 1 binding chưa dược đăng ký:
$this->app->bindIf(Transistor::class, function(Application $app) {
return new Transistor($app->make(PodcastParser::class));
});Lưu ý lại lần nữa: không cần bind class vào container nếu nó không phụ thuộc vào interface nào. Container không cần được hướng dẫn cách build những object này, vì nó có thể tự động resolve những object này sử dụng ánh xạ.
Binding Singleton:
Phương thức singleton bind 1 class hoặc interface vào container mà chỉ resolve 1 lần. Một khi đối tượng đã được resolve, cùng 1 instance này sẽ được trả về trong những lần gọi tiếp theo:
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
$this->app->singleton(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});Bạn có thể sử dụng phương thức singletonIf để đăng ký 1 singleton binding chỉ nếu khi binding chưa đăng ký:
$this->app->singletonIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});Binding scoped singletons:
Method scoped bind 1 class hoặc interface vào container mà chỉ được resolve 1 lần trong vòng đời request của ứng dụng. Method này tương tự singleton, nhưng instance sử dụng method scoped sẽ được flush khi ứng dụng bắt đầu vòng đời mới. Ví dụ khi Laravel queue thực thi 1 tác vụ mới:
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
$this->app->scoped(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});Binding instances:
Bạn có thể bind 1 object instance đã tồn tại vào container sử dụng method instance. Instance này sẽ được trả về trong lần gọi tiếp theo tới container:
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);Bind interfaces đến implements:
Service container cho phép bind 1 interface đến 1 implement cho trước. Ví dụ ta có 1 interface EventPusher và class implement RedisEventPusher. Register trong container như sau:
use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
$this->app->bind(EventPusher::class, RedisEventPusher::class);Lệnh này báo cho container biết nó sẽ inject RedisEventPusher khi 1 class cần 1 thực thi của EventPusher. Bây giờ chúng ta có thể type-hint interface EventPusher trong constructor của class. Ví dụ controller, event listener, middleware …
use App\Contracts\EventPusher;
/**
* Create a new class instance.
*/
public function __construct(
protected EventPusher $pusher
) {}Contextual Binding:
Thi thoảng bạn có 2 class sử dụng 1 interface, bạn cần inject 2 implement khác nhau cho 2 tình huống khác nhau. Ví dụ 2 controller khác nhau cần inject 2 implement khác nhau của Illuminate\Contracts\Filesystem\Filesystem contract:
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\UploadController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});Binding Primitives (giá trị nguyên thủy):
Thi thoảng bạn có 1 class nhận một số injected class khác, nhưng cũng cần 1 giá trị nguyên thủy ví dụ như integer. Bạn có thể sử dụng contextual binding để inject bất kỳ giá trị nào mà class cần:
use App\Http\Controllers\UserController;
$this->app->when(UserController::class)
->needs('$variableName')
->give($value);Thi thoảng 1 class có thể phụ thuộc vào 1 mảng tagged instances (xem ở phần dưới). Sử dụng giveTagged method, bạn có thể inject tất cả container binding với tag đó:
$this->app->when(ReportAggregator::class)
->needs('$reports')
->giveTagged('reports');Nếu bạn muốn inject 1 giá trị từ file cấu hình, sử dụng method giveConfig:
$this->app->when(ReportAggregator::class)
->needs('$timezone')
->giveConfig('app.timezone');Binding Typed Variadics:
Thi thoảng bạn có thể có 1 class nhận 1 array typed objects sử dụng tham số variadic constructor:
<?php
use App\Models\Filter;
use App\Services\Logger;
class Firewall
{
/**
* The filter instances.
*
* @var array
*/
protected $filters;
/**
* Create a new class instance.
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}Sử dụng contextual binding, bạn có thể resolve dependency này bằng cách pass cho method give 1 closure mà trả về 1 array của Filter instance:
$this->app->when(Firewall::class)
->needs(Filter::class)
->give(function (Application $app) {
return [
$app->make(NullFilter::class),
$app->make(ProfanityFilter::class),
$app->make(TooLongFilter::class),
];
});Để thuận tiện, bạn có thể chỉ cần cung cấp tên class:
$this->app->when(Firewall::class)
->needs(Filter::class)
->give([
NullFilter::class,
ProfanityFilter::class,
TooLongFilter::class,
]);Variadic Tag Dependency:
Thi thoảng 1 class có thể có 1 variadic dependency mà được type-hint như 1 class đã cho (Report ...$reports). Sử dụng giveTagged để inject toàn bộ binding:
$this->app->when(ReportAggregator::class)
->needs(Report::class)
->giveTagged('reports');Tagging:
Thi thoảng bạn cần resolve tất cả 1 nhóm binding, ví dụ bạn build 1 phân tích báo cáo nhận 1 mảng nhiều implement của interface Report. Sau khi register các implement của Report, bạn có thể gán tag cho chúng sử dụng method tag:
$this->app->bind(CpuReport::class, function () {
// ...
});
$this->app->bind(MemoryReport::class, function () {
// ...
});
$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');Khi các services được tag, bạn có thể dễ dàng resolve sử dụng method tagged:
$this->app->bind(ReportAnalyzer::class, function (Application $app) {
return new ReportAnalyzer($app->tagged('reports'));
});Extend Bindings:
Method extend cho phép sửa đổi resolve services. Ví dụ khi 1 service được resolve, bạn có thể chạy 1 đoạn code để sửa đổi service. Method này chấp nhận 2 tham số, class service và 1 closure trả về service đã sửa đổi:
$this->app->extend(Service::class, function (Service $service, Application $app) {
return new DecoratedService($service);
});Resolve:
Sử dụng method make: method này chấp nhận tên class hoặc interface bạn muốn resolve:
use App\Services\Transistor;
$transistor = $this->app->make(Transistor::class);Một số dependency không thể resolve qua container, bạn có thể inject chúng bằng cách pass chúng như 1 array tương ứng vào method makeWith. Ví dụ, chúng ta có thể pass bằng tay tham số constructor $id được yêu cầu bởi Transistor:
use App\Services\Transistor;
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);Method bound có thể sử dụng để xác định 1 class hoặc interface có ràng buộc rõ ràng trong container hay không:
if ($this->app->bound(Transistor::class)) {
// ...
}Nếu bạn ở ngoài 1 service provider và không thể tiếp cận $app, bạn có thể sử dụng App facade hoặc app helper:
use App\Services\Transistor;
use Illuminate\Support\Facades\App;
$transistor = App::make(Transistor::class);
$transistor = app(Transistor::class);Nếu bạn muốn chính bản thân container instance được resolve, bạn có thể type-hint class Illuminate\Container\Container vào class constructor:
use Illuminate\Container\Container;
/**
* Create a new class instance.
*/
public function __construct(protected Container $container) {}
Tự động injection:
Bạn có thể type-hint dependency trong constructor của class mà được resolve bởi container, bao gồm controller, event listener, middleware … Bạn cũng có thể type-hint dependency trong method handle của queue jobs. Trong thực hành đây là cách mà hầu hết object được resolve bởi container:
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
use App\Models\User;
class UserController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected UserRepository $users,
) {}
/**
* Show the user with the given ID.
*/
public function show(string $id): User
{
$user = $this->users->findOrFail($id);
return $user;
}
}Method Invocation & Injection:
Thi thoảng bạn cần gọi 1 method hoặc 1 object instance trong khi cho phép container tự động inject những method của dependency. Ví dụ class cho dưới đây:
<?php
namespace App;
use App\Repositories\UserRepository;
class UserReport
{
/**
* Generate a new user report.
*/
public function generate(UserRepository $repository): array
{
return [
// ...
];
}
}Bạn có thể gọi method generate thông qua container như sau:
use App\UserReport;
use Illuminate\Support\Facades\App;
$report = App::call([new UserReport, 'generate']);Method call của container được dùng để gọi 1 closure trong khi tự động inject các dependency của nó:
use App\Repositories\UserRepository;
use Illuminate\Support\Facades\App;
$result = App::call(function (UserRepository $repository) {
// ...
});Container Events:
Service container sẽ kích hoạt 1 sự kiện mỗi khi nó resolve 1 object. Bạn có thể lắng nghe sự kiện đó sử dụng resolving method:
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
// Called when container resolves objects of type "Transistor"...
});
$this->app->resolving(function (mixed $object, Application $app) {
// Called when container resolves object of any type...
});