門面 (Facades)
簡介
在整個 Laravel 文件中,你會看到許多透過“門面(facades)”與 Laravel 功能互動的程式碼示例。門面為應用程式服務容器中可用的類提供了“靜態”介面。Laravel 附帶了許多門面,幾乎涵蓋了 Laravel 的所有功能。
Laravel 門面作為服務容器中底層類的“靜態代理”,既提供了簡潔、富有表現力的語法優勢,又保持了比傳統靜態方法更好的可測試性和靈活性。如果你不完全理解門面是如何工作的也沒關係——順其自然,繼續學習 Laravel 即可。
Laravel 的所有門面都定義在 Illuminate\Support\Facades 名稱空間中。因此,我們可以像這樣輕鬆訪問門面:
1use Illuminate\Support\Facades\Cache;2use Illuminate\Support\Facades\Route;3 4Route::get('/cache', function () {5 return Cache::get('key');6});
在整個 Laravel 文件中,許多示例都將使用門面來演示框架的各種功能。
輔助函式
為了補充門面,Laravel 提供了各種全域性“輔助函式(helper functions)”,使與常見 Laravel 功能的互動變得更加容易。你可能會用到的一些常用輔助函式包括 view、response、url、config 等。Laravel 提供的每個輔助函式都記錄在其相應的功能文件中;不過,完整的列表可在專門的輔助函式文件中檢視。
例如,與其使用 Illuminate\Support\Facades\Response 門面來生成 JSON 響應,我們完全可以使用 response 函式。由於輔助函式是全域性可用的,因此無需匯入任何類即可使用它們:
1use Illuminate\Support\Facades\Response; 2 3Route::get('/users', function () { 4 return Response::json([ 5 // ... 6 ]); 7}); 8 9Route::get('/users', function () {10 return response()->json([11 // ...12 ]);13});
何時使用門面
門面有很多好處。它們提供了簡潔、易記的語法,讓你能夠使用 Laravel 的功能,而無需記住必須手動注入或配置的長類名。此外,由於它們獨特地使用了 PHP 的動態方法,因此它們非常易於測試。
然而,使用門面時需要小心。門面的主要風險在於類“作用域蔓延(scope creep)”。由於門面使用起來非常簡單且不需要注入,很容易導致你的類持續增長,並在單個類中使用過多的門面。使用依賴注入時,大型建構函式會提供視覺反饋,提示你的類正變得過於臃腫,從而緩解了這種可能性。因此,在使用門面時,請特別注意類的大小,以確保其職責範圍保持在狹窄的範圍內。如果你的類變得過大,請考慮將其拆分為多個較小的類。
門面 vs. 依賴注入
依賴注入的主要好處之一是能夠替換注入類的實現。這在測試過程中非常有用,因為你可以注入一個模擬物件(mock)或存根(stub),並斷言該存根上的各種方法已被呼叫。
通常,測試真正的靜態類方法是不可能的。但是,由於門面使用動態方法將方法呼叫代理給從服務容器解析出來的物件,我們實際上可以像測試已注入的類例項一樣測試門面。例如,給定以下路由:
1use Illuminate\Support\Facades\Cache;2 3Route::get('/cache', function () {4 return Cache::get('key');5});
使用 Laravel 的門面測試方法,我們可以編寫以下測試來驗證 Cache::get 方法是否按預期引數被呼叫:
1use Illuminate\Support\Facades\Cache; 2 3test('basic example', function () { 4 Cache::shouldReceive('get') 5 ->with('key') 6 ->andReturn('value'); 7 8 $response = $this->get('/cache'); 9 10 $response->assertSee('value');11});
1use Illuminate\Support\Facades\Cache; 2 3/** 4 * A basic functional test example. 5 */ 6public function test_basic_example(): void 7{ 8 Cache::shouldReceive('get') 9 ->with('key')10 ->andReturn('value');11 12 $response = $this->get('/cache');13 14 $response->assertSee('value');15}
門面 vs. 輔助函式
除了門面之外,Laravel 還包含各種可以執行常見任務(如生成檢視、觸發事件、分發任務或傳送 HTTP 響應)的“輔助”函式。許多這些輔助函式執行與相應門面相同的功能。例如,以下門面呼叫和輔助函式呼叫是等效的:
1return Illuminate\Support\Facades\View::make('profile');2 3return view('profile');
門面和輔助函式之間在實際使用中沒有任何區別。當使用輔助函式時,你仍然可以像測試相應門面一樣對其進行測試。例如,給定以下路由:
1Route::get('/cache', function () {2 return cache('key');3});
cache 輔助函式將呼叫 Cache 門面底層類上的 get 方法。因此,即使我們使用的是輔助函式,我們也可以編寫以下測試來驗證該方法是否按預期引數被呼叫:
1use Illuminate\Support\Facades\Cache; 2 3/** 4 * A basic functional test example. 5 */ 6public function test_basic_example(): void 7{ 8 Cache::shouldReceive('get') 9 ->with('key')10 ->andReturn('value');11 12 $response = $this->get('/cache');13 14 $response->assertSee('value');15}
門面如何工作
在 Laravel 應用程式中,門面是一個提供對容器中物件訪問的類。實現這一功能的機制位於 Facade 類中。Laravel 的門面以及你自己建立的任何自定義門面,都將繼承基礎的 Illuminate\Support\Facades\Facade 類。
Facade 基類利用 __callStatic() 魔術方法將呼叫從你的門面延遲到從容器解析出的物件。在下面的示例中,呼叫了 Laravel 的快取系統。僅看這段程式碼,人們可能會認為是在 Cache 類上呼叫了靜態 get 方法:
1<?php 2 3namespace App\Http\Controllers; 4 5use Illuminate\Support\Facades\Cache; 6use Illuminate\View\View; 7 8class UserController extends Controller 9{10 /**11 * Show the profile for the given user.12 */13 public function showProfile(string $id): View14 {15 $user = Cache::get('user:'.$id);16 17 return view('profile', ['user' => $user]);18 }19}
請注意,在檔案頂部我們“匯入”了 Cache 門面。此門面作為訪問 Illuminate\Contracts\Cache\Factory 介面底層實現的代理。我們使用門面進行的任何呼叫都將傳遞給 Laravel 快取服務的底層例項。
如果我們檢視那個 Illuminate\Support\Facades\Cache 類,你會發現其中並沒有靜態的 get 方法:
1class Cache extends Facade 2{ 3 /** 4 * Get the registered name of the component. 5 */ 6 protected static function getFacadeAccessor(): string 7 { 8 return 'cache'; 9 }10}
相反,Cache 門面繼承了基類 Facade 並定義了 getFacadeAccessor() 方法。此方法的作用是返回服務容器繫結的名稱。當用戶在 Cache 門面上引用任何靜態方法時,Laravel 會從服務容器中解析 cache 繫結,並對該物件執行請求的方法(在本例中為 get)。
即時門面
使用即時門面,你可以將應用程式中的任何類視為門面。為了說明它是如何使用的,我們先來看一些不使用即時門面的程式碼。例如,假設我們的 Podcast 模型有一個 publish 方法。然而,為了釋出播客,我們需要注入一個 Publisher 例項:
1<?php 2 3namespace App\Models; 4 5use App\Contracts\Publisher; 6use Illuminate\Database\Eloquent\Model; 7 8class Podcast extends Model 9{10 /**11 * Publish the podcast.12 */13 public function publish(Publisher $publisher): void14 {15 $this->update(['publishing' => now()]);16 17 $publisher->publish($this);18 }19}
將釋出者實現注入到該方法中,使我們能夠輕鬆地獨立測試該方法,因為我們可以模擬注入的釋出者。但是,它要求我們每次呼叫 publish 方法時都必須傳遞一個釋出者例項。使用即時門面,我們可以保持相同的可測試性,同時無需顯式傳遞 Publisher 例項。要生成即時門面,只需在匯入類的名稱空間前加上 Facades 字首即可:
1<?php 2 3namespace App\Models; 4 5use App\Contracts\Publisher; 6use Facades\App\Contracts\Publisher; 7use Illuminate\Database\Eloquent\Model; 8 9class Podcast extends Model10{11 /**12 * Publish the podcast.13 */14 public function publish(Publisher $publisher): void 15 public function publish(): void 16 {17 $this->update(['publishing' => now()]);18 19 $publisher->publish($this); 20 Publisher::publish($this); 21 }22}
當使用即時門面時,釋出者實現將使用 Facades 字首之後的那部分介面或類名從服務容器中解析出來。在測試時,我們可以使用 Laravel 內建的門面測試輔助函式來模擬此方法呼叫:
1<?php 2 3use App\Models\Podcast; 4use Facades\App\Contracts\Publisher; 5use Illuminate\Foundation\Testing\RefreshDatabase; 6 7pest()->use(RefreshDatabase::class); 8 9test('podcast can be published', function () {10 $podcast = Podcast::factory()->create();11 12 Publisher::shouldReceive('publish')->once()->with($podcast);13 14 $podcast->publish();15});
1<?php 2 3namespace Tests\Feature; 4 5use App\Models\Podcast; 6use Facades\App\Contracts\Publisher; 7use Illuminate\Foundation\Testing\RefreshDatabase; 8use Tests\TestCase; 9 10class PodcastTest extends TestCase11{12 use RefreshDatabase;13 14 /**15 * A test example.16 */17 public function test_podcast_can_be_published(): void18 {19 $podcast = Podcast::factory()->create();20 21 Publisher::shouldReceive('publish')->once()->with($podcast);22 23 $podcast->publish();24 }25}
門面類參考
下面列出了每個門面及其底層類。這是一個快速查閱特定門面根(facade root)API 文件的有用工具。適用時,還包括了服務容器繫結鍵。