<?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\Admin\Product;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\ORM\NoResultException;
use Eccube\Controller\AbstractController;
use Eccube\Entity\ClassName;
use Eccube\Entity\Product;
use Eccube\Entity\ProductClass;
use Eccube\Entity\ProductStock;
use Eccube\Form\Type\Admin\ProductClassMatrixType;
use Eccube\Repository\BaseInfoRepository;
use Eccube\Repository\ClassCategoryRepository;
use Eccube\Repository\ProductClassRepository;
use Eccube\Repository\ProductRepository;
use Eccube\Repository\TaxRuleRepository;
use Eccube\Util\CacheUtil;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
class ProductClassController extends AbstractController
{
/**
* @var ProductRepository
*/
protected $productRepository;
/**
* @var ProductClassRepository
*/
protected $productClassRepository;
/**
* @var ClassCategoryRepository
*/
protected $classCategoryRepository;
/**
* @var BaseInfoRepository
*/
protected $baseInfoRepository;
/**
* @var TaxRuleRepository
*/
protected $taxRuleRepository;
/**
* ProductClassController constructor.
*
* @param ProductClassRepository $productClassRepository
* @param ClassCategoryRepository $classCategoryRepository
*/
public function __construct(
ProductRepository $productRepository,
ProductClassRepository $productClassRepository,
ClassCategoryRepository $classCategoryRepository,
BaseInfoRepository $baseInfoRepository,
TaxRuleRepository $taxRuleRepository
) {
$this->productRepository = $productRepository;
$this->productClassRepository = $productClassRepository;
$this->classCategoryRepository = $classCategoryRepository;
$this->baseInfoRepository = $baseInfoRepository;
$this->taxRuleRepository = $taxRuleRepository;
}
/**
* 商品規格が登録されていなければ新規登録, 登録されていれば更新画面を表示する
*
* @Route("/%eccube_admin_route%/product/product/class/{id}", requirements={"id" = "\d+"}, name="admin_product_product_class", methods={"GET", "POST"})
* @Template("@admin/Product/product_class.twig")
*/
public function index(Request $request, $id, CacheUtil $cacheUtil)
{
$Product = $this->findProduct($id);
if (!$Product) {
throw new NotFoundHttpException();
}
$ClassName1 = null;
$ClassName2 = null;
if ($Product->hasProductClass()) {
// 規格ありの商品は編集画面を表示する.
$ProductClasses = $Product->getProductClasses()
->filter(function ($pc) {
return $pc->getClassCategory1() !== null;
});
// 設定されている規格名1, 2を取得(商品規格の規格分類には必ず同じ値がセットされている)
$FirstProductClass = $ProductClasses->first();
$ClassName1 = $FirstProductClass->getClassCategory1()->getClassName();
$ClassCategory2 = $FirstProductClass->getClassCategory2();
$ClassName2 = $ClassCategory2 ? $ClassCategory2->getClassName() : null;
// 規格名1/2から組み合わせを生成し, DBから取得した商品規格とマージする.
$ProductClasses = $this->mergeProductClasses(
$this->createProductClasses($ClassName1, $ClassName2),
$ProductClasses);
// 組み合わせのフォームを生成する.
$form = $this->createMatrixForm($ProductClasses, $ClassName1, $ClassName2,
['product_classes_exist' => true]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// フォームではtokenを無効化しているのでここで確認する.
$this->isTokenValid();
$this->saveProductClasses($Product, $form['product_classes']->getData());
$this->addSuccess('admin.common.save_complete', 'admin');
$cacheUtil->clearDoctrineCache();
if ($request->get('return_product_list')) {
return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return_product_list' => true]);
}
return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
}
} else {
// 規格なし商品
$form = $this->createMatrixForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// フォームではtokenを無効化しているのでここで確認する.
$this->isTokenValid();
// 登録,更新ボタンが押下されたかどうか.
$isSave = $form['save']->isClicked();
// 規格名1/2から商品規格の組み合わせを生成する.
$ClassName1 = $form['class_name1']->getData();
$ClassName2 = $form['class_name2']->getData();
$ProductClasses = $this->createProductClasses($ClassName1, $ClassName2);
// 組み合わせのフォームを生成する.
// class_name1, class_name2が取得できるのがsubmit後のため, フォームを再生成して組み合わせ部分を構築している
// submit後だと, フォーム項目の追加やデータ変更が許可されないため.
$form = $this->createMatrixForm($ProductClasses, $ClassName1, $ClassName2,
['product_classes_exist' => true]);
// 登録ボタン押下時
if ($isSave) {
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->saveProductClasses($Product, $form['product_classes']->getData());
$this->addSuccess('admin.common.save_complete', 'admin');
$cacheUtil->clearDoctrineCache();
if ($request->get('return_product_list')) {
return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return_product_list' => true]);
}
return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
}
}
}
}
return [
'Product' => $Product,
'form' => $form->createView(),
'clearForm' => $this->createForm(FormType::class)->createView(),
'ClassName1' => $ClassName1,
'ClassName2' => $ClassName2,
'return_product_list' => $request->get('return_product_list') ? true : false,
];
}
/**
* 商品規格を初期化する.
*
* @Route("/%eccube_admin_route%/product/product/class/{id}/clear", requirements={"id" = "\d+"}, name="admin_product_product_class_clear", methods={"POST"})
*/
public function clearProductClasses(Request $request, Product $Product, CacheUtil $cacheUtil)
{
if (!$Product->hasProductClass()) {
return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
}
$form = $this->createForm(FormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$ProductClasses = $this->productClassRepository->findBy([
'Product' => $Product,
]);
try {
// デフォルト規格のみ有効にし、他は削除する
foreach ($ProductClasses as $ProductClass) {
$ProductClass->setVisible(false);
}
foreach ($ProductClasses as $ProductClass) {
if (null === $ProductClass->getClassCategory1() && null === $ProductClass->getClassCategory2()) {
$ProductClass->setVisible(true);
break;
}
}
foreach ($ProductClasses as $ProductClass) {
if (!$ProductClass->isVisible()) {
$this->entityManager->remove($ProductClass);
}
}
$this->entityManager->flush();
$this->addSuccess('admin.product.reset_complete', 'admin');
$cacheUtil->clearDoctrineCache();
} catch (ForeignKeyConstraintViolationException $e) {
log_error('商品規格の初期化失敗', [$e]);
$message = trans('admin.common.delete_error_foreign_key', ['%name%' => trans('admin.product.product_class')]);
$this->addError($message, 'admin');
}
}
if ($request->get('return_product_list')) {
return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return_product_list' => true]);
}
return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
}
/**
* 規格名1/2から, 商品規格の組み合わせを生成する.
*
* @param ClassName $ClassName1
* @param ClassName|null $ClassName2
*
* @return array|ProductClass[]
*/
protected function createProductClasses(ClassName $ClassName1, ClassName $ClassName2 = null)
{
$ProductClasses = [];
$ClassCategories1 = $this->classCategoryRepository->findBy(['ClassName' => $ClassName1], ['sort_no' => 'DESC']);
$ClassCategories2 = [];
if ($ClassName2) {
$ClassCategories2 = $this->classCategoryRepository->findBy(['ClassName' => $ClassName2],
['sort_no' => 'DESC']);
}
foreach ($ClassCategories1 as $ClassCategory1) {
// 規格1のみ
if (!$ClassName2) {
$ProductClass = new ProductClass();
$ProductClass->setClassCategory1($ClassCategory1);
$ProductClasses[] = $ProductClass;
continue;
}
// 規格1/2
foreach ($ClassCategories2 as $ClassCategory2) {
$ProductClass = new ProductClass();
$ProductClass->setClassCategory1($ClassCategory1);
$ProductClass->setClassCategory2($ClassCategory2);
$ProductClasses[] = $ProductClass;
}
}
return $ProductClasses;
}
/**
* 商品規格の配列をマージする.
*
* @param $ProductClassesForMatrix
* @param $ProductClasses
*
* @return array|ProductClass[]
*/
protected function mergeProductClasses($ProductClassesForMatrix, $ProductClasses)
{
$mergedProductClasses = [];
foreach ($ProductClassesForMatrix as $pcfm) {
foreach ($ProductClasses as $pc) {
if ($pcfm->getClassCategory1()->getId() === $pc->getClassCategory1()->getId()) {
$cc2fm = $pcfm->getClassCategory2();
$cc2 = $pc->getClassCategory2();
if (null === $cc2fm && null === $cc2) {
$mergedProductClasses[] = $pc;
continue 2;
}
if ($cc2fm && $cc2 && $cc2fm->getId() === $cc2->getId()) {
$mergedProductClasses[] = $pc;
continue 2;
}
}
}
$mergedProductClasses[] = $pcfm;
}
return $mergedProductClasses;
}
/**
* 商品規格を登録, 更新する.
*
* @param Product $Product
* @param array|ProductClass[] $ProductClasses
*/
protected function saveProductClasses(Product $Product, $ProductClasses = [])
{
foreach ($ProductClasses as $pc) {
// 新規登録時、チェックを入れていなければ更新しない
if (!$pc->getId() && !$pc->isVisible()) {
continue;
}
// 無効から有効にした場合は, 過去の登録情報を検索.
if (!$pc->getId()) {
/** @var ProductClass $ExistsProductClass */
$ExistsProductClass = $this->productClassRepository->findOneBy([
'Product' => $Product,
'ClassCategory1' => $pc->getClassCategory1(),
'ClassCategory2' => $pc->getClassCategory2(),
]);
// 過去の登録情報があればその情報を復旧する.
if ($ExistsProductClass) {
$ExistsProductClass->copyProperties($pc, [
'id',
'price01_inc_tax',
'price02_inc_tax',
'create_date',
'update_date',
'Creator',
'ProductStock',
]);
$pc = $ExistsProductClass;
}
}
// 更新時, チェックを外した場合はPOST内容を破棄してvisibleのみ更新する.
if ($pc->getId() && !$pc->isVisible()) {
$this->entityManager->refresh($pc);
$pc->setVisible(false);
continue;
}
$pc->setProduct($Product);
$this->entityManager->persist($pc);
// 在庫の更新
$ProductStock = $pc->getProductStock();
if (!$ProductStock) {
$ProductStock = new ProductStock();
$ProductStock->setProductClass($pc);
$this->entityManager->persist($ProductStock);
}
$ProductStock->setStock($pc->isStockUnlimited() ? null : $pc->getStock());
if ($this->baseInfoRepository->get()->isOptionProductTaxRule()) {
$rate = $pc->getTaxRate();
$TaxRule = $pc->getTaxRule();
if (is_numeric($rate)) {
if ($TaxRule) {
$TaxRule->setTaxRate($rate);
} else {
// 現在の税率設定の計算方法を設定する
$TaxRule = $this->taxRuleRepository->newTaxRule();
$TaxRule->setProduct($Product);
$TaxRule->setProductClass($pc);
$TaxRule->setTaxRate($rate);
$TaxRule->setApplyDate(new \DateTime());
$this->entityManager->persist($TaxRule);
}
} else {
if ($TaxRule) {
$this->taxRuleRepository->delete($TaxRule);
$pc->setTaxRule(null);
}
}
}
}
// デフォルト規格を非表示にする.
$DefaultProductClass = $this->productClassRepository->findOneBy([
'Product' => $Product,
'ClassCategory1' => null,
'ClassCategory2' => null,
]);
$DefaultProductClass->setVisible(false);
$this->entityManager->flush();
}
/**
* 商品規格登録フォームを生成する.
*
* @param array $ProductClasses
* @param ClassName|null $ClassName1
* @param ClassName|null $ClassName2
* @param array $options
*
* @return \Symfony\Component\Form\FormInterface
*/
protected function createMatrixForm(
$ProductClasses = [],
ClassName $ClassName1 = null,
ClassName $ClassName2 = null,
array $options = []
) {
$options = array_merge(['csrf_protection' => false], $options);
$builder = $this->formFactory->createBuilder(ProductClassMatrixType::class, [
'product_classes' => $ProductClasses,
'class_name1' => $ClassName1,
'class_name2' => $ClassName2,
], $options);
return $builder->getForm();
}
/**
* 商品を取得する.
* 商品規格はvisible=trueのものだけを取得し, 規格分類はsort_no=DESCでソートされている.
*
* @param $id
*
* @return Product|null
*
* @throws \Doctrine\ORM\NonUniqueResultException
*/
protected function findProduct($id)
{
$qb = $this->productRepository->createQueryBuilder('p')
->addSelect(['pc', 'cc1', 'cc2'])
->leftJoin('p.ProductClasses', 'pc')
->leftJoin('pc.ClassCategory1', 'cc1')
->leftJoin('pc.ClassCategory2', 'cc2')
->where('p.id = :id')
->andWhere('pc.visible = :pc_visible')
->setParameter('id', $id)
->setParameter('pc_visible', true)
->orderBy('cc1.sort_no', 'DESC')
->addOrderBy('cc2.sort_no', 'DESC');
try {
return $qb->getQuery()->getSingleResult();
} catch (NoResultException $e) {
return null;
}
}
}