Laravel 底層原理:門(mén)面(Facades)
簡(jiǎn)介
Facades 為應(yīng)用服務(wù)容器中的綁定類(lèi)提供了一個(gè)“靜態(tài)”接口。
Laravel 內(nèi)置了很多 Facades ,可以訪(fǎng)問(wèn)絕大部分 Laravel 的功能。
Laravel 的門(mén)面作為服務(wù)容器中底層類(lèi)的“靜態(tài)代理”,相比于傳統(tǒng)靜態(tài)方法,在維護(hù)時(shí)能夠提供更加易于測(cè)試、更加靈活、簡(jiǎn)明優(yōu)雅的語(yǔ)法。
Laravel 的所有門(mén)面都定義在 Illuminate\Support\Facades 命名空間下。
我們可以輕松訪(fǎng)問(wèn)到門(mén)面:
use?Illuminate\Support\Facades\Cache;Route::get('/cache', function () {return Cache::get('key');});
在整個(gè) Laravel 文檔中,很多例子使用了門(mén)面來(lái)演示框架的各種功能特性。
何時(shí)使用 Facades
門(mén)面有諸多優(yōu)點(diǎn),其提供了簡(jiǎn)單、易記的語(yǔ)法,讓我們無(wú)需記住長(zhǎng)長(zhǎng)的類(lèi)名即可使用 Laravel 提供的功能特性,此外,由于他們對(duì) PHP 動(dòng)態(tài)方法的獨(dú)到用法,使得它們很容易測(cè)試。
在使用 Facades 時(shí),有些地方還需要特別注意。
使用 Facades 最主要的風(fēng)險(xiǎn)就是會(huì)引起類(lèi)作用范圍的膨脹。
因?yàn)?Facades 使用起來(lái)非常簡(jiǎn)單而且不需要注入,就會(huì)使得我們?cè)诓唤?jīng)意間在單個(gè)類(lèi)中使用許多 Facades,從而導(dǎo)致類(lèi)變的越來(lái)越大。
而使用依賴(lài)注入的時(shí)候,使用的類(lèi)越多,構(gòu)造方法就會(huì)越長(zhǎng),在視覺(jué)上就會(huì)引起注意,提醒你這個(gè)類(lèi)有點(diǎn)龐大了。因此在使用 Facades 的時(shí)候,要特別注意控制好類(lèi)的大小,讓類(lèi)的作用范圍保持短小。
在開(kāi)發(fā)與 Laravel 進(jìn)行交互的第三方擴(kuò)展包時(shí),建議最好選擇注入 Laravel 契約 ,而不是使用 Facades 的方式來(lái)使用類(lèi)。因?yàn)閿U(kuò)展包是在 Laravel 本身之外構(gòu)建,所以你無(wú)法使用 Laravel Facades 測(cè)試輔助函數(shù)。
Facades Vs. 依賴(lài)注入
依賴(lài)注入的主要優(yōu)點(diǎn)之一是切換注入類(lèi)的實(shí)現(xiàn)的能力。這在測(cè)試的時(shí)候很有用,因?yàn)槟憧梢宰⑷胍粋€(gè) mock 或者 stub ,并斷言在 stub 上調(diào)用的各種方法。
通常,真正的靜態(tài)方法是不可能被 mock 或者 stub。但是,因?yàn)?Facades 使用動(dòng)態(tài)方法來(lái)代理從服務(wù)容器解析的對(duì)象的方法調(diào)用,我們可以像測(cè)試注入的類(lèi)實(shí)例一樣來(lái)測(cè)試 Facades。例如,像下面的路由:
use Illuminate\Support\Facades\Cache;
?
Route::get('/cache', function () {
? ? return Cache::get('key');
})
我們可以這樣編寫(xiě)測(cè)試來(lái)驗(yàn)證 Cache::get 方法以我們期望的方式被調(diào)用:
use Illuminate\Support\Facades\Cache;
/**
* 一個(gè)基礎(chǔ)功能的測(cè)試用例。
*
* @return void
*/
public function testBasicExample()
{
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$this->visit('/cache')
->see('value');
}
Facades Vs. 輔助函數(shù)
除了 Facades, Laravel 還包含各種「輔助函數(shù)」來(lái)實(shí)現(xiàn)一些常用的功能,比如生成視圖、觸發(fā)事件、調(diào)度任務(wù)或者發(fā)送 HTTP 響應(yīng)。
許多輔助函數(shù)的功能都有與之對(duì)應(yīng)的 Facade。例如,下面這個(gè) Facade 的調(diào)用和輔助函數(shù)的作用是一樣的:
return View::make('profile');
return view('profile');
這里的 Facades 和輔助函數(shù)之間沒(méi)有實(shí)際的區(qū)別。當(dāng)你使用輔助函數(shù)時(shí),你可以使用對(duì)應(yīng)的 Facade 進(jìn)行測(cè)試。例如,下面的路由:
Route::get('/cache', function () {
return cache('key');
});
在底層,輔助函數(shù) cache 實(shí)際上是調(diào)用了 Cache facade 中的 get 方法。
因此,盡管我們使用的是輔助函數(shù),我們依然可以編寫(xiě)以下測(cè)試來(lái)驗(yàn)證該方法是否使用我們預(yù)期的參數(shù)來(lái)調(diào)用:
use Illuminate\Support\Facades\Cache;
/**
* 一個(gè)基礎(chǔ)功能的測(cè)試用例。
*
* @return void
*/
public function testBasicExample()
{
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$this->visit('/cache')
->see('value');
}
Facades 工作原理
在 Laravel 應(yīng)用中,門(mén)面就是一個(gè)為容器中的對(duì)象提供訪(fǎng)問(wèn)方式的類(lèi)。該機(jī)制的原理由 Facade 類(lèi)實(shí)現(xiàn)。
不管是 Laravel 自帶的 Facades,還是用戶(hù)自定義的 Facades ,都繼承自 Illuminate\Support\Facades\Facade 類(lèi)。
門(mén)面類(lèi)只需要實(shí)現(xiàn)一個(gè)方法:getFacadeAccessor。正是 getFacadeAccessor 方法定義了從容器中解析什么,然后 Facade 基類(lèi)使用魔術(shù)方法 __callStatic() 從你的門(mén)面中調(diào)用解析對(duì)象。
在下面的例子中,調(diào)用了 Laravel 的緩存系統(tǒng)。通過(guò)瀏覽這段代碼,可以假定在 Cache 類(lèi)中調(diào)用了靜態(tài)方法 get:
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
use App\Http\Controllers\Controller;
class UserController extends Controller
{
/**
* 顯示給定用戶(hù)的信息。
*
* @param int $id
* @return Response
*/
public function showProfile($id)
{
$user = Cache::get('user:'.$id);
return view('profile', ['user' => $user]);
}
}
在上面這段代碼中,我們「導(dǎo)入」了 Cache Facade 。這個(gè) Facade 作為訪(fǎng)問(wèn) Illuminate\Contracts\Cache\Factory 接口底層實(shí)現(xiàn)的代理。我們使用 Facade 進(jìn)行的任何調(diào)用都將傳遞給 Laravel 緩存服務(wù)的底層實(shí)例。
如果我們看一下 Illuminate\Support\Facades\Cache 這個(gè)類(lèi),你會(huì)發(fā)現(xiàn)類(lèi)中根本沒(méi)有 get 這個(gè)靜態(tài)方法:
class Cache extends Facade
{
/**
* 獲取組件的注冊(cè)名稱(chēng)。
*
* @return string
*/
protected static function getFacadeAccessor() { return 'cache'; }
}
Cache Facade 繼承了 Facade 的基類(lèi),并定義了 getFacadeAccessor() 方法。這個(gè)方法的作用是返回服務(wù)容器綁定的類(lèi)的名稱(chēng)。
當(dāng)用戶(hù)調(diào)用 Cache Facade 中的任何靜態(tài)方法時(shí), Laravel 會(huì)從 服務(wù)容器 中解析 cache 綁定,然后在解析出的對(duì)象上調(diào)用所有的請(qǐng)求方法(本例中是 get)。
實(shí)時(shí)門(mén)面
使用實(shí)時(shí)門(mén)面,可以將應(yīng)用中的任意類(lèi)當(dāng)做門(mén)面來(lái)使用。
為了說(shuō)明如何使用這個(gè)功能,我們先看一個(gè)替代方案。例如我們假設(shè) Podcast 模型有一個(gè) publish 方法,盡管如此,為了發(fā)布博客,我們需要注入 Publisher 實(shí)例:
namespace App;
use App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;
class Podcast extends Model
{
/**
* Publish the podcast.
*
* @param Publisher $publisher
* @return void
*/
public function publish(Publisher $publisher)
{
$this->update(['publishing' => now()]);
$publisher->publish($this);
}
}
因?yàn)榭梢阅M注入的發(fā)布服務(wù),所以注入發(fā)布實(shí)例到該方法后允許我們輕松在隔離狀態(tài)下測(cè)試該方法。不過(guò),這要求我們每次調(diào)用 publish 方法時(shí),都要傳遞一個(gè)發(fā)布服務(wù)實(shí)例。
使用實(shí)時(shí)門(mén)面,我們可以在維持這種易于測(cè)試的前提下不必顯式傳遞 Publisher 實(shí)例。要生成一個(gè)實(shí)時(shí)門(mén)面,在導(dǎo)入類(lèi)前面加上 Facades 命名空間前綴即可:
namespace App;
use Facades\App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;
class Podcast extends Model
{
/**
* Publish the podcast.
*
* @return void
*/
public function publish()
{
$this->update(['publishing' => now()]);
Publisher::publish($this);
}
}
使用實(shí)時(shí)門(mén)面后,發(fā)布服務(wù)實(shí)例將會(huì)通過(guò)使用 Facades 前綴后的接口或類(lèi)名在服務(wù)容器中解析。
在測(cè)試的時(shí)候,我們可以使用 Laravel 自帶的門(mén)面測(cè)試輔助函數(shù)來(lái)模擬這個(gè)方法調(diào)用。
namespace Tests\Feature;
use App\Podcast;
use Tests\TestCase;
use Facades\App\Contracts\Publisher;
use Illuminate\Foundation\Testing\RefreshDatabase;
class PodcastTest extends TestCase
{
use RefreshDatabase;
/**
* A test example.
*
* @return void
*/
public function test_podcast_can_be_published()
{
$podcast = factory(Podcast::class)->create();
Publisher::shouldReceive('publish')->once()->with($podcast);
$podcast->publish();
}
}
Facade 類(lèi)參考
下面列出了每個(gè)門(mén)面及其對(duì)應(yīng)的底層類(lèi)。
這是一個(gè)查找給定 Facade 類(lèi) API 文檔的工具。服務(wù)容器綁定的可用鍵值也包含在內(nèi)。
| Facade | 類(lèi) | 服務(wù)容器綁定 |
|---|---|---|
| App | Illuminate\Foundation\Application | app |
| Artisan | Illuminate\Contracts\Console\Kernel | artisan |
| Auth | Illuminate\Auth\AuthManager | auth |
| Blade | Illuminate\View\Compilers\BladeCompiler | blade.compiler |
| Bus | Illuminate\Contracts\Bus\Dispatcher | |
| Cache | Illuminate\Cache\Repository | cache |
| Config | Illuminate\Config\Repository | config |
| Cookie | Illuminate\Cookie\CookieJar | cookie |
| Crypt | Illuminate\Encryption\Encrypter | encrypter |
| DB | Illuminate\Database\DatabaseManager | db |
| DB (Instance) | Illuminate\Database\Connection | |
| Event | Illuminate\Events\Dispatcher | events |
| File | Illuminate\Filesystem\Filesystem | files |
| Gate | Illuminate\Contracts\Auth\Access\Gate | |
| Hash | Illuminate\Contracts\Hashing\Hasher | hash |
| Lang | Illuminate\Translation\Translator | translator |
| Log | Illuminate\Log\Writer | log |
| Illuminate\Mail\Mailer | mailer | |
| Notification | Illuminate\Notifications\ChannelManager | |
| Password | Illuminate\Auth\Passwords\PasswordBrokerManager | auth.password |
| Queue | Illuminate\Queue\QueueManager | queue |
| Queue (Instance) | Illuminate\Contracts\Queue\Queue | queue |
| Queue (Base Class) | Illuminate\Queue\Queue | |
| Redirect | Illuminate\Routing\Redirector | redirect |
| Redis | Illuminate\Redis\Database | redis |
| Request | Illuminate\Http\Request | request |
| Response | Illuminate\Contracts\Routing\ResponseFactory | |
| Route | Illuminate\Routing\Router | router |
| Schema | Illuminate\Database\Schema\Blueprint | |
| Session | Illuminate\Session\SessionManager | session |
| Session (Instance) | Illuminate\Session\Store | |
| Storage | Illuminate\Contracts\Filesystem\Factory | filesystem |
| URL | Illuminate\Routing\UrlGenerator | url |
| Validator | Illuminate\Validation\Factory | validator |
| Validator (Instance) | Illuminate\Validation\Validator | |
| View | Illuminate\View\Factory | view |
| View (Instance) | Illuminate\View\View |
