Magento2 Plugins入門


この記事は[Magento Advent Calendar 2017](https://adventar.org/calendars/2349)の12日目です。

## はじめに

Magento2では各処理の前後に条件付きですが、新しく別の処理を差し込むことが可能です
公式ドキュメント: [Plugins (Interceptors)](http://devdocs.magento.com/guides/v2.2/extension-dev-guide/plugins.html)
上記ドキュメントの[limitations](http://devdocs.magento.com/guides/v2.2/extension-dev-guide/plugins.html#limitations)に制約が書いてありますが、publicなメソッドならだいたいなんとかなると思います

今回は商品CSV取込`Magento\CatalogImportExport\Model\Import\Product`に処理を差し込んでみます

## 下準備

下記の例ではベンダー名を`Claves`、モジュール名を`ExtendCsvImport`にしています
ベンダー名は会社名や個人名に差し替えるといいです

### モジュール登録

いつもの必須作業というかなんというか、モジュールを登録していきます

`app/code/Claves/ExtendCsvImport/registration.php`

```registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Claves_ExtendCsvImport',
    __DIR__
);
```

`app/code/Claves/ExtendCsvImport/etc/module.xml`

```module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Claves_ExtendCsvImport" setup_version="1.0.0">
        <sequence>
            <module name="Magento_CatalogImportExport"/>
        </sequence>
    </module>
</config>
```

`sequence`ブロックには処理を差し込みたい既存クラスのモジュール名を書いておきましょう

### Dependency injection

`app/code/Claves/ExtendCsvImport/etc/di.xml`

```di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\CatalogImportExport\Model\Import\Product">
        <plugin name="Claves_ExtendCsvImport_Plugin_Magento_CatalogImportExport_Model_Import_Product"
                type="Claves\ExtendCsvImport\Plugin\Magento\CatalogImportExport\Model\Import\ProductPlugin"
                sortOrder="1"
        />
    </type>
</config>
```

#### typeブロック

`name`には元の`class`を指定します

#### pluginブロック

`sortOrder`は処理の差し込み順です。他のPluginsとの優先順位づけに利用します
`name`は他のplugin名と被らないようにしましょう。`Claves_ExtendCsvImport::CatalogImportExport`のように記載しているPluginsもありました
`type`は処理を差し込むphpファイルのパスと揃えてください

## 処理を差し込む

今回差し込む既存の処理は`validateRow`です。商品CSV取込のバリデーションを行っています

`app/code/Magento/CatalogImportExport/Model/Import/Product.php`

```Product.php
<?php
public function validateRow(array $rowData, $rowNum)
{
    // (...省略...)
}
```

`validateRow`を実行前にSKUの重複チェック処理と`product_websites`の空白チェック処理を追加します

`app/code/Claves/ExtendCsvImport/Plugin/Magento/CatalogImportExport/Model/Import/ProductPlugin.php`


```ProductPlugin.php
<?php
namespace Claves\ExtendCsvImport\Plugin\Magento\CatalogImportExport\Model\Import;

use Magento\CatalogImportExport\Model\Import\Product;
use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface;

class ProductPlugin extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
{
    /**
     * @var \Psr\Log\LoggerInterface
     */
    protected $logger;

    private $appliedSkus;

    /**
     * @param \Psr\Log\LoggerInterface $logger
     */
    public function __construct(\Psr\Log\LoggerInterface $logger)
    {
        $this->logger = $logger;
        $this->appliedSkus = array();
    }

    public function validateRow(array $rowData, $rowNum)
    {
    }

    public function _importData()
    {
    }

    public function getEntityTypeCode()
    {
        return 'catalog_product';
    }

    public function beforeValidateRow(Product $subject, array $rowData, $rowNum)
    {
        if (isset($this->_validatedRows[$rowNum])) {
            // check that row is already validated
            return array($rowData, $rowNum);
        }
        $this->_validatedRows[$rowNum] = true;

        if (empty($rowData[Product::COL_PRODUCT_WEBSITES])) {
            $subject->addRowError(ValidatorInterface::ERROR_INVALID_WEBSITE, $rowNum,
                Product::COL_PRODUCT_WEBSITES,
                Product::COL_PRODUCT_WEBSITES . ' is empty.');
        }
        $rowScope = $subject->getRowScope($rowData);
        // $this->logger->debug($rowScope . $rowData[Product::COL_SKU]);
        if (Product::SCOPE_DEFAULT == $rowScope && isset($this->appliedSkus[$rowData[Product::COL_SKU]])) {
            $subject->addRowError(ValidatorInterface::ERROR_DUPLICATE_UNIQUE_ATTRIBUTE, $rowNum,
                Product::COL_SKU,
                Product::COL_SKU . ' is duplicate.');
        }
        $this->appliedSkus[$rowData[Product::COL_SKU]] = true;
        return array($rowData, $rowNum);
    }
}
```

`AbstractEntity`を継承しているためちょっと長いですが、`beforeValidateRow`だけが大事です

#### 実装するメソッドのルール

* 処理名は`beforeValidateRow`と差し込む処理の前に`before`をつけてキャメルケースにします
* 既存の処理の後に差し込む場合は`after`、前後に差し込む場合は`around`をつけます。
* 第一引数は既存のクラス（のオブジェクト）、第二引数以降は既存の処理の引数です
* 既存の処理の引数と同じオブジェクトを返却します。複数ある場合はArrayに詰めて返します

処理の中身について詳細には触れませんがひとつだけ、第一引数で既存のクラスを渡されていることからも分かるように、publicメソッドならなんでも呼び出せます。上記例で言うと`$subject->addRowError(...)`でエラーメッセージを追加しています

## 所感
Magento2は既存の処理を拡張する仕組みが複数用意されていて、そのどれもが強力です。Pluginsについては制約がそれなりにありますが、うまく使いこなせれば割りと自然な形で実装できると思います