このエントリはMagento Advent Calendar 2016の25日目です。いよいよ最終日です。
今回は前回に引き続き、Magento2での決済エクステンションの作り方について解説していきます。

前回ではMagento2の決済エクステンションの概要を解説しました。
その中でAdapterによる実装が今後のスタンダードになることを取り上げましたが、今回はAdapterを用いた実装の実際について解説していきます。 

Adapterとdi.xml

Adapterとdi.xmlは切っても切れない関係にあります。

基本的にAdapterを拡張する必要性は低く、関連するクラスを定義・実装するだけで動作するように設計されているからです。また、関連するクラスの定義にはdi.xmlを使用するようになっているため、決済エクステンションを実装するためには両方の仕組みをよく理解しておくことが重要です。

Adapterクラスのコンストラクタ定義

最初にAdapterクラスのコンストラクタ定義を見てみましょう。
下記のコードを見てください。

    /**
     * @param ManagerInterface $eventManager
     * @param ValueHandlerPoolInterface $valueHandlerPool
     * @param PaymentDataObjectFactory $paymentDataObjectFactory
     * @param string $code
     * @param string $formBlockType
     * @param string $infoBlockType
     * @param CommandPoolInterface $commandPool
     * @param ValidatorPoolInterface $validatorPool
     * @param CommandManagerInterface $commandExecutor
     */
    public function __construct(
        ManagerInterface $eventManager,
        ValueHandlerPoolInterface $valueHandlerPool,
        PaymentDataObjectFactory $paymentDataObjectFactory,
        $code,
        $formBlockType,
        $infoBlockType,
        CommandPoolInterface $commandPool = null,
        ValidatorPoolInterface $validatorPool = null,
        CommandManagerInterface $commandExecutor = null
    ) {
        $this->valueHandlerPool = $valueHandlerPool;
        $this->validatorPool = $validatorPool;
        $this->commandPool = $commandPool;
        $this->code = $code;
        $this->infoBlockType = $infoBlockType;
        $this->formBlockType = $formBlockType;
        $this->eventManager = $eventManager;
        $this->paymentDataObjectFactory = $paymentDataObjectFactory;
        $this->commandExecutor = $commandExecutor;
    }

9つの引数を取るように定義が行われていることがわかります。
そのうち6つは所定の型定義がされており、3つは文字列型の引数であることがわかります。
これら全ての引数はdi.xmlで調整できることを覚えておいてください。どのような値を与えるかは、実装者自身の自由です。
例えばBraintree連携では以下のように書かれています。

    <virtualType name="BraintreeFacade" type="Magento\Payment\Model\Method\Adapter">
        <arguments>
            <argument name="code" xsi:type="const">Magento\Braintree\Model\Ui\ConfigProvider::CODE</argument>
            <argument name="formBlockType" xsi:type="string">Magento\Braintree\Block\Form</argument>
            <argument name="infoBlockType" xsi:type="string">Magento\Braintree\Block\Info</argument>
            <argument name="valueHandlerPool" xsi:type="object">BraintreeValueHandlerPool</argument>
            <argument name="validatorPool" xsi:type="object">BraintreeValidatorPool</argument>
            <argument name="commandPool" xsi:type="object">BraintreeCommandPool</argument>
        </arguments>
    </virtualType>

実際のケースでは9つある引数全てを定義する必要はありません。接続先のサービスに合わせて調整すれば良い仕組みになっています。
ただ、最低でも

  • code
  • formBlockType
  • infoBlockType
  • valueHandlerPool
  • validatorPool
  • commandPool

の6つは書かないといけないと思います。

di.xmlにおけるtypeとvirtualType定義

さて、di.xmlでわかりにくいポイントの1つに、

  • type
  • virtualType

の2つがあります。
決済エクステンションの実装ではvirturalTypeを多用するので、ここで解説しておきます。

typeとvirtualTypeの違い

実際のところ、どちらも似た動きをします。
あるクラスに対するコンストラクタインジェクションの内容を定義するという点では全く同じです。
例えば下記の2つの定義が良い例です。

    <type name="Magento\Braintree\Block\Info">
        <arguments>
            <argument name="config" xsi:type="object">Magento\Braintree\Gateway\Config\Config</argument>
        </arguments>
    </type>
    <virtualType name="BraintreePayPalInfo" type="Magento\Braintree\Block\Info">
        <arguments>
            <argument name="config" xsi:type="object">Magento\Braintree\Gateway\Config\PayPal\Config</argument>
        </arguments>
    </virtualType>

前者の方は、新たに「Magento\Braintree\Block\Info」クラスに対する引数定義を行っています。
このクラスのコンストラクタ定義は以下のようになっている(正確には親クラスですが)ため、適切な定義です。

    /**
     * @param Context $context
     * @param ConfigInterface $config
     * @param array $data
     */
    public function __construct(
        Context $context,
        ConfigInterface $config,
        array $data = []
    ) {
        parent::__construct($context, $data);
        $this->config = $config;

        if (isset($data['pathPattern'])) {
            $this->config->setPathPattern($data['pathPattern']);
        }

        if (isset($data['methodCode'])) {
            $this->config->setMethodCode($data['methodCode']);
        }
    }

要は第二引数のConfigInterface型の引数を指定しているわけです。

これに対し、後者の定義は既に定義されている「Magento\Braintree\Block\Info」クラスの定義を変更し、それに別名を付けています。
virturalTypeとは、「既にあるtype定義に対して別名を付けて定義を行う機能」であるわけです。

決済エクステンションのdi.xml定義ではvirtualType定義を多用します。
どのシーンでtype定義をするか、あるいはvirtualType定義をするかをよく見極め、使い分けてください。

関連クラスの定義と実装

さて、ここからはAdapterが使用する関連クラスの定義と実装を解説していきます。

コマンドクラス

まずはコマンドクラスです。コマンドクラスは「Magento\Payment\Gateway\Command\CommandPool」を経由して呼び出される、決済処理それぞれを担当するクラスです。
Adapterは標準で以下の9つのコマンドを用意しています。

  • authorize
  • sale
  • capture
  • cancel
  • refund
  • void
  • accept_payment
  • deny_payment
  • initialize

もし、これ以外のコマンドが使用したい場合は、Adapterを継承した独自のクラスを定義し、コマンドを呼び出すメソッドを実装すればOKです。

コマンドクラスのdi.xml定義

コマンドクラスを使用する場合、di.xmlでは以下のように書きます。

    <virtualType name="BraintreeCommandPool" type="Magento\Payment\Gateway\Command\CommandPool">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="authorize" xsi:type="string">BraintreeAuthorizeCommand</item>
                <item name="sale" xsi:type="string">BraintreeSaleCommand</item>
                <item name="capture" xsi:type="string">BraintreeCaptureStrategyCommand</item>
                <item name="void" xsi:type="string">BraintreeVoidCommand</item>
                <item name="refund" xsi:type="string">BraintreeRefundCommand</item>
                <item name="cancel" xsi:type="string">BraintreeVoidCommand</item>
                <item name="deny_payment" xsi:type="string">BraintreeVoidCommand</item>
            </argument>
        </arguments>
    </virtualType>

CommandPoolクラスのコンストラクタ引数として「$commands」が既に定義されているので、その内容として使用するコマンドクラス名を列挙します。
itemタグの値はtypeかvirtualTypeのname属性を取ることができます。大抵はvirtualType定義を使うことになるでしょう。

コマンドクラスの定義

CommandPoolに与えるコマンドクラスは、以下のようにdi.xml上では定義します。

    <virtualType name="BraintreeAuthorizeCommand" type="Magento\Payment\Gateway\Command\GatewayCommand">
        <arguments>
            <argument name="requestBuilder" xsi:type="object">BraintreeAuthorizeRequest</argument>
            <argument name="transferFactory" xsi:type="object">Magento\Braintree\Gateway\Http\TransferFactory</argument>
            <argument name="client" xsi:type="object">Magento\Braintree\Gateway\Http\Client\TransactionSale</argument>
            <argument name="handler" xsi:type="object">BraintreeAuthorizationHandler</argument>
            <argument name="validator" xsi:type="object">Magento\Braintree\Gateway\Validator\ResponseValidator</argument>
        </arguments>
    </virtualType>

1つのコマンドを実装するためには、「Magento\Payment\Gateway\Command\GatewayCommand」を通じて具体的な処理を行うクラスを定義する必要があります。
このクラスは以下のように6つの引数を取りますが、Loggerは独自定義しないのであれば、定義しなくても構いません。

    /**
     * @param BuilderInterface $requestBuilder
     * @param TransferFactoryInterface $transferFactory
     * @param ClientInterface $client
     * @param LoggerInterface $logger
     * @param HandlerInterface $handler
     * @param ValidatorInterface $validator
     */
    public function __construct(
        BuilderInterface $requestBuilder,
        TransferFactoryInterface $transferFactory,
        ClientInterface $client,
        LoggerInterface $logger,
        HandlerInterface $handler = null,
        ValidatorInterface $validator = null
    ) {
        $this->requestBuilder = $requestBuilder;
        $this->transferFactory = $transferFactory;
        $this->client = $client;
        $this->handler = $handler;
        $this->validator = $validator;
        $this->logger = $logger;
    }

重要なポイントとしては、コマンド内部の処理は以下のように流れていく、ということです。

  1. requestBuilder
  2. transferFactory
  3. client
  4. validator
  5. handler

1から5の順番に様々な処理が行われ、決済ゲートウェイに値を送信し、結果を検証するという流れになっています。
Magento1の実装と異なるのは、それぞれの処理が役割別に細分化され、かつ仕様変更が起きた際でも担当する処理部分だけを差し替えれば良い仕組みになっている点です。

各実体処理クラス

いよいよ決済連携の実装も一番深い所まで来ました。
実体処理クラスの実装をする際には、各決済ゲートウェイの仕様書を読みながら作業することになりますが、ここでは大枠の構造を解説します。

requestBuilderの定義と実装

requestBuilderは、差し替え可能な多段階に渡る、決済リクエストデータの組み立てを行います。
Braintree連携の場合は以下のように8段階に渡る処理が定義されています。

    <virtualType name="BraintreeAuthorizeRequest" type="Magento\Payment\Gateway\Request\BuilderComposite">
        <arguments>
            <argument name="builders" xsi:type="array">
                <item name="customer" xsi:type="string">Magento\Braintree\Gateway\Request\CustomerDataBuilder</item>
                <item name="payment" xsi:type="string">Magento\Braintree\Gateway\Request\PaymentDataBuilder</item>
                <item name="channel" xsi:type="string">Magento\Braintree\Gateway\Request\ChannelDataBuilder</item>
                <item name="address" xsi:type="string">Magento\Braintree\Gateway\Request\AddressDataBuilder</item>
                <item name="vault" xsi:type="string">Magento\Braintree\Gateway\Request\VaultDataBuilder</item>
                <item name="3dsecure" xsi:type="string">Magento\Braintree\Gateway\Request\ThreeDSecureDataBuilder</item>
                <item name="device_data" xsi:type="string">Magento\Braintree\Gateway\Request\KountPaymentDataBuilder</item>
                <item name="dynamic_descriptor" xsi:type="string">Magento\Braintree\Gateway\Request\DescriptorDataBuilder</item>
            </argument>
        </arguments>
    </virtualType>

各クラスはbuildメソッドをもち、下記のように配列を返します。

    public function build(array $buildSubject)
    {
        $paymentDO = $this->subjectReader->readPayment($buildSubject);

        $order = $paymentDO->getOrder();
        $billingAddress = $order->getBillingAddress();

        return [
            self::CUSTOMER => [
                self::FIRST_NAME => $billingAddress->getFirstname(),
                self::LAST_NAME => $billingAddress->getLastname(),
                self::COMPANY => $billingAddress->getCompany(),
                self::PHONE => $billingAddress->getTelephone(),
                self::EMAIL => $billingAddress->getEmail(),
            ]
        ];
    }

配列が多次元に渡っても構いません。重要な点は、決済ゲートウェイが要求するデータ項目やフォーマットに合わせた形式になるように、データを収集・整形することです。
ここで処理された結果の配列は、最終的に1つの配列にマージされ、transferFactoryに引き渡されます。

transferFactoryの定義と実装

transferFactoryは、requestBuilderが作成したリクエストデータ配列をTransferBuilderに引き渡し、Transferクラスのインスタンスを作成します。
TransferBuilderに処理を引き渡す際に必要に応じて以下のデータを渡すことができます。

  • clientConfig
  • headers
  • body
  • auth
  • method
  • uri
  • encode

bodyはどう考えても必須ですが、その他の値はclientでも指定できます。コマンドごとに調整が必要であれば、という感じでしょう。
Braintree連携の場合は以下のように書かれています。

    /**
     * Builds gateway transfer object
     *
     * @param array $request
     * @return TransferInterface
     */
    public function create(array $request)
    {
        return $this->transferBuilder
            ->setBody($request)
            ->build();
    }

clientの定義と実装

ようやく決済ゲートウェイにデータを送信するclientクラスの定義と実装です。
clientクラスは、「Magento\Payment\Gateway\Http\ClientInterface」型である必要があります。このインターフェイスには「placeRequest」メソッドが定義されているだけで、あとは何も定義されていません。
要はこのメソッドを実装し、何らかの方法で決済ゲートウェイにデータを送信し、戻り値を配列型で返せばOKです。

各社の違いをどう吸収するか

clientクラスの先には実際のHTTPリクエストを扱うクラスがいます。
どのようなクラスでもかまわないのですが、SDKのような形で接続ライブラリを提供してくれている会社の場合、それを使うのが無難でしょう。
ただし、そのライブラリがComposer非対応の場合は少々厄介です。
エクステンションのパッケージ内に収めることができる程度のものであればどうにかなるかもしれませんが、できない場合はどうにか工夫するか、代替のコードを自力で書く必要があります。

また、文字コードの問題やテストと本番の接続先の切り替えなど、各社固有の課題をここで吸収しなければなりません。
実装の際には仕様書をよく読み、どのように実装するのが良いかよく検討してください。

3Dセキュアやリンクタイプ実装における課題

3Dセキュアを使う場合、多くの決済サービスは、オーソリ要求のリクエストの戻りデータの中に3Dセキュアに関する情報を含めてきます。
しかしながら、Magento2.1系ではMagento1系で利用できた実装と同じ方法が通用しません。
注文確定に連なる処理は、JavaScriptを介して呼び出されるAPIで実装されており、成功もしくは所定の1通りのエラーメッセージしか返すことができません。
3Dセキュアの分岐を発生させるためには、一工夫必要であるということを覚えておいてください。

リンクタイプ実装の場合は、2通りのパターンがありえます。

  • 先にゲートウェイ側に所定のデータを送信し、決済ページのURLを取得する方式を取る会社
  • 決済に要する全ての情報を非表示のフォームからまとめて送信し、ページごと移動してしまう会社

後者の場合、とりあえず一旦注文を確定してしまい、その後リダイレクトページを作成しておいて、情報を送信することになるでしょう。

validatorの定義と実装

validatorは決済ゲートウェイから戻ってきたデータを検証する処理を担うクラスです。
「Magento\Payment\Gateway\Validator\ValidatorInterface」を実装したクラスが処理を担当します。
以下のように戻り値を検証します。

    public function validate(array $validationSubject)
    {
        /** @var Successful|Error $response */
        $response = $this->subjectReader->readResponseObject($validationSubject);

        $isValid = true;
        $errorMessages = [];

        foreach ($this->getResponseValidators() as $validator) {
            $validationResult = $validator($response);

            if (!$validationResult[0]) {
                $isValid = $validationResult[0];
                $errorMessages = array_merge($errorMessages, $validationResult[1]);
            }
        }

        return $this->createResult($isValid, $errorMessages);
    }
protected function createResult($isValid, array $fails = []) { return $this->resultInterfaceFactory->create( [ 'isValid' => (bool)$isValid, 'failsDescription' => $fails ] ); }

検証処理は、単独のクラスが担当しても良いですし、di.xmlを通じてValidatorInterface型のクラスに与えても構いません。 

handlerの定義と実装

handlerは、正常な結果が得られた際に、決済ゲートウェイからの戻り値を適切な形でMagento側に保存する処理を担当します。
Braintree連携の場合は以下のように定義されています。

    <virtualType name="BraintreeAuthorizationHandler" type="Magento\Payment\Gateway\Response\HandlerChain">
        <arguments>
            <argument name="handlers" xsi:type="array">
                <item name="payment_details" xsi:type="string">Magento\Braintree\Gateway\Response\PaymentDetailsHandler</item>
                <item name="txn_id" xsi:type="string">Magento\Braintree\Gateway\Response\TransactionIdHandler</item>
                <item name="card_details" xsi:type="string">Magento\Braintree\Gateway\Response\CardDetailsHandler</item>
                <item name="risk_data" xsi:type="string">Magento\Braintree\Gateway\Response\RiskDataHandler</item>
                <item name="vault_details" xsi:type="string">Magento\Braintree\Gateway\Response\VaultDetailsHandler</item>
                <item name="3d_secure" xsi:type="string">Magento\Braintree\Gateway\Response\ThreeDSecureDetailsHandler</item>
            </argument>
        </arguments>
    </virtualType>

各handlerは以下のような処理をもち、結果データをデータベースに保存していきます。

    public function handle(array $handlingSubject, array $response)
    {
        $paymentDO = $this->subjectReader->readPayment($handlingSubject);
        /** @var \Braintree\Transaction $transaction */
        $transaction = $this->subjectReader->readTransaction($response);
        /** @var OrderPaymentInterface $payment */
        $payment = $paymentDO->getPayment();

        $payment->setCcTransId($transaction->id);
        $payment->setLastTransId($transaction->id);

        //remove previously set payment nonce
        $payment->unsAdditionalInformation(DataAssignObserver::PAYMENT_METHOD_NONCE);
        foreach ($this->additionalInformationMapping as $item) {
            if (!isset($transaction->$item)) {
                continue;
            }
            $payment->setAdditionalInformation($item, $transaction->$item);
        }
    }

どの程度決済ゲートウェイが詳細に情報を返すかによって、この処理の実装量は変わります。
仕様書をよく読んで、調整してください。

ValueHandlerの定義と実装

ValueHandlerは設定値や処理を行ってよいかどうかの判定を行うクラスを定義します。
Braintree連携の場合は以下のように定義されています。

    <virtualType name="BraintreeValueHandlerPool" type="Magento\Payment\Gateway\Config\ValueHandlerPool">
        <arguments>
            <argument name="handlers" xsi:type="array">
                <item name="default" xsi:type="string">BraintreeConfigValueHandler</item>
                <item name="can_void" xsi:type="string">Magento\Braintree\Gateway\Config\CanVoidHandler</item>
                <item name="can_cancel" xsi:type="string">Magento\Braintree\Gateway\Config\CanVoidHandler</item>
            </argument>
        </arguments>
    </virtualType>
    <virtualType name="BraintreeConfigValueHandler" type="Magento\Payment\Gateway\Config\ConfigValueHandler">
        <arguments>
            <argument name="configInterface" xsi:type="object">Magento\Braintree\Gateway\Config\Config</argument>
        </arguments>
    </virtualType>

重要なのは「can_void」「can_cancel」を司るHandlerです。
決済サービスによっては、売上確定(実売)や払い戻しといった処理がない場合があります。(コンビニや銀振りはほぼ用意されていません)
これらの処理を決済ゲートウェイを通じて行うかどうかという判定を行うのが、ValueHandlerの役割です。

例えばCanVoidHandlerでは以下のような判定処理を行っています。

    public function handle(array $subject, $storeId = null)
    {
        $paymentDO = $this->subjectReader->readPayment($subject);

        $payment = $paymentDO->getPayment();
        return $payment instanceof Payment && !(bool)$payment->getAmountPaid();
    } 

まとめ

あまりに長い記事になりそうなので、フロントエンドの実装は次回に回します。
今回のまとめとしては、

  • 決済エクステンションの実装ではAdapterとdi.xmlの関係を知る必要がある
  • di.xmlの役割をよく理解し、上手に使う必要がある
  • 関連する小さなクラスが多いので、それぞれの役割をよく理解しておく必要がある
  • 決済サービス側の仕様をよく理解して設計・実装を行うこと
  • 決済サービス側が提供するライブラリが使えるときは、できるだけ利用する

最後に、これらのクラスに対し、コツコツとユニットテストを書いていくことが望ましいです。
関連するクラスが多いということは、不具合が何処にあるかを突き止めるだけでも手間がかかります。
それぞれの処理の品質が担保されているということは、エクステンション全体の品質向上につながるので、きちんとテストを書くようにしましょう。

2016年のAdvent Calendarはこれで終了です。
ご協力いただいた皆様、ありがとうございました!