make解析
首先歡迎關注我的部落格: www.learnku.com/blog/leoyang
本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...
服務容器對物件的自動解析是服務容器的核心功能,make 函式、build 函式是例項化物件重要的核心,先大致看一下程式碼:
public function make($abstract)
{
$abstract = $this->getAlias($abstract);
if (isset($this->deferredServices[$abstract])) {
$this->loadDeferredProvider($abstract);
}
return parent::make($abstract);
}
public function make($abstract)
{
$needsContextualBuild = ! is_null(
$this->getContextualConcrete($abstract = $this->getAlias($abstract))
);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$concrete = $this->getConcrete($abstract);
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
return $object;
}
在講解解析流程之前,我們先說說使用make函式進行解析的分類:
我們詳細的講一下上圖。這裡我把使用make函式進行解析的情況分為大致兩種:
- 解析物件沒有繫結過任何類,例如:
$app->make('App\Http\Controllers\HomeController');
- 解析物件繫結過實現類
對於繫結過實現類的物件可以分為兩種:
- 繫結的是類名,例如:
$app->when('App\Http\Controllers\HomeController')
->needs('App\Http\Requests\InRequest')
->give('App\Http\Requests\TestsRequest');
- 繫結的是閉包
對於繫結的是閉包的又可以分為:
- 使用者繫結閉包,例如:
$app->singleton('auth',function($app){
return new AuthManager($app)
});//物件類直接實現方法
$app->singleton(EloquentFactory::class, function ($app) {
return EloquentFactory::construct(
$app->make(FakerGenerator::class), database_path('factories')
);//物件類依賴注入
});
- 服務容器外包一層閉包函式(make/build),例如:
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);//包裝make
$app->singleton(
App\ConSole\Kernel::class,
);//包裝build
我們在這裡先大致講一下服務容器解析的流程,值得注意的是其中 build 函式有可能會遞迴呼叫 make:
- 獲取服務名稱。
- 載入延遲服務。判斷當前的介面是否是延遲服務提供者,若是延遲服務提供者,那麼還要對服務提供者進行註冊與啟動操作。
- 解析單例。如果介面服務是已經被解析過的單例物件,而且並非上下文繫結,那麼直接取出物件。
- 獲取註冊的實現。實現方式可能是上下文繫結的,也可能是 binding 陣列中的閉包,也有可能就是介面本身。
- build 解析。首先判斷是否需要遞迴。是,則遞迴 make;否,則呼叫 build 函式;直到呼叫 build 為止
- 執行擴充套件。若當前解析物件存在擴充套件,執行擴充套件函式。
- 創造單例物件。若 shared 為真,且不存在上下文繫結,則放入單例陣列中
- 回撥
- 標誌解析
下面我們開始詳細分解程式碼邏輯。由於 getAlias 函式已經在 上一篇 講過,這裡不會再說。而loadDeferredProvider 函式作用是載入延遲服務,與容器解析關係不大,我們放在以後再說。
獲取註冊的實現
獲取解析類的真正實現,函式優先去獲取上下文繫結的實現,否則獲取 binding 陣列中的實現,獲取不到就是直接返回自己作為實現:
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
一般來說,上下文繫結的服務是透過依賴注入來實現的:
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
class PhotoController{
protected $file;
public function __construct(Filesystem $file){
$this->file = $file;
}
}
服務容器會在解析 PhotoController 的時候,透過放射獲取引數型別 Filesystem,並且會把 Filesystem 自動解析為 Storage::disk('local')。如何實現的呢?首先,從 上一篇 文章我們知道,當進行上下文繫結的時候,實際上是維護 contextual 陣列,透過上下文繫結,這個陣列中存在:
contextual[PhotoController][Filesystem] = function () { return Storage::disk('local'); }
若是服務容器試圖構造 PhotoController 類,那麼由於其建構函式依賴於 Filesystem,所以容器必須先生成 Filesystem 類,然後再注入到 PhotoController 中。
在構造 Filesystem 之前,服務容器會先把 PhotoController 放入 buildStack 中,繼而再去解析 Filesystem。
解析 Filesystem 時,執行 getContextualConcrete 函式:
protected function getContextualConcrete($abstract)
{
if (! is_null($binding = $this->findInContextualBindings($abstract))) {
return $binding;
}
if (empty($this->abstractAliases[$abstract])) {
return;
}
foreach ($this->abstractAliases[$abstract] as $alias) {
if (! is_null($binding = $this->findInContextualBindings($alias))) {
return $binding;
}
}
}
protected function findInContextualBindings($abstract)
{
if (isset($this->contextual[end($this->buildStack)][$abstract])) {
return $this->contextual[end($this->buildStack)][$abstract];
}
}
從上面可以看出,getContextualConcrete 函式把當前解析的類(Filesystem)作為 abstract,buildStack 最後一個類(PhotoController)作為 concrete,尋找 this->contextual[concrete] [abstract] (contextual[PhotoController] [Filesystem])中的值,在這個例子裡面這個陣列值就是那個匿名函式。
build 解析
對於服務容器來說,繫結是可以遞迴的,例如:
$app->bind('a','b');
$app->bind('b','c');
$app->bind('c',function(){
return new C;
})
遇到這樣的情況,bind 繫結中 getClosure 函式開始發揮作用,該函式會給類包一層閉包,閉包內呼叫 make 函式,服務容器會不斷遞迴呼叫 make 函式,直到最後一層,也就是繫結 c 的匿名函式。但是另一方面,有一些繫結方式並沒有呼叫 bind 函式,例如上下文繫結 context:
$this->app->when(E::class)
->needs(F::class)
->give(A::class);
當make(E::class)的時候,getConcrete 返回 A 類,而不是呼叫 make 函式的閉包,所以並不會啟動遞迴流程得到 C 的匿名函式,所以造成 A 類完全無法解析,isBuildable 函式就是解決這種問題的,當發現需要解析構造的物件很有可能是遞迴的,那麼就遞迴呼叫 make 函式,否則才會呼叫build。
...
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
...
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}
執行擴充套件
獲取擴充套件閉包,並執行擴充套件函式:
protected function getExtenders($abstract)
{
$abstract = $this->getAlias($abstract);
if (isset($this->extenders[$abstract])) {
return $this->extenders[$abstract];
}
return [];
}
回撥
先後啟動全域性的解析事件回撥函式,再啟動針對型別的事件回撥函式:
protected function fireResolvingCallbacks($abstract, $object)
{
$this->fireCallbackArray($object, $this->globalResolvingCallbacks);
$this->fireCallbackArray(
$object, $this->getCallbacksForType($abstract, $object, $this->resolvingCallbacks)
);
$this->fireAfterResolvingCallbacks($abstract, $object);
}
protected function getCallbacksForType($abstract, $object, array $callbacksPerType)
{
$results = [];
foreach ($callbacksPerType as $type => $callbacks) {
if ($type === $abstract || $object instanceof $type) {
$results = array_merge($results, $callbacks);
}
}
return $results;
}
protected function fireAfterResolvingCallbacks($abstract, $object)
{
$this->fireCallbackArray($object, $this->globalAfterResolvingCallbacks);
$this->fireCallbackArray(
$object, $this->getCallbacksForType($abstract, $object, $this->afterResolvingCallbacks)
);
build 解析
make 函式承擔瞭解析的大致框架,build 主要的職責就是利用反射將類構造出來,先看看主要程式碼:
public function build($concrete)
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) {
return $concrete($this);
}
$reflector = new ReflectionClass($concrete);
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface of Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
我們下面詳細的說一下各個部分:
閉包函式執行
if ($concrete instanceof Closure) {
return $concrete($this);
}
這段程式碼很簡單,但是作用很大。前面說過閉包函式有很多種類:
- 使用者繫結時提供的直接提供實現類的方式:
$app->singleton('auth',function($app){
return new AuthManager($app)
});//物件類直接實現方法
這種情況 concrete(this) 直接就可以解析構造出具體實現類,服務容器解析完畢。
- 使用者繫結時提供的帶有依賴注入的實現:
$app->singleton(EloquentFactory::class, function ($app) {
return EloquentFactory::construct(
$app->make(FakerGenerator::class), database_path('factories')
);//物件類依賴注入
這種情況下,concrete(this) 會轉而去解析 FakerGenerator::class,遞迴呼叫 make 函式。
- bind函式使用 getClosure 包裝而來:
function($container, $parameters = []){
method = make/build;
return $container->$method($concrete, $parameters);
}
這種情況,concrete(this) 將會繼續遞迴呼叫 make 或者 build。
反射
當 build 的引數是類名而不是閉包的時候,就要利用反射構建類物件,如果構建的類物件不需要依賴任何其他引數,那麼:
$reflector = new ReflectionClass($concrete);
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
return new $concrete;
}
如果需要依賴注入,那麼就要用反射機制來獲取 __construct 函式所需要注入的依賴,如果依賴是類對像,那麼遞迴呼叫 make 函式,如果依賴是變數值,那麼就從上下文中或者引數預設值中去獲取:
...
$dependencies = $constructor->getParameters();
$instances = $this->resolveDependencies($dependencies);
...
protected function resolveDependencies(array $dependencies)
{
$results = [];
foreach ($dependencies as $dependency) {
$results[] = is_null($class = $dependency->getClass())
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
}
return $results;
}
解析變數值引數,如果變數值在上下文繫結中設定過,則去取上下文繫結的值,否則透過反射去取引數預設值,如果沒有預設值,那麼就要終止報錯:
protected function resolvePrimitive(ReflectionParameter $parameter)
{
if (! is_null($concrete = $this->getContextualConcrete('$'.$parameter->name))) {
return $concrete instanceof Closure ? $concrete($this) : $concrete;
}
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
$this->unresolvablePrimitive($parameter);
}
解析類引數,利用服務容器進行依賴注入:
protected function resolveClass(ReflectionParameter $parameter)
{
try {
return $this->make($parameter->getClass()->name);
}
catch (BindingResolutionException $e) {
if ($parameter->isOptional()) {
return $parameter->getDefaultValue();
}
throw $e;
}
}
buildstack 解析棧
值的注意的是服務容器裡面有個 buildStack,每次利用反射對引數進行依賴注入的時候,都要向這個陣列中壓入當前的解析物件,前面說過這部分是為了上下文繫結而設計的:
...
$this->buildStack[] = $concrete;//壓入陣列棧中
...
$instances = $this->resolveDependencies($dependencies);//解析依賴注入的引數
array_pop($this->buildStack);//彈出陣列棧
...
解析標籤
使用標籤繫結的類,將會使用 tagged 來解析:
public function tagged($tag)
{
$results = [];
if (isset($this->tags[$tag])) {
foreach ($this->tags[$tag] as $abstract) {
$results[] = $this->make($abstract);
}
}
return $results;
}
call方法注入
服務容器中,我們直接使用或者間接的使用 make 來構造服務物件,但是在實際的應用場景中,會有這樣的需求:我們擁有一個物件或者閉包函式,想要呼叫它的一個函式,但是它函式里面卻有其他類的引數,這個就需要進行 call 方法注入
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
在 上一篇 文章中,我們說過,call 函式中的 callback 引數有以下幾種形式:
- 閉包 Closure
- class@method
- 類靜態函式,class::method
- 類靜態函式: [ className/classObj, method ];類非靜態函式: [ classObj, method ]
- 若 defaultMethod 不為空,className
首先,我們先看看 call 方法注入的流程圖:
從流程圖中我們可以看出來,雖然呼叫 call 的形式有 5 種,但是實際最終的形式是三種,第二種和第五種被轉化為了第四種。
接下來,我們詳細的解析原始碼:
call
先看一下 call 方法的主體:
public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
{
if (static::isCallableWithAtSign($callback) || $defaultMethod) {
return static::callClass($container, $callback, $parameters, $defaultMethod);
}
return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
return call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
);
});
}
可以看出來,call 方法注入主要有 4 個大的步驟:
- 對於 className@method 和 className-defaultMethod,例項化 className 為類物件,轉化為 [ classObj, method ]。
- 判斷 [ classObj / classname, method ] 是否存在被繫結的方法,如果有則呼叫。
- 利用服務容器解析依賴的引數。
- 呼叫 call_user_func_array。
例項化類物件
在這裡 className@method 和 className-defaultMethod 兩種情況被轉化為 [ classObj, method ], className會被例項化為類物件,並重新呼叫 call:
protected static function isCallableWithAtSign($callback)
{
return is_string($callback) && strpos($callback, '@') !== false;
}
protected static function callClass($container, $target, array $parameters = [], $defaultMethod = null)
{
$segments = explode('@', $target);
$method = count($segments) == 2
? $segments[1] : $defaultMethod;
if (is_null($method)) {
throw new InvalidArgumentException('Method not provided.');
}
return static::call(
$container, [$container->make($segments[0]), $method], $parameters
);
}
執行繫結方法
針對 [ className/classObj, method ], 呼叫被繫結的方法:
protected static function callBoundMethod($container, $callback, $default)
{
if (! is_array($callback)) {
return value($default);
}
$method = static::normalizeMethod($callback);
if ($container->hasMethodBinding($method)) {
return $container->callMethodBinding($method, $callback[0]);
}
return value($default);
}
protected static function normalizeMethod($callback)
{
$class = is_string($callback[0]) ? $callback[0] : get_class($callback[0]);
return "{$class}@{$callback[1]}";
}
public function hasMethodBinding($method)
{
return isset($this->methodBindings[$method]);
}
public function callMethodBinding($method, $instance)
{
return call_user_func($this->methodBindings[$method], $instance, $this);
}
那麼這個被繫結的方法 methodBindings 從哪裡來呢,就是 上一篇 文章提的 bindMethod:
public function bindMethod($method, $callback)
{
$this->methodBindings[$method] = $callback;
}
從上面可以看出來,methodBindings 中 callback 引數一定是 classname@method 形式的。
例項化依賴
這一步就要透過反射來獲取函式方法需要注入的引數型別,然後利用服務容器對引數型別進行解析構建:
protected static function getMethodDependencies($container, $callback, array $parameters = [])
{
$dependencies = [];
foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
}
return array_merge($dependencies, $parameters);
}
getCallReflector 函式利用反射來獲取引數型別,值得注意的是class::method是需要拆分處理的:
protected static function getCallReflector($callback)
{
if (is_string($callback) && strpos($callback, '::') !== false) {
$callback = explode('::', $callback);
}
return is_array($callback)
? new ReflectionMethod($callback[0], $callback[1])
: new ReflectionFunction($callback);
}
利用傳入的引數,利用服務容器構建解析引數型別,或者獲取引數預設值:
protected static function addDependencyForCallParameter($container, $parameter,
array &$parameters, &$dependencies)
{
if (array_key_exists($parameter->name, $parameters)) {
$dependencies[] = $parameters[$parameter->name];
unset($parameters[$parameter->name]);
} elseif ($parameter->getClass()) {
$dependencies[] = $container->make($parameter->getClass()->name);
} elseif ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
}
}
call_user_func_array
關於這個函式可以參考 Laravel學習筆記之Callback Type
call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
);
Written with StackEdit.
本作品採用《CC 協議》,轉載必須註明作者和本文連結