【EC CUBE 4】価格帯から商品を検索できるブロックを作ってみた

フォームに価格を入力してボタンをクリックすると、その価格に合った商品を検索して一覧表示してくれるブロックをトップページに実装してみたので、その手順を公開します。

実装内容
トップページに価格検索ブロック(search.twig)を設置

価格検索ブロック:「最小価格」「最大価格」の入力フォームと「商品を探す」ボタンを設置

検索した商品一覧を表示するページ(search-list.twig)

入力した価格レンジに当てはまる商品一覧が表示される

開発前にデバッグモードの設定をお薦めします

デバッグモードを設定しておくと、エラーが起きたときに詳細情報が表示されるようになります。
エラー箇所を探しやすくなるので、開発前に設定しておくのをオススメします。
デバッグモードの設定方法については 以下記事 で解説しています。

カスタマイズ後は、デバッグモードの解除を忘れないように。

【動作環境】
EC CUBEのバージョン:4.3.0
サーバー:XServer

目次

実装の流れ

  1. 商品を取得するためのリポジトリを作成
    • ProductSearchRepository を作成し、価格に応じた商品を取得するメソッドを実装する。
  2. 入力を受け取るためのフォームタイプを作成
    • ProductPriceSearchType を作成し、ユーザーが価格を入力できるフォームを定義する。
  3. 検索結果を表示するページを作成
    • ProductSearchController を作成し、価格条件に合致する商品の一覧を取得して表示する。
    • search-list.twig に商品一覧を出力する。
  4. トップページにフォームを設置
    • TopController にフォームを表示・処理する機能を追加し、search.twig にフォームを配置する。
STEP

商品を取得するためのリポジトリを作成

まず、入力された商品価格(最小価格、最大価格)に応じて商品を検索するメソッドfindByPriceRange(?int $minPrice, ?int $maxPrice)を定義したリポジトリ(ProductSearchRepository)を作成し、「app/Customize/Repository」にアップします。

<?php

namespace Customize\Repository;

use Eccube\Entity\Product;
use Eccube\Repository\AbstractRepository;
use Doctrine\Persistence\ManagerRegistry as RegistryInterface;

/**
 * 商品検索用のリポジトリ
 *
 * 指定された価格範囲内の商品を、最も安い価格順に取得する
 */
class ProductSearchRepository extends AbstractRepository
{
    /**
     * コンストラクタ
     *
     * @param RegistryInterface $registry エンティティマネージャーのレジストリ
     */
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Product::class);
    }

    /**
     * 指定された価格範囲内の商品を、最も安い価格順に取得
     *
     * @param int|null $minPrice 最小価格 (nullの場合は制限なし)
     * @param int|null $maxPrice 最大価格 (nullの場合は制限なし)
     * @return Product[] 該当する商品リスト
     */
    public function findByPriceRange(?int $minPrice, ?int $maxPrice)
    {
        // クエリビルダーの作成
        $qb = $this->createQueryBuilder('p')
            // 商品と紐づく商品クラス (ProductClasses) を内部結合
            ->innerJoin('p.ProductClasses', 'pc')
            // 公開中の商品(Status = 1) のみを対象にする
            ->where('p.Status = 1')
            // 商品ごとにグループ化 (1つの商品に複数のProductClassesが存在するため)
            ->groupBy('p.id')
            // 最安値 (pc.price02の最小値) の昇順で並び替え
            ->orderBy('MIN(pc.price02)', 'ASC');
    
        // 最小価格が指定されている場合、条件を追加
        if (!is_null($minPrice)) {
            $qb->andHaving('MIN(pc.price02) >= :minPrice')
               ->setParameter('minPrice', $minPrice);
        }
    
        // 最大価格が指定されている場合、条件を追加
        if (!is_null($maxPrice)) {
            $qb->andHaving('MIN(pc.price02) <= :maxPrice')
               ->setParameter('maxPrice', $maxPrice);
        }
    
        // クエリを実行して結果を取得
        return $qb->getQuery()->getResult();
    }
}

▼▼▼ リポジトリの作り方はこちらに纏めています ▼▼▼

STEP

入力を受け取るためのフォームタイプを作成

次に、トップページに設置するフォームを生成するためのフォームタイプ(ProductPriceSearchType)を作成し、「app/Customize/Form/Type」にアップします。

<?php

namespace Customize\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * 商品価格検索フォーム
 * 
 * ユーザーが最小価格・最大価格を入力し、価格範囲内の商品を検索するためのフォーム
 */
class ProductPriceSearchType extends AbstractType
{
    /**
     * フォームの構築メソッド
     *
     * @param FormBuilderInterface $builder フォームビルダー
     * @param array $options フォームのオプション
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // 最小価格入力フィールド
            ->add('min_price', IntegerType::class, [
                'label' => '最小価格', // ラベルの設定
                'required' => false, // 入力必須ではない
                'constraints' => [
                    new Assert\PositiveOrZero(), // 0以上の整数のみ許可
                ],
                'attr' => [
                    'min' => 0, // HTMLの最小値制限
                    'placeholder' => '例: 1000' // プレースホルダー(例を表示)
                ],
            ])
            // 最大価格入力フィールド
            ->add('max_price', IntegerType::class, [
                'label' => '最大価格', // ラベルの設定
                'required' => false, // 入力必須ではない
                'constraints' => [
                    new Assert\PositiveOrZero(), // 0以上の整数のみ許可
                ],
                'attr' => [
                    'min' => 0, // HTMLの最小値制限
                    'placeholder' => '例: 5000' // プレースホルダー(例を表示)
                ],
            ])
            // 検索ボタン
            ->add('search', SubmitType::class, [
                'label' => '商品を探す', // ボタンのラベル
            ]);
    }
}

▼▼▼ フォームタイプの作り方はこちらに纏めています ▼▼▼

STEP

検索結果を表示するページを作成

以下手順に従って、トップページからフォームを送信したときに遷移する、検索結果を表示するページ(search-list.twig)とコントローラ(ProductSearchController)を新しく用意します。

STEP

管理画面 → コンテンツ管理 → ページ管理より、ページ(search-list.twig)を新規作成

  • ページ名:検索商品一覧ページ
  • URL:products/search-list
  • ファイル名:search-list.twig
STEP

新規作成したページのTwigテンプレートを「app/template/default」にコピー

管理画面から新規作成したページのTwigテンプレートは「app/template/user_data」にあります。

STEP

コピーしたTwigテンプレートを以下の通り修正

{% extends 'default_frame.twig' %}

{% block main %}
{{ Title }}
  <div class="ec-shelfRole">
      <ul class="ec-shelfGrid">
          {# Productsに格納されているデータを順に取り出し、Productに代入 #}
          {% for Product in Products %}
            <li class="ec-shelfGrid__item">
                <a href="{{ url('product_detail', {'id': Product.id}) }}">
                    {# 商品画像の表示 #}
                    <p class="ec-shelfGrid__item-image">
                        <img src="{{ asset(Product.main_list_image|no_image_product, 'save_image') }}" alt="{{ Product.name }}" loading="lazy">
                    </p>
                    {# 商品名の表示 #}
                    <p>{{ Product.name }}</p>
                </a>
            </li>
          {% endfor %}
      </ul>
  </div>
{% endblock %}
STEP

以下のコントローラ(ProductSearchController)を「app/Customize/Controller」にアップ

<?php

namespace Customize\Controller;

use Eccube\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Customize\Repository\ProductSearchRepository;
use Customize\Form\Type\ProductPriceSearchType;

/**
 * 商品検索コントローラー
 * 
 * ユーザーが価格範囲を指定して商品を検索する機能を提供する
 */
class ProductSearchController extends AbstractController
{
    /**
     * @var ProductSearchRepository 商品検索リポジトリ
     */
    protected $productSearchRepository;

    /**
     * コンストラクタ
     *
     * @param ProductSearchRepository $productSearchRepository 商品検索リポジトリの依存注入
     */
    public function __construct(ProductSearchRepository $productSearchRepository) {
        $this->ProductSearchRepository = $productSearchRepository;
    }
    
    /**
     * 商品検索ページ
     * 
     * @Route("products/search-list", name="products_search-list")
     * @Template("search-list.twig")
     *
     * @param Request $request リクエスト情報
     * @return array テンプレートに渡すデータ
     */
    public function index(Request $request)
    {
        $title = ''; // 初期メッセージ
        $products = []; // 検索結果を格納する配列
    
        // 価格検索フォームの作成
        $form = $this->createForm(ProductPriceSearchType::class);
        $form->handleRequest($request); // リクエストデータをフォームに適用
    
        // フォームが送信され、かつ有効な場合のみ処理を実行
        if ($form->isSubmitted() && $form->isValid()) {
            // フォームから最小・最大価格を取得
            $minPrice = $form->get('min_price')->getData();
            $maxPrice = $form->get('max_price')->getData();
    
            // 数値チェックを行い、null の場合は変換(数値でなければ null にする)
            $minPrice = is_numeric($minPrice) ? (int) $minPrice : null;
            $maxPrice = is_numeric($maxPrice) ? (int) $maxPrice : null;
    
            // 最小価格または最大価格のいずれかが指定されている場合に商品を検索
            if (!is_null($minPrice) || !is_null($maxPrice)) {
                $products = $this->ProductSearchRepository->findByPriceRange($minPrice, $maxPrice);
            }
        }

        // 検索結果が空の場合のメッセージ
        if (empty($products)) {
            $title = 'お探しの商品は見つかりませんでした。';
        }

        // テンプレートに渡すデータを返す
        return [
            'Title' => $title,
            'Products' => $products,
        ];
    }
}
STEP

「dtb_page」テーブルに保存されたページ情報を修正

  • edit_type02に修正
  • urlproducts/search-listproducts_search-listに修正

▼▼▼ 新規ページの作り方は以下記事に纏めています ▼▼▼

STEP

トップページにフォームを設置

{% block stylesheet %}
  <style>
    .search-container form {
        display: flex;
        align-items: center;
        gap: 20px;
        margin: 60px 48px;
    }
    
    .search-container input,
    .search-container button {
        min-width: 100px;
    }
    
  </style>
{% endblock %}

<div class="search-container">
    <form method="post" action="{{ path('products_search-list') }}">
        {{ form_start(priceSearchForm) }}
        {{ form_widget(priceSearchForm.min_price) }}
        <span>~</span>
        {{ form_widget(priceSearchForm.max_price) }}
        {{ form_widget(priceSearchForm.search) }}
        {{ form_end(priceSearchForm) }}
    </form>
</div>

フォームタイプで定義したフォーム情報を渡すため、トップページのコントローラ(TopController.php)を以下の通り修正して「app/Customize/Controller」にアップします。

<?php

namespace Customize\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\Routing\Annotation\Route;
use Eccube\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Customize\Form\Type\ProductPriceSearchType;

class TopController extends AbstractController
{
    /**
     * @Route("/", name="homepage", methods={"GET"})
     * @Template("index.twig")
     */
    public function index()
    {
        $priceSearchForm = $this->createForm(ProductPriceSearchType::class);

        return [
            'priceSearchForm' => $priceSearchForm->createView(),
        ];
    }
}
STEP

キャッシュを削除

以上でカスタマイズ完了です。
管理画面 → コンテンツ管理よりキャッシュを削除し、実装できているか確認しましょう。

まとめ

実装の手順

  1. リポジトリ を作成し、価格条件で商品を検索できるようにする
  2. フォーム を作成し、ユーザーが価格を入力できるようにする
  3. 検索結果を表示するページを作成 し、該当する商品を表示する
  4. トップページにフォームを設置 し、検索ページへ遷移できるようにする

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次