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 functionmake($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框架不夠精簡,很多功能需要實現,就必須要通過一些方式方法。
原創內容,禁止轉載!
Laravel框架中的make方法詳解