原文地址 : 深入探討 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 安装 : 22 详细请参考如何使用 Laravel Debugbar?
$ composer require barryvdh/laravel-debugbar
接着我们会在config/app.php
的providers
加入Barryvdh\Debugbar\ServiceProvider::class
。
config/app.php
'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 :
- 想自己载入 package。(As Bootstrapper)
- 想管理自己的 service container。(As Organizer)
自己写 package。请参考 如何开发自己的 Package?
自己载入 Package
使用–dev 安装 package
以 Laravel Debugbar 为例,虽然可以使用 package 所提供的 service provider,并在
config/app.php
中注册,不过由于 Laravel Debugbar 属于开发
用的 package,因此我不希望正式上线
主机也安装,若使用之前的安装方式,则连正式上线主机也会有 Laravel Debugbar。$ composer require barryvdh/laravel-debugbar --dev
composer 加上
--dev
参数后,package 只会安装在require-dev
区段,将来在正式上线主机只要下composer install --no-dev
,就不会安装 Laravel Debugbar。composer require
执行完,composer.json
内容会如下图所示 :
config.json"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
$ php artisan make:provider MyLaravelDebugbarServiceProvider
在
app\Providers\
目录下会建立自己的MyLaravelServiceProvider.php
,预设会有boot()
与register()
。
app/Providers/MyLaravelServiceProvider.phpnamespace 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()
。
Illuminate/Support/ServiceProvider.phpnamespace Illuminate\Support;
use BadMethodCallException;
abstract class ServiceProvider
{
...
abstract public function register();
...
}
register()
有两个功能 :让你手动
register
一个 service provider。- 让你手动将一个 interface
bind
到指定 class。
第一个功能用在自己载入package
,第二个功能用在管理自己的service container
,在下个范例会看到。
在 register()注册
Illuminate/Support/ServiceProvider.php
/**
* 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
config/app.php
'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
app/Contracts/PostRepositoryInterface.php
namespace App\Contracts;
use Illuminate\Database\Eloquent\Collection;
/**
* Interface PostRepositoryInterface
* @package App\Contracts
*/
interface PostRepositoryInterface
{
/**
* 传回最新3笔文章
*
* @return Collection
*/
public function getLatest3Posts();
}
定义PostRepositoryInterface
,只有一个getLatest3Post()
。
实现 Interface
app/Repositories/PostRepository.php
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 行
/**
* Class PostRepository
* @package App\Repositories
*/
class PostRepository implements PostRepositoryInterface
PostRepository
class 实践了PostRepositoryInterface
。
app/Repositories/MyRepository.php
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 行
/**
* Class MyRepository
* @package App\Repositories
*/
class MyRepository implements PostRepositoryInterface
MyRepository
class 一样实践了PostRepositoryInterface
。
13 行
/**
* 传回最新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
app/Http/Controllers/PostsController.php
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 行
/**
* @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()
。
app/Providers/RepositoryServiceProvider.php
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 行
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->app->bind(
PostRepositoryInterface::class,
PostRepository::class
);
}
当你要注入的是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 的Application
的bootProvider()
会去呼叫boot()
。
Illuminate/Foundation/Application.php
/**
* 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()
,有两个原因 :
- 根据SOLID的单一职责原则,
register()
只负责 service container 的 register 与 binding,boot()
负责初始化物件。 - 若在
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
app/Providers/RepositoryServiceProvider.php
在自己的 service provider 内加入class RepositoryServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
...
}
$defer
property 为 true。加入 provides()
app/Providers/RepositoryServiceProvider.php
在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
所有要启动的 service provider 都会被 compile 在$ php artisan clear-compiled
bootstrap/cache/service.json
,因为我们刚刚将PostRepositoryServiceProvider
改成deferred provider
,所以必须删除service.json
重新建立。重新启动 Laravel
bootstrap/cache/service.json
Laravel 重新启动后,会重新建立{
"providers": [
...
"App\\Providers\\RepositoryServiceProvider"
],
"eager": [
...
],
"deferred": {
...
"App\\Contracts\\PostRepositoryInterface": "App\\Providers\\RepositoryServiceProvider"
},
"when": {
...
}
}
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上找到。