LoginSignup
10
7

More than 1 year has passed since last update.

EC-CUBE4 購入時オプションを追加

Last updated at Posted at 2021-09-16

概要

商品を購入(カートに入れる)時にオプションを指定できるようにするカスタマイズ方法です。
以下で手順は、ラベル 項目を追加する簡単なサンプルです。

エンティティの拡張

エンティティ拡張ファイルの作成

CartItem エンティティと OrderItem エンティティの2つを拡張します。

app/Customize/Entity/CartItemExtension.php
<?php

declare(strict_types=1);

namespace Customize\Entity;

use Doctrine\ORM\Mapping as ORM;
use Eccube\Annotation\EntityExtension;

/**
 * @EntityExtension("Eccube\Entity\CartItem")
 */
trait CartItemExtension
{
    /**
     * @var string|null
     *
     * @ORM\Column(name="label", type="text", nullable=true)
     */
    private $label;

    public function getLabel(): ?string
    {
        return $this->label;
    }

    public function setLabel(?string $label): self
    {
        $this->label = $label;

        return $this;
    }
}
app/Customize/Entity/OrderItemExtension.php
<?php

declare(strict_types=1);

namespace Customize\Entity;

use Doctrine\ORM\Mapping as ORM;
use Eccube\Annotation\EntityExtension;

/**
 * @EntityExtension("Eccube\Entity\OrderItem")
 */
trait OrderItemExtension
{
    /**
     * @var string|null
     *
     * @ORM\Column(name="label", type="text", nullable=true)
     */
    private $label;

    public function getLabel(): ?string
    {
        return $this->label;
    }

    public function setLabel(?string $label): self
    {
        $this->label = $label;

        return $this;
    }
}

エンティティ拡張をDBに反映

次のコマンドを実行して、エンティティの拡張内容をデータベースに反映します。

# プロキシークラスの生成
./bin/console eccube:generate:proxies

# キャッシュファイルの削除
./bin/console cache:clear --no-warmup

# 実行するDDLの確認
./bin/console doctrine:schema:update --dump-sql

# DDLを実行
./bin/console doctrine:schema:update --dump-sql --force

正常に完了すると dtb_cart_item.labeldtb_order_item.label とが追加されています。

スクリーンショット 2021-02-09 8.54.45.png

スクリーンショット 2021-02-09 8.55.16.png

フォームの拡張

商品ページの「カートに入れる」ボタンのフォームを拡張します。

スクリーンショット 2021-02-09 8.53.55.png

フォーム拡張ファイルの作成

app/Customize/Form/Extension/Front/AddCartTypeExtension.php
<?php

declare(strict_types=1);

namespace Customize\Form\Extension\Front;

use Eccube\Form\Type\AddCartType;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class AddCartTypeExtension extends AbstractTypeExtension
{
    public function getExtendedType(): string
    {
        return AddCartType::class;
    }

    public function getExtendedTypes(): iterable
    {
        return [AddCartType::class];
    }

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('label', TextType::class);
    }
}

追加フィールドをフォームに表示する

app/template/YOUR_TEMPLATE_CODE/Product/detail.twig
<div class="ec-numberInput"><span>{{ 'common.quantity'|trans }}</span>
    {{ form_widget(form.quantity) }}
    {{ form_errors(form.quantity) }}
</div>
+ <div class="ec-numberInput"><span>ラベル</span>
+     {{ form_widget(form.label) }}
+     {{ form_errors(form.label) }}
+ </div>

スクリーンショット 2021-02-09 9.01.24.png

カートアイテムの分類分けを変更

標準は、商品規格が同一あれば数量が更新され、商品規格が異なれば別商品としてカートアイテムが識別されます。
今回は、更に先ほど拡張したラベルを比較して、識別されるように変更します。

カートアイテムの比較クラスの作成

app/Customize/Service/Cart/ProductClassAndLabelComparator.php
<?php

declare(strict_types=1);

namespace Customize\Service\Cart;

use Eccube\Entity\CartItem;
use Eccube\Service\Cart\CartItemComparator;

class ProductClassAndLabelComparator implements CartItemComparator
{
    public function compare(CartItem $item1, CartItem $item2): bool
    {
        $classA = $item1->getProductClass();
        $classB = $item2->getProductClass();

        $a = $classA === null ? null : (string)$classA->getId();
        $b = $classB === null ? null : (string)$classB->getId();

        if ($a !== $b) {
            return false;
        }

        $labelA = $item1->getLabel();
        $labelB = $item2->getLabel();

        return $labelA === $labelB;
    }
}

カートアイテム比較クラスの変更

app/config/eccube/packages/cart.yaml
services:
    Eccube\Service\Cart\CartItemComparator:
        class: Customize\Service\Cart\ProductClassAndLabelComparator

カートサービスクラスの拡張

app/Customize/Service/CartService.php
<?php

declare(strict_types=1);

namespace Customize\Service;

use Eccube\Entity\CartItem;
use Eccube\Entity\ProductClass;

class CartService extends \Eccube\Service\CartService
{
    /**
     * @param array{string, string} $options
     */
    public function addCartItem(ProductClass $productClass, int $quantity = 1, array $options = []): bool
    {
        $classCategory1 = $productClass->getClassCategory1();
        if ($classCategory1 && ! $classCategory1->isVisible()) {
            return false;
        }

        $classCategory2 = $productClass->getClassCategory2();
        if ($classCategory2 && ! $classCategory2->isVisible()) {
            return false;
        }

        $cartItem = new CartItem();
        $cartItem->setProductClass($productClass)
                 ->setPrice($productClass->getPrice02IncTax())
                 ->setQuantity($quantity);

        if (isset($options['label'])) {
            $cartItem->setLable($options['label']);
        }

        $cartItems = $this->mergeAllCartItems([$cartItem]);
        $this->restoreCarts($cartItems);

        return true;
    }

    public function changeCartItemQuantity(int $cartItemId, int $quantity): bool
    {
        $itemFound = false;

        $carts = $this->getCarts();
        foreach ($carts as $cart) {
            foreach ($cart->getCartItems() as $cartItem) {
                if ($cartItem->getId() !== $cartItemId) {
                    continue;
                }

                $cartItem->setQuantity($quantity);
                $itemFound = true;

                break;
            }

            if ($itemFound) {
                break;
            }
        }

        if ($itemFound === false) {
            return false;
        }

        $cartItems = $this->mergeAllCartItems();
        $this->restoreCarts($cartItems);

        return true;
    }
}
app/config/eccube/services.yaml
    Eccube\Session\Storage\Handler\SameSiteNoneCompatSessionHandler:
        arguments:
            - '@native_file_session_handler'
+
+    Customize\Service\CartService:
+        decorates: Eccube\Service\CartService

カートに入れる処理を変更

<?php

declare(strict_types=1);

namespace Customize\Controller;

use Customize\Service\CartService as CustomizedCartService;
use Eccube\Controller\AbstractController;
use Eccube\Entity\CartItem;
use Eccube\Entity\Master\ProductStatus;
use Eccube\Entity\Product;
use Eccube\Event\EccubeEvents;
use Eccube\Event\EventArgs;
use Eccube\Form\Type\AddCartType;
use Eccube\Repository\ProductClassRepository;
use Eccube\Service\CartService;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Eccube\Service\PurchaseFlow\PurchaseFlow;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;

class FrontController extends AbstractController
{
    /** @var CartService */
    private $cartService;

    /** @var PurchaseFlow */
    protected $purchaseFlow;

    public function __construct(CartService $cartService, PurchaseFlow $purchaseFlow)
    {
        $this->cartService = $cartService;
        $this->purchaseFlow = $purchaseFlow;
    }

    /**
     * EC-CUBE標準の「カートに追加」を上書き
     *
     * @Route("/products/add_cart/{id}", name="product_add_cart", methods={"POST"}, requirements={"id" = "\d+"})
     */
    public function productAddCart(Request $request, Product $product): Response
    {
        if (! $this->isAvailableProduct($product)) {
            throw new NotFoundHttpException();
        }

        $builder = $this->formFactory->createNamedBuilder(
            '',
            AddCartType::class,
            null,
            [
                'product' => $product,
                'id_add_product_id' => false,
            ]
        );

        $event = new EventArgs(
            [
                'builder' => $builder,
                'Product' => $product,
            ],
            $request
        );
        $this->eventDispatcher->dispatch(EccubeEvents::FRONT_PRODUCT_CART_ADD_INITIALIZE, $event);

        $form = $builder->getForm();
        $form->handleRequest($request);

        if (! $form->isValid()) {
            throw new NotFoundHttpException();
        }

        $data = $form->getData();
        assert($data instanceof CartItem);

        log_info(
            'カート追加処理開始',
            [
                'product_id' => $product->getId(),
                'product_class_id' => $data->getProductClassId(),
                'quantity' => $data->getQuantity(),
            ]
        );

        assert($this->cartService instanceof CustomizedCartService);

        $this->cartService->addCartItem($data->getProductClass(), $data->getQuantity(), [
            'label' => $data->getLabel(),
        ]);

        $errorMessages = [];
        $carts = $this->cartService->getCarts();
        foreach ($carts as $Cart) {
            $result = $this->purchaseFlow->validate($Cart, new PurchaseContext($Cart, $this->getUser()));

            if ($result->hasError()) {
                $this->cartService->removeProduct($data->getProductClassId());
                foreach ($result->getErrors() as $error) {
                    $errorMessages[] = $error->getMessage();
                }
            }

            foreach ($result->getWarning() as $warning) {
                $errorMessages[] = $warning->getMessage();
            }
        }

        $this->cartService->save();

        log_info(
            'カート追加処理完了',
            [
                'product_id' => $product->getId(),
                'product_class_id' => $data->getProductClassId(),
                'quantity' => $data->getQuantity(),
            ]
        );

        $event = new EventArgs(
            [
                'form' => $form,
                'Product' => $product,
            ],
            $request
        );
        $this->eventDispatcher->dispatch(EccubeEvents::FRONT_PRODUCT_CART_ADD_COMPLETE, $event);

        if ($event->getResponse() !== null) {
            return $event->getResponse();
        }

        if ($request->isXmlHttpRequest()) {
            if (empty($errorMessages)) {
                return $this->json([
                    'done' => true,
                    'messages' => [trans('front.product.add_cart_complete')],
                ]);
            }

            return $this->json([
                'done' => false,
                'messages' => $errorMessages,
            ]);
        }

        foreach ($errorMessages as $errorMessage) {
            $this->addRequestError($errorMessage);
        }

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

    /**
     * @see Eccube\Controller\ProductController::checkVisibility()
     */
    private function isAvailableProduct(Product $product): bool
    {
        if ($this->session->has('_security_admin')) {
            return true;
        }

        $status = $product->getStatus();

        return $status !== null && $status->getId() === ProductStatus::DISPLAY_SHOW;
    }
}

カート一覧で数量を変更したときの対応

現在の実装は、product_class_id に対して数量を加減しているようです。
ということは、同じの商品(つまり product_class_id が同じ)で、ラベル違いの商品がカートに入っている場合、理想の結果にはなりませんので、カート数量変更は、カートアイテム毎に行うようにします。

// FIXME

注文サービスクラスの拡張

app/Customize/Service/OrderHelper.php
<?php

declare(strict_types=1);

namespace Customize\Service;

class OrderHelper extends \Eccube\Service\OrderHelper
{

}
app/Customize/Service/OrderPdfService.php
<?php

declare(strict_types=1);

namespace Customize\Service;

class OrderPdfService extends \Eccube\Service\OrderPdfService
{

}
app/config/eccube/services.yaml
    Eccube\Session\Storage\Handler\SameSiteNoneCompatSessionHandler:
        arguments:
            - '@native_file_session_handler'

    Customize\Service\CartService:
        decorates: Eccube\Service\CartService

+    Customize\Service\OrderHelper:
+        decorates: Eccube\Service\OrderHelper
+
+    Customize\Service\OrderPdfService:
+        decorates: Eccube\Service\OrderPdfService

注意点

一部のプロパティが private になっています。
そのため、オリジナルクラスを継承したサブクラスでも、アクセスできないプロパティがあります。
その場合は、カスタマイズ領域に用意したサービスクラスで DI します。

例:OrderPdfService

/** @var EccubeConfig */
private $eccubeConfig;

/**
    * @param EccubeConfig $eccubeConfig
    *
    * @required
    */
public function setEccubeConfig(EccubeConfig $eccubeConfig): void
{
    $this->eccubeConfig = $eccubeConfig;
}

Autowiring other Methods (e.g. Setters)

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7