Service Container & Dependency Injection — Deep Dive
Nếu bạn đang dùng Laravel, bạn đã dùng Service Container mỗi ngày mà có thể không biết. Mỗi khi bạn type-hint một class trong controller hay service, Laravel tự tạo instance cho bạn — đó chính là Container đang hoạt động.
Bài viết này sẽ giúp bạn hiểu từ gốc: Container làm gì, khi nào nó tự xử lý, và khi nào bạn phải can thiệp.
1. Service Container là gì?
Analogy: Nhà hàng buffet thông minh
Tưởng tượng Service Container là một nhà hàng buffet tự động:
- Bạn (Controller/Service) = khách hàng, đến và nói: "Tôi cần món X"
- Container = bếp trưởng, biết cách nấu mọi món
- Binding = công thức nấu ăn mà bếp trưởng ghi nhớ
- Dependency Injection = bồi bàn tự mang món đến bàn bạn — bạn không cần vào bếp lấy
Bếp trưởng có 2 khả năng:
- Tự suy luận (Auto-resolution): Món đơn giản (concrete class) → bếp tự biết nấu, không cần công thức
- Theo công thức (Explicit binding): Bạn nói "Tôi cần đồ uống" (interface) → bếp không biết bạn muốn Pepsi hay Coca → cần công thức
2. Auto-Resolution — Tại sao bạn "ít dùng binding"?
90% trường hợp, Laravel auto-resolve được mà không cần bạn viết binding nào.
// Controller này KHÔNG CẦN binding — Laravel tự xử lý hết
class OrderController extends Controller
{
public function store(
StoreOrderRequest $request, // Laravel tự resolve
OrderService $service // Laravel tự resolve (concrete class)
) {
return $service->create($request->validated());
}
}
class OrderService
{
public function __construct(
private OrderRepository $repo, // Laravel tự new OrderRepository()
private LogService $logger // Laravel tự new LogService()
) {}
}
Container xử lý thế nào?
OrderControllercầnOrderService→ class cụ thể → tự tạo ✅OrderServicecầnOrderRepository→ class cụ thể → tự tạo ✅- Đệ quy cho đến khi hết dependency
Bạn dùng Container mỗi ngày mà không biết — đó là sức mạnh của auto-resolution.
3. Khi nào BẮT BUỘC phải binding?
Có 3 tình huống Container không tự xử lý được:
Tình huống 1: Inject Interface
class CheckoutService
{
public function __construct(
private PaymentGatewayInterface $gateway // Interface → Container không biết chọn impl nào!
) {}
}
Giải pháp: Nói cho Container biết interface → implementation nào.
// AppServiceProvider::register()
$this->app->bind(
PaymentGatewayInterface::class,
StripeGateway::class
);
Tại sao dùng Interface? → Nguyên tắc Dependency Inversion (SOLID):
- Đổi Stripe → VNPay? Sửa 1 dòng binding, không sửa 20 controller
- Test dễ hơn: mock interface, không cần gọi API thật
Tình huống 2: Class cần config/parameter đặc biệt
class ReportExporter
{
public function __construct(
private string $storagePath, // Container không biết truyền gì
private int $maxRows // Container không biết truyền gì
) {}
}
Giải pháp:
$this->app->bind(ReportExporter::class, function ($app) {
return new ReportExporter(
storagePath: config('reports.export_path'),
maxRows: config('reports.max_rows'),
);
});
Tình huống 3: Cần Singleton (1 instance duy nhất)
// Mỗi lần resolve CartService → cùng 1 instance (trong 1 request)
$this->app->singleton(CartService::class, function ($app) {
return new CartService(
session: $app->make(SessionManager::class)
);
});
4. Các loại Binding trong Laravel
| Loại | Cú pháp | Behavior | Khi nào dùng |
|---|---|---|---|
bind |
$this->app->bind(A, B) |
Tạo instance mới mỗi lần resolve | Stateless service, cần fresh instance |
singleton |
$this->app->singleton(A, B) |
Tạo 1 lần, reuse toàn bộ request | Config, cache manager, DB pool |
scoped |
$this->app->scoped(A, B) |
Singleton trong 1 request, reset giữa requests | Dùng với Octane — tránh state leak |
instance |
$this->app->instance('key', $obj) |
Bind object đã tồn tại | Test, mock, config override |
| Contextual | $this->app->when(A)->needs(B)->give(C) |
Inject khác nhau tùy context | Cùng interface, controller khác cần impl khác |
5. Ví dụ thực tế: Notification System
Giả sử hệ thống booking cần gửi notification khi có đặt phòng mới. Có 3 kênh: Email, LINE, SMS.
Bước 1: Định nghĩa interface
interface NotificationChannelInterface
{
public function send(User $user, string $message): void;
}
Bước 2: Implement các channel
class EmailChannel implements NotificationChannelInterface
{
public function send(User $user, string $message): void
{
Mail::to($user->email)->send(new GenericNotification($message));
}
}
class LineChannel implements NotificationChannelInterface
{
public function send(User $user, string $message): void
{
Http::post('https://api.line.me/v2/bot/message/push', [
'to' => $user->line_id,
'messages' => [['type' => 'text', 'text' => $message]],
]);
}
}
Bước 3: Service sử dụng interface
class BookingNotificationService
{
public function __construct(
private NotificationChannelInterface $channel
) {}
public function notifyNewBooking(Booking $booking): void
{
$this->channel->send(
$booking->user,
"Đặt phòng #{$booking->id} đã được xác nhận!"
);
}
}
Bước 4: Binding — quyết định dùng kênh nào
// Cách 1: Luôn dùng LINE
$this->app->bind(
NotificationChannelInterface::class,
LineChannel::class
);
// Cách 2: Đổi kênh theo config — không sửa code
$this->app->bind(NotificationChannelInterface::class, function ($app) {
return match (config('notification.default_channel')) {
'email' => new EmailChannel(),
'line' => new LineChannel(),
'sms' => new SmsChannel(),
default => new EmailChannel(),
};
});
// Cách 3: Contextual — controller khác dùng kênh khác
$this->app->when(AdminBookingController::class)
->needs(NotificationChannelInterface::class)
->give(EmailChannel::class); // Admin nhận email
$this->app->when(CustomerBookingController::class)
->needs(NotificationChannelInterface::class)
->give(LineChannel::class); // Customer nhận LINE
6. Tổng kết: Khi nào cần binding?
| Tình huống | Auto-resolve? | Cần binding? |
|---|---|---|
| Inject concrete class (không interface) | ✅ Tự động | ❌ Không |
| Inject interface | ❌ | ✅ bind hoặc singleton |
| Class cần config/parameter đặc biệt | ❌ | ✅ bind với closure |
| Cần 1 instance duy nhất trong request | ❌ (mặc định tạo mới) | ✅ singleton hoặc scoped |
| Cùng interface, context khác cần impl khác | ❌ | ✅ Contextual binding |
| Dùng Octane — tránh state leak | ❌ (singleton nguy hiểm) | ✅ scoped |
7. Kiểm tra hiểu biết
- Nếu type-hint
OrderService(concrete class) trong controller, có cần binding không? Tại sao? - Có 2 payment gateway (Stripe quốc tế, VNPay nội địa) cùng
PaymentGatewayInterface— controller khác dùng impl khác → dùng loại binding nào? -
singletonvsscoped— nếu project chạy trên Laravel Octane, chọn cái nào choCartService? Tại sao?