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> </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> </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に対して行えば対応可能です。
もし動作しないとかありましたらコメントしてください。