Laravelのsingletonメソッドの機能とその仕組みについて

#はじめに
　前回の自分が書いた記事「[Laravelのサービスコンテナのバインドと解決の仕組みが知りたい！](https://qiita.com/chess-san/items/a08f200614718315a63b)」で話しきれなかった、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メソッドと同じクラスをインスタンスとしてバインドし、解決した時の結果について比べていきます。

＜バインドするクラス＞

```../Model/Test.php
<?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";
    }
}

?>
```

＜バインドと解決＞

```index.php
<?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メソッド**

```Container.php
public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }
```

同クラス内のbindメソッドの第三引数に`true`が渡されることにより、`$this->bindings[$abstract][shared] = true`となります。

**bindメソッド**

```Container.php
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メソッド**

```Container.php
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のサービスコンテナのバインドと解決の仕組みが知りたい！](https://qiita.com/chess-san/items/a08f200614718315a63b)
[始めてWebサイトを作成した時にお世話になった記事](https://qiita.com/chess-san/items/85b159640e64f8ddd664)