這是一份面向初學者的 Laravel 5.1 中構建 Service Provider 的教程。
我在自己過去的博客中提到了我喜歡 Laravel 5.1 的架構,尤其是它引入了Service Provider,從而使你模塊化的構建應用成為了可能。應用的配置常常可能成為棘手的任務,完全取決於你正在使用的框架,但幸運的是,我們正在使用的 Laravel 讓這件事變得相當簡單。
所以讓我們開始創建一個用於演示的路由(route)。到 app/Http/routes.php中添加下面這條路由:
Route::resource('demo', 'DemoController');
通過使用 Route::resource,我們就獲得了預定義好的 index,show,create,edit,update,store 和 destroy 路由。
為了實現良好的對稱性,現在我們可以使用 artisan 命令行工具來為我們創建對應的控制器(controller)。鍵入如下指令:
php artisan make:controller DemoController
讓我們打開創建好的文件,將 index 方法修改為如下內容:
public function index() { return view('demo.index'); }
現在讓我們繼續在 app/Resources/views 目錄下創建一個名為 Demo 的文件夾,並在文件夾中創建一個名為 index.blade.php 的視圖(view)文件,內容如下:
@extends('layouts.master')
@section('content') <h1>Demo Page</h1> @endsection
這個例子中我們正在調用一個我已經在 layouts 文件夾中創建了的 master 頁面master.blade.php。如果你的 master 用了另一個名字,那么這里你得替換掉。如果你沒有 master 頁面,那么就刪掉第一行 extends 的全部內容,包括 @sectioin 申明。
假設你已經配置好了你的開發環境並解析了你的域名,那么當你訪問路由yourapplication.com/demo,你應該可以看到內容 Demo Page 了。
好的,那么現在就讓我們來創建一個Service Provider。這個Service Provider不會做太多特別有用的事情。它只是用來向你展示如何搭建它。
讓我們在 app 目錄下創建一個 Helpers 文件夾。然后在 Helpers 文件夾里,創建一個 Contracts 文件夾。在 Contracts 文件夾里,創建文件RocketShipContract.php 並寫入下面的內容:
<?php
namespace App\Helpers\Contracts; Interface RocketShipContract { public function blastOff(); }
正如你所知,接口(interface)是一種用來強化架構的契約(contract)。為了定義類的接口,它必須包含名為 blastOff 的公共函數(public function)。
所以為什么要費力地創建一個契約呢?其實,Laravel 有一個神奇的功能是你可以類型提示契約,Service Provider會返回一個受它約束的具體類的實例。這實現了無與倫比的靈活性和松耦合的結構,因為你的工作將可以輕松地通過一行代碼來完成。我們即將看到這是如何工作的。
首先,讓我們創建一個具體類。在 app/Helpers 文件夾中,創建RocketShip.php,代碼如下:
<?php
namespace app\Helpers; use App\Helpers\Contracts\RocketShipContract; class RocketShip implements RocketShipContract { public function blastOff() { return 'Houston, we have ignition'; } }
你可以看到我們的具體類沒有做很多事,但我們則對如何配合在一起更感興趣。你可以自己決定你想給你的應用提供什么服務。
好的,現在我們要來創建一個符合契約和具體類的Service Provider了。在命令行中鍵入下面的指令:
php artisan make:provider RocketShipServiceProvider
回車確認,它就會為你創建好一個類。
新文件位於 app/Providers。前往這個文件,修改為如下內容:
<?php
namespace App\Providers; use Illuminate\Support\ServiceProvider; use App\Helpers\RocketLauncher; class RocketShipServiceProvider extends ServiceProvider { protected $defer = true; /** * Bootstrap the application services. * * @return void */ public function boot() { // } /** * Register the application services. * * @return void */ public function register() { $this->app->bind('App\Helpers\Contracts\RocketShipContract', function(){ return new RocketShip(); }); } /** * Get the services provided by the provider. * * @return array */ public function provides() { return ['App\Helpers\Contracts\RocketShipContract']; } }
讓我們看一下這一段:
<?php
namespace App\Providers; use Illuminate\Support\ServiceProvider; use App\Helpers\RocketShip; class RocketShipServiceProvider extends ServiceProvider {
簡單粗暴。我們有了命名空間,use 申明和 class 申明。當你創建Service Provider時,你要導入(import)具體類,像這里我在 use 申明中導入了 RocketShip。
接下來是:
protected $defer = true;
屬性 $defer 設置為 true 代表這個類只有在必要的時候才會被加載,這樣應用可以更高效地運行。
接下來我們有一個 boot 函數,這只是個空的存根,我們不會對它做任何配置。
然后,我們有 register 方法:
/**
* Register the application services.
*
* @return void */ public function register() { $this->app->bind('App\Helpers\Contracts\RocketShipContract', function(){ return new RocketShip(); }); }
你可以看到我們正在使用綁定方法來將契約和具體類綁定到一起。這是就是Service Provider定義具體類方法的地方。所以你可以很便捷地調整你想要綁定的類。之后我們會看到這如何起效。
最后,我們有 provides 方法:
/**
* Get the services provided by the provider.
*
* @return array */ public function provides() { return ['App\Helpers\Contracts\RocketShipContract']; } }
如果你把屬性 $defer 設為 true 的話你就會需要這個方法了。
不管怎么說,這是一個相當簡單的類,只是涵蓋了部分的精華。
好,接下來我們需要告訴我們的應用來找到這個類,我們通過把它添加到config/app.php 中的 providers 數組來實現。
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\RocketShipServiceProvider::class,
這里包含了一些其它的Provider作為參考,你可以看到我們的Provider在最后一行。保存完你就可以離開這里繼續后面的了。
我們來修改一下 DemoController 的 index 方法:
public function index(RocketShipContract $rocketship) { $boom = $rocketship->blastOff(); return view('demo.index', compact('boom')); }
所以在這里,我們鍵入 RocketShipContract 並傳遞給實例變量 $rocketship。Laravel 通過Service Provider獲知你其實想要的是 RocketShip 類,因為你在服務提供者里把它和契約綁定了。是不是很酷?
然后我們簡單地調用 blastoff 方法並把它賦值給一個要傳遞向視圖的變量。讓我們來修改一下視圖:
@extends('layouts.master')
@section('content') {{ $boom }} @endsection
你可以看到我正在使用 blade 打印變量。所以瀏覽器中應該可以看到:
Houston, we have ignition.
所以現在為了更簡單的描述能實現什么,我們可以在 Helpers 文件夾中創建第二個具體類。我們把它命名為 RocketLauncher.php,內容如下:
<?php
namespace app\Helpers; use App\Helpers\Contracts\RocketShipContract; class RocketLauncher implements RocketShipContract { public function blastOff() { return 'Houston, we have launched!'; } }
你可以發現這個我們的 RocketShip 類很像,只是 blastoff 方法略有不同。所以我們在Service Provider的 register 方法中修改其中一行代碼來實現它:
public function register() { $this->app->bind('App\Helpers\Contracts\RocketShipContract', function(){ return new RocketLauncher(); }); }
還包括 use 申明:
use App\Helpers\RocketLauncher;
根據上面的簡單變動,我們現在有了基於契約約束的不同的實現,所以瀏覽器里的結果也會產生相應變化。
盡管我們為了這個教程只是做了一個超級無聊的例子,你還是可以通過它看到這個架構的好處。通過編寫一個契約而不是一個具體類,我們給自己提供了一種更靈活和簡單的而方法來管理代碼。
這里有一些“陷阱”你需要注意。你沒法直接重命名一個Service Provider,而是需要刪除它並通過 artisan 新建一個,因為創建時其它地方也會有一些不被注意的改動。這很可能與自動加載(autoload)有關。如果你發生了這類問題,你可以嘗試在命令行運行 composer dump-autoload。如果仍不起作用,那就還是刪除文件並重新創建吧。
另一件事是務必在最后一步才把Service Provider添加到 config/app.php。如果里面配置了一個並不存在的類估計 artisan 會崩潰。