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

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