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

Open in your IDE?
  1. <?php
  2. namespace Eccube\Controller\Admin\Product;
  3. use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
  4. use Eccube\Common\Constant;
  5. use Eccube\Controller\AbstractController;
  6. use Eccube\Entity\BaseInfo;
  7. use Eccube\Entity\ExportCsvRow;
  8. use Eccube\Entity\Master\CsvType;
  9. use Eccube\Entity\Master\ProductStatus;
  10. use Eccube\Entity\Product;
  11. use Eccube\Entity\ProductCategory;
  12. use Eccube\Entity\ProductClass;
  13. use Eccube\Entity\ProductImage;
  14. use Eccube\Entity\ProductStock;
  15. use Eccube\Entity\ProductTag;
  16. use Eccube\Event\EccubeEvents;
  17. use Eccube\Event\EventArgs;
  18. use Eccube\Form\Type\Admin\ProductType;
  19. use Eccube\Form\Type\Admin\SearchProductType;
  20. use Eccube\Repository\BaseInfoRepository;
  21. use Eccube\Repository\CategoryRepository;
  22. use Eccube\Repository\Master\PageMaxRepository;
  23. use Eccube\Repository\Master\ProductStatusRepository;
  24. use Eccube\Repository\ProductClassRepository;
  25. use Eccube\Repository\ProductImageRepository;
  26. use Eccube\Repository\ProductRepository;
  27. use Eccube\Repository\TagRepository;
  28. use Eccube\Repository\TaxRuleRepository;
  29. use Eccube\Service\CsvExportService;
  30. use Eccube\Util\CacheUtil;
  31. use Eccube\Util\FormUtil;
  32. use Knp\Component\Pager\PaginatorInterface;
  33. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  34. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  35. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  36. use Symfony\Component\Asset\Packages;
  37. use Symfony\Component\Filesystem\Filesystem;
  38. use Symfony\Component\HttpFoundation\File\File;
  39. use Symfony\Component\HttpFoundation\RedirectResponse;
  40. use Symfony\Component\HttpFoundation\Request;
  41. use Symfony\Component\HttpFoundation\Response;
  42. use Symfony\Component\HttpFoundation\StreamedResponse;
  43. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  44. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  45. use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
  46. use Symfony\Component\Routing\Annotation\Route;
  47. use Symfony\Component\Routing\RouterInterface;
  48. class ProductController extends AbstractController {
  49.   /**
  50.    * @var CsvExportService
  51.    */
  52.   protected $csvExportService;
  53.   /**
  54.    * @var ProductClassRepository
  55.    */
  56.   protected $productClassRepository;
  57.   /**
  58.    * @var ProductImageRepository
  59.    */
  60.   protected $productImageRepository;
  61.   /**
  62.    * @var TaxRuleRepository
  63.    */
  64.   protected $taxRuleRepository;
  65.   /**
  66.    * @var CategoryRepository
  67.    */
  68.   protected $categoryRepository;
  69.   /**
  70.    * @var ProductRepository
  71.    */
  72.   protected $productRepository;
  73.   /**
  74.    * @var BaseInfo
  75.    */
  76.   protected $BaseInfo;
  77.   /**
  78.    * @var PageMaxRepository
  79.    */
  80.   protected $pageMaxRepository;
  81.   /**
  82.    * @var ProductStatusRepository
  83.    */
  84.   protected $productStatusRepository;
  85.   /**
  86.    * @var TagRepository
  87.    */
  88.   protected $tagRepository;
  89.   /**
  90.    * @var \Eccube\Repository\CustomerFavoriteProductRepository
  91.    */
  92.   protected $customerFavoriteProductRepository;
  93.   /**
  94.    * @var \Eccube\Repository\OrderRepository
  95.    */
  96.   protected $orderRepository;
  97.   /**
  98.    * @var \Plugin\ProductPlus42\Repository\ProductDataDetailRepository
  99.    */
  100.   protected $productDataDetailRepository;
  101.   /**
  102.    * @var \Plugin\CustomerRank42\Repository\CustomerPriceRepository
  103.    */
  104.   protected $customerPriceRepository;
  105.   /**
  106.    * @var \Plugin\CustomerPlus\Repository\CustomerDataDetailRepository
  107.    */
  108.   protected $customerPlusDataDetailRepository;
  109.   /**
  110.    * @var \Plugin\CustomerPlus\Repository\CustomerItemOptionRepository
  111.    */
  112.   protected $customerPlusItemOptionRepository;
  113.   /**
  114.    * @var Packages
  115.    */
  116.   protected $packages;
  117.   /**
  118.    * ProductController constructor.
  119.    *
  120.    * @param CsvExportService $csvExportService
  121.    * @param ProductClassRepository $productClassRepository
  122.    * @param ProductImageRepository $productImageRepository
  123.    * @param TaxRuleRepository $taxRuleRepository
  124.    * @param CategoryRepository $categoryRepository
  125.    * @param ProductRepository $productRepository
  126.    * @param BaseInfoRepository $baseInfoRepository
  127.    * @param PageMaxRepository $pageMaxRepository
  128.    * @param ProductStatusRepository $productStatusRepository
  129.    * @param TagRepository $tagRepository
  130.    * @param \Eccube\Repository\CustomerFavoriteProductRepository $customerFavoriteProductRepository
  131.    * @param \Eccube\Repository\OrderRepository $orderRepository
  132.    * @param \Plugin\ProductPlus42\Repository\ProductDataDetailRepository $productDataDetailRepository
  133.    * @param \Plugin\CustomerRank42\Repository\CustomerPriceRepository $customerPriceRepository
  134.    */
  135.   public function __construct(
  136.     CsvExportService $csvExportService,
  137.     ProductClassRepository $productClassRepository,
  138.     ProductImageRepository $productImageRepository,
  139.     TaxRuleRepository $taxRuleRepository,
  140.     CategoryRepository $categoryRepository,
  141.     ProductRepository $productRepository,
  142.     BaseInfoRepository $baseInfoRepository,
  143.     PageMaxRepository $pageMaxRepository,
  144.     ProductStatusRepository $productStatusRepository,
  145.     TagRepository $tagRepository,
  146.     \Eccube\Repository\CustomerFavoriteProductRepository $customerFavoriteProductRepository,
  147.     \Eccube\Repository\OrderRepository $orderRepository,
  148.     \Plugin\ProductPlus42\Repository\ProductDataDetailRepository $productDataDetailRepository,
  149.     \Plugin\CustomerRank42\Repository\CustomerPriceRepository $customerPriceRepository,
  150.     \Plugin\CustomerPlus\Repository\CustomerDataDetailRepository $customerPlusDataDetailRepository,
  151.     \Plugin\CustomerPlus\Repository\CustomerItemOptionRepository $customerPlusItemOptionRepository,
  152.     Packages $packages
  153.   ) {
  154.     $this->csvExportService $csvExportService;
  155.     $this->productClassRepository $productClassRepository;
  156.     $this->productImageRepository $productImageRepository;
  157.     $this->taxRuleRepository $taxRuleRepository;
  158.     $this->categoryRepository $categoryRepository;
  159.     $this->productRepository $productRepository;
  160.     $this->BaseInfo $baseInfoRepository->get();
  161.     $this->pageMaxRepository $pageMaxRepository;
  162.     $this->productStatusRepository $productStatusRepository;
  163.     $this->tagRepository $tagRepository;
  164.     $this->customerFavoriteProductRepository $customerFavoriteProductRepository;
  165.     $this->orderRepository $orderRepository;
  166.     $this->productDataDetailRepository $productDataDetailRepository;
  167.     $this->customerPriceRepository $customerPriceRepository;
  168.     $this->customerPlusDataDetailRepository $customerPlusDataDetailRepository;
  169.     $this->customerPlusItemOptionRepository $customerPlusItemOptionRepository;
  170.     $this->packages $packages;
  171.   }
  172.   /**
  173.    * @Route("/%eccube_admin_route%/product", name="admin_product", methods={"GET", "POST"})
  174.    * @Route("/%eccube_admin_route%/product/page/{page_no}", requirements={"page_no" = "\d+"}, name="admin_product_page", methods={"GET", "POST"})
  175.    * @Template("@admin/Product/index.twig")
  176.    */
  177.   public function index(Request $requestPaginatorInterface $paginator$page_no null) {
  178.     $builder $this->formFactory
  179.       ->createBuilder(SearchProductType::class);
  180.     $event = new EventArgs(
  181.       [
  182.       'builder' => $builder,
  183.       ], $request
  184.     );
  185.     $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_INDEX_INITIALIZE);
  186.     $searchForm $builder->getForm();
  187.     /**
  188.      * ページの表示件数は, 以下の順に優先される.
  189.      * - リクエストパラメータ
  190.      * - セッション
  191.      * - デフォルト値
  192.      * また, セッションに保存する際は mtb_page_maxと照合し, 一致した場合のみ保存する.
  193.      * */
  194.     $page_count $this->session->get('eccube.admin.product.search.page_count'$this->eccubeConfig->get('eccube_default_page_count'));
  195.     $page_count_param = (int) $request->get('page_count');
  196.     $pageMaxis $this->pageMaxRepository->findAll();
  197.     if ($page_count_param) {
  198.       foreach ($pageMaxis as $pageMax) {
  199.         if ($page_count_param == $pageMax->getName()) {
  200.           $page_count $pageMax->getName();
  201.           $this->session->set('eccube.admin.product.search.page_count'$page_count);
  202.           break;
  203.         }
  204.       }
  205.     }
  206.     if ('POST' === $request->getMethod()) {
  207.       $searchForm->handleRequest($request);
  208.       if ($searchForm->isValid()) {
  209.         /**
  210.          * 検索が実行された場合は, セッションに検索条件を保存する.
  211.          * ページ番号は最初のページ番号に初期化する.
  212.          */
  213.         $page_no 1;
  214.         $searchData $searchForm->getData();
  215.         // 検索条件, ページ番号をセッションに保持.
  216.         $this->session->set('eccube.admin.product.search'FormUtil::getViewData($searchForm));
  217.         $this->session->set('eccube.admin.product.search.page_no'$page_no);
  218.       } else {
  219.         // 検索エラーの際は, 詳細検索枠を開いてエラー表示する.
  220.         return [
  221.           'searchForm' => $searchForm->createView(),
  222.           'pagination' => [],
  223.           'pageMaxis' => $pageMaxis,
  224.           'page_no' => $page_no,
  225.           'page_count' => $page_count,
  226.           'has_errors' => true,
  227.         ];
  228.       }
  229.     } else {
  230.       if (null !== $page_no || $request->get('resume')) {
  231.         /*
  232.          * ページ送りの場合または、他画面から戻ってきた場合は, セッションから検索条件を復旧する.
  233.          */
  234.         if ($page_no) {
  235.           // ページ送りで遷移した場合.
  236.           $this->session->set('eccube.admin.product.search.page_no', (int)$page_no);
  237.         } else {
  238.           // 他画面から遷移した場合.
  239.           $page_no $this->session->get('eccube.admin.product.search.page_no'1);
  240.         }
  241.         $viewData $this->session->get('eccube.admin.product.search', []);
  242.         $searchData FormUtil::submitAndGetData($searchForm$viewData);
  243.       } else {
  244.         /**
  245.          * 初期表示の場合.
  246.          */
  247.         $page_no 1;
  248.         // submit default value
  249.         $viewData FormUtil::getViewData($searchForm);
  250.         $searchData FormUtil::submitAndGetData($searchForm$viewData);
  251.         // セッション中の検索条件, ページ番号を初期化.
  252.         $this->session->set('eccube.admin.product.search'$viewData);
  253.         $this->session->set('eccube.admin.product.search.page_no'$page_no);
  254.       }
  255.     }
  256.     // ヘッダー検索(multiパラメータ)の対応
  257.     if(!empty($_GET["multi"])) {
  258.       $searchData["status"] = "";
  259.       $searchData["id"] = $_GET["multi"];  // 商品名・商品ID・商品コード・検索ワード・鑑定番号で検索
  260.     }
  261.     $qb $this->productRepository->getQueryBuilderBySearchDataForAdmin($searchData);
  262.     $event = new EventArgs(
  263.       [
  264.       'qb' => $qb,
  265.       'searchData' => $searchData,
  266.       ], $request
  267.     );
  268.     $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_INDEX_SEARCH);
  269.     $qb->andWhere('pc.SaleType != :saleType')
  270.       ->setParameter('saleType'2);
  271.     $sortKey $searchData['sortkey'];
  272.     if (empty($this->productRepository::COLUMNS[$sortKey]) || $sortKey == 'code' || $sortKey == 'status') {
  273.       $pagination $paginator->paginate(
  274.         $qb$page_no$page_count
  275.       );
  276.     } else {
  277.       $pagination $paginator->paginate(
  278.         $qb$page_no$page_count, ['wrap-queries' => true]
  279.       );
  280.     }
  281.     // 各種データをハッシュで取得
  282.     $htKantei $this->productDataDetailRepository->getHash(2);  // 鑑定番号
  283.     $htPrice $this->customerPriceRepository->getHash(2);  // レギュラー会員価格
  284.     $htOwner $this->productDataDetailRepository->getHash(40);  // オーナー名
  285.     $htOwnerId $this->productDataDetailRepository->getHash(6);  // オーナーID
  286.     $htPrice2 $this->productDataDetailRepository->getHash(55);  // 目安価格
  287.     $htOrder $this->orderRepository->getHashCustomerName();  // 購入者
  288.     $htFavorite $this->customerFavoriteProductRepository->getHashNum();  // お気に入り数
  289.     $htType $this->productRepository->getHashType();  // 商品種別
  290.     $htJigane $this->productRepository->getHashJigane();  // 地金型コイン
  291.     $htMibun $this->customerPlusDataDetailRepository->getHash(20);    // 身分証明書の種別 (customer_id → option_id)
  292.     $htMibunNm $this->customerPlusItemOptionRepository->getHash(20);  // option_id → 種別名
  293.     $htProductOrderCnt $this->orderRepository->getHashOrderCountByProduct(); // 受注件数
  294.     return [
  295.       'searchForm' => $searchForm->createView(),
  296.       'pagination' => $pagination,
  297.       'pageMaxis' => $pageMaxis,
  298.       'page_no' => $page_no,
  299.       'page_count' => $page_count,
  300.       'has_errors' => false,
  301.       'htKantei' => $htKantei,
  302.       'htPrice' => $htPrice,
  303.       'htOwner' => $htOwner,
  304.       'htOwnerId' => $htOwnerId,
  305.       'htPrice2' => $htPrice2,
  306.       'htOrder' => $htOrder,
  307.       'htFavorite' => $htFavorite,
  308.       'htType' => $htType,
  309.       'htJigane' => $htJigane,
  310.       'htMibun' => $htMibun,
  311.       'htMibunNm' => $htMibunNm,
  312.       'htProductOrderCnt' => $htProductOrderCnt,
  313.     ];
  314.   }
  315.   /**
  316.    * @Route("/%eccube_admin_route%/product/classes/{id}/load", name="admin_product_classes_load", methods={"GET"}, requirements={"id" = "\d+"}, methods={"GET"})
  317.    * @Template("@admin/Product/product_class_popup.twig")
  318.    * @ParamConverter("Product", options={"repository_method":"findWithSortedClassCategories"})
  319.    */
  320.   public function loadProductClasses(Request $requestProduct $Product) {
  321.     if (!$request->isXmlHttpRequest() && $this->isTokenValid()) {
  322.       throw new BadRequestHttpException();
  323.     }
  324.     $data = [];
  325.     /** @var $Product ProductRepository */
  326.     if (!$Product) {
  327.       throw new NotFoundHttpException();
  328.     }
  329.     if ($Product->hasProductClass()) {
  330.       $class $Product->getProductClasses();
  331.       foreach ($class as $item) {
  332.         $data[] = $item;
  333.       }
  334.     }
  335.     return [
  336.       'data' => $data,
  337.     ];
  338.   }
  339.   /**
  340.    * 画像アップロード時にリクエストされるメソッド.
  341.    *
  342.    * @see https://pqina.nl/filepond/docs/api/server/#process
  343.    * @Route("/%eccube_admin_route%/product/product/image/process", name="admin_product_image_process", methods={"POST"})
  344.    */
  345.   public function imageProcess(Request $request) {
  346.     if (!$request->isXmlHttpRequest() && $this->isTokenValid()) {
  347.       throw new BadRequestHttpException();
  348.     }
  349.     $images $request->files->get('admin_product');
  350.     $allowExtensions = ['gif''jpg''jpeg''png''webp'];
  351.     $files = [];
  352.     if (count($images) > 0) {
  353.       foreach ($images as $img) {
  354.         foreach ($img as $image) {
  355.           // ファイルフォーマット検証
  356.           $mimeType $image->getMimeType();
  357.           if (!== strpos($mimeType'image')) {
  358.             throw new UnsupportedMediaTypeHttpException();
  359.           }
  360.           // 拡張子
  361.           $extension $image->getClientOriginalExtension();
  362.           if (!in_array(strtolower($extension), $allowExtensions)) {
  363.             throw new UnsupportedMediaTypeHttpException();
  364.           }
  365.           $filename date('mdHis') . uniqid('_') . '.' $extension;
  366.           $image->move($this->eccubeConfig['eccube_temp_image_dir'], $filename);
  367.           $files[] = $filename;
  368.         }
  369.       }
  370.     }
  371.     $event = new EventArgs(
  372.       [
  373.       'images' => $images,
  374.       'files' => $files,
  375.       ], $request
  376.     );
  377.     $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_ADD_IMAGE_COMPLETE);
  378.     $files $event->getArgument('files');
  379.     return new Response(array_shift($files));
  380.   }
  381.   /**
  382.    * アップロード画像を取得する際にコールされるメソッド.
  383.    *
  384.    * @see https://pqina.nl/filepond/docs/api/server/#load
  385.    * @Route("/%eccube_admin_route%/product/product/image/load", name="admin_product_image_load", methods={"GET"})
  386.    */
  387.   public function imageLoad(Request $request) {
  388.     if (!$request->isXmlHttpRequest()) {
  389.       throw new BadRequestHttpException();
  390.     }
  391.     $dirs = [
  392.       $this->eccubeConfig['eccube_save_image_dir'],
  393.       $this->eccubeConfig['eccube_temp_image_dir'],
  394.     ];
  395.     foreach ($dirs as $dir) {
  396.       if (strpos($request->query->get('source'), '..') !== false) {
  397.         throw new NotFoundHttpException();
  398.       }
  399.       $image \realpath($dir '/' $request->query->get('source'));
  400.       $dir \realpath($dir);
  401.       if (\is_file($image) && \str_starts_with($image$dir)) {
  402.         $file = new \SplFileObject($image);
  403.         return $this->file($file$file->getBasename());
  404.       }
  405.     }
  406.     throw new NotFoundHttpException();
  407.   }
  408.   /**
  409.    * アップロード画像をすぐ削除する際にコールされるメソッド.
  410.    *
  411.    * @see https://pqina.nl/filepond/docs/api/server/#revert
  412.    * @Route("/%eccube_admin_route%/product/product/image/revert", name="admin_product_image_revert", methods={"DELETE"})
  413.    */
  414.   public function imageRevert(Request $request) {
  415.     if (!$request->isXmlHttpRequest() && $this->isTokenValid()) {
  416.       throw new BadRequestHttpException();
  417.     }
  418.     $tempFile $this->eccubeConfig['eccube_temp_image_dir'] . '/' $request->getContent();
  419.     if (is_file($tempFile) && stripos(realpath($tempFile), $this->eccubeConfig['eccube_temp_image_dir']) === 0) {
  420.       $fs = new Filesystem();
  421.       $fs->remove($tempFile);
  422.       return new Response(nullResponse::HTTP_NO_CONTENT);
  423.     }
  424.     throw new NotFoundHttpException();
  425.   }
  426.   /**
  427.    * 商品説明(リッチテキストエディタ)に挿入する画像をアップロードする.
  428.    *
  429.    * @Route("/%eccube_admin_route%/product/product/description/image", name="admin_product_description_image_upload", methods={"POST"})
  430.    * @IsGranted("ROLE_ADMIN")
  431.    */
  432.   public function uploadDescriptionImage(Request $request) {
  433.     if (!$request->isXmlHttpRequest()) {
  434.       throw new BadRequestHttpException();
  435.     }
  436.     $this->isTokenValid();
  437.     $image $request->files->get('file');
  438.     if (!$image) {
  439.       throw new BadRequestHttpException();
  440.     }
  441.     $allowExtensions = ['gif''jpg''jpeg''png''webp'];
  442.     $mimeType $image->getMimeType();
  443.     if (!== strpos((string) $mimeType'image')) {
  444.       throw new UnsupportedMediaTypeHttpException();
  445.     }
  446.     $extension strtolower($image->getClientOriginalExtension());
  447.     if (!in_array($extension$allowExtensionstrue)) {
  448.       throw new UnsupportedMediaTypeHttpException();
  449.     }
  450.     $filename date('mdHis') . uniqid('_') . '.' $extension;
  451.     $image->move($this->eccubeConfig['eccube_save_image_dir'], $filename);
  452.     return $this->json(['location' => $this->packages->getUrl($filename'save_image')]);
  453.   }
  454.   /**
  455.    * @Route("/%eccube_admin_route%/product/product/new", name="admin_product_product_new", methods={"GET", "POST"})
  456.    * @Route("/%eccube_admin_route%/product/product/{id}/edit", requirements={"id" = "\d+"}, name="admin_product_product_edit", methods={"GET", "POST"})
  457.    * @Template("@admin/Product/product.twig")
  458.    */
  459.   public function edit(Request $requestRouterInterface $routerCacheUtil $cacheUtil$id null) {
  460.     $has_class false;
  461.     if (is_null($id)) {
  462.       $Product = new Product();
  463.       $ProductClass = new ProductClass();
  464.       $ProductStatus $this->productStatusRepository->find(ProductStatus::DISPLAY_HIDE);
  465.       $Product
  466.         ->addProductClass($ProductClass)
  467.         ->setStatus($ProductStatus);
  468.       $ProductClass
  469.         ->setVisible(true)
  470.         ->setStockUnlimited(true)
  471.         ->setProduct($Product);
  472.       $ProductStock = new ProductStock();
  473.       $ProductClass->setProductStock($ProductStock);
  474.       $ProductStock->setProductClass($ProductClass);
  475.     } else {
  476.       $Product $this->productRepository->findWithSortedClassCategories($id);
  477.       $ProductClass null;
  478.       $ProductStock null;
  479.       if (!$Product) {
  480.         throw new NotFoundHttpException();
  481.       }
  482.       // 規格無しの商品の場合は、デフォルト規格を表示用に取得する
  483.       $has_class $Product->hasProductClass();
  484.       if (!$has_class) {
  485.         $ProductClasses $Product->getProductClasses();
  486.         foreach ($ProductClasses as $pc) {
  487.           if (!is_null($pc->getClassCategory1())) {
  488.             continue;
  489.           }
  490.           if ($pc->isVisible()) {
  491.             $ProductClass $pc;
  492.             break;
  493.           }
  494.         }
  495.         if ($this->BaseInfo->isOptionProductTaxRule() && $ProductClass->getTaxRule()) {
  496.           $ProductClass->setTaxRate($ProductClass->getTaxRule()->getTaxRate());
  497.         }
  498.         $ProductStock $ProductClass->getProductStock();
  499.       }
  500.     }
  501.     $builder $this->formFactory
  502.       ->createBuilder(ProductType::class, $Product);
  503.     // 規格あり商品の場合、規格関連情報をFormから除外
  504.     if ($has_class) {
  505.       $builder->remove('class');
  506.     }
  507.     $event = new EventArgs(
  508.       [
  509.       'builder' => $builder,
  510.       'Product' => $Product,
  511.       ], $request
  512.     );
  513.     $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_EDIT_INITIALIZE);
  514.     $form $builder->getForm();
  515.     if (!$has_class) {
  516.       $ProductClass->setStockUnlimited($ProductClass->isStockUnlimited());
  517.       $form['class']->setData($ProductClass);
  518.     }
  519.     // ファイルの登録
  520.     $images = [];
  521.     $ProductImages $Product->getProductImage();
  522.     foreach ($ProductImages as $ProductImage) {
  523.       $images[] = $ProductImage->getFileName();
  524.     }
  525.     $form['images']->setData($images);
  526.     $categories = [];
  527.     $ProductCategories $Product->getProductCategories();
  528.     foreach ($ProductCategories as $ProductCategory) {
  529.       /* @var $ProductCategory \Eccube\Entity\ProductCategory */
  530.       $categories[] = $ProductCategory->getCategory();
  531.     }
  532.     $form['Category']->setData($categories);
  533.     $Tags $Product->getTags();
  534.     $form['Tag']->setData($Tags);
  535.     if ('POST' === $request->getMethod()) {
  536.       $form->handleRequest($request);
  537.       if ($form->isValid()) {
  538.         log_info('商品登録開始', [$id]);
  539.         $Product $form->getData();
  540.         if (!$has_class) {
  541.           $ProductClass $form['class']->getData();
  542.           // 個別消費税
  543.           if ($this->BaseInfo->isOptionProductTaxRule()) {
  544.             if ($ProductClass->getTaxRate() !== null) {
  545.               if ($ProductClass->getTaxRule()) {
  546.                 $ProductClass->getTaxRule()->setTaxRate($ProductClass->getTaxRate());
  547.               } else {
  548.                 $taxrule $this->taxRuleRepository->newTaxRule();
  549.                 $taxrule->setTaxRate($ProductClass->getTaxRate());
  550.                 $taxrule->setApplyDate(new \DateTime());
  551.                 $taxrule->setProduct($Product);
  552.                 $taxrule->setProductClass($ProductClass);
  553.                 $ProductClass->setTaxRule($taxrule);
  554.               }
  555.               $ProductClass->getTaxRule()->setTaxRate($ProductClass->getTaxRate());
  556.             } else {
  557.               if ($ProductClass->getTaxRule()) {
  558.                 $this->taxRuleRepository->delete($ProductClass->getTaxRule());
  559.                 $ProductClass->setTaxRule(null);
  560.               }
  561.             }
  562.           }
  563.           $this->entityManager->persist($ProductClass);
  564.           // 在庫情報を作成
  565.           if (!$ProductClass->isStockUnlimited()) {
  566.             $ProductStock->setStock($ProductClass->getStock());
  567.           } else {
  568.             // 在庫無制限時はnullを設定
  569.             $ProductStock->setStock(null);
  570.           }
  571.           $this->entityManager->persist($ProductStock);
  572.         }
  573.         // カテゴリの登録
  574.         // 一度クリア
  575.         /* @var $Product \Eccube\Entity\Product */
  576.         foreach ($Product->getProductCategories() as $ProductCategory) {
  577.           $Product->removeProductCategory($ProductCategory);
  578.           $this->entityManager->remove($ProductCategory);
  579.         }
  580.         $this->entityManager->persist($Product);
  581.         $this->entityManager->flush();
  582.         $count 1;
  583.         $Categories $form->get('Category')->getData();
  584.         $categoriesIdList = [];
  585.         foreach ($Categories as $Category) {
  586.           foreach ($Category->getPath() as $ParentCategory) {
  587.             if (!isset($categoriesIdList[$ParentCategory->getId()])) {
  588.               $ProductCategory $this->createProductCategory($Product$ParentCategory$count);
  589.               $this->entityManager->persist($ProductCategory);
  590.               $count++;
  591.               /* @var $Product \Eccube\Entity\Product */
  592.               $Product->addProductCategory($ProductCategory);
  593.               $categoriesIdList[$ParentCategory->getId()] = true;
  594.             }
  595.           }
  596.           if (!isset($categoriesIdList[$Category->getId()])) {
  597.             $ProductCategory $this->createProductCategory($Product$Category$count);
  598.             $this->entityManager->persist($ProductCategory);
  599.             $count++;
  600.             /* @var $Product \Eccube\Entity\Product */
  601.             $Product->addProductCategory($ProductCategory);
  602.             $categoriesIdList[$Category->getId()] = true;
  603.           }
  604.         }
  605.         // 画像の登録
  606.         $add_images $form->get('add_images')->getData();
  607.         foreach ($add_images as $add_image) {
  608.           $ProductImage = new \Eccube\Entity\ProductImage();
  609.           $ProductImage
  610.             ->setFileName($add_image)
  611.             ->setProduct($Product)
  612.             ->setSortNo(1);
  613.           $Product->addProductImage($ProductImage);
  614.           $this->entityManager->persist($ProductImage);
  615.           // 移動
  616.           $file = new File($this->eccubeConfig['eccube_temp_image_dir'] . '/' $add_image);
  617.           $file->move($this->eccubeConfig['eccube_save_image_dir']);
  618.         }
  619.         // 画像の削除
  620.         $delete_images $form->get('delete_images')->getData();
  621.         $fs = new Filesystem();
  622.         foreach ($delete_images as $delete_image) {
  623.           $ProductImage $this->productImageRepository->findOneBy([
  624.             'Product' => $Product,
  625.             'file_name' => $delete_image,
  626.           ]);
  627.           if ($ProductImage instanceof ProductImage) {
  628.             $Product->removeProductImage($ProductImage);
  629.             $this->entityManager->remove($ProductImage);
  630.             $this->entityManager->flush();
  631.             // 他に同じ画像を参照する商品がなければ画像ファイルを削除
  632.             if (!$this->productImageRepository->findOneBy(['file_name' => $delete_image])) {
  633.               $fs->remove($this->eccubeConfig['eccube_save_image_dir'] . '/' $delete_image);
  634.             }
  635.           } else {
  636.             // 追加してすぐに削除した画像は、Entityに追加されない
  637.             $fs->remove($this->eccubeConfig['eccube_temp_image_dir'] . '/' $delete_image);
  638.           }
  639.         }
  640.         $this->entityManager->flush();
  641.         if (array_key_exists('product_image'$request->request->get('admin_product'))) {
  642.           $product_image $request->request->get('admin_product')['product_image'];
  643.           foreach ($product_image as $sortNo => $filename) {
  644.             $ProductImage $this->productImageRepository
  645.               ->findOneBy([
  646.               'file_name' => pathinfo($filenamePATHINFO_BASENAME),
  647.               'Product' => $Product,
  648.             ]);
  649.             if ($ProductImage !== null) {
  650.               $ProductImage->setSortNo($sortNo);
  651.               $this->entityManager->persist($ProductImage);
  652.             }
  653.           }
  654.           $this->entityManager->flush();
  655.         }
  656.         // 商品タグの登録
  657.         // 商品タグを一度クリア
  658.         $ProductTags $Product->getProductTag();
  659.         foreach ($ProductTags as $ProductTag) {
  660.           $Product->removeProductTag($ProductTag);
  661.           $this->entityManager->remove($ProductTag);
  662.         }
  663.         // 商品タグの登録
  664.         $Tags $form->get('Tag')->getData();
  665.         foreach ($Tags as $Tag) {
  666.           $ProductTag = new ProductTag();
  667.           $ProductTag
  668.             ->setProduct($Product)
  669.             ->setTag($Tag);
  670.           $Product->addProductTag($ProductTag);
  671.           $this->entityManager->persist($ProductTag);
  672.         }
  673.         $Product->setUpdateDate(new \DateTime());
  674.         $this->entityManager->flush();
  675.         log_info('商品登録完了', [$id]);
  676.         $event = new EventArgs(
  677.           [
  678.           'form' => $form,
  679.           'Product' => $Product,
  680.           ], $request
  681.         );
  682.         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_EDIT_COMPLETE);
  683.         $this->addSuccess('admin.common.save_complete''admin');
  684.         if ($returnLink $form->get('return_link')->getData()) {
  685.           try {
  686.             // $returnLinkはpathの形式で渡される. pathが存在するかをルータでチェックする.
  687.             $pattern '/^' preg_quote($request->getBasePath(), '/') . '/';
  688.             $returnLink preg_replace($pattern''$returnLink);
  689.             $result $router->match($returnLink);
  690.             // パラメータのみ抽出
  691.             $params array_filter($result, function ($key) {
  692.               return !== \strpos($key'_');
  693.             }, ARRAY_FILTER_USE_KEY);
  694.             // pathからurlを再構築してリダイレクト.
  695.             return $this->redirectToRoute($result['_route'], $params);
  696.           } catch (\Exception $e) {
  697.             // マッチしない場合はログ出力してスキップ.
  698.             log_warning('URLの形式が不正です。');
  699.           }
  700.         }
  701.         $cacheUtil->clearDoctrineCache();
  702.         return $this->redirectToRoute('admin_product_product_edit', ['id' => $Product->getId()]);
  703.       }
  704.     }
  705.     // 検索結果の保持
  706.     $builder $this->formFactory
  707.       ->createBuilder(SearchProductType::class);
  708.     $event = new EventArgs(
  709.       [
  710.       'builder' => $builder,
  711.       'Product' => $Product,
  712.       ], $request
  713.     );
  714.     $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_EDIT_SEARCH);
  715.     $searchForm $builder->getForm();
  716.     if ('POST' === $request->getMethod()) {
  717.       $searchForm->handleRequest($request);
  718.     }
  719.     // Get Tags
  720.     $TagsList $this->tagRepository->getList();
  721.     // ツリー表示のため、ルートからのカテゴリを取得
  722.     $TopCategories $this->categoryRepository->getList(null);
  723.     $ChoicedCategoryIds array_map(function ($Category) {
  724.       return $Category->getId();
  725.     }, $form->get('Category')->getData());
  726.     return [
  727.       'Product' => $Product,
  728.       'Tags' => $Tags,
  729.       'TagsList' => $TagsList,
  730.       'form' => $form->createView(),
  731.       'searchForm' => $searchForm->createView(),
  732.       'has_class' => $has_class,
  733.       'id' => $id,
  734.       'TopCategories' => $TopCategories,
  735.       'ChoicedCategoryIds' => $ChoicedCategoryIds,
  736.       'orderCount' => $id $this->orderRepository->getOrderCountByProductId((int)$id) : 0,
  737.     ];
  738.   }
  739.   /**
  740.    * @Route("/%eccube_admin_route%/product/product/{id}/delete", requirements={"id" = "\d+"}, name="admin_product_product_delete", methods={"DELETE"})
  741.    */
  742.   public function delete(Request $requestCacheUtil $cacheUtil$id null) {
  743.     $this->isTokenValid();
  744.     $session $request->getSession();
  745.     $page_no intval($session->get('eccube.admin.product.search.page_no'));
  746.     $page_no $page_no $page_no Constant::ENABLED;
  747.     $success false;
  748.     if (!is_null($id)) {
  749.       /* @var $Product \Eccube\Entity\Product */
  750.       $Product $this->productRepository->find($id);
  751.       if (!$Product) {
  752.         if ($request->isXmlHttpRequest()) {
  753.           $message trans('admin.common.delete_error_already_deleted');
  754.           return $this->json(['success' => $success'message' => $message]);
  755.         } else {
  756.           $this->deleteMessage();
  757.           $rUrl $this->generateUrl('admin_product_page', ['page_no' => $page_no]) . '?resume=' Constant::ENABLED;
  758.           return $this->redirect($rUrl);
  759.         }
  760.       }
  761.       if ($Product instanceof Product) {
  762.         log_info('商品削除開始', [$id]);
  763.         $ProductClasses $Product->getProductClasses();
  764.         try {
  765.           // 論理削除(del_flg = 1)
  766.           $Product->setDelFlg(1);
  767.           $this->entityManager->persist($Product);
  768.           $this->entityManager->flush();
  769.           $event = new EventArgs(
  770.             [
  771.             'Product' => $Product,
  772.             'ProductClass' => $ProductClasses,
  773.             ], $request
  774.           );
  775.           $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_DELETE_COMPLETE);
  776.           // 論理削除のため画像ファイルは削除しない
  777.           log_info('商品削除完了(論理削除)', [$id]);
  778.           $success true;
  779.           $message trans('admin.common.delete_complete');
  780.           $cacheUtil->clearDoctrineCache();
  781.         } catch (ForeignKeyConstraintViolationException $e) {
  782.           log_info('商品削除エラー', [$id]);
  783.           $message trans('admin.common.delete_error_foreign_key', ['%name%' => $Product->getName()]);
  784.         }
  785.       } else {
  786.         log_info('商品削除エラー', [$id]);
  787.         $message trans('admin.common.delete_error');
  788.       }
  789.     } else {
  790.       log_info('商品削除エラー', [$id]);
  791.       $message trans('admin.common.delete_error');
  792.     }
  793.     if ($request->isXmlHttpRequest()) {
  794.       return $this->json(['success' => $success'message' => $message]);
  795.     } else {
  796.       if ($success) {
  797.         $this->addSuccess($message'admin');
  798.       } else {
  799.         $this->addError($message'admin');
  800.       }
  801.       $rUrl $this->generateUrl('admin_product_page', ['page_no' => $page_no]) . '?resume=' Constant::ENABLED;
  802.       return $this->redirect($rUrl);
  803.     }
  804.   }
  805.   /**
  806.    * @Route("/%eccube_admin_route%/product/product/{id}/copy", requirements={"id" = "\d+"}, name="admin_product_product_copy", methods={"POST"})
  807.    */
  808.   public function copy(Request $request$id null) {
  809.     $this->isTokenValid();
  810.     if (!is_null($id)) {
  811.       $Product $this->productRepository->find($id);
  812.       if ($Product instanceof Product) {
  813.         $CopyProduct = clone $Product;
  814.         $CopyProduct->copy();
  815.         $ProductStatus $this->productStatusRepository->find(ProductStatus::DISPLAY_HIDE);
  816.         $CopyProduct->setStatus($ProductStatus);
  817.         $CopyProductCategories $CopyProduct->getProductCategories();
  818.         foreach ($CopyProductCategories as $Category) {
  819.           $this->entityManager->persist($Category);
  820.         }
  821.         // 規格あり商品の場合は, デフォルトの商品規格を取得し登録する.
  822.         if ($CopyProduct->hasProductClass()) {
  823.           $dummyClass $this->productClassRepository->findOneBy([
  824.             'visible' => false,
  825.             'ClassCategory1' => null,
  826.             'ClassCategory2' => null,
  827.             'Product' => $Product,
  828.           ]);
  829.           $dummyClass = clone $dummyClass;
  830.           $dummyClass->setProduct($CopyProduct);
  831.           $CopyProduct->addProductClass($dummyClass);
  832.         }
  833.         $CopyProductClasses $CopyProduct->getProductClasses();
  834.         foreach ($CopyProductClasses as $Class) {
  835.           $Stock $Class->getProductStock();
  836.           $CopyStock = clone $Stock;
  837.           $CopyStock->setProductClass($Class);
  838.           $this->entityManager->persist($CopyStock);
  839.           $TaxRule $Class->getTaxRule();
  840.           if ($TaxRule) {
  841.             $CopyTaxRule = clone $TaxRule;
  842.             $CopyTaxRule->setProductClass($Class);
  843.             $CopyTaxRule->setProduct($CopyProduct);
  844.             $this->entityManager->persist($CopyTaxRule);
  845.           }
  846.           $this->entityManager->persist($Class);
  847.         }
  848.         $Images $CopyProduct->getProductImage();
  849.         foreach ($Images as $Image) {
  850.           // 画像ファイルを新規作成
  851.           $extension pathinfo($Image->getFileName(), PATHINFO_EXTENSION);
  852.           $filename date('mdHis') . uniqid('_') . '.' $extension;
  853.           try {
  854.             $fs = new Filesystem();
  855.             $fs->copy($this->eccubeConfig['eccube_save_image_dir'] . '/' $Image->getFileName(), $this->eccubeConfig['eccube_save_image_dir'] . '/' $filename);
  856.           } catch (\Exception $e) {
  857.             // エラーが発生しても無視する
  858.           }
  859.           $Image->setFileName($filename);
  860.           $this->entityManager->persist($Image);
  861.         }
  862.         $Tags $CopyProduct->getProductTag();
  863.         foreach ($Tags as $Tag) {
  864.           $this->entityManager->persist($Tag);
  865.         }
  866.         $this->entityManager->persist($CopyProduct);
  867.         $this->entityManager->flush();
  868.         $event = new EventArgs(
  869.           [
  870.           'Product' => $Product,
  871.           'CopyProduct' => $CopyProduct,
  872.           'CopyProductCategories' => $CopyProductCategories,
  873.           'CopyProductClasses' => $CopyProductClasses,
  874.           'images' => $Images,
  875.           'Tags' => $Tags,
  876.           ], $request
  877.         );
  878.         $this->eventDispatcher->dispatch($eventEccubeEvents::ADMIN_PRODUCT_COPY_COMPLETE);
  879.         $this->addSuccess('admin.product.copy_complete''admin');
  880.         return $this->redirectToRoute('admin_product_product_edit', ['id' => $CopyProduct->getId()]);
  881.       } else {
  882.         $this->addError('admin.product.copy_error''admin');
  883.       }
  884.     } else {
  885.       $msg trans('admin.product.copy_error');
  886.       $this->addError($msg'admin');
  887.     }
  888.     return $this->redirectToRoute('admin_product');
  889.   }
  890.   /**
  891.    * 商品CSVの出力.
  892.    *
  893.    * @Route("/%eccube_admin_route%/product/export", name="admin_product_export", methods={"GET"})
  894.    *
  895.    * @param Request $request
  896.    *
  897.    * @return StreamedResponse
  898.    */
  899.   public function export(Request $request) {
  900.     // タイムアウトを無効にする.
  901.     set_time_limit(0);
  902.     // sql loggerを無効にする.
  903.     $em $this->entityManager;
  904.     $em->getConfiguration()->setSQLLogger(null);
  905.     $response = new StreamedResponse();
  906.     $response->setCallback(function () use ($request) {
  907.       // CSV種別を元に初期化.
  908.       $this->csvExportService->initCsvType(CsvType::CSV_TYPE_PRODUCT);
  909.       // 商品データ検索用のクエリビルダを取得.
  910.       $qb $this->csvExportService->getProductQueryBuilder($request);
  911.       // パレス商品のみに絞り込み(sale_type_id = NULL or 1 or 3)
  912.       $qb->andWhere('pc.SaleType IS NULL OR pc.SaleType IN (:saleTypes)')
  913.         ->setParameter('saleTypes', [13]);
  914.       // チェックボックスで選択された商品IDで絞り込み
  915.       $ids $request->get('ids');
  916.       if (!empty($ids)) {
  917.         $qb->andWhere($qb->expr()->in('p.id'':ids'))
  918.           ->setParameter('ids'$ids);
  919.       }
  920.       // ヘッダ行の出力.
  921.       $this->csvExportService->exportHeader();
  922.       // Get stock status
  923.       $isOutOfStock 0;
  924.       $session $request->getSession();
  925.       if ($session->has('eccube.admin.product.search')) {
  926.         $searchData $session->get('eccube.admin.product.search', []);
  927.         if (isset($searchData['stock_status']) && $searchData['stock_status'] === 0) {
  928.           $isOutOfStock 1;
  929.         }
  930.       }
  931.       // ★ ProductPlus項目を一括取得(最適化)
  932.       $conn $this->entityManager->getConnection();
  933.       // product_item_value用(テキスト値): item_id → product_id → value
  934.       $sql "SELECT pd.product_id, pd.product_item_id, pdd.value
  935.               FROM plg_productplus_dtb_product_data pd
  936.               JOIN plg_productplus_dtb_product_data_detail pdd
  937.                   ON pd.id = pdd.product_data_id";
  938.       $rows $conn->executeQuery($sql)->fetchAllAssociative();
  939.       $hashProductPlusValue = [];
  940.       foreach ($rows as $row) {
  941.           $itemId $row['product_item_id'];
  942.           $productId $row['product_id'];
  943.           // 前後の空白を削除(半角・全角両方)
  944.           $value preg_replace('/^[\s ]+|[\s ]+$/u'''$row['value'] ?? '');
  945.           if (!isset($hashProductPlusValue[$itemId])) {
  946.               $hashProductPlusValue[$itemId] = [];
  947.           }
  948.           // 同じitem_id/product_idに複数の値がある場合はカンマ区切りで結合
  949.           if (isset($hashProductPlusValue[$itemId][$productId])) {
  950.               $hashProductPlusValue[$itemId][$productId] .= ',' $value;
  951.           } else {
  952.               $hashProductPlusValue[$itemId][$productId] = $value;
  953.           }
  954.       }
  955.       // product_item_id用(選択肢ID): item_id → product_id → num_value
  956.       $sql "SELECT pd.product_id, pd.product_item_id, pdd.num_value
  957.               FROM plg_productplus_dtb_product_data pd
  958.               JOIN plg_productplus_dtb_product_data_detail pdd
  959.                   ON pd.id = pdd.product_data_id
  960.               WHERE pdd.num_value IS NOT NULL";
  961.       $rows $conn->executeQuery($sql)->fetchAllAssociative();
  962.       $hashProductPlusItemId = [];
  963.       foreach ($rows as $row) {
  964.           $itemId $row['product_item_id'];
  965.           $productId $row['product_id'];
  966.           if (!isset($hashProductPlusItemId[$itemId])) {
  967.               $hashProductPlusItemId[$itemId] = [];
  968.           }
  969.           // 同じitem_id/product_idに複数の値がある場合はカンマ区切りで結合
  970.           if (isset($hashProductPlusItemId[$itemId][$productId])) {
  971.               $hashProductPlusItemId[$itemId][$productId] .= ',' $row['num_value'];
  972.           } else {
  973.               $hashProductPlusItemId[$itemId][$productId] = $row['num_value'];
  974.           }
  975.       }
  976.       // ★ CustomerRank価格を一括取得(最適化)
  977.       $sql "SELECT product_class_id, customer_rank_id, price
  978.               FROM plg_customerrank_dtb_customer_price";
  979.       $rows $conn->executeQuery($sql)->fetchAllAssociative();
  980.       $hashCustomerRank = [];
  981.       foreach ($rows as $row) {
  982.           $rankId $row['customer_rank_id'];
  983.           $classId $row['product_class_id'];
  984.           if (!isset($hashCustomerRank[$rankId])) {
  985.               $hashCustomerRank[$rankId] = [];
  986.           }
  987.           $hashCustomerRank[$rankId][$classId] = $row['price'];
  988.       }
  989.       // joinする場合はiterateが使えないため, select句をdistinctする.
  990.       // http://qiita.com/suin/items/2b1e98105fa3ef89beb7
  991.       // distinctのmysqlとpgsqlの挙動をあわせる.
  992.       // http://uedatakeshi.blogspot.jp/2010/04/distinct-oeder-by-postgresmysql.html
  993.       $qb->resetDQLPart('select');
  994.       if ($isOutOfStock) {
  995.         $qb->select('p, pc')
  996.           ->distinct();
  997.       } else {
  998.         $qb->select('p')
  999.           ->distinct();
  1000.       }
  1001.       // データ行の出力.
  1002.       $this->csvExportService->setExportQueryBuilder($qb);
  1003.       $this->csvExportService->exportData(function ($entityCsvExportService $csvService) use ($request$hashProductPlusValue$hashProductPlusItemId$hashCustomerRank) {
  1004.         $Csvs $csvService->getCsvs();
  1005.         /** @var $Product \Eccube\Entity\Product */
  1006.         $Product $entity;
  1007.         $productId $Product->getId();
  1008.         /** @var $ProductClasses \Eccube\Entity\ProductClass[] */
  1009.         $ProductClasses $Product->getProductClasses();
  1010.         foreach ($ProductClasses as $ProductClass) {
  1011.           $productClassId $ProductClass->getId();
  1012.           $ExportCsvRow = new ExportCsvRow();
  1013.           // CSV出力項目と合致するデータを取得.
  1014.           foreach ($Csvs as $Csv) {
  1015.             // 商品データを検索.
  1016.             $ExportCsvRow->setData($csvService->getData($Csv$Product));
  1017.             if ($ExportCsvRow->isDataNull()) {
  1018.               // 商品規格情報を検索.
  1019.               $ExportCsvRow->setData($csvService->getData($Csv$ProductClass));
  1020.             }
  1021.             // ★ ProductPlus項目を直接処理(イベント不要)
  1022.             if ($ExportCsvRow->isDataNull()) {
  1023.               $csvEntityName str_replace('\\\\''\\'$Csv->getEntityName());
  1024.               if ($csvEntityName == 'Plugin\ProductPlus42\Entity\ProductData') {
  1025.                 $product_item_id = (int)$Csv->getReferenceFieldName();
  1026.                 $fieldName $Csv->getFieldName();
  1027.                 if ($fieldName == 'product_item_id') {
  1028.                   // 選択肢ID(num_value)
  1029.                   $value $hashProductPlusItemId[$product_item_id][$productId] ?? null;
  1030.                   $ExportCsvRow->setData($value);
  1031.                 } elseif ($fieldName == 'product_item_value') {
  1032.                   // テキスト値(value)
  1033.                   $value $hashProductPlusValue[$product_item_id][$productId] ?? null;
  1034.                   $ExportCsvRow->setData($value);
  1035.                 }
  1036.               }
  1037.               // ★ CustomerRank価格を直接処理(イベント不要)
  1038.               if ($csvEntityName == 'Plugin\CustomerRank42\Entity\CustomerPrice') {
  1039.                 $fieldName $Csv->getFieldName();
  1040.                 // customerrank_price_2 → rank_id = 2
  1041.                 if (preg_match('/customerrank_price_(\d+)/'$fieldName$matches)) {
  1042.                   $rankId = (int)$matches[1];
  1043.                   $value $hashCustomerRank[$rankId][$productClassId] ?? null;
  1044.                   $ExportCsvRow->setData($value);
  1045.                 }
  1046.               }
  1047.             }
  1048.             $ExportCsvRow->pushData();
  1049.           }
  1050.           // $row[] = number_format(memory_get_usage(true));
  1051.           // 出力.
  1052.           $row $ExportCsvRow->getRow();
  1053.           foreach($row as $k => $v){
  1054.               if (empty($v)) {
  1055.                   $row[$k] = "";
  1056.               } else {
  1057.                   // 前後の空白を削除(半角・全角両方)
  1058.                   $v preg_replace('/^[\s ]+|[\s ]+$/u'''$v);
  1059.                   // 末尾の.00を削除
  1060.                   $row[$k] = preg_replace('/\.00$/'''$v);
  1061.               }
  1062.           }
  1063.           $csvService->fputcsv($row);
  1064.         }
  1065.       });
  1066.     });
  1067.     $now = new \DateTime();
  1068.     $filename 'product_' $now->format('YmdHis') . '.csv';
  1069.     $response->headers->set('Content-Type''application/octet-stream');
  1070.     $response->headers->set('Content-Disposition''attachment; filename=' $filename);
  1071.     log_info('商品CSV出力ファイル名', [$filename]);
  1072.     return $response;
  1073.   }
  1074.   /**
  1075.    * 商品CSV2の出力.
  1076.    *
  1077.    * @Route("/%eccube_admin_route%/product/export2", name="admin_product_export2", methods={"GET"})
  1078.    *
  1079.    * @param Request $request
  1080.    *
  1081.    * @return StreamedResponse
  1082.    */
  1083.   public function export2(Request $request) {
  1084.     set_time_limit(0);
  1085.     $em $this->entityManager;
  1086.     $em->getConfiguration()->setSQLLogger(null);
  1087.     $response = new StreamedResponse();
  1088.     $response->setCallback(function () use ($request) {
  1089.       $this->csvExportService->initCsvType(CsvType::CSV_TYPE_PRODUCT2);
  1090.       $qb $this->csvExportService->getProductQueryBuilder($request);
  1091.       $qb->andWhere('pc.SaleType IS NULL OR pc.SaleType IN (:saleTypes)')
  1092.         ->setParameter('saleTypes', [13]);
  1093.       $ids $request->get('ids');
  1094.       if (!empty($ids)) {
  1095.         $qb->andWhere($qb->expr()->in('p.id'':ids'))
  1096.           ->setParameter('ids'$ids);
  1097.       }
  1098.       $this->csvExportService->exportHeader();
  1099.       $isOutOfStock 0;
  1100.       $session $request->getSession();
  1101.       if ($session->has('eccube.admin.product.search')) {
  1102.         $searchData $session->get('eccube.admin.product.search', []);
  1103.         if (isset($searchData['stock_status']) && $searchData['stock_status'] === 0) {
  1104.           $isOutOfStock 1;
  1105.         }
  1106.       }
  1107.       $conn $this->entityManager->getConnection();
  1108.       $sql "SELECT pd.product_id, pd.product_item_id, pdd.value
  1109.               FROM plg_productplus_dtb_product_data pd
  1110.               JOIN plg_productplus_dtb_product_data_detail pdd
  1111.                   ON pd.id = pdd.product_data_id";
  1112.       $rows $conn->executeQuery($sql)->fetchAllAssociative();
  1113.       $hashProductPlusValue = [];
  1114.       foreach ($rows as $row) {
  1115.           $itemId $row['product_item_id'];
  1116.           $productId $row['product_id'];
  1117.           $value preg_replace('/^[\s ]+|[\s ]+$/u'''$row['value'] ?? '');
  1118.           if (!isset($hashProductPlusValue[$itemId])) {
  1119.               $hashProductPlusValue[$itemId] = [];
  1120.           }
  1121.           if (isset($hashProductPlusValue[$itemId][$productId])) {
  1122.               $hashProductPlusValue[$itemId][$productId] .= ',' $value;
  1123.           } else {
  1124.               $hashProductPlusValue[$itemId][$productId] = $value;
  1125.           }
  1126.       }
  1127.       $sql "SELECT pd.product_id, pd.product_item_id, pdd.num_value
  1128.               FROM plg_productplus_dtb_product_data pd
  1129.               JOIN plg_productplus_dtb_product_data_detail pdd
  1130.                   ON pd.id = pdd.product_data_id
  1131.               WHERE pdd.num_value IS NOT NULL";
  1132.       $rows $conn->executeQuery($sql)->fetchAllAssociative();
  1133.       $hashProductPlusItemId = [];
  1134.       foreach ($rows as $row) {
  1135.           $itemId $row['product_item_id'];
  1136.           $productId $row['product_id'];
  1137.           if (!isset($hashProductPlusItemId[$itemId])) {
  1138.               $hashProductPlusItemId[$itemId] = [];
  1139.           }
  1140.           if (isset($hashProductPlusItemId[$itemId][$productId])) {
  1141.               $hashProductPlusItemId[$itemId][$productId] .= ',' $row['num_value'];
  1142.           } else {
  1143.               $hashProductPlusItemId[$itemId][$productId] = $row['num_value'];
  1144.           }
  1145.       }
  1146.       $sql "SELECT product_class_id, customer_rank_id, price
  1147.               FROM plg_customerrank_dtb_customer_price";
  1148.       $rows $conn->executeQuery($sql)->fetchAllAssociative();
  1149.       $hashCustomerRank = [];
  1150.       foreach ($rows as $row) {
  1151.           $rankId $row['customer_rank_id'];
  1152.           $classId $row['product_class_id'];
  1153.           if (!isset($hashCustomerRank[$rankId])) {
  1154.               $hashCustomerRank[$rankId] = [];
  1155.           }
  1156.           $hashCustomerRank[$rankId][$classId] = $row['price'];
  1157.       }
  1158.       $qb->resetDQLPart('select');
  1159.       if ($isOutOfStock) {
  1160.         $qb->select('p, pc')
  1161.           ->distinct();
  1162.       } else {
  1163.         $qb->select('p')
  1164.           ->distinct();
  1165.       }
  1166.       $this->csvExportService->setExportQueryBuilder($qb);
  1167.       $this->csvExportService->exportData(function ($entityCsvExportService $csvService) use ($request$hashProductPlusValue$hashProductPlusItemId$hashCustomerRank) {
  1168.         $Csvs $csvService->getCsvs();
  1169.         /** @var $Product \Eccube\Entity\Product */
  1170.         $Product $entity;
  1171.         $productId $Product->getId();
  1172.         /** @var $ProductClasses \Eccube\Entity\ProductClass[] */
  1173.         $ProductClasses $Product->getProductClasses();
  1174.         foreach ($ProductClasses as $ProductClass) {
  1175.           $productClassId $ProductClass->getId();
  1176.           $ExportCsvRow = new ExportCsvRow();
  1177.           foreach ($Csvs as $Csv) {
  1178.             $ExportCsvRow->setData($csvService->getData($Csv$Product));
  1179.             if ($ExportCsvRow->isDataNull()) {
  1180.               $ExportCsvRow->setData($csvService->getData($Csv$ProductClass));
  1181.             }
  1182.             if ($ExportCsvRow->isDataNull()) {
  1183.               $csvEntityName str_replace('\\\\''\\'$Csv->getEntityName());
  1184.               if ($csvEntityName == 'Plugin\ProductPlus42\Entity\ProductData') {
  1185.                 $product_item_id = (int)$Csv->getReferenceFieldName();
  1186.                 $fieldName $Csv->getFieldName();
  1187.                 if ($fieldName == 'product_item_id') {
  1188.                   $value $hashProductPlusItemId[$product_item_id][$productId] ?? null;
  1189.                   $ExportCsvRow->setData($value);
  1190.                 } elseif ($fieldName == 'product_item_value') {
  1191.                   $value $hashProductPlusValue[$product_item_id][$productId] ?? null;
  1192.                   $ExportCsvRow->setData($value);
  1193.                 }
  1194.               }
  1195.               if ($csvEntityName == 'Plugin\CustomerRank42\Entity\CustomerPrice') {
  1196.                 $fieldName $Csv->getFieldName();
  1197.                 if (preg_match('/customerrank_price_(\d+)/'$fieldName$matches)) {
  1198.                   $rankId = (int)$matches[1];
  1199.                   $value $hashCustomerRank[$rankId][$productClassId] ?? null;
  1200.                   $ExportCsvRow->setData($value);
  1201.                 }
  1202.               }
  1203.             }
  1204.             $ExportCsvRow->pushData();
  1205.           }
  1206.           $row $ExportCsvRow->getRow();
  1207.           foreach($row as $k => $v){
  1208.               if (empty($v)) {
  1209.                   $row[$k] = "";
  1210.               } else {
  1211.                   $v preg_replace('/^[\s ]+|[\s ]+$/u'''$v);
  1212.                   $row[$k] = preg_replace('/\.00$/'''$v);
  1213.               }
  1214.           }
  1215.           $csvService->fputcsv($row);
  1216.         }
  1217.       });
  1218.     });
  1219.     $now = new \DateTime();
  1220.     $filename 'product2_' $now->format('YmdHis') . '.csv';
  1221.     $response->headers->set('Content-Type''application/octet-stream');
  1222.     $response->headers->set('Content-Disposition''attachment; filename=' $filename);
  1223.     log_info('商品CSV2出力ファイル名', [$filename]);
  1224.     return $response;
  1225.   }
  1226.   /**
  1227.    * ProductCategory作成
  1228.    *
  1229.    * @param \Eccube\Entity\Product $Product
  1230.    * @param \Eccube\Entity\Category $Category
  1231.    * @param integer $count
  1232.    *
  1233.    * @return \Eccube\Entity\ProductCategory
  1234.    */
  1235.   private function createProductCategory($Product$Category$count) {
  1236.     $ProductCategory = new ProductCategory();
  1237.     $ProductCategory->setProduct($Product);
  1238.     $ProductCategory->setProductId($Product->getId());
  1239.     $ProductCategory->setCategory($Category);
  1240.     $ProductCategory->setCategoryId($Category->getId());
  1241.     return $ProductCategory;
  1242.   }
  1243.   /**
  1244.    * Bulk public action
  1245.    *
  1246.    * @Route("/%eccube_admin_route%/product/bulk/product-status/{id}", requirements={"id" = "\d+"}, name="admin_product_bulk_product_status", methods={"POST"})
  1247.    *
  1248.    * @param Request $request
  1249.    * @param ProductStatus $ProductStatus
  1250.    *
  1251.    * @return RedirectResponse
  1252.    */
  1253.   public function bulkProductStatus(Request $requestProductStatus $ProductStatusCacheUtil $cacheUtil) {
  1254.     $this->isTokenValid();
  1255.     /** @var Product[] $Products */
  1256.     $Products $this->productRepository->findBy(['id' => $request->get('ids')]);
  1257.     $count 0;
  1258.     foreach ($Products as $Product) {
  1259.       try {
  1260.         $Product->setStatus($ProductStatus);
  1261.         $this->productRepository->save($Product);
  1262.         $count++;
  1263.       } catch (\Exception $e) {
  1264.         $this->addError($e->getMessage(), 'admin');
  1265.       }
  1266.     }
  1267.     try {
  1268.       if ($count) {
  1269.         $this->entityManager->flush();
  1270.         $msg $this->translator->trans('admin.product.bulk_change_status_complete', [
  1271.           '%count%' => $count,
  1272.           '%status%' => $ProductStatus->getName(),
  1273.         ]);
  1274.         $this->addSuccess($msg'admin');
  1275.         $cacheUtil->clearDoctrineCache();
  1276.       }
  1277.     } catch (\Exception $e) {
  1278.       $this->addError($e->getMessage(), 'admin');
  1279.     }
  1280.     return $this->redirectToRoute('admin_product', ['resume' => Constant::ENABLED]);
  1281.   }
  1282.   /**
  1283.    * 目安価格を保存
  1284.    *
  1285.    * @Route("/%eccube_admin_route%/product/update_price2", name="admin_product_update_price2", methods={"POST"})
  1286.    */
  1287.   public function updatePrice2(Request $request) {
  1288.     $productId $request->get('product_id');
  1289.     $price2 $request->get('price2');
  1290.     try {
  1291.       // EntityManagerを取得
  1292.       $em $this->entityManager;
  1293.       $conn $em->getConnection();
  1294.       // product_data_idを取得(plg_productplus_dtb_product_dataから)
  1295.       $pdSql "SELECT id FROM plg_productplus_dtb_product_data WHERE product_id = :product_id AND product_item_id = 55";
  1296.       $pdStmt $conn->prepare($pdSql);
  1297.       $pdStmt->bindValue('product_id'$productId);
  1298.       $pdResultSet $pdStmt->executeQuery();
  1299.       $productData $pdResultSet->fetchAssociative();
  1300.       if ($productData) {
  1301.         // product_dataが存在する場合、detailを確認
  1302.         $pddSql "SELECT id FROM plg_productplus_dtb_product_data_detail WHERE product_data_id = :product_data_id";
  1303.         $pddStmt $conn->prepare($pddSql);
  1304.         $pddStmt->bindValue('product_data_id'$productData['id']);
  1305.         $pddResultSet $pddStmt->executeQuery();
  1306.         $existing $pddResultSet->fetchAssociative();
  1307.         if ($existing) {
  1308.           // 更新
  1309.           $updateSql "UPDATE plg_productplus_dtb_product_data_detail SET value = :price2 WHERE id = :id";
  1310.           $updateStmt $conn->prepare($updateSql);
  1311.           $updateStmt->bindValue('price2'$price2);
  1312.           $updateStmt->bindValue('id'$existing['id']);
  1313.           $updateStmt->executeQuery();
  1314.         } else {
  1315.           // detail追加
  1316.           $insertDetailSql "INSERT INTO plg_productplus_dtb_product_data_detail (product_data_id, value, sort_no) VALUES (:product_data_id, :price2, 0)";
  1317.           $insertDetailStmt $conn->prepare($insertDetailSql);
  1318.           $insertDetailStmt->bindValue('product_data_id'$productData['id']);
  1319.           $insertDetailStmt->bindValue('price2'$price2);
  1320.           $insertDetailStmt->executeQuery();
  1321.         }
  1322.       } else {
  1323.         // product_dataから作成
  1324.         $insertDataSql "INSERT INTO plg_productplus_dtb_product_data (product_id, product_item_id, create_date) VALUES (:product_id, 55, NOW())";
  1325.         $insertDataStmt $conn->prepare($insertDataSql);
  1326.         $insertDataStmt->bindValue('product_id'$productId);
  1327.         $insertDataStmt->executeQuery();
  1328.         $newProductDataId $conn->lastInsertId();
  1329.         // detail追加
  1330.         $insertDetailSql "INSERT INTO plg_productplus_dtb_product_data_detail (product_data_id, value, sort_no) VALUES (:product_data_id, :price2, 0)";
  1331.         $insertDetailStmt $conn->prepare($insertDetailSql);
  1332.         $insertDetailStmt->bindValue('product_data_id'$newProductDataId);
  1333.         $insertDetailStmt->bindValue('price2'$price2);
  1334.         $insertDetailStmt->executeQuery();
  1335.       }
  1336.       return $this->json(['success' => true'message' => '目安価格を反映しました。']);
  1337.     } catch (\Exception $e) {
  1338.       return $this->json(['success' => false'message' => 'エラーが発生しました: ' $e->getMessage()], 500);
  1339.     }
  1340.   }
  1341. }