小白也能看懂的 Laravel 核心概念講解
自動依賴註入
什麽是依賴註入,用大白話將通過類型提示的方式向函數傳遞參數。
實例 1
首先,定義一個類:
/routes/web.php
class Bar {}
假如我們在其他地方要使用到 Bar
提供的功能(服務),怎麽辦,直接傳入參數即可:
/routes/web.php
Route::get(‘bar‘, function(Bar $bar) {
dd($bar);
});
訪問 /bar
,顯示 $bar
的實例:
Bar {#272}
也就是說,我們不需要先對其進行實例!如果學過 PHP 的面向對象,都知道,正常做法是這樣:
class Bar {}
$bar = new Bar();
dd($bar);
實例 2
可以看一個稍微復雜的例子:
class Baz {}
class Bar
{
public $baz;
public function __construct(Baz $baz)
{
$this->baz = $baz;
}
}
$baz = new Baz();
$bar = new Bar($baz);
dd($bar);
為了在 Bar
中能夠使用 Baz
的功能,我們需要實例化一個 Baz
,然後在實例化 Bar
的時候傳入 Baz
實例。
在 Laravel 中,不僅僅可以自動註入 Bar
,也可以自動註入 Baz
/routes/web.php
class Baz {}
class Bar
{
public $baz;
public function __construct(Baz $baz)
{
$this->baz = $baz;
}
}
Route::get(‘bar‘, function(Bar $bar) {
dd($bar->baz);
});
顯示結果:
Baz {#276}
小結
通過上述兩個例子,可以看出,在 Laravel 中,我們要在類或者函數中使用其他類體用的服務,只需要通過類型提示的方式傳遞參數,而 Laravel 會自動幫我們去尋找響對應的依賴。
那麽,Laravel 是如何完成這項工作的呢?答案就是通過服務容器。
服務容器
什麽是服務容器
服務容器,很好理解,就是裝著各種服務實例的特殊類。可以通過「去餐館吃飯」來進行類比:
-
吃飯 - 使用服務,即調用該服務的地方
-
飯 - 服務
-
盤子 - 裝飯的容器,即服務容器
-
服務員 - 服務提供者,負責裝飯、上飯
這個過程在 Laravel 中如何實現呢?
-
飯
定義 Rice 類:
/app/Rice.php
<?php
namespace App;
class Rice
{
public function food()
{
return ‘香噴噴的白米飯‘;
}
}
-
把飯裝盤子
在容器中定義了名為 rice
的變量(你也可以起其他名字,比如 rice_container
),綁定了 Food
的實例:
app()->bind(‘rice‘, function (){
return new \App\Rice();
});
也可以寫成:
app()->bind(‘rice‘,\App\Rice::class);
現在,吃飯了,通過 make
方法提供吃飯的服務:
Route::get(‘eat‘, function() {
return app()->make(‘rice‘)->food();
// 或者 return resolve(‘rice‘)->food();
});
make
方法傳入我們剛才定義的變量名即可調用該服務。
訪問 /eat
,返回 香噴噴的白米飯
。
為了方便起見,我們在路由文件中直接實現了該過程,相當於自給自足。但是服務通常由服務提供者來管理的。
因此,我們可以讓 AppServiceProvider
這個服務員來管理該服務:
/app/Providers/AppServiceProvider.php
namespace App\Providers;
public function register()
{
$this->app->bind(‘food_container‘,Rice::class);
}
更為常見的是,我們自己創建一個服務員:
$ php artisan make:provider RiceServiceProvider
註冊:
/app/Providers/RiceServiceProvider.php
<?php
use App\Rice;
public function register()
{
$this->app->bind(‘rice‘,Rice::class);
}
這裏定義了 register()
方法,但是還需要調用該方法才能真正綁定服務到容器,因此,需要將其添加到 providers
數組中:
/config/app.php
‘providers‘ => [
App\Providers\RiceServiceProvider::class,
],
這一步有何作用呢?Laravel 在啟動的時候會訪問該文件,然後調用裏面的所有服務提供者的 register()
方法,這樣我們的服務就被綁定到容器中了。
小結
通過上述的例子,基本上可以理解服務容器和服務提供者的使用。當然了,我們更為常見的還是使用類型提示來傳遞參數:
use App\Rice;
Route::get(‘eat‘, function(Rice $rice) {
return $rice->food();
});
在本例中,使用自動依賴註入即可。不需要在用 bind
來手動綁定以及 make
來調用服務。那麽,為什麽還需要 bind
和 make
呢? make
比較好理解,我們有一些場合 Laravel 不能提供自動解析,那麽這時候手動使用 make
解析就可以了,而 bind
的學問就稍微大了點,後面將會詳細說明。
門面
門面是什麽,我們回到剛才的「吃飯」的例子:
Route::get(‘eat‘, function(Rice $rice) {
return $rice->food();
});
在 Laravel,通常還可以這麽寫:
Route::get(‘eat‘, function() {
return Rice::food();
});
或者
Route::get(‘eat‘, function() {
return rice()->food();
});
那麽,Laravel 是如何實現的呢?答案是通過門面。
門面方法實現
先來實現 Rice::food()
,只需要一步:
/app/RiceFacade.php
<?php
namespace App;
use Illuminate\Support\Facades\Facade;
class RiceFacade extends Facade
{
protected static function getFacadeAccessor()
{
return ‘rice‘;
}
}
現在,RiceFacade
就代理了 Rice
類了,這就是門面的本質了。我們就可以直接使用:
Route::get(‘eat‘, function() {
dd(\App\RiceFacade::food());
});
因為 \App\RiceFacade
比較冗長,我們可以用 php 提供的 class_alias
方法起個別名吧:
/app/Providers/RiceServiceProvider.php
public function register()
{
$this->app->bind(‘rice‘,\App\Rice::class);
class_alias(\App\RiceFacade::class, ‘Rice‘);
}
這樣做的話,就實現了一開始的用法:
Route::get(‘eat‘, function() {
return Rice::food();
});
看上去就好像直接調用了 Rice
類,實際上,調用的是 RiceFacade
類來代理,因此,個人覺得Facade
翻譯成假象比較合適。
最後,為了便於給代理類命名,Laravel 提供了統一命名別名的地方:
/config/app.php
‘aliases‘ => [
‘Rice‘ => \App\RiceFacade::class,
],
門面實現過程分析
首先:
Rice::food();
因為 Rice
是別名,所以實際上執行的是:
\App\RiceFacade::food()
但是我們的 RiceFacade
類裏面並沒有定義靜態方法 food
啊?怎麽辦呢?直接拋出異常嗎?不是,在 PHP 裏,如果訪問了不可訪問的靜態方法,會先調用 __callstatic
,所以執行的是:
\App\RiceFacade::__callStatic()
雖然我們在 RiceFacade
中沒有定義,但是它的父類 Facade
已經定義好了:
/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php
public static function __callStatic($method, $args)
{
// 實例化 Rice {#270}
$instance = static::getFacadeRoot();
// 實例化失敗,拋出異常
if (! $instance) {
throw new RuntimeException(‘A facade root has not been set.‘);
}
// 調用該實例的方法
return $instance->$method(...$args);
}
主要工作就是第一步實例化:
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
// 本例中:static::resolveFacadeInstance(‘rice‘)
}
進一步查看 resolveFacadeInstance()
方法:
protected static function resolveFacadeInstance($name)
{
// rice 是字符串,因此跳過該步驟
if (is_object($name)) {
return $name;
}
// 是否設置了 `rice` 實例
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
return static::$resolvedInstance[$name] = static::$app[$name];
}
第一步比較好理解,如果我們之前在 RiceFacade
這樣寫:
protected static function getFacadeAccessor()
{
return new \App\Rice;
}
那麽就直接返回 Rice
實例了,這也是一種實現方式。
主要難點在於最後這行:
return static::$resolvedInstance[$name] = static::$app[$name];
看上去像是在訪問 $app
數組,實際上是使用 數組方式來訪問對象,PHP 提供了這種訪問方式接口,而 Laravel 實現了該接口。
也就是說,$app
屬性其實就是對 Laravel 容器的引用,因此這裏實際上就是訪問容器上名為 rice
的對象。而我們之前學習容器的時候,已經將 rice
綁定了 Rice
類:
public function register()
{
$this->app->bind(‘rice‘,\App\Rice::class);
// class_alias(\App\RiceFacade::class, ‘Rice‘);
}
所以,其實就是返回該類的實例了。懂得了服務容器和服務提供者,理解門面也就不難了。
輔助方法實現
輔助方法的實現,更簡單了。不就是把 app->make(‘rice‘)
封裝起來嘛:
/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
if (! function_exists(‘rice‘)) {
function rice()
{
return app()->make(‘rice‘);
// 等價於 return app(‘rice‘);
// 等價於 return app()[‘rice‘];
}
}
然後我們就可以使用了:
Route::get(‘eat‘, function() {
dd(rice()->food());
});
小結
Laravel 提供的三種訪問類的方式:
-
依賴註入:通過類型提示的方式實現自動依賴註入
-
門面:通過代理來訪問類
-
輔助方法:通過方法的方式來訪問類
本質上,這三種方式都是借助於服務容器和服務提供者來實現。那麽,服務容器本身有什麽好處呢?我們接下來著重介紹下。
IOC
不好的實現
我們來看另外一個例子(為了方便測試,該例子都寫在路由文件中),假設有三種類型的插座:USB、雙孔、三孔插座,分別提供插入充電的服務:
class UsbsocketService
{
public function insert($deviceName){
return $deviceName." 正在插入 USB 充電";
}
}
class DoubleSocketService
{
public function insert($deviceName){
return $deviceName." 正在插入雙孔插座充電";
}
}
class ThreeSocketService
{
public function insert($deviceName){
return $deviceName." 正在插入三孔插座充電";
}
}
設備要使用插座的服務來充電:
class Device {
protected $socketType; // 插座類型
public function __construct()
{
$this->socketType = new UsbSocketService();
}
public function power($deviceName)
{
return $this->socketType->insert($deviceName);
}
}
現在有一臺手機要進行充電:
Route::get(‘/charge‘,function(){
$device = new Device();
return $device->power("手機");
});
因為 Laravel 提供了自動依賴註入功能,因此可以寫成:
Route::get(‘/charge/{device}‘,function(Device $device){
return $device->power("手機");
});
訪問 /charge/phone
,頁面顯示 phone 正在插入 USB 充電
。
假如,現在有一臺電腦要充電,用的是三孔插座,那麽我們就需要去修改 Device
類:
$this->socketType = new ThreeSocketService();
這真是糟糕的設計,設備類對插座服務類產生了依賴。更換設備類型時,經常就要去修改類的內部結構。
好的實現
為了解決上面的問題,可以參考「IOC」思路:即將依賴轉移到外部。來看看具體怎麽做。
首先定義插座類型接口:
interface SocketType {
public function insert($deviceName);
}
讓每一種插座都實現該接口:
class UsbsocketService implements SocketType
{
public function insert($deviceName){
return $deviceName." 正在插入 USB 充電";
}
}
class DoubleSocketService implements SocketType
{
public function insert($deviceName){
return $deviceName." 正在插入雙孔插座充電";
}
}
class ThreeSocketService implements SocketType
{
public function insert($deviceName){
return $deviceName." 正在插入三孔插座充電";
}
}
最後,設備中傳入接口類型而非具體的類:
class Device {
protected $socketType; // 插座類型
public function __construct(SocketType $socketType) // 傳入接口
{
$this->socketType = $socketType;
}
public function power($deviceName)
{
return $this->socketType->insert($deviceName);
}
}
實例化的時候再決定使用哪種插座類型,這樣依賴就轉移到了外部:
Route::get(‘/charge‘,function(){
$socketType = new ThreeSocketService();
$device = new Device($socketType);
echo $device->power("電腦");
});
我們現在可以再不修改類結構的情況下,方便的更換插座來滿足不同設備的充電需求:
Route::get(‘/charge‘,function(){
$socketType = new DoubleSocketService();
$device = new Device($socketType);
echo $device->power("臺燈");
});
自動依賴註入的失效
上面舉的例子,我們通過 Laravel 的自動依賴註入可以進一步簡化:
Route::get(‘/charge‘,function(Device $device){
echo $device->power("電腦");
});
這裏的類型提示有兩個,一個是 Device $device
,一個是 Device 類內部構造函數傳入的 SocketType $sockType
。第一個沒有問題,之前也試過。但是第二個 SocketType
是接口,而 Laravel 會將其當成類試圖去匹配 SocketType
的類並將其實例化,因此訪問 /charge
時候就會報錯:
Target [SocketType] is not instantiable while building [Device].
錯誤原因很明顯,Laravel 沒法自動綁定接口。因此,我們就需要之前的 bind
方法來手動綁定接口啦:
app()->bind(‘SocketType‘,ThreeSocketService::class);
Route::get(‘/charge‘,function(Device $device){
echo $device->power("電腦");
});
現在,如果要更換設備,我們只需要改變綁定的值就可以了:
app()->bind(‘SocketType‘,DoubleSocketService::class);
Route::get(‘/charge‘,function(Device $device){
echo $device->power("臺燈");
});
也就是說,我們將依賴轉移到了外部之後,進一步由第三方容器來管理,這就是 IOC。
契約
契約,不是什麽新奇的概念。其實就是上一個例子中,我們定義的接口:
interface SocketType {
public function insert($deviceName);
}
通過契約,我們就可以保持松耦合了:
public function __construct(SocketType $socketType) // 傳入接口而非具體的插座類型
{
$this->socketType = $socketType;
}
然後服務容器再根據需要去綁定哪種服務即可:
app()->bind(‘SocketType‘,UsbSocketService::class);
app()->bind(‘SocketType‘,DoubleSocketService::class);
app()->bind(‘SocketType‘,ThreeSocketService::class);
轉載:https://segmentfault.com/a/1190000009171779#articleHeader0
小白也能看懂的 Laravel 核心概念講解