app/Plugin/PiaHistory/EventSubscriber/ProductEditEventSubscriber.php line 129

Open in your IDE?
  1. <?php
  2. namespace Plugin\PiaHistory\EventSubscriber;
  3. use Eccube\Entity\Product;
  4. use Eccube\Repository\ProductRepository;
  5. use Eccube\Repository\MemberRepository;
  6. use Doctrine\ORM\EntityManagerInterface;
  7. use Plugin\PiaHistory\Entity\PiaHistory;
  8. use Plugin\PiaHistory\Entity\PiaHisProduct;
  9. use Plugin\PiaHistory\Entity\PiaHisProductClass;
  10. use Plugin\PiaHistory\Entity\PiaHisProductCategory;
  11. use Plugin\PiaHistory\Entity\PiaHisProductImage;
  12. use Plugin\PiaHistory\Entity\PiaHisProductStock;
  13. use Plugin\PiaHistory\Entity\PiaHisProductTag;
  14. use Plugin\PiaHistory\Entity\PiaHisProductAdd;
  15. use Plugin\PiaHistory\Entity\PiaHisRankPrice;
  16. use Plugin\PiaHistory\Entity\PiaHisCustomerClass;
  17. use Plugin\PiaHistory\Entity\PiaHisPoint;
  18. use Plugin\PiaHistory\Entity\PiaHisMeyasu;
  19. use Plugin\PiaHistory\Repository\PiaHistoryRepository;
  20. use Plugin\PiaHistory\Repository\PiaHisProductRepository;
  21. use Plugin\PiaHistory\Repository\PiaHisProductClassRepository;
  22. use Plugin\PiaHistory\Repository\PiaHisProductCategoryRepository;
  23. use Plugin\PiaHistory\Repository\PiaHisProductImageRepository;
  24. use Plugin\PiaHistory\Repository\PiaHisProductStockRepository;
  25. use Plugin\PiaHistory\Repository\PiaHisProductTagRepository;
  26. use Plugin\PiaHistory\Repository\PiaHisProductAddRepository;
  27. use Plugin\PiaHistory\Repository\PiaHisRankPriceRepository;
  28. use Plugin\PiaHistory\Repository\PiaHisCustomerClassRepository;
  29. use Plugin\PiaHistory\Repository\PiaHisPointRepository;
  30. use Plugin\PiaHistory\Repository\PiaHisMeyasuRepository;
  31. use Eccube\Event\EccubeEvents;
  32. use Eccube\Event\EventArgs;
  33. use Symfony\Component\HttpFoundation\RequestStack;
  34. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  35. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  36. use Symfony\Component\HttpKernel\Event\RequestEvent;
  37. class ProductEditEventSubscriber implements EventSubscriberInterface
  38. {
  39.     private $piaHistoryRepository;
  40.     private $piaHisProductRepository;
  41.     private $piaHisProductClassRepository;
  42.     private $piaHisProductCategoryRepository;
  43.     private $piaHisProductImageRepository;
  44.     private $piaHisProductStockRepository;
  45.     private $piaHisProductTagRepository;
  46.     private $piaHisProductAddRepository;
  47.     private $piaHisRankPriceRepository;
  48.     private $piaHisCustomerClassRepository;
  49.     private $piaHisPointRepository;
  50.     private $piaHisMeyasuRepository;
  51.     private $productRepository;
  52.     private $memberRepository;
  53.     private $tokenStorage;
  54.     private $entityManager;
  55.     private $requestStack;
  56.     // 商品基本情報のフィールドラベル(updateNote生成用)
  57.     private static $basicFieldLabels = [
  58.         'name'               => '商品名',
  59.         'note'               => 'ショップ用メモ欄',
  60.         'description_list'   => '商品一覧コメント',
  61.         'description_detail' => '商品説明',
  62.         'search_word'        => '検索ワード',
  63.         'free_area'          => 'フリーエリア',
  64.         'status'             => '商品ステータス',
  65.         'datetime_start'     => '表示設定日時(開始)',
  66.         'datetime_end'       => '表示設定日時(終了)',
  67.     ];
  68.     // 商品規格フィールドのラベル(updateNote生成用)
  69.     private static $classFieldLabels = [
  70.         'price02' => '販売価格',
  71.         'price01' => '通常価格',
  72.         'stock'   => '在庫',
  73.     ];
  74.     public function __construct(
  75.         PiaHistoryRepository $piaHistoryRepository,
  76.         PiaHisProductRepository $piaHisProductRepository,
  77.         PiaHisProductClassRepository $piaHisProductClassRepository,
  78.         PiaHisProductCategoryRepository $piaHisProductCategoryRepository,
  79.         PiaHisProductImageRepository $piaHisProductImageRepository,
  80.         PiaHisProductStockRepository $piaHisProductStockRepository,
  81.         PiaHisProductTagRepository $piaHisProductTagRepository,
  82.         PiaHisProductAddRepository $piaHisProductAddRepository,
  83.         PiaHisRankPriceRepository $piaHisRankPriceRepository,
  84.         PiaHisCustomerClassRepository $piaHisCustomerClassRepository,
  85.         PiaHisPointRepository $piaHisPointRepository,
  86.         PiaHisMeyasuRepository $piaHisMeyasuRepository,
  87.         ProductRepository $productRepository,
  88.         MemberRepository $memberRepository,
  89.         TokenStorageInterface $tokenStorage,
  90.         EntityManagerInterface $entityManager,
  91.         RequestStack $requestStack
  92.     ) {
  93.         $this->piaHistoryRepository $piaHistoryRepository;
  94.         $this->piaHisProductRepository $piaHisProductRepository;
  95.         $this->piaHisProductClassRepository $piaHisProductClassRepository;
  96.         $this->piaHisProductCategoryRepository $piaHisProductCategoryRepository;
  97.         $this->piaHisProductImageRepository $piaHisProductImageRepository;
  98.         $this->piaHisProductStockRepository $piaHisProductStockRepository;
  99.         $this->piaHisProductTagRepository $piaHisProductTagRepository;
  100.         $this->piaHisProductAddRepository $piaHisProductAddRepository;
  101.         $this->piaHisRankPriceRepository $piaHisRankPriceRepository;
  102.         $this->piaHisCustomerClassRepository $piaHisCustomerClassRepository;
  103.         $this->piaHisPointRepository $piaHisPointRepository;
  104.         $this->piaHisMeyasuRepository $piaHisMeyasuRepository;
  105.         $this->productRepository $productRepository;
  106.         $this->memberRepository $memberRepository;
  107.         $this->tokenStorage $tokenStorage;
  108.         $this->entityManager $entityManager;
  109.         $this->requestStack $requestStack;
  110.     }
  111.     public static function getSubscribedEvents()
  112.     {
  113.         return [
  114.             'kernel.request' => ['onKernelRequest'0],
  115.             EccubeEvents::ADMIN_PRODUCT_EDIT_COMPLETE => ['onProductEditComplete', -1000],
  116.         ];
  117.     }
  118.     /**
  119.      * 商品編集画面のPOSTリクエストを検知して変更前の値を保存
  120.      */
  121.     public function onKernelRequest(RequestEvent $event)
  122.     {
  123.         $request $event->getRequest();
  124.         if ($request->isMethod('POST') &&
  125.             $request->attributes->get('_route') === 'admin_product_product_edit') {
  126.             $productId $request->get('id');
  127.             error_log('PiaHistory: onKernelRequest - Product ID: ' $productId);
  128.             if ($productId) {
  129.                 $this->saveProductBeforeData($productId);
  130.             }
  131.         }
  132.     }
  133.     public function onProductEditComplete(EventArgs $event)
  134.     {
  135.         error_log('PiaHistory: onProductEditComplete called');
  136.         $Product $event->getArgument('Product');
  137.         if ($Product && $Product->getId()) {
  138.             $this->saveProductHistory($Product->getId());
  139.         }
  140.     }
  141.     // -------------------------------------------------------------------------
  142.     // 変更前データをセッションに保存
  143.     // -------------------------------------------------------------------------
  144.     private function saveProductBeforeData($productId)
  145.     {
  146.         try {
  147.             $product $this->productRepository->find($productId);
  148.             if (!$product) {
  149.                 return;
  150.             }
  151.             $request $this->requestStack->getCurrentRequest();
  152.             if (!$request) {
  153.                 return;
  154.             }
  155.             $session $request->getSession();
  156.             $beforeData = [
  157.                 'product'            => $this->buildProductSnapshot($product),
  158.                 'product_classes'    => $this->buildProductClassesSnapshot($product),
  159.                 'product_categories' => $this->buildProductCategoriesSnapshot($product),
  160.                 'product_images'     => $this->buildProductImagesSnapshot($product),
  161.                 'product_tags'       => $this->buildProductTagsSnapshot($product),
  162.                 'rank_prices'        => $this->buildRankPricesSnapshot($product),
  163.                 'product_add'        => $this->buildProductAddSnapshot($product),
  164.             ];
  165.             $session->set('pia_history_before_data_' $productId$beforeData);
  166.         } catch (\Exception $e) {
  167.             error_log('PiaHistory: 変更前データ保存エラー - ' $e->getMessage());
  168.         }
  169.     }
  170.     /** 商品基本情報スナップショット */
  171.     private function buildProductSnapshot(Product $product): array
  172.     {
  173.         return [
  174.             'id'                 => $product->getId(),
  175.             'name'               => $product->getName(),
  176.             'note'               => $product->getNote(),
  177.             'description_list'   => $product->getDescriptionList(),
  178.             'description_detail' => $product->getDescriptionDetail(),
  179.             'search_word'        => $product->getSearchWord(),
  180.             'free_area'          => $product->getFreeArea(),
  181.             'status'             => $product->getStatus() ? $product->getStatus()->getId() : null,
  182.             'datetime_start'     => $this->getDisplayStartDate($product),
  183.             'datetime_end'       => $this->getDisplayEndDate($product),
  184.         ];
  185.     }
  186.     /** 商品規格スナップショット(変更検知で比較するフィールドのみ) */
  187.     private function buildProductClassesSnapshot(Product $product): array
  188.     {
  189.         $data = [];
  190.         foreach ($product->getProductClasses() as $pc) {
  191.             $data[] = [
  192.                 'id'       => $pc->getId(),
  193.                 'code'     => $pc->getCode(),
  194.                 'price01'  => (string)$pc->getPrice01(),
  195.                 'price02'  => (string)$pc->getPrice02(),
  196.                 'stock'    => $pc->isStockUnlimited() ? null : (string)$pc->getStock(),
  197.             ];
  198.         }
  199.         return $data;
  200.     }
  201.     /** 商品カテゴリースナップショット */
  202.     private function buildProductCategoriesSnapshot(Product $product): array
  203.     {
  204.         $data = [];
  205.         foreach ($product->getProductCategories() as $i => $pc) {
  206.             $data[] = [
  207.                 'category_id' => $pc->getCategory()->getId(),
  208.                 'rank'        => $i 1,
  209.             ];
  210.         }
  211.         return $data;
  212.     }
  213.     /** 商品画像スナップショット */
  214.     private function buildProductImagesSnapshot(Product $product): array
  215.     {
  216.         $data = [];
  217.         foreach ($product->getProductImage() as $i => $img) {
  218.             $data[] = [
  219.                 'file_name' => $img->getFileName(),
  220.                 'rank'      => $img->getSortNo() ?: ($i 1),
  221.             ];
  222.         }
  223.         return $data;
  224.     }
  225.     /** 商品タグスナップショット */
  226.     private function buildProductTagsSnapshot(Product $product): array
  227.     {
  228.         $data = [];
  229.         foreach ($product->getProductTag() as $pt) {
  230.             $data[] = ['tag_id' => $pt->getTag()->getId()];
  231.         }
  232.         return $data;
  233.     }
  234.     /** 会員別価格スナップショット(CustomerRank42) */
  235.     private function buildRankPricesSnapshot(Product $product): array
  236.     {
  237.         $data = [];
  238.         if (!class_exists('Plugin\CustomerRank42\Entity\CustomerPrice')) {
  239.             return $data;
  240.         }
  241.         try {
  242.             $repo $this->entityManager->getRepository('Plugin\CustomerRank42\Entity\CustomerPrice');
  243.             foreach ($product->getProductClasses() as $pc) {
  244.                 $prices $repo->findBy(['ProductClass' => $pc]);
  245.                 foreach ($prices as $cp) {
  246.                     $data[] = [
  247.                         'product_class_id' => $pc->getId(),
  248.                         'product_code'     => $pc->getCode(),
  249.                         'rank_id'          => $cp->getCustomerRank()->getId(),
  250.                         'rank_name'        => $cp->getCustomerRank()->getName(),
  251.                         'price'            => (string)$cp->getPrice(),
  252.                     ];
  253.                 }
  254.             }
  255.         } catch (\Exception $e) {
  256.             error_log('PiaHistory: 会員別価格スナップショットエラー - ' $e->getMessage());
  257.         }
  258.         return $data;
  259.     }
  260.     /** 商品追加項目スナップショット(ProductPlus42) */
  261.     private function buildProductAddSnapshot(Product $product): array
  262.     {
  263.         $data = [];
  264.         if (!class_exists('Plugin\ProductPlus42\Entity\ProductData')) {
  265.             return $data;
  266.         }
  267.         try {
  268.             $repo $this->entityManager->getRepository('Plugin\ProductPlus42\Entity\ProductData');
  269.             $productDatas $repo->findBy(['Product' => $product]);
  270.             foreach ($productDatas as $pd) {
  271.                 $item $pd->getProductItem();
  272.                 if (!$item) {
  273.                     continue;
  274.                 }
  275.                 $data[] = [
  276.                     'item_id'   => $item->getId(),
  277.                     'item_name' => $item->getName(),
  278.                     'value'     => $pd->getDataValue(),
  279.                 ];
  280.             }
  281.         } catch (\Exception $e) {
  282.             error_log('PiaHistory: 商品追加項目スナップショットエラー - ' $e->getMessage());
  283.         }
  284.         return $data;
  285.     }
  286.     // -------------------------------------------------------------------------
  287.     // 履歴保存メイン
  288.     // -------------------------------------------------------------------------
  289.     private function saveProductHistory($productId)
  290.     {
  291.         try {
  292.             error_log('PiaHistory: saveProductHistory called for Product ID: ' $productId);
  293.             if (!$this->entityManager->isOpen()) {
  294.                 error_log('PiaHistory: EntityManager is closed, skipping history save');
  295.                 return;
  296.             }
  297.             $product $this->productRepository->find($productId);
  298.             if (!$product) {
  299.                 return;
  300.             }
  301.             $request $this->requestStack->getCurrentRequest();
  302.             if (!$request) {
  303.                 return;
  304.             }
  305.             $session  $request->getSession();
  306.             $beforeData $session->get('pia_history_before_data_' $productId);
  307.             if (!$beforeData) {
  308.                 return;
  309.             }
  310.             // 変更検知
  311.             $changes $this->detectDetailedChanges($product$beforeData);
  312.             error_log('PiaHistory: Changes detected: ' . (empty($changes) ? 'NO' 'YES'));
  313.             if (empty($changes)) {
  314.                 $session->remove('pia_history_before_data_' $productId);
  315.                 return;
  316.             }
  317.             // updateNote / updateNum を生成
  318.             ['num' => $num'note' => $note] = $this->generateUpdateNote($changes);
  319.             // 管理者情報を取得
  320.             [$adminId$adminName$adminDepartment] = $this->getAdminInfo();
  321.             $this->entityManager->beginTransaction();
  322.             // 履歴メイン登録
  323.             $history = new PiaHistory();
  324.             $history->setProductId($productId);
  325.             $history->setUpdateId($adminId);
  326.             $history->setUpdateName($adminName);
  327.             $history->setUpdateDepartment($adminDepartment);
  328.             $history->setUpdateNum($num);
  329.             $history->setUpdateNote($note);
  330.             $history->setCreateDate(new \DateTime());
  331.             $history->setUpdateDate(new \DateTime());
  332.             $history->setDelFlg(0);
  333.             $this->entityManager->persist($history);
  334.             $this->entityManager->flush();
  335.             $historyId $history->getId();
  336.             // 各詳細テーブルへの保存
  337.             if (isset($changes['product'])) {
  338.                 $this->saveProductBasicHistory($historyId$product$changes['product']);
  339.             }
  340.             if (isset($changes['product_classes'])) {
  341.                 $this->saveProductClassHistory($historyId$product);
  342.             }
  343.             if (isset($changes['product_categories'])) {
  344.                 $this->saveProductCategoryHistory($historyId$product);
  345.             }
  346.             if (isset($changes['product_images'])) {
  347.                 $this->saveProductImageHistory($historyId$product);
  348.             }
  349.             if (isset($changes['product_tags'])) {
  350.                 $this->saveProductTagHistory($historyId$product);
  351.             }
  352.             // 在庫は常に現状を保存
  353.             $this->saveProductStockHistory($historyId$product);
  354.             // 会員別価格(変更があった場合のみ)
  355.             if (isset($changes['rank_prices'])) {
  356.                 $this->saveRankPriceHistory($historyId$product);
  357.             }
  358.             // 商品追加項目(変更があった場合のみ)
  359.             if (isset($changes['product_add'])) {
  360.                 $this->saveProductAddHistory($historyId$product);
  361.             }
  362.             // 目安価格
  363.             $this->saveMeyasuHistory($historyId$product);
  364.             $this->entityManager->flush();
  365.             $this->entityManager->commit();
  366.             $session->remove('pia_history_before_data_' $productId);
  367.         } catch (\Exception $e) {
  368.             if ($this->entityManager->getConnection()->isTransactionActive()) {
  369.                 $this->entityManager->rollback();
  370.             }
  371.             error_log('PiaHistory: 商品履歴保存エラー - ' $e->getMessage());
  372.         }
  373.     }
  374.     // -------------------------------------------------------------------------
  375.     // 変更検知
  376.     // -------------------------------------------------------------------------
  377.     private function detectDetailedChanges(Product $product, array $beforeData): array
  378.     {
  379.         $changes = [];
  380.         // 基本情報
  381.         $currentProduct $this->buildProductSnapshot($product);
  382.         $diffFields = [];
  383.         foreach (array_keys(self::$basicFieldLabels) as $field) {
  384.             if (($currentProduct[$field] ?? null) !== ($beforeData['product'][$field] ?? null)) {
  385.                 $diffFields[$field] = [
  386.                     'before' => $beforeData['product'][$field] ?? null,
  387.                     'after'  => $currentProduct[$field] ?? null,
  388.                 ];
  389.             }
  390.         }
  391.         if (!empty($diffFields)) {
  392.             $changes['product'] = $diffFields;
  393.         }
  394.         // 商品規格(同一フィールド構造で比較)
  395.         $currentClasses $this->buildProductClassesSnapshot($product);
  396.         if ($currentClasses !== ($beforeData['product_classes'] ?? [])) {
  397.             $changes['product_classes'] = [
  398.                 'before' => $beforeData['product_classes'] ?? [],
  399.                 'after'  => $currentClasses,
  400.             ];
  401.         }
  402.         // カテゴリー
  403.         $currentCategories $this->buildProductCategoriesSnapshot($product);
  404.         if ($currentCategories !== ($beforeData['product_categories'] ?? [])) {
  405.             $changes['product_categories'] = [
  406.                 'before' => $beforeData['product_categories'] ?? [],
  407.                 'after'  => $currentCategories,
  408.             ];
  409.         }
  410.         // 画像
  411.         $currentImages $this->buildProductImagesSnapshot($product);
  412.         if ($currentImages !== ($beforeData['product_images'] ?? [])) {
  413.             $changes['product_images'] = [
  414.                 'before' => $beforeData['product_images'] ?? [],
  415.                 'after'  => $currentImages,
  416.             ];
  417.         }
  418.         // タグ
  419.         $currentTags $this->buildProductTagsSnapshot($product);
  420.         if ($currentTags !== ($beforeData['product_tags'] ?? [])) {
  421.             $changes['product_tags'] = [
  422.                 'before' => $beforeData['product_tags'] ?? [],
  423.                 'after'  => $currentTags,
  424.             ];
  425.         }
  426.         // 会員別価格(CustomerRank42)
  427.         $changedRankPrices $this->detectRankPriceChanges($product$beforeData['rank_prices'] ?? []);
  428.         if (!empty($changedRankPrices)) {
  429.             $changes['rank_prices'] = [
  430.                 'before'  => $beforeData['rank_prices'] ?? [],
  431.                 'after'   => $this->buildRankPricesSnapshot($product),
  432.                 'changed' => $changedRankPrices,
  433.             ];
  434.         }
  435.         // 商品追加項目(ProductPlus42)
  436.         $changedProductAdd $this->detectProductAddChanges($product$beforeData['product_add'] ?? []);
  437.         if (!empty($changedProductAdd)) {
  438.             $changes['product_add'] = [
  439.                 'before'  => $beforeData['product_add'] ?? [],
  440.                 'after'   => $this->buildProductAddSnapshot($product),
  441.                 'changed' => $changedProductAdd,
  442.             ];
  443.         }
  444.         return $changes;
  445.     }
  446.     /** 会員別価格の変更検知 */
  447.     private function detectRankPriceChanges(Product $product, array $beforeRankPrices): array
  448.     {
  449.         if (!class_exists('Plugin\CustomerRank42\Entity\CustomerPrice')) {
  450.             return [];
  451.         }
  452.         $currentRankPrices $this->buildRankPricesSnapshot($product);
  453.         // [rank_id_productClassId] => price のマップで比較
  454.         $beforeMap = [];
  455.         foreach ($beforeRankPrices as $rp) {
  456.             $key $rp['rank_id'] . '_' $rp['product_class_id'];
  457.             $beforeMap[$key] = $rp;
  458.         }
  459.         $afterMap = [];
  460.         foreach ($currentRankPrices as $rp) {
  461.             $key $rp['rank_id'] . '_' $rp['product_class_id'];
  462.             $afterMap[$key] = $rp;
  463.         }
  464.         $changed = [];
  465.         foreach ($afterMap as $key => $after) {
  466.             $before $beforeMap[$key] ?? null;
  467.             if (!$before || $before['price'] !== $after['price']) {
  468.                 $changed[] = [
  469.                     'rank_name'    => $after['rank_name'],
  470.                     'product_code' => $after['product_code'],
  471.                     'before_price' => $before $before['price'] : null,
  472.                     'after_price'  => $after['price'],
  473.                 ];
  474.             }
  475.         }
  476.         foreach ($beforeMap as $key => $before) {
  477.             if (!isset($afterMap[$key])) {
  478.                 $changed[] = [
  479.                     'rank_name'    => $before['rank_name'],
  480.                     'product_code' => $before['product_code'],
  481.                     'before_price' => $before['price'],
  482.                     'after_price'  => null,
  483.                 ];
  484.             }
  485.         }
  486.         return $changed;
  487.     }
  488.     /** 商品追加項目の変更検知 */
  489.     private function detectProductAddChanges(Product $product, array $beforeProductAdd): array
  490.     {
  491.         if (!class_exists('Plugin\ProductPlus42\Entity\ProductData')) {
  492.             return [];
  493.         }
  494.         $currentProductAdd $this->buildProductAddSnapshot($product);
  495.         $beforeMap = [];
  496.         foreach ($beforeProductAdd as $pa) {
  497.             $beforeMap[$pa['item_id']] = $pa;
  498.         }
  499.         $afterMap = [];
  500.         foreach ($currentProductAdd as $pa) {
  501.             $afterMap[$pa['item_id']] = $pa;
  502.         }
  503.         $changed = [];
  504.         foreach ($afterMap as $itemId => $after) {
  505.             $before $beforeMap[$itemId] ?? null;
  506.             if (!$before || $before['value'] !== $after['value']) {
  507.                 $changed[] = [
  508.                     'item_name'    => $after['item_name'],
  509.                     'before_value' => $before $before['value'] : null,
  510.                     'after_value'  => $after['value'],
  511.                 ];
  512.             }
  513.         }
  514.         return $changed;
  515.     }
  516.     // -------------------------------------------------------------------------
  517.     // updateNote / updateNum の生成(EC-CUBE3準拠形式)
  518.     // -------------------------------------------------------------------------
  519.     private function generateUpdateNote(array $changes): array
  520.     {
  521.         $logs = [];
  522.         $num  0;
  523.         // 商品基本情報
  524.         if (isset($changes['product'])) {
  525.             foreach (array_keys($changes['product']) as $field) {
  526.                 $label self::$basicFieldLabels[$field] ?? $field;
  527.                 $logs[] = "「{$label}」";
  528.                 $num++;
  529.             }
  530.         }
  531.         // 商品規格(価格・在庫)フィールド単位でログ
  532.         if (isset($changes['product_classes'])) {
  533.             $beforeMap = [];
  534.             foreach ($changes['product_classes']['before'] as $bc) {
  535.                 if (isset($bc['code'])) {
  536.                     $beforeMap[$bc['code']] = $bc;
  537.                 }
  538.             }
  539.             foreach ($changes['product_classes']['after'] as $ac) {
  540.                 $code $ac['code'] ?? '';
  541.                 $bc   $beforeMap[$code] ?? null;
  542.                 if (!$bc) {
  543.                     continue;
  544.                 }
  545.                 foreach (self::$classFieldLabels as $field => $label) {
  546.                     if (array_key_exists($field$ac) && array_key_exists($field$bc) &&
  547.                         $ac[$field] !== $bc[$field]) {
  548.                         $logs[] = "「{$code}{$label}」";
  549.                         $num++;
  550.                     }
  551.                 }
  552.             }
  553.         }
  554.         // 会員別価格
  555.         if (isset($changes['rank_prices']['changed'])) {
  556.             foreach ($changes['rank_prices']['changed'] as $rp) {
  557.                 $code $rp['product_code'] ?? '';
  558.                 $label $code "{$code}の会員価格:{$rp['rank_name']}"会員価格:{$rp['rank_name']}";
  559.                 $logs[] = "「{$label}」";
  560.                 $num++;
  561.             }
  562.         }
  563.         // 商品追加項目
  564.         if (isset($changes['product_add']['changed'])) {
  565.             foreach ($changes['product_add']['changed'] as $pa) {
  566.                 $logs[] = "「拡張項目:{$pa['item_name']}」";
  567.                 $num++;
  568.             }
  569.         }
  570.         // カテゴリー
  571.         if (isset($changes['product_categories'])) {
  572.             $logs[] = '「カテゴリー」';
  573.             $num++;
  574.         }
  575.         // 画像
  576.         if (isset($changes['product_images'])) {
  577.             $logs[] = '「商品画像」';
  578.             $num++;
  579.         }
  580.         // タグ
  581.         if (isset($changes['product_tags'])) {
  582.             $logs[] = '「商品タグ」';
  583.             $num++;
  584.         }
  585.         $note = !empty($logs)
  586.             ? implode('、'$logs) . 'を更新しました。'
  587.             '変更箇所無し';
  588.         return ['num' => $num'note' => $note];
  589.     }
  590.     // -------------------------------------------------------------------------
  591.     // 各詳細テーブルへの保存
  592.     // -------------------------------------------------------------------------
  593.     private function saveProductBasicHistory(int $historyIdProduct $product, array $changedFields)
  594.     {
  595.         $productHistory = new PiaHisProduct();
  596.         $productHistory->setHisId($historyId);
  597.         $productHistory->setProductId($product->getId());
  598.         $productHistory->setStatus($product->getStatus() ? $product->getStatus()->getId() : 0);
  599.         $productHistory->setName($product->getName());
  600.         $productHistory->setNote($product->getNote());
  601.         $productHistory->setDescriptionList($product->getDescriptionList());
  602.         $productHistory->setDescriptionDetail($product->getDescriptionDetail());
  603.         $productHistory->setSearchWord($product->getSearchWord());
  604.         $productHistory->setFreeArea($product->getFreeArea());
  605.         $productHistory->setDatetimeStart($this->getDisplayStartDate($product));
  606.         $productHistory->setDatetimeEnd($this->getDisplayEndDate($product));
  607.         $productHistory->setDelFlg(0);
  608.         $productHistory->setChangeData(json_encode($changedFieldsJSON_UNESCAPED_UNICODE));
  609.         $this->entityManager->persist($productHistory);
  610.     }
  611.     private function saveProductClassHistory(int $historyIdProduct $product)
  612.     {
  613.         foreach ($product->getProductClasses() as $pc) {
  614.             $classHistory = new PiaHisProductClass();
  615.             $classHistory->setHisId($historyId);
  616.             $classHistory->setProductId($product->getId());
  617.             $classHistory->setProductTypeId(null);
  618.             $classHistory->setClassCategoryId1($pc->getClassCategory1() ? $pc->getClassCategory1()->getId() : null);
  619.             $classHistory->setClassCategoryId2($pc->getClassCategory2() ? $pc->getClassCategory2()->getId() : null);
  620.             $classHistory->setDeliveryDateId($pc->getDeliveryDuration() ? $pc->getDeliveryDuration()->getId() : null);
  621.             $classHistory->setProductCode($pc->getCode());
  622.             $classHistory->setStock($pc->getStock());
  623.             $classHistory->setStockUnlimited($pc->isStockUnlimited());
  624.             $classHistory->setSaleLimit($pc->getSaleLimit());
  625.             $classHistory->setPrice01($pc->getPrice01());
  626.             $classHistory->setPrice02($pc->getPrice02());
  627.             $classHistory->setDeliveryFee($pc->getDeliveryFee());
  628.             $this->entityManager->persist($classHistory);
  629.         }
  630.     }
  631.     private function saveProductCategoryHistory(int $historyIdProduct $product)
  632.     {
  633.         foreach ($product->getProductCategories() as $i => $productCategory) {
  634.             $categoryHistory = new PiaHisProductCategory();
  635.             $categoryHistory->setHisId($historyId);
  636.             $categoryHistory->setProductId($product->getId());
  637.             $categoryHistory->setCategoryId($productCategory->getCategory()->getId());
  638.             $categoryHistory->setRank($i 1);
  639.             $this->entityManager->persist($categoryHistory);
  640.         }
  641.     }
  642.     private function saveProductImageHistory(int $historyIdProduct $product)
  643.     {
  644.         foreach ($product->getProductImage() as $i => $productImage) {
  645.             $imageHistory = new PiaHisProductImage();
  646.             $imageHistory->setHisId($historyId);
  647.             $imageHistory->setProductId($product->getId());
  648.             $imageHistory->setFileName($productImage->getFileName());
  649.             $imageHistory->setRank($productImage->getSortNo() ?: ($i 1));
  650.             $this->entityManager->persist($imageHistory);
  651.         }
  652.     }
  653.     private function saveProductStockHistory(int $historyIdProduct $product)
  654.     {
  655.         foreach ($product->getProductClasses() as $pc) {
  656.             $stockHistory = new PiaHisProductStock();
  657.             $stockHistory->setHisId($historyId);
  658.             $stockHistory->setProductClassId($pc->getId());
  659.             $stockHistory->setStock($pc->isStockUnlimited() ? null $pc->getStock());
  660.             $this->entityManager->persist($stockHistory);
  661.         }
  662.     }
  663.     private function saveProductTagHistory(int $historyIdProduct $product)
  664.     {
  665.         foreach ($product->getProductTag() as $productTag) {
  666.             $tagHistory = new PiaHisProductTag();
  667.             $tagHistory->setHisId($historyId);
  668.             $tagHistory->setProductId($product->getId());
  669.             $tagHistory->setTag($productTag->getTag()->getId());
  670.             $this->entityManager->persist($tagHistory);
  671.         }
  672.     }
  673.     /** 会員別価格履歴を保存(CustomerRank42) */
  674.     private function saveRankPriceHistory(int $historyIdProduct $product)
  675.     {
  676.         if (!class_exists('Plugin\CustomerRank42\Entity\CustomerPrice')) {
  677.             return;
  678.         }
  679.         try {
  680.             $repo $this->entityManager->getRepository('Plugin\CustomerRank42\Entity\CustomerPrice');
  681.             foreach ($product->getProductClasses() as $pc) {
  682.                 $prices $repo->findBy(['ProductClass' => $pc]);
  683.                 foreach ($prices as $cp) {
  684.                     $rankHistory = new PiaHisRankPrice();
  685.                     $rankHistory->setHisId($historyId);
  686.                     $rankHistory->setCustomerRankId($cp->getCustomerRank()->getId());
  687.                     $rankHistory->setProductClassId($pc->getId());
  688.                     $rankHistory->setPrice($cp->getPrice());
  689.                     $this->entityManager->persist($rankHistory);
  690.                 }
  691.             }
  692.         } catch (\Exception $e) {
  693.             error_log('PiaHistory: 会員別価格履歴保存エラー - ' $e->getMessage());
  694.         }
  695.     }
  696.     /** 商品追加項目履歴を保存(ProductPlus42) */
  697.     private function saveProductAddHistory(int $historyIdProduct $product)
  698.     {
  699.         if (!class_exists('Plugin\ProductPlus42\Entity\ProductData')) {
  700.             return;
  701.         }
  702.         try {
  703.             $repo $this->entityManager->getRepository('Plugin\ProductPlus42\Entity\ProductData');
  704.             $productDatas $repo->findBy(['Product' => $product]);
  705.             foreach ($productDatas as $pd) {
  706.                 $item $pd->getProductItem();
  707.                 if (!$item) {
  708.                     continue;
  709.                 }
  710.                 $addHistory = new PiaHisProductAdd();
  711.                 $addHistory->setHisId($historyId);
  712.                 $addHistory->setProductId($product->getId());
  713.                 $addHistory->setColumnId($item->getId());
  714.                 $addHistory->setValue($pd->getDataValue());
  715.                 $this->entityManager->persist($addHistory);
  716.             }
  717.         } catch (\Exception $e) {
  718.             error_log('PiaHistory: 商品追加項目履歴保存エラー - ' $e->getMessage());
  719.         }
  720.     }
  721.     /** 目安価格履歴を保存(PiaMeyasu) */
  722.     private function saveMeyasuHistory(int $historyIdProduct $product)
  723.     {
  724.         try {
  725.             if (!class_exists('Plugin\PiaMeyasu\Entity\PiaMeyasu')) {
  726.                 return;
  727.             }
  728.             $meyasuRepo $this->entityManager->getRepository('Plugin\PiaMeyasu\Entity\PiaMeyasu');
  729.             $meyasuData $meyasuRepo->findOneBy(['product_id' => $product->getId()]);
  730.             if ($meyasuData) {
  731.                 $meyasuHistory = new PiaHisMeyasu();
  732.                 $meyasuHistory->setHisId($historyId);
  733.                 $meyasuHistory->setProductId($product->getId());
  734.                 $meyasuHistory->setCoinCost($meyasuData->getCoinCost());
  735.                 $meyasuHistory->setExTypeId($meyasuData->getExTypeId());
  736.                 $meyasuHistory->setCommission($meyasuData->getCommission());
  737.                 $meyasuHistory->setEsDispPrice($meyasuData->getEsDispPrice());
  738.                 $this->entityManager->persist($meyasuHistory);
  739.             }
  740.         } catch (\Exception $e) {
  741.             error_log('PiaHistory: 目安価格履歴保存エラー - ' $e->getMessage());
  742.         }
  743.     }
  744.     // -------------------------------------------------------------------------
  745.     // ユーティリティ
  746.     // -------------------------------------------------------------------------
  747.     /** ログイン管理者情報を取得 */
  748.     private function getAdminInfo(): array
  749.     {
  750.         $adminId         1;
  751.         $adminName       '管理者';
  752.         $adminDepartment '';
  753.         if ($this->tokenStorage->getToken() && $this->tokenStorage->getToken()->getUser()) {
  754.             $admin   $this->tokenStorage->getToken()->getUser();
  755.             $adminId $admin->getId();
  756.             $member $this->memberRepository->find($adminId);
  757.             if ($member) {
  758.                 $adminName $member->getName();
  759.                 if (method_exists($member'getDepartment')) {
  760.                     $adminDepartment $member->getDepartment() ?: '';
  761.                 }
  762.             }
  763.         }
  764.         return [$adminId$adminName$adminDepartment];
  765.     }
  766.     /** PiaProductDispプラグインから表示開始日を取得 */
  767.     private function getDisplayStartDate(Product $product): ?string
  768.     {
  769.         try {
  770.             if (class_exists('Plugin\PiaProductDisp\Entity\ProductDisplayPeriod')) {
  771.                 $repo $this->entityManager->getRepository('Plugin\PiaProductDisp\Entity\ProductDisplayPeriod');
  772.                 $dp   $repo->findOneBy(['Product' => $product]);
  773.                 if ($dp && $dp->getDisplayStartDate()) {
  774.                     return $dp->getDisplayStartDate()->format('Y-m-d H:i:s');
  775.                 }
  776.             }
  777.         } catch (\Exception $e) {
  778.             error_log('PiaHistory: 表示開始日取得エラー - ' $e->getMessage());
  779.         }
  780.         return null;
  781.     }
  782.     /** PiaProductDispプラグインから表示終了日を取得 */
  783.     private function getDisplayEndDate(Product $product): ?string
  784.     {
  785.         try {
  786.             if (class_exists('Plugin\PiaProductDisp\Entity\ProductDisplayPeriod')) {
  787.                 $repo $this->entityManager->getRepository('Plugin\PiaProductDisp\Entity\ProductDisplayPeriod');
  788.                 $dp   $repo->findOneBy(['Product' => $product]);
  789.                 if ($dp && $dp->getDisplayEndDate()) {
  790.                     return $dp->getDisplayEndDate()->format('Y-m-d H:i:s');
  791.                 }
  792.             }
  793.         } catch (\Exception $e) {
  794.             error_log('PiaHistory: 表示終了日取得エラー - ' $e->getMessage());
  795.         }
  796.         return null;
  797.     }
  798. }