Laravel Service Container: Bạn Dùng Mỗi Ngày Mà Không Biết

7 phút đọc

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:

  1. 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
  2. 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?

  1. OrderController cần OrderService → class cụ thể → tự tạo ✅
  2. OrderService cần OrderRepository → class cụ thể → tự tạo ✅
  3. Đệ 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?

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?
  • singleton vs scoped — nếu project chạy trên Laravel Octane, chọn cái nào cho CartService? Tại sao?

Bài viết liên quan

Đang cập nhật...