Laravelの仕組み整理
用語整理
- 依存性注入
- サービスプロバイダ
- 遅延プロバイダ
- サービスコンテナ
- singleton(シングルトン)
- bind(結合)
- bindings
- abstract
- concrete
- register(登録)
- resolive(解決)
- boot
- ファサード(Facade)
- Laravel内でグローバル関数であるapp() / グローバル変数である$app
- make
サービスコンテナとはLaravelに備わっている機能のことで、以下のような機能がある。
- アプリケーション内のサービスをまとめて管理する
- 依存性注入を扱いやすくする
1に出てきたサービスとは「認証情報を扱う」「ログを記録する」などの機能の単位を指す。
サービスコンテナは読んで字の如くサービスを格納する入れ物のようなものである。
先ほどのサービスをサービスコンテナ内に詰める動作をbind(バインド)と呼び、サービスコンテナに詰めたサービスを使用する際にはresolveというプロセスを行う。resolveによってバインドしたサービスをインスタンス化することができる。
2はアプリケーション内のプログラムにある依存の解決を自動でやってくれる。自動で解決できない場合もあるが、そのときは1に書いた方法でバインドのプロセスで依存解決の方法を明示してあげることができる。
ここで「依存」という単語が登場したが、プログラミングにおける依存という単語は「あるプログラムが別のプログラムに依存している」という状態を表す。
class UserController extends Controller
{
public function create($param) {
$repository = new UserRepository;
$repository->save($param);
...
}
}
例えば上記のUserControllerクラスはUserRepositoryクラスを呼び出さないとcreateメソッドは成り立たないので、UserControllerクラスはUserRepositoryクラスに依存している状態だと言える。
サービスコンテナの依存性注入は、コンストラクタやメソッドに必要な依存オブジェクトを自動的に生成して渡す機能である。
アプリケーションインスタンスの作成
Laravelのアプリケーションを実行する経路は「HTTP通信によるリクエスト」と「php artisan
コマンドのように内部から実行するもの」がある。
HTTP通信によるリクエストの場合はpublic/index.php
が起点となり、artinsanコマンドの場合はプロジェクトルートのartisan
ファイルが実行されるが、いずれも下記の共通した記述の通りbootstrap/app.php
が読み込まれてアプリケーションのインスタンスが生成される。
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
bootstrap/app.phpではIlluminate\Foundation\Applicationのインスタンスを生成して返す。
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
//...省略
return $app;
Applicationクラスの処理
ApplicationクラスのコンストラクタではregisterBaseBindings
メソッド、registerBaseServiceProviders
メソッド、registerCoreContainerAliases
メソッドが実行される。
public function __construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();
}
registerBaseBindings
protected function registerBaseBindings()
{
static::setInstance($this);
$this->instance('app', $this);
$this->instance(Container::class, $this);
$this->singleton(Mix::class);
$this->singleton(PackageManifest::class, function () {
return new PackageManifest(
new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
);
});
}
- setInstanceで自クラスをinstanceとして設定(インスタンスが生成された後にコンストラクタが実行されるので、$thisはAplicationクラス自身のインスタンスを指す)。
setinstance()
public static function setInstance(ContainerContract $container = null)
{
return static::$instance = $container;
}
=> setInstanceメソッドはApplicationクラスの継承元のContainerクラスのメソッドであるため、遅延静的束縛の仕組みを使ってApplicationクラス自身のインスタンスをバインドする。
$this->instance('app', $this);でアプリケーションインスタンスを生成。
instance()
public function instance($abstract, $instance)
{
$this->removeAbstractAlias($abstract);
$isBound = $this->bound($abstract);
unset($this->aliases[$abstract]);
$this->instances[$abstract] = $instance;
if ($isBound) {
$this->rebound($abstract);
}
return $instance;
}
- removeAbstractAliasでエイリアス=>抽象(クラス/インターフェース)の登録を削除。
- boundはbindの過去分詞形で、$abstractに渡された値でバインドされているクラス/インターフェースがあるかをチェック。
-
unset($this->aliases[$abstract]);
で抽象(クラス/インターフェース)=>エイリアスの登録を削除。 - ここまでで古いエイリアスと抽象(クラス/インターフェース)を取り除いた上で、抽象とインスタンスの対応を登録する(
$this->instances[$abstract] = $instance;
)。
registerBaseServiceProviders()
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
3つのサービスプロバイダーを登録しているが、コンストラクタは以下のようになっている。
public function __construct($app)
{
$this->app = $app;
}
サービスプロバイダはIlluminate\Foundation\Application
クラスに依存しており、サービスプロバイダにはコンストラクタインジェクションでIlluminate\Foundation\Application
を注入している。
Application::register()
public function register($provider, $force = false)
{
// 既に登録済み($serviceProvidersにセットされている)であれば処理を終了
if (($registered = $this->getProvider($provider)) && ! $force) {
return $registered;
}
if (is_string($provider)) {
$provider = $this->resolveProvider($provider);
}
// サービスプロバイダークラスのregisterメソッドを実行
$provider->register();
// $bindings or $singletonプロパティの存在をチェックして、結合を登録する。
if (property_exists($provider, 'bindings')) {
foreach ($provider->bindings as $key => $value) {
$this->bind($key, $value);
}
}
if (property_exists($provider, 'singletons')) {
foreach ($provider->singletons as $key => $value) {
$key = is_int($key) ? $value : $key;
$this->singleton($key, $value);
}
}
$this->markAsRegistered($provider);
if ($this->isBooted()) {
$this->bootProvider($provider);
}
return $provider;
}
この時点で各サービスプロバイダのbootメソッドは実行されていない。
シングルトン
シングルトンという言葉が一般的に指すのは、「あるクラスのインスタンスが同時に1つだけ存在させる」設計パターンを指す。
LaravelにおいてはApplicationクラスのインスタンス($appとして参照されるもの)がシングルトンとして管理されている。
/**
* Register a shared binding in the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @return void
*/
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
Containerクラスには上記のようなsingletonメソッドが存在し、抽象名とそれに対応する具体クラスをシングルトンとして管理されたApplicationクラスのインスタンスに対してバインドする。これによって抽象名によって具体的な処理が書かれたクラスをどこからでも呼び出すことができる。
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
// If no concrete type was given, we will simply set the concrete type to the
// abstract type. After that, the concrete type to be registered as shared
// without being forced to state their classes in both of the parameters.
if (is_null($concrete)) {
$concrete = $abstract;
}
// If the factory is not a Closure, it means it is just a class name which is
// bound into this container to the abstract type and we will just wrap it
// up inside its own Closure to give us more convenience when extending.
if (! $concrete instanceof Closure) {
if (! is_string($concrete)) {
throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
}
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
// If the abstract type was already resolved in this container we'll fire the
// rebound listener so that any objects which have already gotten resolved
// can have their copy of the object updated via the listener callbacks.
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
bindしたものを呼び出すのはapp()->make('abstract')
AliasesとAbstractAliases
ContainerクラスにはAliasesとAbstractAliasの2つのインスタンス変数が定義されている。
/**
* The registered type aliases.
*
* @var string[]
*/
protected $aliases = [];
/**
* The registered aliases keyed by the abstract name.
*
* @var array[]
*/
protected $abstractAliases = [];
ReflectionClassを使って調べる
上記2つの変数はアクセス修飾子にprotectedがついているため、直接中身を見て確認することはできない。
そこでPHPの組み込み機能であるReflrectionClassを使って、中身を見ていく。
$app = app();
$reflection = new ReflectionClass($app);
$property = $reflection->getProperty('abstractAliases');
$property->setAccessible(true);
dd($property->getValue($app)); // プロパティの値をダンプ
Containerクラスの$aliasesをダンプした結果
[
"Illuminate\Foundation\Application" => "app"
"Illuminate\Contracts\Container\Container" => "app"
"Illuminate\Contracts\Foundation\Application" => "app"
"Psr\Container\ContainerInterface" => "app"
"Illuminate\Auth\AuthManager" => "auth"
"Illuminate\Contracts\Auth\Factory" => "auth"
...
]
Containerクラスの$abstractAliasesをダンプした結果
"app" => [
0 => "Illuminate\Foundation\Application"
1 => "Illuminate\Contracts\Container\Container"
2 => "Illuminate\Contracts\Foundation\Application"
3 => "Psr\Container\ContainerInterface"
]
"auth" => [
0 => "Illuminate\Auth\AuthManager"
1 => "Illuminate\Contracts\Auth\Factory"
上記のようにエイリアス=>クラス/インターフェース
が格納されている。また、クラスやインターフェースとエイリアスは1対1でバインドされているとは限らない。
サービスコンテナ文脈におけるabstractとconcreateとは
concreatは具体的な実装なんだなとイメージしやすいが、abstractという表現が分かりづらい。
上記の記事ではabstractをラベルに例えていて、分かりやすい説明だった。
bindメソッドとsingletonメソッドの違い
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
// If no concrete type was given, we will simply set the concrete type to the
// abstract type. After that, the concrete type to be registered as shared
// without being forced to state their classes in both of the parameters.
if (is_null($concrete)) {
$concrete = $abstract;
}
// If the factory is not a Closure, it means it is just a class name which is
// bound into this container to the abstract type and we will just wrap it
// up inside its own Closure to give us more convenience when extending.
if (! $concrete instanceof Closure) {
if (! is_string($concrete)) {
throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
}
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
// If the abstract type was already resolved in this container we'll fire the
// rebound listener so that any objects which have already gotten resolved
// can have their copy of the object updated via the listener callbacks.
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
singletonのメソッドではbindメソッドの呼び出しだけ行われており、singletonに渡されたものと同じパラメータを渡すが、bindメソッドではデフォルトでfalseになっていた第3引数のみtrueにして渡している。
- HTTPリクエストの場合は/public/index.php、Consoleの場合は/artisanを実行
- bootstrap/app.phpが呼ばれる。
- Applicationクラスのインスタンスを作成
- コンストラクタ内で以下の処理を行う
- アプリケーション内の各ディレクトリ(app、configなど)の絶対パスを登録
- アプリ内の依存解決やインスタンス管理を行うためのバインドを行う
- 自分自身($this)をサービスコンテナに登録
- $bindings['Illuminate\Foundation\Mix'] = Illuminate\Foundation\Mix::classをシングルトン(shared=true)としてバインド
- PackageManifestクラス(Composerのキャッシュされたパッケージ情報を扱うクラス)を読み取り、依存関係をシングルトンとしてバインド
- サービスプロバイダの登録
- このタイミングでは必ず仕様するサービスプロバイダ(EventServiceProvider、LogServiceProvider、RoutingServiceProvider)を登録する。
- エイリアスの登録
- コンストラクタ内で以下の処理を行う
サービスプロバイダの読み込み
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
Applicationクラスのインスタンスが生成された後、HttpカーネルとConsoleカーネルがシングルトンとして結合される。
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
consoleの場合はartisanの処理中でApplicationクラスのインスタンスからmakeメソッドを呼び出し、Illuminate\Contracts\Console\Kernel::class
が引数として渡される。
先ほどのApp\Console\Kernel::class
のインスタンスが初期化される。
App\Console\Kernel::class
はIlluminate\Foundation\Console\Kernel
が継承されている。
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
artisanに戻るとKernelのhandleメソッドがコールされており、ここでartisanコマンド実行時の引数が渡される。
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\SetRequestForConsole::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
...省略
public function handle($input, $output = null)
{
$this->commandStartedAt = Carbon::now();
try {
if (in_array($input->getFirstArgument(), ['env:encrypt', 'env:decrypt'], true)) {
$this->bootstrapWithoutBootingProviders();
}
$this->bootstrap();
return $this->getArtisan()->run($input, $output);
} catch (Throwable $e) {
$this->reportException($e);
$this->renderException($output, $e);
return 1;
}
}
Illuminate\Foundation\Console\Kernel
のhandleメソッドでは$bootstrappersがロードされており、\Illuminate\Foundation\Bootstrap\RegisterProviders::class
も含まれている。
class RegisterProviders
{
public function bootstrap(Application $app)
{
$app->registerConfiguredProviders();
}
}
RegisterProvidersクラスのbootstrapメソッドでは、ApplicationインスタンスのregisterdConfiguredProvidersメソッドが実行されており、
public function registerConfiguredProviders()
{
$providers = Collection::make($this->make('config')->get('app.providers'))
->partition(fn ($provider) => str_starts_with($provider, 'Illuminate\\'));
$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
->load($providers->collapse()->toArray());
}
Applicationクラスの該当メソッドを見ると上記のようになっている。
以前はconfig/app.phpに記載されていたコアファサードのエイリアスはLaravel9.xから内部に移動した模様。
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
上記のように呼び出し、、、
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
// aliases => [
// "Illuminate\Foundation\Application" => "app"
// "Illuminate\Contracts\Container\Container" => "app"
// "Illuminate\Contracts\Foundation\Application" => "app"
// "Psr\Container\ContainerInterface" => "app"
// "Illuminate\Auth\AuthManager" => "auth"
//...多いので省略
// ]
//
// 上記のaliasesに$abstractキーが存在すれば、そのaliasを返す。なければ$abstractをそのまま返す。
$abstract = $this->getAlias($abstract);
// イベントハンドラ(raisEvents)が有効な場合に、解決のプロセスを行う前に実行が必要な関数を処理する。
// グローバルなものと、抽象ごとに設定されたものがある。
if ($raiseEvents) {
$this->fireBeforeResolvingCallbacks($abstract, $parameters);
}
// サービスコンテナにコンテクストバインディングが存在すれば返す。
// 通常のバインディングと違って特定のコンテキクストのみで解決されるように定義されている。
$concrete = $this->getContextualConcrete($abstract);
// ビルド(オブジェクトをインスタンス化したものを返却するプロセス)が必要であるかを判定
$needsContextualBuild = ! empty($parameters) || ! is_null($concrete);
// インスタンス化されたものが存在してビルドの必要もない場合は、既存のインスタンスを返す。
// シングルトンオブジェクトの要求である場合はここで終了。
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
// オブジェクトの解決時に必要なパラメータを格納しておく。
$this->with[] = $parameters;
// コンテクストバインディングが解決されなかった場合に、通常のバインディングを解決。
// インスタンスを返す。
if (is_null($concrete)) {
$concrete = $this->getConcrete($abstract);
}
// 具体がビルド可能であるかを判定($concrete===$abstracctであるか※、$concreteがクロージャーである場合※)
// ビルド可能である場合はビルドを行う。
// 具体的には解決先オブジェクトのコンストラクタを参照し、引数にタイプ品ティングがある場合はコンストラクタインジェクションを済ませた状態でインスタンスを生成する。
$object = $this->isBuildable($concrete, $abstract)
? $this->build($concrete)
: $this->make($concrete);
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// オブジェクトがシングルトンとして登録されている、かつ動的なコンテキストがない場合は$instancesにセットすることでキャッシュしておく。
// 再度要求された場合に新たにオブジェクトを生成せずに、キャッシュしたものを返す。
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
// イベントハンドラ(raisEvents)が有効な場合に、実行が必要な関数を処理する。
if ($raiseEvents) {
$this->fireResolvingCallbacks($abstract, $object);
}
// オブジェクトの解決が完了したことを示すためにフラグを立てる。
$this->resolved[$abstract] = true;
// 配列の末尾を取り除くことで、先ほどオブジェクトの解決のためにセットしておいたパラメータを取り除く。
array_pop($this->with);
return $object;
}
流れとしては
- abstractキーに対応するaliasが存在すればaliasを取得。なければabstractをそのまま使う。
- 呼び出し元の抽象に対してコンテクストバインドが存在するかチェック。
- コンテクストビルドが必要であるかを判定
- 抽象に対してサービスコンテナが既にシングルトンインスタンスを以ている場合は、それをそのまま返して終了。
- コンテクストバインディングが存在しない場合に、今度は通常のバインディングをチェック。ここまででresolveメソッドに渡された抽象に対して、解決先の具体が決定している。
- オブジェクトの解決を行う。
- オブジェクトの解決が終わったことを示すフラグを立てる。
buildメソッドでコンストラクタのパラメータを参照し、依存性注入(コンストラクタインジェクション)を済ませたオブジェクトを返却する。
public function build($concrete)
{
// クロージャであれば先ほどの$this->withに格納したパラメータを渡してそのまま実行する。
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
// ReflectionClassを使って解決するオブジェクトの情報を取得。
// 取得できない(そのクラスが存在しない)場合は例外をスロー
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e) {
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
}
// オブジェクトがインスタンス化できない場合(※)はnotinstantiableメソッド経由で例外を投げる。
// 問題なければビルドスタックに突っ込む。
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
// コンストラクタの情報を取得
$constructor = $reflector->getConstructor();
// コンストラクタが存在しない場合はそのままnewして返す。
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
// reflectionClassの機能を使ってコンストラクタのパタメータ情報を依存関係として取得
// この情報にはパタメータの名前や型が含まれている。
$dependencies = $constructor->getParameters();
// 先ほどの依存関係を解決していく
try {
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
}
array_pop($this->buildStack);
// 依存性注入を行なったオブジェクトを返す
return $reflector->newInstanceArgs($instances);
}
※インスタンス化できないオブジェクトの例としては、
- インターフェース
- 抽象クラス
- トレイト
- コンストラクタにprivateアクセス修飾子がついている場合
などがある。
https://www.php.net/manual/ja/reflectionclass.isinstantiable.php
サービスプロバイダとは
一言で言うと「サービスの登録や設定を行うための仕組み」となる。
Laravelの全てのコアサービスや独自のアプリケーション(Composer経由でインストールしたものなど)はサービスプロバイダ経由で登録される。
具体的にはregisterメソッド内でbind
、もしくはsingleton
メソッドを呼び出して特定のサービスやクラスをサービスコンテナに登録している。
以下、実際のソースコードを追ってみる。
'providers' => ServiceProvider::defaultProviders()->merge([
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
])->toArray(),
サービスプロバイダは/config/app.php
のprovidersに列挙されている。
Laravel8以前はコア機能に関するサービスプロバイダも列挙されていたが、9.x以降はvendor内の\Illuminate\Support\DefaultProviders
に移された。
一例として\Illuminate\Auth\AuthServiceProvider
を覗いてみる。
class AuthServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerAuthenticator();
$this->registerUserResolver();
$this->registerAccessGate();
$this->registerRequirePassword();
$this->registerRequestRebindHandler();
$this->registerEventRebindHandler();
}
protected function registerAuthenticator()
{
$this->app->singleton('auth', fn ($app) => new AuthManager($app));
$this->app->singleton('auth.driver', fn ($app) => $app['auth']->guard());
}
... 省略
}
registerメソッドの中ではサービスコンテナに何かを結合することだけを行わなければなりません。イベントリスナやルート、その他のどんな機能もregisterメソッドの中では決して行ってはいけません。これを守らないと、サービスプロバイダがまだロードしていないサービスを意図せず使ってしまう羽目になるでしょう。
https://readouble.com/laravel/10.x/ja/providers.html
この場合はregisterメソッド内で同クラス内の複数のメソッドを呼び出しているが、いずれにしてもサービスコンテナのbindメソッドかsingletonメソッドが呼び出されているだけであり、上記のルールは守られている。
public function registerCoreContainerAliases()
{
foreach ([
'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class],
// ...多すぎるので省略
] as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}
ちなみにアプリケーションのインスタンス生成時に上記のようなエイリアスの登録を行うが、上記は名前を紐づけているだけであり、インスタンスの登録はサービスプロバイダが担っている。