Laravel核心程式碼學習 -- 服務容器(IocContainer)

kevinyan發表於2018-05-21

服務容器

Laravel的核心是IocContainer, 文件中稱其為“服務容器”,服務容器是一個用於管理類依賴和執行依賴注入的強大工具,Laravel中的功能模組比如 Route、Eloquent ORM、Request、Response等等等等,實際上都是與核心無關的類模組提供的,這些類從註冊到例項化,最終被我們所使用,其實都是 laravel 的服務容器負責的。

如果對服務容器是什麼沒有清晰概念的話推薦一篇博文來了解一下服務容器的來龍去脈:laravel神奇的服務容器

服務容器中有兩個概念控制反轉(IOC)和依賴注入(DI):

依賴注入和控制反轉是對同一件事情的不同描述,它們描述的角度不同。依賴注入是從應用程式的角度在描述,應用程式依賴容器建立並注入它所需要的外部資源。而控制反轉是從容器的角度在描述,容器控制應用程式,由容器反向的嚮應用程式注入應用程式所需要的外部資源。

在Laravel中框架把自帶的各種服務繫結到服務容器,我們也可以繫結自定義服務到容器。當應用程式需要使用某一個服務時,服務容器會講服務解析出來同時自動解決服務之間的依賴然後交給應用程式使用。

本篇就來探討一下Laravel中的服務繫結和解析是如何實現的

服務繫結

常用的繫結服務到容器的方法有instance, bind, singleton, alias。下面我們分別來看一下。

instance

將一個已存在的物件繫結到服務容器裡,隨後通過名稱解析該服務時,容器將總返回這個繫結的例項。

$api = new HelpSpot\API(new HttpClient);
$this->app->instance('HelpSpot\Api', $api);
複製程式碼

會把物件註冊到服務容器的$instnces屬性裡

[
     'HelpSpot\Api' => $api//$api是API類的物件,這裡簡寫了
 ]
複製程式碼

bind

繫結服務到服務容器

有三種繫結方式:

1.繫結自身
$this->app->bind('HelpSpot\API', null);

2.繫結閉包
$this->app->bind('HelpSpot\API', function () {
	return new HelpSpot\API();
});//閉包直接提供類實現方式
$this->app->bind('HelpSpot\API', function ($app) {
	return new HelpSpot\API($app->make('HttpClient'));
});//閉包返回需要依賴注入的類
3. 繫結介面和實現
$this->app->bind('Illuminate\Tests\Container\IContainerContractStub', 'Illuminate\Tests\Container\ContainerImplementationStub');
複製程式碼

針對第一種情況,其實在bind方法內部會在繫結服務之前通過getClosure()為服務生成閉包,我們來看一下bind方法原始碼。

public function bind($abstract, $concrete = null, $shared = false)
{
    $abstract = $this->normalize($abstract);
    
    $concrete = $this->normalize($concrete);
    //如果$abstract為陣列類似['Illuminate/ServiceName' => 'service_alias']
	//抽取別名"service_alias"並且註冊到$aliases[]中
	//注意:陣列繫結別名的方式在5.4中被移除,別名繫結請使用下面的alias方法
    if (is_array($abstract)) {
        list($abstract, $alias) = $this->extractAlias($abstract);

        $this->alias($abstract, $alias);
    }

    $this->dropStaleInstances($abstract);

    if (is_null($concrete)) {
        $concrete = $abstract;
    }
	//如果只提供$abstract,則在這裡為其生成concrete閉包
    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)
{
    // $c 就是$container,即服務容器,會在回撥時傳遞給這個變數
    return function ($c, $parameters = []) use ($abstract, $concrete) {
        $method = ($abstract == $concrete) ? 'build' : 'make';

        return $c->$method($concrete, $parameters);
    };
}
複製程式碼

bind把服務註冊到服務容器的$bindings屬性裡類似這樣:

$bindings = [
	'HelpSpot\API' =>  [//閉包繫結
		'concrete' => function ($app, $paramters = []) {
			return $app->build('HelpSpot\API');
		},
		'shared' => false//如果是singleton繫結,這個值為true
	]		
	'Illuminate\Tests\Container\IContainerContractStub' => [//介面實現繫結
		'concrete' => 'Illuminate\Tests\Container\ContainerImplementationStub',
		'shared' => false
	]
]
複製程式碼

singleton

public function singleton($abstract, $concrete = null)
{
    $this->bind($abstract, $concrete, true);
}
複製程式碼

singleton 方法是bind方法的變種,繫結一個只需要解析一次的類或介面到容器,然後接下來對於容器的呼叫該服務將會返回同一個例項

alias

把服務和服務別名註冊到容器:

public function alias($abstract, $alias)
{
    $this->aliases[$alias] = $this->normalize($abstract);
}
複製程式碼

alias 方法在上面講bind方法裡有用到過,它會把把服務別名和服務類的對應關係註冊到服務容器的$aliases屬性裡。 例如:

$this->app->alias('\Illuminate\ServiceName', 'service_alias');    
複製程式碼

繫結完服務後在使用時就可以通過

$this->app->make('service_alias');
複製程式碼

將服務物件解析出來,這樣make的時候就不用寫那些比較長的類名稱了,對make方法的使用體驗上有很大提升。

服務解析

make: 從服務容器中解析出服務物件,該方法接收你想要解析的類名或介面名作為引數

/**
 * Resolve the given type from the container.
 *
 * @param  string  $abstract
 * @param  array   $parameters
 * @return mixed
 */
public function make($abstract, array $parameters = [])
{
	//getAlias方法會假定$abstract是繫結的別名,從$aliases找到對映的真實型別名
	//如果沒有對映則$abstract即為真實型別名,將$abstract原樣返回
    $abstract = $this->getAlias($this->normalize($abstract));

    // 如果服務是通過instance()方式繫結的,就直接解析返回繫結的service
    if (isset($this->instances[$abstract])) {
        return $this->instances[$abstract];
    }

    // 獲取$abstract介面對應的$concrete(介面的實現)
    $concrete = $this->getConcrete($abstract);

    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete, $parameters);
    } else {
        //如果時介面實現這種繫結方式,通過介面拿到實現後需要再make一次才能
        //滿足isBuildable的條件 ($abstract === $concrete)
        $object = $this->make($concrete, $parameters);
    }

    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

	//如果服務是以singleton方式註冊進來的則,把構建好的服務物件放到$instances裡,
	//避免下次使用時重新構建
    if ($this->isShared($abstract)) {
        $this->instances[$abstract] = $object;
    }

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

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

    return $object;
}

protected function getConcrete($abstract)
{
    if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
        return $concrete;
    }

    // 如果是$abstract之前沒有註冊類實現到服務容器裡,則服務容器會認為$abstract本身就是介面的類實現
    if (! isset($this->bindings[$abstract])) {
        return $abstract;
    }

    return $this->bindings[$abstract]['concrete'];
}

protected function isBuildable($concrete, $abstract)
{        
    return $concrete === $abstract || $concrete instanceof Closure;
}
複製程式碼

通過對make方法的梳理我們發現,build方法的職能是構建解析出來的服務的物件的,下面看一下構建物件的具體流程。(構建過程中用到了PHP類的反射來實現服務的依賴注入)

public function build($concrete, array $parameters = [])
{
    // 如果是閉包直接執行閉包並返回(對應閉包繫結)
    if ($concrete instanceof Closure) {
        return $concrete($this, $parameters);
    }
    
    // 使用反射ReflectionClass來對實現類進行反向工程
    $reflector = new ReflectionClass($concrete);

    // 如果不能例項化,這應該是介面或抽象類,再或者就是建構函式是private的
    if (! $reflector->isInstantiable()) {
        if (! empty($this->buildStack)) {
            $previous = implode(', ', $this->buildStack);

            $message = "Target [$concrete] is not instantiable while building [$previous].";
        } else {
            $message = "Target [$concrete] is not instantiable.";
        }

        throw new BindingResolutionException($message);
    }

    $this->buildStack[] = $concrete;

    // 獲取建構函式
    $constructor = $reflector->getConstructor();

    // 如果建構函式是空,說明沒有任何依賴,直接new返回
    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }
    
    // 獲取建構函式的依賴(形參),返回一組ReflectionParameter物件組成的陣列表示每一個引數
    $dependencies = $constructor->getParameters();

    $parameters = $this->keyParametersByArgument(
        $dependencies, $parameters
    );

    // 構建建構函式需要的依賴
    $instances = $this->getDependencies(
        $dependencies, $parameters
    );

    array_pop($this->buildStack);

    return $reflector->newInstanceArgs($instances);
}

//獲取依賴
protected function getDependencies(array $parameters, array $primitives = [])
{
    $dependencies = [];

    foreach ($parameters as $parameter) {
        $dependency = $parameter->getClass();

        // 某一依賴值在$primitives中(即build方法的$parameters引數)已提供
        // $parameter->name返回引數名
        if (array_key_exists($parameter->name, $primitives)) {
            $dependencies[] = $primitives[$parameter->name];
        } 
        elseif (is_null($dependency)) {
             // 引數的ReflectionClass為null,說明是基本型別,如'int','string'
            $dependencies[] = $this->resolveNonClass($parameter);
        } else {
        	 // 引數是一個類的物件, 則用resolveClass去把物件解析出來
            $dependencies[] = $this->resolveClass($parameter);
        }
    }

    return $dependencies;
}

//解析出依賴類的物件
protected function resolveClass(ReflectionParameter $parameter)
{
    try {
        // $parameter->getClass()->name返回的是類名(引數在typehint裡宣告的型別)
        // 然後遞迴繼續make(在make時發現依賴類還有其他依賴,那麼會繼續make依賴的依賴
        // 直到所有依賴都被解決了build才結束)
        return $this->make($parameter->getClass()->name);
    } catch (BindingResolutionException $e) {
        if ($parameter->isOptional()) {
            return $parameter->getDefaultValue();
        }

        throw $e;
    }
}
複製程式碼

服務容器就是laravel的核心, 它通過依賴注入很好的替我們解決物件之間的相互依賴關係,而又通過控制反轉讓外部來來定義具體的行為(Route, Eloquent這些都是外部模組,它們自己定義了行為規範,這些類從註冊到例項化給你使用才是服務容器負責的)。

一個類要被容器所能夠提取,必須要先註冊至這個容器。既然 laravel 稱這個容器叫做服務容器,那麼我們需要某個服務,就得先註冊、繫結這個服務到容器,那麼提供服務並繫結服務至容器的東西就是服務提供器(ServiceProvider)。服務提供者主要分為兩個部分:register(註冊) 和 boot(引導、初始化)這就引出了我們後面要學習的內容。

本文已經收錄在系列文章Laravel原始碼學習裡,歡迎訪問閱讀。

相關文章