Magentoのdi.xmlを理解しよう〜エクステンション開発上級編その3〜

Magentoのエクステンション開発に関する記事の3本目です。前回はObserverやCron、コマンドラインツールについて解説をしました。今回はこれまでの内容で何度か出てきていながら、あまり深掘りしなかったdi.xmlについて解説したいと思います。

di.xmlってそもそもなんだろう?

Magentoでエクステンションを開発する際に避けては通れないもののひとつがdi.xmlです。di.xmlにはもともと以下のような役割が与えられていて、使い方次第では似たようなコードの量産を避けることもできます。

  • インターフェイスに対する標準の実装クラス名の定義
  • 既存のクラスに対する上書きの宣言
  • 特定のクラスに対するコンストラクタ引数の調整
  • 特定のクラスに対するエイリアスの宣言
  • 特定のクラス・インターフェイスに対するプラグインの定義

今回はこのdi.xmlの仕組みについて深堀りしたいと思います。di.xmlについて理解していると、用途に合わせたクラス間の関係性を定義できるだけでなく、無駄なコードの量産を防ぐこともできます。

di.xmlを構成するタグ

最初に、di.xmlで使えるタグについて把握しておきましょう。di.xmlには、以下のようなタグが利用できます。

タグ名 親要素 子要素 属性
preference
  • config
  • なし
  • for
  • type
type
  • config
  • arguments
  • plugin
  • name
  • shared
virtualType
  • config
  • arguments
  • name
  • type
  • shared
arguments
  • type
  • virtualType
  • argument
  • なし
argument
  • arguments
  • item
  • name
  • xsi:type
item
  • argument
  • item
  • item
  • name
  • xsi:type
plugin
  • type
  • なし
  • name
  • type
  • disabled
  • sortOrder

前回までの記事の中で、typeタグやpluginタグ、preferenceタグは既に登場してきているかと思いますが、今回はその他のタグについても解説をしていきます。

preferenceタグ

基本的な仕組み

preferenceタグは、主にインターフェイスに対する標準の実装クラスの定義を行います。該当のインターフェイス型のインスタンスがMagento内部で要求された場合、自動的にpreferenceタグで指定した実装クラスのインスタンスが得られる仕組みになっています。例えば以下のようなコンストラクタ定義とpreference定義があるとします。

<preference for="My\Example\Api\SamepleInterface" type="My\Example\Model\Sample" />

この場合、Magentoの内部処理では以下のような流れで実際のインスタンスを作成します。

  1. ObjectManagerのcreateメソッドに対して、作成したいインスタンスの型とコンストラクタ引数をセットして呼び出す
  2. ObjectManagerがコンストラクタ定義を確認し、引数でどのような型の変数が要求されているかを調べる
  3. 引数がオブジェクトの場合は、そのクラスのコンストラクタ定義を確認する
  4. コンストラクタで要求されているクラスの型をpreference定義から調べ、実際に生成するクラスの型を判定する
  5. 判明したクラスの型のインスタンスをObjectMangerに対して要求する
  6. 順番に階層を戻り、1のクラスの処理まで戻る
  7. 1で要求されたクラスのインスタンスが戻り値として返る

ObjectManagerというのは、Magentoのフレームワークに用意されているクラスです。ObjectManagerを使うことで、様々なクラスの依存関係が自動的に解決できるようになり、開発者は依存関係に悩まなくて済むようになります。上記の流れでは、4のところで例に示したpreference定義を参照し、指定されたインターフェイスに対する実装クラスを判定します。

なお、Magentoの実装ルールではObjectManagerを直接用いてオブジェクトのインスタンスを取得することは推奨されていません(Magentoのコア実装ではそこそこ使われていますが、エクステンション開発者向けには非推奨になっています)。可能な限り、あるクラスが依存するクラスはコンストラクタの定義に記述して利用することとされています。

preferenceタグを使った既存クラスの上書き

preferenceタグを書く際に、次のようにfor属性の値にインターフェイス名ではなくクラス名を指定することもできます。

<preference for="My\Example\Model\Sample" type="My\NewExample\Model\NewSample" />

この場合、for属性に指定されたクラスのインスタンスが要求されると、ObjectManagerはtype属性に指定されたクラスのインスタンスを返すようになります。こうすることで、既存クラスを継承して実装を調整したクラスを他の処理に使わせることができ、関係するコードを書き換えずにカスタマイズを行えます。なお、preferenceタグによる既存クラスの置き換えについては「できるだけやらないほうが良い」とされています。その理由としては、

  • もともとのクラスの実装がバージョンアップで変わることがある
  • 他のクラスが対象のクラスを利用している場合、不具合の原因になることがある
  • 他のモジュールが同じクラスをリライトしようとして、競合してしまう

これらはMagento1の時代から頻発する問題として認識されてきました。そのため、Magento2ではPluginなどを活用し、できるだけコア実装を改変せずに対処することが推奨されています。

typeタグ

typeタグは、既存のクラスやインターフェイスのコンストラクタ引数に対する調整を行うために使用します。このタグには以下の子要素がXSDで定義されています。

  • arguments
  • argument
  • item
  • plugin

例えば次のようなコンストラクタ定義を持つ\My\Example\Model\Serviceクラスを考えます。

public function __construct(
    \My\Example\Api\Data\SampleInterface $sample,
    \My\Example\Api\SampleRepositoryInterface $sampleRepository,
    array $data
){

この場合、typeタグを使用してコンストラクタ引数の調整を行う場合は、di.xmlに以下のように定義をします。

<type name="My\Example\Model\Service">
    <arguments>
        <argument name="sample" xsi:type="object">My\Example\Model\Sample</argument>
    </arguments>
</type>

上記の例では、引数のひとつである$sampleをdi.xmlで変更しています。他の引数はコンストラクタの定義通りとなり、di.xmlで指定した引数だけが置き換わります。このとき注意しなければならない点としては、「変更前と変更後のクラスが同じ型でなければならない」という点です。ほとんどのコンストラクタ定義ではクラスの型と変数名を記述します。型が宣言されている場合はPHPが与えられた引数の型チェックを行うので、di.xmlで誤って正しくない型を指定してしまうと、エラーが発生します。

次に、コンストラクタ引数が配列だった場合のパターンを考えます。次の例を見てください。

<type name="My\Example\Model\Service">
    <arguments>
        <argument name="data" xsi:type="array">
            <item name="id" xsi:type="string">sample</item>
            <item name="key" xsi:type="const">My\Example\Model\Sample::KEY</item>
            <item name="data" xsi:type="array">
                <item name="key" xsi:type="string">sample2</item>
            </item>
        </argument>
    </arguments>
</type>

配列の場合はargumentタグのxsi:type属性にarrayを指定します。xsi:type属性がarrayの場合は、更に子要素としてitemタグを定義できます。itemタグは配列の要素を定義するために使えるので、配列が二次元三次元であったとしても定義は可能です(めんどくさいですが)。

xsi:type属性とは

argumentタグもitemタグもxsi:type属性の定義が必須です。この属性は、そのタグで指定する値の型を指定するものです。

  • object
  • string
  • const
  • array

が代表的な値として使用できます。constはクラス定数の名前を書きます。stringとobjectは文字列を指定するという点では同じですが、objectの方はObjectManagerを通してオブジェクト型の変数として与えられるのに対し、stringは単なる文字列として与えられます。うっかり引数や配列のデータとしてオブジェクト型を想定しているクラスに対し、文字列を与えてしまうとエラーになってしまいます。もともとのコンストラクタ定義がどうなっているかはdi.xmlで定義をする前によく確認しておきましょう。配列型の場合は配列の構成がどのようになっているかもある程度知っておく必要があります。

virtualTypeタグ

virtualTypeタグは、既存のクラスやインターフェイスに対して別名をつけるために使用します。例えば以下のように定義します。

<type name="My\Example\Model\Sample">
    <arguments>
        <argument name="handle" xsi:type="object">My\Example\Model\Handle\Default</argument>
    </arguments>
</type>

<virtualType name="MyVirtualSample" type="My\Example\Model\Sample">
    <arguments>
        <argument name="handle" xsi:type="object">My\Example\Model\Handle\Custom</argument>
    </arguments>
</virtualType>

name属性の値が別名として扱われます。type属性は既にあるクラス名をネームスペースを含めて指定します。ネームスペースを含めた完全なクラス名が冗長に感じるときは、virtualTypeを使って別名を定義することをおすすめします。それが何度もXML上で出てくるような場合は特に文字数の削減にも繋がります。例を見ていただくとわかると思いますが、virtualTypeタグではもともとのクラスの定義と異なるコンストラクタ引数の定義ができます。もちろん同じ型でなければなりませんが、同じ型であれば自由に入れ替えができます。そして、定義したvirtualTypeは以下のように他のtypeタグやvirtualTypeタグで利用できます。

<type name="My\Example\Model\Service">
    <arguments>
        <argument name="sample" xsi:type="object">MyVirtualSample</argument>
    </arguments>
</type>

上記の例では別のクラスにvirtualTypeで定義したものを与えています。このようにvirtualTypeを活用すると、同じクラスでも構成の違うものを作成でき、設定による処理の切り替えが実現できます。

pluginタグ

以前の記事でもご紹介しましたが、pluginタグを使うと、あるクラスのpublicメソッドに対して以下の3種類の処理を定義できます。

  • before
  • around
  • after

Pluginには優先順位をつけることができるので、どのタイミングで処理を行うかを実装者が調整することができます。preferenceタグによる既存クラスの差し替えよりも、Pluginを活用するほうがモジュール間の競合を調整しやすいので、protectedやprivateメソッドに対する改変でない限りはできるだけPluginを使うほうが望ましいといえます。

di.xmlを定義できる場所

di.xmlは、エクステンションディレクトリ内の以下の箇所で定義ができます。

  • etc
  • etc/adminhtml
  • etc/frontend
  • etc/webapi_rest
  • etc/webapi_soap

Magentoはetc下のdi.xmlを読み込んだあとに、各サブディレクトリ内のdi.xmlを読み込んで定義内容をマージします。管理画面やフロントエンドだけで特定の定義を使用したい場合は、それぞれのディレクトリにdi.xmlを用意して定義すると良いでしょう。

まとめ

今回はdi.xmlについて解説しました。di.xmlは、

  • インターフェイスに対する標準実装クラスの定義ができる
  • クラス・インターフェイスのコンストラクタ引数の調整ができる
  • 既存のクラスに対するエイリアスを定義できる
  • 既存のクラス・インターフェイスのpublicメソッドに対してPluginを定義できる

という役割をになっています。最初はわかりにくい存在ですが、それぞれのタグや属性の使い方を理解すると、Magentoをカスタマイズする際の幅が広がります。

今回で実は最終回

今回まで20回お送りしてきたこの連載ですが、実は今回で最終回になります。
MagentoはPHPのアプリケーションとしてはかなり大きく、複雑な部類にはいります。これまでの記事でご紹介してきた内容はまだほんの一部で、Magentoが持っている様々な機能についてそのすべてをご紹介できていません。

英語ではありますが、公式のDevDocsには概ね最新の情報が掲載されているほか、弊社サイトでも日本語の情報を今後も掲載していきます。この連載を読んでMagentoに興味を持たれた方は、ぜひMagentoのカスタマイズやサイト構築にトライしてみてください。