はじめに
Laravelについて勉強しているなか、自分はふと
「Laravelでは、bindとかmakeを使って手軽にインスタンスの受け渡しやインスタンスの利用ができて便利だけど、その仕組みはどうなっているんだろ?」
と感じました。
そこで、実際にbindなどのバインドの仕組みとmakeなどの解決の仕組みについてLaravelのコードを見て調べてみました!
本題
環境
ツール | バージョン |
---|---|
PHP | 7.4.8 |
Laravel | 8.14.0 |
バインドと解決の大まかな仕組み(というかこの記事の結論)
バインド(というかbindメソッド)の大まかな仕組みは、
bindメソッドの第一引数をキーとして、第二引数をクラスのインスタンスやクロージャとなるキーの値としてLravelであらかじめ用意された配列にセットする
というものです。
そして解決(というかmakeメソッド)の大まかな仕組みは、
bindメソッドでセットした配列のキーと同じ値がmakeメソッドの第一引数にセットされたとき、そのキーに対応するインスタンスやクロージャを返す
というものです。
コンテナのバインドと解決の流れを図にするとこんな感じです。
今度は実際のLaravelのコードを見て、バインドの仕組みについてみていきたいと思います。
バインドの仕組みについて
実際のbindメソッドのコードはこのようになっています。ちなみにbindメソッドは、Laravelのサービスコンテナの実態であるApplication.phpの親クラスであるContainer.phpにあります。
bindメソッド
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract); //.....(1)
if (is_null($concrete)) {
$concrete = $abstract;
}
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);
} //.....(2)
$this->bindings[$abstract] = compact('concrete', 'shared'); //.....(3)
if ($this->resolved($abstract)) {
$this->rebound($abstract);
} //.....(4)
}
ポイントを(1)~(4)に分けて説明します。
(1)では、bindメソッドの第一引数が、同クラス内のinstanceメソッドやaliasメソッドなどによりセットされた配列のキーとして含まれていた場合、そのキーの要素を消去します。
(2)では、もし$concrete
の値がクロージャでなかった場合(クラスである場合)、同クラス内のgetClousureメソッドによりその値はインスタンス化されます。(getClousureメソッドはメソッド内のbuildメソッドやmakeメソッドによりクラスをインスタンス化していますが、これらのメソッドについては後程取り上げるので説明は省略)
もし$concrete
の値が文字列データ出ない場合はこの時点で例外処理が行われます。
(3)がこのbindメソッドでやる最も重要なことかなと思います。なぜなら、このひとつ前の章(バインドと解決の大まかな仕組み)で言っていた、あらかじめ用意された配列
とは$this->bindings
のことだからです。
この配列にbindメソッドの$abstract
の値をキーとして、$concrete
の値をインスタンスやクロージャとしてセットします。
($shared
はデフォルトではfalseとなり特に何の効果も生みませんが、singletonメソッドでバインドされた際はtrueとなり、ある効果を生み出しますが本筋と離れるので詳しい説明は省きます。また、PHPのマジックメソッドであるcompactメソッドは変数名をキー、値をそのキーの値として配列を作ります。詳しくはこちら。)
<追記 今回説明しきれなかった、singletonメソッドについての記事上げました。こちらも是非!
Laravelのsingletonメソッドの機能とその仕組みについて>
(4)では、もし$this->bindings
のキーに対応するインスタンス又はクロージャが解決済みの場合(resolveメソッドにより解決済みかどうかが判断できる)、同クラス内のrebindingメソッドによりセットされた配列の同名のキーの値である関数が行われる。
(いつrebindingメソッドが行われるか、なぜこのメソッドが必要なのかわかっていません。わかる方、コメントで教えてください!)
解決(makeメソッド)の仕組みについて
解決の流れはLaravelのサービスコンテナの実態であるApplication.phpのmakeメソッドを経て親クラスのContainer.phpのmakeメソッドが実行されますが、Application.phpのmakeメソッドについて説明すると本筋と離れるのでこの記事ではContainer.phpの方だけを取り扱います。
makeメソッド
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
makeメソッドの第一引数に解決したい値、第二引数にその値のメソッドの引数(なければnull)を入れ、それらの引数を基に同クラス内のresolveメソッドが動きます。
単純な作りで良いですね。
resolveメソッド
解決の機能の大部分を担っているのがresolveメソッドです。
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract); //.....(1)
$concrete = $this->getContextualConcrete($abstract); //...(2)
$needsContextualBuild = ! empty($parameters) || ! is_null($concrete);
//.....(3)
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
} //.....(4)
$this->with[] = $parameters; //.....(5)
if (is_null($concrete)) {
$concrete = $this->getConcrete($abstract);
} //.....(6)
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
} //.....(7)
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
} //.....(8)
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
} //.....(9)
if ($raiseEvents) {
$this->fireResolvingCallbacks($abstract, $object);
} //.....(10)
$this->resolved[$abstract] = true; //.....(11)
array_pop($this->with);
return $object;
}
resolveメソッドの機能は、このメソッドの第一引数である$abstract
の値とbindメソッドで作成した$this->bindings
のキーの値が同じとき、そのキーに対応したインスタンスやクロージャが返される、というものです。でもこれを言うだけじゃ全コード載せた意味がないので、(1)~(10)に分けてもう少し細かく見ていきます。
(1)では、$abstract
の値がaliasメソッドなどでセットされる$this->alaiases
という配列のキーに含まれていない場合、$abstract
の値はそのままです。もし含まれていた場合はエラーを吐きます。
(2)$abstract
の値が同クラス内のaddContextualBindingメソッドでセットされる$this->contextual
のキーとしてセットされている場合は$concrete
にその値が入れられ、それ以外はnull
が入れられます。
(3)では、$parameters
の値が存在しないかつ$concrete
の値がnull
の時以外は$needsContextualBuild
の値はtrueになります。(たいていはfalseになります。というかなぜ$this->contextual
が存在するのか自分では理解していません。わかる方はぜひ教えてほしいです。)
(4)についてですが、ここの機能はsingletonメソッドを使用する際に使われるですので今回は説明を省きます。
(5)では、$parameters
の値が$this->with
という配列にセットします。this->with
はbuildメソッドで使います。そしてresolveメソッドの最後に$this->with
の値はリセットされます。
(6)では、(2)の結果がnull
の場合、$concrete
に同クラス内のgetConcreteメソッドの結果をセットします。getConcreteメソッドの機能は、resolveメソッド内の$abstract
の値と同じ$this->bindings
のキーのインスタンス又はクロージャを返す、というものです。また、それができなければgetConcreteメソッドの引数(ここでは$abstract
)の値をそのまま返します。
(7)では、まず同クラス内のisBuildableメソッドによって処理の分岐が行われます。isBuildableでは、もし$abstract
と$concrete
の値が同じか$concrete
の値がクロージャであった場合、tureを返しそうでない場合はfalseを返します。
もしtrueであった場合は$object
の値がbuildメソッドの結果となります(buildメソッドについては後程取り上げます)。
もしfalseであった場合は第一引数を$concrete
の値にしてresolveメソッド(makeメソッドはresolveメソッドを引き起こしているだけ)を行います。これにより今度は$abstract
の値と$concrete
の値が同じになり、trueの場合と同じことが行われます。
(8)では、もし同クラス内のextendメソッドによりセットされた$this->extenders
の配列のキーと$abstract
の値が同じ場合は、そのキーに対応したクロージャを$object
の値にセットします(extendメソッドの使われ方は理解できていません。わかる方教えてください)。
(9)は、singletonメソッドに関係するところなので、説明を省きます。
(10)では、同クラス内のresolvingメソッドで$this->globalResolvingCallbacks
にクロージャがセットされていたらそのクロージャを実行します。セットされていなければ何もしません。
(11)では、$this->resolved
の配列の$abstract
の値がキーとなっている値にtrue
をセットします。これにより、$abstract
の値を解決しようとしたときにすでにその値が解決済みかどうかがわかります。
最後に$this->bindings[$abstract]
に対応したインスタンスかクロージャを返します。
buildメソッド
buildメソッドはbindメソッドでセットした$this->binding
のキーに対するクロージャやインスタンスを用意してくれる、サービスコンテナの中で重要な役割を担うメソッドです。
これがなければ解決どころかバインドすらも成立しません。
public function build($concrete)
{
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
} //.....(1)
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e) {
throw new BindingResolutionException("Target class [$concrete]
does not exist.", 0, $e);
} //.....(2)
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
} //.....(3)
$this->buildStack[] = $concrete; //.....(4)
$constructor = $reflector->getConstructor(); //.....(5)
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
} //.....(6)
$dependencies = $constructor->getParameters(); //.....(7)
try {
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
} //.....(8)
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances); //.....(9)
}
buildメソッドは渡される引数によって中で起こることが変わります。
(1)は、もしbuildメソッドに渡された引数がクロージャである行われます。というかクロージャの場合は(1)で終了です。クロージャが返されるときに引数にとる$this->getLastParameterOverride()
の値はmakeメソッドやresolveメソッドの第二引数であるarray $parameters
の値となります。なお、返り値のクロージャの第一引数は絶対$this(Applicationクラスをインスタンスにとるオブジェクト。大抵は$app
)となるので、もしクロージャに値を受け渡したいときは、bindメソッドでセットするクロージャの第二引数に受け渡したい値をセットしましょう。
(2)のReflectionClass
はPHP5以降からPHPにあらかじめセットされたクラスで、使用したいクラスにuse ReflectionClass;
と入力することで使えるようになります(詳しくはこちら)。
ReflectionClass
に組み込まれているメソッドを使うことで、引数に指定されているクラスの様々な情報が分かったり、そのクラスの数値を取り出すこともできます。もしこの時にクラスが存在しないなどのエラーが出た場合は例外処理が行われます。
(3)ではReflectionClass
の引数のクラス(これからは$concrete
)がインスタンス化できるものかを調べ、できなければエラーを吐きます。
(4)の$this->buildStack
は、buildメソッドが行われている間$concrete
の値を他のメソッドに受け渡す際に使われます。buildメソッドの終了時に$this->buildStack
の値はリセットされます。
(5)では、もし$concrete
がコンストラクトメソッドを含む場合に、それを取得します。(詳しく言うと、$constructor
にReflectionMethod
のオブジェクトをセットします。もしコンストラクトメソッドがなければnull
をセットします。ReflectionMethod
はReflectionClass
と同じようにPHP5以降から元からセットされたクラスで、$concrete
のメソッドについて調べます。詳しくはこちら。)
(6)ではもし$concrete
にコンストラクトメソッドがなければ、$concrete
をインスタンスとして返します。
(7)では、$concrete
のコンストラクトメソッドの引数の値を$dependencies
にセットします。
(ここでも詳しく言うと、ここでは$dependencies
にReflectionParameter
のインスタンスをセットします。ReflectionParameter
はReflectionClass
と同じように以下略。$concrete
のメソッドの引数について調べます。詳しくはこちら。)
(8)では、同クラス内のresolveDependencies
メソッドを使用し、もし調べる対象の$concrete
のメソッドに値入りの引数があればその値を、タイプヒント付きの引数があればタイプヒントに表記されたクラスのインスタンスを$instances
にセットします。(そのとき事前にタイプヒントとして表記されたクラスがバインドされていなければならない)もしここでエラーが発生した場合は$this->buildStack
を解放したうえで、例外処理が行われます。
(9)では$concrete
にコンストラクトメソッドに入る引数(なければ空)をセットし、$concrete
のインスタンスを返します。
(newInstanceArgs
メソッドはReflectionClassに組み込まれたメソッドです。詳しくはこちら。)
実際に作って動かしてみよう!
まず作ろう
ここまで調べてくると、今まで調べたクラス、メソッドをコピペし、うまく組み合わせればフレームワーク要らずでバインドと解決の機能を再現できるんじゃね、と思い作ってみました。
実際に(コピペして)作ったコードがこちら
<?php
namespace App; //ここはどうでも良いよ
use ReflectionClass;
use Closure;
use ReflectionParameter;
/**
*
*/
class Application
{
protected $bindings = [];
protected $instances = [];
protected $resolved = [];
protected $abstractAliases = [];
protected $contextual = [];
protected $buildStack = [];
protected $with;
public function bind($abstract, $concrete = null, $shared = false)
{
if (is_null($concrete)) {
$concrete = $abstract;
}
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
}
protected function getClosure($abstract, $concrete)
{
return function ($container, $parameters = []) use ($abstract, $concrete) {
if ($abstract == $concrete) {
return $container->build($concrete);
}
return $container->make($concrete, $parameters);
};
}
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
public function resolve($abstract, $parameters = [])
{
$needsContextualBuild = ! empty($parameters);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
public function build($concrete)
{
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
if (! $reflector->isInstantiable()) {
//return $this->notInstantiable();
echo "Error";
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
protected function getConcrete($abstract)
{
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}
public function isShared($abstract)
{
return isset($this->instances[$abstract]) ||
(isset($this->bindings[$abstract]['shared']) &&
$this->bindings[$abstract]['shared'] === true);
}
protected function getLastParameterOverride()
{
return count($this->with) ? end($this->with) : [];
}
protected function notInstantiable()
{
$message = 'Error!! This Class is not instantiable.';
throw new Exception($message);
}
protected function resolveDependencies(array $dependencies)
{
$results = [];
foreach ($dependencies as $dependency) {
if ($this->hasParameterOverride($dependency)) {
$results[] = $this->getParameterOverride($dependency);
continue;
}
$results[] = is_null($dependency->getClass())
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
}
return $results;
}
protected function hasParameterOverride($dependency)
{
return array_key_exists(
$dependency->name, $this->getLastParameterOverride()
);
}
protected function resolvePrimitive(ReflectionParameter $parameter)
{
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
$this->unresolvablePrimitive($parameter);
}
protected function unresolvablePrimitive(ReflectionParameter $parameter)
{
$message = "Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}";
echo $message;
}
protected function resolveClass(ReflectionParameter $parameter)
{
return $this->make($parameter->getClass()->name);
}
}
?>
Laravelで実際に書かれているコードと比べるとだいぶ少ない分量でいけました。
動かそう
次に今回のように調べる&コピペで作ったオートローダ(その時の記事はこちら)と共に動かしていきます。
動作の結果は、ブラウザ上で確認します。
ちなみにファイルの構造はこんな感じ(白丸は黒丸の中にあるファイル)
<アプリケーション内>
- app
- Application.php(バインドと解決)
- Model(このフォルダは実行したいクラスが入る)
- First.php
- Second.php
- Third.php
- app.php(Laravelのbootstrap\app.phpのポジション)
- index.php(Laravelのpublic\index.phpのポジション。こいつの表示を見て動いているかどうかを見る。)
- autoload_real.php(これ以下のファイルはオートロードの機能を担うがオートロードは今回の本筋ではないため紹介しない)
- autoload_static.php
- autoloas.php
- ClassLoader.php
実行するクラス
<?php
namespace Model;
/**
*
*/
class First
{
public function copy()
{
echo "耳コピでしゅか?".nl2br("\n"); //nl2br()はブラウザ上で文字を改行するため
}
public function pai()
{
echo "じゃあだれが一体パイを焼くんだい?";
}
}
?>
<?php
namespace Model;
/**
*
*/
class Second extends First
{
public function __construct()
{
$this->comment = "キャー、のび太さんの".nl2br("\n");
}
public function comment()
{
echo $this->comment."エッジのきいたカッティングリフー!".nl2br("\n");
}
}
?>
<?php
namespace Model;
/**
*
*/
class Third
{
public function __construct(First $first)
{
$this->first = $first;
}
public function end()
{
$this->first->pai();
}
}
?>
ひたすらバインドを行うクラス
<?php
$app = new App\Application();
$app->bind('Teacher', function ()
{
echo "コピーよろしく".nl2br("\n");
});
$app->bind('Nobita', Model\First::class);
$app->bind('Shizuka', Model\Second::class);
$app->bind('End', Model\Third::class);
return $app;
?>
結果をブラウザに写すクラス(解決を行うクラス)
<?php
require 'autoload.php'; //自作のコピペオートローダの読み込み
$app = require_once __DIR__.'/app.php'; //自作のコピペサービスコンテナの読み込み
$app->make('Teacher');
$answer = $app->make('Nobita');
$answer->copy();
$comment = $app->make('Shizuka');
$comment->comment();
$ending = $app->make('End');
$ending->end();
?>
結果
コピーよろしく 耳コピでしゅか? キャー、のび太さんの エッジのきいたカッティングリフー! じゃあだれが一体パイを焼くんだい?
終わりに
他の人に自分の調べたことを伝えることよりも自分の調べたことを言葉でまとめることに意識がいってしまい、わかりにくいところが多い記事になっていると思います。読んでくれた方ありがとうございます、そしてすいません。
フレームワークについて調べると、そのフレームワークの理解が深まるだけではなく、言語自体の理解も深まることもあり、面白かったです。singletonメソッドなどまだまとめきれなかった項目が残っているので、今後これらも取り上げられたら良いなと思っています。
今回説明しきれなかった、singletonメソッドについての記事上げました。こちらも是非!
Laravelのsingletonメソッドの機能とその仕組みについて
こんな記事も書いてます!良かったらぜひ~
LaravelのCSRF対策の処理を実際のコードから見てみる