Laravel框架中的make方法详解
为什么网上已经有这么多的介绍Laravel的执行流程了,Laravel的容器详解了,Laravel的特性了,Laravel的启动过程了之类的文章,我还要来再分享呢?
因为,每个人的思维方式和方向是不一样的,所以就会出现这样的一个场景,当你遇到一个问题在网上寻求答案的时候,有很多文章都解释了你的这个问题,但是你只对其中一篇感兴趣,那是因为作者的思维方式和你的很接近而作者的文笔也可能是你喜欢的那种类型。正因如此,我也来分享一些我在研究Laravel框架时的一些观点和看法,希望给那些和我有类似思维方式的同学们一些启发和帮助。也欢迎大家拍砖。
这次的内容会有些多,因为这个make实在是太重要了,如果大家能耐着性子看完的话,相信会有所帮助。
Laravel中的make方法是用来从容器当中解析一个type,这个type是源码当中定义的,不是很好翻译成中文。解析后返回的结果就是type的一个实例。
看过源码的同学应该知道在Illuminate\Foundation\Application这个类和它的父类Illuminate\Container\Container类中都有make方法,那么当执行如index.php中的这行代码,
1 $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
的时候,就会首先去执行Illuminate\Foundation\Application中的make方法,那么我们就先看看它。(这篇文章就以make这个Kernel类为例)
1 /** 2 * Resolve the given type from the container. 从容器当中解析给定的type 3 * 4 * (Overriding Container::make) 覆盖了父类中的make方法 5 * 6 * @param string $abstract 给定的type 7 * @param array $parameters 指定一些参数 可选项 8 * @return mixed 9 */ 10 public function make($abstract, array $parameters = []) 11 { 12 $abstract = $this->getAlias($abstract);//调用父类中的getAlias方法 13 //如果在deferredServices这个数组设置了这个type并且在instances数组中没有设置这个type 14 if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) { 15 $this->loadDeferredProvider($abstract);//那么就执行这个方法:加载被定义为延迟的服务提供者 16 } 17 18 return parent::make($abstract, $parameters);//调用父类的make方法 19 }
好,我们一步一步的来,先看看这个getAlias方法,这个方法的作用就是返回这个类的别名,如果给出的是一个完整的类名且在aliases中已经设置了那么就返回这个类名的别名,如果没有设置过就返回这个类名本身,大家在看这个方法的时候可以先var_dump一下$app,对照着看里面的aliases数组,框架作者写这个方法真的很巧妙,至少这种递归方式在我实际开发当中很少用到。
1 /** 2 * Get the alias for an abstract if available. 3 * 4 * @param string $abstract 5 * @return string 6 * 7 * @throws \LogicException 8 */ 9 public function getAlias($abstract) 10 { 11 if (! isset($this->aliases[$abstract])) { 12 return $abstract; 13 } 14 15 if ($this->aliases[$abstract] === $abstract) { 16 throw new LogicException("[{$abstract}] is aliased to itself."); 17 } 18 19 return $this->getAlias($this->aliases[$abstract]); 20 }
接下来就是对deferredServices和instances这个两个数组进行判断,在本例 $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 当中,判断的结果为false,因此不执行loadDeferredProvider方法。
再接下来就是调用父类Illuminate\Container\Container中的make方法了,
1 /** 2 * Resolve the given type from the container. 3 * 4 * @param string $abstract 5 * @param array $parameters 6 * @return mixed 7 */ 8 public function make($abstract, array $parameters = []) 9 { 10 return $this->resolve($abstract, $parameters);//直接调用resolve方法 11 }
重点来了,我们看看这个resolve方法执行了哪些操作,
/** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @return mixed */ protected function resolve($abstract, $parameters = []) { //resolve.注1:还是调用getAlias方法 同上面的一样 $abstract = $this->getAlias($abstract); //resolve.注2:判断实例化这个类是否需要其他一些有关联的类 //如果$parameters非空或getContextualConcrete这个方法返回非空 //那么该变量就为true 这里所谓的关联并不是类本身的依赖 应该是逻辑上的关联 $needsContextualBuild = ! empty($parameters) || ! is_null( $this->getContextualConcrete($abstract)//resolve.注3 ); // if($abstract == \Illuminate\Contracts\Http\Kernel::class){ // dump($this->getContextualConcrete($abstract)); // exit; // } //如果当前需要解析的type被定义为一个单例的话 先判断是否已经被实例化了 如果是那么直接返回这个实例 // 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. //在容器中已经被实例化的类会存储在instances数组中 这跟大部分框架中保存类实例的方式一样 if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract]; } //将parameters赋值给成员属性with 在实例化的时候会用到 不过在本例当中parameters为null $this->with[] = $parameters; //resolve.注4 $concrete = $this->getConcrete($abstract); //我们现在已经准备好了去实例化这个具体的type 实例化这个type的同时还会递归的去解析它所有的依赖 // 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. //resolve.注5:先来判断一下它是否是可以被build的 我们来看一下这个isBuildable方法 if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete);//注6:调用build方法开始实例化这个type } else { $object = $this->make($concrete); } // if($abstract == \Illuminate\Contracts\Http\Kernel::class){ // dump($object); // exit; // } // if($abstract == \Illuminate\Contracts\Http\Kernel::class){ // dump($this->getExtenders($abstract)); // exit; // } //resolve.注6:判断这个type是否有扩展 如果有扩展那么就使用扩展继续处理这个type实例 在本例当中没有 // 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); } //resolve.注7:判断这个type是否是一个单例 如果在绑定的时候定义为单例的话 那么就将其保存在instances数组中 //后面其他地方再需要make它的时候直接从instances中取出即可 本例当中Kernel在绑定的时候时通过singleton方法 //绑定的 因此是一个单例 // 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; } // if($abstract == \Illuminate\Contracts\Http\Kernel::class){ // dump($this->instances); // exit; // } //触发解析后的回调动作 也就是在得到了type的实例以后还需要做哪些后续的处理 本例当中没有后续的处理 $this->fireResolvingCallbacks($abstract, $object); //resolve.注8:设置resolved数组并将with数组中的数据弹出 // Before returning, we will also set the resolved flag to "true" and pop off // the parameter overrides for this build. After those two things are done // we will be ready to return back the fully constructed class instance. $this->resolved[$abstract] = true; array_pop($this->with); //终于return了 return $object; }
resolve.注3:这里我就不粘贴这个getContextualConcrete方法的代码了,这个方法就是通过判断在Illuminate\Container\Container类中$contextual这个数组里面有没有这个type的相关数据,如果有就返回这个数据,如果没有就返回null,在本例当中返回的是null。
resolve.注4:我们看一下这个getConcrete方法,同样在看代码的时候dump一下$app,对照着看,这个方法在绝大部分时候都是会返回这个type在绑定的时候注册的那个Closure,
/** * Get the concrete type for a given abstract. * * @param string $abstract * @return mixed $concrete */ protected function getConcrete($abstract) { //还是先调用这个方法来判断是否存在有关联关系的数据 如果有直接返回该数据 if (! is_null($concrete = $this->getContextualConcrete($abstract))) { return $concrete; } //如果上面没有被返回 那么就判断这个type在绑定到容器的时候有没有绑定一个concrete属性 //也就是一个回调 laravel的习惯是在绑定type的时候会提供一个Closure作为这个type实例化时 //的一些操作 比如最简单的就是 new xxx(); // If we don't have a registered resolver or concrete for the type, we'll just // assume each type is a concrete name and will attempt to resolve it as is // since the container should be able to resolve concretes automatically. if (isset($this->bindings[$abstract])) { return $this->bindings[$abstract]['concrete']; } //如果都没有那么返回它本身了 return $abstract; }
resolve.注5:isBuildable方法是很简单的一个判断,通过查看$app,这个type的concrete是一个Closure,因此这里返回true
protected function isBuildable($concrete, $abstract) { //返回的是一个bool值 如果concrete和abstract全等或concrete是一个Closure返回true return $concrete === $abstract || $concrete instanceof Closure; }
resolve.注6:在分析这个build方法之前我们先来看看在本例当中的type对应的concrete保存的是个什么东东,还是使用dump,不得不说symfony的这个var-dumper包真的很好用啊,从下面的截图中可以看到这个闭包函数共有两个参数就是parameters里面的那两个,同时还use了两个参数,这个闭包函数的定义在Container.php文件的251至257行(也就是laravel容器提供的一个通用闭包函数,这个type在绑定的时候并没有提供自己单独的闭包函数),大家可以对照着看一下bindings数组当中其他的一些concrete的值。
好,我们来看一下这个build方法,也是很复杂的一个方法啊。先看方法中第一个判断,在本例当中判断为true,因此直接执行这个Closure并返回结果。
/** * 实例化一个这个type的具体对象 * Instantiate a concrete instance of the given type. * * @param string $concrete * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function build($concrete) { //判断如果concrete是一个Closure实例 那么直接执行它 在本例当中是 因此直接执行并返回 // 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) {
//调用这个Closure的时候传递了两个参数 return $concrete($this, $this->getLastParameterOverride()); } //build.注1 $reflector = new ReflectionClass($concrete); //build.注2:调用reflection本身的isInstantiable方法来判断这个类是否可以被实例化如果是接口或是抽象类的话就返回false
//在本例当中可以被实例化因此返回true // 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); } //build.注3:把待实例化的类名保存在buildStack数组当中 因为如果这个类有依赖的话 那么还需要实例化它全部的依赖
//因此将待实例化的类名都保存起来 来确保完整性 $this->buildStack[] = $concrete; //build.注4:获取类的构造函数 $constructor = $reflector->getConstructor(); //build.注5:如果构造函数为空那么先将待构建堆栈中数组buildStack中刚才插入那条数据删除 然后直接 new 这个类 结束 // 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; } //build.注6 $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(//build.注7 调用这个方法来解析这些依赖 $dependencies ); array_pop($this->buildStack); //build.注7 return $reflector->newInstanceArgs($instances); }
我们去看看这个Closure的执行过程,也就是Container.php的251至257行的代码,看到了吗?再来一遍!!
//在上面build方法中调用这个Closure的时候已经看到传递了两个实参 //分别对应这里的形参为 $container=$this, $parameters=$this->getLastParameterOverride() //use中的两个参数我们在上面的截图当中也能找到 分别为 //$abstract="Illuminate\Contracts\Http\Kernel" $concrete="Zhiyi\Plus\Http\Kernel" return function ($container, $parameters = []) use ($abstract, $concrete) { if ($abstract == $concrete) {//不相等 return $container->build($concrete); } //执行这句 也就是$this->make("Zhiyi\Plus\Http\Kernel", $parameters) //我的妈呀 再来一遍的节奏啊 return $container->make($concrete, $parameters); };
由于篇幅的关系我就不带大家看重复的内容了,在执行了make方法的一系列操作之后,会重新来到build方法中,在build的第一个判断中为false
if ($concrete instanceof Closure) { return $concrete($this, $this->getLastParameterOverride()); }
因此执行它下面的代码,从build.注1开始,根据这个concrete实例化一个反射类(现在这个concrete的值为Zhiyi\Plus\Http\Kernel),反射的相关知识大家自己看手册就了解了,很简单的。
给大家一个小技巧,在调试PHP代码的时候如果仅仅使用var_dump配合exit这种方法的话,可能不会出现你预期的效果,因为也许exit过早或过完,就没有你想看到的东西了,再或者所有经过这里的代码都打印出来,影响大家的调试。我使用的方法是在需要调试的地方加上一个判断,比如这里,我已经知道concrete的值,那么我就判断一下,如果这里出现了预期的值就dump然后exit
$reflector = new ReflectionClass($concrete); if($concrete == 'Zhiyi\Plus\Http\Kernel'){ dump($reflector); exit; }
来看下效果吧,果然浏览器中只打印出了我们想看的内容。
build.注4:在本例当中类为Zhiyi\Plus\Http\Kernel,看这个类的源码发现没有构造函数。那么在看它继承的父类Illuminate\Foundation\Http\Kernel当中有没有,发现是有的。
build.注5:如果类及其父类当中都没有构造方法,那么直接 new 这个类并返回这个实例对象,new 这个类是依靠composer的自动加载机制来实现的。
在本例中,类中是存在构造方法的,那么我们打印出这个获取到的构造函数看看,同样还是使用我介绍那种调试小技巧
$constructor = $reflector->getConstructor(); if($concrete == 'Zhiyi\Plus\Http\Kernel'){ dump($constructor); exit; }
看看结果吧,发现这个类Illuminate\Foundation\Http\Kernel的构造方法需要两个参数,$app和$router
build.注6:执行 $dependencies = $constructor->getParameters(); 来获取构造方法的参数,这两个参数在上图中已经看到了,参数$app的typeHint(类型提示)为Illuminate\Contracts\Foundation\Application类的实例,参数$router的typeHint为Illuminate\Routing\Router类的实例
build.注7:执行resolveDependencies方法去依次解析这些依赖,如果依赖是对象的话,也就是去实例化这些类来获取这个对象,也就是再依次重复上面所有的过程,从make开始。因为只要是想在laravel框架的容器里面获得一个类的实例就要执行make方法。我们来看看这个resolveDependencies方法,
/** * Resolve all of the dependencies from the ReflectionParameters. * * @param array $dependencies * @return array */ protected function resolveDependencies(array $dependencies) { $results = []; //在循环当中依次解析这些依赖 foreach ($dependencies as $dependency) { // If this dependency has a override for this particular build we will use // that instead as the value. Otherwise, we will continue with this run // of resolutions and let reflection attempt to determine the result.
//判断这个依赖是否被重新定义过 也就是在resolve方法中执行的一句代码 $this->with[]=$parameters;
//判断的依据(也就是这个方法内部)就是这个with数组 在本例当中with中是两个空数组 因此判断为false if ($this->hasParameterOverride($dependency)) { $results[] = $this->getParameterOverride($dependency); continue; } //这里调用$dependency的getClass方法 $dependency是一个ReflectionParameter对象 这个对象有getClass方法
//reflection的相关内容请查阅手册
//在本例当中getClass会返回一个ReflectionClass对象 不为空 那么是哪个类的ReflectionClass对象呢 当前循环中
//的$dependency是illuminate\Contracts\Foundation\Application这个依赖 // If the class is null, it means the dependency is a string or some other // primitive type which we can not resolve since it is not a class and // we will just bomb out with an error since we have no-where to go. $results[] = is_null($dependency->getClass()) ? $this->resolvePrimitive($dependency) : $this->resolveClass($dependency); } return $results; }
补充:with这个数组的作用是什么,我理解为类的动态实例化,也就是在不同的情况下new这个类的时候可以传入不同的构造方法参数。
我们来看一下这个resolveClass方法吧,任然还是执行make方法,这次make的参数大家应该都清楚了吧,就是Illuminate\Contracts\Foundation\Application,又是一个循环。
protected function resolveClass(ReflectionParameter $parameter) { try { return $this->make($parameter->getClass()->name); } // If we can not resolve the class instance, we will check to see if the value // is optional, and if it is we will return the optional parameter value as // the value of the dependency, similarly to how we do this with scalars. catch (BindingResolutionException $e) { if ($parameter->isOptional()) { return $parameter->getDefaultValue(); } throw $e; } }
再次调用make方法,过程就不看了,反正在resolveDependencies这个方法执行结束后,会返回一个results数组,我们dump一下这个results,这次返回的是真正的两个对象了,也就是本例Illuminate\Contracts\Http\Kernel中构造方法所需要的两个参数
到这里为止resolveDependcies这个方法就执行结束了,大家还记得这个方法是在哪里被调用的吗?哈哈,我也有点乱了,别急我们看看上面的内容。哦!是在build方法中被调用的,那么我们就接着看build方法下面的代码吧,
build.注7:调用newInstanceArgs方法,手册中给出的这个方法的解释为“创建一个类的新实例,给出的参数将传递到类的构造函数”。 OK,搞定。我们来看看build执行之后的结果,dump一下,终于Kernel类被new出来了。
你真的以为这样就搞定了吗?还没呢,还记得build方法是在哪里被调用的吗?是在resolve方法中,走吧,我们返回resolve方法中看看build方法执行之后还有哪些操作。
resolve.注6:判断这个type是否有扩展 如果有扩展那么就使用扩展继续处理这个type实例 在本例当中没有
resolve.注7:判断这个type是否是一个单例 如果在绑定的时候定义为单例的话 那么就将其保存在instances数组中后面其他地方再需要make它的时候直接从instances中取出即可 本例当中Kernel在绑定的时候时通过singleton方法绑定的,因此是一个单例
resolve.注8:设置resolved数组并将with数组中的数据弹出
最后,终于返回这个Illuminate\Contracts\Http\Kernel类的对象了。resolve方法return给了make方法。怎么样?大家看的明白了吗?
大体的流程就是根据绑定这个类的时候提供的参数, 来对这个类进行实例化。提供的参数可以是很多种类型如:闭包函数,字符串。那么框架会根据不同的类型来确定如何实例化,同时在实例化类的时候去递归的解决类的依赖问题。
总结
其实如果有耐心有时间的话一行行的去读源码,就会发现虽然功能上很复杂,但是原理上很简单。功能上的复杂仅仅是为了做到框架的兼容和全面,仔细看下来发现并没有什么多余的代码,并不像有些人所说的Laravel框架不够精简,很多功能需要实现,就必须要通过一些方式方法。
原创内容,禁止转载!