Giao diện
Service Container
Giới thiệu (Introduction)
Laravel service container là công cụ mạnh mẽ để quản lý class dependencies và thực hiện dependency injection. Dependency injection là cụm từ nghe phức tạp nhưng bản chất có nghĩa: các dependencies của class được "inject" (tiêm) vào class qua constructor hoặc, trong một số trường hợp, qua "setter" methods.
Xem ví dụ đơn giản:
php
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
use Illuminate\View\View;
class PodcastController extends Controller
{
/**
* Tạo controller instance mới.
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* Hiển thị thông tin podcast.
*/
public function show(string $id): View
{
return view('podcasts.show', [
'podcast' => $this->apple->findPodcast($id)
]);
}
}Trong ví dụ này, PodcastController cần lấy podcasts từ nguồn dữ liệu. Vì service được inject, chúng ta có thể dễ dàng "mock" (tạo bản giả) khi testing.
Hiểu sâu về Laravel service container là điều thiết yếu để xây dựng ứng dụng lớn, mạnh mẽ, cũng như để đóng góp vào Laravel core.
Phân giải không cần cấu hình (Zero Configuration Resolution)
Nếu một class không có dependencies hoặc chỉ phụ thuộc vào các concrete classes (không phải interfaces), container không cần được chỉ dẫn cách phân giải class đó:
php
<?php
class Service
{
// ...
}
Route::get('/', function (Service $service) {
dd($service::class);
});Container tự động phân giải class Service và inject vào handler. Nhiều classes bạn viết khi xây dựng ứng dụng Laravel tự động nhận dependencies qua container, bao gồm controllers, event listeners, middleware, và nhiều hơn.
Khi nào sử dụng Container (When to Utilize the Container)
Nhờ zero configuration resolution, bạn thường type-hint dependencies trên routes, controllers, event listeners mà không cần tương tác trực tiếp với container:
php
use Illuminate\Http\Request;
Route::get('/', function (Request $request) {
// ...
});Trong nhiều trường hợp, nhờ automatic dependency injection và facades, bạn có thể xây dựng ứng dụng Laravel mà không cần manually bind hay resolve bất cứ gì từ container.
Vậy khi nào bạn cần manually tương tác với container? Hai tình huống:
- Nếu bạn viết class implement một interface và muốn type-hint interface đó, bạn phải chỉ cho container cách phân giải interface.
- Nếu bạn viết Laravel package để chia sẻ, bạn có thể cần bind services của package vào container.
Binding
Cơ bản về Binding (Binding Basics)
Simple Bindings
Hầu hết tất cả service container bindings sẽ được đăng ký trong service providers. Trong service provider, bạn luôn có quyền truy cập container qua property $this->app. Đăng ký binding bằng method bind:
php
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));
});Bạn cũng có thể sử dụng App facade ngoài service provider:
php
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;
App::bind(Transistor::class, function (Application $app) {
// ...
});Dùng bindIf để chỉ đăng ký binding nếu chưa tồn tại:
php
$this->app->bindIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});Bạn có thể bỏ qua tên class/interface riêng biệt và để Laravel suy luận type từ return type của closure:
php
App::bind(function (Application $app): Transistor {
return new Transistor($app->make(PodcastParser::class));
});GỢI Ý
Không cần bind classes vào container nếu chúng không phụ thuộc vào bất kỳ interface nào. Container có thể tự động phân giải các objects này bằng reflection.
Binding Singleton
Method singleton bind class/interface vào container và chỉ phân giải một lần. Các lần gọi tiếp theo sẽ trả về cùng object instance:
php
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));
});Hoặc sử dụng attribute #[Singleton]:
php
<?php
namespace App\Services;
use Illuminate\Container\Attributes\Singleton;
#[Singleton]
class Transistor
{
// ...
}Binding Scoped Singletons
Method scoped bind class/interface và chỉ phân giải một lần trong vòng đời request/job. Khác với singleton, instances đăng ký bằng scoped sẽ bị xóa khi ứng dụng Laravel bắt đầu "lifecycle" mới (ví dụ khi Laravel Octane worker xử lý request mới):
php
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));
});Hoặc sử dụng attribute #[Scoped]:
php
<?php
namespace App\Services;
use Illuminate\Container\Attributes\Scoped;
#[Scoped]
class Transistor
{
// ...
}Binding Instances
Bạn cũng có thể bind một object instance đã tồn tại vào container bằng method instance:
php
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);Binding Interfaces với Implementations
Một tính năng rất mạnh mẽ là khả năng bind interface với implementation cụ thể:
php
use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
$this->app->bind(EventPusher::class, RedisEventPusher::class);Câu lệnh này nói cho container rằng nên inject RedisEventPusher khi class cần implementation của EventPusher:
php
use App\Contracts\EventPusher;
public function __construct(
protected EventPusher $pusher,
) {}Bind Attribute
Laravel cũng cung cấp Bind attribute. Bạn có thể áp dụng cho interface để chỉ định implementation nào được inject, kể cả theo môi trường:
php
<?php
namespace App\Contracts;
use App\Services\FakeEventPusher;
use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;
#[Bind(RedisEventPusher::class)]
#[Bind(FakeEventPusher::class, environments: ['local', 'testing'])]
interface EventPusher
{
// ...
}Contextual Binding
Đôi khi hai classes sử dụng cùng interface nhưng bạn muốn inject implementations khác nhau:
php
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
Dùng contextual binding để inject giá trị primitive:
php
use App\Http\Controllers\UserController;
$this->app->when(UserController::class)
->needs('$variableName')
->give($value);Dùng giveTagged để inject tất cả bindings theo tag:
php
$this->app->when(ReportAggregator::class)
->needs('$reports')
->giveTagged('reports');Dùng giveConfig để inject giá trị từ file cấu hình:
php
$this->app->when(ReportAggregator::class)
->needs('$timezone')
->giveConfig('app.timezone');Binding Typed Variadics
Khi class nhận mảng typed objects qua variadic constructor argument:
php
<?php
use App\Models\Filter;
use App\Services\Logger;
class Firewall
{
protected $filters;
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}Phân giải bằng contextual binding:
php
$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),
];
});Hoặc đơn giản truyền mảng class names:
php
$this->app->when(Firewall::class)
->needs(Filter::class)
->give([
NullFilter::class,
ProfanityFilter::class,
TooLongFilter::class,
]);Tagging (Gắn thẻ)
Đôi khi bạn cần phân giải tất cả bindings của một "category". Sau khi đăng ký implementations, gán tag bằng method tag:
php
$this->app->bind(CpuReport::class, function () {
// ...
});
$this->app->bind(MemoryReport::class, function () {
// ...
});
$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');Phân giải tất cả bằng method tagged:
php
$this->app->bind(ReportAnalyzer::class, function (Application $app) {
return new ReportAnalyzer($app->tagged('reports'));
});Mở rộng Bindings (Extending Bindings)
Method extend cho phép sửa đổi resolved services:
php
$this->app->extend(Service::class, function (Service $service, Application $app) {
return new DecoratedService($service);
});Resolving (Phân giải)
Method make
Dùng method make để phân giải class instance từ container:
php
use App\Services\Transistor;
$transistor = $this->app->make(Transistor::class);Nếu một số dependencies không thể phân giải qua container, truyền chúng qua makeWith:
php
use App\Services\Transistor;
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);Dùng method bound để kiểm tra class/interface đã được bind hay chưa:
php
if ($this->app->bound(Transistor::class)) {
// ...
}Ngoài service provider, dùng App facade hoặc app helper:
php
use App\Services\Transistor;
use Illuminate\Support\Facades\App;
$transistor = App::make(Transistor::class);
$transistor = app(Transistor::class);Automatic Injection (Tiêm tự động)
Bạn có thể type-hint dependency trong constructor của class được container phân giải, bao gồm controllers, event listeners, middleware, và nhiều hơn:
php
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
class PodcastController extends Controller
{
public function __construct(
protected AppleMusic $apple,
) {}
public function show(string $id): Podcast
{
return $this->apple->findPodcast($id);
}
}Gọi Method và Injection (Method Invocation and Injection)
Đôi khi bạn muốn gọi method trên object instance trong khi container tự động inject dependencies:
php
<?php
namespace App;
use App\Services\AppleMusic;
class PodcastStats
{
public function generate(AppleMusic $apple): array
{
return [
// ...
];
}
}Gọi method generate qua container:
php
use App\PodcastStats;
use Illuminate\Support\Facades\App;
$stats = App::call([new PodcastStats, 'generate']);Method call cũng có thể gọi closure và tự động inject dependencies:
php
use App\Services\AppleMusic;
use Illuminate\Support\Facades\App;
$result = App::call(function (AppleMusic $apple) {
// ...
});Container Events
Service container phát sự kiện mỗi khi phân giải object. Lắng nghe bằng method resolving:
php
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
// Được gọi khi container phân giải objects kiểu "Transistor"...
});
$this->app->resolving(function (mixed $object, Application $app) {
// Được gọi khi container phân giải object bất kỳ kiểu nào...
});Rebinding
Method rebinding cho phép lắng nghe khi service được re-bound (đăng ký lại hoặc ghi đè):
php
use App\Contracts\PodcastPublisher;
use App\Services\SpotifyPublisher;
use App\Services\TransistorPublisher;
use Illuminate\Contracts\Foundation\Application;
$this->app->bind(PodcastPublisher::class, SpotifyPublisher::class);
$this->app->rebinding(
PodcastPublisher::class,
function (Application $app, PodcastPublisher $newInstance) {
//
},
);
// Binding mới sẽ trigger rebinding closure...
$this->app->bind(PodcastPublisher::class, TransistorPublisher::class);PSR-11
Laravel service container implement PSR-11 interface:
php
use App\Services\Transistor;
use Psr\Container\ContainerInterface;
Route::get('/', function (ContainerInterface $container) {
$service = $container->get(Transistor::class);
// ...
});Ngoại lệ sẽ được throw nếu identifier không thể phân giải. Exception là instance của Psr\Container\NotFoundExceptionInterface nếu identifier chưa được bind, hoặc Psr\Container\ContainerExceptionInterface nếu đã bind nhưng không thể phân giải.