2016年9月2日 星期五

深入探討Service Provider

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.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的程式皆不必修改。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.phpproviders加入Barryvdh\Debugbar\ServiceProvider::class

config/app.php
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 : 
  1. 想自己載入package。(As Bootstrapper)
  2. 想管理自己的service container。(As Organizer)
  3. 自己寫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內容會如下圖所示 :

config.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()

app/Providers/MyLaravelServiceProvider.php
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()
    {
     //
    }
}
所有的service provider都是繼承Illuminate\Support\ServiceProvider,因為ServiceProvider是一個abstract class,且定義了register()這個abstract function,所以繼承的MyLaravelDebugbarServiceProvider必須實作register()

Illuminate/Support/ServiceProvider.php
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()有兩個功能 : 
  1. 讓你手動register一個service provider。
  2. 讓你手動將一個interface bind到指定class。
第一個功能用在自己載入package,第二個功能用在管理自己的service container,在下個範例會看到。

在register()註冊


Illuminate/Support/ServiceProvider.php
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.phpproviders加入Barryvdh\Debugbar\ServiceProvider::class

註冊自己的Service Provider


config/app.php
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


app/Contracts/PostRepositoryInterface.php
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


app/Repositories/PostRepository.php
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

app/Repositories/MyRepository.php
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


app/Http/Controllers/PostsController.php
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 hintinterface,由於實踐該interface可能有很多物件,你必須使用App::bind()告訴Laravel該interface必須載入什麼物件,否則無法載入。
至於App::bind()該寫在哪裡呢?Taylor建議你寫在service providerregister()

app/Providers/RepositoryServiceProvider.php
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的register()執行完後,接著會執行各serive provider的boot(),在
Laravel source的ApplicationbootProvider()會去呼叫boot()

Illuminate/Foundation/Application.php
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(),有兩個原因 : 
  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
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()


app/Providers/RepositoryServiceProvider.php
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


bootstrap/cache/service.json
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上找到。
  1. My Laravel Debugbar
  2. Repository with Interface
  3. Repository with Deferred

from : http://oomusou.io/laravel/laravel-service-provider/

沒有留言:

wibiya widget