1. 程式人生 > >Laravel 服務提供者指南

Laravel 服務提供者指南

這是一篇翻譯文章,譯文首發於  Laravel 服務提供者指南,轉載請註明出處。

來源:https://segmentfault.com/a/1190000015158113

如果你使用過 Laravel 框架的話,那麼,你不可能沒聽說過服務容器和服務提供者。事實上,它們是 Lavavel 框架核心,它們完成 Larvel 應用中服務啟動的艱鉅任務。

在這篇文章中,我們將簡單介紹「服務容器」,同時還會深入講解服務提供者。本教程還將演示如何在 Laravel 中建立一個自定義的服務提供者。另外,如果你需要在 Laravel 中成功使用服務容器,還需要註冊它。那麼,讓我們開始吧。

實現一個自定義的服務提供者,需要實現兩個非常重要的方法:boot 和 register 方法。關於這兩個方法將在教程最後一個小節討論。

在學習服務提供者之前,簡單介紹一下服務容器,服務容器會在服務提供者中被經常使用。

理解服務容器和服務提供者

什麼是服務容器

簡而言之,Laravel 服務容器 是一個用於儲存繫結元件的盒子,它還會為應用提供所需的服務。

Laravel 文件中描述如下:

Laravel 服務容器是用於管理類的依賴和執行依賴注入的工具 -  Laravel 文件

這樣,當我們需要注入一個內建的元件或服務時,可以在建構函式或方法中使用型別提示功能注入,然後在使用時從服務容器中自動解析出所需例項及其依賴!是不是很酷?這個功能可以讓我們從手動管理元件中解脫出來,從而降低系統耦合度。

讓我們看一個簡單例項來加深理解。

<?php

Class SomeClass
{
    public function __construct(FooBar $foobarObject)
    {
        // use $foobarObject object
    }
}

如你所見,SomeClass 需要使用 FooBar 例項。換句話說它需要依賴其它元件。Laravel 實現自動注入需要從服務容器中查詢並執行注入適當的依賴。

如果你希望瞭解 Laravel 是如何知道需要將哪個元件或服務繫結到服務容器中的,答案是通過服務提供者實現的。服務提供者完成將元件繫結到服務容器的工作。在服務提供者內部,這個工作被稱之為服務容器繫結,繫結處理由服務提供者完成。

服務提供者實現了服務繫結,繫結處理則由 register 方法完成。

同時,這又會引入一個新的問題:Laravel 是如何知道有哪些服務提供者的呢?這個我們貌似還沒有討論到吧?我到時看到,之前有說 Laravel 會自動的去查詢到服務!朋友,你的問題太多了:Laravel 只是一個框架,它不是一個超級英雄,不是麼?我們當然需要去明確的告知 Laravel 框架我們有哪些服務提供者。

讓我們來瞧瞧 config/app.php 配置檔案。你會找到一個用於 Laravel 應用啟動過程中被載入的服務提供者配置列表。

'providers' => [
  
        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,
  
        /*
         * Package Service Providers...
         */
        Laravel\Tinker\TinkerServiceProvider::class,
  
        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
],

以上就是有關服務容器的基本概念。下一節,我們將焦點聚集到服務提供者這個核心主題上!

什麼是服務提供者

如果說服務容器是提供繫結和依賴注入的的工具,那麼 服務提供者 則是實現繫結的工具。

讓我們先來看一個內容提供的服務提供者服務來理解它的執行原理。開啟 vender/laravel/framework/src/Illuminate/Cache/CacheServiceProvider.php 檔案。

public function register()
{
    $this->app->singleton('cache', function ($app) {
        return new CacheManager($app);
    });
  
    $this->app->singleton('cache.store', function ($app) {
        return $app['cache']->driver();
    });
  
    $this->app->singleton('memcached.connector', function () {
        return new MemcachedConnector;
    });
}

這裡我們需要將重點集中在 register 方法中,這個方法用於繫結服務到服務容器。如你所見,這裡一共執行了三個服務的繫結處理:cachecache.store 和 memcached.connector

然後,當我們需要在 Laravel 中使用 cache 服務是,服務容器會解析出 CacheManager 例項並返回。也就是說我們僅僅是提供了一個可以從 $this->app 訪問的對應關係表。

通過服務提供者繫結服務是 Laravel 服務容器繫結服務的正確開啟方式。同時通過服務提供者的 register 方法,還有利於理解 Laravel 服務容器是如何管理所有的服務的。我們之前提到過,通過從 config/app.php 配置檔案中讀取服務提供者配置列表,從將所有服務註冊服務容器中。

以上,就是服務提供者和它的故事。下一節,我們會學習如何建立一個服務提供者來實現將自己的服務註冊到 Laravel 服務容器。

自定義服務提供者

Laravel 已經內建了一個用於建立服務提供者的 artisan 命令來簡化建立流程。進入命令列模式後執行下面命令來建立服務提供者。

php artisan make:provider EnvatoCustomServiceProvider

執行後會在 app/Providers 目錄下建立 EnvatoCustomServiceProvider.php 檔案。開啟該檔案看下它的原始碼。

<?php
namespace App\Providers;
  
use Illuminate\Support\ServiceProvider;
  
class EnvatoCustomServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
  
    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

之前我們有提到服務提供者有兩個重要方法:boot 和 register 方法,在實現自定義服務提供者時大部分都是在處理這兩個方法。

register 方法用於執行服務繫結處理。另外在 boot 方法中可以使用所有已繫結的服務。在這個教程的最後一節我們將學習更多有關這兩個方法的細節,但在這裡我們會先了解些這兩個方法的使用示例加深理解。

註冊自定義服務提供者

前面我們建立了一個自定義的服務提供者。接下來需要讓 Laravel 知道如何讓這個服務提供者同其它服務提供者一樣在應用啟動時被載入到 Laravel 中。

為了完成註冊服務提供者的功能,僅需要將類名加入到 config/app.php 配置檔案的 providers 節點。

'providers' => [
  
        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,
  
        /*
         * Package Service Providers...
         */
        Laravel\Tinker\TinkerServiceProvider::class,
  
        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
        App\Providers\EnvatoCustomServiceProvider::class,
],

就是如此簡單,現在你已經將自定義服務提供者註冊到了 Laravel 中。只不過現在這個服務提供者還幾乎什麼都沒有處理。下一節,我們將以例項演示如何使用 register 和 boot 方法。

深入講解 register 和 boot 方法

起先,我們來深入研究 register 方法加深你對這個方法的理解。開啟之前建立的 app/Providers/EnvatoCustomServiceProvider.php 檔案,加入如下程式碼。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Library\Services\DemoOne;

class EnvatoCustomServiceProvider extends ServiceProvider
{
    public function boot()
    {
    }

    public function register()
    {
        $this->app->bind('App\Library\Services\DemoOne', function ($app) {
            return new DemoOne();
        });
    }
}

這裡我們做了兩個處理:

  • 引入需要使用的 AppLibraryServicesDemoOne 服務。DemoOne 類現在還沒有建立,但之後會建立這個類。
  • 在 register 方法中,我們使用服務容器的 bind 方法將服務繫結到容器。這樣,當需要使用 AppLibraryServicesDemoOne 服務而被解析時,就回呼叫閉包方法,建立例項並返回 AppLibraryServicesDemoOne 物件。

現在建立 app/Library/Services/DemoOne.php 檔案。

<?php
namespace App\Library\Services;
  
class DemoOne
{
    public function doSomethingUseful()
    {
      return 'Output from DemoOne';
    }
}

然後,在控制器的建構函式中注入依賴。

<?php
namespace App\Http\Controllers;
  
use App\Http\Controllers\Controller;
use App\Library\Services\DemoOne;
  
class TestController extends Controller
{
    public function index(DemoOne $customServiceInstance)
    {
        echo $customServiceInstance->doSomethingUseful();
    }
}

以上便是一個使用繫結的簡單方法。事實上,對於這個示例其實並不需要建立一個服務提供者,並實現 register 方法,因為 Laravel 還可以通過 PHP 的方式功能自動解析。

Laravel 文件中對此有一個說明:

如果我們的依賴無需任何介面,則無需將類繫結到容器。容器此時不需要了解建立物件的具體細節,而可以通過反射功能實現自動注入。

換句話說,如果我們需要繫結的服務依賴於其它介面,建立服務提供者則很有必要。接著來看一個例項以加深理解。

首先,建立一個簡單的介面 app/Library/Services/Contracts/CustomServiceInterface.php

<?php
// app/Library/Services/Contracts/CustomServiceInterface.php
namespace App\Library\Services\Contracts;
  
Interface CustomServiceInterface
{
    public function doSomethingUseful();
}

然後,建立兩個基於此介面的具體實現。或者說,建立兩個繼承此介面的實現類。

一個是定義在 app/Library/Services/DemoOne.php 檔案中的 DemoOne 類。

<?php
// app/Library/Services/DemoOne.php
namespace App\Library\Services;
  
use App\Library\Services\Contracts\CustomServiceInterface;
  
class DemoOne implements CustomServiceInterface
{
    public function doSomethingUseful()
    {
      return 'Output from DemoOne';
    }
}

類似的,還有 app/Library/Services/DemoTwo.php

<?php
// app/Library/Services/DemoTwo.php
namespace App\Library\Services;
  
use App\Library\Services\Contracts\CustomServiceInterface;
  
class DemoTwo implements CustomServiceInterface
{
    public function doSomethingUseful()
    {
      return 'Output from DemoTwo';
    }
}

現在,將繫結具體類名修改為繫結介面。開啟 EnvatoCustomServiceProvider.php 檔案並改成如何程式碼。

<?php
namespace App\Providers;
  
use Illuminate\Support\ServiceProvider;
use App\Library\Services\DemoOne;
  
class EnvatoCustomServiceProvider extends ServiceProvider
{
    public function boot()
    {
    }
  
    public function register()
    {
        $this->app->bind('App\Library\Services\Contracts\CustomServiceInterface', function ($app) {
          return new DemoOne();
        });
    }
}

這裡,我們將 DemoOne 實現類繫結到 AppLibraryServicesContractsCustomServiceInterface 介面。後續,所有依賴 AppLibraryServicesContractsCustomServiceInterface 介面的功能都被解析成 AppLibraryServicesDemoOne 物件。 這個示例是不是更有實際意義呢?

當然,我們還需要調整下控制器中的程式碼。

<?php
namespace App\Http\Controllers;
  
use App\Http\Controllers\Controller;
use App\Library\Services\Contracts\CustomServiceInterface;
  
class TestController extends Controller
{
    public function index(CustomServiceInterface $customServiceInstance)
    {
        echo $customServiceInstance->doSomethingUseful();
    }
}

或許你已經猜到 $customServiceInstance 物件是 AppLibraryServicesDemoOne 類的例項!這種方案的優勢在於可以很容易的替換掉 DemoOne 這個實現。

假如你想使用 DemoTwo 替換掉 DemoOne 服務。此時,僅需簡單的調整下服務提供者中的程式碼 EnvatoCustomServiceProvider.php

將:

use App\Library\Services\DemoOne;

替換成:

use App\Library\Services\DemoTwo;

然後替換:

return new DemoOne();

到:

return new DemoTwo();

使用同樣的手法甚至可以將自定義的實現替換掉任何核心服務中的依賴。不僅如此,除了 bind 方法;Laravel 服務容器還提供多種繫結方法。可以檢視 Laravel 服務容器 文件瞭解更多。

下一個主題是可以擴充套件 Laravel 核心服務的 boot 方法。在這個方法中,你可以獲取所有通過服務提供者註冊到容器中的服務。通常,你會在這個方法中註冊某些功能完成後需要觸發其它操作的事件監聽器。

依照慣例看幾個示例先。

建立一個用於 Laravel 校驗的自定義表單驗證器。

public function boot()
{
    Validator::extend('my_custom_validator', function ($attribute, $value, $parameters, $validator) {
        // validation logic goes here...
    });
}

也許你想建立一個 view composer。在 boot 方法中建立是個不錯的選擇。

public function boot()
{
    View::composer(
        'demo', 'App\Http\ViewComposers\DemoComposer'
    );
}

當然在這裡需要率先匯入 IlluminateSupportFacadesView

有時,我們還需要建立一些共享資料。

public function boot()
{
    View::share('key', 'value');
}

甚至可以顯示的建立模型繫結。

public function boot()
{
    parent::boot();
  
    Route::model('user', App\User::class);
}

這些示例演示了 boot 方法的一些用法。只有更深入的理解,才能掌握它的使用方法!

與此同時,我們需要說再見了。我希望你喜歡本文所討論的主題。

結論

本文討論的是服務提供者,這是本文的中心思想,儘管我們是以服務容器作為開篇,因為它是理解服務提供者的重要組成部分。

隨後,我們建立了一個自定義服務提供者,並且在本文的後半部分中,我們介紹了幾個實際的示例。