前陣子在SO回答了一個問題
http://stackoverflow.com/questions/27341595/contracts-in-laravel-5/29812568
這問題不難,官方文件就有寫。
過幾天,下面的comments接著有網友 amosmos 提問 IoC Container 實作細節的問題。
他的問題考倒我了,完全無法回答。
於是花了幾天讀原始碼,最後終於搞懂了。
跟大家分享一下這個心得,也順便分享下我在過程中,讀原始碼的整套思路。
正文開始
簡單來說,網友 amosmos 的問題如下:
在 Illuminate/Foundation/Application.php 檔,會在 function registerCoreContainerAliases 看到這些:
$aliases = [ 'app' => ['Illuminate\Foundation\Application', 'Illuminate\Contracts\Container\Container', 'Illuminate\Contracts\Foundation\Application'], 'auth' => 'Illuminate\Auth\AuthManager', 'auth.driver' => ['Illuminate\Auth\Guard', 'Illuminate\Contracts\Auth\Guard'], 'auth.password.tokens' => 'Illuminate\Auth\Passwords\TokenRepositoryInterface', 'blade.compiler' => 'Illuminate\View\Compilers\BladeCompiler', 'cache' => ['Illuminate\Cache\CacheManager', 'Illuminate\Contracts\Cache\Factory'], 'cache.store' => ['Illuminate\Cache\Repository', 'Illuminate\Contracts\Cache\Repository'], // ... ];
讓我們先稱呼’app’、’auth’這些字串為 IoC Container 內的 「key」 好了。
乍看之下,大部份的 service 都在這時候被綁定到 IoC Container內,對應到某個 key。
看起來非常合理,也很漂亮,對吧?Laravel會用到的service都在這時候統一登記好、綁進這IoC Container。
問題來了,在 config/app.php 內,有這個陣列:
'providers' => [ /* * Laravel Framework Service Providers... */ Illuminate\Foundation\Providers\ArtisanServiceProvider::class, Illuminate\Auth\AuthServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class, Illuminate\Bus\BusServiceProvider::class, Illuminate\Cache\CacheServiceProvider::class, Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, // ... ];
你如果熟悉 Service Provider (下文以SP代稱) 的話,就會知道每個 SP 內必定有 function register,負責綁定service到container。
以CacheServiceProvider來說,會出現這幾行:
public function register() { $this->app->singleton('cache', function ($app) { return new CacheManager($app); }); //... }
問題來了,在上面的 Application.php,已經有這個了:
'cache' => ['Illuminate\Cache\CacheManager', 'Illuminate\Contracts\Cache\Factory'],
所以這CacheManager,到底是何時被綁定到 ‘cache’ 這個key上的?
是Application在建構式呼叫registerCoreContainerAliases時綁定的嗎?
還是在某處讀取 config/app.php 的providers時綁定的?
這邊出現了我們的問題一:
Q1: 難道重複綁定了兩次嗎?CacheManager被初始化了兩次?所以是 Laravel 原始碼的瑕疵?
讓我們做個實驗吧,把Application.php裡面這行comment掉:
//'cache' => ['Illuminate\Cache\CacheManager', 'Illuminate\Contracts\Cache\Factory'],
接著在cmd輸入
php artisan tinker
很好,跑起來了,至少laravel沒有炸掉。
接著試試cache功能是否還活著?
Cache::store('file')->put('foo', 'this is foo in the cache', 10); Cache::store('file')->get('foo'); // => "this is foo in the cache"
看起來沒問題!難道真的是Laravel的瑕疵?
接著把 Application.php恢復原狀,然後把CacheServiceProvider的綁定給comment掉:
public function register() { /* $this->app->singleton('cache', function ($app) { return new CacheManager($app); }); */
接著在cmd輸入
php artisan tinker
[ReflectionException]
Class cache does not exist
Laravel炸掉了!根本不能跑!
所以不算是重複綁兩次?
那就出現了我們的問題二:
Q2: 為什麼 Application.php 內的key給comment掉,功能還是正常運作?
反覆翻閱一下 Application.php、Illuminate\Container\Container.php,會發現 function registerCoreContainerAliases 似乎將整串陣列存在container的成員變數$aliases上?
接著尋找讀取config\app.php的地方,會找到 Illuminate\Foundation\Bootstrap\LoadConfiguration.php這檔案,然後發現Illuminate\Foundation\Bootstrap\RegisterProviders.php這檔案,翻閱Application.php和 Illuminate\Foundation\ProviderRepository.php之後,會發現config\app.php內的providers似乎會存在container的成員變數$bindings上?
有一個很簡單的方法去驗證這件事情。
將 Illuminate\Container\Container.php 的 $aliases 與 $bindings 成員變數改成 public,接著 php artisan tinker
$app = App::make('app'); $app->aliases; => [ "Illuminate\Foundation\Application" => "app", "Illuminate\Contracts\Container\Container" => "app", "Illuminate\Contracts\Foundation\Application" => "app", "Illuminate\Auth\AuthManager" => "auth", "Illuminate\Auth\Guard" => "auth.driver", "Illuminate\Contracts\Auth\Guard" => "auth.driver", //... $app->bindings; => [ //... "command.migrate.refresh" => [ "concrete" => Closure {#236 class: "Illuminate\Database\MigrationServiceProvider", this: Illuminate\Database\MigrationServiceProvider {#230 …}, file: "/home/howtomakeaturn/projects/modmaj.com/vendor/laravel/framework/src/Illuminate/Database/MigrationServiceProvider.php", line: "144 to 146", }, "shared" => true, ], "command.migrate.install" => [ "concrete" => Closure {#237 class: "Illuminate\Database\MigrationServiceProvider", this: Illuminate\Database\MigrationServiceProvider {#230 …}, parameters: [ "$app" => [], ], file: "/home/howtomakeaturn/projects/modmaj.com/vendor/laravel/framework/src/Illuminate/Database/MigrationServiceProvider.php", line: "168 to 170", }, "shared" => true, ], // ...
會發現的確是這樣沒錯!
事到如今,我們會發現Q1的問題解決了:Application.php是存在aliases內,而config\app.php是存在bindings內。並沒有重複綁定兩次的問題!而是container實作細節沒搞清楚的問題!
所以真正的問題應該是這個:
Q3: IoC Container內的$aliases跟$bindings分別是什麼?兩者如何互動?
要解決這個問題,必須去讀container最核心的function make部份程式碼。
/** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @return mixed */ // 首先注意到的是,之前我們稱呼’app’、’auth’這些字串為 IoC Container 內的 「key」 // 其實正確名稱是「abstract」 public function make($abstract, array $parameters = []) { // 這個getAlias的功能如下 // >>> $app->getAlias('app'); // => "app" // >>> $app->getAlias('Illuminate\Foundation\Application'); // => "app" $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. // 如果是用singleton或是instance方法,應該是會把物件存在instances陣列 //這地方會檢查abstract是否真的跟物件綁定,是的話就回傳,收工 if (isset($this->instances[$abstract])) { return $this->instances[$abstract]; } // getConcrete內會去查 $this->bindings[$abstract]['concrete'] // 尋找這abstract對應到的是哪個類別、或是Closure $concrete = $this->getConcrete($abstract); // 後面一大段省略 }
看到這邊,我們幾乎可以確定$aliases跟$bindings的關係為何了:
我們在使用 App::make($abstract) 時,輸入的abstract,實際上主要是去bindings找對應並叫出來。
而aliases只是紀錄abstract的其他別名而已,舉例如下:
>>> App::make('cache'); => Illuminate\Cache\CacheManager {#431} >>> App::make('Illuminate\Cache\CacheManager'); => Illuminate\Cache\CacheManager {#431} >>> App::make('Illuminate\Contracts\Cache\Factory'); => Illuminate\Cache\CacheManager {#431}
還可以再做進一步的測試,將Container的function alias改成public,然後做以下測試:
$app = App::make('app'); $app->bind('greet', function(){ return 'Hello World!'; }); $app->make('greet'); // => "Hello World!" $app->make('GreetClass'); // ReflectionException with message 'Class GreetClass does not exist' $app->alias('greet', 'GreetClass'); $app->make('GreetClass'); // => "Hello World!"
所以正式回答Q3的問題:
Service Provider的function register內會去登記到IoC Container的abstract,實際上是存在$bindings或$instances陣列。
而App::make($abstract)實際上也是從這兩個陣列找東西出來用。
$aliases陣列只是存abstract的其他別名,讓 Binding Interfaces To Implementations 成為可能,也允許直接打類別全名進去。
結論
讀Laravel原始碼有幾個小技巧
1. 多用php artisan tinker去跟laravel互動
2. 把一些source code的變數、函數改成public,在tinker硬倒出來觀察
3. 有些東西很難在一天內搞清楚,睡個覺醒來再看通常很有幫助
(完)