1. 程式人生 > >Laravel —— 門面(Facades)

Laravel —— 門面(Facades)

簡介

門面為應用服務容器中的繫結類提供了一個“靜態”介面。Laravel 內建了很多門面,你可能在不知道的情況下正在使用它們。Laravel 的門面作為服務容器中底層類的“靜態代理”,相比於傳統靜態方法,在維護時能夠提供更加易於測試、更加靈活、簡明優雅的語法。

Laravel 的所有門面都定義在 Illuminate\Support\Facades 名稱空間下,所以我們可以輕鬆訪問到門面:

use Illuminate\Support\Facades\Cache;

Route::get('/cache', function () {
    return Cache::get('key');
});

在整個 Laravel 文件中,很多例子使用了門面來演示框架的各種功能特性。

何時使用門面

門面有諸多優點,其提供了簡單、易記的語法,讓我們無需記住長長的類名即可使用 Laravel 提供的功能特性,此外,由於他們對 PHP 動態方法的獨到用法,使得它們很容易測試。

但是,使用門面也有需要注意的地方,一個最主要的危險就是類範圍蠕變。由於門面如此好用並且不需要注入,在單個類中使用過多門面,會讓類很容易變得越來越大。使用依賴注入則會讓此類問題緩解,因為一個巨大的建構函式會讓我們很容易判斷出類在變大。因此,使用門面的時候要尤其注意類的大小,以便控制其有限職責。

注:構建與 Laravel 互動的第三方擴充套件包時,最好注入 Laravel 

契約而不是使用門面,因為擴充套件包在 Laravel 之外構建,你將不能訪問 Laravel 的門面測試輔助函式。

門面 vs. 依賴注入

依賴注入的最大優點是可以替換注入類的實現,這在測試時很有用,因為你可以注入一個模擬或存根並且在存根上斷言不同的方法。

但是在靜態類方法上進行模擬或存根卻行不通,不過,由於門面使用了動態方法對服務容器中解析出來的物件方法呼叫進行了代理,我們也可以像測試注入類例項那樣測試門面。例如,給定以下路由:

use Illuminate\Support\Facades\Cache;

Route::get('/cache', function () {
    return Cache::get('key');
});

我們可以這樣編寫測試來驗證 Cache::get 方法以我們期望的方式被呼叫:

use Illuminate\Support\Facades\Cache;

/**
 * A basic functional test example.
 *
 * @return void
 */
public function testBasicExample()
{
    Cache::shouldReceive('get')
        ->with('key')
        ->andReturn('value');

    $this->visit('/cache')
        ->see('value');
}

門面 vs 輔助函式

除了門面之外,Laravel 還內建了許多輔助函式用於執行通用任務,比如生成檢視、觸發事件、分配任務,以及傳送 HTTP 響應等。很多輔助函式提供了和相應門面一樣的功能,例如,下面這個門面呼叫和輔助函式呼叫是等價的:

return View::make('profile');
return view('profile');

門面和輔助函式之間並不存在實質性差別,使用輔助函式的時候,可以像測試相應門面那樣測試它們。例如,給定以下路由:

Route::get('/cache', function () {
    return cache('key');
});

在呼叫底層, cache 方法會去呼叫 Cache 門面上的 get 方法,因此,儘管我們使用這個輔助函式,我們還是可以編寫如下測試來驗證這個方法以我們期望的方式和引數被呼叫:

use Illuminate\Support\Facades\Cache;

/**
 * A basic functional test example.
 *
 * @return void
 */
public function testBasicExample()
{
    Cache::shouldReceive('get')
        ->with('key')
        ->andReturn('value');

    $this->visit('/cache')
        ->see('value');
}

門面工作原理

在 Laravel 應用中,門面就是一個為容器中物件提供訪問方式的類。該機制原理由 Facade 類實現。Laravel 自帶的門面,以及我們建立的自定義門面,都會繼承自 Illuminate\Support\Facades\Facade 基類。

門面類只需要實現一個方法:getFacadeAccessor。正是 getFacadeAccessor 方法定義了從容器中解析什麼,然後 Facade 基類使用魔術方法 __callStatic() 從你的門面中呼叫解析物件。

下面的例子中,我們將會呼叫 Laravel 的快取系統,瀏覽程式碼後,也許你會覺得我們呼叫了 Cache 的靜態方法 get

<?php

namespace App\Http\Controllers;

use Cache;
use App\Http\Controllers\Controller;

class UserController extends Controller{
    /**
     * 為指定使用者顯示屬性
     *
     * @param  int  $id
     * @return Response
     */
    public function showProfile($id)
    {
        $user = Cache::get('user:'.$id);

        return view('profile', ['user' => $user]);
    }
}

注意我們在頂部位置引入了 Cache 門面。該門面作為代理訪問底層 Illuminate\Contracts\Cache\Factory 介面的實現。我們對門面的所有呼叫都會被傳遞給 Laravel 快取服務的底層例項。

如果我們檢視 Illuminate\Support\Facades\Cache 類的原始碼,將會發現其中並沒有靜態方法 get

class Cache extends Facade
{
    /**
     * 獲取元件註冊名稱
     *
     * @return string
     */
    protected static function getFacadeAccessor() { 
        return 'cache'; 
    }
}

Cache 門面繼承 Facade 基類並定義了 getFacadeAccessor 方法,該方法的工作就是返回服務容器繫結類的別名,當用戶引用 Cache 類的任何靜態方法時,Laravel 從服務容器中解析 cache 繫結,然後在解析出的物件上呼叫所有請求方法(本例中是 get)。

實時門面

使用實時門面,可以將應用中的任意類當做門面來使用。為了說明如何使用這個功能,我們先看一個替代方案。例如我們假設 Podcast 模型有一個 publish 方法,儘管如此,為了釋出部落格,我們需要注入 Publisher 例項:

<?php

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);
    }
}

因為可以模擬注入的釋出服務,所以注入釋出實現到該方法後允許我們輕鬆在隔離狀態下測試該方法。不過,這要求我們每次呼叫 publish 方法都要傳遞一個釋出服務例項,使用實時門面,我們可以在維持這種易於測試的前提下不必顯式傳遞 Publisher 例項。要生成一個實時門面,在匯入類前面加上 Facades 名稱空間字首即可:

<?php

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);
    }
}

使用實時門面後,釋出服務實現將會通過使用 Facades 字首後的介面或類名在服務容器中解析。在測試的時候,我們可以使用 Laravel 自帶的門面測試輔助函式來模擬這個方法呼叫:

<?php

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();
    }
}

門面類列表

下面列出了每個門面及其對應的底層類,這對深入給定根門面的 API 文件而言是個很有用的工具。服務容器繫結鍵也被包含進來:

門面 服務容器繫結
App Illuminate\Foundation\Application app
Artisan Illuminate\Contracts\Console\Kernel artisan
Auth Illuminate\Auth\AuthManager auth
Auth(例項) Illuminate\Contracts\Auth\Guard auth.driver
Blade Illuminate\View\Compilers\BladeCompiler blade.compiler
Broadcast Illuminate\Contracts\Broadcasting\Factory  
Broadcast(例項) Illuminate\Contracts\Broadcasting\Broadcaster  
Bus Illuminate\Contracts\Bus\Dispatcher  
Cache Illuminate\Cache\CacheManager cache
Cache(例項) Illuminate\Cache\Repository cache.store
Config Illuminate\Config\Repository config
Cookie Illuminate\Cookie\CookieJar cookie
Crypt Illuminate\Encryption\Encrypter encrypter
DB Illuminate\Database\DatabaseManager db
DB(例項) Illuminate\Database\Connection db.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
Mail Illuminate\Mail\Mailer mailer
Notification Illuminate\Notifications\ChannelManager  
Password Illuminate\Auth\Passwords\PasswordBrokerManager auth.password
Password(例項) Illuminate\Auth\Passwords\PasswordBroker auth.password.broker
Queue Illuminate\Queue\QueueManager queue
Queue(例項) Illuminate\Contracts\Queue\Queue queue.connection
Queue(基類) Illuminate\Queue\Queue  
Redirect Illuminate\Routing\Redirector redirect
Redis Illuminate\Redis\RedisManager redis
Redis(例項) Illuminate\Redis\Connections\Connection redis.connection
Request Illuminate\Http\Request request
Response Illuminate\Contracts\Routing\ResponseFactory  
Response(例項) Illuminate\Http\Response  
Route Illuminate\Routing\Router router
Schema Illuminate\Database\Schema\Builder  
Session Illuminate\Session\SessionManager session
Session(例項) Illuminate\Session\Store session.store
Storage Illuminate\Filesystem\FilesystemManager filesystem
Storage (例項) Illuminate\Contracts\Filesystem\Filesystem filesystem.disk
URL Illuminate\Routing\UrlGenerator url
Validator Illuminate\Validation\Factory validator
Validator(例項) Illuminate\Validation\Validator  
View Illuminate\View\Factory view
View(例項) Illuminate\View\View