<?php
/*
* This file is part of PiaProductReview
*
* Copyright(c) Pia Staff. All Rights Reserved.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Plugin\PiaProductReview\EventSubscriber;
use Eccube\Event\TemplateEvent;
use Plugin\PiaProductReview\Entity\ProductReview;
use Plugin\PiaProductReview\Form\Type\ProductReviewType;
use Plugin\PiaProductReview\Repository\ProductReviewRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface;
class ProductDetailEventSubscriber implements EventSubscriberInterface
{
/**
* @var ProductReviewRepository
*/
private $productReviewRepository;
/**
* @var FormFactoryInterface
*/
private $formFactory;
public function __construct(
ProductReviewRepository $productReviewRepository,
FormFactoryInterface $formFactory
) {
$this->productReviewRepository = $productReviewRepository;
$this->formFactory = $formFactory;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
'Product/detail.twig' => 'onProductDetail',
];
}
/**
* 商品詳細ページにレビュー機能を追加
*
* @param TemplateEvent $event
*/
public function onProductDetail(TemplateEvent $event)
{
$Product = $event->getParameter('Product');
if (!$Product) {
return;
}
// レビュー統計を取得
$reviewStats = [
'count' => $this->productReviewRepository->getReviewCount($Product, true),
'average' => $this->productReviewRepository->getAverageRating($Product),
'rating_breakdown' => $this->getRatingBreakdown($Product)
];
// 最新レビューを3件取得
$recentReviews = $this->productReviewRepository
->createQueryBuilder('pr')
->where('pr.Product = :product')
->andWhere('pr.is_visible = :visible')
->setParameter('product', $Product)
->setParameter('visible', true)
->orderBy('pr.create_date', 'DESC')
->setMaxResults(3)
->getQuery()
->getResult();
// レビュー投稿フォーム
$productReview = new ProductReview();
$reviewForm = $this->formFactory->create(ProductReviewType::class, $productReview, [
'product_id' => $Product->getId()
]);
// テンプレートに変数を追加
$event->setParameter('reviewStats', $reviewStats);
$event->setParameter('recentReviews', $recentReviews);
$event->setParameter('reviewForm', $reviewForm->createView());
// レビューセクションをテンプレートに追加
$reviewSection = '
<!-- レビューセクション -->
<div class="ec-productRole__description">
<!-- レビューサマリー -->
<div class="product-review-summary-section">
' . $this->renderReviewSummary($Product, $reviewStats, $recentReviews) . '
</div>
<!-- レビュー投稿フォーム -->
<div id="review-form-section" style="display: none;">
' . $this->renderReviewForm($reviewForm->createView(), $Product) . '
</div>
</div>
<script>
$(function() {
// レビューフォーム表示切替
$(document).on("click", "#toggle-review-form", function() {
$("#review-form-section").slideToggle();
var $btn = $(this);
var text = $btn.text();
if (text.indexOf("レビューを投稿") !== -1) {
$btn.html("<i class=\"fa fa-times\"></i> キャンセル");
} else {
$btn.html("<i class=\"fa fa-edit\"></i> レビューを投稿する");
}
});
// 星評価の視覚的フィードバック
$(".star-rating-input label").on("mouseenter", function() {
var rating = $(this).data("rating");
$(".star-rating-input label").each(function() {
if ($(this).data("rating") >= rating) {
$(this).css("color", "#ffc107");
} else {
$(this).css("color", "#ddd");
}
});
});
$(".star-rating-input").on("mouseleave", function() {
var checkedRating = $(".star-rating-input input:checked").val();
$(".star-rating-input label").each(function() {
if ($(this).data("rating") <= checkedRating) {
$(this).css("color", "#ffc107");
} else {
$(this).css("color", "#ddd");
}
});
});
});
</script>';
// 商品説明の後にレビューセクションを挿入
$source = $event->getSource();
$search = '/<h3>関連動画<\/h3>/';
if (preg_match($search, $source, $result)) {
$searchText = $result[0];
$replace = $reviewSection . $searchText;
$source = str_replace($searchText, $replace, $source);
$event->setSource($source);
}
}
/**
* 評価別レビュー数の集計
*/
private function getRatingBreakdown($product)
{
$qb = $this->productReviewRepository->createQueryBuilder('pr')
->select('pr.rating, COUNT(pr.id) as count')
->where('pr.Product = :product')
->andWhere('pr.is_visible = :visible')
->setParameter('product', $product)
->setParameter('visible', true)
->groupBy('pr.rating')
->orderBy('pr.rating', 'DESC');
$results = $qb->getQuery()->getResult();
$breakdown = [1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0];
foreach ($results as $result) {
$breakdown[$result['rating']] = (int) $result['count'];
}
return $breakdown;
}
/**
* レビューサマリーのHTML生成
*/
private function renderReviewSummary($product, $reviewStats, $recentReviews)
{
$html = '<div class="product-review-summary" style="padding: 1rem; background-color: #f8f9fa; border-radius: 8px; margin: 1rem 0;">';
if ($reviewStats['count'] > 0) {
$rating = $reviewStats['average'] ?: 0;
$stars = '';
for ($i = 1; $i <= 5; $i++) {
if ($i <= $rating) {
$stars .= '<i class="fa fa-star text-warning"></i>';
} elseif ($i - 0.5 <= $rating) {
$stars .= '<i class="fa fa-star-half-o text-warning"></i>';
} else {
$stars .= '<i class="fa fa-star-o text-muted"></i>';
}
}
$html .= '
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<div style="font-size: 1.2rem;">' . $stars . '</div>
<span style="font-weight: 500; color: #495057;">' . number_format($rating, 1) . ' (' . $reviewStats['count'] . '件のレビュー)</span>
</div>
<div style="display: flex; gap: 0.5rem;">
<a href="/products/' . $product->getId() . '/reviews" class="btn btn-outline-primary btn-sm">
<i class="fa fa-eye"></i> 全てのレビューを見る
</a>
<button type="button" class="btn btn-primary btn-sm" id="toggle-review-form">
<i class="fa fa-edit"></i> レビューを投稿する
</button>
</div>
</div>';
} else {
$html .= '
<div style="text-align: center; padding: 1rem 0;">
<div style="font-size: 1.2rem; margin-bottom: 0.5rem;">
<i class="fa fa-star-o text-muted"></i>
<i class="fa fa-star-o text-muted"></i>
<i class="fa fa-star-o text-muted"></i>
<i class="fa fa-star-o text-muted"></i>
<i class="fa fa-star-o text-muted"></i>
</div>
<span style="color: #6c757d; margin: 0 1rem;">まだレビューはありません</span>
<button type="button" class="btn btn-primary btn-sm ml-2" id="toggle-review-form">
<i class="fa fa-edit"></i> 最初のレビューを投稿する
</button>
</div>';
}
$html .= '</div>';
return $html;
}
/**
* レビュー投稿フォームのHTML生成
*/
private function renderReviewForm($form, $product)
{
return '
<div style="background-color: #f8f9fa; border-radius: 8px; padding: 2rem; margin: 1rem 0;">
<h4 style="margin-bottom: 1.5rem;">レビューを投稿する</h4>
<form action="/product_reviews/' . $product->getId() . '" METHOD="POST" NOVALIDATE>
<INPUT TYPE="HIDDEN" NAME="PRODUCT_REVIEW[_TOKEN]" VALUE="' . $form->children['_token']->vars['value'] . '">
<input type="hidden" name="product_review[Product]" value="' . $product->getId() . '">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="product_review_reviewer_name">お名前 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="product_review_reviewer_name" name="product_review[reviewer_name]" required placeholder="お名前を入力してください">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="product_review_reviewer_email">メールアドレス</label>
<input type="email" class="form-control" id="product_review_reviewer_email" name="product_review[reviewer_email]" placeholder="example@email.com(任意)">
<small class="form-text text-muted">メールアドレスは公開されません</small>
</div>
</div>
</div>
<div class="form-group">
<label for="product_review_title">レビュータイトル</label>
<input type="text" class="form-control" id="product_review_title" name="product_review[title]" placeholder="レビューのタイトル(任意)">
</div>
<div class="form-group">
<label>評価 <span class="text-danger">*</span></label>
<div class="star-rating-input" style="display: flex; flex-direction: row-reverse; gap: 0.25rem; margin-bottom: 1rem;">
<input type="radio" id="rating_5" name="product_review[rating]" value="5" style="display: none;">
<label for="rating_5" data-rating="5" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
<input type="radio" id="rating_4" name="product_review[rating]" value="4" style="display: none;">
<label for="rating_4" data-rating="4" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
<input type="radio" id="rating_3" name="product_review[rating]" value="3" style="display: none;">
<label for="rating_3" data-rating="3" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
<input type="radio" id="rating_2" name="product_review[rating]" value="2" style="display: none;">
<label for="rating_2" data-rating="2" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
<input type="radio" id="rating_1" name="product_review[rating]" value="1" style="display: none;">
<label for="rating_1" data-rating="1" style="font-size: 2rem; color: #ddd; cursor: pointer;"><i class="fa fa-star"></i></label>
</div>
<small class="form-text text-muted">星をクリックして評価してください</small>
</div>
<div class="form-group">
<label for="product_review_comment">コメント</label>
<textarea class="form-control" id="product_review_comment" name="product_review[comment]" rows="5" placeholder="商品についての感想をお聞かせください(任意)"></textarea>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">
<i class="fa fa-paper-plane"></i> レビューを投稿する
</button>
</div>
</form>
</div>';
}
}