この記事はMagento Advent Calendar 2019の1日目です。

プログラムを書いていると、既存の処理に対して少し形を変えた処理を書く必要に迫られることが多々あります。 例えば、HTTPリクエストのヘッダをチェックする処理を書くとします。具体的な実装を見ながら考えてみることにしましょう。 MagentoにバンドルされているGraphQLモジュールに例があるので、こちらを読んでいくことにします。

di.xmlの定義とクラスの定義

Magento/Graphql/etc/graphql/di.xml には以下のような宣言があります。

<type name="Magento\GraphQl\Controller\HttpRequestProcessor">
    <arguments>
        <argument name="requestValidators" xsi:type="array">
            <item name="ContentTypeValidator" xsi:type="object">Magento\GraphQl\Controller\HttpRequestValidator\ContentTypeValidator</item>
            <item name="VerbValidator" xsi:type="object">Magento\GraphQl\Controller\HttpRequestValidator\HttpVerbValidator</item>
        </argument>
    </arguments>
</type>

これだけでは何のことかわからないので、HTTPRequestProcessorの実装を見てみましょう。
2.3.3では以下のように書かれています。 

/**
 * @param HttpHeaderProcessorInterface[] $graphQlHeaders
 * @param HttpRequestValidatorInterface[] $requestValidators
 */
public function __construct(array $graphQlHeaders = [], array $requestValidators = [])
{
    $this->headerProcessors = $graphQlHeaders;
    $this->requestValidators = $requestValidators;
}
/** * Process the headers from a request given from usually the controller * * @param Http $request * @return void */ public function processHeaders(Http $request) : void { foreach ($this->headerProcessors as $headerName => $headerClass) { $headerClass->processHeaderValue((string)$request->getHeader($headerName)); } }
/** * Validate HTTP request * * @param Http $request * @return void */ public function validateRequest(Http $request) : void { foreach ($this->requestValidators as $requestValidator) { $requestValidator->validate($request); } }

この実装のパターンでは、di.xmlで定義したrequestValidatorsがそのままHTTPRequestProcessorのrequestValidatorsに渡される仕組みになっています。 di.xmlで宣言する際にxsi:type=“object”を指定しておくと、ObjectManagerがそのクラス名(あるいはインターフェイス型)のインスタンスを作成してくれます。

この方法の長所と短所

この実装のやり方を使うと、同じインターフェイスを実装したクラスであればrequestValidatorとして利用でき、クラスを定義してdi.xmlに追記するだけで処理の差し替えができてしまいます。
di.xmlが持っているvirtualType定義などを活用すれば、同じクラスでも異なる構成の定義が作れるので、比較的簡単に処理の差し替えもできるようになります。

短所としては、利用する側のオブジェクトのインスタンスを生成する際に、関連するすべてのインスタンスが作成されてしまう点があります。
今回の例では2つだけなのでさほど大きなコストにはなりませんが、もっと数が増えてくるとミリ秒単位で無駄が出てきてしまいます。

そこでMagentoフレームワークに含まれているTMapの出番です。 

TMapとは

TMapは、Magentoフレームワークに含まれているクラスで、ObjectManagerパッケージの中に入っています。 要はObjectManagerの関連クラス、というわけです。 通常は同じ場所にあるTMapFactoryを通してインスタンスを作成して使います。 例えば下記のように使います。

public function __construct(
    TMapFactory $tmapFactory,
    array $service = []
)
{
    $this->tmapFactory = $tmapFactory;
    $this->handlers = $tmapFactory->create(
        [
            'array' => $service,
            'type' => HttpRequestValidatorInterface::class
        ]
    );
}

この例では、TMapFactoryクラスのcreateメソッドに2要素の配列を与え、TMapクラスのインスタンスを作成しています。
TMapクラスはコンストラクタに与えられたarrayに入っている内容が、typeで与えられた型であるかをチェックしながらインスタンス生成を行います。
TMapを使う場合は、di.xmlに書く内容も少し変わります。例えば先程の定義であれば、

<type name="Magento\GraphQl\Controller\HttpRequestProcessor">
    <arguments>
        <argument name="requestValidators" xsi:type="array">
            <item name="ContentTypeValidator" xsi:type=“string">Magento\GraphQl\Controller\HttpRequestValidator\ContentTypeValidator</item>
            <item name="VerbValidator" xsi:type=“string">Magento\GraphQl\Controller\HttpRequestValidator\HttpVerbValidator</item>
        </argument>
    </arguments>
</type>

というようにobjectをstringに書き換えることができます。

たったこれだけしか違いはないのですが、TMapを使用する場合は「インスタンスの生成を遅延させる」ことができます。
どういうことなのかTMapの実装を深堀りしてみましょう。

TMapのコンストラクタ実装

TMapのコンストラクタを見ると、以下のように書かれています。

    /**
     * @param string $type
     * @param ObjectManagerInterface $objectManager
     * @param ConfigInterface $configInterface
     * @param array $array
     * @param \Closure $objectCreationStrategy
     */
    public function __construct(
        $type,
        ObjectManagerInterface $objectManager,
        ConfigInterface $configInterface,
        array $array = [],
        \Closure $objectCreationStrategy = null
    ) {
        if (!class_exists($this->type) && !interface_exists($type)) {
            throw new \InvalidArgumentException(sprintf('Unknown type %s', $type));
        }

        $this->type = $type;

        $this->objectManager = $objectManager;
        $this->configInterface = $configInterface;

        array_walk(
            $array,
            function ($item, $index) {
                $this->assertValidTypeLazy($item, $index);
            }
        );

        $this->array = $array;
        $this->counter = count($array);
        $this->objectCreationStrategy = $objectCreationStrategy;
    }

TMapのコンストラクタでは、引数に渡された値はarray_walkを通じて一旦評価されますが、結局はそのまま変数に格納されます。asserValidTypeLazyは単に型チェックをしているだけなので、実際にはインスタンスの生成は行っていません。

実際のインスタンス生成はどこで行っているのか?

更にコードを読んでいくと、getIteratorというメソッドに行き着きます。
この実装は以下のようになっています。

    /**
     * {inheritdoc}
     */
    public function getIterator()
    {
        if (array_keys($this->array) != array_keys($this->objectsArray)) {
            foreach (array_keys($this->array) as $index) {
                $this->initObject($index);
            }
        }

        return new \ArrayIterator($this->objectsArray);
    }

この中にinitObjectがあります。このメソッドが実際のインスタンス生成を行っています。つまりTMapを使う場合はTMapが持っている要素にアクセスする際に、実際のインスタンスが生成され、返却されます。

TMapを使うメリットとデメリット

getIteratorの実装にある通り、TMapを使えばインスタンスの生成タイミングを本当に使う直前まで遅延させることができます。
最初に示した例との違いはここにあって、同じような処理を実装する場合であっても関連するクラスが多いような場合はこちらのほうが初期化時のコストを下げることができます。

ただし、TMapを使う場合はTMapFactoryとTMapをセットで使わないといけないので、余分なコードが増えてしまいます。
初期化コストが低い場合はTMapを使わないほうが良いかもしれません。

ケース・バイ・ケースで使い分け、シンプルでわかりやすい構造の処理を実装するようにしたいものです。

明日は2日目。kzkick2ndさんによる「Magento2 の日本語翻訳を変更する2つの方法」です。