app/Plugin/PiaProductReview/EventSubscriber/ProductDetailEventSubscriber.php line 86

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of PiaProductReview
  4.  *
  5.  * Copyright(c) Pia Staff. All Rights Reserved.
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Plugin\PiaProductReview\EventSubscriber;
  11. use Eccube\Event\TemplateEvent;
  12. use Plugin\PiaProductReview\Entity\ProductReview;
  13. use Plugin\PiaProductReview\Form\Type\ProductReviewType;
  14. use Plugin\PiaProductReview\Repository\ProductReviewRepository;
  15. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  16. use Symfony\Component\Form\FormFactoryInterface;
  17. class ProductDetailEventSubscriber implements EventSubscriberInterface
  18. {
  19.     /**
  20.      * @var ProductReviewRepository
  21.      */
  22.     private $productReviewRepository;
  23.     /**
  24.      * @var FormFactoryInterface
  25.      */
  26.     private $formFactory;
  27.     public function __construct(
  28.         ProductReviewRepository $productReviewRepository,
  29.         FormFactoryInterface $formFactory
  30.     ) {
  31.         $this->productReviewRepository $productReviewRepository;
  32.         $this->formFactory $formFactory;
  33.     }
  34.     /**
  35.      * @return array
  36.      */
  37.     public static function getSubscribedEvents()
  38.     {
  39.         return [
  40.             'Product/detail.twig' => 'onProductDetail',
  41.         ];
  42.     }
  43.     /**
  44.      * 商品詳細ページにレビュー機能を追加
  45.      *
  46.      * @param TemplateEvent $event
  47.      */
  48.     public function onProductDetail(TemplateEvent $event)
  49.     {
  50.         $Product $event->getParameter('Product');
  51.         
  52.         if (!$Product) {
  53.             return;
  54.         }
  55.         // レビュー統計を取得
  56.         $reviewStats = [
  57.             'count' => $this->productReviewRepository->getReviewCount($Producttrue),
  58.             'average' => $this->productReviewRepository->getAverageRating($Product),
  59.             'rating_breakdown' => $this->getRatingBreakdown($Product)
  60.         ];
  61.         // 最新レビューを3件取得
  62.         $recentReviews $this->productReviewRepository
  63.             ->createQueryBuilder('pr')
  64.             ->where('pr.Product = :product')
  65.             ->andWhere('pr.is_visible = :visible')
  66.             ->setParameter('product'$Product)
  67.             ->setParameter('visible'true)
  68.             ->orderBy('pr.create_date''DESC')
  69.             ->setMaxResults(3)
  70.             ->getQuery()
  71.             ->getResult();
  72.         // レビュー投稿フォーム
  73.         $productReview = new ProductReview();
  74.         $reviewForm $this->formFactory->create(ProductReviewType::class, $productReview, [
  75.             'product_id' => $Product->getId()
  76.         ]);
  77.         // テンプレートに変数を追加
  78.         $event->setParameter('reviewStats'$reviewStats);
  79.         $event->setParameter('recentReviews'$recentReviews);
  80.         $event->setParameter('reviewForm'$reviewForm->createView());
  81.         // レビューセクションをテンプレートに追加
  82.         $reviewSection '
  83.         <!-- レビューセクション -->
  84.         <div class="ec-productRole__description">
  85.             <!-- レビューサマリー -->
  86.             <div class="product-review-summary-section">
  87.                 ' $this->renderReviewSummary($Product$reviewStats$recentReviews) . '
  88.             </div>
  89.             
  90.             <!-- レビュー投稿フォーム -->
  91.             <div id="review-form-section" style="display: none;">
  92.                 ' $this->renderReviewForm($reviewForm->createView(), $Product) . '
  93.             </div>
  94.         </div>
  95.         
  96.         <script>
  97.         $(function() {
  98.             // レビューフォーム表示切替
  99.             $(document).on("click", "#toggle-review-form", function() {
  100.                 $("#review-form-section").slideToggle();
  101.                 var $btn = $(this);
  102.                 var text = $btn.text();
  103.                 if (text.indexOf("レビューを投稿") !== -1) {
  104.                     $btn.html("<i class=\"fa fa-times\"></i> キャンセル");
  105.                 } else {
  106.                     $btn.html("<i class=\"fa fa-edit\"></i> レビューを投稿する");
  107.                 }
  108.             });
  109.             
  110.             // 星評価の視覚的フィードバック
  111.             $(".star-rating-input label").on("mouseenter", function() {
  112.                 var rating = $(this).data("rating");
  113.                 $(".star-rating-input label").each(function() {
  114.                     if ($(this).data("rating") >= rating) {
  115.                         $(this).css("color", "#ffc107");
  116.                     } else {
  117.                         $(this).css("color", "#ddd");
  118.                     }
  119.                 });
  120.             });
  121.             
  122.             $(".star-rating-input").on("mouseleave", function() {
  123.                 var checkedRating = $(".star-rating-input input:checked").val();
  124.                 $(".star-rating-input label").each(function() {
  125.                     if ($(this).data("rating") <= checkedRating) {
  126.                         $(this).css("color", "#ffc107");
  127.                     } else {
  128.                         $(this).css("color", "#ddd");
  129.                     }
  130.                 });
  131.             });
  132.         });
  133.         </script>';
  134.         // 商品説明の後にレビューセクションを挿入
  135.         $source $event->getSource();
  136.         $search '/<h3>関連動画<\/h3>/';
  137.         if (preg_match($search$source$result)) {
  138.             $searchText $result[0];
  139.             $replace $reviewSection $searchText;
  140.             $source str_replace($searchText$replace$source);
  141.             $event->setSource($source);
  142.         }
  143.     }
  144.     /**
  145.      * 評価別レビュー数の集計
  146.      */
  147.     private function getRatingBreakdown($product)
  148.     {
  149.         $qb $this->productReviewRepository->createQueryBuilder('pr')
  150.             ->select('pr.rating, COUNT(pr.id) as count')
  151.             ->where('pr.Product = :product')
  152.             ->andWhere('pr.is_visible = :visible')
  153.             ->setParameter('product'$product)
  154.             ->setParameter('visible'true)
  155.             ->groupBy('pr.rating')
  156.             ->orderBy('pr.rating''DESC');
  157.         $results $qb->getQuery()->getResult();
  158.         
  159.         $breakdown = [=> 0=> 0=> 0=> 0=> 0];
  160.         
  161.         foreach ($results as $result) {
  162.             $breakdown[$result['rating']] = (int) $result['count'];
  163.         }
  164.         return $breakdown;
  165.     }
  166.     /**
  167.      * レビューサマリーのHTML生成
  168.      */
  169.     private function renderReviewSummary($product$reviewStats$recentReviews)
  170.     {
  171.         $html '<div class="product-review-summary" style="padding: 1rem; background-color: #f8f9fa; border-radius: 8px; margin: 1rem 0;">';
  172.         
  173.         if ($reviewStats['count'] > 0) {
  174.             $rating $reviewStats['average'] ?: 0;
  175.             $stars '';
  176.             for ($i 1$i <= 5$i++) {
  177.                 if ($i <= $rating) {
  178.                     $stars .= '<i class="fa fa-star text-warning"></i>';
  179.                 } elseif ($i 0.5 <= $rating) {
  180.                     $stars .= '<i class="fa fa-star-half-o text-warning"></i>';
  181.                 } else {
  182.                     $stars .= '<i class="fa fa-star-o text-muted"></i>';
  183.                 }
  184.             }
  185.             
  186.             $html .= '
  187.                 <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
  188.                     <div style="display: flex; align-items: center; gap: 0.5rem;">
  189.                         <div style="font-size: 1.2rem;">' $stars '</div>
  190.                         <span style="font-weight: 500; color: #495057;">' number_format($rating1) . ' (' $reviewStats['count'] . '件のレビュー)</span>
  191.                     </div>
  192.                     <div style="display: flex; gap: 0.5rem;">
  193.                         <a href="/products/' $product->getId() . '/reviews" class="btn btn-outline-primary btn-sm">
  194.                             <i class="fa fa-eye"></i> 全てのレビューを見る
  195.                         </a>
  196.                         <button type="button" class="btn btn-primary btn-sm" id="toggle-review-form">
  197.                             <i class="fa fa-edit"></i> レビューを投稿する
  198.                         </button>
  199.                     </div>
  200.                 </div>';
  201.         } else {
  202.             $html .= '
  203.                 <div style="text-align: center; padding: 1rem 0;">
  204.                     <div style="font-size: 1.2rem; margin-bottom: 0.5rem;">
  205.                         <i class="fa fa-star-o text-muted"></i>
  206.                         <i class="fa fa-star-o text-muted"></i>
  207.                         <i class="fa fa-star-o text-muted"></i>
  208.                         <i class="fa fa-star-o text-muted"></i>
  209.                         <i class="fa fa-star-o text-muted"></i>
  210.                     </div>
  211.                     <span style="color: #6c757d; margin: 0 1rem;">まだレビューはありません</span>
  212.                     <button type="button" class="btn btn-primary btn-sm ml-2" id="toggle-review-form">
  213.                         <i class="fa fa-edit"></i> 最初のレビューを投稿する
  214.                     </button>
  215.                 </div>';
  216.         }
  217.         
  218.         $html .= '</div>';
  219.         
  220.         return $html;
  221.     }
  222.     /**
  223.      * レビュー投稿フォームのHTML生成
  224.      */
  225.     private function renderReviewForm($form$product)
  226.     {
  227.         return '
  228.         <div style="background-color: #f8f9fa; border-radius: 8px; padding: 2rem; margin: 1rem 0;">
  229.             <h4 style="margin-bottom: 1.5rem;">レビューを投稿する</h4>
  230.             <form action="/product_reviews/' $product->getId() . '" METHOD="POST" NOVALIDATE>
  231.                 <INPUT TYPE="HIDDEN" NAME="PRODUCT_REVIEW[_TOKEN]" VALUE="' $form->children['_token']->vars['value'] . '">
  232.                 <input type="hidden" name="product_review[Product]" value="' $product->getId() . '">
  233.                 
  234.                 <div class="row">
  235.                     <div class="col-md-6">
  236.                         <div class="form-group">
  237.                             <label for="product_review_reviewer_name">お名前 <span class="text-danger">*</span></label>
  238.                             <input type="text" class="form-control" id="product_review_reviewer_name" name="product_review[reviewer_name]" required placeholder="お名前を入力してください">
  239.                         </div>
  240.                     </div>
  241.                     <div class="col-md-6">
  242.                         <div class="form-group">
  243.                             <label for="product_review_reviewer_email">メールアドレス</label>
  244.                             <input type="email" class="form-control" id="product_review_reviewer_email" name="product_review[reviewer_email]" placeholder="example@email.com(任意)">
  245.                             <small class="form-text text-muted">メールアドレスは公開されません</small>
  246.                         </div>
  247.                     </div>
  248.                 </div>
  249.                 
  250.                 <div class="form-group">
  251.                     <label for="product_review_title">レビュータイトル</label>
  252.                     <input type="text" class="form-control" id="product_review_title" name="product_review[title]" placeholder="レビューのタイトル(任意)">
  253.                 </div>
  254.                 
  255.                 <div class="form-group">
  256.                     <label>評価 <span class="text-danger">*</span></label>
  257.                     <div class="star-rating-input" style="display: flex; flex-direction: row-reverse; gap: 0.25rem; margin-bottom: 1rem;">
  258.                         <input type="radio" id="rating_5" name="product_review[rating]" value="5" style="display: none;">
  259.                         <label for="rating_5" data-rating="5" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
  260.                         <input type="radio" id="rating_4" name="product_review[rating]" value="4" style="display: none;">
  261.                         <label for="rating_4" data-rating="4" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
  262.                         <input type="radio" id="rating_3" name="product_review[rating]" value="3" style="display: none;">
  263.                         <label for="rating_3" data-rating="3" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
  264.                         <input type="radio" id="rating_2" name="product_review[rating]" value="2" style="display: none;">
  265.                         <label for="rating_2" data-rating="2" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
  266.                         <input type="radio" id="rating_1" name="product_review[rating]" value="1" style="display: none;">
  267.                         <label for="rating_1" data-rating="1" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
  268.                     </div>
  269.                     <small class="form-text text-muted">星をクリックして評価してください</small>
  270.                 </div>
  271.                 
  272.                 <div class="form-group">
  273.                     <label for="product_review_comment">コメント</label>
  274.                     <textarea class="form-control" id="product_review_comment" name="product_review[comment]" rows="5" placeholder="商品についての感想をお聞かせください(任意)"></textarea>
  275.                 </div>
  276.                 
  277.                 <div class="text-center">
  278.                     <button type="submit" class="btn btn-primary">
  279.                         <i class="fa fa-paper-plane"></i> レビューを投稿する
  280.                     </button>
  281.                 </div>
  282.             </form>
  283.         </div>';
  284.     }
  285. }