app/Customize/Controller/CustomizePhotoProductController.php line 133

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of EC-CUBE
  4.  *
  5.  * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
  6.  *
  7.  * http://www.ec-cube.co.jp/
  8.  *
  9.  * For the full copyright and license information, please view the LICENSE
  10.  * file that was distributed with this source code.
  11.  */
  12. namespace Customize\Controller;
  13. use Eccube\Entity\BaseInfo;
  14. use Eccube\Entity\Master\ProductStatus;
  15. use Eccube\Entity\Product;
  16. use Eccube\Event\EccubeEvents;
  17. use Eccube\Event\EventArgs;
  18. use Eccube\Form\Type\AddCartType;
  19. use Eccube\Form\Type\SearchProductType;
  20. use Eccube\Repository\BaseInfoRepository;
  21. use Eccube\Repository\CustomerFavoriteProductRepository;
  22. use Eccube\Repository\Master\ProductListMaxRepository;
  23. use Eccube\Repository\ProductRepository;
  24. use Eccube\Service\CartService;
  25. use Eccube\Service\PurchaseFlow\PurchaseContext;
  26. use Eccube\Service\PurchaseFlow\PurchaseFlow;
  27. use Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination;
  28. use Knp\Component\Pager\PaginatorInterface;
  29. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  30. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  31. use Symfony\Component\HttpFoundation\Request;
  32. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  33. use Symfony\Component\Routing\Annotation\Route;
  34. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  35. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  36. use Eccube\Controller\ProductController as BaseController;
  37. use Customize\Repository\CustomizeProductRepository;
  38. use Plugin\RecentlyViewedProducts\Service\RecentlyViewedProductService;
  39. use Eccube\Entity\Master\Pref;
  40. use Doctrine\ORM\EntityManagerInterface;
  41. use Customize\Repository\CustomizeStripeRecOrderRepository;
  42. class CustomizePhotoProductController extends BaseController
  43. {
  44.     /**
  45.      * @var PurchaseFlow
  46.      */
  47.     protected $purchaseFlow;
  48.     /**
  49.      * @var CustomerFavoriteProductRepository
  50.      */
  51.     protected $customerFavoriteProductRepository;
  52.     /**
  53.      * @var CartService
  54.      */
  55.     protected $cartService;
  56.     /**
  57.      * @var ProductRepository
  58.      */
  59.     protected $productRepository;
  60.     /**
  61.      * @var BaseInfo
  62.      */
  63.     protected $BaseInfo;
  64.     /**
  65.      * @var AuthenticationUtils
  66.      */
  67.     protected $helper;
  68.     /**
  69.      * @var ProductListMaxRepository
  70.      */
  71.     protected $productListMaxRepository;
  72.     private $title '';
  73.     /**
  74.      * @var RecentlyViewedProductService
  75.      */
  76.     protected $recentlyViewedProductService;
  77.     protected CustomizeStripeRecOrderRepository $customizeStripeRecOrderRepository;
  78.     /**
  79.      * ProductController constructor.
  80.      *
  81.      * @param PurchaseFlow $cartPurchaseFlow
  82.      * @param CustomerFavoriteProductRepository $customerFavoriteProductRepository
  83.      * @param CartService $cartService
  84.      * @param ProductRepository $productRepository
  85.      * @param BaseInfoRepository $baseInfoRepository
  86.      * @param AuthenticationUtils $helper
  87.      * @param ProductListMaxRepository $productListMaxRepository
  88.      */
  89.     public function __construct(
  90.         PurchaseFlow $cartPurchaseFlow,
  91.         CustomerFavoriteProductRepository $customerFavoriteProductRepository,
  92.         CartService $cartService,
  93.         ProductRepository $productRepository,
  94.         BaseInfoRepository $baseInfoRepository,
  95.         AuthenticationUtils $helper,
  96.         ProductListMaxRepository $productListMaxRepository,
  97.         RecentlyViewedProductService $recentlyViewedProductService,
  98.         CustomizeStripeRecOrderRepository $customizeStripeRecOrderRepository
  99.     ) {
  100.         $this->purchaseFlow $cartPurchaseFlow;
  101.         $this->customerFavoriteProductRepository $customerFavoriteProductRepository;
  102.         $this->cartService $cartService;
  103.         $this->productRepository $productRepository;
  104.         $this->BaseInfo $baseInfoRepository->get();
  105.         $this->helper $helper;
  106.         $this->productListMaxRepository $productListMaxRepository;
  107.         $this->recentlyViewedProductService $recentlyViewedProductService;
  108.         $this->customizeStripeRecOrderRepository $customizeStripeRecOrderRepository;
  109.     }
  110.     /**
  111.      * 商品一覧画面.
  112.      *
  113.      * @Route("/photo-products/list", name="photo-product_list", methods={"GET"})
  114.      * @Template("@user_data/photo-product_list.twig")
  115.      */
  116.     public function index(Request $requestPaginatorInterface $paginator)
  117.     {
  118.         // Doctrine SQLFilter
  119.         if ($this->BaseInfo->isOptionNostockHidden()) {
  120.             $this->entityManager->getFilters()->enable('option_nostock_hidden');
  121.         }
  122.         // handleRequestは空のqueryの場合は無視するため
  123.         if ($request->getMethod() === 'GET') {
  124.             $request->query->set('pageno'$request->query->get('pageno'''));
  125.         }
  126.         // searchForm
  127.         /* @var $builder \Symfony\Component\Form\FormBuilderInterface */
  128.         $builder $this->formFactory->createNamedBuilder(''SearchProductType::class);
  129.         if ($request->getMethod() === 'GET') {
  130.             $builder->setMethod('GET');
  131.         }
  132.         $event = new EventArgs(
  133.             [
  134.                 'builder' => $builder,
  135.             ],
  136.             $request
  137.         );
  138.         $this->eventDispatcher->dispatch($eventEccubeEvents::FRONT_PRODUCT_INDEX_INITIALIZE);
  139.         /* @var $searchForm \Symfony\Component\Form\FormInterface */
  140.         $searchForm $builder->getForm();
  141.         $searchForm->handleRequest($request);
  142.         //ここからカスタマイズ
  143.         // 撮影アングル(チェックボックスは配列なので all() で)
  144.         $selectedAngles $request->query->all('camera_angles');
  145.         if (empty($selectedAngles)) {
  146.             // 互換(もし過去に vehicle_types[] だった場合)
  147.             $selectedAngles $request->query->all('vehicle_types');
  148.         }
  149.         $selectedAngles array_values(array_filter(array_map('trim'$selectedAngles), fn($v) => $v !== ''));
  150.         // カラー種別
  151.         $selectedColors $request->query->all('color_types');
  152.         $selectedColors array_values(array_filter(array_map('trim'$selectedColors), fn($v) => $v !== ''));
  153.         //ここまでカスタマイズ
  154.         // paginator
  155.         $searchData $searchForm->getData();
  156.         $qb $this->productRepository->getQueryBuilderBySearchData($searchData);
  157.         //ここからカスタマイズ
  158.         //paginationに、SaleType.idが2(画像商品)しか渡さないようにする
  159.         $sub $this->entityManager->createQueryBuilder()
  160.             ->select('MIN(pc2.id)')
  161.             ->from(\Eccube\Entity\ProductClass::class, 'pc2')
  162.             ->where('pc2.Product = p')
  163.             ->getDQL();
  164.         $qb
  165.             ->innerJoin('p.ProductClasses''pc3')
  166.             ->innerJoin('pc3.SaleType''st')
  167.             ->andWhere($qb->expr()->eq('pc3.id''(' $sub ')'))
  168.             ->andWhere('st.id = 2');
  169.         // ─ 撮影アングル(複数チェック → OR LIKE) ─
  170.         if (!empty($selectedAngles)) {
  171.             $orX $qb->expr()->orX();
  172.             foreach ($selectedAngles as $i => $val) {
  173.                 $param "ang{$i}";
  174.                 $orX->add($qb->expr()->like('p.camera_angle'":{$param}"));
  175.                 $qb->setParameter($param'%'.$val.'%');
  176.             }
  177.             $qb->andWhere($orX);
  178.         }
  179.         // ─ カラー種別(参考:同じパターン) ─
  180.         if (!empty($selectedColors)) {
  181.             $orY $qb->expr()->orX();
  182.             foreach ($selectedColors as $i => $val) {
  183.                 $param "col{$i}";
  184.                 $orY->add($qb->expr()->like('p.color_type'":{$param}"));
  185.                 $qb->setParameter($param'%'.$val.'%');
  186.             }
  187.             $qb->andWhere($orY);
  188.         }
  189.         //ここまでカスタマイズ
  190.         $event = new EventArgs(
  191.             [
  192.                 'searchData' => $searchData,
  193.                 'qb' => $qb,
  194.             ],
  195.             $request
  196.         );
  197.         $this->eventDispatcher->dispatch($eventEccubeEvents::FRONT_PRODUCT_INDEX_SEARCH);
  198.         $searchData $event->getArgument('searchData');
  199.         $query $qb->getQuery()
  200.             ->useResultCache(true$this->eccubeConfig['eccube_result_cache_lifetime_short']);
  201.         /** @var SlidingPagination $pagination */
  202.         $pagination $paginator->paginate(
  203.             $query,
  204.             !empty($searchData['pageno']) ? $searchData['pageno'] : 1,
  205.             !empty($searchData['disp_number']) ? $searchData['disp_number']->getId() : $this->productListMaxRepository->findOneBy([], ['sort_no' => 'ASC'])->getId()
  206.         );
  207.         $ids = [];
  208.         foreach ($pagination as $Product) {
  209.             $ids[] = $Product->getId();
  210.         }
  211.         $ProductsAndClassCategories $this->productRepository->findProductsWithSortedClassCategories($ids'p.id');
  212.         // addCart form
  213.         $forms = [];
  214.         foreach ($pagination as $Product) {
  215.             /* @var $builder \Symfony\Component\Form\FormBuilderInterface */
  216.             $builder $this->formFactory->createNamedBuilder(
  217.                 '',
  218.                 AddCartType::class,
  219.                 null,
  220.                 [
  221.                     'product' => $ProductsAndClassCategories[$Product->getId()],
  222.                     'allow_extra_fields' => true,
  223.                 ]
  224.             );
  225.             $addCartForm $builder->getForm();
  226.             $forms[$Product->getId()] = $addCartForm->createView();
  227.         }
  228.         $Category $searchForm->get('category_id')->getData();
  229.         //カスタマイズ開始
  230.         //category_idを元に、検索欄を動的に変更
  231.         //operator(運行会社)
  232.         $operators = [];
  233.         if ($Category) {
  234.                 // 該当カテゴリを持つ商品の ProductClass を取得
  235.                 $qbOperator $this->entityManager->createQueryBuilder()
  236.                         ->select('DISTINCT p.operator')  // 重複しない operator を抽出
  237.                         ->from(\Eccube\Entity\ProductClass::class, 'pc')
  238.                         ->join('pc.Product''p')
  239.                         ->join('p.ProductCategories''pcat')
  240.                         ->where('pcat.Category = :category')
  241.                         ->setParameter('category'$Category);
  242.                 $operatorResults $qbOperator->getQuery()->getResult();
  243.                 foreach ($operatorResults as $row) {
  244.                         // operator は string の場合と null の場合があるので検査
  245.                         if (isset($row['operator']) && $row['operator'] !== '') {
  246.                                 $operators[] = $row['operator'];
  247.                         }
  248.                 }
  249.         }
  250.         //photographing_period(撮影年代)
  251.         $photographing_periods = [];
  252.         if ($Category) {
  253.                 // 該当カテゴリを持つ商品の ProductClass を取得
  254.                 $qbPhotographingPeriod $this->entityManager->createQueryBuilder()
  255.                         ->select('DISTINCT p.photographing_period')  // 重複しない photographing_period を抽出
  256.                         ->from(\Eccube\Entity\ProductClass::class, 'pc')
  257.                         ->join('pc.Product''p')
  258.                         ->join('p.ProductCategories''pcat')
  259.                         ->where('pcat.Category = :category')
  260.                         ->setParameter('category'$Category);
  261.                 $photographingPeriodrResults $qbPhotographingPeriod->getQuery()->getResult();
  262.                 foreach ($photographingPeriodrResults as $row) {
  263.                         // photographing_period は string の場合と null の場合があるので検査
  264.                         if (isset($row['photographing_period']) && $row['photographing_period'] !== '') {
  265.                                 $photographing_periods[] = $row['photographing_period'];
  266.                         }
  267.                 }
  268.         }
  269.         //photographing_period(撮影年代)
  270.         $photographing_periods = [];
  271.         if ($Category) {
  272.                 // 該当カテゴリを持つ商品の ProductClass を取得
  273.                 $qbPhotographingPeriod $this->entityManager->createQueryBuilder()
  274.                         ->select('DISTINCT p.photographing_period')  // 重複しない operator を抽出
  275.                         ->from(\Eccube\Entity\ProductClass::class, 'pc')
  276.                         ->join('pc.Product''p')
  277.                         ->join('p.ProductCategories''pcat')
  278.                         ->where('pcat.Category = :category')
  279.                         ->setParameter('category'$Category);
  280.                 $photographingPeriodrResults $qbPhotographingPeriod->getQuery()->getResult();
  281.                 foreach ($photographingPeriodrResults as $row) {
  282.                         // photographing_period は string の場合と null の場合があるので検査
  283.                         if (isset($row['photographing_period']) && $row['photographing_period'] !== '') {
  284.                                 $photographing_periods[] = $row['photographing_period'];
  285.                         }
  286.                 }
  287.         }
  288.         //location(撮影地)
  289.         $locations = [];
  290.         if ($Category) {
  291.                 // 該当カテゴリを持つ商品の ProductClass を取得
  292.                 $qbVehicleType $this->entityManager->createQueryBuilder()
  293.                         ->select('DISTINCT p.location')  // 重複しない location を抽出
  294.                         ->from(\Eccube\Entity\ProductClass::class, 'pc')
  295.                         ->join('pc.Product''p')
  296.                         ->join('p.ProductCategories''pcat')
  297.                         ->where('pcat.Category = :category')
  298.                         ->setParameter('category'$Category);
  299.                 $vehicleTypeResults $qbVehicleType->getQuery()->getResult();
  300.                 foreach ($vehicleTypeResults as $row) {
  301.                         // location は string の場合と null の場合があるので検査
  302.                         if (isset($row['location']) && $row['location'] !== '') {
  303.                                 $locations[] = $row['location'];
  304.                         }
  305.                 }
  306.         }
  307.         //weather(撮影天候)
  308.         $weathers = [];
  309.         if ($Category) {
  310.                 // 該当カテゴリを持つ商品の ProductClass を取得
  311.                 $qbTrainType $this->entityManager->createQueryBuilder()
  312.                         ->select('DISTINCT p.weather')  // 重複しない weather を抽出
  313.                         ->from(\Eccube\Entity\ProductClass::class, 'pc')
  314.                         ->join('pc.Product''p')
  315.                         ->join('p.ProductCategories''pcat')
  316.                         ->where('pcat.Category = :category')
  317.                         ->setParameter('category'$Category);
  318.                 $trainTypeResults $qbTrainType->getQuery()->getResult();
  319.                 foreach ($trainTypeResults as $row) {
  320.                         // weather は string の場合と null の場合があるので検査
  321.                         if (isset($row['weather']) && $row['weather'] !== '') {
  322.                                 $weathers[] = $row['weather'];
  323.                         }
  324.                 }
  325.         }
  326.         //camera_angle(撮影アングル)
  327.         $camera_angles = [];
  328.         if ($Category) {
  329.                 // 該当カテゴリを持つ商品の ProductClass を取得
  330.                 $qbAngleCand $this->entityManager->createQueryBuilder()
  331.                         ->select('DISTINCT p.camera_angle AS angle')  // 重複しない camera_angle を抽出
  332.                         ->from(\Eccube\Entity\ProductClass::class, 'pc')
  333.                         ->join('pc.Product''p')
  334.                         ->join('p.ProductCategories''pcat')
  335.                         ->where('pcat.Category = :category')
  336.                         ->setParameter('category'$Category);
  337.                 $rows $qbAngleCand->getQuery()->getResult();
  338.                 $tmp = [];
  339.                 foreach ($rows as $r) {
  340.                         $raw $r['angle'] ?? null;
  341.                         if (!is_string($raw) || $raw === '') continue;
  342.                         // 1レコードに複数語("正面, 俯瞰" など)が入る可能性に対応
  343.                         foreach (preg_split('/[,\s、]+/u'$raw, -1PREG_SPLIT_NO_EMPTY) as $name) {
  344.                                 $name trim($name);
  345.                                 if ($name === '') continue;
  346.                                 $tmp[] = $name;
  347.                         }
  348.                 }
  349.                 // 重複排除
  350.                 $camera_angles array_values(array_unique($tmp));
  351.                 // (任意)表示順を固定したい場合
  352.                 $order = ['右頭','左頭','正面','側面','俯瞰'];
  353.                 $pos array_flip($order);
  354.                 usort($camera_angles, fn($a,$b)=>($pos[$a]??PHP_INT_MAX)<=>($pos[$b]??PHP_INT_MAX) ?: strcmp($a,$b));
  355.         }
  356.         //color_type(撮影方式)
  357.         $color_types = [];
  358.         if ($Category) {
  359.                 // 該当カテゴリを持つ商品の ProductClass を取得
  360.                 $qbColorType $this->entityManager->createQueryBuilder()
  361.                         ->select('DISTINCT p.color_type')  // 重複しない color_type を抽出
  362.                         ->from(\Eccube\Entity\ProductClass::class, 'pc')
  363.                         ->join('pc.Product''p')
  364.                         ->join('p.ProductCategories''pcat')
  365.                         ->where('pcat.Category = :category')
  366.                         ->setParameter('category'$Category);
  367.                 $colorTypeResults $qbColorType->getQuery()->getResult();
  368.                 foreach ($colorTypeResults as $row) {
  369.                         // color_type は string の場合と null の場合があるので検査
  370.                         if (isset($row['color_type']) && $row['color_type'] !== '') {
  371.                                 $color_types[] = $row['color_type'];
  372.                         }
  373.                 }
  374.         }
  375.         //feature(一押し)
  376.         $features = [];
  377.         if ($Category) {
  378.                 // 該当カテゴリを持つ商品の ProductClass を取得
  379.                 $qbTrainType $this->entityManager->createQueryBuilder()
  380.                         ->select('DISTINCT p.feature')  // 重複しない feature を抽出
  381.                         ->from(\Eccube\Entity\ProductClass::class, 'pc')
  382.                         ->join('pc.Product''p')
  383.                         ->join('p.ProductCategories''pcat')
  384.                         ->where('pcat.Category = :category')
  385.                         ->setParameter('category'$Category);
  386.                 $trainTypeResults $qbTrainType->getQuery()->getResult();
  387.                 foreach ($trainTypeResults as $row) {
  388.                         // feature は string の場合と null の場合があるので検査
  389.                         if (isset($row['feature']) && $row['feature'] !== '') {
  390.                                 $features[] = $row['feature'];
  391.                         }
  392.                 }
  393.         }
  394.         //prefectures(撮影地(都道府県))
  395.         $prefectures = [];
  396.         if ($Category) {
  397.                 // 該当カテゴリを持つ商品の ProductClass を取得
  398.                 $qbPref $this->entityManager->createQueryBuilder()
  399.                         ->select('DISTINCT p.prefectures')  // 重複しない prefectures を抽出
  400.                         ->from(\Eccube\Entity\ProductClass::class, 'pc')
  401.                         ->join('pc.Product''p')
  402.                         ->join('p.ProductCategories''pcat')
  403.                         ->where('pcat.Category = :category')
  404.                         ->setParameter('category'$Category);
  405.                 $prefResults $qbPref->getQuery()->getResult();
  406.                 $existingSet = []; // 集合(キーのみ使用)
  407.                 foreach ($prefResults as $row) {
  408.                         // prefectures は string の場合と null の場合があるので検査
  409.                         $raw $row['prefectures'] ?? null;
  410.                         if (!is_string($raw) || $raw === '') {
  411.                                 continue;
  412.                         }
  413.                         // 全角スペース→半角、前後空白除去
  414.                         $raw trim(mb_convert_kana($raw's')); // s: スペースを半角に
  415.                         // 区切りで分割(, / スペース / 全角読点)
  416.                         $parts preg_split('/[,\s、]+/u'$raw, -1PREG_SPLIT_NO_EMPTY);
  417.                         foreach ($parts as $name) {
  418.                                 $name trim($name);
  419.                                 if ($name === '') continue;
  420.                                 // 表記ゆれ補正
  421.                                 if (isset($aliasMap[$name])) {
  422.                                         $name $aliasMap[$name];
  423.                                 }
  424.                                 $existingSet[$name] = true// 集合化(重複排除)
  425.                         }
  426.                 }
  427.                 // 3) JIS順マスター(この順序を“常に”使う)
  428.                 $jisPrefNames = [
  429.                         '北海道','青森県','岩手県','宮城県','秋田県','山形県','福島県',
  430.                         '茨城県','栃木県','群馬県','埼玉県','千葉県','東京都','神奈川県',
  431.                         '新潟県','富山県','石川県','福井県','山梨県','長野県',
  432.                         '岐阜県','静岡県','愛知県','三重県','滋賀県','京都府','大阪府','兵庫県','奈良県','和歌山県',
  433.                         '鳥取県','島根県','岡山県','広島県','山口県',
  434.                         '徳島県','香川県','愛媛県','高知県',
  435.                         '福岡県','佐賀県','長崎県','熊本県','大分県','宮崎県','鹿児島県','沖縄県',
  436.                 ];
  437.                 // 4) マスター順で「存在する県だけ」を抽出 → これが最終配列
  438.                 $prefectures = [];
  439.                 foreach ($jisPrefNames as $name) {
  440.                         if (isset($existingSet[$name])) {
  441.                                 $prefectures[] = $name;
  442.                         }
  443.                 }
  444.         }
  445.         //カスタマイズ終了
  446.         return [
  447.             'subtitle' => $this->getPageTitle($searchData),
  448.             'pagination' => $pagination,
  449.             'search_form' => $searchForm->createView(),
  450.             'forms' => $forms,
  451.             'Category' => $Category,
  452.             'operators' => $operators,
  453.             'photographing_periods' => $photographing_periods,
  454.             'locations' => $locations,
  455.             'weathers' => $weathers,
  456.             'camera_angles' => $camera_angles,
  457.             'color_types' => $color_types,
  458.             'features' => $features,
  459.             'prefectures' => $prefectures,
  460.         ];
  461.     }
  462.     //カスタマイズ開始
  463.     // 関連商品を取得する共通関数
  464.     public function getRelatedProducts(array $Products, array $currentKeywordsint $limit 10)
  465.     {
  466.         $productScores = [];
  467.         foreach ($Products as $product) {
  468.             $keywords = [];
  469.             // operator
  470.             if (!empty($product->getOperator())) {
  471.                 $keywords[] = trim($product->getOperator());
  472.             }
  473.             // route_name
  474.             if (!empty($product->getRouteName())) {
  475.                 $keywords[] = trim($product->getRouteName());
  476.             }
  477.             // location
  478.             if (!empty($product->getLocation())) {
  479.                 $keywords[] = trim($product->getLocation());
  480.             }
  481.             // weather
  482.             if (!empty($product->getWeather())) {
  483.                 $keywords[] = trim($product->getWeather());
  484.             }
  485.             // camera_angle
  486.             if (!empty($product->getCameraAngle())) {
  487.                 $keywords[] = trim($product->getCameraAngle());
  488.             }
  489.             // feature
  490.             if (!empty($product->getFeature())) {
  491.                 $keywords[] = trim($product->getFeature());
  492.             }
  493.             // prefectures
  494.             if (!empty($product->getPrefectures())) {
  495.                 $keywords[] = trim($product->getPrefectures());
  496.             }
  497.             // name
  498.             if (!empty($product->getName())) {
  499.                 $keywords[] = trim($product->getName());
  500.             }
  501.             // search_word(カンマ区切り → 各単語を 1 倍で参照)
  502.             if ($product->getSearchWord()) {
  503.                 $words array_map('trim'explode(','$product->getSearchWord()));
  504.                 $keywords array_merge($keywords$words);
  505.             }
  506.             // category_name
  507.             foreach ($product->getProductCategories() as $ProductCategory) {
  508.                 $category $ProductCategory->getCategory();
  509.                if ($category && $category->getName()) {
  510.                     $keywords[] = trim($category->getName());
  511.                 }
  512.             }
  513.             $keywords array_unique($keywords);
  514.             // スコア計算(重み付き)
  515.             $score 0;
  516.             foreach ($currentKeywords as $keyword) {
  517.                 if (in_array($keyword$keywordstrue)) {
  518.                     // 重み判定
  519.                     if ($keyword === trim($product->getOperator())) {
  520.                         $score += 10;
  521.                     } elseif ($keyword === trim($product->getRouteName())) {
  522.                         $score += 10;
  523.                     } elseif ($keyword === trim($product->getName())) {
  524.                         $score += 10;
  525.                     } else {
  526.                         // search_word, location, related_station, category_name → +1
  527.                         $score += 1;
  528.                     }
  529.                 }
  530.             }
  531.             $productScores[] = [
  532.                 'product' => $product,
  533.                 'score' => $score,
  534.             ];
  535.         }
  536.         usort($productScores, function($a$b) {
  537.             return $b['score'] <=> $a['score'];
  538.         });
  539.         return array_map(function($item) {
  540.             return $item['product'];
  541.         }, array_slice($productScores0$limit));
  542.     }
  543.     //カスタマイズ終了
  544.     /**
  545.      * 商品詳細画面.
  546.      *
  547.      * @Route("/photo-products/detail/{id}", name="photo-product_detail", methods={"GET"}, requirements={"id" = "\d+"})
  548.      * @Template("@user_data/photo-detail.twig")
  549.      * @ParamConverter("Product", options={"repository_method" = "findWithSortedClassCategories"})
  550.      *
  551.      * @param Request $request
  552.      * @param Product $Product
  553.      *
  554.      * @return array
  555.      */
  556.     public function detail(Request $requestProduct $Product)
  557.     {
  558.         $Customer $this->getUser();
  559.         if (!$this->checkVisibility($Product)) {
  560.             throw new NotFoundHttpException();
  561.         }
  562.         // Stripe 定期購入申し込み履歴取得
  563.         if(!$Customer){
  564.             return $this->redirectToRoute('subscription_detail', [
  565.                 'id' => 2004,
  566.             ]);
  567.         }else{
  568.             $qb_rec $this->customizeStripeRecOrderRepository->getRecOrdersQueryBuilderByCustomer($Customer);
  569.             $qb_rec
  570.                 ->leftJoin('ro.OrderItems''roi')
  571.                 ->addSelect('roi');
  572.             // qb実行(サブスクステータス)
  573.             $stripe_rec_orders $qb_rec->getQuery()->getResult();
  574.             // ① 空なら return
  575.             if (empty($stripe_rec_orders)) {
  576.                 return $this->redirectToRoute('subscription_detail', [
  577.                     'id' => 2004,
  578.                 ]);
  579.             }
  580.             // ② 最初の要素を取得
  581.             $firstRecOrder reset($stripe_rec_orders);
  582.             // ③ RecStatus 判定
  583.             if (in_array($firstRecOrder->getRecStatus(), ['canceled''scheduled_canceled'], true) && $Customer->getPoint() == 0) {
  584.                 return $this->redirectToRoute('subscription_detail', [
  585.                     'id' => 2004,
  586.                 ]);
  587.             }
  588.         }
  589.         $builder $this->formFactory->createNamedBuilder(
  590.             '',
  591.             AddCartType::class,
  592.             null,
  593.             [
  594.                 'product' => $Product,
  595.                 'id_add_product_id' => false,
  596.             ]
  597.         );
  598.         $event = new EventArgs(
  599.             [
  600.                 'builder' => $builder,
  601.                 'Product' => $Product,
  602.             ],
  603.             $request
  604.         );
  605.         $this->eventDispatcher->dispatch($eventEccubeEvents::FRONT_PRODUCT_DETAIL_INITIALIZE);
  606.         $is_favorite false;
  607.         if ($this->isGranted('ROLE_USER')) {
  608.             $Customer $this->getUser();
  609.             $is_favorite $this->customerFavoriteProductRepository->isFavorite($Customer$Product);
  610.         }
  611.         //あなたへのおすすめ機能実装
  612.         $productRepository $this->getDoctrine()->getRepository(\Eccube\Entity\Product::class);
  613.         // ProductにTagもJOINして取得する
  614.         $qb $productRepository->createQueryBuilder('p')
  615.             ->leftJoin('p.ProductTag''pt')
  616.             ->leftJoin('pt.Tag''t')
  617.             ->leftJoin('p.ProductCategories''pc')
  618.             ->leftJoin('pc.Category''c')         
  619.             ->addSelect('pt')
  620.             ->addSelect('t')
  621.             ->addSelect('pc')                        
  622.             ->addSelect('c')                         
  623.             ->where('p.Status = 1')
  624.             ->orderBy('p.id''DESC');
  625.         $Products $qb->getQuery()->getResult();
  626.     //カスタマイズ開始
  627.     // 関連書籍の取得
  628.     $rqb $productRepository->createQueryBuilder('p')
  629.         ->innerJoin('p.ProductClasses''pc')
  630.         ->innerJoin('p.ProductCategories''pcat')
  631.         ->innerJoin('pcat.Category''c')
  632.         ->where('p.Status = :status')
  633.         ->andWhere('pc.SaleType = :saleType')
  634.         ->andWhere('pc.stock > 0')
  635.         ->setParameter('status'1)
  636.         ->setParameter('saleType'1);
  637.     $BookProducts $rqb->getQuery()->getResult();
  638.     // 関連写真の取得
  639.     $rqp $productRepository->createQueryBuilder('p')
  640.         ->innerJoin('p.ProductClasses''pc')
  641.         ->innerJoin('p.ProductCategories''pcat')
  642.         ->innerJoin('pcat.Category''c')
  643.         ->where('p.Status = :status')
  644.         ->andWhere('pc.SaleType = :saleType')
  645.         ->setParameter('status'1)
  646.         ->setParameter('saleType'2);
  647.     $PhotoProducts $rqp->getQuery()->getResult();
  648.     // 現在の商品から特徴語を作成
  649.     $currentKeywords = [];
  650.     foreach ([
  651.         $Product->getOperator(),
  652.         $Product->getRouteName(),
  653.         $Product->getLocation(),
  654.         $Product->getWeather(),
  655.         $Product->getCameraAngle(),
  656.         $Product->getFeature(),
  657.         $Product->getName(),
  658.     ] as $val) {
  659.         if (!empty($val)) {
  660.             $currentKeywords[] = trim($val);
  661.         }
  662.     }
  663.     if ($Product->getSearchWord()) {
  664.         $words array_map('trim'explode(','$Product->getSearchWord()));
  665.         $currentKeywords array_merge($currentKeywords$words);
  666.     }
  667.     foreach ($Product->getProductCategories() as $ProductCategory) {
  668.         $category $ProductCategory->getCategory();
  669.         if ($category && $category->getName()) {
  670.             $currentKeywords[] = trim($category->getName());
  671.         }
  672.     }
  673.     $currentKeywords array_unique($currentKeywords);
  674.     // 関数で relatedBooks, relatedPhotos を作成
  675.     $relatedBooks $this->getRelatedProducts($BookProducts$currentKeywords);
  676.     $relatedPhotos $this->getRelatedProducts($PhotoProducts$currentKeywords);
  677.     //カスタマイズ終了
  678.         //カスタマイズ開始(購入履歴があるかないかで購入ボタンの表記を変える)
  679. $purchaseDate null;
  680. if ($this->getUser()) {
  681.     $Customer $this->getUser();
  682.     $purchaseqb $this->entityManager->createQueryBuilder();
  683.     $purchaseqb->select('o')
  684.         ->from(\Eccube\Entity\Order::class, 'o')
  685.         ->leftJoin('o.OrderItems''oi')
  686.         ->leftJoin('oi.Product''p')
  687.         ->where('o.Customer = :Customer')
  688.         ->andWhere('p.id = :ProductId')
  689.         ->andWhere('o.OrderStatus NOT IN (:excludedStatus)')
  690.         ->orderBy('o.order_date''DESC')
  691.         ->setMaxResults(1)
  692.         ->setParameter('Customer'$Customer)
  693.         ->setParameter('ProductId'$Product->getId())
  694.         ->setParameter('excludedStatus', [7]);
  695.     $order $purchaseqb->getQuery()->getOneOrNullResult();
  696.     if ($order) {
  697.         $purchaseDate $order->getOrderDate();
  698.     }
  699. }
  700. //カスタマイズ終了
  701.         return [
  702.             'title' => $this->title,
  703.             'subtitle' => $Product->getName(),
  704.             'form' => $builder->getForm()->createView(),
  705.             'Product' => $Product,
  706.             'is_favorite' => $is_favorite,
  707.             'relatedBooks' => $relatedBooks,  //← 追加
  708.             'relatedPhotos' => $relatedPhotos,  //← 追加
  709.             'purchaseDate' => $purchaseDate,  //← 追加
  710.             'Customer' => $Customer,  //← 追加
  711.         ];
  712.     }
  713.     /**
  714.      * お気に入り追加.
  715.      *
  716.      * @Route("/stockphoto-products/add_favorite/{id}", name="stockphoto-product_add_favorite", requirements={"id" = "\d+"}, methods={"GET", "POST"})
  717.      */
  718.     public function addFavorite(Request $requestProduct $Product)
  719.     {
  720.         $this->checkVisibility($Product);
  721.         $event = new EventArgs(
  722.             [
  723.                 'Product' => $Product,
  724.             ],
  725.             $request
  726.         );
  727.         $this->eventDispatcher->dispatch($eventEccubeEvents::FRONT_PRODUCT_FAVORITE_ADD_INITIALIZE);
  728.         if ($this->isGranted('ROLE_USER')) {
  729.             $Customer $this->getUser();
  730.             $this->customerFavoriteProductRepository->addFavorite($Customer$Product);
  731.             $this->session->getFlashBag()->set('product_detail.just_added_favorite'$Product->getId());
  732.             $event = new EventArgs(
  733.                 [
  734.                     'Product' => $Product,
  735.                 ],
  736.                 $request
  737.             );
  738.             $this->eventDispatcher->dispatch($eventEccubeEvents::FRONT_PRODUCT_FAVORITE_ADD_COMPLETE);
  739.             return $this->redirectToRoute('photo-product_detail', ['id' => $Product->getId()]);
  740.         } else {
  741.             // 非会員の場合、ログイン画面を表示
  742.             //  ログイン後の画面遷移先を設定
  743.             $this->setLoginTargetPath($this->generateUrl('product_add_favorite', ['id' => $Product->getId()], UrlGeneratorInterface::ABSOLUTE_URL));
  744.             $this->session->getFlashBag()->set('eccube.add.favorite'true);
  745.             $event = new EventArgs(
  746.                 [
  747.                     'Product' => $Product,
  748.                 ],
  749.                 $request
  750.             );
  751.             $this->eventDispatcher->dispatch($eventEccubeEvents::FRONT_PRODUCT_FAVORITE_ADD_COMPLETE);
  752.             return $this->redirectToRoute('mypage_login');
  753.         }
  754.     }
  755.     /**
  756.      * カートに追加.
  757.      *
  758.      * @Route("/stockphoto-products/add_cart/{id}", name="stockphoto-product_add_cart", methods={"POST"}, requirements={"id" = "\d+"})
  759.      */
  760.     public function addCart(Request $requestProduct $Product)
  761.     {
  762.         // エラーメッセージの配列
  763.         $errorMessages = [];
  764.         if (!$this->checkVisibility($Product)) {
  765.             throw new NotFoundHttpException();
  766.         }
  767.         $builder $this->formFactory->createNamedBuilder(
  768.             '',
  769.             AddCartType::class,
  770.             null,
  771.             [
  772.                 'product' => $Product,
  773.                 'id_add_product_id' => false,
  774.             ]
  775.         );
  776.         $event = new EventArgs(
  777.             [
  778.                 'builder' => $builder,
  779.                 'Product' => $Product,
  780.             ],
  781.             $request
  782.         );
  783.         $this->eventDispatcher->dispatch($eventEccubeEvents::FRONT_PRODUCT_CART_ADD_INITIALIZE);
  784.         /* @var $form \Symfony\Component\Form\FormInterface */
  785.         $form $builder->getForm();
  786.         $form->handleRequest($request);
  787.         if (!$form->isValid()) {
  788.             throw new NotFoundHttpException();
  789.         }
  790.         $addCartData $form->getData();
  791.         log_info(
  792.             'カート追加処理開始',
  793.             [
  794.                 'product_id' => $Product->getId(),
  795.                 'product_class_id' => $addCartData['product_class_id'],
  796.                 'quantity' => $addCartData['quantity'],
  797.             ]
  798.         );
  799.         // カートへ追加
  800.         $this->cartService->addProduct($addCartData['product_class_id'], $addCartData['quantity']);
  801.         // 明細の正規化
  802.         $Carts $this->cartService->getCarts();
  803.         foreach ($Carts as $Cart) {
  804.             $result $this->purchaseFlow->validate($Cart, new PurchaseContext($Cart$this->getUser()));
  805.             // 復旧不可のエラーが発生した場合は追加した明細を削除.
  806.             if ($result->hasError()) {
  807.                 $this->cartService->removeProduct($addCartData['product_class_id']);
  808.                 foreach ($result->getErrors() as $error) {
  809.                     $errorMessages[] = $error->getMessage();
  810.                 }
  811.             }
  812.             foreach ($result->getWarning() as $warning) {
  813.                 $errorMessages[] = $warning->getMessage();
  814.             }
  815.         }
  816.         $this->cartService->save();
  817.         log_info(
  818.             'カート追加処理完了',
  819.             [
  820.                 'product_id' => $Product->getId(),
  821.                 'product_class_id' => $addCartData['product_class_id'],
  822.                 'quantity' => $addCartData['quantity'],
  823.             ]
  824.         );
  825.         $event = new EventArgs(
  826.             [
  827.                 'form' => $form,
  828.                 'Product' => $Product,
  829.             ],
  830.             $request
  831.         );
  832.         $this->eventDispatcher->dispatch($eventEccubeEvents::FRONT_PRODUCT_CART_ADD_COMPLETE);
  833.         if ($event->getResponse() !== null) {
  834.             return $event->getResponse();
  835.         }
  836.         if ($request->isXmlHttpRequest()) {
  837.             // ajaxでのリクエストの場合は結果をjson形式で返す。
  838.             // 初期化
  839.             $messages = [];
  840.             if (empty($errorMessages)) {
  841.                 // エラーが発生していない場合
  842.                 $done true;
  843.                 array_push($messagestrans('front.product.add_cart_complete'));
  844.             } else {
  845.                 // エラーが発生している場合
  846.                 $done false;
  847.                 $messages $errorMessages;
  848.             }
  849.             return $this->json(['done' => $done'messages' => $messages]);
  850.         } else {
  851.             // ajax以外でのリクエストの場合はカート画面へリダイレクト
  852.             foreach ($errorMessages as $errorMessage) {
  853.                 $this->addRequestError($errorMessage);
  854.             }
  855.             return $this->redirectToRoute('cart');
  856.         }
  857.     }
  858.     /**
  859.      * ページタイトルの設定
  860.      *
  861.      * @param  array|null $searchData
  862.      *
  863.      * @return str
  864.      */
  865.     protected function getPageTitle($searchData)
  866.     {
  867.         if (isset($searchData['name']) && !empty($searchData['name'])) {
  868.             return trans('front.product.search_result');
  869.         } elseif (isset($searchData['category_id']) && $searchData['category_id']) {
  870.             return $searchData['category_id']->getName();
  871.         } else {
  872.             return trans('front.product.all_products');
  873.         }
  874.     }
  875.     /**
  876.      * 閲覧可能な商品かどうかを判定
  877.      *
  878.      * @param Product $Product
  879.      *
  880.      * @return boolean 閲覧可能な場合はtrue
  881.      */
  882.     protected function checkVisibility(Product $Product)
  883.     {
  884.         $is_admin $this->session->has('_security_admin');
  885.         // 管理ユーザの場合はステータスやオプションにかかわらず閲覧可能.
  886.         if (!$is_admin) {
  887.             // 在庫なし商品の非表示オプションが有効な場合.
  888.             // if ($this->BaseInfo->isOptionNostockHidden()) {
  889.             //     if (!$Product->getStockFind()) {
  890.             //         return false;
  891.             //     }
  892.             // }
  893.             // 公開ステータスでない商品は表示しない.
  894.             if ($Product->getStatus()->getId() !== ProductStatus::DISPLAY_SHOW) {
  895.                 return false;
  896.             }
  897.         }
  898.         return true;
  899.     }
  900. }