2019-06-11

Pimpleコードリーディング

Pimpleのコードリーディングをしました。バージョンは3.2.3です。

このケースで追ってみます

<?php

require_once 'vendor/autoload.php';

use Pimple\Container;

$container = new Container();
$container['session'] = function ($c) {
    return new Session();
};
var_dump($container['session']); // eval
$container['session'] = $container->factory(function ($c) {
    return new Session();
});

コードリーディング

Containerクラス内ではいくつかのインスタンス変数が管理されています。

class Container implements \ArrayAccess
{
    private $values = array();
    private $factories;
    private $protected;
    private $frozen = array();
    private $raw = array();
    private $keys = array();

    public function __construct(array $values = array())
    {
        $this->factories = new \SplObjectStorage();
        $this->protected = new \SplObjectStorage();

        foreach ($values as $key => $value) {
            $this->offsetSet($key, $value);
        }
    }

\ArrayAccessをimplementsしてあるのでこれによって添字アクセスなどが可能になっています。

#offsetSetではvalueskeysに値をセットしています。keysの値はtrueがセットされJavaでいうSetのような感じで扱われています。

    public function offsetSet($id, $value)
    {
        if (isset($this->frozen[$id])) {
            throw new FrozenServiceException($id);
        }

        $this->values[$id] = $value;
        $this->keys[$id] = true;
    }

#offsetGetは以下のように定義されています。初回呼び出し時でClosureをセットしている場合は早期returnされず、$raw($this)でClosureが評価されて評価された値がvaluesに再度セットされます。 さらに、rawにClosure本体がセットされるので、factoryでセットしていなければ二回目以降の呼び出しは最初に生成したインスタンスがそのまま利用されることになります。

    public function offsetGet($id)
    {
        if (!isset($this->keys[$id])) {
            throw new UnknownIdentifierException($id);
        }

        if (
            isset($this->raw[$id])
            || !\is_object($this->values[$id])
            || isset($this->protected[$this->values[$id]])
            || !\method_exists($this->values[$id], '__invoke')
        ) {
            return $this->values[$id];
        }

        if (isset($this->factories[$this->values[$id]])) {
            return $this->values[$id]($this);
        }

        $raw = $this->values[$id];
        $val = $this->values[$id] = $raw($this);
        $this->raw[$id] = $raw;

        $this->frozen[$id] = true;

        return $val;
    }

factoryでセットしている場合はfactoriesというSplObjectStorageのオブジェクトにClosureがattachされます。

    public function factory($callable)
    {
        if (!\method_exists($callable, '__invoke')) {
            throw new ExpectedInvokableException('Service definition is not a Closure or invokable object.');
        }

        $this->factories->attach($callable);

        return $callable;
    }

factoryでセットされた値に対して#offsetGetが呼ばれるとisset($this->factories[$this->values[$id])がtrueになりClosureを評価した値が返されます。 この場合、$rawには何も保存されないため毎回この処理が走るため、結果として呼び出すたびに新しいインスタンスが生成されることになります。

このエントリーをはてなブックマークに追加