Facade
簡介
在 Laravel 的說明文件中,我們可以看到範例程式碼都使用「Facade」來操作 Laravel 的功能。Facade 提供了一個「靜態 (Static)」介面來存取 Service Container 中提供的類別。Laravel 隨附了許多 Facade,幾乎可以存取所有的 Laravel 功能。
Laravel Facade 是一個用來存取 Service Container 中類別的一個「靜態代理 (Static Proxy)」,讓我們能使用簡潔、語意化的語法,卻由不像傳統靜態方法一樣要犧牲可測試性與靈活性。若你還不了解 Facade 如何運作,完全沒關係 —— 只要繼續使用並持續學習 Laravel 就好。
Laravel 中所有的 Facade 都定義在 Illuminate\Support\Facades
Namespace 下。因此,我們可以像這樣輕鬆地存取 Facade:
1use Illuminate\Support\Facades\Cache;2use Illuminate\Support\Facades\Route;34Route::get('/cache', function () {5 return Cache::get('key');6});
1use Illuminate\Support\Facades\Cache;2use Illuminate\Support\Facades\Route;34Route::get('/cache', function () {5 return Cache::get('key');6});
在 Laravel 說明文件中,有許多的範例都使用 Facade 來示範 Laravel 的許多功能:
輔助函式
為了能與 Facade 互補,Laravel 還提供了一系列的全域「輔助函式」,能讓你更輕鬆的使用常見的 Laravel 功能。其中,你可能常使用到的輔助函式包含:view
、response
、url
、config
⋯⋯等。Laravel 中提供的每個輔助函式都有說明文件來說明其功能。關於輔助函式完整的列表列在專門的輔助函式說明文件。
舉例來說,除了使用 Illuminate\Support\Facades\Response
Facade 來產生 JSON 回應以外,我們也可以使用 response
韓式。由於輔助函式在全域都可使用,因此我們不需要特別 Import 任何函式就可以使用:
1use Illuminate\Support\Facades\Response;23Route::get('/users', function () {4 return Response::json([5 // ...6 ]);7});89Route::get('/users', function () {10 return response()->json([11 // ...12 ]);13});
1use Illuminate\Support\Facades\Response;23Route::get('/users', function () {4 return Response::json([5 // ...6 ]);7});89Route::get('/users', function () {10 return response()->json([11 // ...12 ]);13});
When to Utilize Facades
Facade 提供了許多的好處。Facade 提供了簡介、好記憶的語法,能讓你不需記著長長的類別名稱、不需手動插入或設定類別,就能使用 Laravel 的功能。此外,由於 Facade 使用了獨特的 PHP 動態方法,因此要測試 Facade 也很簡單。
不過,在使用 Facade 的時候有幾點需要注意。第一個要注意的點是,Facade 是類別的「作用範圍陷阱 (Scope Creep)」。由於 Facade 很容易使用,而且不需要做相依性插入,所以我們很容易讓類別不斷增長、並在單一類別中使用太多的 Facade。在使用相依性插入時,這種問題很容易一眼看出,因為我們看到類別的 Constructor (建構函式) 就知道類別太肥大了。因此,在使用 Facade 時,請特別注意類別的大小,讓類別的功能範圍保持專一。若類別變的太大,請考慮將其拆分為多個小類別。
Facades vs. Dependency Injection
使用相依性插入的主要好處就是我們能替換掉要插入類別的實作。在測試時這點特別適用,因為這樣我們就能插入 Mock (模擬) 或 Stub (虛設常式),並在 Stub 上檢查各種方法是否真的有被呼叫。
一般來說,對真正的靜態類別方法來說,我們是不可能去 Mock 或 Stub 的。不過,因為 Facade 使用動態方法來代理這些方法呼叫到 Service Container 解析的物件上,因此我們就可以測試這些 Facade,就像我們在測試插入的類別實體一樣。舉例來說,假設有下列 Route:
1use Illuminate\Support\Facades\Cache;23Route::get('/cache', function () {4 return Cache::get('key');5});
1use Illuminate\Support\Facades\Cache;23Route::get('/cache', function () {4 return Cache::get('key');5});
使用 Laravel 的 Facade 測試方法,我們就能撰寫下列測試,並驗證 Cache::get
方法是否有使用預期的引數呼叫:
1use Illuminate\Support\Facades\Cache;23test('basic example', function () {4 Cache::shouldReceive('get')5 ->with('key')6 ->andReturn('value');78 $response = $this->get('/cache');910 $response->assertSee('value');11});
1use Illuminate\Support\Facades\Cache;23test('basic example', function () {4 Cache::shouldReceive('get')5 ->with('key')6 ->andReturn('value');78 $response = $this->get('/cache');910 $response->assertSee('value');11});
1use Illuminate\Support\Facades\Cache;23/**4 * A basic functional test example.5 */6public function test_basic_example(): void7{8 Cache::shouldReceive('get')9 ->with('key')10 ->andReturn('value');1112 $response = $this->get('/cache');1314 $response->assertSee('value');15}
1use Illuminate\Support\Facades\Cache;23/**4 * A basic functional test example.5 */6public function test_basic_example(): void7{8 Cache::shouldReceive('get')9 ->with('key')10 ->andReturn('value');1112 $response = $this->get('/cache');1314 $response->assertSee('value');15}
Facades vs. Helper Functions
除了 Facade 外,Laravel 也提供了多個「輔助」函式,可用來處理像是產生 View、觸發事件、分派任務、送出 HTTP Response⋯⋯等常見的工作,其中許多的輔助函式都與對應的 Facade 提供相同的功能。舉例來說,下列的 Facade 呼叫與輔助函式的呼叫是相同的:
1return Illuminate\Support\Facades\View::make('profile');23return view('profile');
1return Illuminate\Support\Facades\View::make('profile');23return view('profile');
在實務上,使用 Facade 方法與輔助函式並沒有不同,使用輔助函式時,我們還是可以像對 Facade 一樣測試這些功能。舉例來說,假設有下列 Route:
1Route::get('/cache', function () {2 return cache('key');3});
1Route::get('/cache', function () {2 return cache('key');3});
在 Laravel 中,cache
輔助函式會去呼叫 Cache
Facade 底層類別的 get
方法。因此,雖然我們在使用的是輔助函式,但我們可以撰寫下列這樣的測試來驗證該方法是否有用我們給定的引數呼叫:
1use Illuminate\Support\Facades\Cache;23/**4 * A basic functional test example.5 */6public function test_basic_example(): void7{8 Cache::shouldReceive('get')9 ->with('key')10 ->andReturn('value');1112 $response = $this->get('/cache');1314 $response->assertSee('value');15}
1use Illuminate\Support\Facades\Cache;23/**4 * A basic functional test example.5 */6public function test_basic_example(): void7{8 Cache::shouldReceive('get')9 ->with('key')10 ->andReturn('value');1112 $response = $this->get('/cache');1314 $response->assertSee('value');15}
Facade 是如何運作的?
在 Laravel 程式中,Facade 能讓我們從 Container 內存取物件。為什麼我們能這麼做?答案就藏在 Facade
類別中。Laravel 的 Facade 與你自己建立的自訂 Facade 都繼承一個基礎的 Illuminate\Support\Facades\Facade
類別。
Facade
的基礎類別使用 __callStatic()
魔法方法來將所有對 Facade 的呼叫轉移到從 Container 中解析出來的物件上。在上面的例子中,我們呼叫了 Laravel 快取系統。看一眼這個程式碼,有人可能會假設我們是在呼叫 Cache
類別的靜態 get
方法:
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use Illuminate\Support\Facades\Cache;7use Illuminate\View\View;89class UserController extends Controller10{11 /**12 * Show the profile for the given user.13 */14 public function showProfile(string $id): View15 {16 $user = Cache::get('user:'.$id);1718 return view('profile', ['user' => $user]);19 }20}
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use Illuminate\Support\Facades\Cache;7use Illuminate\View\View;89class UserController extends Controller10{11 /**12 * Show the profile for the given user.13 */14 public function showProfile(string $id): View15 {16 $user = Cache::get('user:'.$id);1718 return view('profile', ['user' => $user]);19 }20}
可以注意到,在檔案最上端,我們「Import」了 Cache
Facade。這個 Facade 會作為代理來讓我們存取底層 Illuminate\Contracts\Cache\Factory
介面的實作。使用 Facade 呼叫的所有方法都會被傳到 Laravel 快取系統的底層實體上。
若我們打開 Illuminate\Support\Facades\Cache
類別看,會發現裡面沒有靜態的 get
方法:
1class Cache extends Facade2{3 /**4 * Get the registered name of the component.5 */6 protected static function getFacadeAccessor(): string7 {8 return 'cache';9 }10}
1class Cache extends Facade2{3 /**4 * Get the registered name of the component.5 */6 protected static function getFacadeAccessor(): string7 {8 return 'cache';9 }10}
沒有 get
方法,Cache
Facade 只有繼承了基礎的 Facade
類別並定義了 getFacadeAccessor()
方法。這個方法的功能就是用來回傳 Service Container 繫結的名稱。當使用者在 Cache
Facade 上參照任何靜態方法時,Laravel 會去從 Service Container 中解析出 cache
繫結,然後在這個物件上執行要求的方法 (在這個例子中就是 get
)。
即時 Facade
使用即時 Facade 時,我們可以把程式內的任何類別都當作 Facade 來使用。為了說明這個功能,我們先來看一些不使用即時 Facade 的例子。舉例來說,我們先假設 Podcast
Model 有個 publish
方法。不過,為了要發布 (Publish) 這個 Podcast,我們還需要插入 Publisher
實體:
1<?php23namespace App\Models;45use App\Contracts\Publisher;6use Illuminate\Database\Eloquent\Model;78class Podcast extends Model9{10 /**11 * Publish the podcast.12 */13 public function publish(Publisher $publisher): void14 {15 $this->update(['publishing' => now()]);1617 $publisher->publish($this);18 }19}
1<?php23namespace App\Models;45use App\Contracts\Publisher;6use Illuminate\Database\Eloquent\Model;78class Podcast extends Model9{10 /**11 * Publish the podcast.12 */13 public function publish(Publisher $publisher): void14 {15 $this->update(['publishing' => now()]);1617 $publisher->publish($this);18 }19}
將 Publisher 實作插入到這個方法後,只要 Mock 這個插入的 Publisher,我們就能輕鬆地在分離的狀態下測試這個方法。不過,這樣一來每次我們呼叫 publish
方法也都需要傳入一個 Publisher 實體。使用即時 Facade,我們一樣可以能保有可測試性,又不需要顯式傳入 Publisher
實體。若要產生即時 Facade,請在 Import 類別的 Namespace 前方加上 Facades
:
1<?php23namespace App\Models;45use App\Contracts\Publisher;6use Facades\App\Contracts\Publisher;7use Illuminate\Database\Eloquent\Model;89class Podcast extends Model10{11 /**12 * Publish the podcast.13 */14 public function publish(Publisher $publisher): void15 public function publish(): void16 {17 $this->update(['publishing' => now()]);1819 $publisher->publish($this);20 Publisher::publish($this);21 }22}
1<?php23namespace App\Models;45use App\Contracts\Publisher;6use Facades\App\Contracts\Publisher;7use Illuminate\Database\Eloquent\Model;89class Podcast extends Model10{11 /**12 * Publish the podcast.13 */14 public function publish(Publisher $publisher): void15 public function publish(): void16 {17 $this->update(['publishing' => now()]);1819 $publisher->publish($this);20 Publisher::publish($this);21 }22}
在使用即時 Facade 時,Laravel 會使用 Facades
前置詞後方的介面或類別名稱來從 Service Container 上解析出 Publisher 實作。測試時,我們可以使用 Laravel 的內建 Facade 測試工具來 Mock 這個方法呼叫:
1<?php23use App\Models\Podcast;4use Facades\App\Contracts\Publisher;5use Illuminate\Foundation\Testing\RefreshDatabase;67uses(RefreshDatabase::class);89test('podcast can be published', function () {10 $podcast = Podcast::factory()->create();1112 Publisher::shouldReceive('publish')->once()->with($podcast);1314 $podcast->publish();15});
1<?php23use App\Models\Podcast;4use Facades\App\Contracts\Publisher;5use Illuminate\Foundation\Testing\RefreshDatabase;67uses(RefreshDatabase::class);89test('podcast can be published', function () {10 $podcast = Podcast::factory()->create();1112 Publisher::shouldReceive('publish')->once()->with($podcast);1314 $podcast->publish();15});
1<?php23namespace Tests\Feature;45use App\Models\Podcast;6use Facades\App\Contracts\Publisher;7use Illuminate\Foundation\Testing\RefreshDatabase;8use Tests\TestCase;910class PodcastTest extends TestCase11{12 use RefreshDatabase;1314 /**15 * A test example.16 */17 public function test_podcast_can_be_published(): void18 {19 $podcast = Podcast::factory()->create();2021 Publisher::shouldReceive('publish')->once()->with($podcast);2223 $podcast->publish();24 }25}
1<?php23namespace Tests\Feature;45use App\Models\Podcast;6use Facades\App\Contracts\Publisher;7use Illuminate\Foundation\Testing\RefreshDatabase;8use Tests\TestCase;910class PodcastTest extends TestCase11{12 use RefreshDatabase;1314 /**15 * A test example.16 */17 public function test_podcast_can_be_published(): void18 {19 $podcast = Podcast::factory()->create();2021 Publisher::shouldReceive('publish')->once()->with($podcast);2223 $podcast->publish();24 }25}
Facade 類別參照
在下表中,讀者可以找到所有的 Facade 與其底層的類別。對於像在 API 說明文件中找到某個 Facade 來源的時候,下表是很實用的工具。若有 Service container 的繫結索引鍵時,下表中也會列出。