Skip to content

深入探讨 Service Provider ​

🔗

文章系转载, 原文地址(原文已失效) : 深入探討 Service Provider

Service Provider 是 Laravel 管理 Package 的核心技术

Laravel 提供了 service container 让我们方便实现 依赖注入,而service provider则是我们注册及管理 service container 的地方。

事实上 Laravel 内部所有的核心组件都是使用 service provider 统一管理,除了可以用来管理 package 外,也可以用来管理自己写的物件

定义 ​

As Bootstrapper ​

我们知道 Laravel 提供了 service container,方便我们实现SOLID的依赖倒转原则,当 type hint 搭配 interface 时,需要自己下App::bind(),Laravel 才知道要载入什麽物件,但App::bind()要写在哪裡呢?Laravel 提供了service provider,专门负责App::bind()。

我们可以在config/app.php的providers看到所有的 package,事实上 Laravel 核心与其他 package 都是靠 service provider 载入。

As Organizer ​

Taylor 在书中一直强调 : 不要认为只有package才会使用service provider,它可以用来管理自己的service container ,也就是说,若因为需求而需要垫interface时,可以把 service provider 当成Simple Factory pattern 使用,将变化封装在 service provider 内,将来需求若有变化,只要改 service provider 即可,其他使用该 interface 的程式皆不必修改。Laravel: From Apprentice To Artisan

邂逅 : 安装 Package ​

初学者第一次接触 service provider,应该是在安装 package 时,以安装Laravel Debugbar为例,一开始我们会使用 composer 安装 :详细请参考如何使用 Laravel Debugbar?

Plaintext
$ composer require barryvdh/laravel-debugbar

接着我们会在config/app.php的providers加入Barryvdh\\Debugbar\\ServiceProvider::class

Plaintext
'providers' => [
     /*
     * Laravel Framework Service Providers...
     */
    ...
     Illuminate\\View\\ViewServiceProvider::class,
     Barryvdh\\Debugbar\\ServiceProvider::class,
     /*
     * Application Service Providers...
     */
     App\\Providers\\AppServiceProvider::class,
     App\\Providers\\AuthServiceProvider::class,
     App\\Providers\\EventServiceProvider::class,
     App\\Providers\\RouteServiceProvider::class,
],

上半部为Laravel Framework Service Provider,载入 Laravel 预设的 package。

下半部为Application Service Provider,载入自己所使用的service container。

为什麽使用 composer 安装完 package 之后,还要设定 service provider 呢?

以 Laravel Debugbar 为例,使用 composer 安装完 package 之后,只是将 package 安装在/vendor/barryvdh/laravel-debugbar 目录下,此时 Laravel 还不知道有这个 package,必须在config/app.php中注册该 package 所提供的 service provider,Laravel 才知道 Laravel Debugbar 的存在,并在Laravel 启动时载入时透过 Laravel Debugbar 的 service provider 去载入 Laravel Debugbar。

建立 Service Provider ​

一般来说,有 3 个地方我们会自己建立 service provider :

  1. 想自己载入 package。(As Bootstrapper)
  2. 想管理自己的 service container。(As Organizer)
  3. 自己写 package。请参考 如何开发自己的 Package?

自己载入 Package ​

使用–dev 安装 package ​

以 Laravel Debugbar 为例,虽然可以使用 package 所提供的 service provider,并在config/app.php中注册,不过由于 Laravel Debugbar 属于开发用的 package,因此我不希望正式上线主机也安装,若使用之前的安装方式,则连正式上线主机也会有 Laravel Debugbar。

Plaintext
$ composer require barryvdh/laravel-debugbar --dev

composer 加上--dev参数后,package 只会安装在require-dev区段,将来在正式上线主机只要下composer install --no-dev,就不会安装 Laravel Debugbar。

composer require执行完,composer.json内容会如下图所示 :

Plaintext
"require": {
    "php": ">=5.5.9",
    "laravel/framework": "5.1.*"
},
"require-dev": {
    "fzaninotto/faker": "~1.4",
    "mockery/mockery": "0.9.*",
    "phpunit/phpunit": "~4.0",
    "phpspec/phpspec": "~2.1",
    "laravel/homestead": "^2.1",
    "barryvdh/laravel-debugbar": "^2.0"
},

产生 Service Provider ​

Plaintext
$ php artisan make:provider MyLaravelDebugbarServiceProvider

在app\\Providers\\目录下会建立自己的MyLaravelServiceProvider.php,预设会有boot()与register()。

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

所有的 service provider 都是继承Illuminate\\Support\\ServiceProvider,因为ServiceProvider是一个abstract class
,且定义了register()这个abstract function
,所以继承的MyLaravelDebugbarServiceProvider必须实作register()

Plaintext
namespace Illuminate\\Support;
use BadMethodCallException;
abstract class ServiceProvider
{
    ...
    abstract public function register();
    ...
}

register()有两个功能 :

  1. 让你手动register一个 service provider。
  2. 让你手动将一个 interface bind到指定 class。

第一个功能用在自己载入package,第二个功能用在管理自己的service container,在下个范例会看到。

在 register()注册 ​

Plaintext
/**
 * Register the application services.
 *
 * @return void
 */
public function register()
{
    if ($this->app->environment() == 'local')
    {
        $this->app->register('Barryvdh\\Debugbar\\ServiceProvider');
    }
}

由于 Laravel Debugbar 不适合在正式上线主机使用,因此我们特别判断application enviromnent是否为local,若为 local,才使用$this->app->register()
注册Barryvdh\\Debugbar\\ServiceProvider,这相当于在config/app.php的providers
加入Barryvdh\\Debugbar\\ServiceProvider::class

注册自己的 Service Provider ​

Plaintext
'providers' => [
     /*
     * Laravel Framework Service Providers...
     */
     Illuminate\\Foundation\\Providers\\ArtisanServiceProvider::class,
     (略)
     /*
     * Application Service Providers...
     */
     App\\Providers\\AppServiceProvider::class,
     App\\Providers\\AuthServiceProvider::class,
     App\\Providers\\EventServiceProvider::class,
     App\\Providers\\RouteServiceProvider::class,
     App\\Providers\\MyLaravelDebugbarServiceProvider::class,
 ],

在config/app.php的最下方加入App\\Providers\\MyLaravelDebugbarServiceProvider::class
,载入刚刚我们自己建立的MyLaravelDebugbarServiceProvider。

也就是说,原本config/app.php是直接载入 Laravel Debugbar 提供的 service provider,现在改成载入自己写的 service provider,加入了判断 application
environment,再自行载入 Laravel Debugbar 提供的 service provider,以避免在正式上线主机载入 Laravel Debugbar。

管理自己的 Service Container ​

在如何对 Repository 做测试?中,我们曾经使用了Repository Pattern
搭配 controller,不过当初并没有垫 interface,现在我们加上了PostControllerInterface,并使用 service provider 管理。

建立 Interface ​

Plaintext
namespace App\\Contracts;
use Illuminate\\Database\\Eloquent\\Collection;
/**
 * Interface PostRepositoryInterface
 * @package App\\Contracts
 */
interface PostRepositoryInterface
{
     /**
     * 传回最新3笔文章
     *
     * @return Collection
     */
     public function getLatest3Posts();
}

定义PostRepositoryInterface,只有一个getLatest3Post()。

实现 Interface ​

Plaintext
namespace App\\Repositories;
use App\\Contracts\\PostRepositoryInterface;
use App\\Post;
use Illuminate\\Database\\Eloquent\\Collection;
/**
 * Class PostRepository
 * @package App\\Repositories
 */
class PostRepository implements PostRepositoryInterface
{
    /**
    * @var Post
    */
    protected $Post;
    /**
    * PostRepository constructor.
    * @param Post $Post
    */
    public function __construct(Post $Post)
    {
        $this->Post = $Post;
    }
    /**
    * 传回最新3笔文章
    *
    * @return Collection
    */
    public function getLatest3Posts()
    {
        return $this->Post
        ->query()
        ->orderBy('id', 'desc')
        ->limit(3)
        ->get();
    }
}

第 7 行

Plaintext
/**
 * Class PostRepository
 * @package App\\Repositories
 */
class PostRepository implements PostRepositoryInterface

PostRepository class 实践了PostRepositoryInterface。

Plaintext
namespace App\\Repositories;
use App\\Contracts\\PostRepositoryInterface;
use Illuminate\\Database\\Eloquent\\Collection;
/**
 * Class MyRepository
 * @package App\\Repositories
 */
class MyRepository implements PostRepositoryInterface
{
    /**
    * 传回最新3笔文章
    *
    * @return Collection
    */
    public function getLatest3Posts()
    {
        $posts = new Collection();
        for ($i = 1; $i 3; $i++) {
            $post = [
                'id'        => $i,
                'title'     => 'My title' . $i,
                'sub_title' => 'My sub_title' . $i,
                'content'   => 'My content' . $i,
            ];
            $posts->push((object)$post);
        }
        return $posts;
    }
}

第 6 行

Plaintext
/**
 * Class MyRepository
 * @package App\\Repositories
 */
class MyRepository implements PostRepositoryInterface

MyRepository class 一样实践了PostRepositoryInterface。

13 行

Plaintext
/**
 * 传回最新3笔文章
 *
 * @return Collection
 */
public function getLatest3Posts()
{
    $posts = new Collection();
    for ($i = 1; $i 3; $i++) {
        $post = [
            'id'        => $i,
            'title'     => 'My title' . $i,
            'sub_title' => 'My sub_title' . $i,
            'content'   => 'My content' . $i,
        ];
        $posts->push((object)$post);
    }
    return $posts;
}

没到透过Post model 向资料库读取资料,而是自己用Collection凑 3 笔资料。

比较特别的是$post为阵列,所以要push进 collection 时,需要转型成object,否则 blade 在显示时会出错。

注入 Container ​

Plaintext
namespace App\\Http\\Controllers;
use App\\Contracts\\PostRepositoryInterface;
use App\\Http\\Requests;
class PostsController extends Controller
{
    /**
    * @var PostRepositoryInterface
    */
    protected $posts;
    /**
    * PostsController constructor.
    * @param $posts
    */
    public function __construct(PostRepositoryInterface $posts)
    {
        $this->posts = $posts;
    }
    /**
    * Display a listing of the resource.
    *
    * @return \\Illuminate\\Http\\Response
    */
    public function index()
    {
        $posts = $this->posts->getLatest3Posts();
        $data = compact('posts');
        return View('posts.index', $data);
    }
}

第 8 行

Plaintext
/**
 * @var PostRepositoryInterface
 */
protected $posts;
/**
 * PostsController constructor.
 * @param $posts
 */
public function __construct(PostRepositoryInterface $posts)
{
    $this->posts = $posts;
}

将 repository 由 constructor 注入到 controller,注意现在$post的型别为PostRepositoryInterface,而不是PostRepository。

切换 class ​

Service container 神奇的地方就在于任何有type hint的地方,Laravel 都会自动帮你载入物件,但若type hint 为 interface ,由于实践该 interface 可能有很多物件,你必须使用App::bind()告诉 Laravel 该 interface 必须载入什麽物件,否则无法载入

至于App::bind()该写在哪裡呢?Taylor 建议你写在service provider的register()。

Plaintext
namespace App\\Providers;
use Illuminate\\Support\\ServiceProvider;
use App\\Contracts\\PostRepositoryInterface;
use App\\Repositories\\PostRepository;
use App\\Repositories\\MyRepository;
/**
 * Class RepositoryServiceProvider
 * @package App\\Providers
 */
class RepositoryServiceProvider extends ServiceProvider
{
     /**
     * Bootstrap the application services.
     *
     * @return void
     */
     public function boot()
     {
         //
     }
     /**
     * Register the application services.
     *
     * @return void
     */
     public function register()
     {
         $this->app->bind(
             PostRepositoryInterface::class,
             PostRepository::class
         );
     }
}

24 行

Plaintext
/**
 * Register the application services.
 *
 * @return void
 */
public function register()
{
     $this->app->bind(
         PostRepositoryInterface::class,
         PostRepository::class
     );
}

当你要注入的是PostRepository时,就 bindPostRepository::class,若要注入的是MyRepository时,就 bind MyRepository::class,controller 完全不用修改

Register()与 Boot() ​

当我们使用php artisan make:provider建立 service provider 时,预设会建立register()与boot()
,之前已经讨论过register()是来自于ServiceProvider的 abstract
method,所以我们必须实践,但boot()呢?boot()并不是ServiceProvider的 abstract method,所以我们可以不
实践,但为什麽php artisan make:provider
也帮我们建立了boot()呢?

当所有 service provider 的register()执行完后,接着会执行各 serive provider 的boot(),在

Laravel source 的Application的bootProvider()会去呼叫boot()

Plaintext
/**
 * Boot the given service provider.
 *
 * @param  \\Illuminate\\Support\\ServiceProvider  $provider
 * @return void
 */
 protected function bootProvider(ServiceProvider $provider)
 {
     if (method_exists($provider, 'boot')) {
        return $this->call([$provider, 'boot']);
     }
 }

所以 Laravel 并没有强迫要实践boot(),Laravel 再执行完所有 service provider 的register()之后,若你有实作boot() 的话,就会来执行该 service provider 的boot()

到底什麽程式该写在 register()?什麽程式该写在 boot()呢?

register()应该只拿来写App::bind()或App:register(),若要使用初始化物件,或使用其他相依物件,则应该写在boot(),有两个原因 :

  1. 根据SOLID的单一职责原则,register()只负责 service container 的 register 与 binding,boot()负责初始化物件。
  2. 若在register()使用其他相依物件,可能该物件还没bind,而导致执行错误;boot()在所有register()
    之后才执行,因此可以确保所有物件都已经bind。

Deferred Providers ​

在config/app.php的providers中 service provider,都会在 Laravel 一启动时做 register 与 binding,若一些 service
container 较少被使用,你想在该 service container
实际被使用才做 register 与 binding,以加快 Laravel 启动,可以使用deferred provider。

加入$defer ​

Plaintext
class RepositoryServiceProvider extends ServiceProvider
{
     /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
     protected $defer = true;
     ...
}

在自己的 service provider 内加入$defer property 为 true。

加入 provides() ​

Plaintext
class RepositoryServiceProvider extends ServiceProvider
{
     /**
     * Get the services provided by the provider
     *
     * @return array
     */
     public function provides()
     {
        return [PostRepositoryInterface::class];
     }
}

在provides()回传该 service provider 所要处理的完整 interface 名称。

删除 service.json ​

Plaintext
$ php artisan clear-compiled

所有要启动的 service provider 都会被 compile 在bootstrap/cache/service.json
,因为我们刚刚将PostRepositoryServiceProvider改成deferred provider ,所以必须删除service.json重新建立

重新启动 Laravel ​

Plaintext
{
     "providers": [
        ...
        "App\\Providers\\RepositoryServiceProvider"
     ],
     "eager": [
        ...
     ],
     "deferred": {
         ...
        "App\\Contracts\\PostRepositoryInterface": "App\\Providers\\RepositoryServiceProvider"
     },
     "when": {
         ...
     }
}

Laravel 重新启动后,会重新建立service.json,在providers属性,会列出所有 service
provider,因为我们刚刚将PostRepositoryServiceProvider加上$deffered = true
,所以现在defferred属性会有该 service provider,而provides()所传回的 interface,正是物件的 property。

Conclusion ​

  • Service provider 提供了统一了大家写App::bind()之处。
  • register()内只应该写 register 与 binding,而boot()内只应该写初始化物件或使用其他相依物件。
  • Service provider 不单只是 package 会使用,也可以拿来管理 service container,将变化封装在 service provider 内,当将来需求变化时,只要修改 service provider 即可。

Sample Code ​

完整的范例可以在我的GitHub上找到。

  1. My Laravel Debugbar
  2. Repository with Interface
  3. Repository with Deferred