Chapter 06

バックエンド - 購入直前

ta.toshio
ta.toshio
2021.06.06に更新

処理フロー

  1. 注文手続き画面を表示
    |~ src/Eccube/Controller/ShoppingController.php - index
  2. Order、OrderItem、Shippingレコードの作成
    |~ src/Eccube/Service/OrderHelper.php - initializeOrder
    |~ src/Eccube/Service/OrderHelper.php - createPurchaseProcessingOrder
  3. 購入前にOrder、OrderItem、Shippingに処理を施したい
    |~ app/config/eccube/packages/purchaseflow.yaml
    |~ src/Eccube/Service/OrderHelper.php - executePurchaseFlow

1. 注文手続き画面を表示(コントローラー)

src/Eccube/Controller/ShoppingController.php

    /**
     * 注文手続き画面を表示する
     *
     * 未ログインまたはRememberMeログインの場合はログイン画面に遷移させる.
     * ただし、非会員でお客様情報を入力済の場合は遷移させない.
     *
     * カート情報から受注データを生成し, `pre_order_id`でカートと受注の紐付けを行う.
     * 既に受注が生成されている場合(pre_order_idで取得できる場合)は, 受注の生成を行わずに画面を表示する.
     *
     * purchaseFlowの集計処理実行後, warningがある場合はカートど同期をとるため, カートのPurchaseFlowを実行する.
     *
     * @Route("/shopping", name="shopping")
     * @Template("Shopping/index.twig")
     */
    public function index(PurchaseFlow $cartPurchaseFlow)
    {
...
        // 受注の初期化.
        log_info('[注文手続] 受注の初期化処理を開始します.');
        $Customer = $this->getUser() ? $this->getUser() : $this->orderHelper->getNonMember();
        $Order = $this->orderHelper->initializeOrder($Cart, $Customer);

        // 集計処理.
        log_info('[注文手続] 集計処理を開始します.', [$Order->getId()]);
        $flowResult = $this->executePurchaseFlow($Order, false);
        $this->entityManager->flush();

2.Order、OrderItem、Shippingレコードの作成

src/Eccube/Service/OrderHelper.php

受注確定しなくてもorder, order_item, shippingのレコードを作成してしまう。

    /**
     * @param Cart $Cart
     * @param Customer $Customer
     *
     * @return Order|null
     */
    public function initializeOrder(Cart $Cart, Customer $Customer)
    {
        // 購入処理中の受注情報を取得
        if ($Order = $this->getPurchaseProcessingOrder($Cart->getPreOrderId())) {
            return $Order;
        }

        // 受注情報を作成
        $Order = $this->createPurchaseProcessingOrder($Cart, $Customer);
        $Cart->setPreOrderId($Order->getPreOrderId());

        return $Order;
    }

    /**
     * 購入処理中の受注を生成する.
     *
     * @param Customer $Customer
     * @param $CartItems
     *
     * @return Order
     */
    public function createPurchaseProcessingOrder(Cart $Cart, Customer $Customer)
    {
        $OrderStatus = $this->orderStatusRepository->find(OrderStatus::PROCESSING);
        $Order = new Order($OrderStatus);

        $preOrderId = $this->createPreOrderId();
        $Order->setPreOrderId($preOrderId);

        // 顧客情報の設定
        $this->setCustomer($Order, $Customer);

        $DeviceType = $this->deviceTypeRepository->find($this->mobileDetector->isMobile() ? DeviceType::DEVICE_TYPE_MB : DeviceType::DEVICE_TYPE_PC);
        $Order->setDeviceType($DeviceType);

        // 明細情報の設定
	// CartItemからOrderItemの作成。愚直に値をセットして保存している
	// https://github.com/EC-CUBE/ec-cube/blob/7d8d59a4995e4c1b50084048a3e7870ed1f84438/src/Eccube/Service/OrderHelper.php#L361
        $OrderItems = $this->createOrderItemsFromCartItems($Cart->getCartItems());
        $OrderItemsGroupBySaleType = array_reduce($OrderItems, function ($result, $item) {
            /* @var OrderItem $item */
            $saleTypeId = $item->getProductClass()->getSaleType()->getId();
            $result[$saleTypeId][] = $item;

            return $result;
        }, []);

        foreach ($OrderItemsGroupBySaleType as $OrderItems) {
	    // OrderItemとCustomerの情報からShipmmentの作成
	    // https://github.com/EC-CUBE/ec-cube/blob/7d8d59a4995e4c1b50084048a3e7870ed1f84438/src/Eccube/Service/OrderHelper.php#L402
            $Shipping = $this->createShippingFromCustomer($Customer);
            $Shipping->setOrder($Order);
	    // shippingにorderItem追加
	    // orderにorderItem追加
	    // orderItemにshippingセット
	    // orderItemにorderセット
            $this->addOrderItems($Order, $Shipping, $OrderItems);
	    // 購入予定の商品から配送情報取得して一番上にあるものをセット
            $this->setDefaultDelivery($Shipping);
            $this->entityManager->persist($Shipping);
            $Order->addShipping($Shipping);
        }

        $this->setDefaultPayment($Order);

        $this->entityManager->persist($Order);

        return $Order;
    }

    /**
     * @param Collection|ArrayCollection|CartItem[] $CartItems
     *
     * @return OrderItem[]
     */
    protected function createOrderItemsFromCartItems($CartItems)
    {
        $ProductItemType = $this->orderItemTypeRepository->find(OrderItemType::PRODUCT);

        return array_map(function ($item) use ($ProductItemType) {
...
            $OrderItem = new OrderItem();
...

    /**
     * @param Customer $Customer
     *
     * @return Shipping
     */
    protected function createShippingFromCustomer(Customer $Customer)
    {
        $Shipping = new Shipping();

3. 購入前にOrder、OrderItem、Shippingに処理を施したい

cart、shopping、orderの文脈時に受注、受注明細に対して実行してほしい処理郡の機能を用意している。前項でも説明したPurchaseFlowを利用している。

設定定義

cart、shopping、orderの文脈時に受注、受注明細に対して実行してほしい処理郡を定義。

app/config/eccube/packages/purchaseflow.yaml

    # Purchase Flow for Shopping
    eccube.purchase.flow.shopping:
        class: Eccube\Service\PurchaseFlow\PurchaseFlow
        calls:
            - [setFlowType, ['shopping']]
            - [setItemValidators, ['@eccube.purchase.flow.shopping.item_validators']]
            - [setItemHolderValidators, ['@eccube.purchase.flow.shopping.holder_validators']]
            - [setItemHolderPreprocessors, ['@eccube.purchase.flow.shopping.holder_preprocessors']]
            - [setDiscountProcessors, ['@eccube.purchase.flow.shopping.discount_processors']]
            - [setItemHolderPostValidators, ['@eccube.purchase.flow.shopping.holder_post_validators']]
            - [setPurchaseProcessors, ['@eccube.purchase.flow.shopping.purchase']]

    eccube.purchase.flow.shopping.item_validators:
        class: Doctrine\Common\Collections\ArrayCollection
        arguments:
            - #
                - '@Eccube\Service\PurchaseFlow\Processor\DeliverySettingValidator'
                - '@Eccube\Service\PurchaseFlow\Processor\ProductStatusValidator'
                - '@Eccube\Service\PurchaseFlow\Processor\PriceChangeValidator'

    eccube.purchase.flow.shopping.holder_validators:
        class: Doctrine\Common\Collections\ArrayCollection
        arguments:
            - #
                - '@Eccube\Service\PurchaseFlow\Processor\StockMultipleValidator'
                - '@Eccube\Service\PurchaseFlow\Processor\SaleLimitMultipleValidator'
                - '@Eccube\Service\PurchaseFlow\Processor\EmptyItemsValidator'  # 空明細の削除処理

    eccube.purchase.flow.shopping.holder_preprocessors:
        class: Doctrine\Common\Collections\ArrayCollection
        arguments:
            - #
                - '@Eccube\Service\PurchaseFlow\Processor\TaxProcessor'  # 税額の計算(商品明細に対して税額計算)
                - '@Eccube\Service\PurchaseFlow\Processor\OrderNoProcessor'
                - '@Eccube\Service\PurchaseFlow\Processor\DeliveryFeePreprocessor'
                - '@Eccube\Service\PurchaseFlow\Processor\DeliveryFeeFreeByShippingPreprocessor'
                - '@Eccube\Service\PurchaseFlow\Processor\PaymentChargePreprocessor'
                - '@Eccube\Service\PurchaseFlow\Processor\TaxProcessor'  # 税額の計算(送料明細・手数料明細に対して税額を計算)

    eccube.purchase.flow.shopping.discount_processors:
        class: Doctrine\Common\Collections\ArrayCollection
        arguments:
            - #
                - '@Eccube\Service\PurchaseFlow\Processor\PointProcessor' # ポイント明細の追加

    eccube.purchase.flow.shopping.holder_post_validators:
        class: Doctrine\Common\Collections\ArrayCollection
        arguments:
            - #
                - '@Eccube\Service\PurchaseFlow\Processor\AddPointProcessor'  # 加算ポイントの計算
                - '@Eccube\Service\PurchaseFlow\Processor\PaymentTotalLimitValidator'
                - '@Eccube\Service\PurchaseFlow\Processor\PaymentTotalNegativeValidator'
                - '@Eccube\Service\PurchaseFlow\Processor\PaymentChargeChangeValidator' # 手数料の変更検知
                - '@Eccube\Service\PurchaseFlow\Processor\DeliveryFeeChangeValidator' # 送料の変更検知

    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'

Purchaseflow呼び出し箇所 - コントローラー

src/Eccube/Controller/AbstractShoppingController.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\Controller;

use Eccube\Entity\ItemHolderInterface;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Eccube\Service\PurchaseFlow\PurchaseFlow;
use Eccube\Service\PurchaseFlow\PurchaseFlowResult;

class AbstractShoppingController extends AbstractController
{
    /**
     * @var PurchaseFlow
     */
    protected $purchaseFlow;

    /**
     * @param PurchaseFlow $shoppingPurchaseFlow
     * @required
     */
    public function setPurchaseFlow(PurchaseFlow $shoppingPurchaseFlow)
    {
        $this->purchaseFlow = $shoppingPurchaseFlow;
    }

    /**
     * @param ItemHolderInterface $itemHolder
     * @param bool $returnResponse レスポンスを返すかどうか. falseの場合はPurchaseFlowResultを返す.
     *
     * @return PurchaseFlowResult|\Symfony\Component\HttpFoundation\RedirectResponse
     */
    protected function executePurchaseFlow(ItemHolderInterface $itemHolder, $returnResponse = true)
    {
        /** @var PurchaseFlowResult $flowResult */
        $flowResult = $this->purchaseFlow->validate($itemHolder, new PurchaseContext(clone $itemHolder, $itemHolder->getCustomer()));
        foreach ($flowResult->getWarning() as $warning) {
            $this->addWarning($warning->getMessage());
        }
        foreach ($flowResult->getErrors() as $error) {
            $this->addError($error->getMessage());
        }

        if (!$returnResponse) {
            return $flowResult;
        }

        if ($flowResult->hasError()) {
            log_info('Errorが発生したため購入エラー画面へ遷移します.', [$flowResult->getErrors()]);

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

        if ($flowResult->hasWarning()) {
            log_info('Warningが発生したため注文手続き画面へ遷移します.', [$flowResult->getWarning()]);

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

PurchaseFlowクラス該当処理

src/Eccube/Service/PurchaseFlow/PurchaseFlow.php

    public function validate(ItemHolderInterface $itemHolder, PurchaseContext $context)
    {
        $context->setFlowType($this->flowType);

        $this->calculateAll($itemHolder);

        $flowResult = new PurchaseFlowResult($itemHolder);

/**
- '@Eccube\Service\PurchaseFlow\Processor\DeliverySettingValidator' # 配送設定のチェック
- '@Eccube\Service\PurchaseFlow\Processor\ProductStatusValidator' # 商品の公開状態のチェック
- '@Eccube\Service\PurchaseFlow\Processor\PriceChangeValidator' # 商品価格の変更検知
*/
        foreach ($itemHolder->getItems() as $item) {
            foreach ($this->itemValidators as $itemValidator) {
                $result = $itemValidator->execute($item, $context);
                $flowResult->addProcessResult($result);
            }
        }

        $this->calculateAll($itemHolder);

/**
- '@Eccube\Service\PurchaseFlow\Processor\StockMultipleValidator' # 在庫チェック
- '@Eccube\Service\PurchaseFlow\Processor\SaleLimitMultipleValidator' # 購入数制限以上かチェック
- '@Eccube\Service\PurchaseFlow\Processor\EmptyItemsValidator'  # 空明細の削除処理
*/
        foreach ($this->itemHolderValidators as $itemHolderValidator) {
            $result = $itemHolderValidator->execute($itemHolder, $context);
            $flowResult->addProcessResult($result);
        }

        $this->calculateAll($itemHolder);

        foreach ($itemHolder->getItems() as $item) {
            foreach ($this->itemPreprocessors as $itemPreprocessor) {
                $itemPreprocessor->process($item, $context);
            }
        }

        $this->calculateAll($itemHolder);

/**
- '@Eccube\Service\PurchaseFlow\Processor\TaxProcessor'  # 税額の計算(商品明細に対して税額計算)
- '@Eccube\Service\PurchaseFlow\Processor\OrderNoProcessor' // order_noの値を決め。eccube_order_no_formatに値がなかったらorder.idが入る。
- '@Eccube\Service\PurchaseFlow\Processor\DeliveryFeePreprocessor' // 送料を表現するorder_itemデータ(DBデータのこと)を削除して送料オブジェクト(ItemOrder)をOrderとShippingオブジェクトに追加
- '@Eccube\Service\PurchaseFlow\Processor\DeliveryFeeFreeByShippingPreprocessor' # 購入金額がbase_info.delivery_free_amount以上、または購入個数がbase_info.delivery_free_quantity以上なら送料を無料にする(送料を表すOrderItemのquantityを0にセットする)
- '@Eccube\Service\PurchaseFlow\Processor\PaymentChargePreprocessor' # 支払い方法の手数料料金をOrderItemオブジェクトで表現してOrderオブジェクトに追加
- '@Eccube\Service\PurchaseFlow\Processor\TaxProcessor'  # 税額の計算(送料明細・手数料明細に対して税額を計算)
*/
        foreach ($this->itemHolderPreprocessors as $holderPreprocessor) {
            $result = $holderPreprocessor->process($itemHolder, $context);
            if ($result) {
                $flowResult->addProcessResult($result);
            }

            $this->calculateAll($itemHolder);
        }

/**
- '@Eccube\Service\PurchaseFlow\Processor\PointProcessor' # ポイント明細の追加
いったん削除してから、追加する
*/
        foreach ($this->discountProcessors as $discountProcessor) {
            $discountProcessor->removeDiscountItem($itemHolder, $context);
        }

        $this->calculateAll($itemHolder);


        foreach ($this->discountProcessors as $discountProcessor) {
            $result = $discountProcessor->addDiscountItem($itemHolder, $context);
            if ($result) {
                $flowResult->addProcessResult($result);
            }
            $this->calculateAll($itemHolder);
        }

/**
- '@Eccube\Service\PurchaseFlow\Processor\AddPointProcessor'  # 加算ポイントの計算
- '@Eccube\Service\PurchaseFlow\Processor\PaymentTotalLimitValidator' # 支払金額の上限チェック
- '@Eccube\Service\PurchaseFlow\Processor\PaymentTotalNegativeValidator' # 支払金額のマイナスチェック
- '@Eccube\Service\PurchaseFlow\Processor\PaymentChargeChangeValidator' # 手数料の変更検知
- '@Eccube\Service\PurchaseFlow\Processor\DeliveryFeeChangeValidator' # 送料の変更検知
*/
        foreach ($this->itemHolderPostValidators as $itemHolderPostValidator) {
            $result = $itemHolderPostValidator->execute($itemHolder, $context);
            $flowResult->addProcessResult($result);

            $this->calculateAll($itemHolder);
        }

        return $flowResult;
    }

バリデーション・プロセッサー

cart、shopping、orderの文脈時に受注、受注明細に対して実行している処理を1つ例として記載する。

税額計算プロセッサー

出所: https://github.com/EC-CUBE/ec-cube/pull/3420

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

    /**
     * @param ItemHolderInterface $itemHolder
     * @param PurchaseContext $context
     *
     * @throws \Doctrine\ORM\NoResultException
     */
    public function process(ItemHolderInterface $itemHolder, PurchaseContext $context)
    {
        if (!$itemHolder instanceof Order) {
            return;
        }

        foreach ($itemHolder->getOrderItems() as $item) {
            // 明細種別に応じて税区分, 税表示区分を設定する,
            $OrderItemType = $item->getOrderItemType();

            if (!$item->getTaxType()) {
                $item->setTaxType($this->getTaxType($OrderItemType));
            }
            if (!$item->getTaxDisplayType()) {
                $item->setTaxDisplayType($this->getTaxDisplayType($OrderItemType));
            }

            // 税区分: 非課税, 不課税
            if ($item->getTaxType()->getId() != TaxType::TAXATION) {
                $item->setTax(0);
                $item->setTaxRate(0);
                $item->setRoundingType(null);

                continue;
            }

            // 注文フロー内で税率が変更された場合を考慮し反映する
            // 受注管理画面内では既に登録された税率は自動で変更しない
            if ($context->isShoppingFlow() || $item->getRoundingType() === null) {
                $TaxRule = $item->getOrderItemType()->isProduct()
                    ? $this->taxRuleRepository->getByRule($item->getProduct(), $item->getProductClass())
                    : $this->taxRuleRepository->getByRule();

                $item->setTaxRate($TaxRule->getTaxRate())
                    ->setTaxAdjust($TaxRule->getTaxAdjust())
                    ->setRoundingType($TaxRule->getRoundingType());
            }

            // 税込表示の場合は, priceが税込金額のため割り戻す.
            if ($item->getTaxDisplayType()->getId() == TaxDisplayType::INCLUDED) {
                $tax = $this->taxRuleService->calcTaxIncluded(
                    $item->getPrice(), $item->getTaxRate(), $item->getRoundingType()->getId(),
                    $item->getTaxAdjust());
            } else {
                $tax = $this->taxRuleService->calcTax(
                    $item->getPrice(), $item->getTaxRate(), $item->getRoundingType()->getId(),
                    $item->getTaxAdjust());
            }

            $item->setTax($tax);
        }
    

税額計算ヘルパークラス

src/Eccube/Service/TaxRuleService.php

    /**
     * 税込金額から税金額を計算する
     *
     * @param  int    $price     計算対象の金額
     * @param  int    $taxRate   税率(%単位)
     * @param  int    $RoundingType  端数処理
     * @param  int    $taxAdjust 調整額
     *
     * @return float  税金額
     */
    public function calcTaxIncluded($price, $taxRate, $RoundingType, $taxAdjust = 0)
    {
        $tax = ($price - $taxAdjust) * $taxRate / (100 + $taxRate);

        return $this->roundByRoundingType($tax, $RoundingType);
    }

    /**
     * 税金額を計算する
     *
     * @param  int    $price     計算対象の金額
     * @param  int    $taxRate   税率(%単位)
     * @param  int    $RoundingType  端数処理
     * @param  int    $taxAdjust 調整額
     *
     * @return double 税金額
     */
    public function calcTax($price, $taxRate, $RoundingType, $taxAdjust = 0)
    {
        $tax = $price * $taxRate / 100;
        $roundTax = $this->roundByRoundingType($tax, $RoundingType);

        return $roundTax + $taxAdjust;
    }

税額の計算補足

setTaxType、setTaxDisplayTypeに設定した値で、TaxProcessorにおける税額の計算方法を決定する判断材料として利用されている。

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

    /**
     * Add charge item to item holder
     *
     * @param ItemHolderInterface $itemHolder
     */
    protected function addChargeItem(ItemHolderInterface $itemHolder)
    {
        /** @var Order $itemHolder */
        $OrderItemType = $this->orderItemTypeRepository->find(OrderItemType::CHARGE);
        $TaxDisplayType = $this->taxDisplayTypeRepository->find(TaxDisplayType::INCLUDED);
        $Taxation = $this->taxTypeRepository->find(TaxType::TAXATION);
        $item = new OrderItem();
        $item->setProductName($OrderItemType->getName())
            ->setQuantity(1)
            ->setPrice($itemHolder->getPayment()->getCharge())
            ->setOrderItemType($OrderItemType)
            ->setOrder($itemHolder)
            ->setTaxDisplayType($TaxDisplayType) // ここで支払い方法の手数料は
            ->setTaxType($Taxation) // どのような形で税額計算させるかの定義を設定
            ->setProcessorName(PaymentChargePreprocessor::class);
        $itemHolder->addItem($item);
    }

PurchaseFlow Factory

例えば、ShoppingControllerでpurcahseflowがshpping用に設定された状態で利用できるのだが、おそらくpp/config/eccube/services.yamlで定義されたものが利用されているのではないか、

app/config/eccube/services.yaml

services:
    # default configuration for services in *this* file
    _defaults:
        # automatically injects dependencies in your services
        autowire: true
        # automatically registers your services as commands, event subscribers, etc.
        autoconfigure: true
        # this means you cannot fetch services directly from the container via $container->get()
        # if you need to do this, you can override this setting on individual services
        public: false

        bind:
          $cartPurchaseFlow: '@eccube.purchase.flow.cart'
          $shoppingPurchaseFlow: '@eccube.purchase.flow.shopping'
          $orderPurchaseFlow: '@eccube.purchase.flow.order'
          $_orderStateMachine: '@state_machine.order'

利用箇所

class AbstractShoppingController extends AbstractController
{
    /**
     * @var PurchaseFlow
     */
    protected $purchaseFlow;

    /**
     * @param PurchaseFlow $shoppingPurchaseFlow // <-ここの記述でDIされる?要調査
     * @required
     */
    public function setPurchaseFlow(PurchaseFlow $shoppingPurchaseFlow)
    {
        $this->purchaseFlow = $shoppingPurchaseFlow;
    }