src/Eccube/Controller/Admin/Product/ProductClassController.php line 89

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of EC-CUBE
  4.  *
  5.  * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
  6.  *
  7.  * http://www.ec-cube.co.jp/
  8.  *
  9.  * For the full copyright and license information, please view the LICENSE
  10.  * file that was distributed with this source code.
  11.  */
  12. namespace Eccube\Controller\Admin\Product;
  13. use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
  14. use Doctrine\ORM\NoResultException;
  15. use Eccube\Controller\AbstractController;
  16. use Eccube\Entity\ClassName;
  17. use Eccube\Entity\Product;
  18. use Eccube\Entity\ProductClass;
  19. use Eccube\Entity\ProductStock;
  20. use Eccube\Form\Type\Admin\ProductClassMatrixType;
  21. use Eccube\Repository\BaseInfoRepository;
  22. use Eccube\Repository\ClassCategoryRepository;
  23. use Eccube\Repository\ProductClassRepository;
  24. use Eccube\Repository\ProductRepository;
  25. use Eccube\Repository\TaxRuleRepository;
  26. use Eccube\Util\CacheUtil;
  27. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  28. use Symfony\Component\Form\Extension\Core\Type\FormType;
  29. use Symfony\Component\HttpFoundation\Request;
  30. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  31. use Symfony\Component\Routing\Annotation\Route;
  32. class ProductClassController extends AbstractController
  33. {
  34.     /**
  35.      * @var ProductRepository
  36.      */
  37.     protected $productRepository;
  38.     /**
  39.      * @var ProductClassRepository
  40.      */
  41.     protected $productClassRepository;
  42.     /**
  43.      * @var ClassCategoryRepository
  44.      */
  45.     protected $classCategoryRepository;
  46.     /**
  47.      * @var BaseInfoRepository
  48.      */
  49.     protected $baseInfoRepository;
  50.     /**
  51.      * @var TaxRuleRepository
  52.      */
  53.     protected $taxRuleRepository;
  54.     /**
  55.      * ProductClassController constructor.
  56.      *
  57.      * @param ProductClassRepository $productClassRepository
  58.      * @param ClassCategoryRepository $classCategoryRepository
  59.      */
  60.     public function __construct(
  61.         ProductRepository $productRepository,
  62.         ProductClassRepository $productClassRepository,
  63.         ClassCategoryRepository $classCategoryRepository,
  64.         BaseInfoRepository $baseInfoRepository,
  65.         TaxRuleRepository $taxRuleRepository
  66.     ) {
  67.         $this->productRepository $productRepository;
  68.         $this->productClassRepository $productClassRepository;
  69.         $this->classCategoryRepository $classCategoryRepository;
  70.         $this->baseInfoRepository $baseInfoRepository;
  71.         $this->taxRuleRepository $taxRuleRepository;
  72.     }
  73.     /**
  74.      * 商品規格が登録されていなければ新規登録, 登録されていれば更新画面を表示する
  75.      *
  76.      * @Route("/%eccube_admin_route%/product/product/class/{id}", requirements={"id" = "\d+"}, name="admin_product_product_class", methods={"GET", "POST"})
  77.      * @Template("@admin/Product/product_class.twig")
  78.      */
  79.     public function index(Request $request$idCacheUtil $cacheUtil)
  80.     {
  81.         $Product $this->findProduct($id);
  82.         if (!$Product) {
  83.             throw new NotFoundHttpException();
  84.         }
  85.         $ClassName1 null;
  86.         $ClassName2 null;
  87.         if ($Product->hasProductClass()) {
  88.             // 規格ありの商品は編集画面を表示する.
  89.             $ProductClasses $Product->getProductClasses()
  90.                 ->filter(function ($pc) {
  91.                     return $pc->getClassCategory1() !== null;
  92.                 });
  93.             // 設定されている規格名1, 2を取得(商品規格の規格分類には必ず同じ値がセットされている)
  94.             $FirstProductClass $ProductClasses->first();
  95.             $ClassName1 $FirstProductClass->getClassCategory1()->getClassName();
  96.             $ClassCategory2 $FirstProductClass->getClassCategory2();
  97.             $ClassName2 $ClassCategory2 $ClassCategory2->getClassName() : null;
  98.             // 規格名1/2から組み合わせを生成し, DBから取得した商品規格とマージする.
  99.             $ProductClasses $this->mergeProductClasses(
  100.                 $this->createProductClasses($ClassName1$ClassName2),
  101.                 $ProductClasses);
  102.             // 組み合わせのフォームを生成する.
  103.             $form $this->createMatrixForm($ProductClasses$ClassName1$ClassName2,
  104.                 ['product_classes_exist' => true]);
  105.             $form->handleRequest($request);
  106.             if ($form->isSubmitted() && $form->isValid()) {
  107.                 // フォームではtokenを無効化しているのでここで確認する.
  108.                 $this->isTokenValid();
  109.                 $this->saveProductClasses($Product$form['product_classes']->getData());
  110.                 $this->addSuccess('admin.common.save_complete''admin');
  111.                 $cacheUtil->clearDoctrineCache();
  112.                 if ($request->get('return_product_list')) {
  113.                     return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return_product_list' => true]);
  114.                 }
  115.                 return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
  116.             }
  117.         } else {
  118.             // 規格なし商品
  119.             $form $this->createMatrixForm();
  120.             $form->handleRequest($request);
  121.             if ($form->isSubmitted() && $form->isValid()) {
  122.                 // フォームではtokenを無効化しているのでここで確認する.
  123.                 $this->isTokenValid();
  124.                 // 登録,更新ボタンが押下されたかどうか.
  125.                 $isSave $form['save']->isClicked();
  126.                 // 規格名1/2から商品規格の組み合わせを生成する.
  127.                 $ClassName1 $form['class_name1']->getData();
  128.                 $ClassName2 $form['class_name2']->getData();
  129.                 $ProductClasses $this->createProductClasses($ClassName1$ClassName2);
  130.                 // 組み合わせのフォームを生成する.
  131.                 // class_name1, class_name2が取得できるのがsubmit後のため, フォームを再生成して組み合わせ部分を構築している
  132.                 // submit後だと, フォーム項目の追加やデータ変更が許可されないため.
  133.                 $form $this->createMatrixForm($ProductClasses$ClassName1$ClassName2,
  134.                     ['product_classes_exist' => true]);
  135.                 // 登録ボタン押下時
  136.                 if ($isSave) {
  137.                     $form->handleRequest($request);
  138.                     if ($form->isSubmitted() && $form->isValid()) {
  139.                         $this->saveProductClasses($Product$form['product_classes']->getData());
  140.                         $this->addSuccess('admin.common.save_complete''admin');
  141.                         $cacheUtil->clearDoctrineCache();
  142.                         if ($request->get('return_product_list')) {
  143.                             return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return_product_list' => true]);
  144.                         }
  145.                         return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
  146.                     }
  147.                 }
  148.             }
  149.         }
  150.         return [
  151.             'Product' => $Product,
  152.             'form' => $form->createView(),
  153.             'clearForm' => $this->createForm(FormType::class)->createView(),
  154.             'ClassName1' => $ClassName1,
  155.             'ClassName2' => $ClassName2,
  156.             'return_product_list' => $request->get('return_product_list') ? true false,
  157.         ];
  158.     }
  159.     /**
  160.      * 商品規格を初期化する.
  161.      *
  162.      * @Route("/%eccube_admin_route%/product/product/class/{id}/clear", requirements={"id" = "\d+"}, name="admin_product_product_class_clear", methods={"POST"})
  163.      */
  164.     public function clearProductClasses(Request $requestProduct $ProductCacheUtil $cacheUtil)
  165.     {
  166.         if (!$Product->hasProductClass()) {
  167.             return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
  168.         }
  169.         $form $this->createForm(FormType::class);
  170.         $form->handleRequest($request);
  171.         if ($form->isSubmitted() && $form->isValid()) {
  172.             $ProductClasses $this->productClassRepository->findBy([
  173.                 'Product' => $Product,
  174.             ]);
  175.             try {
  176.                 // デフォルト規格のみ有効にし、他は削除する
  177.                 foreach ($ProductClasses as $ProductClass) {
  178.                     $ProductClass->setVisible(false);
  179.                 }
  180.                 foreach ($ProductClasses as $ProductClass) {
  181.                     if (null === $ProductClass->getClassCategory1() && null === $ProductClass->getClassCategory2()) {
  182.                         $ProductClass->setVisible(true);
  183.                         break;
  184.                     }
  185.                 }
  186.                 foreach ($ProductClasses as $ProductClass) {
  187.                     if (!$ProductClass->isVisible()) {
  188.                         $this->entityManager->remove($ProductClass);
  189.                     }
  190.                 }
  191.                 $this->entityManager->flush();
  192.                 $this->addSuccess('admin.product.reset_complete''admin');
  193.                 $cacheUtil->clearDoctrineCache();
  194.             } catch (ForeignKeyConstraintViolationException $e) {
  195.                 log_error('商品規格の初期化失敗', [$e]);
  196.                 $message trans('admin.common.delete_error_foreign_key', ['%name%' => trans('admin.product.product_class')]);
  197.                 $this->addError($message'admin');
  198.             }
  199.         }
  200.         if ($request->get('return_product_list')) {
  201.             return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return_product_list' => true]);
  202.         }
  203.         return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
  204.     }
  205.     /**
  206.      * 規格名1/2から, 商品規格の組み合わせを生成する.
  207.      *
  208.      * @param ClassName $ClassName1
  209.      * @param ClassName|null $ClassName2
  210.      *
  211.      * @return array|ProductClass[]
  212.      */
  213.     protected function createProductClasses(ClassName $ClassName1ClassName $ClassName2 null)
  214.     {
  215.         $ProductClasses = [];
  216.         $ClassCategories1 $this->classCategoryRepository->findBy(['ClassName' => $ClassName1], ['sort_no' => 'DESC']);
  217.         $ClassCategories2 = [];
  218.         if ($ClassName2) {
  219.             $ClassCategories2 $this->classCategoryRepository->findBy(['ClassName' => $ClassName2],
  220.                 ['sort_no' => 'DESC']);
  221.         }
  222.         foreach ($ClassCategories1 as $ClassCategory1) {
  223.             // 規格1のみ
  224.             if (!$ClassName2) {
  225.                 $ProductClass = new ProductClass();
  226.                 $ProductClass->setClassCategory1($ClassCategory1);
  227.                 $ProductClasses[] = $ProductClass;
  228.                 continue;
  229.             }
  230.             // 規格1/2
  231.             foreach ($ClassCategories2 as $ClassCategory2) {
  232.                 $ProductClass = new ProductClass();
  233.                 $ProductClass->setClassCategory1($ClassCategory1);
  234.                 $ProductClass->setClassCategory2($ClassCategory2);
  235.                 $ProductClasses[] = $ProductClass;
  236.             }
  237.         }
  238.         return $ProductClasses;
  239.     }
  240.     /**
  241.      * 商品規格の配列をマージする.
  242.      *
  243.      * @param $ProductClassesForMatrix
  244.      * @param $ProductClasses
  245.      *
  246.      * @return array|ProductClass[]
  247.      */
  248.     protected function mergeProductClasses($ProductClassesForMatrix$ProductClasses)
  249.     {
  250.         $mergedProductClasses = [];
  251.         foreach ($ProductClassesForMatrix as $pcfm) {
  252.             foreach ($ProductClasses as $pc) {
  253.                 if ($pcfm->getClassCategory1()->getId() === $pc->getClassCategory1()->getId()) {
  254.                     $cc2fm $pcfm->getClassCategory2();
  255.                     $cc2 $pc->getClassCategory2();
  256.                     if (null === $cc2fm && null === $cc2) {
  257.                         $mergedProductClasses[] = $pc;
  258.                         continue 2;
  259.                     }
  260.                     if ($cc2fm && $cc2 && $cc2fm->getId() === $cc2->getId()) {
  261.                         $mergedProductClasses[] = $pc;
  262.                         continue 2;
  263.                     }
  264.                 }
  265.             }
  266.             $mergedProductClasses[] = $pcfm;
  267.         }
  268.         return $mergedProductClasses;
  269.     }
  270.     /**
  271.      * 商品規格を登録, 更新する.
  272.      *
  273.      * @param Product $Product
  274.      * @param array|ProductClass[] $ProductClasses
  275.      */
  276.     protected function saveProductClasses(Product $Product$ProductClasses = [])
  277.     {
  278.         foreach ($ProductClasses as $pc) {
  279.             // 新規登録時、チェックを入れていなければ更新しない
  280.             if (!$pc->getId() && !$pc->isVisible()) {
  281.                 continue;
  282.             }
  283.             // 無効から有効にした場合は, 過去の登録情報を検索.
  284.             if (!$pc->getId()) {
  285.                 /** @var ProductClass $ExistsProductClass */
  286.                 $ExistsProductClass $this->productClassRepository->findOneBy([
  287.                     'Product' => $Product,
  288.                     'ClassCategory1' => $pc->getClassCategory1(),
  289.                     'ClassCategory2' => $pc->getClassCategory2(),
  290.                 ]);
  291.                 // 過去の登録情報があればその情報を復旧する.
  292.                 if ($ExistsProductClass) {
  293.                     $ExistsProductClass->copyProperties($pc, [
  294.                         'id',
  295.                         'price01_inc_tax',
  296.                         'price02_inc_tax',
  297.                         'create_date',
  298.                         'update_date',
  299.                         'Creator',
  300.                         'ProductStock',
  301.                     ]);
  302.                     $pc $ExistsProductClass;
  303.                 }
  304.             }
  305.             // 更新時, チェックを外した場合はPOST内容を破棄してvisibleのみ更新する.
  306.             if ($pc->getId() && !$pc->isVisible()) {
  307.                 $this->entityManager->refresh($pc);
  308.                 $pc->setVisible(false);
  309.                 continue;
  310.             }
  311.             $pc->setProduct($Product);
  312.             $this->entityManager->persist($pc);
  313.             // 在庫の更新
  314.             $ProductStock $pc->getProductStock();
  315.             if (!$ProductStock) {
  316.                 $ProductStock = new ProductStock();
  317.                 $ProductStock->setProductClass($pc);
  318.                 $this->entityManager->persist($ProductStock);
  319.             }
  320.             $ProductStock->setStock($pc->isStockUnlimited() ? null $pc->getStock());
  321.             if ($this->baseInfoRepository->get()->isOptionProductTaxRule()) {
  322.                 $rate $pc->getTaxRate();
  323.                 $TaxRule $pc->getTaxRule();
  324.                 if (is_numeric($rate)) {
  325.                     if ($TaxRule) {
  326.                         $TaxRule->setTaxRate($rate);
  327.                     } else {
  328.                         // 現在の税率設定の計算方法を設定する
  329.                         $TaxRule $this->taxRuleRepository->newTaxRule();
  330.                         $TaxRule->setProduct($Product);
  331.                         $TaxRule->setProductClass($pc);
  332.                         $TaxRule->setTaxRate($rate);
  333.                         $TaxRule->setApplyDate(new \DateTime());
  334.                         $this->entityManager->persist($TaxRule);
  335.                     }
  336.                 } else {
  337.                     if ($TaxRule) {
  338.                         $this->taxRuleRepository->delete($TaxRule);
  339.                         $pc->setTaxRule(null);
  340.                     }
  341.                 }
  342.             }
  343.         }
  344.         // デフォルト規格を非表示にする.
  345.         $DefaultProductClass $this->productClassRepository->findOneBy([
  346.             'Product' => $Product,
  347.             'ClassCategory1' => null,
  348.             'ClassCategory2' => null,
  349.         ]);
  350.         $DefaultProductClass->setVisible(false);
  351.         $this->entityManager->flush();
  352.     }
  353.     /**
  354.      * 商品規格登録フォームを生成する.
  355.      *
  356.      * @param array $ProductClasses
  357.      * @param ClassName|null $ClassName1
  358.      * @param ClassName|null $ClassName2
  359.      * @param array $options
  360.      *
  361.      * @return \Symfony\Component\Form\FormInterface
  362.      */
  363.     protected function createMatrixForm(
  364.         $ProductClasses = [],
  365.         ClassName $ClassName1 null,
  366.         ClassName $ClassName2 null,
  367.         array $options = []
  368.     ) {
  369.         $options array_merge(['csrf_protection' => false], $options);
  370.         $builder $this->formFactory->createBuilder(ProductClassMatrixType::class, [
  371.             'product_classes' => $ProductClasses,
  372.             'class_name1' => $ClassName1,
  373.             'class_name2' => $ClassName2,
  374.         ], $options);
  375.         return $builder->getForm();
  376.     }
  377.     /**
  378.      * 商品を取得する.
  379.      * 商品規格はvisible=trueのものだけを取得し, 規格分類はsort_no=DESCでソートされている.
  380.      *
  381.      * @param $id
  382.      *
  383.      * @return Product|null
  384.      *
  385.      * @throws \Doctrine\ORM\NonUniqueResultException
  386.      */
  387.     protected function findProduct($id)
  388.     {
  389.         $qb $this->productRepository->createQueryBuilder('p')
  390.             ->addSelect(['pc''cc1''cc2'])
  391.             ->leftJoin('p.ProductClasses''pc')
  392.             ->leftJoin('pc.ClassCategory1''cc1')
  393.             ->leftJoin('pc.ClassCategory2''cc2')
  394.             ->where('p.id = :id')
  395.             ->andWhere('pc.visible = :pc_visible')
  396.             ->setParameter('id'$id)
  397.             ->setParameter('pc_visible'true)
  398.             ->orderBy('cc1.sort_no''DESC')
  399.             ->addOrderBy('cc2.sort_no''DESC');
  400.         try {
  401.             return $qb->getQuery()->getSingleResult();
  402.         } catch (NoResultException $e) {
  403.             return null;
  404.         }
  405.     }
  406. }