「Laravel 服務容器」自己的理解

ligkwww發表於2019-11-05

深入理解laravel服務容器

簡介

服務容器的核心是管理類依賴和執行依賴注入的工具,
是laravel中最核心的概念之一,充分理解有助於我們更好的使用框架進行開發,這篇文章將深入剖析原始碼解開容器的神祕面紗。

準備

  • 依賴注入

    依賴注入即是將類中所需要用到的依賴,通過外部注入到類中進行例項化的過程。

    通俗的講,以往我們在類A中例項化另外一個類B的過程,轉移到類外進行,從而將已經例項化的物件B當做引數傳遞至A中,此過程就完成了依賴注入。

  • 控制反轉

    控制反轉即是將類中所需依賴的控制權從類內部轉移至外部,由外部決定依賴的物件,他們之間的關係更像是:控制反轉是「目的」,而依賴注入是「方法」,通過依賴注入的方式實現了控制反轉的目的。

  • 依賴注入容器

    往往我們在使用依賴注入時,並不會單獨的對依賴進行例項化, 而是通過一個專門負責管理例項化類的模組來進行,這就是依賴注入容器也就是我們本章的主題-服務容器,而在使用容器時我們不需要一個一個的使用關鍵字new去例項化所需的依賴,一切都交給容器來自動進行,這正是「laravel服務容器」的強大之處。

圖解

image

原始碼分析

1.入口檔案

index.php:

require __DIR__.'/../vendor/autoload.php';

//這裡就是核心,載入服務容器
$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

2.容器初始化

bootstrap/app.php:

//註冊容器
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

//下面幾個就是載入框架基礎服務
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

3.註冊容器

3.1構造方法裡主要註冊了容器啟動的一些和使用者自定義以及業務無關的基礎服務

Application.php:

public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }

    //註冊基礎繫結例項
    $this->registerBaseBindings();

    //註冊服務提供者
    $this->registerBaseServiceProviders();

    //註冊別名,這個方法太長就不貼出來了,邏輯相對簡單大家一看應該就瞭解
    $this->registerCoreContainerAliases();
}

//主要是將容器本身註冊,這樣就方便我們在其他服務提供者中使用容器物件
protected function registerBaseBindings()
{
    static::setInstance($this);

    $this->instance('app', $this);

    $this->instance(Container::class, $this);

    $this->instance(PackageManifest::class, new PackageManifest(
        new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
    ));
}

//這裡主要註冊一些基礎的服務提供者:事件處理、日誌、路由相關
protected function registerBaseServiceProviders()
{
    $this->register(new EventServiceProvider($this));

    $this->register(new LogServiceProvider($this));

    $this->register(new RoutingServiceProvider($this));
}
3.2下面我們來著重看下register方法,看他是如何將服務註冊進容器中的。

Application.php:

//第一個引數provider 是想要註冊的服務,第二個引數是註冊服務需要用到的引數, 第三個參數列示是否每次都重新註冊
public function register($provider, $options = [], $force = false)
{
    //這裡校驗如果服務提供已經註冊,就不重新註冊了
    if (($registered = $this->getProvider($provider)) && ! $force) {
        return $registered;
    }

    if (is_string($provider)) {
        $provider = $this->resolveProvider($provider);
    }

    //這裡是重點, 如果給定的類中存在register方法,那麼就呼叫他,框架初始化中也是使用這種方法。
    if (method_exists($provider, 'register')) {
        $provider->register();
    }

    //如果服務提供者中有bindings屬性,則將bindings中的物件一一註冊,例如:protected $bindings = [Cache::class => function($app){ return new Cache();}];
    if (property_exists($provider, 'bindings')) {
        foreach ($provider->bindings as $key => $value) {
            $this->bind($key, $value);
        }
    }

    //和bind方法一樣,只不過這裡是單例
    if (property_exists($provider, 'singletons')) {
        foreach ($provider->singletons as $key => $value) {
            $this->singleton($key, $value);
        }
    }

    //對服務進行標記,下次就不再重新註冊了
    $this->markAsRegistered($provider);

    //如果服務已經註冊,則重新執行服務的boot方法
    if ($this->booted) {
        $this->bootProvider($provider);
    }

    return $provider;
}

//註冊服務至容器中,這裡說明一下,單例也是呼叫這個方法,只不過shared引數傳true
public function bind($abstract, $concrete = null, $shared = false)
{

    //刪除已繫結的例項,否則讀取時會優先讀取instances屬性,那樣的話這裡的繫結就始終不會讀取了
    $this->dropStaleInstances($abstract);

    if (is_null($concrete)) {
        $concrete = $abstract;
    }

    //如果傳來的實現不是一個閉包,則為其生成一個閉包
    if (! $concrete instanceof Closure) {
        $concrete = $this->getClosure($abstract, $concrete);
    }

    //將服務的名稱與實現繫結至容器中
    $this->bindings[$abstract] = compact('concrete', 'shared');

    if ($this->resolved($abstract)) {
        $this->rebound($abstract);
    }
}

//為例項生成一個閉包
protected function getClosure($abstract, $concrete)
{
    //這裡的意思是如果抽象名稱和實現一樣的話,則利用反射解析具體實現類
    //如果不一樣的話則例項化傳來的實現類,這裡如果$concrete是一個介面的話,需要提前註冊至容器中,否則無法例項化。
    return function ($container, $parameters = []) use ($abstract, $concrete) {
        if ($abstract == $concrete) {
            return $container->build($concrete);
        }

        return $container->make($concrete, $parameters);
    };
}
3.3這裡我們僅拿出部分Provider進行展示一下
class EventServiceProvider extends ServiceProvider
{

    //可以看到這個類中只有一個register方法, 作用就是註冊events服務
    public function register()
    {
        $this->app->singleton('events', function ($app) {
            return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
                return $app->make(QueueFactoryContract::class);
            });
        });
    }
}

class RoutingServiceProvider extends ServiceProvider
{

    //register方法中提供了一系列的註冊服務
    public function register()
    {
        $this->registerRouter();
        $this->registerUrlGenerator();
        $this->registerRedirector();
        $this->registerPsrRequest();
        $this->registerPsrResponse();
        $this->registerResponseFactory();
        $this->registerControllerDispatcher();
    }

    /**
     * Register the router instance.
     *
     * @return void
     */
    protected function registerRouter()
    {
        $this->app->singleton('router', function ($app) {
            return new Router($app['events'], $app);
        });
    }

}

這裡需要說明一下,大部分的provider方法裡只會有register和boot方法,框架執行的順序是先執行所有服務提供者的register方法,然後再依次執行boot方法,所以這也就為什麼框架中建議我們在register方法中不要調取其他服務的原因。

3.4待容器初始化完成後,註冊幾個我們應用的基礎服務

bootstrap/app.php:

//處理我們Http請求
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

//處理控制檯請求的
$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

//處理debug的
$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

至此容器的註冊繫結已經完成(其中各服務內部處理沒有講解,只關注重點部分),下面就是容器的解析(make)部分

4.容器使用

4.1容器的使用只有幾行程式碼,內部處理了我們所有的http請求。

index.php:

//入口檔案中只make了一個Illuminate\Contracts\Http\Kernel::class服務, 還記得我們是在哪裡註冊他的嗎?
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

//這裡就是用我們解析出來的物件去處理http請求了,暫時不深入去看他了。
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
4.2下面我們著重看一下make方法

Container.php:


//make方法底層最終呼叫Container類中的resolve方法,中間做了下別名相關的處理,這裡就不一層一層展示了,我們直接看底層方法。
protected function resolve($abstract, $parameters = [])
{
    //獲取要解析的服務別名
    $abstract = $this->getAlias($abstract);

    //是否需要上下文引數繫結。
    //這裡給我感覺好像是將傳來的引數設定至容器的上下文引數中,等後續例項化需要的時候直接拿來用。
    //這個解釋不一定對,對這裡理解的不夠深入,有了解這塊的同學可以分享一下
    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    //如果此服務已經繫結至例項中,就直接拿來用了。
    //注意instances裡存的都是已經例項化過的物件,可以直接使用,這也就是為什麼在bind方法中要先刪除例項的原因。
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    //將傳來的引數儲存起來,在build方法中返回閉包時再傳入
    $this->with[] = $parameters;

    //獲取此服務的具體實現
    $concrete = $this->getConcrete($abstract);

    //這裡就是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);
    }

    //這裡如果此服務是單例,則將解析出來的物件存至instances屬性中,下次再make時就直接使用了。
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }

    $this->fireResolvingCallbacks($abstract, $object);

    $this->resolved[$abstract] = true;

    array_pop($this->with);

    //返回實現物件
    return $object;
}

//解析實現,最終返回例項化的物件
public function build($concrete)
{
    //如果實現本身就是一個閉包,則直接返回
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    //例項化一個反射物件
    $reflector = new ReflectionClass($concrete);

    //如果類無法例項化,丟擲異常(比如實現是一個介面類)
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $this->buildStack[] = $concrete;

    //獲取構造方法
    $constructor = $reflector->getConstructor();

    //如果類中沒有構造方法,則直接例項化後返回物件。
    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);
}

//迴圈解析依賴
protected function resolveDependencies(array $dependencies)
{
    $results = [];

    foreach ($dependencies as $dependency) {
        //沒太明白這裡是幹啥用的,但好像絕大部分都沒有走到這裡,有了解的同學可以分享一下。
        if ($this->hasParameterOverride($dependency)) {
            $results[] = $this->getParameterOverride($dependency);

            continue;
        }

        //判斷如果引數是一個類,則解析類
        //如果不是類,則從上下文引數中獲取對應引數或者使用預設值。(如果上下文和預設值都沒有,就丟擲異常,無法例項化)
        $results[] = is_null($dependency->getClass())
                        ? $this->resolvePrimitive($dependency)
                        : $this->resolveClass($dependency);
    }

    return $results;
}

//解析依賴類
protected function resolveClass(ReflectionParameter $parameter)
{
    try {
        //可以看到這裡其實最終呼叫的是容器中的make方法來獲取某個類的具體實現
        //也就是說依靠反射來解析的類依賴,需要提前繫結至容器中,否則就無法例項化。
        return $this->make($parameter->getClass()->name);
    }

    catch (BindingResolutionException $e) {
        //如果是可選的就返回預設值
        if ($parameter->isOptional()) {
            return $parameter->getDefaultValue();
        }

        throw $e;
    }
}

至此,laravel容器的註冊與獲取已經分析完成,其實容器本身不難理解,只不過laravel中做了一些其他的處理,例如:別名、例項、單例、反射等,就是為了提升效能以及方便我們使用,所以大家可以深入瞭解一下。

我在這個過程中基本只關注了容器本身的實現流程,對服務中定義實現程式碼沒有太過深入閱讀,有對這方面感興趣的同學,可以閱讀官方出的「laravel生命週期」等系列文章。

網上雖然已經有很多類似的文章,但閱讀原始碼後還是儘可能的輸出一下,方便自己加深理解的同事也有可能幫助到其他人,其中對流程中的關鍵程式碼註解了一些自己的理解,可能不一定正確,如果有不對的地方歡迎大家指出。

相關文章