tohokuaikiのチラシの裏

技術的ネタとか。

LaravelのClassのオートロードを読んでみて、Facadeがどうやって短い名前でCallされているかを考える

本丸まで

とりあえず、読み込み順としては

  1. public/index.php
  2. bootstrap/autoload.php
  3. vendor/autoload.php
  4. composer/autoload_real.php
  5. vendor/composer/ClassLoader.php
  6. vendor/composer/autoload_classmap.php とくる。

Composer\Autoload\ClassLoaderで実行されていること

このClassで、ClassMapを持ってそのクラスとファイルの対応表が vendor/composer/autoload_classmap.php にあるのでその一覧からClassを読み込んでいる。

とりあえず今手元のautoload_classmap.phpを見てみたら、3000個近くのClassが列挙してあった。

この3000個の中にあるClassであれば、 Symfony\Component\CssSelector\XPath\Extension\FunctionExtension というのも、

new Symfony\Component\CssSelector\XPath\Extension\FunctionExtension

だけで使えるようになる。

Facadesは何故いきなりuseだけで使えるようになるのか?

これ、結構わかりづらい。

  1. vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterFacades.php で config/app.php のClassエイリアスを登録
  2. vendor/laravel/framework/src/Illuminate/Foundation/AliasLoader.php のloadによりautoloadの一番優先度の高いautoloadとしてregisterされる
  3. autoloadされた時点で、class_aliasによりClassのエイリアスを作る

これにより、例えばMiddlewareファイルで

<?php
namespace App\Http\Middleware;

use Auth;
error_log(class_exists('Auth') ? 'Auth exists': 'none');

とすると、

  • use Authの時点ではまだAuthというクラスに対する実装は決まっていない
  • class_existsを実行した時点でautoloadが働く
  • Authクラスのエイリアスは、config/appでIlluminate\Support\Facades\Authとなっている

ことから、Illuminate\Support\Facades\Authクラスがautoloadされるようになっている。

ついでに、このIlluminate\Support\Facades\Authクラスから実際の$authインスタンスが決められる手順

結論から言うと、

vendor/laravel/framework/src/Illuminate/Foundation/Application.php

のregisterCoreContainerAliases()により決定されている。

このregisterCoreContainerAliases()により、authファサードだと'Illuminate\Auth\AuthManager', 'Illuminate\Contracts\Auth\Factory'の2つの・routeファサードだと、'Illuminate\Routing\Router', 'Illuminate\Contracts\Routing\Registrar'の2つのクラスがその対象になるのである。

エイリアスの一覧は、そのregisterCoreContainerAliasesメソッドに20個ほど記述がある。

aliasからのインスタンス生成

このファサードエイリアス名と関連付けられたClassのインスタンスがFacadeの実体として実行されるのだが、この候補のうちからどれを選択するか?なのか?

候補が詰まった、Facade::$app(Illuminate/Foundation/Applicationのインスタンス)は、Illuminate/Container/Container.phpを継承しておりこれはArrayAccessをインプリメントしている。なので、このFacade::$app[$name]した時の挙動はContainerの実装に委ねられている。

・・・と考えてまでコード読み切れなかったので挫折。

なんだけど、これ実は候補じゃなくって単に1つ目のClassがインスタンスになっているだけという

このregisterCoreContainerAliases()内のローカル変数$aliasesって、キーの値の配列が

2つの場合 (router, auth) =>
* 1つ目が普通のClass * 2つ目がinterface

3つの場合 (cookie)=>
* 1つ目が普通のClass * 2つ目がinterfaceの親クラス * 3つ目が普通のClassがimplementしているinterface

3つの場合 (queue)=>
* 1つ目が普通のClass * 2つ目がinterface * 3つ目もinterface(2個目)

となっている。ということは、Facadeで呼ばれるのは1つ目のClassになる。

これに意味があるんだろうか?

と思って、authの2つ目のinterfaceクラスを削除したら

Illuminate \ Contracts \ Container \ BindingResolutionException
Target [Illuminate\Contracts\Auth\Factory] is not instantiable while building [Illuminate\Auth\Middleware\Authenticate].

ってエラーが出た。

なんじゃこりゃ・・・。

デバッグトレースを見てみると(filp/whoopsが無かったらとてもできる代物ではない)、middlewareの"auth"呼び出しの際にそのインスタンス解決に対して

array:1 [▼
  0 => ReflectionParameter {#169 ▼
    +name: "auth"
    position: 0
    typeHint: "Illuminate\Contracts\Auth\Factory"
  }
]

となっていた。

おそらくであるが、middlewareのauthに対して「"Illuminate\Contracts\Auth\Factory"」を実装したものであればFacadeで登録されていたものであっても使えるようにするという方針があるのではないか?

ということで、それっぽいところがMiddleware使ってるところで無いかと調べてみると

vendor/laravel/framework/src/Illuminate/Session/Middleware/AuthenticateSession.php

のコンストラクタでタイプヒントに使っていた。

ということは、AuthenticateSessionをnewする際の引数として

  • Illuminate\Contracts\Auth\Factory がTypeHintとして与えられる
  • そのTypeHintから実装可能なClassを探す
  • 探す手段として、FacadeのApplicationインスタンスで登録されているものを探す
  • 無かった場合、そのTypeHintを直接Newしてみる。
  • TypeHintがinterfaceだった場合、Fatalで終了

ということなんだな。

ところで、PHPって関数やクラスをNewする際にその関数名やクラス名からタイプヒントのClassを取れるの?

ReflectionParameterクラスを使うことでできるっぽい。

んだけど、Reflection使うのは本当に大変そうなのでちょっと実際にどうやってるかはあとで。