Tìm Hiểu Sâu: Laravel Service Container

· 4 min read

Service Container là khái niệm quan trọng nhất trong Laravel. Nó quản lý các dependency của class và thực hiện dependency injection.

Nếu bạn hiểu container, bạn hiểu Laravel. Nếu không, Laravel dường như là "phép thuật."

Dependency Injection (DI) là gì?

Không có DI:

class UserController extends Controller {
    public function index() {
        // Tightly coupled. Khó test.
        $mailer = new MailgunMailer('api-key');
        $mailer->send(...);
    }
}

Với DI:

class UserController extends Controller {
    protected $mailer;

    // Laravel tự động "inject" Mailer vào đây
    public function __construct(Mailer $mailer) {
        $this->mailer = $mailer;
    }
}

Chúng ta không cần biết cách tạo Mailer. Chúng ta chỉ cần yêu cầu nó.

Binding trong Service Provider

AppServiceProvider là nơi chúng ta nói cho Laravel biết cách xây dựng các thứ.

1. Simple Binding

$this->app->bind(TransistorResult::class, function ($app) {
    return new TransistorResult($app->make(TwitterClient::class));
});

Mỗi khi bạn yêu cầu TransistorResult, bạn nhận được một instance mới.

2. Singleton

$this->app->singleton(TwitterClient::class, function ($app) {
    return new TwitterClient('api-key');
});

Lần đầu tiên được yêu cầu, nó được tạo. Mỗi lần sau đó, cùng một instance được trả về. Điều này rất cần thiết cho kết nối Database hoặc Cache store.

3. Interface Binding

Đây là pattern mạnh mẽ nhất. Bạn type-hint một Interface, nhưng Laravel inject một Class cụ thể.

// Trong AppServiceProvider
$this->app->bind(
    \App\Contracts\PaymentGateway::class, 
    \App\Services\StripePaymentGateway::class
);

Controller:

public function store(PaymentGateway $gateway) {
    // Đây thực sự là một instance của StripePaymentGateway
    $gateway->charge(100);
}

Lợi ích: Bạn có thể đổi Stripe sang PayPal bằng cách thay đổi một dòng trong Provider, mà không cần chạm vào bất kỳ Controller nào.

Contextual Binding

Đôi khi bạn muốn cùng một Interface resolve thành các implementation khác nhau tùy thuộc vào class đang yêu cầu nó.

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

Ví Dụ Thực Tế: Markdown Service

Trong dự án blog này, chúng ta có MarkdownPostService. Chúng ta không nên new nó trong Controller.

Không tốt:

public function show($slug) {
    $service = new MarkdownPostService();
    return $service->get($slug);
}

Tốt:

public function show(string $slug, MarkdownPostService $service) {
    return $service->get($slug);
}

Tại sao? Vì sau này, MarkdownPostService có thể cần dependency như CacheRepository hoặc Parser. Nếu để Container xử lý việc khởi tạo, chúng ta không cần thay đổi code Controller khi constructor của Service thay đổi.

Kết Luận

Service Container cơ bản là một mảng lớn tuyệt vời chứa các "công thức" để tạo object.

  1. Bind công thức.
  2. Type-hint class/interface.
  3. Để Laravel resolve nó một cách đệ quy.

Làm chủ điều này loại bỏ "phép thuật" và cho bạn toàn quyền kiểm soát kiến trúc ứng dụng.

Bình luận