app/Customize/Controller/Admin/Product/ProductController.php line 157

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 Customize\Controller\Admin\Product;
  13. use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
  14. use Eccube\Common\Constant;
  15. use Eccube\Controller\AbstractController;
  16. use Eccube\Entity\BaseInfo;
  17. use Eccube\Entity\ExportCsvRow;
  18. use Eccube\Entity\Master\CsvType;
  19. use Eccube\Entity\Master\ProductStatus;
  20. use Eccube\Entity\Product;
  21. use Eccube\Entity\ProductCategory;
  22. use Eccube\Entity\ProductClass;
  23. use Eccube\Entity\ProductImage;
  24. use Eccube\Entity\ProductStock;
  25. use Eccube\Entity\ProductTag;
  26. use Eccube\Event\EccubeEvents;
  27. use Eccube\Event\EventArgs;
  28. use Eccube\Form\Type\Admin\ProductType;
  29. use Eccube\Form\Type\Admin\SearchProductType;
  30. use Eccube\Repository\BaseInfoRepository;
  31. use Eccube\Repository\CategoryRepository;
  32. use Eccube\Repository\Master\PageMaxRepository;
  33. use Eccube\Repository\Master\ProductStatusRepository;
  34. use Eccube\Repository\ProductClassRepository;
  35. use Eccube\Repository\ProductImageRepository;
  36. use Eccube\Repository\ProductRepository;
  37. use Eccube\Repository\TagRepository;
  38. use Eccube\Repository\TaxRuleRepository;
  39. use Eccube\Service\CsvExportService;
  40. use Eccube\Util\CacheUtil;
  41. use Eccube\Util\FormUtil;
  42. use Knp\Component\Pager\PaginatorInterface;
  43. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  44. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  45. use Symfony\Component\Filesystem\Filesystem;
  46. use Symfony\Component\HttpFoundation\File\File;
  47. use Symfony\Component\HttpFoundation\RedirectResponse;
  48. use Symfony\Component\HttpFoundation\Request;
  49. use Symfony\Component\HttpFoundation\Response;
  50. use Symfony\Component\HttpFoundation\StreamedResponse;
  51. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  52. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  53. use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
  54. use Symfony\Component\Routing\Annotation\Route;
  55. use Symfony\Component\Routing\RouterInterface;
  56. use Customize\Entity\ColorImage;
  57. class ProductController extends AbstractController
  58. {
  59.     /**
  60.      * @var CsvExportService
  61.      */
  62.     protected $csvExportService;
  63.     /**
  64.      * @var ProductClassRepository
  65.      */
  66.     protected $productClassRepository;
  67.     /**
  68.      * @var ProductImageRepository
  69.      */
  70.     protected $productImageRepository;
  71.     /**
  72.      * @var TaxRuleRepository
  73.      */
  74.     protected $taxRuleRepository;
  75.     /**
  76.      * @var CategoryRepository
  77.      */
  78.     protected $categoryRepository;
  79.     /**
  80.      * @var ProductRepository
  81.      */
  82.     protected $productRepository;
  83.     /**
  84.      * @var BaseInfo
  85.      */
  86.     protected $BaseInfo;
  87.     /**
  88.      * @var PageMaxRepository
  89.      */
  90.     protected $pageMaxRepository;
  91.     /**
  92.      * @var ProductStatusRepository
  93.      */
  94.     protected $productStatusRepository;
  95.     /**
  96.      * @var TagRepository
  97.      */
  98.     protected $tagRepository;
  99.     /**
  100.      * ProductController constructor.
  101.      *
  102.      * @param CsvExportService $csvExportService
  103.      * @param ProductClassRepository $productClassRepository
  104.      * @param ProductImageRepository $productImageRepository
  105.      * @param TaxRuleRepository $taxRuleRepository
  106.      * @param CategoryRepository $categoryRepository
  107.      * @param ProductRepository $productRepository
  108.      * @param BaseInfoRepository $baseInfoRepository
  109.      * @param PageMaxRepository $pageMaxRepository
  110.      * @param ProductStatusRepository $productStatusRepository
  111.      * @param TagRepository $tagRepository
  112.      */
  113.     public function __construct(
  114.         CsvExportService $csvExportService,
  115.         ProductClassRepository $productClassRepository,
  116.         ProductImageRepository $productImageRepository,
  117.         TaxRuleRepository $taxRuleRepository,
  118.         CategoryRepository $categoryRepository,
  119.         ProductRepository $productRepository,
  120.         BaseInfoRepository $baseInfoRepository,
  121.         PageMaxRepository $pageMaxRepository,
  122.         ProductStatusRepository $productStatusRepository,
  123.         TagRepository $tagRepository
  124.     ) {
  125.         $this->csvExportService $csvExportService;
  126.         $this->productClassRepository $productClassRepository;
  127.         $this->productImageRepository $productImageRepository;
  128.         $this->taxRuleRepository $taxRuleRepository;
  129.         $this->categoryRepository $categoryRepository;
  130.         $this->productRepository $productRepository;
  131.         $this->BaseInfo $baseInfoRepository->get();
  132.         $this->pageMaxRepository $pageMaxRepository;
  133.         $this->productStatusRepository $productStatusRepository;
  134.         $this->tagRepository $tagRepository;
  135.     }
  136.     /**
  137.      * @Route("/%eccube_admin_route%/product", name="admin_product", methods={"GET", "POST"})
  138.      * @Route("/%eccube_admin_route%/product/page/{page_no}", requirements={"page_no" = "\d+"}, name="admin_product_page", methods={"GET", "POST"})
  139.      * @Template("@admin/Product/index.twig")
  140.      */
  141.     public function index(Request $requestPaginatorInterface $paginator$page_no null)
  142.     {
  143.         $builder $this->formFactory
  144.             ->createBuilder(SearchProductType::class);
  145.         $event = new EventArgs(
  146.             [
  147.                 'builder' => $builder,
  148.             ],
  149.             $request
  150.         );
  151.         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_INDEX_INITIALIZE);
  152.         $searchForm $builder->getForm();
  153.         /**
  154.          * ページの表示件数は, 以下の順に優先される.
  155.          * - リクエストパラメータ
  156.          * - セッション
  157.          * - デフォルト値
  158.          * また, セッションに保存する際は mtb_page_maxと照合し, 一致した場合のみ保存する.
  159.          **/
  160.         $page_count $this->session->get('eccube.admin.product.search.page_count',
  161.             $this->eccubeConfig->get('eccube_default_page_count'));
  162.         $page_count_param = (int) $request->get('page_count');
  163.         $pageMaxis $this->pageMaxRepository->findAll();
  164.         if ($page_count_param) {
  165.             foreach ($pageMaxis as $pageMax) {
  166.                 if ($page_count_param == $pageMax->getName()) {
  167.                     $page_count $pageMax->getName();
  168.                     $this->session->set('eccube.admin.product.search.page_count'$page_count);
  169.                     break;
  170.                 }
  171.             }
  172.         }
  173.         if ('POST' === $request->getMethod()) {
  174.             $searchForm->handleRequest($request);
  175.             if ($searchForm->isValid()) {
  176.                 /**
  177.                  * 検索が実行された場合は, セッションに検索条件を保存する.
  178.                  * ページ番号は最初のページ番号に初期化する.
  179.                  */
  180.                 $page_no 1;
  181.                 $searchData $searchForm->getData();
  182.                 // 検索条件, ページ番号をセッションに保持.
  183.                 $this->session->set('eccube.admin.product.search'FormUtil::getViewData($searchForm));
  184.                 $this->session->set('eccube.admin.product.search.page_no'$page_no);
  185.             } else {
  186.                 // 検索エラーの際は, 詳細検索枠を開いてエラー表示する.
  187.                 return [
  188.                     'searchForm' => $searchForm->createView(),
  189.                     'pagination' => [],
  190.                     'pageMaxis' => $pageMaxis,
  191.                     'page_no' => $page_no,
  192.                     'page_count' => $page_count,
  193.                     'has_errors' => true,
  194.                 ];
  195.             }
  196.         } else {
  197.             if (null !== $page_no || $request->get('resume')) {
  198.                 /*
  199.                  * ページ送りの場合または、他画面から戻ってきた場合は, セッションから検索条件を復旧する.
  200.                  */
  201.                 if ($page_no) {
  202.                     // ページ送りで遷移した場合.
  203.                     $this->session->set('eccube.admin.product.search.page_no', (int) $page_no);
  204.                 } else {
  205.                     // 他画面から遷移した場合.
  206.                     $page_no $this->session->get('eccube.admin.product.search.page_no'1);
  207.                 }
  208.                 $viewData $this->session->get('eccube.admin.product.search', []);
  209.                 $searchData FormUtil::submitAndGetData($searchForm$viewData);
  210.             } else {
  211.                 /**
  212.                  * 初期表示の場合.
  213.                  */
  214.                 $page_no 1;
  215.                 // submit default value
  216.                 $viewData FormUtil::getViewData($searchForm);
  217.                 $searchData FormUtil::submitAndGetData($searchForm$viewData);
  218.                 // セッション中の検索条件, ページ番号を初期化.
  219.                 $this->session->set('eccube.admin.product.search'$viewData);
  220.                 $this->session->set('eccube.admin.product.search.page_no'$page_no);
  221.             }
  222.         }
  223.         $qb $this->productRepository->getQueryBuilderBySearchDataForAdmin($searchData);
  224.         $event = new EventArgs(
  225.             [
  226.                 'qb' => $qb,
  227.                 'searchData' => $searchData,
  228.             ],
  229.             $request
  230.         );
  231.         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_INDEX_SEARCH);
  232.         $sortKey $searchData['sortkey'];
  233.         if (empty($this->productRepository::COLUMNS[$sortKey]) || $sortKey == 'code' || $sortKey == 'status') {
  234.             $pagination $paginator->paginate(
  235.                 $qb,
  236.                 $page_no,
  237.                 $page_count
  238.             );
  239.         } else {
  240.             $pagination $paginator->paginate(
  241.                 $qb,
  242.                 $page_no,
  243.                 $page_count,
  244.                 ['wrap-queries' => true]
  245.             );
  246.         }
  247.         return [
  248.             'searchForm' => $searchForm->createView(),
  249.             'pagination' => $pagination,
  250.             'pageMaxis' => $pageMaxis,
  251.             'page_no' => $page_no,
  252.             'page_count' => $page_count,
  253.             'has_errors' => false,
  254.         ];
  255.     }
  256.     /**
  257.      * @Route("/%eccube_admin_route%/product/classes/{id}/load", name="admin_product_classes_load", methods={"GET"}, requirements={"id" = "\d+"}, methods={"GET"})
  258.      * @Template("@admin/Product/product_class_popup.twig")
  259.      * @ParamConverter("Product", options={"repository_method":"findWithSortedClassCategories"})
  260.      */
  261.     public function loadProductClasses(Request $requestProduct $Product)
  262.     {
  263.         if (!$request->isXmlHttpRequest() && $this->isTokenValid()) {
  264.             throw new BadRequestHttpException();
  265.         }
  266.         $data = [];
  267.         /** @var $Product ProductRepository */
  268.         if (!$Product) {
  269.             throw new NotFoundHttpException();
  270.         }
  271.         if ($Product->hasProductClass()) {
  272.             $class $Product->getProductClasses();
  273.             foreach ($class as $item) {
  274.                 $data[] = $item;
  275.             }
  276.         }
  277.         return [
  278.             'data' => $data,
  279.         ];
  280.     }
  281.     /**
  282.      * 画像アップロード時にリクエストされるメソッド.
  283.      *
  284.      * @see https://pqina.nl/filepond/docs/api/server/#process
  285.      * @Route("/%eccube_admin_route%/product/product/image/process", name="admin_product_image_process", methods={"POST"})
  286.      */
  287.     public function imageProcess(Request $request)
  288.     {
  289.         if (!$request->isXmlHttpRequest() && $this->isTokenValid()) {
  290.             throw new BadRequestHttpException();
  291.         }
  292.         $images $request->files->get('admin_product');
  293.         $allowExtensions = ['gif''jpg''jpeg''png'];
  294.         $files = [];
  295.         if (count($images) > 0) {
  296.             foreach ($images as $img) {
  297.                 foreach ($img as $image) {
  298.                     // ファイルフォーマット検証
  299.                     $mimeType $image->getMimeType();
  300.                     if (!== strpos($mimeType'image')) {
  301.                         throw new UnsupportedMediaTypeHttpException();
  302.                     }
  303.                     // 拡張子
  304.                     $extension $image->getClientOriginalExtension();
  305.                     if (!in_array(strtolower($extension), $allowExtensions)) {
  306.                         throw new UnsupportedMediaTypeHttpException();
  307.                     }
  308.                     $filename date('mdHis').uniqid('_').'.'.$extension;
  309.                     $image->move($this->eccubeConfig['eccube_temp_image_dir'], $filename);
  310.                     $files[] = $filename;
  311.                 }
  312.             }
  313.         }
  314.         $event = new EventArgs(
  315.             [
  316.                 'images' => $images,
  317.                 'files' => $files,
  318.             ],
  319.             $request
  320.         );
  321.         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_ADD_IMAGE_COMPLETE);
  322.         $files $event->getArgument('files');
  323.         return new Response(array_shift($files));
  324.     }
  325.     /**
  326.      * アップロード画像を取得する際にコールされるメソッド.
  327.      *
  328.      * @see https://pqina.nl/filepond/docs/api/server/#load
  329.      * @Route("/%eccube_admin_route%/product/product/image/load", name="admin_product_image_load", methods={"GET"})
  330.      */
  331.     public function imageLoad(Request $request)
  332.     {
  333.         if (!$request->isXmlHttpRequest()) {
  334.             throw new BadRequestHttpException();
  335.         }
  336.         $dirs = [
  337.             $this->eccubeConfig['eccube_save_image_dir'],
  338.             $this->eccubeConfig['eccube_temp_image_dir'],
  339.         ];
  340.         foreach ($dirs as $dir) {
  341.             if (strpos($request->query->get('source'), '..') !== false) {
  342.                 throw new NotFoundHttpException();
  343.             }
  344.             $image \realpath($dir.'/'.$request->query->get('source'));
  345.             $dir \realpath($dir);
  346.             if (\is_file($image) && \str_starts_with($image$dir)) {
  347.                 $file = new \SplFileObject($image);
  348.                 return $this->file($file$file->getBasename());
  349.             }
  350.         }
  351.         throw new NotFoundHttpException();
  352.     }
  353.     /**
  354.      * アップロード画像をすぐ削除する際にコールされるメソッド.
  355.      *
  356.      * @see https://pqina.nl/filepond/docs/api/server/#revert
  357.      * @Route("/%eccube_admin_route%/product/product/image/revert", name="admin_product_image_revert", methods={"DELETE"})
  358.      */
  359.     public function imageRevert(Request $request)
  360.     {
  361.         if (!$request->isXmlHttpRequest() && $this->isTokenValid()) {
  362.             throw new BadRequestHttpException();
  363.         }
  364.         $tempFile $this->eccubeConfig['eccube_temp_image_dir'].'/'.$request->getContent();
  365.         if (is_file($tempFile) && stripos(realpath($tempFile), $this->eccubeConfig['eccube_temp_image_dir']) === 0) {
  366.             $fs = new Filesystem();
  367.             $fs->remove($tempFile);
  368.             return new Response(nullResponse::HTTP_NO_CONTENT);
  369.         }
  370.         throw new NotFoundHttpException();
  371.     }
  372.     /**
  373.      * @Route("/%eccube_admin_route%/product/product/new", name="admin_product_product_new", methods={"GET", "POST"})
  374.      * @Route("/%eccube_admin_route%/product/product/{id}/edit", requirements={"id" = "\d+"}, name="admin_product_product_edit", methods={"GET", "POST"})
  375.      * @Template("@admin/Product/product.twig")
  376.      */
  377.     public function edit(Request $requestRouterInterface $routerCacheUtil $cacheUtil$id null)
  378.     {
  379.         $has_class false;
  380.         if (is_null($id)) {
  381.             $Product = new Product();
  382.             $ProductClass = new ProductClass();
  383.             $ProductStatus $this->productStatusRepository->find(ProductStatus::DISPLAY_HIDE);
  384.             $Product
  385.                 ->addProductClass($ProductClass)
  386.                 ->setStatus($ProductStatus);
  387.             $ProductClass
  388.                 ->setVisible(true)
  389.                 ->setStockUnlimited(true)
  390.                 ->setProduct($Product);
  391.             $ProductStock = new ProductStock();
  392.             $ProductClass->setProductStock($ProductStock);
  393.             $ProductStock->setProductClass($ProductClass);
  394.         } else {
  395.             $Product $this->productRepository->findWithSortedClassCategories($id);
  396.             $ProductClass null;
  397.             $ProductStock null;
  398.             if (!$Product) {
  399.                 throw new NotFoundHttpException();
  400.             }
  401.             // 規格無しの商品の場合は、デフォルト規格を表示用に取得する
  402.             $has_class $Product->hasProductClass();
  403.             if (!$has_class) {
  404.                 $ProductClasses $Product->getProductClasses();
  405.                 foreach ($ProductClasses as $pc) {
  406.                     if (!is_null($pc->getClassCategory1())) {
  407.                         continue;
  408.                     }
  409.                     if ($pc->isVisible()) {
  410.                         $ProductClass $pc;
  411.                         break;
  412.                     }
  413.                 }
  414.                 if ($this->BaseInfo->isOptionProductTaxRule() && $ProductClass->getTaxRule()) {
  415.                     $ProductClass->setTaxRate($ProductClass->getTaxRule()->getTaxRate());
  416.                 }
  417.                 $ProductStock $ProductClass->getProductStock();
  418.             }
  419.         }
  420.         $builder $this->formFactory
  421.             ->createBuilder(ProductType::class, $Product);
  422.         // 規格あり商品の場合、規格関連情報をFormから除外
  423.         if ($has_class) {
  424.             $builder->remove('class');
  425.         }
  426.         $event = new EventArgs(
  427.             [
  428.                 'builder' => $builder,
  429.                 'Product' => $Product,
  430.             ],
  431.             $request
  432.         );
  433.         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_EDIT_INITIALIZE);
  434.         $form $builder->getForm();
  435.         if (!$has_class) {
  436.             $ProductClass->setStockUnlimited($ProductClass->isStockUnlimited());
  437.             $form['class']->setData($ProductClass);
  438.         }
  439.         // ファイルの登録
  440.         $images = [];
  441.         $ProductImages $Product->getProductImage();
  442.         foreach ($ProductImages as $ProductImage) {
  443.             $images[] = $ProductImage->getFileName();
  444.         }
  445.         $form['images']->setData($images);
  446.         $categories = [];
  447.         $ProductCategories $Product->getProductCategories();
  448.         foreach ($ProductCategories as $ProductCategory) {
  449.             /* @var $ProductCategory \Eccube\Entity\ProductCategory */
  450.             $categories[] = $ProductCategory->getCategory();
  451.         }
  452.         $form['Category']->setData($categories);
  453.         $Tags $Product->getTags();
  454.         $form['Tag']->setData($Tags);
  455.         if ('POST' === $request->getMethod()) {
  456.             $form->handleRequest($request);
  457.             if ($form->isValid()) {
  458.                 log_info('商品登録開始', [$id]);
  459.                 $Product $form->getData();
  460.                 if (!$has_class) {
  461.                     $ProductClass $form['class']->getData();
  462.                     // 個別消費税
  463.                     if ($this->BaseInfo->isOptionProductTaxRule()) {
  464.                         if ($ProductClass->getTaxRate() !== null) {
  465.                             if ($ProductClass->getTaxRule()) {
  466.                                 $ProductClass->getTaxRule()->setTaxRate($ProductClass->getTaxRate());
  467.                             } else {
  468.                                 $taxrule $this->taxRuleRepository->newTaxRule();
  469.                                 $taxrule->setTaxRate($ProductClass->getTaxRate());
  470.                                 $taxrule->setApplyDate(new \DateTime());
  471.                                 $taxrule->setProduct($Product);
  472.                                 $taxrule->setProductClass($ProductClass);
  473.                                 $ProductClass->setTaxRule($taxrule);
  474.                             }
  475.                             $ProductClass->getTaxRule()->setTaxRate($ProductClass->getTaxRate());
  476.                         } else {
  477.                             if ($ProductClass->getTaxRule()) {
  478.                                 $this->taxRuleRepository->delete($ProductClass->getTaxRule());
  479.                                 $ProductClass->setTaxRule(null);
  480.                             }
  481.                         }
  482.                     }
  483.                     $this->entityManager->persist($ProductClass);
  484.                     // 在庫情報を作成
  485.                     if (!$ProductClass->isStockUnlimited()) {
  486.                         $ProductStock->setStock($ProductClass->getStock());
  487.                     } else {
  488.                         // 在庫無制限時はnullを設定
  489.                         $ProductStock->setStock(null);
  490.                     }
  491.                     $this->entityManager->persist($ProductStock);
  492.                 }
  493.                 // カテゴリの登録
  494.                 // 一度クリア
  495.                 /* @var $Product \Eccube\Entity\Product */
  496.                 foreach ($Product->getProductCategories() as $ProductCategory) {
  497.                     $Product->removeProductCategory($ProductCategory);
  498.                     $this->entityManager->remove($ProductCategory);
  499.                 }
  500.                 $this->entityManager->persist($Product);
  501.                 $this->entityManager->flush();
  502.                 $count 1;
  503.                 $Categories $form->get('Category')->getData();
  504.                 $categoriesIdList = [];
  505.                 foreach ($Categories as $Category) {
  506.                     foreach ($Category->getPath() as $ParentCategory) {
  507.                         if (!isset($categoriesIdList[$ParentCategory->getId()])) {
  508.                             $ProductCategory $this->createProductCategory($Product$ParentCategory$count);
  509.                             $this->entityManager->persist($ProductCategory);
  510.                             $count++;
  511.                             /* @var $Product \Eccube\Entity\Product */
  512.                             $Product->addProductCategory($ProductCategory);
  513.                             $categoriesIdList[$ParentCategory->getId()] = true;
  514.                         }
  515.                     }
  516.                     if (!isset($categoriesIdList[$Category->getId()])) {
  517.                         $ProductCategory $this->createProductCategory($Product$Category$count);
  518.                         $this->entityManager->persist($ProductCategory);
  519.                         $count++;
  520.                         /* @var $Product \Eccube\Entity\Product */
  521.                         $Product->addProductCategory($ProductCategory);
  522.                         $categoriesIdList[$Category->getId()] = true;
  523.                     }
  524.                 }
  525.                 // 画像の登録
  526.                 $add_images $form->get('add_images')->getData();
  527.                 foreach ($add_images as $add_image) {
  528.                     $ProductImage = new \Eccube\Entity\ProductImage();
  529.                     $ProductImage
  530.                         ->setFileName($add_image)
  531.                         ->setProduct($Product)
  532.                         ->setSortNo(1);
  533.                     $Product->addProductImage($ProductImage);
  534.                     $this->entityManager->persist($ProductImage);
  535.                     // 移動
  536.                     $file = new File($this->eccubeConfig['eccube_temp_image_dir'].'/'.$add_image);
  537.                     $file->move($this->eccubeConfig['eccube_save_image_dir']);
  538.                 }
  539.                 // 画像の削除
  540.                 $delete_images $form->get('delete_images')->getData();
  541.                 $fs = new Filesystem();
  542.                 foreach ($delete_images as $delete_image) {
  543.                     $ProductImage $this->productImageRepository->findOneBy([
  544.                         'Product' => $Product,
  545.                         'file_name' => $delete_image,
  546.                     ]);
  547.                     if ($ProductImage instanceof ProductImage) {
  548.                         $Product->removeProductImage($ProductImage);
  549.                         $this->entityManager->remove($ProductImage);
  550.                         $this->entityManager->flush();
  551.                         // 他に同じ画像を参照する商品がなければ画像ファイルを削除
  552.                         if (!$this->productImageRepository->findOneBy(['file_name' => $delete_image])) {
  553.                             $fs->remove($this->eccubeConfig['eccube_save_image_dir'].'/'.$delete_image);
  554.                         }
  555.                     } else {
  556.                         // 追加してすぐに削除した画像は、Entityに追加されない
  557.                         $fs->remove($this->eccubeConfig['eccube_temp_image_dir'].'/'.$delete_image);
  558.                     }
  559.                 }
  560.                 $this->entityManager->flush();
  561.                 if (array_key_exists('product_image'$request->request->get('admin_product'))) {
  562.                     $product_image $request->request->get('admin_product')['product_image'];
  563.                     foreach ($product_image as $sortNo => $filename) {
  564.                         $ProductImage $this->productImageRepository
  565.                             ->findOneBy([
  566.                                 'file_name' => pathinfo($filenamePATHINFO_BASENAME),
  567.                                 'Product' => $Product,
  568.                             ]);
  569.                         if ($ProductImage !== null) {
  570.                             $ProductImage->setSortNo($sortNo);
  571.                             $this->entityManager->persist($ProductImage);
  572.                         }
  573.                     }
  574.                     $this->entityManager->flush();
  575.                 }
  576.                 // 商品タグの登録
  577.                 // 商品タグを一度クリア
  578.                 $ProductTags $Product->getProductTag();
  579.                 foreach ($ProductTags as $ProductTag) {
  580.                     $Product->removeProductTag($ProductTag);
  581.                     $this->entityManager->remove($ProductTag);
  582.                 }
  583.                 // 商品タグの登録
  584.                 $Tags $form->get('Tag')->getData();
  585.                 foreach ($Tags as $Tag) {
  586.                     $ProductTag = new ProductTag();
  587.                     $ProductTag
  588.                         ->setProduct($Product)
  589.                         ->setTag($Tag);
  590.                     $Product->addProductTag($ProductTag);
  591.                     $this->entityManager->persist($ProductTag);
  592.                 }
  593.                 $Product->setUpdateDate(new \DateTime());
  594.                 $this->entityManager->flush();
  595.                 log_info('商品登録完了', [$id]);
  596.                 $event = new EventArgs(
  597.                     [
  598.                         'form' => $form,
  599.                         'Product' => $Product,
  600.                     ],
  601.                     $request
  602.                 );
  603.                 $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_EDIT_COMPLETE);
  604.                 $this->addSuccess('admin.common.save_complete''admin');
  605.                 if ($returnLink $form->get('return_link')->getData()) {
  606.                     try {
  607.                         // $returnLinkはpathの形式で渡される. pathが存在するかをルータでチェックする.
  608.                         $pattern '/^'.preg_quote($request->getBasePath(), '/').'/';
  609.                         $returnLink preg_replace($pattern''$returnLink);
  610.                         $result $router->match($returnLink);
  611.                         // パラメータのみ抽出
  612.                         $params array_filter($result, function ($key) {
  613.                             return !== \strpos($key'_');
  614.                         }, ARRAY_FILTER_USE_KEY);
  615.                         // pathからurlを再構築してリダイレクト.
  616.                         return $this->redirectToRoute($result['_route'], $params);
  617.                     } catch (\Exception $e) {
  618.                         // マッチしない場合はログ出力してスキップ.
  619.                         log_warning('URLの形式が不正です。');
  620.                     }
  621.                 }
  622.                 $cacheUtil->clearDoctrineCache();
  623.                 return $this->redirectToRoute('admin_product_product_edit', ['id' => $Product->getId()]);
  624.             }
  625.         }
  626.         // 検索結果の保持
  627.         $builder $this->formFactory
  628.             ->createBuilder(SearchProductType::class);
  629.         $event = new EventArgs(
  630.             [
  631.                 'builder' => $builder,
  632.                 'Product' => $Product,
  633.             ],
  634.             $request
  635.         );
  636.         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_EDIT_SEARCH);
  637.         $searchForm $builder->getForm();
  638.         if ('POST' === $request->getMethod()) {
  639.             $searchForm->handleRequest($request);
  640.         }
  641.         // Get Tags
  642.         $TagsList $this->tagRepository->getList();
  643.         // ツリー表示のため、ルートからのカテゴリを取得
  644.         $TopCategories $this->categoryRepository->getList(null);
  645.         $ChoicedCategoryIds array_map(function ($Category) {
  646.             return $Category->getId();
  647.         }, $form->get('Category')->getData());
  648.         return [
  649.             'Product' => $Product,
  650.             'Tags' => $Tags,
  651.             'TagsList' => $TagsList,
  652.             'form' => $form->createView(),
  653.             'searchForm' => $searchForm->createView(),
  654.             'has_class' => $has_class,
  655.             'id' => $id,
  656.             'TopCategories' => $TopCategories,
  657.             'ChoicedCategoryIds' => $ChoicedCategoryIds,
  658.             'ColorImages' => $Product->getColorImages(),
  659.         ];
  660.     }
  661.     /**
  662.      * @Route("/%eccube_admin_route%/product/product/{id}/delete", requirements={"id" = "\d+"}, name="admin_product_product_delete", methods={"DELETE"})
  663.      */
  664.     public function delete(Request $requestCacheUtil $cacheUtil$id null)
  665.     {
  666.         $this->isTokenValid();
  667.         $session $request->getSession();
  668.         $page_no intval($session->get('eccube.admin.product.search.page_no'));
  669.         $page_no $page_no $page_no Constant::ENABLED;
  670.         $success false;
  671.         if (!is_null($id)) {
  672.             /* @var $Product \Eccube\Entity\Product */
  673.             $Product $this->productRepository->find($id);
  674.             if (!$Product) {
  675.                 if ($request->isXmlHttpRequest()) {
  676.                     $message trans('admin.common.delete_error_already_deleted');
  677.                     return $this->json(['success' => $success'message' => $message]);
  678.                 } else {
  679.                     $this->deleteMessage();
  680.                     $rUrl $this->generateUrl('admin_product_page', ['page_no' => $page_no]).'?resume='.Constant::ENABLED;
  681.                     return $this->redirect($rUrl);
  682.                 }
  683.             }
  684.             if ($Product instanceof Product) {
  685.                 log_info('商品削除開始', [$id]);
  686.                 $deleteImages $Product->getProductImage();
  687.                 $ProductClasses $Product->getProductClasses();
  688.                 try {
  689.                     $this->productRepository->delete($Product);
  690.                     $this->entityManager->flush();
  691.                     $event = new EventArgs(
  692.                         [
  693.                             'Product' => $Product,
  694.                             'ProductClass' => $ProductClasses,
  695.                             'deleteImages' => $deleteImages,
  696.                         ],
  697.                         $request
  698.                     );
  699.                     $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_DELETE_COMPLETE);
  700.                     $deleteImages $event->getArgument('deleteImages');
  701.                     // 画像ファイルの削除(commit後に削除させる)
  702.                     /** @var ProductImage $deleteImage */
  703.                     foreach ($deleteImages as $deleteImage) {
  704.                         if ($this->productImageRepository->findOneBy(['file_name' => $deleteImage->getFileName()])) {
  705.                             continue;
  706.                         }
  707.                         try {
  708.                             $fs = new Filesystem();
  709.                             $fs->remove($this->eccubeConfig['eccube_save_image_dir'].'/'.$deleteImage);
  710.                         } catch (\Exception $e) {
  711.                             // エラーが発生しても無視する
  712.                         }
  713.                     }
  714.                     log_info('商品削除完了', [$id]);
  715.                     $success true;
  716.                     $message trans('admin.common.delete_complete');
  717.                     $cacheUtil->clearDoctrineCache();
  718.                 } catch (ForeignKeyConstraintViolationException $e) {
  719.                     log_info('商品削除エラー', [$id]);
  720.                     $message trans('admin.common.delete_error_foreign_key', ['%name%' => $Product->getName()]);
  721.                 }
  722.             } else {
  723.                 log_info('商品削除エラー', [$id]);
  724.                 $message trans('admin.common.delete_error');
  725.             }
  726.         } else {
  727.             log_info('商品削除エラー', [$id]);
  728.             $message trans('admin.common.delete_error');
  729.         }
  730.         if ($request->isXmlHttpRequest()) {
  731.             return $this->json(['success' => $success'message' => $message]);
  732.         } else {
  733.             if ($success) {
  734.                 $this->addSuccess($message'admin');
  735.             } else {
  736.                 $this->addError($message'admin');
  737.             }
  738.             $rUrl $this->generateUrl('admin_product_page', ['page_no' => $page_no]).'?resume='.Constant::ENABLED;
  739.             return $this->redirect($rUrl);
  740.         }
  741.     }
  742.     /**
  743.      * @Route("/%eccube_admin_route%/product/product/{id}/copy", requirements={"id" = "\d+"}, name="admin_product_product_copy", methods={"POST"})
  744.      */
  745.     public function copy(Request $request$id null)
  746.     {
  747.         $this->isTokenValid();
  748.         if (!is_null($id)) {
  749.             $Product $this->productRepository->find($id);
  750.             if ($Product instanceof Product) {
  751.                 $CopyProduct = clone $Product;
  752.                 $CopyProduct->copy();
  753.                 $ProductStatus $this->productStatusRepository->find(ProductStatus::DISPLAY_HIDE);
  754.                 $CopyProduct->setStatus($ProductStatus);
  755.                 $CopyProductCategories $CopyProduct->getProductCategories();
  756.                 foreach ($CopyProductCategories as $Category) {
  757.                     $this->entityManager->persist($Category);
  758.                 }
  759.                 // 規格あり商品の場合は, デフォルトの商品規格を取得し登録する.
  760.                 if ($CopyProduct->hasProductClass()) {
  761.                     $dummyClass $this->productClassRepository->findOneBy([
  762.                         'visible' => false,
  763.                         'ClassCategory1' => null,
  764.                         'ClassCategory2' => null,
  765.                         'Product' => $Product,
  766.                     ]);
  767.                     $dummyClass = clone $dummyClass;
  768.                     $dummyClass->setProduct($CopyProduct);
  769.                     $CopyProduct->addProductClass($dummyClass);
  770.                 }
  771.                 $CopyProductClasses $CopyProduct->getProductClasses();
  772.                 foreach ($CopyProductClasses as $Class) {
  773.                     $Stock $Class->getProductStock();
  774.                     $CopyStock = clone $Stock;
  775.                     $CopyStock->setProductClass($Class);
  776.                     $this->entityManager->persist($CopyStock);
  777.                     $TaxRule $Class->getTaxRule();
  778.                     if ($TaxRule) {
  779.                         $CopyTaxRule = clone $TaxRule;
  780.                         $CopyTaxRule->setProductClass($Class);
  781.                         $CopyTaxRule->setProduct($CopyProduct);
  782.                         $this->entityManager->persist($CopyTaxRule);
  783.                     }
  784.                     $this->entityManager->persist($Class);
  785.                 }
  786.                 $Images $CopyProduct->getProductImage();
  787.                 foreach ($Images as $Image) {
  788.                     // 画像ファイルを新規作成
  789.                     $extension pathinfo($Image->getFileName(), PATHINFO_EXTENSION);
  790.                     $filename date('mdHis').uniqid('_').'.'.$extension;
  791.                     try {
  792.                         $fs = new Filesystem();
  793.                         $fs->copy($this->eccubeConfig['eccube_save_image_dir'].'/'.$Image->getFileName(), $this->eccubeConfig['eccube_save_image_dir'].'/'.$filename);
  794.                     } catch (\Exception $e) {
  795.                         // エラーが発生しても無視する
  796.                     }
  797.                     $Image->setFileName($filename);
  798.                     $this->entityManager->persist($Image);
  799.                 }
  800.                 $Tags $CopyProduct->getProductTag();
  801.                 foreach ($Tags as $Tag) {
  802.                     $this->entityManager->persist($Tag);
  803.                 }
  804.                 $this->entityManager->persist($CopyProduct);
  805.                 $this->entityManager->flush();
  806.                 $event = new EventArgs(
  807.                     [
  808.                         'Product' => $Product,
  809.                         'CopyProduct' => $CopyProduct,
  810.                         'CopyProductCategories' => $CopyProductCategories,
  811.                         'CopyProductClasses' => $CopyProductClasses,
  812.                         'images' => $Images,
  813.                         'Tags' => $Tags,
  814.                     ],
  815.                     $request
  816.                 );
  817.                 $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_COPY_COMPLETE);
  818.                 $this->addSuccess('admin.product.copy_complete''admin');
  819.                 return $this->redirectToRoute('admin_product_product_edit', ['id' => $CopyProduct->getId()]);
  820.             } else {
  821.                 $this->addError('admin.product.copy_error''admin');
  822.             }
  823.         } else {
  824.             $msg trans('admin.product.copy_error');
  825.             $this->addError($msg'admin');
  826.         }
  827.         return $this->redirectToRoute('admin_product');
  828.     }
  829.     /**
  830.      * 商品CSVの出力.
  831.      *
  832.      * @Route("/%eccube_admin_route%/product/export", name="admin_product_export", methods={"GET"})
  833.      *
  834.      * @param Request $request
  835.      *
  836.      * @return StreamedResponse
  837.      */
  838.     public function export(Request $request)
  839.     {
  840.         // タイムアウトを無効にする.
  841.         set_time_limit(0);
  842.         // sql loggerを無効にする.
  843.         $em $this->entityManager;
  844.         $em->getConfiguration()->setSQLLogger(null);
  845.         $response = new StreamedResponse();
  846.         $response->setCallback(function () use ($request) {
  847.             // CSV種別を元に初期化.
  848.             $this->csvExportService->initCsvType(CsvType::CSV_TYPE_PRODUCT);
  849.             // 商品データ検索用のクエリビルダを取得.
  850.             $qb $this->csvExportService
  851.                 ->getProductQueryBuilder($request);
  852.             // ヘッダ行の出力.
  853.             $this->csvExportService->exportHeader();
  854.             // Get stock status
  855.             $isOutOfStock 0;
  856.             $session $request->getSession();
  857.             if ($session->has('eccube.admin.product.search')) {
  858.                 $searchData $session->get('eccube.admin.product.search', []);
  859.                 if (isset($searchData['stock_status']) && $searchData['stock_status'] === 0) {
  860.                     $isOutOfStock 1;
  861.                 }
  862.             }
  863.             // joinする場合はiterateが使えないため, select句をdistinctする.
  864.             // http://qiita.com/suin/items/2b1e98105fa3ef89beb7
  865.             // distinctのmysqlとpgsqlの挙動をあわせる.
  866.             // http://uedatakeshi.blogspot.jp/2010/04/distinct-oeder-by-postgresmysql.html
  867.             $qb->resetDQLPart('select');
  868.             if ($isOutOfStock) {
  869.                 $qb->select('p, pc')
  870.                     ->distinct();
  871.             } else {
  872.                 $qb->select('p')
  873.                     ->distinct();
  874.             }
  875.             // データ行の出力.
  876.             $this->csvExportService->setExportQueryBuilder($qb);
  877.             $this->csvExportService->exportData(function ($entityCsvExportService $csvService) use ($request) {
  878.                 $Csvs $csvService->getCsvs();
  879.                 /** @var $Product \Eccube\Entity\Product */
  880.                 $Product $entity;
  881.                 /** @var $ProductClasses \Eccube\Entity\ProductClass[] */
  882.                 $ProductClasses $Product->getProductClasses();
  883.                 foreach ($ProductClasses as $ProductClass) {
  884.                     $ExportCsvRow = new ExportCsvRow();
  885.                     // CSV出力項目と合致するデータを取得.
  886.                     foreach ($Csvs as $Csv) {
  887.                         // 商品データを検索.
  888.                         $ExportCsvRow->setData($csvService->getData($Csv$Product));
  889.                         if ($ExportCsvRow->isDataNull()) {
  890.                             // 商品規格情報を検索.
  891.                             $ExportCsvRow->setData($csvService->getData($Csv$ProductClass));
  892.                         }
  893.                         $event = new EventArgs(
  894.                             [
  895.                                 'csvService' => $csvService,
  896.                                 'Csv' => $Csv,
  897.                                 'ProductClass' => $ProductClass,
  898.                                 'ExportCsvRow' => $ExportCsvRow,
  899.                             ],
  900.                             $request
  901.                         );
  902.                         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_CSV_EXPORT);
  903.                         $ExportCsvRow->pushData();
  904.                     }
  905.                     // $row[] = number_format(memory_get_usage(true));
  906.                     // 出力.
  907.                     $csvService->fputcsv($ExportCsvRow->getRow());
  908.                 }
  909.             });
  910.         });
  911.         $now = new \DateTime();
  912.         $filename 'product_'.$now->format('YmdHis').'.csv';
  913.         $response->headers->set('Content-Type''application/octet-stream');
  914.         $response->headers->set('Content-Disposition''attachment; filename='.$filename);
  915.         log_info('商品CSV出力ファイル名', [$filename]);
  916.         return $response;
  917.     }
  918.     /**
  919.      * ProductCategory作成
  920.      *
  921.      * @param \Eccube\Entity\Product $Product
  922.      * @param \Eccube\Entity\Category $Category
  923.      * @param integer $count
  924.      *
  925.      * @return \Eccube\Entity\ProductCategory
  926.      */
  927.     private function createProductCategory($Product$Category$count)
  928.     {
  929.         $ProductCategory = new ProductCategory();
  930.         $ProductCategory->setProduct($Product);
  931.         $ProductCategory->setProductId($Product->getId());
  932.         $ProductCategory->setCategory($Category);
  933.         $ProductCategory->setCategoryId($Category->getId());
  934.         return $ProductCategory;
  935.     }
  936.     /**
  937.      * Bulk public action
  938.      *
  939.      * @Route("/%eccube_admin_route%/product/bulk/product-status/{id}", requirements={"id" = "\d+"}, name="admin_product_bulk_product_status", methods={"POST"})
  940.      *
  941.      * @param Request $request
  942.      * @param ProductStatus $ProductStatus
  943.      *
  944.      * @return RedirectResponse
  945.      */
  946.     public function bulkProductStatus(Request $requestProductStatus $ProductStatusCacheUtil $cacheUtil)
  947.     {
  948.         $this->isTokenValid();
  949.         /** @var Product[] $Products */
  950.         $Products $this->productRepository->findBy(['id' => $request->get('ids')]);
  951.         $count 0;
  952.         foreach ($Products as $Product) {
  953.             try {
  954.                 $Product->setStatus($ProductStatus);
  955.                 $this->productRepository->save($Product);
  956.                 $count++;
  957.             } catch (\Exception $e) {
  958.                 $this->addError($e->getMessage(), 'admin');
  959.             }
  960.         }
  961.         try {
  962.             if ($count) {
  963.                 $this->entityManager->flush();
  964.                 $msg $this->translator->trans('admin.product.bulk_change_status_complete', [
  965.                     '%count%' => $count,
  966.                     '%status%' => $ProductStatus->getName(),
  967.                 ]);
  968.                 $this->addSuccess($msg'admin');
  969.                 $cacheUtil->clearDoctrineCache();
  970.             }
  971.         } catch (\Exception $e) {
  972.             $this->addError($e->getMessage(), 'admin');
  973.         }
  974.         return $this->redirectToRoute('admin_product', ['resume' => Constant::ENABLED]);
  975.     }
  976.     /**
  977.      * Configure ColorImage
  978.      *
  979.      * @Route("/%eccube_admin_route%/product/configure/color-image", name="configure_color_image", methods={"POST"})
  980.      *
  981.      * @param Request $request
  982.      *
  983.      * @return Response
  984.      */
  985.     public function configureColorImage(Request $request)
  986.     {
  987.         $params $request->request->all();
  988.         $ColorImageRepository $this->entityManager->getRepository(\Customize\Entity\ColorImage::class);
  989.         $ColorImage $ColorImageRepository->findOneBy(['filename' => $params['filename']]);
  990.         $Product $this->productRepository->find($params['product_id']);
  991.         switch ($params['tag']) {
  992.             case 'set':
  993.                 if (! $ColorImage) {
  994.                     $ColorImage = new ColorImage();
  995.                     $ColorImage->setFilename($params['filename']);
  996.                     $ColorImage->setProduct($Product);
  997.                     $Product->addColorImage($ColorImage);
  998.                 }
  999.                 $ColorImage->setClassCategoryId($params['class_category']);
  1000.                 break;
  1001.             default:
  1002.                 if ($ColorImage) {
  1003.                     $ColorImage->setClassCategoryId(null);
  1004.                     $Product->removeColorImage($ColorImage);
  1005.                 }
  1006.         }
  1007.         $this->entityManager->persist($Product);
  1008.         $this->entityManager->persist($ColorImage);
  1009.         $this->entityManager->flush();
  1010.         return new Response('success');
  1011.     }
  1012. }