Magento1.x の開発に慣れ親しんだ人にとって、Mage クラスはまさに神そのものでした。
とても便利なクラスで、どこからでも呼び出すことができ、しかも Model や Helper、Block といった様々なクラスのインスタンスを生成することができました。私も Magento1.x のコードを書く際は大変重宝していますし、お世話になったクラスです。

ところが Magento2 ではこの Mage クラスは完全に廃止され、 「Dependency Injection (依存性の注入)」という概念が導入されました。
今回はこの新しく導入された概念について解説をしたいと思います。

Magento1.x でのインスタンスの生成

Magento1.x では、PHPの標準的なオブジェクト型変数を生成する以下の書式を使うことは限定的でした。 

$obj = new ExampleClass();

多くのシーンでは以下のように記述し、Model や Helper オブジェクトのインスタンスを取得し、処理を行わせてきました。

$customerModel = Mage::getModel('customer/customer');
$customerHelper = Mage::helper('customer');
$customerSession = Mage::getSingleton('customer/session');

この書式は Mage.php が読み込まれているあらゆる場所で利用することができるので、Magento1.x で開発を行う際にはとりあえず上記の書式を知っていれば希望する Model や Helper をいつでも取得することができました。

Mage クラスの功罪

Mage クラスがあることで、当時としては複雑な Magento1.x をカスタマイズしようという開発者にとっては大きな助けになりました。
単に処理途中のデータをログに記録したい場合でも、

Mage::log('Some Message');

と書くだけでとりあえずは system.log に記録ができるほか、

Mage::logException($e);

とすることで例外が発生した際のスタックトレースを exception.log に記録することができました。
その他様々な機能を Mage クラスは提供してくれたため、まさに Magento1.x の開発者にとっては神様に等しい便利クラスでした。

反面、どのエクステンションからでも任意の Model や Helper を呼び出すことができてしまうため、エクステンション間の依存性や関係性を外部から整理することが難しくなっていきました。
カスタマイズすればするほど導入するエクステンションの数や、独自に作成したコードの量は増えるため、依存性・関係性の整理は重要な要素になります。Magento1.x ではそれが開発ドキュメント化されにくい状態になっており、その原因が Mage クラスにあったと言っても言い過ぎではないかもしれません。

Dependency Injection とは

Dependency Injection、日本語では「依存性の注入」と表現される技術用語です。
何も知らない人が聞くと大変危険なもののように思われるかもしれませんが、そんなことはありません。
詳しくは Wikipedia の解説などを見ていただくと良いでしょう。 Java などでは10年以上前から存在する概念なので、特別新しい概念というわけではありません。

Magento2 における Dependency Injection の実装

Magento2 における Dependency Injection の実装は、「コンストラクタ注入」という方式を採用しています。
まず、Magento1.x のクラス定義の例を見ていただきましょう。

class Foo_Bar_Model_Sample extends Mage_Core_Model_Abstract
{
    protected function _construct()
    {
        $this->_init('bar/sample');
    }
}

基本的に Magento1.x のクラス定義では PHP のコンストラクタを書くことがありません。その理由は、大元のクラスで既にコンストラクタが定義されていて、大半のケースではそれをわざわざ変更する必要性がないからです。

これに対して、Magento2 のクラス定義はというと、以下のようになります。

namespace Foo\Bar\Model;
use Magento\Framework\Locale\Format;

class Sample
{

    /**
     * @var \Magento\Framework\App\ScopeResolverInterface
     */
    protected $_scopeResolver;

    /**
     * @var \Magento\Framework\Locale\ResolverInterface
     */
    protected $_localeResolver;

    /**
     * @var \Magento\Directory\Model\CurrencyFactory
     */
    protected $_currencyFactory;

    /**
     * @param \Magento\Framework\App\ScopeResolverInterface $scopeResolver
     * @param ResolverInterface $localeResolver
     * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory
     */
    public function __construct(
        \Magento\Framework\App\ScopeResolverInterface $scopeResolver,
        \Magento\Framework\Locale\ResolverInterface $localeResolver,
        \Magento\Directory\Model\CurrencyFactory $currencyFactory
    ) {
        $this->_scopeResolver = $scopeResolver;
        $this->_localeResolver = $localeResolver;
        $this->_currencyFactory = $currencyFactory;
    }

}

Magento2 では各クラスは原則としてコンストラクタを定義しなければなりません。
親クラスを継承する際に、全く同じ定義を使用するのであれば不要ですが、親クラスで使用されていないモジュールを使用したい場合は明示的に定義を記述する必要性があります。

言い換えるなら、Magento2 の場合はコンストラクタに正しく記述すれば、様々なモジュールのインスタンスが該当のインスタンスを生成する際に取得できます。
(もちろん全てがそうである、というわけではありませんが)

Magento2 でのインスタンスの生成(ログ記録の例)

さて、Magento2 では Mage クラスが廃止され、あるクラスが必要とする別のクラスのインスタンスが欲しい時はコンストラクタで定義を行うということはご説明しました。
とはいえ、全てを定義することには限界があります。同様に単にログを一時的に記録したいだけなのに大掛かりなコード改造をしなければならないとかという気持ちにもなります。

Magento2 であるオブジェクトのインスタンスをコンストラクタの定義を介さずに行いたい場合は、例えば以下のように書く方法があります。

\Magento\Framework\App\ObjectManager::getInstance()
    ->get('Psr\Log\LoggerInterface')
        ->debug('message');

上記のように書くと、任意の内容をログファイルに記録することができます。
ポイントになるのは、 ObjectManager のインスタンスを取得することです。
Magento2 では、オブジェクトのインスタンスを取得する場合は、ObjectManager を介して行う仕組みになっています。

動作モードに注意

Magento2 のコンストラクタ注入による Dependency Injection 実装は、比較的わかりやすくかつ実用的な実装だと思われます。
PHPという動的型付け言語の特性をうまく使いながら、Magento というアプリケーションに必要な形にまとめています。

ただ、1点注意すべき点があります。
Magento1 と Magento2 の違いとは?」で取り上げたのですが、Magento2 には「動作モード」という概念があります。
もし production モードで Magento2 を動作させる場合は注意が必要です。 production モードでは Dependency Injection を行うための定義データの作成が自動では行われません。必ず実運用に上げる前に準備をする必要があります。

Magento2 には多くのコマンドが用意されていますが、 production モードで Dependency Injection の定義データを更新したい場合は以下のようにする必要があります。

cd /path/to/magento
php bin/magento maintenance:enable
rm -rf var/di
php bin/magento setup:di:compile
php bim/magento maintenance:disable

この一連の手順を踏まずに production モードで Magento2 を動かした場合、Dependency Injection の定義データが更新されません。
新たに追加したエクステンションや、カスタマイズしたコードが上手く動作しない事があります。

まとめ

さて、最後にまとめましょう。
Magento2 では以下のように変更が加えられ、新しい手順も増えました。
これから Magento2 で開発をしていこうと考えている方は、よく覚えておいていただきたいと思います。

  • Magento2 では Magento1.x で重宝した Mage クラスが廃止された。
  • 代わりにコンストラクタ注入による Dependency Injection が導入された。
  • Magento2 の Dependency Injection は ObjectManager を介して行われており、ObjectManager のインスタンスを取得することで、ほかのクラスのインスタンスを取得できる。
  • 動作モードが production の時は、Dependency Injection の定義が更新されないので、所定の手順で更新しなければならない。