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

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.                 if (!$has_class) {
  460.                     $ProductClass $form['class']->getData();
  461.                     // 個別消費税
  462.                     if ($this->BaseInfo->isOptionProductTaxRule()) {
  463.                         if ($ProductClass->getTaxRate() !== null) {
  464.                             if ($ProductClass->getTaxRule()) {
  465.                                 $ProductClass->getTaxRule()->setTaxRate($ProductClass->getTaxRate());
  466.                             } else {
  467.                                 $taxrule $this->taxRuleRepository->newTaxRule();
  468.                                 $taxrule->setTaxRate($ProductClass->getTaxRate());
  469.                                 $taxrule->setApplyDate(new \DateTime());
  470.                                 $taxrule->setProduct($Product);
  471.                                 $taxrule->setProductClass($ProductClass);
  472.                                 $ProductClass->setTaxRule($taxrule);
  473.                             }
  474.                             $ProductClass->getTaxRule()->setTaxRate($ProductClass->getTaxRate());
  475.                         } else {
  476.                             if ($ProductClass->getTaxRule()) {
  477.                                 $this->taxRuleRepository->delete($ProductClass->getTaxRule());
  478.                                 $ProductClass->setTaxRule(null);
  479.                             }
  480.                         }
  481.                     }
  482.                     $this->entityManager->persist($ProductClass);
  483.                     // 在庫情報を作成
  484.                     if (!$ProductClass->isStockUnlimited()) {
  485.                         $ProductStock->setStock($ProductClass->getStock());
  486.                     } else {
  487.                         // 在庫無制限時はnullを設定
  488.                         $ProductStock->setStock(null);
  489.                     }
  490.                     $this->entityManager->persist($ProductStock);
  491.                 }
  492.                 // カテゴリの登録
  493.                 // 一度クリア
  494.                 /* @var $Product \Eccube\Entity\Product */
  495.                 foreach ($Product->getProductCategories() as $ProductCategory) {
  496.                     $Product->removeProductCategory($ProductCategory);
  497.                     $this->entityManager->remove($ProductCategory);
  498.                 }
  499.                 $this->entityManager->persist($Product);
  500.                 $this->entityManager->flush();
  501.                 $count 1;
  502.                 $Categories $form->get('Category')->getData();
  503.                 $categoriesIdList = [];
  504.                 foreach ($Categories as $Category) {
  505.                     foreach ($Category->getPath() as $ParentCategory) {
  506.                         if (!isset($categoriesIdList[$ParentCategory->getId()])) {
  507.                             $ProductCategory $this->createProductCategory($Product$ParentCategory$count);
  508.                             $this->entityManager->persist($ProductCategory);
  509.                             $count++;
  510.                             /* @var $Product \Eccube\Entity\Product */
  511.                             $Product->addProductCategory($ProductCategory);
  512.                             $categoriesIdList[$ParentCategory->getId()] = true;
  513.                         }
  514.                     }
  515.                     if (!isset($categoriesIdList[$Category->getId()])) {
  516.                         $ProductCategory $this->createProductCategory($Product$Category$count);
  517.                         $this->entityManager->persist($ProductCategory);
  518.                         $count++;
  519.                         /* @var $Product \Eccube\Entity\Product */
  520.                         $Product->addProductCategory($ProductCategory);
  521.                         $categoriesIdList[$Category->getId()] = true;
  522.                     }
  523.                 }
  524.                 // 画像の登録
  525.                 $add_images $form->get('add_images')->getData();
  526.                 foreach ($add_images as $add_image) {
  527.                     $ProductImage = new \Eccube\Entity\ProductImage();
  528.                     $ProductImage
  529.                         ->setFileName($add_image)
  530.                         ->setProduct($Product)
  531.                         ->setSortNo(1);
  532.                     $Product->addProductImage($ProductImage);
  533.                     $this->entityManager->persist($ProductImage);
  534.                     // 移動
  535.                     $file = new File($this->eccubeConfig['eccube_temp_image_dir'].'/'.$add_image);
  536.                     $file->move($this->eccubeConfig['eccube_save_image_dir']);
  537.                 }
  538.                 // 画像の削除
  539.                 $delete_images $form->get('delete_images')->getData();
  540.                 $fs = new Filesystem();
  541.                 foreach ($delete_images as $delete_image) {
  542.                     $ProductImage $this->productImageRepository->findOneBy([
  543.                         'Product' => $Product,
  544.                         'file_name' => $delete_image,
  545.                     ]);
  546.                     if ($ProductImage instanceof ProductImage) {
  547.                         $Product->removeProductImage($ProductImage);
  548.                         $this->entityManager->remove($ProductImage);
  549.                         $this->entityManager->flush();
  550.                         // 他に同じ画像を参照する商品がなければ画像ファイルを削除
  551.                         if (!$this->productImageRepository->findOneBy(['file_name' => $delete_image])) {
  552.                             $fs->remove($this->eccubeConfig['eccube_save_image_dir'].'/'.$delete_image);
  553.                         }
  554.                     } else {
  555.                         // 追加してすぐに削除した画像は、Entityに追加されない
  556.                         $fs->remove($this->eccubeConfig['eccube_temp_image_dir'].'/'.$delete_image);
  557.                     }
  558.                 }
  559.                 $this->entityManager->flush();
  560.                 if (array_key_exists('product_image'$request->request->get('admin_product'))) {
  561.                     $product_image $request->request->get('admin_product')['product_image'];
  562.                     foreach ($product_image as $sortNo => $filename) {
  563.                         $ProductImage $this->productImageRepository
  564.                             ->findOneBy([
  565.                                 'file_name' => pathinfo($filenamePATHINFO_BASENAME),
  566.                                 'Product' => $Product,
  567.                             ]);
  568.                         if ($ProductImage !== null) {
  569.                             $ProductImage->setSortNo($sortNo);
  570.                             $this->entityManager->persist($ProductImage);
  571.                         }
  572.                     }
  573.                     $this->entityManager->flush();
  574.                 }
  575.                 // 商品タグの登録
  576.                 // 商品タグを一度クリア
  577.                 $ProductTags $Product->getProductTag();
  578.                 foreach ($ProductTags as $ProductTag) {
  579.                     $Product->removeProductTag($ProductTag);
  580.                     $this->entityManager->remove($ProductTag);
  581.                 }
  582.                 // 商品タグの登録
  583.                 $Tags $form->get('Tag')->getData();
  584.                 foreach ($Tags as $Tag) {
  585.                     $ProductTag = new ProductTag();
  586.                     $ProductTag
  587.                         ->setProduct($Product)
  588.                         ->setTag($Tag);
  589.                     $Product->addProductTag($ProductTag);
  590.                     $this->entityManager->persist($ProductTag);
  591.                 }
  592.                 $Product->setUpdateDate(new \DateTime());
  593.                 $this->entityManager->flush();
  594.                 log_info('商品登録完了', [$id]);
  595.                 $event = new EventArgs(
  596.                     [
  597.                         'form' => $form,
  598.                         'Product' => $Product,
  599.                     ],
  600.                     $request
  601.                 );
  602.                 $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_EDIT_COMPLETE);
  603.                 $this->addSuccess('admin.common.save_complete''admin');
  604.                 if ($returnLink $form->get('return_link')->getData()) {
  605.                     try {
  606.                         // $returnLinkはpathの形式で渡される. pathが存在するかをルータでチェックする.
  607.                         $pattern '/^'.preg_quote($request->getBasePath(), '/').'/';
  608.                         $returnLink preg_replace($pattern''$returnLink);
  609.                         $result $router->match($returnLink);
  610.                         // パラメータのみ抽出
  611.                         $params array_filter($result, function ($key) {
  612.                             return !== \strpos($key'_');
  613.                         }, ARRAY_FILTER_USE_KEY);
  614.                         // pathからurlを再構築してリダイレクト.
  615.                         return $this->redirectToRoute($result['_route'], $params);
  616.                     } catch (\Exception $e) {
  617.                         // マッチしない場合はログ出力してスキップ.
  618.                         log_warning('URLの形式が不正です。');
  619.                     }
  620.                 }
  621.                 $cacheUtil->clearDoctrineCache();
  622.                 return $this->redirectToRoute('admin_product_product_edit', ['id' => $Product->getId()]);
  623.             }
  624.         }
  625.         // 検索結果の保持
  626.         $builder $this->formFactory
  627.             ->createBuilder(SearchProductType::class);
  628.         $event = new EventArgs(
  629.             [
  630.                 'builder' => $builder,
  631.                 'Product' => $Product,
  632.             ],
  633.             $request
  634.         );
  635.         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_EDIT_SEARCH);
  636.         $searchForm $builder->getForm();
  637.         if ('POST' === $request->getMethod()) {
  638.             $searchForm->handleRequest($request);
  639.         }
  640.         // Get Tags
  641.         $TagsList $this->tagRepository->getList();
  642.         // ツリー表示のため、ルートからのカテゴリを取得
  643.         $TopCategories $this->categoryRepository->getList(null);
  644.         $ChoicedCategoryIds array_map(function ($Category) {
  645.             return $Category->getId();
  646.         }, $form->get('Category')->getData());
  647.         return [
  648.             'Product' => $Product,
  649.             'Tags' => $Tags,
  650.             'TagsList' => $TagsList,
  651.             'form' => $form->createView(),
  652.             'searchForm' => $searchForm->createView(),
  653.             'has_class' => $has_class,
  654.             'id' => $id,
  655.             'TopCategories' => $TopCategories,
  656.             'ChoicedCategoryIds' => $ChoicedCategoryIds,
  657.         ];
  658.     }
  659.     /**
  660.      * @Route("/%eccube_admin_route%/product/product/{id}/delete", requirements={"id" = "\d+"}, name="admin_product_product_delete", methods={"DELETE"})
  661.      */
  662.     public function delete(Request $requestCacheUtil $cacheUtil$id null)
  663.     {
  664.         $this->isTokenValid();
  665.         $session $request->getSession();
  666.         $page_no intval($session->get('eccube.admin.product.search.page_no'));
  667.         $page_no $page_no $page_no Constant::ENABLED;
  668.         $success false;
  669.         if (!is_null($id)) {
  670.             /* @var $Product \Eccube\Entity\Product */
  671.             $Product $this->productRepository->find($id);
  672.             if (!$Product) {
  673.                 if ($request->isXmlHttpRequest()) {
  674.                     $message trans('admin.common.delete_error_already_deleted');
  675.                     return $this->json(['success' => $success'message' => $message]);
  676.                 } else {
  677.                     $this->deleteMessage();
  678.                     $rUrl $this->generateUrl('admin_product_page', ['page_no' => $page_no]).'?resume='.Constant::ENABLED;
  679.                     return $this->redirect($rUrl);
  680.                 }
  681.             }
  682.             if ($Product instanceof Product) {
  683.                 log_info('商品削除開始', [$id]);
  684.                 $deleteImages $Product->getProductImage();
  685.                 $ProductClasses $Product->getProductClasses();
  686.                 try {
  687.                     $this->productRepository->delete($Product);
  688.                     $this->entityManager->flush();
  689.                     $event = new EventArgs(
  690.                         [
  691.                             'Product' => $Product,
  692.                             'ProductClass' => $ProductClasses,
  693.                             'deleteImages' => $deleteImages,
  694.                         ],
  695.                         $request
  696.                     );
  697.                     $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_DELETE_COMPLETE);
  698.                     $deleteImages $event->getArgument('deleteImages');
  699.                     // 画像ファイルの削除(commit後に削除させる)
  700.                     /** @var ProductImage $deleteImage */
  701.                     foreach ($deleteImages as $deleteImage) {
  702.                         if ($this->productImageRepository->findOneBy(['file_name' => $deleteImage->getFileName()])) {
  703.                             continue;
  704.                         }
  705.                         try {
  706.                             $fs = new Filesystem();
  707.                             $fs->remove($this->eccubeConfig['eccube_save_image_dir'].'/'.$deleteImage);
  708.                         } catch (\Exception $e) {
  709.                             // エラーが発生しても無視する
  710.                         }
  711.                     }
  712.                     log_info('商品削除完了', [$id]);
  713.                     $success true;
  714.                     $message trans('admin.common.delete_complete');
  715.                     $cacheUtil->clearDoctrineCache();
  716.                 } catch (ForeignKeyConstraintViolationException $e) {
  717.                     log_info('商品削除エラー', [$id]);
  718.                     $message trans('admin.common.delete_error_foreign_key', ['%name%' => $Product->getName()]);
  719.                 }
  720.             } else {
  721.                 log_info('商品削除エラー', [$id]);
  722.                 $message trans('admin.common.delete_error');
  723.             }
  724.         } else {
  725.             log_info('商品削除エラー', [$id]);
  726.             $message trans('admin.common.delete_error');
  727.         }
  728.         if ($request->isXmlHttpRequest()) {
  729.             return $this->json(['success' => $success'message' => $message]);
  730.         } else {
  731.             if ($success) {
  732.                 $this->addSuccess($message'admin');
  733.             } else {
  734.                 $this->addError($message'admin');
  735.             }
  736.             $rUrl $this->generateUrl('admin_product_page', ['page_no' => $page_no]).'?resume='.Constant::ENABLED;
  737.             return $this->redirect($rUrl);
  738.         }
  739.     }
  740.     /**
  741.      * @Route("/%eccube_admin_route%/product/product/{id}/copy", requirements={"id" = "\d+"}, name="admin_product_product_copy", methods={"POST"})
  742.      */
  743.     public function copy(Request $request$id null)
  744.     {
  745.         $this->isTokenValid();
  746.         if (!is_null($id)) {
  747.             $Product $this->productRepository->find($id);
  748.             if ($Product instanceof Product) {
  749.                 $CopyProduct = clone $Product;
  750.                 $CopyProduct->copy();
  751.                 $ProductStatus $this->productStatusRepository->find(ProductStatus::DISPLAY_HIDE);
  752.                 $CopyProduct->setStatus($ProductStatus);
  753.                 $CopyProductCategories $CopyProduct->getProductCategories();
  754.                 foreach ($CopyProductCategories as $Category) {
  755.                     $this->entityManager->persist($Category);
  756.                 }
  757.                 // 規格あり商品の場合は, デフォルトの商品規格を取得し登録する.
  758.                 if ($CopyProduct->hasProductClass()) {
  759.                     $dummyClass $this->productClassRepository->findOneBy([
  760.                         'visible' => false,
  761.                         'ClassCategory1' => null,
  762.                         'ClassCategory2' => null,
  763.                         'Product' => $Product,
  764.                     ]);
  765.                     $dummyClass = clone $dummyClass;
  766.                     $dummyClass->setProduct($CopyProduct);
  767.                     $CopyProduct->addProductClass($dummyClass);
  768.                 }
  769.                 $CopyProductClasses $CopyProduct->getProductClasses();
  770.                 foreach ($CopyProductClasses as $Class) {
  771.                     $Stock $Class->getProductStock();
  772.                     $CopyStock = clone $Stock;
  773.                     $CopyStock->setProductClass($Class);
  774.                     $this->entityManager->persist($CopyStock);
  775.                     $TaxRule $Class->getTaxRule();
  776.                     if ($TaxRule) {
  777.                         $CopyTaxRule = clone $TaxRule;
  778.                         $CopyTaxRule->setProductClass($Class);
  779.                         $CopyTaxRule->setProduct($CopyProduct);
  780.                         $this->entityManager->persist($CopyTaxRule);
  781.                     }
  782.                     $this->entityManager->persist($Class);
  783.                 }
  784.                 $Images $CopyProduct->getProductImage();
  785.                 foreach ($Images as $Image) {
  786.                     // 画像ファイルを新規作成
  787.                     $extension pathinfo($Image->getFileName(), PATHINFO_EXTENSION);
  788.                     $filename date('mdHis').uniqid('_').'.'.$extension;
  789.                     try {
  790.                         $fs = new Filesystem();
  791.                         $fs->copy($this->eccubeConfig['eccube_save_image_dir'].'/'.$Image->getFileName(), $this->eccubeConfig['eccube_save_image_dir'].'/'.$filename);
  792.                     } catch (\Exception $e) {
  793.                         // エラーが発生しても無視する
  794.                     }
  795.                     $Image->setFileName($filename);
  796.                     $this->entityManager->persist($Image);
  797.                 }
  798.                 $Tags $CopyProduct->getProductTag();
  799.                 foreach ($Tags as $Tag) {
  800.                     $this->entityManager->persist($Tag);
  801.                 }
  802.                 $this->entityManager->persist($CopyProduct);
  803.                 $this->entityManager->flush();
  804.                 $event = new EventArgs(
  805.                     [
  806.                         'Product' => $Product,
  807.                         'CopyProduct' => $CopyProduct,
  808.                         'CopyProductCategories' => $CopyProductCategories,
  809.                         'CopyProductClasses' => $CopyProductClasses,
  810.                         'images' => $Images,
  811.                         'Tags' => $Tags,
  812.                     ],
  813.                     $request
  814.                 );
  815.                 $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_COPY_COMPLETE);
  816.                 $this->addSuccess('admin.product.copy_complete''admin');
  817.                 return $this->redirectToRoute('admin_product_product_edit', ['id' => $CopyProduct->getId()]);
  818.             } else {
  819.                 $this->addError('admin.product.copy_error''admin');
  820.             }
  821.         } else {
  822.             $msg trans('admin.product.copy_error');
  823.             $this->addError($msg'admin');
  824.         }
  825.         return $this->redirectToRoute('admin_product');
  826.     }
  827.     /**
  828.      * 商品CSVの出力.
  829.      *
  830.      * @Route("/%eccube_admin_route%/product/export", name="admin_product_export", methods={"GET"})
  831.      *
  832.      * @param Request $request
  833.      *
  834.      * @return StreamedResponse
  835.      */
  836.     public function export(Request $request)
  837.     {
  838.         // タイムアウトを無効にする.
  839.         set_time_limit(0);
  840.         // sql loggerを無効にする.
  841.         $em $this->entityManager;
  842.         $em->getConfiguration()->setSQLLogger(null);
  843.         $response = new StreamedResponse();
  844.         $response->setCallback(function () use ($request) {
  845.             // CSV種別を元に初期化.
  846.             $this->csvExportService->initCsvType(CsvType::CSV_TYPE_PRODUCT);
  847.             // 商品データ検索用のクエリビルダを取得.
  848.             $qb $this->csvExportService
  849.                 ->getProductQueryBuilder($request);
  850.             // ヘッダ行の出力.
  851.             $this->csvExportService->exportHeader();
  852.             // Get stock status
  853.             $isOutOfStock 0;
  854.             $session $request->getSession();
  855.             if ($session->has('eccube.admin.product.search')) {
  856.                 $searchData $session->get('eccube.admin.product.search', []);
  857.                 if (isset($searchData['stock_status']) && $searchData['stock_status'] === 0) {
  858.                     $isOutOfStock 1;
  859.                 }
  860.             }
  861.             // joinする場合はiterateが使えないため, select句をdistinctする.
  862.             // http://qiita.com/suin/items/2b1e98105fa3ef89beb7
  863.             // distinctのmysqlとpgsqlの挙動をあわせる.
  864.             // http://uedatakeshi.blogspot.jp/2010/04/distinct-oeder-by-postgresmysql.html
  865.             $qb->resetDQLPart('select');
  866.             if ($isOutOfStock) {
  867.                 $qb->select('p, pc')
  868.                     ->distinct();
  869.             } else {
  870.                 $qb->select('p')
  871.                     ->distinct();
  872.             }
  873.             // データ行の出力.
  874.             $this->csvExportService->setExportQueryBuilder($qb);
  875.             $this->csvExportService->exportData(function ($entityCsvExportService $csvService) use ($request) {
  876.                 $Csvs $csvService->getCsvs();
  877.                 /** @var $Product \Eccube\Entity\Product */
  878.                 $Product $entity;
  879.                 /** @var $ProductClasses \Eccube\Entity\ProductClass[] */
  880.                 $ProductClasses $Product->getProductClasses();
  881.                 foreach ($ProductClasses as $ProductClass) {
  882.                     if ($ProductClass->isVisible() == false) {
  883.                         // 非表示の商品規格は除外
  884.                         continue;
  885.                     }
  886.                     $ExportCsvRow = new ExportCsvRow();
  887.                     // CSV出力項目と合致するデータを取得.
  888.                     foreach ($Csvs as $Csv) {
  889.                         // 商品データを検索.
  890.                         $ExportCsvRow->setData($csvService->getData($Csv$Product));
  891.                         if ($ExportCsvRow->isDataNull()) {
  892.                             // 商品規格情報を検索.
  893.                             $ExportCsvRow->setData($csvService->getData($Csv$ProductClass));
  894.                         }
  895.                         $event = new EventArgs(
  896.                             [
  897.                                 'csvService' => $csvService,
  898.                                 'Csv' => $Csv,
  899.                                 'ProductClass' => $ProductClass,
  900.                                 'ExportCsvRow' => $ExportCsvRow,
  901.                             ],
  902.                             $request
  903.                         );
  904.                         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_CSV_EXPORT);
  905.                         $ExportCsvRow->pushData();
  906.                     }
  907.                     // $row[] = number_format(memory_get_usage(true));
  908.                     // 出力.
  909.                     $csvService->fputcsv($ExportCsvRow->getRow());
  910.                 }
  911.             });
  912.         });
  913.         $now = new \DateTime();
  914.         $filename 'product_'.$now->format('YmdHis').'.csv';
  915.         $response->headers->set('Content-Type''application/octet-stream');
  916.         $response->headers->set('Content-Disposition''attachment; filename='.$filename);
  917.         log_info('商品CSV出力ファイル名', [$filename]);
  918.         return $response;
  919.     }
  920.     /**
  921.      * ProductCategory作成
  922.      *
  923.      * @param \Eccube\Entity\Product $Product
  924.      * @param \Eccube\Entity\Category $Category
  925.      * @param integer $count
  926.      *
  927.      * @return \Eccube\Entity\ProductCategory
  928.      */
  929.     private function createProductCategory($Product$Category$count)
  930.     {
  931.         $ProductCategory = new ProductCategory();
  932.         $ProductCategory->setProduct($Product);
  933.         $ProductCategory->setProductId($Product->getId());
  934.         $ProductCategory->setCategory($Category);
  935.         $ProductCategory->setCategoryId($Category->getId());
  936.         return $ProductCategory;
  937.     }
  938.     /**
  939.      * Bulk public action
  940.      *
  941.      * @Route("/%eccube_admin_route%/product/bulk/product-status/{id}", requirements={"id" = "\d+"}, name="admin_product_bulk_product_status", methods={"POST"})
  942.      *
  943.      * @param Request $request
  944.      * @param ProductStatus $ProductStatus
  945.      *
  946.      * @return RedirectResponse
  947.      */
  948.     public function bulkProductStatus(Request $requestProductStatus $ProductStatusCacheUtil $cacheUtil)
  949.     {
  950.         $this->isTokenValid();
  951.         /** @var Product[] $Products */
  952.         $Products $this->productRepository->findBy(['id' => $request->get('ids')]);
  953.         $count 0;
  954.         foreach ($Products as $Product) {
  955.             try {
  956.                 $Product->setStatus($ProductStatus);
  957.                 $this->productRepository->save($Product);
  958.                 $count++;
  959.             } catch (\Exception $e) {
  960.                 $this->addError($e->getMessage(), 'admin');
  961.             }
  962.         }
  963.         try {
  964.             if ($count) {
  965.                 $this->entityManager->flush();
  966.                 $msg $this->translator->trans('admin.product.bulk_change_status_complete', [
  967.                     '%count%' => $count,
  968.                     '%status%' => $ProductStatus->getName(),
  969.                 ]);
  970.                 $this->addSuccess($msg'admin');
  971.                 $cacheUtil->clearDoctrineCache();
  972.             }
  973.         } catch (\Exception $e) {
  974.             $this->addError($e->getMessage(), 'admin');
  975.         }
  976.         return $this->redirectToRoute('admin_product', ['resume' => Constant::ENABLED]);
  977.     }
  978. }