Laravel提供了
service container
讓我們方便實現Dependency Injection,而service provider
則是我們註冊及管理service container的地方。
事實上Laravel內部所有的核心組件都是使用service provider統一管理,除了可以用來管理package外,也可以用來管理自己寫的物件。
Version
Laravel 5.1
定義
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的程式皆不必修改。11Laravel: From Apprentice To Artisan邂逅 : 安裝Package
初學者第一次接觸service provider,應該是在安裝package時,以安裝
Laravel Debugbar
為例,一開始我們會使用composer安裝 : 22詳細請參考如何使用Laravel Debugbar?1
| oomusou@mac:~/MyProject$ composer require barryvdh/laravel-debugbar
|
接著我們會在
config/app.php
的providers
加入Barryvdh\Debugbar\ServiceProvider::class
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | '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。33請參考
如何開發自己的Package?
自己載入Package
使用–dev安裝package
以Laravel Debugbar為例,雖然可以使用package所提供的service provider,並在
config/app.php
中註冊,不過由於Laravel Debugbar屬於開發
用的package,因此我不希望正式上線
主機也安裝,若使用之前的安裝方式,則連正式上線主機也會有Laravel Debugbar。1
| oomusou@mac:~/MyProject$ composer require barryvdh/laravel-debugbar --dev
|
composer加上
--dev
參數後,package只會安裝在require-dev
區段,將來在正式上線主機只要下composer install --no-dev
,就不會安裝Laravel Debugbar。composer require
執行完,composer.json
內容會如下圖所示 :1 2 3 4 5 6 7 8 9 10 11 12 | "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
1
| oomusou@mac:~/MyProject$ php artisan make:provider MyLaravelDebugbarServiceProvider
|
在
app\Providers\
目錄下會建立自己的MyLaravelServiceProvider.php
,預設會有boot()
與register()
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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() { // } } |
Illuminate\Support\ServiceProvider
,因為ServiceProvider
是一個abstract class
,且定義了register()
這個abstract function
,所以繼承的MyLaravelDebugbarServiceProvider
必須實作register()
。1 2 3 4 5 6 7 8 9 10 11 12 | namespace Illuminate\Support; use BadMethodCallException; abstract class ServiceProvider { (以上略) abstract public function register(); (以下略) } |
register()
有兩個功能 : - 讓你手動
register
一個service provider。 - 讓你手動將一個interface
bind
到指定class。
第一個功能用在
自己載入package
,第二個功能用在管理自己的service container
,在下個範例會看到。在register()註冊
1 2 3 4 5 6 7 8 9 10 11 12 | /** * 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | '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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | namespace App\Contracts; use Illuminate\Database\Eloquent\Collection; /** * Interface PostRepositoryInterface * @package App\Contracts */ interface PostRepositoryInterface { /** * 傳回最新3筆文章 * * @return Collection */ public function getLatest3Posts(); } |
定義
PostRepositoryInterface
,只有一個getLatest3Post()
。實現Interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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行
1 2 3 4 5 | /** * Class PostRepository * @package App\Repositories */ class PostRepository implements PostRepositoryInterface |
PostRepository
class實踐了PostRepositoryInterface
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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行
1 2 3 4 5 | /** * Class MyRepository * @package App\Repositories */ class MyRepository implements PostRepositoryInterface |
MyRepository
class一樣實踐了PostRepositoryInterface
。
13行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * 傳回最新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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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行
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * @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()
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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 // MyRepository::class ); } } |
24行
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * Register the application services. * * @return void */ public function register() { $this->app->bind( PostRepositoryInterface::class, PostRepository::class // MyRepository::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的
Laravel source的
register()
執行完後,接著會執行各serive provider的boot()
,在Laravel source的
Application
的bootProvider()
會去呼叫boot()
。1 2 3 4 5 6 7 8 9 10 11 12 | /** * 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
1 2 3 4 5 6 7 8 9 10 11 12 | class RepositoryServiceProvider extends ServiceProvider { /** * Indicates if loading of the provider is deferred. * * @var bool */ protected $defer = true; (以下略) } |
在自己的service provider內加入
$defer
property為true。加入provides()
1 2 3 4 5 6 7 8 9 10 11 12 | 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
1
| oomusou@mac:~/MyProject$ php artisan clear-compiled
|
所有要啟動的service provider都會被compile在
bootstrap/cache/service.json
,因為我們剛剛將PostRepositoryServiceProvider
改成deferred provider
,所以必須刪除service.json
重新建立。重新啟動Laravel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | { "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上找到。
from : http://oomusou.io/laravel/laravel-service-provider/
沒有留言:
張貼留言