src/Eccube/Controller/Admin/Product/ProductController.php line 155

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