#はじめに
前回の自分が書いた記事「Laravelのサービスコンテナのバインドと解決の仕組みが知りたい!」で話しきれなかった、singletonメソッドについて今回は説明していきたいと思います。
(今回の記事は前述の記事の続きものとして書いています。もしこの記事の特に後半部をよく理解したければ、前述の記事を読むことをお勧めします。)
##環境
ツール | バージョン |
---|---|
PHP | 7.4.8 |
Laravel | 8.14.0 |
#本題
##singletonメソッドの機能について
singletonメソッドとはbindメソッドの仲間のようなもので、このメソッドの第一引数の文字列を取り、第二引数にクラスやクロージャを取ります。そしてmakeメソッドなどで解決される際はこのメソッドの第一引数に対応したインスタンスかメソッドが返されます。
singletonメソッドとbindメソッドの異なる点は、bindメソッドは解決の際毎回クラスのインスタンス化やクロージャを新たに作るが、singletonメソッドは一回解決したクラスのインスタンスやクロージャの結果を保持し続ける点です。
bindメソッドとsingletonメソッドの機能の違いを見るため、実際にbindメソッドは文字列BindResultOne
、BindResultTwo
に対して../Model/Test.php
のTest
クラスをインスタンスとしてバインドし、singletonメソッドは文字列SingletonResultOne
、SingletonResultTwo
に対してbindメソッドと同じクラスをインスタンスとしてバインドし、解決した時の結果について比べていきます。
<バインドするクラス>
<?php
namespace Model;
class Test
{
protected $nameList = ['巨人', '阪神', '中日', 'ヤクルト', 'DNA', '広島'];
public function __construct()
{
$this->nameList[] = $nameList;
$this->winner = $this->nameList[mt_rand(0, 5)];
$this->looser = $this->nameList[mt_rand(0, 5)];
}
public function pennant()
{
echo "今年のペナントレースの順位は?\n";
echo "1位は".$this->winner."\n";
echo "最下位は".$this->looser."\n";
}
}
?>
<バインドと解決>
<?php
require 'autoload.php'; //オートローダ
$app = new App\Application(); //サービスコンテナの読み込み
//バインド
$app->bind('BindResult', Model\Test::class);
$app->singleton('SingletonResult', Model\Test::class);
//bindメソッドを解決
$bindResultOne = $app->make('BindResult');
$bindResultTwo = $app->make('BindResult');
//singletonメソッドを解決
$singletonResultOne = $app->make('SingletonResult');
$singletonResultTwo = $app->make('SingletonResult');
//インスタンス内のメソッドを行う
$bindResultOne->pennant();
$bindResultTwo->pennant();
$singletonResultOne->pennant();
$singletonResultTwo->pennant();
//それぞれのオブジェクトの中にあるインスタンスの値について調べる
var_dump(boolval($bindResultOne === $bindResultTwo));
var_dump(boolval($singletonResultOne === $singletonResultTwo));
?>
結果
今年のペナントレースの順位は? 1位は中日 最下位は阪神 今年のペナントレースの順位は? 1位はヤクルト 最下位はDNA 今年のペナントレースの順位は? 1位は阪神 最下位は巨人 今年のペナントレースの順位は? 1位は阪神 最下位は巨人 bool(false) bool(true)
つまり、bindメソッドでバインドしたインスタンスの値はそれぞれ異なるのに対し、singletonメソッドでバインドしたインスタンスの値は同じである、といえます。
次にsingletonメソッドはどのような仕組みになっているのかを見ていきます。
##singletonメソッドの仕組み
そもそもsingletonメソッドのコードはというと、こんな感じです。
singletonメソッド
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
同クラス内のbindメソッドの第三引数にtrue
が渡されることにより、$this->bindings[$abstract][shared] = true
となります。
bindメソッド
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
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);
}
$this->bindings[$abstract] = compact('concrete', 'shared'); //`shared`の値がtrueに
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
こうなることで、前回の記事で説明した解決を行うmakeメソッドの解決で大きな役割を担うresolveメソッド(1)~(10)のなかで前回説明を省略した(3)、(8)の部分である変化が生まれます。
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;
}
まず(9)の変化についてみていきます。
$this->isShared($abstract)
は$this->instances[$abstract]
がセットされているか、$this->bindings[$abstract][shared]
がセットされていてかつtrue
の場合にtrue
を返します。もし、bindメソッドで$this->bindings[$abstract][shared] = true
となっている場合は、前者の条件は満たしていませんが、後者の条件を満たしているため、$this->isShared($abstract)
はtrue
を返します。
次に$needsContextualBuild
についてですが、これは前回resolveメソッド(2)で説明したように大抵false
なのでその反対である! $needsContextualBuild
はtrue
となります。
これにより、ifの条件式がtrue
の場合の処理である、
$this->instances[$abstract] = $object(buildメソッドによって作られた、$abstractに対応するインスタンスかクロージャ)
を行います。つまり(9)によって行われたことは、
$this->instances
という配列に$abstaract
をキーとし、singletonメソッドの第二引数に渡されたクラスのインスタンスまたはクロージャをセットするということです。
次に(4)の変化についてみていきます。
もしsingletonメソッドでバインドしたインスタンスまたはクロージャが一度しか解決されない場合は、(4)では特に何も起こりません。しかし、二度以上解決解決される場合は(9)によって$this->instances[$abstract]
はセットされているためisset($this->instances[$abstract]
はtrue
となり、! $needsContextualBuild
は前述の理由によりtrue
となります。したがって、ifの条件式がtrue
の場合の処理が行われます。true
の場合の処理は$this->instances[$abstract]
の値を返すことなので、これによりもしsingletonメソッドでバインドされたインスタンスまたはクロージャが二度以上解決される場合は、常に一度目に解決した時に返された値を返すようになります。つまり(4)でおこなわれたことは、
singletonメソッドでバインドされたインスタンスかクロージャが二度以上解決される場合、常に(9)でセットされたインスタンスまたはクロージャの値を返すということです。
#終わりに
今回は前回説明しきれなかったsingletonメソッドについて説明しました。一度常に同じ値のインスタンスやクロージャを使いまわせるので、とても便利ですね。
他の記事も書いてますので、そちらもどうぞ~
Laravelのサービスコンテナのバインドと解決の仕組みが知りたい!
始めてWebサイトを作成した時にお世話になった記事