2系にあった商品並び替え機能をEC-CUBE4系で実装する方法

EC-CUBE Advent Calendar 2021 17日目の記事です。

EC-CUBE2では「商品並び替え」機能が存在していましたが、EC-CUBE3系からはその機能が無くなっています。

今回は「商品並び替え」機能をEC-CUBE4系で実装する方法を説明します。EC-CUBE3系も読み替えれば実現可能と思いますが、今回は説明を割愛します。

ProductCategoryクラスの修正

ProductCategoryクラスに並び順という項目を追加します。

  • src/Eccube/Entity/ProductCategory.php
<?php
〜
〜

/**
 * @var int
 *
 * @ORM\Column(name="sort_no", type="smallint", options={"unsigned":true, "default":0})
 */
private $sort_no = 0;

/**
 * Set sortNo.
 *
 * @param int $sortNo
 *
 * @return ProductCategory
 */
public function setSortNo($sortNo)
{
    $this->sort_no = $sortNo;

    return $this;
}

/**
 * Get sortNo.
 *
 * @return int
 */
public function getSortNo()
{
    return $this->sort_no;
}

追加後、doctrineコマンドを利用してdtb_product_categoryテーブルへカラムを追加します。

EC-CUBEディレクトリ直下にコマンドプロンプトやターミナルで移動します。

先ず、どのようなSQL文が実行されるかを以下のコマンドで確認します。

php bin/console doctrine:schema:update --dump-sql

実際にSQL文をするために、以下のコマンドを実行します。

php bin/console doctrine:schema:update --force

コマンド実行後、エラーが発生しなければDBにdtb_product_categoryテーブルにsort_noというカラムが作成されます。

商品並び替え管理画面の作成

次に、商品並び替え機能用の管理画面を作成します。以下のController、FormType、twigファイルを作成してください。

  • src/Eccube/Controller/Admin/Product/ProductSortController.php
<?php

namespace Eccube\Controller\Admin\Product;

use Eccube\Controller\AbstractController;
use Eccube\Entity\Category;
use Eccube\Entity\ProductCategory;
use Eccube\Form\Type\Admin\ProductCategoryType;
use Eccube\Repository\CategoryRepository;
use Eccube\Repository\ProductCategoryRepository;
use Eccube\Util\CacheUtil;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;

class ProductSortController extends AbstractController
{
    /**
     * @var ProductCategoryRepository
     */
    protected $productCategoryRepository;

    /**
     * @var CategoryRepository
     */
    protected $categoryRepository;

    /**
     * ProductSortController constructor.
     *
     * @param ProductCategoryRepository $productCategoryRepository
     * @param CategoryRepository $categoryRepository
     */
    public function __construct(ProductCategoryRepository $productCategoryRepository, CategoryRepository $categoryRepository)
    {
        $this->productCategoryRepository = $productCategoryRepository;
        $this->categoryRepository = $categoryRepository;
    }


    /**
     * @Route("/%eccube_admin_route%/product/sort", name="admin_product_sort")
     * @Route("/%eccube_admin_route%/product/sort/{parent_id}", requirements={"parent_id" = "\d+"}, name="admin_product_sort_show")
     * @Route("/%eccube_admin_route%/product/sort/{id}/edit", requirements={"id" = "\d+"}, name="admin_product_sort_edit")
     * @Template("@admin/Product/product_sort.twig")
     */
    public function index(Request $request, $parent_id = null, $id = null, CacheUtil $cacheUtil)
    {
        if ($parent_id) {
            /** @var Category $Parent */
            $Parent = $this->categoryRepository->find($parent_id);
            if (!$Parent) {
                throw new NotFoundHttpException();
            }
        } else {
            $Parent = null;
        }
        if ($id) {
            $TargetCategory = $this->categoryRepository->find($id);
            if (!$TargetCategory) {
                throw new NotFoundHttpException();
            }
            $Parent = $TargetCategory->getParent();
        } else {
            $TargetCategory = new Category();
            $TargetCategory->setParent($Parent);
            if ($Parent) {
                $TargetCategory->setHierarchy($Parent->getHierarchy() + 1);
            } else {
                $TargetCategory->setHierarchy(1);
            }
        }

        $ProductCategories = [];

        if ($Parent) {
            $ProductCategories = $this->productCategoryRepository->findBy(['Category' => $Parent], ['sort_no' => 'DESC']);
        }

        // ツリー表示のため、ルートからのカテゴリを取得
        $TopCategories = $this->categoryRepository->getList(null);

        $builder = $this->formFactory
            ->createBuilder(ProductCategoryType::class);

        $form = $builder->getForm();

        $forms = [];
        /** @var ProductCategory $ProductCategory */
        foreach ($ProductCategories as $ProductCategory) {
            $forms[$ProductCategory->getProductId()] = $this->formFactory
                ->createNamed('product_category_'.$ProductCategory->getProductId(), ProductCategoryType::class, $ProductCategory);
        }

        if ($request->getMethod() === 'POST') {
            $form->handleRequest($request);
            if ($form->isValid()) {
                if ($this->eccubeConfig['eccube_category_nest_level'] < $TargetCategory->getHierarchy()) {
                    throw new BadRequestHttpException();
                }
                log_info('カテゴリ登録開始', [$id]);

                $this->productCategoryRepository->save($TargetCategory);
                $this->entityManager->flush();

                log_info('カテゴリ登録完了', [$id]);

                $this->addSuccess('admin.common.save_complete', 'admin');

                $cacheUtil->clearDoctrineCache();

                if ($Parent) {
                    return $this->redirectToRoute('admin_product_sort_show', ['parent_id' => $Parent->getId()]);
                } else {
                    return $this->redirectToRoute('admin_product_sort');
                }
            }

            foreach ($forms as $editForm) {
                $editForm->handleRequest($request);
                if ($editForm->isSubmitted() && $editForm->isValid()) {
                    $this->productCategoryRepository->save($editForm->getData());
                    $this->entityManager->flush();

                    $this->addSuccess('admin.common.save_complete', 'admin');

                    $cacheUtil->clearDoctrineCache();

                    if ($Parent) {
                        return $this->redirectToRoute('admin_product_sort_show', ['parent_id' => $Parent->getId()]);
                    } else {
                        return $this->redirectToRoute('admin_product_sort');
                    }
                }
            }
        }

        $formViews = [];
        foreach ($forms as $key => $value) {
            $formViews[$key] = $value->createView();
        }

        $Ids = [];
        if ($Parent && $Parent->getParents()) {
            foreach ($Parent->getParents() as $item) {
                $Ids[] = $item['id'];
            }
        }
        $Ids[] = intval($parent_id);

        return [
            'form' => $form->createView(),
            'Parent' => $Parent,
            'Ids' => $Ids,
            'ProductCategories' => $ProductCategories,
            'TopCategories' => $TopCategories,
            'TargetCategory' => $TargetCategory,
            'forms' => $formViews,
            'parent_id' => $parent_id,
        ];
    }

    /**
     * @Route("/%eccube_admin_route%/product/sort/sort_no/move/{category_id}", name="admin_product_sort_sort_no_move", methods={"POST"})
     */
    public function moveSortNo(Request $request, CacheUtil $cacheUtil, $category_id = null)
    {
        if (!$request->isXmlHttpRequest()) {
            throw new BadRequestHttpException();
        }

        if ($this->isTokenValid()) {
            $sortNos = $request->request->all();
            foreach ($sortNos as $id => $sortNo) {
                $ProductCategory = $this->productCategoryRepository
                    ->findOneBy(['product_id' => $id, 'category_id' => $category_id]);
                $ProductCategory->setSortNo($sortNo);
                $this->entityManager->persist($ProductCategory);
            }
            $this->entityManager->flush();

            $cacheUtil->clearDoctrineCache();

            return new Response('Successful');
        }
    }

}
  • src/Eccube/Form/Type/Admin/ProductCategoryType.php
<?php

namespace Eccube\Form\Type\Admin;

use Eccube\Common\EccubeConfig;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;

class ProductCategoryType extends AbstractType
{
    /**
     * @var EccubeConfig
     */
    protected $eccubeConfig;

    /**
     * ProductCategoryType constructor.
     *
     * @param EccubeConfig $eccubeConfig
     */
    public function __construct(EccubeConfig $eccubeConfig)
    {
        $this->eccubeConfig = $eccubeConfig;
    }

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('sort_no', IntegerType::class, [
                'label' => '並び順(数字が大きいほど先に表示)',
                'required' => false,
                'constraints' => [
                    new Assert\Length([
                        'max' => $this->eccubeConfig['eccube_int_len'],
                    ]),
                    new Assert\Regex([
                        'pattern' => '/^[0-9\-]+$/u',
                        'message' => 'form_error.numeric_only',
                    ]),
                ],
            ]);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'Eccube\Entity\ProductCategory',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'admin_product_category';
    }
}
  • app/template/admin/Product/product_sort.twig
{% extends '@admin/default_frame.twig' %}

{% set menus = ['product', 'product_sort'] %}

{% block title %}商品並び替え{% endblock %}
{% block sub_title %}{{ 'admin.product.product_management'|trans }}{% endblock %}

{% form_theme form '@admin/Form/bootstrap_4_horizontal_layout.html.twig' %}

{% block javascript %}
    <script src="{{ asset('assets/js/vendor/jquery.ui/jquery.ui.core.min.js', 'admin') }}"></script>
    <script src="{{ asset('assets/js/vendor/jquery.ui/jquery.ui.widget.min.js', 'admin') }}"></script>
    <script src="{{ asset('assets/js/vendor/jquery.ui/jquery.ui.mouse.min.js', 'admin') }}"></script>
    <script src="{{ asset('assets/js/vendor/jquery.ui/jquery.ui.sortable.min.js', 'admin') }}"></script>
    <script>
        $(function() {
            // 初期表示時のsort noを保持.
            var oldSortNos = [];
            $('.sortable-item').each(function() {
                oldSortNos.push(this.dataset.sortNo);
            });
            oldSortNos.sort(function(a, b) {
                return a - b;
            }).reverse();
            // 並び替え後にsort noを更新
            var updateSortNo = function() {
                var newSortNos = {};
                var i = 0;
                $('.sortable-item').each(function() {
                    newSortNos[this.dataset.id] = oldSortNos[i];
                    $('.sort-no', $(this)).text(oldSortNos[i]);
                    i++;
                });
                $.ajax({
                    url: '{{ url('admin_product_sort_sort_no_move', {'category_id': parent_id}) }}',
                    type: 'POST',
                    data: newSortNos
                }).always(function() {
                    $(".modal-backdrop").remove();
                });
            };
            // 最初と最後の↑↓を再描画
            var redrawDisableAllows = function() {
                var items = $('.sortable-item');
                items.find('a.action-up').removeClass('disabled');
                items.find('a.action-down').removeClass('disabled');
                items.first().find('a.action-up').addClass('disabled');
                items.last().find('a.action-down').addClass('disabled');
            };
            // オーバレイ後, 表示順の更新を行う
            var moveSortNo = function() {
                $('body').append($('<div class="modal-backdrop show"></div>'));
                updateSortNo();
                redrawDisableAllows();
            };
            // Drag and Drop
            $('.sortable-container').sortable({
                items: '> .sortable-item',
                cursor: 'move',
                update: function(e, ui) {
                    moveSortNo();
                }
            });
            // Up
            $('.sortable-item').on('click', 'a.action-up', function(e) {
                e.preventDefault();
                var current = $(this).parents("li");
                if (current.prev().hasClass('sortable-item')) {
                    current.prev().before(current);
                    moveSortNo();
                }
            });
            // Down
            $('.sortable-item').on('click', 'a.action-down', function(e) {
                e.preventDefault();
                var current = $(this).parents("li");
                if (current.next().hasClass('sortable-item')) {
                    current.next().after(current);
                    moveSortNo();
                }
            });

            var groupItem = $('.list-group-item');
            groupItem.on('click', 'a.action-edit', function(e) {
                e.preventDefault();
                var current = $(this).parents('li');
                current.find('.mode-view').addClass('d-none');
                current.find('.mode-edit').removeClass('d-none');
            });

            groupItem.on('click', 'button.action-edit-cancel', function(e) {
                e.preventDefault();
                var current = $(this).parents('li');
                current.find('[data-origin-value]').each(function(e) {
                    $(this).val($(this).attr('data-origin-value'));
                });
                current.find('.mode-view').removeClass('d-none');
                current.find('.mode-edit').addClass('d-none');
            });

            groupItem.find('.is-invalid').each(function(e) {
                e.preventDefault();
                var current = $(this).parents("li");
                current.find('.mode-view').addClass('d-none');
                current.find('.mode-edit').removeClass('d-none');
            });

        });
    </script>
{% endblock %}

{% block stylesheet %}
    {#TODO: Move to css file#}
    <style>
        .c-directoryTree ul > li > ul li:not(:last-of-type) > label:before,
        .c-directoryTree ul > li > ul li:last-of-type > label:before {
            margin-right: 1.6em;
        }
    </style>
{% endblock %}

{% block main %}
    <div class="c-outsideBlock">
        <div class="c-outsideBlock__contents mb-2">
            <div class="row">
                <div class="col-6">
                    <nav aria-label="breadcrumb" role="navigation">
                        <ol class="breadcrumb mb-2 p-0">
                            <li class="breadcrumb-item">
                                <a href="{{ url('admin_product_sort') }}">
                                    {{ 'admin.product.category_all'|trans }}
                                </a>
                            </li>
                            {% for ParentCategory in TargetCategory.path %}
                                {% if ParentCategory.id is not null %}
                                    <li class="breadcrumb-item active" aria-current="page">
                                        <a href="{{ url('admin_product_sort_show', { parent_id : ParentCategory.id }) }}">
                                            {{ ParentCategory.name }}
                                        </a>
                                    </li>
                                {% endif %}
                            {% endfor %}
                        </ol>
                    </nav>
                </div>
            </div>
        </div>
    </div>
    <div class="c-contentsArea__cols">
        <div class="c-contentsArea__primaryCol">
            <div id="ex-primaryCol" class="c-primaryCol">
                <div class="card rounded border-0 mb-4">
                    <div class="card-body p-0">
                        <div class="card rounded border-0">
                            <ul class="list-group list-group-flush sortable-container">
                                <li class="list-group-item">
                                    <div class="row">
                                        <div class="col-1"><strong>&nbsp;</strong></div>
                                        <div class="col-1"><strong>ID</strong></div>
                                        <div class="col-2"><strong>コード</strong></div>
                                        <div class="col-3"><strong>商品名</strong></div>
                                        <div class="col-1"><strong>価格</strong></div>
                                        <div class="col-1"><strong>順番</strong></div>
                                        <div class="col-3"><strong>&nbsp;</strong></div>
                                    </div>
                                </li>
                                {% if ProductCategories|length > 0 %}
                                    {% for ProductCategory in ProductCategories %}
                                        <li id="ex-category-{{ ProductCategory.product_id }}" class="list-group-item sortable-item" data-id="{{ ProductCategory.product_id }}" data-sort-no="{{ ProductCategory.sort_no }}">
                                            {% if ProductCategory.category_id != TargetCategory.id %}
                                                <div class="row justify-content-around mode-view">
                                                    <div class="col-1 d-flex align-items-center"><i class="fa fa-bars text-ec-gray"></i></div>
                                                    <div class="col-1 d-flex align-items-center">{{ ProductCategory.product_id }}</div>
                                                    <div class="col-2 d-flex align-items-center">
                                                        {{ ProductCategory.Product.code_min }}
                                                        {% if ProductCategory.Product.code_min != ProductCategory.Product.code_max %}<br>{{ 'admin.common.separator__range'|trans }}<br>{{ ProductCategory.Product.code_max }}
                                                        {% endif %}
                                                    </div>
                                                    <div class="col-3 d-flex align-items-center">
                                                        <a href="{{ url('admin_product_product_edit',  {'id': ProductCategory.product_id}) }}">{{ ProductCategory.Product.name }}</a>
                                                    </div>
                                                    <div class="col-1 d-flex align-items-center">
                                                        {{ ProductCategory.Product.price02_min|price }}
                                                        {% if ProductCategory.Product.price02_min != ProductCategory.Product.price02_max %}<br>{{ 'admin.common.separator__range'|trans }}<br>{{ ProductCategory.Product.price02_max|price }}
                                                        {% endif %}
                                                    </div>
                                                    <div class="col-1 d-flex align-items-center sort-no">{{ ProductCategory.sort_no }}</div>
                                                    <div class="col-3 text-right">
                                                        <a class="btn btn-ec-actionIcon action-up mr-2 {% if loop.first %} disabled {% endif %}" href=""
                                                           data-tooltip="true" data-placement="top"
                                                           title="{{ 'admin.common.up'|trans }}">
                                                            <i class="fa fa-arrow-up fa-lg text-secondary"></i>
                                                        </a>
                                                        <a class="btn btn-ec-actionIcon action-down mr-2 {% if loop.last %} disabled {% endif %}" href=""
                                                           data-tooltip="true" data-placement="top"
                                                           title="{{ 'admin.common.down'|trans }}">
                                                            <i class="fa fa-arrow-down fa-lg text-secondary"></i>
                                                        </a>
                                                        <a class="btn btn-ec-actionIcon mr-2 action-edit"
                                                           href="{{ url('admin_product_category_edit', {id: ProductCategory.product_id}) }}"
                                                           data-tooltip="true" data-placement="top"
                                                           title="{{ 'admin.common.edit'|trans }}">
                                                            <i class="fa fa-pencil fa-lg text-secondary"></i>
                                                        </a>
                                                    </div>
                                                </div>
                                                <form class="form-row d-none mode-edit" method="POST" action="{{ (Parent and Parent.id) ? url('admin_product_sort_show', {'parent_id': Parent.id}) : url('admin_product_sort') }}" enctype="multipart/form-data">
                                                    {{ form_widget(forms[ProductCategory.product_id]._token) }}
                                                    <div class="col-auto align-items-center">
                                                        {{ form_widget(forms[ProductCategory.product_id].sort_no, {'attr': {'data-origin-value': forms[ProductCategory.product_id].sort_no.vars.value}}) }}
                                                        {{ form_errors(forms[ProductCategory.product_id].sort_no) }}
                                                    </div>
                                                    <div class="col-auto align-items-center">
                                                        <button class="btn btn-ec-conversion" type="submit">{{ 'admin.common.decision'|trans }}</button>
                                                    </div>
                                                    <div class="col-auto align-items-center">
                                                        <button class="btn btn-ec-sub action-edit-cancel" type="button">{{ 'admin.common.cancel'|trans }}</button>
                                                    </div>
                                                </form>
                                            {% endif %}
                                        </li>
                                    {% endfor %}
                                {% else %}
                                    <li class="list-group-item">
                                        カテゴリを選択してください。
                                    </li>
                                {% endif %}
                            </ul>

                        </div>
                    </div>
                </div>
                <p>{{ 'admin.common.drag_and_drop_description'|trans }}</p>
            </div>
        </div>

        {% macro tree(Category, TargetId, level, Ids) %}
            {% import _self as selfMacro %}
            {% set level = level + 1 %}
            <li>
                <label {% if (Category.children|length > 0) and (Category.id not in Ids) %}class="collapsed"
                       {% endif %}data-toggle="collapse"
                       href="#directory_category{{ Category.id }}"
                       aria-expanded="{% if Category.id in Ids %}true{% endif %}"
                       aria-controls="directory_category{{ Category.id }}"></label>
                <span>
                    <a href="{{ url('admin_product_sort_show', { parent_id : Category.id }) }}"{% if (Category.id == TargetId) %} class="font-weight-bold"{% endif %}>{{ Category.name }}
                        ({{ Category.children|length }})</a></span>
                {% if Category.children|length > 0 %}
                    <ul class="collapse list-unstyled {% if Category.id in Ids %}show{% endif %}"
                        id="directory_category{{ Category.id }}">
                        {% for ChildCategory in Category.children %}
                            {{ selfMacro.tree(ChildCategory, TargetId, level, Ids) }}
                        {% endfor %}
                    </ul>
                {% endif %}
            </li>
        {% endmacro %}

        <div class="c-contentsArea__secondaryCol">
            <div id="ex-secondaryCol" class="c-secondaryCol">
                <div class="card rounded border-0 mb-4">
                    <div class="card-header">
                        <span class="card-title"><a href="{{ url('admin_product_sort') }}">{{ 'admin.product.category_all'|trans }}</a></span>
                    </div>
                    <div class="card-body">
                        <div class="c-directoryTree mb-3">
                            {% import _self as renderMacro %}
                            {% for TopCategory in TopCategories %}
                                <ul class="list-unstyled">
                                    {{ renderMacro.tree(TopCategory, TargetCategory.Parent.id | default(null), 0, Ids) }}
                                </ul>
                            {% endfor %}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

それぞれファイルを作成後、管理画面のメニューを表示させるために以下のファイルを修正します。

  • app/config/eccube/packages/eccube_nav.yaml
parameters:
    eccube_nav:
        product:
            name: admin.product.product_management
            icon: fa-cube
            children:
                product_master:
                    name: admin.product.product_list
                    url: admin_product
                product_edit:
                    name: admin.product.product_registration
                    url: admin_product_product_new
                class_name:
                    name: admin.product.class_management
                    url: admin_product_class_name
                class_category:
                    name: admin.product.category_management
                    url: admin_product_category
                product_tag:
                    name: admin.product.tag_management
                    url: admin_product_tag
                product_csv_import:
                    name: admin.product.product_csv_upload
                    url: admin_product_csv_import
                category_csv_import:
                    name: admin.product.category_csv_upload
                    url: admin_product_category_csv_import
                product_sort:
                    name: 商品並び替え
                    url: admin_product_sort

eccube_nav.yamlファイルに対して、

product_sort:
    name: 商品並び替え
    url: admin_product_sort

を追加します。

以上で商品並び替え管理画面が作成されます。

ProductRepositoryの変更

商品並び替え機能を動作させるために、ProductRepositoryの変更を行います。

  • src/Eccube/Repository/ProductRepository.php
<?php
〜
〜
〜

/**
 * get query builder.
 *
 * @param  array $searchData
 *
 * @return \Doctrine\ORM\QueryBuilder
 */
public function getQueryBuilderBySearchData($searchData)
{
〜
〜
〜
    // category
    $categoryJoin = false;
    if (!empty($searchData['category_id']) && $searchData['category_id']) {
        $Categories = $searchData['category_id']->getSelfAndDescendants();
        if ($Categories) {
            $qb
                ->innerJoin('p.ProductCategories', 'pct')
                ->innerJoin('pct.Category', 'c')
                ->andWhere($qb->expr()->in('pct.Category', ':Categories'))
                ->setParameter('Categories', $Categories)
                ->addOrderBy('c.hierarchy', 'DESC')
                ->addOrderBy('c.sort_no', 'DESC')
                ->addOrderBy('pct.sort_no', 'DESC');
            $categoryJoin = true;
        }
    }
〜
〜
〜
    // Order By
    // 標準
    if ($categoryJoin === false) {
        $qb
            ->leftJoin('p.ProductCategories', 'pct')
            ->leftJoin('pct.Category', 'c');

        $qb->addOrderBy('c.sort_no', 'DESC');
        $qb->addOrderBy('pct.sort_no', 'DESC');
        $qb->addOrderBy('c.hierarchy', 'ASC');
    }
    // 価格低い順
    $config = $this->eccubeConfig;
    if (!empty($searchData['orderby']) && $searchData['orderby']->getId() == $config['eccube_product_order_price_lower']) {
〜
〜
        $qb->addOrderBy('price02_min', 'ASC');
〜
〜
        // 価格高い順
    } elseif (!empty($searchData['orderby']) && $searchData['orderby']->getId() == $config['eccube_product_order_price_higher']) {
〜
〜
        $qb->addOrderBy('price02_max', 'DESC');
〜
〜
        // 新着順
    } elseif (!empty($searchData['orderby']) && $searchData['orderby']->getId() == $config['eccube_product_order_newer']) {
〜
〜
        $qb->addOrderBy('p.create_date', 'DESC');
〜
〜

こちらの修正では何を行っているかというと、dtb_product_categoryテーブルに追加したsort_noをソート順として追加して並び替えするように変更し、$qb->orderByから$qb->addOrderByに修正を行なっています。

ProductControllerの変更

検索結果が表示されるようにProdutControllerの変更を行います。変更箇所は以下の通りです。

  • src/Eccube/Controller/ProductController.php
<?php
〜
〜
/** @var SlidingPagination $pagination */
$pagination = $paginator->paginate(
    $query,
    !empty($searchData['pageno']) ? $searchData['pageno'] : 1,
    !empty($searchData['disp_number']) ? $searchData['disp_number']->getId() : $this->productListMaxRepository->findOneBy([], ['sort_no' => 'ASC'])->getId(),
    ['wrap-queries' => true]
);
〜
〜

index関数内にある$pagination変数に設定する値に対して、['wrap-queries' => true]を追加しています。

管理画面用ProductControllerの変更

管理画面で商品登録された時にも修正が必要ですので、以下の修正を行います。

  • src/Eccube/Controller/Admin/Product/ProductController.php
<?php
〜
〜
// 追加
$ProductCategories = clone $Product->getProductCategories();

// カテゴリの登録
// 一度クリア
〜
〜
〜
〜
    if (!isset($categoriesIdList[$ParentCategory->getId()])) {
        // 引数を追加
        $ProductCategory = $this->createProductCategory($Product, $ParentCategory, $count, $ProductCategories);
〜
〜
    if (!isset($categoriesIdList[$Category->getId()])) {
        // 引数を追加
        $ProductCategory = $this->createProductCategory($Product, $Category, $count, $ProductCategories);
〜
〜
〜
〜
/**
 * ProductCategory作成
 *
 * @param \Eccube\Entity\Product $Product
 * @param \Eccube\Entity\Category $Category
 * @param integer $count
 * @param $ProductCategories
 *
 * @return \Eccube\Entity\ProductCategory
 */
private function createProductCategory($Product, $Category, $count, $ProductCategories)
{
    $ProductCategory = new ProductCategory();
    $ProductCategory->setProduct($Product);
    $ProductCategory->setProductId($Product->getId());
    $ProductCategory->setCategory($Category);
    $ProductCategory->setCategoryId($Category->getId());

    /** @var ProductCategory $item */
    foreach ($ProductCategories as $item) {
        if ($item->getCategory()->getId() == $Category->getId()) {
            $ProductCategory->setSortNo($item->getSortNo());
            break;
        } else {
            $ProductCategory->setSortNo($count);
        }
    }

    return $ProductCategory;
}

edit関数内で、sort_noを追加したことによる修正を行います。

以上で商品並び替え機能が実装できます。商品CSV登録にも対応が必要となりますが、管理画面の商品登録時と同じような実装をsrc/Eccube/Controller/Admin/Product/CsvImportController.phpに対して行えば対応可能です。

もし動作しないとかありましたらコメントしてください。