EC-CUBE4で商品項目を無限に追加できる機能を実装する方法です。
以下のように無限に商品項目を追加できるようにします。
今回は項目名と内容を登録してフロント側で表示させるだけのシンプルな実装となっています。
細かい説明はしていません。ファイルの設置場所はnamespaceを確認してください。
商品項目を保存するためProductItemのエンティティとリポジトリを用意
以下のように商品項目を保存するためProductItemエンティティを用意します。
<?php
namespace Customize\Entity;
use Doctrine\ORM\Mapping as ORM;
use Eccube\Entity\AbstractEntity;
use Eccube\Entity\Product;
/**
* Class ProductItem
* @package Customize\Entity
*
* @ORM\Table(name="ctb_product_item")
* @ORM\Entity(repositoryClass="Customize\Repository\ProductItemRepository")
*/
class ProductItem extends AbstractEntity
{
/**
* @ORM\Column(name="id", type="integer", options={"unsigned": true})
* @ORM\Id()
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="text")
*/
private $content;
/**
* @ORM\ManyToOne(targetEntity="Eccube\Entity\Product", inversedBy="productItems")
* @ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
private $Product;
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* @param string|null $name
* @return $this
*/
public function setName(?string $name): self
{
$this->name = $name;
return $this;
}
/**
* @return string|null
*/
public function getContent(): ?string
{
return $this->content;
}
/**
* @param string|null $content
* @return $this
*/
public function setContent(?string $content): self
{
$this->content = $content;
return $this;
}
/**
* @return Product|null
*/
public function getProduct(): ?Product
{
return $this->Product;
}
/**
* @param Product|null $product
* @return $this
*/
public function setProduct(?Product $product): self
{
$this->Product = $product;
return $this;
}
}
<?php
namespace Customize\Repository;
use Customize\Entity\ProductItem;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Eccube\Repository\AbstractRepository;
/**
* @method ProductItem|null find($id, $lockMode = null, $lockVersion = null)
* @method ProductItem|null findOneBy(array $criteria, array $orderBy = null)
* @method ProductItem[] findAll()
* @method ProductItem[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ProductItemRepository extends AbstractRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, ProductItem::class);
}
// /**
// * @return ProductItem[] Returns an array of ProductItem objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('p')
->andWhere('p.exampleField = :val')
->setParameter('val', $value)
->orderBy('p.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?ProductItem
{
return $this->createQueryBuilder('p')
->andWhere('p.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}
ProductTraitも用意
ProductTraitも用意します。
<?php
namespace Customize\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Eccube\Annotation\EntityExtension;
/**
* Trait ProductTrait
* @package Customize\Entity
*
* @EntityExtension("Eccube\Entity\Product")
*/
trait ProductTrait
{
/**
* @ORM\OneToMany(targetEntity="Customize\Entity\ProductItem", mappedBy="Product", cascade={"persist", "remove"})
*/
private $productItems;
/**
* @return Collection|ProductItem[]
*/
public function getProductItems(): Collection
{
if(null === $this->productItems) {
$this->productItems = new ArrayCollection();
}
return $this->productItems;
}
public function addProductItem(ProductItem $productItem): self
{
if(null === $this->productItems) {
$this->productItems = new ArrayCollection();
}
if (!$this->productItems->contains($productItem)) {
$this->productItems[] = $productItem;
$productItem->setProduct($this);
}
return $this;
}
public function removeProductItem(ProductItem $productItem): self
{
if(null === $this->productItems) {
$this->productItems = new ArrayCollection();
}
if ($this->productItems->contains($productItem)) {
$this->productItems->removeElement($productItem);
// set the owning side to null (unless already changed)
if ($productItem->getProduct() === $this) {
$productItem->setProduct(null);
}
}
return $this;
}
}
proxyを作成しておいてください。
bin/console eccube:generate:proxies
ProductItemTypeを用意
ProductItemTypeを用意します。
<?php
namespace Customize\Form\Type;
use Customize\Entity\ProductItem;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class ProductItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, [
'label' => '項目名',
'constraints' => [
new NotBlank()
]
])
->add('content', TextareaType::class, [
'label' => '内容',
'constraints' => [
new NotBlank()
],
'attr' => [
'rows' => 6
]
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ProductItem::class
]);
}
}
ProductTypeExtensionを用意
商品登録画面をカスタマイズするのでProductTypeExtensionを用意します。
<?php
namespace Customize\Form\Extension;
use Customize\Entity\ProductItem;
use Customize\Form\Type\ProductItemType;
use Doctrine\ORM\EntityManagerInterface;
use Eccube\Entity\Product;
use Eccube\Form\Type\Admin\ProductType;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class ProductTypeExtension extends AbstractTypeExtension
{
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(
EntityManagerInterface $entityManager
)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('ProductItems', CollectionType::class, [
'entry_type' => ProductItemType::class,
'required' => false,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true
]);
$builder
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event){
/** @var Product $data */
$data = $event->getData();
$form = $event->getForm();
$form->get('ProductItems')->setData($data->getProductItems());
});
$builder
->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event){
/** @var Product $data */
$data = $event->getData();
$form = $event->getForm();
// 商品項目をクリア
$productItems = $this->entityManager->getRepository(ProductItem::class)
->findBy(["Product" => $data]);
foreach ($productItems as $productItem) {
$this->entityManager->remove($productItem);
}
// 商品項目を登録
$productItems = $form->get('ProductItems')->getData();
/** @var ProductItem $productItem */
foreach($productItems as $productItem) {
$productItem->setProduct($data);
$this->entityManager->persist($productItem);
}
});
}
/**
* @inheritDoc
*/
public function getExtendedType()
{
// TODO: Implement getExtendedType() method.
return ProductType::class;
}
public static function getExtendedTypes()
{
return [ProductType::class];
}
}
商品項目を追加するテンプレートを用意
商品項目を追加するテンプレートを用意します。
product_item.twig
<script>
$(function () {
$('#product_items').appendTo('.c-contentsArea__primaryCol');
$('#product_items').removeClass('d-none');
$(document).on('click', 'button.product-item-delete', function () {
var data = $(this).data();
$('#product-item_' + data.id + '').remove();
});
$('.add-another-collection-widget').click(function (e) {
var list = jQuery(jQuery(this).attr('data-list-selector'));
// Try to find the counter of the list or use the length of the list
var counter = list.data('widget-counter') | list.children().length;
if(!counter) { counter = list.children().length }
// grab the prototype template
var newWidget = list.attr('data-prototype');
// replace the "__name__" used in the id and name of the prototype
// with a number that's unique to your emails
// end name attribute looks like name="contact[emails][2]"
newWidget = newWidget.replace(/__name__/g, counter);
// Increase the counter
counter++;
// And store it, the length cannot be used if deleting widgets is allowed
list.data('widget-counter', counter);
// create a new list element and add it to the list
var newElem = $(list.attr('data-widget-tags')).html(newWidget);
newElem.appendTo(list);
});
});
</script>
<div id="product_items" class="d-none">
<div class="card rounded border-0 mb-4">
<div class="card-header">
<span class="card-title">商品項目追加</span>
</div>
<div class="card-body">
<div class="row">
<div class="col mb-2">
{{ form_errors(form.ProductItems) }}
<ul id="product-item-group"
data-prototype="{% filter escape %}{{ include('@admin/Product/product_item_prototype.twig', {'form': form.ProductItems.vars.prototype}) }}{% endfilter %}"
data-widget-tags="{{ '<li class="mb-2 product-item"></li>'|e }}"
data-widget-counter="{{ form.ProductItems|length }}">
{% for child in form.ProductItems %}
<li class="mb-2 product-item">
{{ include('@admin/Product/product_item_prototype.twig', {'form': child, index: loop.index0}) }}
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="row">
<div class="col mb-2">
<button type="button"
class="btn btn-ec-conversion add-another-collection-widget"
data-list-selector="#product-item-group">追加
</button>
</div>
</div>
</div>
</div>
</div>
product_item_prototype.twig
<div id="product-item_{% if index is not defined %}__name__{% else %}{{ index }}{% endif %}">
<div class="row">
<div class="col-2">
<button type="button"
class="product-item-delete btn btn-ec-delete text-right"
data-id="{% if index is not defined %}__name__{% else %}{{ index }}{% endif %}">
削除
</button>
</div>
<div class="col-10">
<div class="row">
<div class="col-2">
{{ form.name.vars.label }}
</div>
<div class="col mb-2">
<div>
{{ form_widget(form.name) }}
{{ form_errors(form.name) }}
</div>
</div>
</div>
<div class="row">
<div class="col-2">
{{ form.content.vars.label }}
</div>
<div class="col mb-2">
<div>
{{ form_widget(form.content) }}
{{ form_errors(form.content) }}
</div>
</div>
</div>
</div>
</div>
<hr />
</div>
テンプレートを商品登録画面に表示するイベントを用意
テンプレートを商品登録画面に表示するイベントを用意します。
<?php
namespace Customize\EventSubscriber;
use Eccube\Event\TemplateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ProductSubscriber implements EventSubscriberInterface
{
/**
* @inheritDoc
*/
public static function getSubscribedEvents()
{
return [
'@admin/Product/product.twig' => 'onTemplateAdminProduct'
];
}
public function onTemplateAdminProduct(TemplateEvent $event)
{
$event->addSnippet('@admin/Product/product_item.twig');
}
}
以上で商品登録画面側は完成です。
登録した商品項目をフロントで表示
以下のようにフロントで商品項目を表示させます。
商品ページテンプレートにタグを追加
商品ページテンプレートに以下のタグを追加してください。
{# 商品項目 #}
{% if Product.ProductItems %}
<div class="ec-productRole__code">
{% for item in Product.ProductItems %}
<p>{{ item.name }}: <span class="product-code-default">{{ item.content }}</span></p>
{% endfor %}
</div>
{% endif %}
以上でフロントも完成です。
商品登録画面で商品項目を登録すると、フロント側で動的に商品項目が表示されるようになります。
ちなみに商品項目登録時の入力チェックは行っておりませんので独自に実装してください。
空のまま登録するとエラーとなります。


