小白也能看懂的 Laravel 核心概念講解

自動依賴注入

什麼是依賴注入,用大白話將經過類型提示的方式向函數傳遞參數。php

實例 1

首先,定義一個類:laravel

/routes/web.php
class Bar {}

假如咱們在其餘地方要使用到 Bar 提供的功能(服務),怎麼辦,直接傳入參數便可:web

/routes/web.php
Route::get('bar', function(Bar $bar) { dd($bar); });

訪問 /bar,顯示 $bar 的實例:segmentfault

Bar {#272}

也就是說,咱們不須要先對其進行實例!若是學過 PHP 的面向對象,都知道,正常作法是這樣:數組

class Bar {} $bar = new Bar(); dd($bar);

實例 2

能夠看一個稍微複雜的例子:app

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 實例。socket

在 Laravel 中,不只僅能夠自動注入 Bar,也能夠自動注入 Baz:ide

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

而後服務容器再根據須要去綁定哪一種服務便可:

轉載:https://segmentfault.com/a/1190000009171779#articleHeader0app()->bind('SocketType',UsbSocketService::class); app()->bind('SocketType',DoubleSocketService::class); app()->bind('SocketType',ThreeSocketService::class);
相關文章
相關標籤/搜索