原文地址 : 深入探討 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.phpproviders看到所有的 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 安装 : 22 详细请参考如何使用 Laravel Debugbar?

  1. $ composer require barryvdh/laravel-debugbar

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

  1. 'providers' => [
  2. /*
  3. * Laravel Framework Service Providers...
  4. */
  5. ...
  6. Illuminate\View\ViewServiceProvider::class,
  7. Barryvdh\Debugbar\ServiceProvider::class,
  8. /*
  9. * Application Service Providers...
  10. */
  11. App\Providers\AppServiceProvider::class,
  12. App\Providers\AuthServiceProvider::class,
  13. App\Providers\EventServiceProvider::class,
  14. App\Providers\RouteServiceProvider::class,
  15. ],

上半部为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。

    1. $ composer require barryvdh/laravel-debugbar --dev

    composer 加上--dev参数后,package 只会安装在require-dev区段,将来在正式上线主机只要下composer install --no-dev,就不会安装 Laravel Debugbar。
    composer require执行完,composer.json内容会如下图所示 :
    config.json

    1. "require": {
    2. "php": ">=5.5.9",
    3. "laravel/framework": "5.1.*"
    4. },
    5. "require-dev": {
    6. "fzaninotto/faker": "~1.4",
    7. "mockery/mockery": "0.9.*",
    8. "phpunit/phpunit": "~4.0",
    9. "phpspec/phpspec": "~2.1",
    10. "laravel/homestead": "^2.1",
    11. "barryvdh/laravel-debugbar": "^2.0"
    12. },

    产生 Service Provider

    1. $ php artisan make:provider MyLaravelDebugbarServiceProvider

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

    1. namespace App\Providers;
    2. use Illuminate\Support\ServiceProvider;
    3. class MyLaravelDebugbarServiceProvider extends ServiceProvider
    4. {
    5. /**
    6. * Bootstrap the application services.
    7. *
    8. * @return void
    9. */
    10. public function boot()
    11. {
    12. //
    13. }
    14. /**
    15. * Register the application services.
    16. *
    17. * @return void
    18. */
    19. public function register()
    20. {
    21. //
    22. }
    23. }

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

    1. namespace Illuminate\Support;
    2. use BadMethodCallException;
    3. abstract class ServiceProvider
    4. {
    5. ...
    6. abstract public function register();
    7. ...
    8. }

    register()有两个功能 :

  4. 让你手动register一个 service provider。

  5. 让你手动将一个 interface bind到指定 class。

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

在 register()注册

Illuminate/Support/ServiceProvider.php

  1. /**
  2. * Register the application services.
  3. *
  4. * @return void
  5. */
  6. public function register()
  7. {
  8. if ($this->app->environment() == 'local')
  9. {
  10. $this->app->register('Barryvdh\Debugbar\ServiceProvider');
  11. }
  12. }

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

注册自己的 Service Provider

config/app.php

  1. 'providers' => [
  2. /*
  3. * Laravel Framework Service Providers...
  4. */
  5. Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
  6. (略)
  7. /*
  8. * Application Service Providers...
  9. */
  10. App\Providers\AppServiceProvider::class,
  11. App\Providers\AuthServiceProvider::class,
  12. App\Providers\EventServiceProvider::class,
  13. App\Providers\RouteServiceProvider::class,
  14. App\Providers\MyLaravelDebugbarServiceProvider::class,
  15. ],

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 Provider - 图1

管理自己的 Service Container

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

建立 Interface

app/Contracts/PostRepositoryInterface.php

  1. namespace App\Contracts;
  2. use Illuminate\Database\Eloquent\Collection;
  3. /**
  4. * Interface PostRepositoryInterface
  5. * @package App\Contracts
  6. */
  7. interface PostRepositoryInterface
  8. {
  9. /**
  10. * 传回最新3笔文章
  11. *
  12. * @return Collection
  13. */
  14. public function getLatest3Posts();
  15. }

定义PostRepositoryInterface,只有一个getLatest3Post()

实现 Interface

app/Repositories/PostRepository.php

  1. namespace App\Repositories;
  2. use App\Contracts\PostRepositoryInterface;
  3. use App\Post;
  4. use Illuminate\Database\Eloquent\Collection;
  5. /**
  6. * Class PostRepository
  7. * @package App\Repositories
  8. */
  9. class PostRepository implements PostRepositoryInterface
  10. {
  11. /**
  12. * @var Post
  13. */
  14. protected $Post;
  15. /**
  16. * PostRepository constructor.
  17. * @param Post $Post
  18. */
  19. public function __construct(Post $Post)
  20. {
  21. $this->Post = $Post;
  22. }
  23. /**
  24. * 传回最新3笔文章
  25. *
  26. * @return Collection
  27. */
  28. public function getLatest3Posts()
  29. {
  30. return $this->Post
  31. ->query()
  32. ->orderBy('id', 'desc')
  33. ->limit(3)
  34. ->get();
  35. }
  36. }

第 7 行

  1. /**
  2. * Class PostRepository
  3. * @package App\Repositories
  4. */
  5. class PostRepository implements PostRepositoryInterface

PostRepository class 实践了PostRepositoryInterface
app/Repositories/MyRepository.php

  1. namespace App\Repositories;
  2. use App\Contracts\PostRepositoryInterface;
  3. use Illuminate\Database\Eloquent\Collection;
  4. /**
  5. * Class MyRepository
  6. * @package App\Repositories
  7. */
  8. class MyRepository implements PostRepositoryInterface
  9. {
  10. /**
  11. * 传回最新3笔文章
  12. *
  13. * @return Collection
  14. */
  15. public function getLatest3Posts()
  16. {
  17. $posts = new Collection();
  18. for ($i = 1; $i 3; $i++) {
  19. $post = [
  20. 'id' => $i,
  21. 'title' => 'My title' . $i,
  22. 'sub_title' => 'My sub_title' . $i,
  23. 'content' => 'My content' . $i,
  24. ];
  25. $posts->push((object)$post);
  26. }
  27. return $posts;
  28. }
  29. }

第 6 行

  1. /**
  2. * Class MyRepository
  3. * @package App\Repositories
  4. */
  5. class MyRepository implements PostRepositoryInterface

MyRepository class 一样实践了PostRepositoryInterface
13 行

  1. /**
  2. * 传回最新3笔文章
  3. *
  4. * @return Collection
  5. */
  6. public function getLatest3Posts()
  7. {
  8. $posts = new Collection();
  9. for ($i = 1; $i 3; $i++) {
  10. $post = [
  11. 'id' => $i,
  12. 'title' => 'My title' . $i,
  13. 'sub_title' => 'My sub_title' . $i,
  14. 'content' => 'My content' . $i,
  15. ];
  16. $posts->push((object)$post);
  17. }
  18. return $posts;
  19. }

没到透过Post model 向资料库读取资料,而是自己用Collection凑 3 笔资料。
比较特别的是$post为阵列,所以要push进 collection 时,需要转型成object,否则 blade 在显示时会出错。

注入 Container

app/Http/Controllers/PostsController.php

  1. namespace App\Http\Controllers;
  2. use App\Contracts\PostRepositoryInterface;
  3. use App\Http\Requests;
  4. class PostsController extends Controller
  5. {
  6. /**
  7. * @var PostRepositoryInterface
  8. */
  9. protected $posts;
  10. /**
  11. * PostsController constructor.
  12. * @param $posts
  13. */
  14. public function __construct(PostRepositoryInterface $posts)
  15. {
  16. $this->posts = $posts;
  17. }
  18. /**
  19. * Display a listing of the resource.
  20. *
  21. * @return \Illuminate\Http\Response
  22. */
  23. public function index()
  24. {
  25. $posts = $this->posts->getLatest3Posts();
  26. $data = compact('posts');
  27. return View('posts.index', $data);
  28. }
  29. }

第 8 行

  1. /**
  2. * @var PostRepositoryInterface
  3. */
  4. protected $posts;
  5. /**
  6. * PostsController constructor.
  7. * @param $posts
  8. */
  9. public function __construct(PostRepositoryInterface $posts)
  10. {
  11. $this->posts = $posts;
  12. }

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

切换 class

Service container 神奇的地方就在于任何有type hint的地方,Laravel 都会自动帮你载入物件,但若type hintinterface,由于实践该 interface 可能有很多物件,你必须使用App::bind()告诉 Laravel 该 interface 必须载入什麽物件,否则无法载入。
至于App::bind()该写在哪裡呢?Taylor 建议你写在service providerregister()
app/Providers/RepositoryServiceProvider.php

  1. namespace App\Providers;
  2. use Illuminate\Support\ServiceProvider;
  3. use App\Contracts\PostRepositoryInterface;
  4. use App\Repositories\PostRepository;
  5. use App\Repositories\MyRepository;
  6. /**
  7. * Class RepositoryServiceProvider
  8. * @package App\Providers
  9. */
  10. class RepositoryServiceProvider extends ServiceProvider
  11. {
  12. /**
  13. * Bootstrap the application services.
  14. *
  15. * @return void
  16. */
  17. public function boot()
  18. {
  19. //
  20. }
  21. /**
  22. * Register the application services.
  23. *
  24. * @return void
  25. */
  26. public function register()
  27. {
  28. $this->app->bind(
  29. PostRepositoryInterface::class,
  30. PostRepository::class
  31. );
  32. }
  33. }

24 行

  1. /**
  2. * Register the application services.
  3. *
  4. * @return void
  5. */
  6. public function register()
  7. {
  8. $this->app->bind(
  9. PostRepositoryInterface::class,
  10. PostRepository::class
  11. );
  12. }

当你要注入的是PostRepository时,就 bindPostRepository::class,若要注入的是MyRepository时,就 bindMyRepository::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 的ApplicationbootProvider()会去呼叫boot()
Illuminate/Foundation/Application.php

  1. /**
  2. * Boot the given service provider.
  3. *
  4. * @param \Illuminate\Support\ServiceProvider $provider
  5. * @return void
  6. */
  7. protected function bootProvider(ServiceProvider $provider)
  8. {
  9. if (method_exists($provider, 'boot')) {
  10. return $this->call([$provider, 'boot']);
  11. }
  12. }

所以 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.phpproviders中 service provider,都会在 Laravel 一启动时做 register 与 binding,若一些 service container 较少被使用,你想在该 service container 实际被使用才做 register 与 binding,以加快 Laravel 启动,可以使用deferred provider

    加入$defer

    app/Providers/RepositoryServiceProvider.php
    1. class RepositoryServiceProvider extends ServiceProvider
    2. {
    3. /**
    4. * Indicates if loading of the provider is deferred.
    5. *
    6. * @var bool
    7. */
    8. protected $defer = true;
    9. ...
    10. }
    在自己的 service provider 内加入$defer property 为 true。

    加入 provides()

    app/Providers/RepositoryServiceProvider.php
    1. class RepositoryServiceProvider extends ServiceProvider
    2. {
    3. /**
    4. * Get the services provided by the provider
    5. *
    6. * @return array
    7. */
    8. public function provides()
    9. {
    10. return [PostRepositoryInterface::class];
    11. }
    12. }
    provides()回传该 service provider 所要处理的完整 interface 名称。

    删除 service.json

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

    重新启动 Laravel

    bootstrap/cache/service.json
    1. {
    2. "providers": [
    3. ...
    4. "App\\Providers\\RepositoryServiceProvider"
    5. ],
    6. "eager": [
    7. ...
    8. ],
    9. "deferred": {
    10. ...
    11. "App\\Contracts\\PostRepositoryInterface": "App\\Providers\\RepositoryServiceProvider"
    12. },
    13. "when": {
    14. ...
    15. }
    16. }
    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