概要
商品を購入(カートに入れる)時にオプションを指定できるようにするカスタマイズ方法です。
以下で手順は、ラベル 項目を追加する簡単なサンプルです。
エンティティの拡張
エンティティ拡張ファイルの作成
CartItem
エンティティと OrderItem
エンティティの2つを拡張します。
<?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;
}
}
<?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.label
と dtb_order_item.label
とが追加されています。
フォームの拡張
商品ページの「カートに入れる」ボタンのフォームを拡張します。
フォーム拡張ファイルの作成
<?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);
}
}
追加フィールドをフォームに表示する
<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>
カートアイテムの分類分けを変更
標準は、商品規格が同一あれば数量が更新され、商品規格が異なれば別商品としてカートアイテムが識別されます。
今回は、更に先ほど拡張したラベルを比較して、識別されるように変更します。
カートアイテムの比較クラスの作成
<?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;
}
}
カートアイテム比較クラスの変更
services:
Eccube\Service\Cart\CartItemComparator:
class: Customize\Service\Cart\ProductClassAndLabelComparator
カートサービスクラスの拡張
<?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;
}
}
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
注文サービスクラスの拡張
<?php
declare(strict_types=1);
namespace Customize\Service;
class OrderHelper extends \Eccube\Service\OrderHelper
{
}
<?php
declare(strict_types=1);
namespace Customize\Service;
class OrderPdfService extends \Eccube\Service\OrderPdfService
{
}
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;
}