MagentoのPluginとProxyを理解しよう〜エクステンション開発上級編その1〜

この連載では、第12回からMagentoのエクステンションの作り方をご紹介してきました。ここまでで基本的なエクステンションの作り方を解説してきましたが、今回からは上級編です。今まではエクステンションの作り方の概略をご紹介してきましたが、上級編ではもう少し突っ込んだエクステンションを作る際のテクニックをご紹介します。そのなかでも今回は「Plugin」と「Proxy」をご紹介します。

Pluginとは?

まず最初はPluginです。
Magento2になって新しく導入された仕組みで、所定の処理に対して事前処理や事後処理を定義できるようになりました。Magento1ではシステム全体で1回しか使えない「リライト」という仕組みで処理の調整をしていましたが、Magento2ではPluginを使うことで、より柔軟に処理の振る舞いを調整することができます。

Pluginが定義できる対象

PluginはMagentoを構成するクラスのpublicメソッドに対して定義できるように設計されています。ただし、一部制約があり、以下のものに対しては定義ができません。

  • final指定されているメソッド
  • final指定されているクラス
  • public指定されていないメソッド
  • static指定されているようなメソッド
  • コンストラクタ
  • di.xmlでVirtual Typeとして定義されているクラス
  • Magento\Framework\Interceptionが実行されるまえにインスタンス化されてしまっているクラス

最後のものが少しわかりにくいかもしれないので、少しだけ説明をしておきます。

Magentoの内部でクラスのインスタンスを取得したい場合、通常はコンストラクタの引数かメソッドの引数として渡します。コンストラクタの引数として定義された場合は、Magentoフレームワーク内でインスタンス生成を担当しているObjectManagerによって自動的に対象クラスのインスタンスが生成され、クラスに渡されるようになっています。そのため、Magentoフレームワークを使用してコードを書いている場合、newを使用してインスタンスを作成してしまうと、Pluginが実行できなくなってしまいます。

データの入れ物に使うようなクラスであればあまり影響はありませんが、何かしらの処理を行うようなクラスの場合は注意が必要です。特別な理由がないのであれば、newを使ってインスタンスを作成しないようにしましょう。

Pluginでできること

Pluginは、1つのpublicメソッドに対して以下の最大3種類の処理を定義できます。

  • before
  • around
  • after

beforeは本来のメソッドの実行前に呼ばれ、メソッドに入る前のパラメータなどを調整できます。afterはbeforeと逆に、メソッドの実行後に呼ばれ、メソッドが処理したあとの結果を調整できます。aroundは本来のメソッドを覆い隠すように実行され、本来のメソッドの実行を省略することもできます。

Pluginの仕組み

Pluginを定義すると、Magentoはgeneratedディレクトリに自動的にクラスを作成します。このとき作成されるクラスはすべてIntercepterという名前で作成されます。中を見てみると、概ね以下のような構造になっています。(クラスによってメソッド名は異なります。例は\Magento\Directory\Model\PriceCurrency\Intercepterです)

/**
 * {@inheritdoc}
 */
public function convert($amount, $scope = null, $currency = null)
{
    $pluginInfo = $this->pluginList->getNext($this->subjectType, 'convert');
    if (!$pluginInfo) {
        return parent::convert($amount, $scope, $currency);
    } else {
        return $this->___callPlugins('convert', func_get_args(), $pluginInfo);
    }
}

ほぼすべてのIntercepterクラスでは、pluginListにはいっている定義済みのPluginリストを調べ、実行できるPluginがある場合は実行しています。

___callPluginsメソッドの定義は、\Magento\Framework\Interception\Intercepterにあり、以下の順番でPluginを実行していきます。

  • before
  • around
  • after

なお、公式devdocsに示されている通り、aroundが途中で定義されている場合は、そのPluginより優先順位の低いプラグインはネストして実行されます(ここは注意が必要なところです)。そのため、aroundについてはできる限り使わないか、元々の処理を完全に置き換えるような場合だけに限るようにするほうが望ましいとされています。

Pluginをいつ使うべきか?

Pluginを使うケースは、以下のようなものでしょう。

  • メソッドの戻り値に追加・変更を加えたいが、処理全体の流れを変えたくはない
  • protectedやprivateメソッドの処理を改変するものではない

Pluginを作成するには

di.xmlに定義を追加

Pluginを定義する場合は、エクステンションのetc/di.xmlに以下のように定義を追加します。

<type name="Magento\Catalog\Model\Product">
    <plugin name="plugin_name" type="My\Extension\Plugin\Product" sortOrder="100"/>
</type>

typeタグにはPluginを定義するクラス名をNamespaceを含めた完全な形で書きます。pluginタグにはname属性にPluginの名前、type属性にはNamespaceを含めたPluginのクラス名を書きます。sortOrder属性は同じクラスに対するPluginの優先順位を定義します。

あとは実際の処理を書いていきます。

Pluginそのものの処理の定義

では、Plugin自体の処理を書いていきましょう。Pluginクラスは他のクラスやインターフェイスを継承する必要はなく、シンプルなPHPクラスとして定義できます。

<?php
namespace \My\Extension\Plugin;

class Product
{


}

まだこの段階では実際のメソッドを書いていないので、クラスの中身は空っぽです。Pluginを定義する場合、どの位置で実行するかによって、定義するメソッドの引数が変わります。Magento\Framework\Interception\Intercepter.phpの___callPluginsの実装を見ると、以下のような違いがあることがわかります。

引数 戻り値
before
  • Plugin定義対象のクラスインスタンス
  • 対象メソッドの引数すべて
  • nullまたはメソッドに与えられた引数と同じ構成の配列
around
  • Plugin定義対象のクラスインスタンス
  • Closureオブジェクト
  • 対象メソッドの引数すべて
  • 元々のメソッドの戻り値と同じ型
after
  • Plugin定義対象のクラスインスタンス
  • Plugin定義対象の戻り値
  • 対象メソッドの引数すべて
  • 元々のメソッドの戻り値と同じ型

今回はafterとaroundを定義してみましょう。今回のPluginは商品クラスに対するものなので、

  • 商品名の改変
  • 商品URLの改変

を実装してみましょう。

afterGetNameの実装

商品名の改変をする処理・afterGetNameを実装する場合、メソッドの定義は以下のように記述します。

public function afterGetName(
                              \Magento\Catalog\Model\Product $subject,
                              $result
) {
    return '[' . $result . ']';
}

この例では、afterGetNameは第1引数に商品オブジェクトを取り、第2引数に結果データを取ります。元々のgetNameには引数がないのでこの例ではここで終わりですが、引数があるメソッドの場合は、元々のメソッドに定義されている引数を列挙していきます。

中の処理では、今回は単に文字列を追加しているだけですが、商品オブジェクトが持っている機能を駆使した処理や、他のクラスをPluginクラスにDependency Injectionすることで様々な処理が実現できます。

aroundFormatUrlKeyの実装

次はaroundです。今回はformatUrlKeyに対して定義するので、以下のように記述します。

public function aroundFormatUrlKey(
    \Magento\Catalog\Model\Product $product,
    \Closure $proceed,
    $urlKey
) {   
    return urlencode($urlKey);
}

aroundの場合は第1引数が元々のオブジェクト、第2引数にメソッド自体が与えられます。$proceedに第3引数以降の値をセットして実行すると、元々の処理を実行できるほか、このPluginより優先度の低い他の処理を実行することができます。(ただし、パフォーマンス劣化が起きます)

例では単純に与えられたURLとなる文字列をURLエンコードして返しています。本来のMagentoの処理ではASCII文字以外を除去して返すのですが、この場合はエンコードしただけの文字列を返します。

Proxyとは?

Magentoでエクステンションを実装する場合に、既存の他のクラスを利用して実装を行うことが多々あります。もちろん既存の安定した処理を利用することは、システムの安定度を高めることにつながるので良い習慣ではありますが、時と場合によってはパフォーマンス劣化をもたらすことがあります。Proxyはパフォーマンス劣化を押さえつつ、既存の部品の再利用をする方法の1つです。

MagentoのDependency Injectionが抱える問題

MagentoのDependency Injectionの仕組みでは、コンストラクタにそのクラスが依存するクラスをすべて記述します。クラスの中には更にその中で多くの依存クラスを持つものが少なくなく、Magentoはクラスのインスタンスが作成される際にこれらの依存関係をすべて再帰的に解決しようとします。勘の良い方であれば予想がつかれるかと思いますが、依存関係によっては非常に大きな計算量になる可能性が潜んでいます。

依存するクラスの中には実際にはある特定のメソッドでしか使われないものがあることは珍しくなく、コンストラクタに全部定義しなければならないMagentoの仕組み上、パフォーマンス劣化の可能性をはらんでいると言っても過言ではありません。

Proxyがもたらす利便性

Proxyはそういったクラス同士の依存関係によるパフォーマンス劣化を回避することを目的としています。Proxyは実際のクラスの代わりにコンストラクタの引数として指定することができ、実際にそのクラスが必要になるときまで、インスタンスの生成を遅延させることができます。遅延させる、ということはそれだけ計算量やメモリ使用量を削減できるわけで、パフォーマンス劣化を防ぐことができるというわけです。

しかも、Proxyはわざわざクラスを定義する必要がありません。Proxyを使用する、とコンストラクタやdi.xmlで宣言するだけで、Magentoのコード生成処理が自動的にProxyクラスを作成してくれます。

Proxyを使用する例

Proxyを使いたい場合は以下のように定義します。

<?php
namespace My\Example\Model;

class MyModel
{

    private $collection;

    public function __construct(
        \Magento\Catalog\Model\Resource\Product\Collection\Proxy $collection
    ) {
        $this->collection = $collection;
    }

}

このように記述すると、generatedディレクトリ下に、Proxyクラスが自動で作成されます。あとはいままでと何ら変わることなく、$this->collectionに対して処理を実行すればOKです。

Proxyクラスの内部では、各メソッドが呼ばれた際に、実際のクラスのインスタンスが既にあるかないかをチェックした上で処理を実行します。(なければインスタンスを作成。あれば再利用します)

Proxyクラス自体は軽量なクラスなので、Dependency Injectionで使用してもあまり負荷にはなりません。

まとめ

今回は「Plugin」と「Proxy」をご紹介しました。
エクステンションを開発する上では、Magentoのコア実装や、他社のエクステンションの「ここをどうにかしたい」というシーンが多々でてきます。Pluginはそういった問題に対処するためにMagentoが用意している仕組みの一つです。コアハックは禁じ手なので、できるだけMagentoフレームワークに用意されている正しいカスタマイズ方法でカスタマイズするようにしましょう。

また、Proxyを使うことでパフォーマンス劣化を防ぐことができます。どうしてもMagentoはメモリ消費やCPU負荷の高いアプリケーションなので、無駄な処理はできるだけ減らす必要があります。クラスの中でほんの少ししか使わない依存クラスであっても、コンストラクタに定義しなければならいという仕様は無駄の生まれやすい状況にあります。無駄をできるだけ省き、より速く・省メモリな処理を書くようにしましょう。

次回は特定の箇所で処理にフックを掛けるObserverと、定時バッチ、コマンドラインツールについて解説します。