Laravel核心概念剖析

xmkl發表於2022-05-30

文章內容純屬個人對理解,如有不對的地方還望大佬多多指教。文章內容基於Laravel5.5,加黑字型的是關鍵詞可以幫助加深理解。

建議結合本社群【服務容器】和【服務提供者】章節看加深理解。

核心概念

Laravel的核心概念是服務容器服務提供者,他們是框架的基礎。框架提供的所有能力都由服務提供者引導繫結到容器中。容器主要定義了一些繫結解析服務的方式,這些方式也可以理解為容器對外提供的介面。服務提供者則引導了容器如何去解析和例項化該服務。下面詳細剖析服務容器和服務提供者的概念:

1、服務容器

服務容器是一個用於管理類依賴以及實現依賴注入的強有力工具,它主要有laravel/framework/src/Illuminate/Container/Container.php類提供服務,文中具體程式碼可以在該類中找到。

1.1 繫結服務介面

容器類用來管理繫結關係的屬性主要有兩個bindingsinstance,其中instance屬性的繫結關係是單例的。下面看繫結服務的幾種方式:

// 1、透過bind繫結
$this->app->bind('redis.connection', function ($app) {
    return $app['redis']->connection();
});

// 2、繫結單例
$this->app->singleton('redis', function ($app) {
    $config = $app->make('config')->get('database.redis');
    return new RedisManager(Arr::pull($config, 'client', 'predis'), $config);
});

// 3、直接繫結例項(單例)
$this->app->instance('test', new Test);

再看這三種繫結方式的具體實現:

1.1.1bind函式

bind在將繫結關係放入到了bindings屬性,此處只是繫結抽象和實現的關係,具體實現不會被例項化。

// 解釋:將抽象$abstract繫結到具體的實現$concrete。$shared定義該實現是否為單例
public function bind($abstract, $concrete = null, $shared = false)
{
    // 刪除alias和instance的繫結,將抽象$abstract繫結到$bindings
    $this->dropStaleInstances($abstract);
    // 如果沒有給出具體型別,則設定具體型別為該抽象型別
    if (is_null($concrete)) {
      $concrete = $abstract;
    }

    // 如果具體的實現$concrete不是匿名函式、則把它包裝為匿名函式
    if (! $concrete instanceof Closure) {
      $concrete = $this->getClosure($abstract, $concrete);
    }

   // 將抽象的實現繫結到bindings屬性
    $this->bindings[$abstract] = compact('concrete', 'shared');

   // 如果該抽象已在容器中解析,則觸發該抽象繫結的回撥。 
    if ($this->resolved($abstract)) {
      $this->rebound($abstract);
    }
}
1.1.2singleton函式

可見singleton函式呼叫的是bind,第三個引數shared引數為true,指定該服務為單例。

// 繫結單例
public function singleton($abstract, $concrete = null)
{
  $this->bind($abstract, $concrete, true);
}
1.1.3instance函式

instance函式繫結的是例項,也可以是其他基礎型別,解析時作為單例處理。

// 繫結例項 
public function instance($abstract, $instance)
{ 
    // 刪除abstractAliases的繫結 
    $this->removeAbstractAlias($abstract); 

    // 檢查是否已繫結到$bindings、$instances或$aliases
    $isBound = $this->bound($abstract); 
    unset($this->aliases[$abstract]); 

    $this->instances[$abstract] = $instance; 

    // 如果該抽象已繫結過(屬於重複繫結),則觸發該抽象繫結的回撥。
    if ($isBound) {
        $this->rebound($abstract); 
    } 
    return $instance;
}

關於繫結大概就這些,此處先不考慮服務延遲載入,上下文繫結等等…先拋開復雜的處理看下繫結的本質,下面看下對解析的處理。

2.2 解析服務介面

解析對外提供的函式有app()->make(),app()->makeWith(),resolve(),app()app()->get()這幾個,其中app()->get()會提前判斷要解析的服務是否存在,不存在話丟擲EntryNotFoundException異常,其他介面則直接丟擲ReflectionException異常。最終的解析邏輯都一樣,主要依賴容器中的resolvebuild函式,下面具體看下這兩個函式的實現:

2.2.1 resolve 函式
protected function resolve($abstract, $parameters = [])
{
    $abstract = $this->getAlias($abstract);

    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    // 對單例服務的處理,不需要處理上下文的情況下,如果存在直接返回,不會重新new一個新的例項
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    $this->with[] = $parameters;

    // 嘗試從bindings中獲取具體實現
    $concrete = $this->getConcrete($abstract);

    // 如果抽象$abstract和實現$concrete完全相等 或者 $concrete是匿名函式,則用build去解析,否則將具體實現$concrete再走一遍make,最終也是走本介面,可以看作是一個遞迴呼叫
    if ($this->isBuildable($concrete, $abstract)) {

       // 這快處理了類的例項化,依賴檢查等,下面具體看
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    // 對這個服務做一些擴充套件處理
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

     // 對單例的處理
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }

     // 觸發解析回撥
    $this->fireResolvingCallbacks($abstract, $object);

    // 標記抽象已被解析
    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
}

resolve它主要做了對單例的處理,上下文檢查,解析事件的觸發和服務的擴充套件性處理。

2.2.2 build函式

此處是依賴處理和例項化物件的關鍵。

public function build($concrete)
{
    // 如果具體實現是個匿名函式,直接呼叫匿名函式,由其返回實現即可。
    if ($concrete instanceof Closure) {
        // getLastParameterOverrid的用意是?
        return $concrete($this, $this->getLastParameterOverride());
    }
     // 獲取類的反射例項
    $reflector = new ReflectionClass($concrete);

    // 不可以例項化時丟擲異常
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $this->buildStack[] = $concrete;
    // 獲取類的建構函式
    $constructor = $reflector->getConstructor();

    // 如果沒有建構函式,直接new出例項即可
    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }

    // 獲取建構函式依賴項
    $dependencies = $constructor->getParameters();

    // 解析建構函式依賴項(有興趣的可以看下這個函式的實現) 
    $instances = $this->resolveDependencies(
        $dependencies
    );

    array_pop($this->buildStack);
    //建立一個類例項,並注入依賴項
    return $reflector->newInstanceArgs($instances);
}

到這裡,服務繫結和服務解析就差不多了。這些東西有什麼用呢?下面再看服務提供者中怎麼使用它們。

2、服務提供者

所有的服務提供者都繼承於Illuminate\Support\ServiceProvider抽象類,該抽象類對子類暴露了容器變數$app用來操作容器。在服務提供者中包會含一個 register 和一個 boot 函式。在 register 方法中, 你可以呼叫容器對外提供的繫結介面註冊服務。boot函式會在所有服務註冊完之後依次呼叫。下面看一個具體的服務提供者例子

namespace App\Providers;

use Riak\Connection;
use Illuminate\Support\ServiceProvider;

 /**
 * 繼承自ServiceProvider類
 */
class RiakServiceProvider extends ServiceProvider
{
    /** 
    * 是否延時載入該服務提供者,此處意味著在框架啟動階段不會呼叫register函式進行服務繫結,只會將該服務放入延遲載入列表,這些資料會放在bootstrap/cache/services.php檔案中,該檔案包含了所有服務提供者提供的所有服務,框架每次啟動時都會檢查是否需要重新生成。
    * @var bool 
    */ 
    protected $defer  =  true;

    // 在服務容器裡註冊(框架啟動階段呼叫)
    public function register()
    {
        $this->app->singleton(Connection::class, function ($app) {
            return new Connection(config('riak'));
        });

        $this->app->bind('redis', function ($app) {
            return new Redis;
        });

        $this->app->instance('test', new Test);
    }

    // 所有的服務提供者載入完後,會依次呼叫boot函式
    public function boot()
    {
        ... code
    }

    /**
     * 獲取提供器提供的服務。 
    * 此處用於生成bootstrap/cache/services.php檔案時獲取該提供者提供的服務列表
    * @return array 
    */
    public function provides()
    {
        return [Connection::class, 'redis', 'test'];
    }
}

新增完服務提供者之後,需要將該提供者在config/app.php 配置檔案中註冊,才會生效。然後你就可以在框架的任何地方使用了。

3、具體使用

怎麼使用呢?這要看框架支援怎樣的依賴注入了,主要有以下幾種方式:

3.1、建構函式注入

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController
{
    /**
     * TestController constructor.
     * 這裡$request變數已經是Request的例項了,不用顯示的new(也不能去new,Request建構函式也會依賴其他服務,這些依賴處理交給框架的服務容器去解析就好了)
     */
    public function __construct(Request $request)
    {
        dump($request->all());
    }
}

3.2、函式引數注入

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController
{
    /**.
     * 和建構函式注入同理
     */
    public function hello(Request $request)
    {
        dump('Hello'.$request->name);
    }
}

3.3、手動解析

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController
{
    public function test(Request $request)
    {
        // 1、解析服務,這裡取出來的redis物件是否為單例取決於哪種方式註冊
        $redis = app()->make('redis');
        $redis = app()->makeWith('redis', ['param' => 'test']);
        $redis = resolve('redis');
        $redis = app('redis');
        $redis = app()->get('redis');

        // 2、例項化類
        app(\App\Test::class, ['var1' => 'var1'])
        app(\App\Test::class, ['var2' => 'var2', 'var1' => 'var1'])
    }
}

例項化類為什麼不直接new呢?因為這樣例項化,不用處理Test的依賴Request

// 示例類
<?php
namespace App;

use Illuminate\Http\Request;

class Test
{
    public $var1;
    public $var2;

     /**
     * Test constructor. * @param $var1
      * @param $var2
      */
    public function __construct(Request $request, $var1, $var2 = 'var2')
    {  
        $this->var1 = $var1;
        $this->var2 = $var2;

        // 不用處理,交給容器解析
        dump($request->all());
    }
 }

4、總結

文章主要講了容器對外提供的繫結和解析服務的幾種方式,隨後講了服務提供者對容器的使用,也給了具體的例子,希望對大家有所幫助。文中只給出了一些基本的實現邏輯,關於上下文的處理,有興趣的可以結合社群文件繼續深挖。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章