Service Provider là gì? Tìm hiểu Service Provider trong Laravel

3371

Ở bài trước, đã có giải thích về việc Laravel đã xử lý một request như thế nào?. Giờ chúng ta cùng xem Service Provider hoạt động như thế nào trong ứng dụng web nhé.

Service Provider là gì?

Service Provider là thành phần trung tâm của việc khởi tạo tất cả các ứng dụng Laravel bao gồm các thành phần core. Việc đăng kí các liên kết tới service container, event listeners, middleware, và thậm chí các route. Service provider chính là nơi để cấu hình ứng dụng của bạn. Đây cũng chính là nơi khai báo các service provider sau này khi muốn viết một service provider. Hay sử dụng các package từ packagist bạn cũng thường phải khai báo thêm service provider mới tại đây.

Service Provider trong Laravel

Cùng quay lại một chút ở bài trước trong phần handle request. Đoạn này nằm xen kẽ giữa lúc Laravel nhận request và dispatch request lên router. Đây là nơi quá trình bootstrapping bắt đầu diễn ra.

$this->bootstrap();

public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }
}

Bạn có thể xem nó tại Illuminate\Foundation\Application.php Hàm này sẽ nạp các bootstrappers. Các bootstrappers ở đây được lấy từ trong hàm $this->bootstrappers(). Đó là những class sau:

protected $bootstrappers = [
    \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, // nạp các biến môi trường
    \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, // nạp các configuration file
    \Illuminate\Foundation\Bootstrap\HandleExceptions::class, // nạp các file cho việc xử lý exceptions
    \Illuminate\Foundation\Bootstrap\RegisterFacades::class, // đăng ký facade
    \Illuminate\Foundation\Bootstrap\RegisterProviders::class, // đăng ký các service provider
    \Illuminate\Foundation\Bootstrap\BootProviders::class, // boot
];

Mở file Illuminate\Foundation\Application.php và tìm đến hàm bootstrapWith ta sẽ thấy hàm này sẽ tạo ra các bootstrappers mà đã liệt kê bên trên, sau đó nạp các bootstrappers này vào trong application của bạn.

$this->make($bootstrapper)->bootstrap($this);

Giờ chúng ta sẽ tìm hiểu xem Laravel Service provider hoạt động như thế nào nhé. Chúng ta biết biết chắc chắn là trong mỗi bootstrapper này đều có một hàm bootstrap.

Ở đây chúng ta sẽ đi vào class \Illuminate\Foundation\Bootstrap\RegisterProviders::classđể xem Service Provider đã làm những gì cho application của chúng ta:

public function bootstrap(Application $app)
{
    $app->registerConfiguredProviders();
}

Ở đoạn xử lý đầu tiên, $providers của chúng ta sẽ trả về một kết quả đó là danh sách các provider nằm trong key 'providers' và bắt đầu bằng Illuminate\\ tại config/app.php.

$providers = Collection::make($this->config['app.providers'])
                ->partition(function ($provider) {
                    return Str::startsWith($provider, 'Illuminate\\');
                });

Ở đoạn xử lý thứ 2, Laravel sẽ gộp các providers có được tại hàm

$this->make(PackageManifest::class)->providers()

vào các providers đã lấy được ở trên. Chúng ta sẽ xem các providers trong cái hàm ở trên là những providers nào nhé. Illuminate\Foundation\PackageManifest.php:

public function providers()
{
    return collect($this->getManifest())->flatMap(function ($configuration) {
        return (array) ($configuration['providers'] ?? []);
    })->filter()->all();
}

Xem hàm getManifest

protected function getManifest()
{
    if (! is_null($this->manifest)) {
        return $this->manifest;
    }

    if (! file_exists($this->manifestPath)) {
        $this->build();
    }

    $this->files->get($this->manifestPath, true);

    return $this->manifest = file_exists($this->manifestPath) ?
        $this->files->getRequire($this->manifestPath) : [];
}

Nên nhớ ở trên chúng ta make một class PackageManifest và không truyền cho nó cái gì cả. Vậy thì constructor của nó cũn không có gì. Vậy là trong hàm getManifest(), property $this->manifest ở đây đang không có giá trị.

Khởi tạo một instance từ Laravel Application

Laravel khởi tạo một instance của PackageManifest.

$this->instance(PackageManifest::class, new PackageManifest(
    new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
));

Trước hết hãy để ý một điều, Laravel sử dụng hàm này trước khi quá trình bootstrapping xuất hiện. Vậy lúc này trong class PackageManifest có gì ??

  Laravel 5.6 thêm Collision Package cho CLI Error Report

Chúng ta đang cần quan tâm đến hàm providers vậy nên ta cần biết tham số thứ 3 trong constructor, đó là $manifestPath là gì.
Tương ứng như chúng ta thấy, đó là:

$this->getCachedPackagesPath() // bootstrap/cache/packages.php

Giờ chúng ta hiểu, giá trị $this->manifestPath chính là chuỗi bootstrap/cache/packages.php, hãy cùng tiếp tục xem Service Provider đã làm gì tiếp theo.
Mở file bootstrap/cache/packages.php , chúng ta thấy 1 array chứa các cặp array khác có key là 'providers' và 'aliases'. Mà hàm providers của chúng ta lấy các key là 'providers'
Đến đây có thể hiểu rằng Laravel sẽ lấy tất cả các providers trong application của chúng ta bằng cách lấy các providers có tiền tố Illuminate\\ tại config/app.php và danh sách các providers tại caches/packages.php. Sau khi tập hợp tất cả các providers chúng ta sẽ quay lại hàm registerConfiguredProviders():

(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($providers->collapse()->toArray());

Trong hàm này, tất cả các đăng ký các event khi load các provider, sau đó từng provider này sẽ được đăng ký bằng cách gọi đến hàm register trong class Application:

foreach ($manifest['when'] as $provider => $events) {
    $this->registerLoadEvents($provider, $events);
}

// We will go ahead and register all of the eagerly loaded providers with the
// application so their services can be registered with the application as
// a provided service. Then we will set the deferred service list on it.
foreach ($manifest['eager'] as $provider) {
    $this->app->register($provider);
}

Trong hàm register tại class Application, mỗi service provider sẽ được gọi đến method register của chính provider đó.

if (method_exists($provider, 'register')) {
    $provider->register();
}

Và nếu các bạn có để ý, trong các provider tại method register, chúng ta sẽ thường sử dụng việc binding tại hàm này, Tuy nhiên có một cách ngắn hơn đó là nếu provider của bạn đang có property là $bindings hoặc $singletons thì Laravel sẽ tự động binding theo cặp key, value này.

if (property_exists($provider, 'bindings')) {
    foreach ($provider->bindings as $key => $value) {
        $this->bind($key, $value);
    }
}

if (property_exists($provider, 'singletons')) {
    foreach ($provider->singletons as $key => $value) {
        $this->singleton($key, $value);
    }
}

// Ví dụ
public $bindings = [
   A::class => B::class,
];
 //=> $this->bind(A::class, B::class);
/**
* All of the container singletons that should be registered.
*
* @var array
*/
public $singletons = [
   A::class => B::class,
];
 //=> $this->singleton(A::class, B::class);

Đến đây, nếu application của bạn chưa nạp các provider thì nó sẽ nạp tất cả các provider đó. Nếu không, nó sẽ chỉ nạp các provider mới. Thay vì phải quay lại làm một công việc từ đầu, thì nó sẽ không làm lại nữa mà chỉ làm các công việc mới.

if ($this->booted) {
    $this->bootProvider($provider);
}

protected function bootProvider(ServiceProvider $provider)
{
    if (method_exists($provider, 'boot')) {
        return $this->call([$provider, 'boot']);
    }
}

Giải thích ngắn gọn dễ hiểu để bạn mường tượng hơn. Ví dụ chúng ta có sẵn một bộ máy tính để bàn bao gồm CPU, chuột, bàn phím…Bạn muốn gắn thêm màn hình hay thay một bàn phím cơ khác thì chỉ cần plug and play thôi. Không phải build lại cái máy tính từ đầu.

Với Laravel cũng vậy, sau khi kết thúc quá trình bootstrapping lần đầu tiên, các package/library hiện có đã có thể sử dụng.

Giả sử bạn muốn install thêm các package/library khác thì Laravel chỉ nạp lại những package mới này và đăng ký các provider tương ứng cho chúng vào application của bạn. Vậy là chúng ta sẽ hiểu việc đăng ký providers như sau:

$providers = Collection::make($this->config['app.providers'])
                ->partition(function ($provider) {
                    return Str::startsWith($provider, 'Illuminate\\');
                });
// lấy tất cả provider có tiền tố là Illuminate\\ từ config/app.php
$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
// lấy tất cả các providers từ bootstrap/cache/packages.php rồi kết hợp với các providers ở trên.
// Lúc này trong application chúng ta đã có đc đầy đủ các providers rồi
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
            ->load($providers->collapse()->toArray());
// Các providers này sẽ được gọi tự động đăng kí (register), nạp vào application (boot)

Vậy nên chúng ta thường thấy các provider sẽ có các phương thức như boot hay register mà không phải các phương thức khác, tất cả là do Laravel đã thiết kế nên như vậy. Đã bao giờ các bạn sử dụng command để thêm các package/library ngoài vào, sau đó thêm các provider của nó vào trong config/app.php chưa?

Thường thì chúng ta chỉ cần import nó vào và sử dụng mà không cần biết rằng tại sao nó lại vận hành được. Thật ra thì vấn đề này Laravel đã giúp chúng ta làm tất cả rồi.

Tổng kết

Service Provider là một khái niệm khá trừu tượng, và là thành phần quan trọng nhất trong quá trình handle một request trong Laravel. Hiểu một cách tổng quan là trong quá trình bootstrapping application thì Service Provider chính là phần quan trọng nhất.

  Tìm hiểu SQL Transaction và cách sử dụng trong Laravel
  Nhận diện khuôn mặt trong ứng dụng Laravel sử dụng Google Cloud Vision API