Laravel 服務容器 源碼閱讀

概念

生活中,容器就是一個存放物品的器具。平時可以把雜亂的東西都放到容器中,要用的時候再去容器中拿。
類似的,Laravel 的服務容器也像一個存放服務的類,通過把要使用的服務綁定到容器中,然后在需要使用的時候從容器中獲取(它還可以通過反射自動創建服務依賴的服務)。
Laravel 框架中所有的服務都是通過容器來創建與獲取的,這樣使得所有的服務都依賴容器而不相互依賴,避免服務之間的過度耦合,讓后面服務的變化變得簡單。

使用

容器主要包括兩種方法:綁定獲取。將需要的服務綁定到容器中,需要的時候再從容器中獲取。

服務綁定

下面是容器提供綁定的主要方法:
$app 為容器的實例。

  1. 簡單綁定
    bind 方法。綁定的內容可以是閉包函數,類名字符串。
// 綁定閉包
$app->bind('redis.connection', function ($app) {
      return $app['redis']->connection();
});
// 綁定類名
$app->bind('database', MySQLDatabase::class);
  1. 單例綁定
    singleton 方法。綁定的內容可以是閉包函數,類名字符串。
$app->singleton('redis', function ($app) {
    // ...
    return new RedisManager($driver, $config);
});

單例綁定和普通綁定的 區別 是在后面獲取綁定內容時,單例綁定的會一直返回同一個對象,而普通綁定的每次獲取都會得到一個重新實例化的對象。

  1. 綁定實例
    instance 方法。綁定的內容是現有的對象。
$this->app->instance('HelpSpot\API', new HelpSpot\API(new HttpClient));
  1. 綁定初始化數據
    容器可以通過反射,在初始化的時候獲得需要注入的對象,但如果要指定初始化的具體值,可以在綁定的時候單獨設置
<?php
// 如果 UserConstroller 類的構造方法如下
public function __construct($variableName) {
  // 其他操作 ... 
}

// 綁定初始化的數據如下
$app->when('App\Http\Controllers\UserController')
        ->needs('$variableName')
        ->give($value);
  1. 綁定接口到實現
    將接口的一個實現綁定到接口,當從容器獲取接口的實例時,會返回綁定的實現。這樣就可以實現面線接口編程。
    bindsingle 方法都可以,與上面的區別是第一個參數是接口名稱。
// 項目開始使用極光推送服務
$app->bind(
    'App\Contracts\Pusher',
    'App\Services\JPusher'
);
// 開始推送
$app->make('App\Contracts\Pusher')->push($message);

// 后面項目改為通過信鴿推送,我們只需要修改綁定的地方,實際推送地方不需要任何修改
$app->bind(
    'App\Contracts\Pusher',
    'App\Services\XgPusher'
);
  1. 上下文綁定
    有時候可能兩個不同的類使用了相同的接口,但是它們需要使用不同的實現類,可以通過下面方法對指定類綁定指定的實現類。
// 在 PhotoController 類中使用 Filesystem 接口時,提供 Storage::disk('local') 的實現
$app->when(PhotoController::class)
    ->needs(Filesystem::class)
    ->give(function () {
        return Storage::disk('local');
});

服務獲取

服務獲取指從容器中獲取服務的方法。

  1. 直接 makemakeWith 獲取對象
    容器提供 $app->make($abstract) 方法獲取指定字符串或接口對應的實現。
    makeWith() 方法可以在指定對象實例化的參數。
  2. 全局的 app() 函數
    其實 app($abstract = null, array $parameters = []) 函數也是調用容器的 make 方法去獲得對象
app($abstract);
  1. 通過容器的屬性獲取對象
    因為 Laravel 容器實現了 ArrayAccess 接口,當獲取容器沒有定義的屬性時會調用 ArrayAccess 接口的 offsetGet 方法,而 offsetGet 方法會調用容器的 make 方法來獲取對象。
// 獲取 $abstract 對應的對象
$app->$abstract;
  1. 通過依賴注入
    注意: 只有由容器創建類或調用方法時,容器會通過反射去自動注入;如果是手動 new 去創建對象,是不會自動依賴注入的。
class AuthController extends Controller
{
    // login 是控制器的一個方法,框架路由到這個方法時,
    // 會根據 $request 接口類型,自動實例化 $request 對象
    public function login(Request $request) {
        // 這里的參數 $request 如果不是 login 的入參,需要由用戶手動傳入
        $this->validateLogin($request);
        // ... 
    }
    
    protected function validateLogin(Request $request) {
        // ... 
    }
}

源碼閱讀

下圖是容器主要方法的關系圖。

container.png

Laravel 在程序入口文件 bootstrap/app 中初始化 Illuminate\Foundation\Application 容器類,然后通過服務提供者將所需要的服務注冊到容器中,以供后面使用。
下面主要分析 bind 方法(綁定)和 make 方法(獲取)。

bind 方法

<?php
   /**
     * 在容器中添加一個綁定。
     *
     * @param  string|array  $abstract              綁定的名稱
     * @param  \Closure|string|null  $concrete      綁定的內容
     * @param  bool  $shared                        綁定內容是否在容器中共享(單例綁定就是設置的共享)
     * @return void
     */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        // 刪除之前綁定名稱為 $abstract 的實例和別名
        $this->dropStaleInstances($abstract);

        // 如果 $concrete 為空,則將 $concrete 注冊為類名為 $abstract 的類。
        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        // 如果 $concrete 是一個類名字符串,而不是閉包。需要將它包裝成為一個閉包,
        // 方便后面生成綁定實例時的擴展
        if (! $concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }
        // 將 concrete、shared 的內容保存到 $this->bindings 數組中
        $this->bindings[$abstract] = compact('concrete', 'shared');

        // 判斷 綁定的名稱 在容器中是否已經被實例化過
        if ($this->resolved($abstract)) {
            // 將 $abstract 名稱綁定的對象重新實例化一次。
            // 重新實例化后,如果 $abstract 有注冊重新實例化通知的回調,會出發重新實例化的回調
            $this->rebound($abstract);
        }
    }

    /**
     * 對通過綁定名稱獲取對象的方法包裝為一個閉包 Get the Closure to be used when building a type.
     *
     * @param  string  $abstract    綁定名稱
     * @param  string  $concrete    綁定內容
     * @return \Closure
     */
    protected function getClosure($abstract, $concrete)
    {
        // 返回一個匿名函數,入參為容器對象
        return function ($container, $parameters = []) use ($abstract, $concrete) {
            // 如果綁定名稱和綁定內容相同,直接通過反射獲取 綁定內容的對象
            if ($abstract == $concrete) {
                return $container->build($concrete);
            }
            // 通過 make 方法獲取綁定名稱為 $concrete 的綁定內容
            return $container->makeWith($concrete, $parameters);
        };
    }

這里以 Laravel 中 bootstrap/app.php 中的內容為例:

<?php
//將類 'App\Http\Kernel' 綁定到接口 'Illuminate\Contracts\Http\Kernel' 上
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

綁定后容器中的內容如下:

bind.png

容器 $app 中的 bindings 數組對應的鍵 Illuminate\Contracts\Http\Hernel 為綁定的名稱;綁定內容為數組,包含了兩個屬性:

  • concrete 綁定的內容。這里是一個閉包,第一個參數為 this 容器本身,第二個參數為傳入的初始化參數。
  • shared 是否為共享。 因為這里是單例綁定,所以是 true

綁定完成后,繼續看 Laravel 是如何從容器中獲取對象的。

make 方法

Illuminate\Foundation\Application 文件中的 make 方法:

<?php
/**
 * 從容器中解析給定類名,得到對應的對象實例
 *
 * (重寫了 Container 中的 make 方法)
 *
 * @param  string  $abstract  給定類名字符串
 * @return mixed
 */
public function make($abstract)
{
   // 獲取 $abstract 類名的別名。
   $abstract = $this->getAlias($abstract);
  // 如果 $abstract 是延遲服務中定義的服務,則加載服務(讓服務在需要的時候再加載)
   if (isset($this->deferredServices[$abstract])) {
        $this->loadDeferredProvider($abstract);
    }
   // 調用 Container 中的 make 方法獲取 $abstract  類名對應的對象
   return parent::make($abstract);
}

從綁定中容器中解析、并獲得給定類名對象的實現主要是在 Illuminate\Container\Container 文件:

<?php
// 這里實際是調用 resolve 方法生成、獲取給定類型的實例
public function make($abstract)
{
    return $this->resolve($abstract);
}

protected function resolve($abstract, $parameters = [])
{
    // 獲取類名的別名。如果別名存在,會去解析容器中別名對應的類
    $abstract = $this->getAlias($abstract);

    // 非主要部分,略過...

    // 如果類名已經綁定到實例,且類名沒有設置上下文綁定,直接返回實例。
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
       return $this->instances[$abstract];
   }
   // 保存參數內容,
   $this->with[] = $parameters;
   // 從容器中獲取 $abstract 類綁定的內容;如果沒有綁定過,直接返回 $abstract 類名
   $concrete = $this->getConcrete($abstract);

   // 如果綁定的內容可以實例化,調用 build 方法實例化;否則地歸去容器解析。
   // 可實例化的條件:
   // 1. 類名與綁定內容一致
   // 2. 綁定的內容是一個閉包
   if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
       $object = $this->make($concrete);
    }

    // ...
    return $object;
}

// 構造綁定的內容
public function build($concrete)
{
    // 如果綁定的內容 $concrete 是一個閉包,直接傳入參數,調用閉包
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }
    
    // 通過反射獲取 $concrete 反射類
    $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);
}

build 方法主要使用 PHP 的反射獲取類名的實例,反射知識參考Laravel 源碼學習 基礎知識

以 Laravel public/index.php 中的內容為例:

<?php
// 獲取接口名稱 Illuminate\Contracts\Http\Kernel 的實例
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

由上面綁定部分可知,Illuminate\Contracts\Http\Kernel::class 在容器中實際綁定的內容是 App\Http\Kernel::class 類實例化的閉包。

before_build.png

在調用 build 方法之前 $concrete 是下面的閉包函數:

<?php
// 此時 $abstract 為 `Illuminate\Contracts\Http\Kernel::class`, 
// $concrete 為 `App\Http\Kernel::class`,
// $app 為容器 $container 。
function ($container, $parameters = []) use ($abstract, $concrete) {
    // 如果綁定名稱和綁定內容相同,直接通過反射獲取 綁定內容的對象
    if ($abstract == $concrete) {
        return $container->build($concrete);
    }
    // 通過 make 方法獲取綁定名稱為 $concrete 的綁定內容
    return $container->makeWith($concrete, $parameters);
};

調用 build 方法后,$kernel 得到的內容是:

<?php
$kernel = $app->makeWith(App\Http\Kernel::class);
// 因為 App\Http\Kernal::class 沒有別名,通過 resolve 函數后實際為
$kernal = $app->build(App\Http\Kernel::class);

最后 Illuminate\Container\Container 容器中的 build 方法獲取 App\Http\Kernel::class 的實例。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。