Chapter 07

バックエンド - 購入

ta.toshio
ta.toshio
2021.06.06に更新

クラス概要

処理フロー

  1. 決済処理
    |- src/Eccube/Controller/ShoppingController.php
    1-1. purchaseflowで処理郡適用
    |- executePurchaseFlow
    1-2. 主要処理
    |- createPaymentMethod
    |- executeApply
    |- executeCheckout
  2. 支払い処理

1. 決済処理(コントローラー)

src/Eccube/Controller/ShoppingController.php

    /**
     * 注文処理を行う.
     *
     * 決済プラグインによる決済処理および注文の確定処理を行います.
     *
     * @Route("/shopping/checkout", name="shopping_checkout", methods={"POST"})
     * @Template("Shopping/confirm.twig")
     */
    public function checkout(Request $request)
    {
...
            try {
                /*
                 * 集計処理
                 */
                log_info('[注文処理] 集計処理を開始します.', [$Order->getId()]);
                $response = $this->executePurchaseFlow($Order);
                $this->entityManager->flush();

                if ($response) {
                    return $response;
                }

                log_info('[注文処理] PaymentMethodを取得します.', [$Order->getPayment()->getMethodClass()]);
		
		// Eccube\Service\Payment\Method\Cashを取得
		// dtb_paymentテーブルのpayment_methodカラムに格納されている。
                $paymentMethod = $this->createPaymentMethod($Order, $form);

                /*
                 * 決済実行(前処理)
                 */
                log_info('[注文処理] PaymentMethod::applyを実行します.');
                if ($response = $this->executeApply($paymentMethod)) {
                    return $response;
                }

                /*
                 * 決済実行
                 *
                 * PaymentMethod::checkoutでは決済処理が行われ, 正常に処理出来た場合はPurchaseFlow::commitがコールされます.
                 */
                log_info('[注文処理] PaymentMethod::checkoutを実行します.');
                if ($response = $this->executeCheckout($paymentMethod)) {
                    return $response;
                }

                $this->entityManager->flush();

                log_info('[注文処理] 注文処理が完了しました.', [$Order->getId()]);
            } catch (ShoppingException $e) {
                log_error('[注文処理] 購入エラーが発生しました.', [$e->getMessage()]);

                $this->entityManager->rollback();

                $this->addError($e->getMessage());

                return $this->redirectToRoute('shopping_error');
            } catch (\Exception $e) {
                log_error('[注文処理] 予期しないエラーが発生しました.', [$e->getMessage()]);

                $this->entityManager->rollback();

                $this->addError('front.shopping.system_error');

                return $this->redirectToRoute('shopping_error');
            }

            // カート削除
            log_info('[注文処理] カートをクリアします.', [$Order->getId()]);
            $this->cartService->clear();

            // 受注IDをセッションにセット
            $this->session->set(OrderHelper::SESSION_ORDER_ID, $Order->getId());

            // メール送信
            log_info('[注文処理] 注文メールの送信を行います.', [$Order->getId()]);
            $this->mailService->sendOrderMail($Order);
            $this->entityManager->flush();

            log_info('[注文処理] 注文処理が完了しました. 購入完了画面へ遷移します.', [$Order->getId()]);

            return $this->redirectToRoute('shopping_complete');
        }

...
        return $this->redirectToRoute('shopping_error');
    }


    /**
     * 購入完了画面を表示する.
     *
     * @Route("/shopping/complete", name="shopping_complete")
     * @Template("Shopping/complete.twig")
     */
    public function complete(Request $request)
    {
        log_info('[注文完了] 注文完了画面を表示します.');

        // 受注IDを取得
        $orderId = $this->session->get(OrderHelper::SESSION_ORDER_ID);
...
        $Order = $this->orderRepository->find($orderId);
...

        log_info('[注文完了] 購入フローのセッションをクリアします. ');
        $this->orderHelper->removeSession();

        $hasNextCart = !empty($this->cartService->getCarts());

        log_info('[注文完了] 注文完了画面を表示しました. ', [$hasNextCart]);

        return [
            'Order' => $Order,
            'hasNextCart' => $hasNextCart,
        ];
    }



    /**
     * PaymentMethodをコンテナから取得する.
     *
     * @param Order $Order
     * @param FormInterface $form
     *
     * @return PaymentMethodInterface
     */
    private function createPaymentMethod(Order $Order, FormInterface $form)
    {
        $PaymentMethod = $this->container->get($Order->getPayment()->getMethodClass());
        $PaymentMethod->setOrder($Order);
        $PaymentMethod->setFormType($form);

        return $PaymentMethod;
    }

    /**
     * PaymentMethod::applyを実行する.
     *
     * @param PaymentMethodInterface $paymentMethod
     *
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
     */
    protected function executeApply(PaymentMethodInterface $paymentMethod)
    {
        $dispatcher = $paymentMethod->apply(); // 決済処理中.

        // リンク式決済のように他のサイトへ遷移する場合などは, dispatcherに処理を移譲する.
        if ($dispatcher instanceof PaymentDispatcher) {
            $response = $dispatcher->getResponse();
            $this->entityManager->flush();

            // dispatcherがresponseを保持している場合はresponseを返す
            if ($response instanceof Response && ($response->isRedirection() || $response->isSuccessful())) {
                log_info('[注文処理] PaymentMethod::applyが指定したレスポンスを表示します.');

                return $response;
            }

            // forwardすることも可能.
            if ($dispatcher->isForward()) {
                log_info('[注文処理] PaymentMethod::applyによりForwardします.',
                    [$dispatcher->getRoute(), $dispatcher->getPathParameters(), $dispatcher->getQueryParameters()]);

                return $this->forwardToRoute($dispatcher->getRoute(), $dispatcher->getPathParameters(),
                    $dispatcher->getQueryParameters());
            } else {
                log_info('[注文処理] PaymentMethod::applyによりリダイレクトします.',
                    [$dispatcher->getRoute(), $dispatcher->getPathParameters(), $dispatcher->getQueryParameters()]);

                return $this->redirectToRoute($dispatcher->getRoute(),
                    array_merge($dispatcher->getPathParameters(), $dispatcher->getQueryParameters()));
            }
        }
    }

    /**
     * PaymentMethod::checkoutを実行する.
     *
     * @param PaymentMethodInterface $paymentMethod
     *
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
     */
    protected function executeCheckout(PaymentMethodInterface $paymentMethod)
    {
        $PaymentResult = $paymentMethod->checkout();
        $response = $PaymentResult->getResponse();
        // PaymentResultがresponseを保持している場合はresponseを返す
        if ($response instanceof Response && ($response->isRedirection() || $response->isSuccessful())) {
            $this->entityManager->flush();
            log_info('[注文処理] PaymentMethod::checkoutが指定したレスポンスを表示します.');

            return $response;
        }

        // エラー時はロールバックして購入エラーとする.
        if (!$PaymentResult->isSuccess()) {
            $this->entityManager->rollback();
            foreach ($PaymentResult->getErrors() as $error) {
                $this->addError($error);
            }

            log_info('[注文処理] PaymentMethod::checkoutのエラーのため, 購入エラー画面へ遷移します.', [$PaymentResult->getErrors()]);

            return $this->redirectToRoute('shopping_error');
        }
    }


1-1. purchaseflowで処理郡適用

前項と仕組みとしては一緒。一番下に少し紹介を記載する。

1-2. 支払い処理

|- createPaymentMethod
|- executeApply
|- executeCheckout

で実行。大まかに言うと、createPaymentMethodで作成したPaymentMethodインスタンスのapplyメソッドとcheckoutメソッドを実行する。その内容は2.支払い処理で記載する。

2.支払い処理

支払い方法によって処理が変わる。
dtb_paymentテーブルのpayment_methodで保持されている値のクラスを実行

src/Eccube/Service/Payment/Method/Cash.php

他の項でも説明しているが、おそらく$shoppingPurchaseFlowに'@eccube.purchase.flow.shopping'の定義内容がDIされている。

<?php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Eccube\Service\Payment\Method;

use Eccube\Entity\Order;
use Eccube\Service\Payment\PaymentMethodInterface;
use Eccube\Service\Payment\PaymentResult;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Eccube\Service\PurchaseFlow\PurchaseFlow;
use Symfony\Component\Form\FormInterface;

/**
 * 銀行振込, 代金引き換えなど, 主に現金を扱う支払い方法を扱うクラス.
 */
class Cash implements PaymentMethodInterface
{
    /** @var Order */
    private $Order;

    /** @var FormInterface */
    private $form;

    /** @var */
    private $purchaseFlow;

    /**
     * Cash constructor.
     *
     * @param PurchaseFlow $shoppingPurchaseFlow
     */
    public function __construct(PurchaseFlow $shoppingPurchaseFlow)
    {
        $this->purchaseFlow = $shoppingPurchaseFlow;
    }

    /**
     * {@inheritdoc}
     *
     * @throws \Eccube\Service\PurchaseFlow\PurchaseException
     */
    public function checkout()
    {
        $this->purchaseFlow->commit($this->Order, new PurchaseContext());

        $result = new PaymentResult();
        $result->setSuccess(true);

        return $result;
    }

    /**
     * {@inheritdoc}
     *
     * @throws \Eccube\Service\PurchaseFlow\PurchaseException
     */
    public function apply()
    {
        $this->purchaseFlow->prepare($this->Order, new PurchaseContext());

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function setFormType(FormInterface $form)
    {
        $this->form = $form;
    }

    /**
     * {@inheritdoc}
     */
    public function verify()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function setOrder(Order $Order)
    {
        $this->Order = $Order;
    }
}

preparecommitの内容はpurchaseflowに移譲されている。
移譲している内容は、この項下の方で記載する。

src/Eccube/Service/PurchaseFlow/PurchaseFlow.php

    /**
     * 購入フロー仮確定処理.
     *
     * @param ItemHolderInterface $target
     * @param PurchaseContext $context
     *
     * @throws PurchaseException
     */
    public function prepare(ItemHolderInterface $target, PurchaseContext $context)
    {
        $context->setFlowType($this->flowType);

        foreach ($this->purchaseProcessors as $processor) {
            $processor->prepare($target, $context);
        }
    }

    /**
     * 購入フロー確定処理.
     *
     * @param ItemHolderInterface $target
     * @param PurchaseContext $context
     *
     * @throws PurchaseException
     */
    public function commit(ItemHolderInterface $target, PurchaseContext $context)
    {
        $context->setFlowType($this->flowType);

        foreach ($this->purchaseProcessors as $processor) {
            $processor->commit($target, $context);
        }
    }

支払い処理のインタフェース

src/Eccube/Service/Payment/PaymentMethodInterface.php

<?php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Eccube\Service\Payment;

use Eccube\Entity\Order;
use Symfony\Component\Form\FormInterface;

/**
 * PaymentMethodInterface
 *
 * 必要に応じて決済手段ごとに実装する
 */
interface PaymentMethodInterface
{
    /**
     * 決済の妥当性を検証し, 検証結果を返します.
     *
     * 主にクレジットカードの有効性チェック等を実装します.
     *
     * @return PaymentResult
     */
    public function verify();

    /**
     * 決済を実行し, 実行結果を返します.
     *
     * 主に決済の確定処理を実装します.
     *
     * @return PaymentResult
     */
    public function checkout();

    /**
     * 注文に決済を適用します.
     *
     * PaymentDispatcher に遷移先の情報を設定することで, 他のコントローラに処理を移譲できます.
     *
     * @return PaymentDispatcher
     */
    public function apply();

    /**
     * PaymentMethod の処理に必要な FormInterface を設定します.
     *
     * @param FormInterface
     *
     * @return PaymentMethod
     */
    public function setFormType(FormInterface $form);

    /**
     * この決済を使用する Order を設定します.
     *
     * @param Order
     *
     * @return PaymentMethod
     */
    public function setOrder(Order $Order);
}

クレジットカード用支払い処理のインターフェース

具象クラスはないが、これを利用した実装例に興味がある。

src/Eccube/Service/Payment/Method/CreditCard.php

<?php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Eccube\Service\Payment\Method;

use Eccube\Entity\Order;
use Eccube\Service\Payment\PaymentMethodInterface;
use Symfony\Component\Form\FormInterface;

/**
 * クレジットカード払いの基底クラス.
 *
 * クレジットカード決済を実装する場合は, このクラスを継承します.
 */
abstract class CreditCard implements PaymentMethodInterface
{
    /**
     * @var Order
     */
    protected $Order;

    /**
     * {@inheritdoc}
     */
    abstract public function verify();

    /**
     * {@inheritdoc}
     */
    abstract public function checkout();

    /**
     * {@inheritdoc}
     */
    abstract public function apply();

    /**
     * {@inheritdoc}
     */
    abstract public function setFormType(FormInterface $form);

    /**
     * {@inheritdoc}
     */
    public function setOrder(Order $Order)
    {
        $this->Order = $Order;
    }
}

支払い結果クラス

どのような支払いを実行したあとでも返却値はこのクラスを変えさせると思う。

src/Eccube/Service/Payment/PaymentResult.php

<?php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Eccube\Service\Payment;

use Symfony\Component\HttpFoundation\Response;

/**
 * 決済結果のクラス.
 */
class PaymentResult
{
    /**
     * @var array
     */
    private $errors = [];

    /**
     * @var boolean
     */
    private $success;

    /**
     * @var Response
     */
    private $response;

    /**
     * 決済が成功したかどうかを設定します.
     *
     * 決済が成功した場合は true, 失敗した場合は false を設定します.
     *
     * @param boolean $success
     *
     * @return PaymentResult
     */
    public function setSuccess($success)
    {
        $this->success = $success;

        return $this;
    }

    /**
     * 決済が成功したかどうか.
     *
     * 決済が成功した場合 true
     *
     * @return boolean
     */
    public function isSuccess()
    {
        return $this->success;
    }

    /**
     * 決済が失敗した場合のエラーの配列を返します.
     *
     * @return array
     */
    public function getErrors()
    {
        return $this->errors;
    }

    /**
     * 決済が失敗した場合のエラーの配列を設定します.
     *
     * @param array $errors
     *
     * @return PaymentResult
     */
    public function setErrors(array $errors)
    {
        $this->errors = $errors;

        return $this;
    }

    /**
     * Response を設定します.
     *
     * 3Dセキュアなど, 決済中に他のサイトへリダイレクトが必要な特殊な用途に使用します.
     *
     * @param Response $response
     *
     * @return PaymentResult
     */
    public function setResponse(Response $response)
    {
        $this->response = $response;

        return $this;
    }

    /**
     * Response を返します.
     *
     * @return Response
     */
    public function getResponse()
    {
        return $this->response;
    }
}

PurchaseFlow(共通処理)

    eccube.purchase.flow.shopping.purchase:
        class: Doctrine\Common\Collections\ArrayCollection
        arguments:
            - #
                - '@Eccube\Service\PurchaseFlow\Processor\PreOrderIdValidator'
                - '@Eccube\Service\PurchaseFlow\Processor\PointProcessor' # 利用ポイントの計算とポイント明細の追加
                - '@Eccube\Service\PurchaseFlow\Processor\StockReduceProcessor' # 購入時は在庫を減らす、失敗やキャンセル時は戻す
                - '@Eccube\Service\PurchaseFlow\Processor\CustomerPurchaseInfoProcessor' # 顧客の最終購入時間、購入回数などを更新
                - '@Eccube\Service\PurchaseFlow\Processor\OrderUpdateProcessor' # Orderのorder_status_idを(だいたい購入処理中から)新規受付に更新

StockReduceProcessorだけ下に記載しておく。

在庫を減らす

src/Eccube/Service/PurchaseFlow/Processor/StockReduceProcessor.php

    /**
     * {@inheritdoc}
     */
    public function prepare(ItemHolderInterface $itemHolder, PurchaseContext $context)
    {
        // 在庫を減らす
        $this->eachProductOrderItems($itemHolder, function ($currentStock, $itemQuantity) {
            return $currentStock - $itemQuantity;
        });
    }

    /**
     * {@inheritdoc}
     */
    public function rollback(ItemHolderInterface $itemHolder, PurchaseContext $context)
    {
        // 在庫を戻す
        $this->eachProductOrderItems($itemHolder, function ($currentStock, $itemQuantity) {
            return $currentStock + $itemQuantity;
        });
    }

    private function eachProductOrderItems(ItemHolderInterface $itemHolder, callable $callback)
    {
        // Order以外の場合は何もしない
        if (!$itemHolder instanceof Order) {
            return;
        }

        foreach ($itemHolder->getProductOrderItems() as $item) {
            // 在庫が無制限かチェックし、制限ありなら在庫数をチェック
            if (!$item->getProductClass()->isStockUnlimited()) {
                // 在庫チェックあり
                /* @var ProductStock $productStock */
                $productStock = $item->getProductClass()->getProductStock();
                if ($productStock->getProductClassId() === null) {
                    // 在庫に対してロックを実行
                    $this->entityManager->lock($productStock, LockMode::PESSIMISTIC_WRITE);
                    $this->entityManager->refresh($productStock);
                    $productStock->setProductClassId($item->getProductClass()->getId());
                }
                $ProductClass = $item->getProductClass();
                $stock = $callback($productStock->getStock(), $item->getQuantity());
                if ($stock < 0) {
                    throw new ShoppingException(trans('purchase_flow.over_stock', ['%name%' => $ProductClass->formattedProductName()]));
                }
                $productStock->setStock($stock);
                $ProductClass->setStock($stock);
            }
        }
    }