LoginSignup
23
24

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-31

はじめに

 前回の自分が書いた記事「Laravelのサービスコンテナのバインドと解決の仕組みが知りたい!」で話しきれなかった、singletonメソッドについて今回は説明していきたいと思います。
(今回の記事は前述の記事の続きものとして書いています。もしこの記事の特に後半部をよく理解したければ、前述の記事を読むことをお勧めします。)

環境

ツール バージョン
PHP 7.4.8
Laravel 8.14.0

本題

singletonメソッドの機能について

 singletonメソッドとはbindメソッドの仲間のようなもので、このメソッドの第一引数の文字列を取り、第二引数にクラスやクロージャを取ります。そしてmakeメソッドなどで解決される際はこのメソッドの第一引数に対応したインスタンスかメソッドが返されます。
singletonメソッドとbindメソッドの異なる点は、bindメソッドは解決の際毎回クラスのインスタンス化やクロージャを新たに作るが、singletonメソッドは一回解決したクラスのインスタンスやクロージャの結果を保持し続ける点です。

 bindメソッドとsingletonメソッドの機能の違いを見るため、実際にbindメソッドは文字列BindResultOneBindResultTwoに対して../Model/Test.phpTestクラスをインスタンスとしてバインドし、singletonメソッドは文字列SingletonResultOneSingletonResultTwoに対して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なのでその反対である! $needsContextualBuildtrueとなります。
これにより、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サイトを作成した時にお世話になった記事

23
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
24