以下カスタマイズの続き・ソート(並び替え)機能までを含んだ実装方法(コード)を紹介します。
- 管理画面 / 商品管理のカスタマイズ(1):商品登録に新メニュー【メーカー管理】を追加する
- 管理画面 / 商品管理のカスタマイズ(2):【メーカー管理】から新規メーカーを追加できるようにする
- 管理画面 / 商品管理のカスタマイズ(3):【メーカー管理】から追加済メーカーの編集・削除ができるようにする
デバッグモードを設定しておくと、エラーが起きたときに詳細情報が表示されるようになります。
エラー箇所を探しやすくなるので、開発前に設定しておくのをオススメします。
デバッグモードの設定方法については 以下記事 で解説しています。
カスタマイズ後は、デバッグモードの解除を忘れないように。
カスタマイズの流れ
- MakerEntityの修正~テーブル拡張(プロパティ
sort_no
の追加) - MakerRepositoryの修正(
sort_no
関連の処理を追加) - MakerControllerの修正(
sort_no
を変更して保存するmoveSortNo()
メソッドの追加など) - maker.twigの修正(上下矢印の追加、表示順を入れ替えるJavaScriptの追加、など)
MakerEntityを修正し、dtb_makerテーブルを拡張する
表示順を格納しておくプロパティsort_no
を「dtb_maker」に追加します。
テーブルを新規作成するときと同様に、以下修正後のエンティティをアップしてからSSHにてコマンドを入力し、テーブルを拡張します。
<?php
namespace Customize\Entity;
use Eccube\Entity\AbstractEntity;
use Doctrine\ORM\Mapping as ORM;
/**
* Maker
*
* @ORM\Table(name="dtb_maker")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="discriminator_type", type="string", length=255)
* @ORM\HasLifecycleCallbacks()
* @ORM\Entity(repositoryClass="Customize\Repository\MakerRepository")
*/
class Maker extends AbstractEntity
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer", options={"unsigned":true})
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*
* メーカーの一意識別ID(自動連番)です。
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255)
*
* メーカーの名前です。
*/
private $name;
/**
* @var string|null
*
* @ORM\Column(name="code", type="string", length=8, nullable=true)
*
* メーカーのコードです。null(未入力)も許容されます。
*/
private $code;
/**
* @var \DateTime
*
* @ORM\Column(name="create_date", type="datetimetz")
*
* メーカー情報の作成日時です。
*/
private $create_date;
/**
* @var \DateTime
*
* @ORM\Column(name="update_date", type="datetimetz")
*
* メーカー情報の最終更新日時です。
*/
private $update_date;
/**
* @var int
*
* @ORM\Column(name="sort_no", type="integer", options={"unsigned":true})
*
* 並び順です。
*/
private $sort_no;
/**
* Get id.
*
* @return int
*
* メーカーのIDを取得します。
*/
public function getId()
{
return $this->id;
}
/**
* Set name.
*
* @param string $name
*
* @return Maker
*
* メーカーの名前を設定します。
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*
* メーカーの名前を取得します。
*/
public function getName()
{
return $this->name;
}
/**
* Set code.
*
* @param string|null $code
*
* @return Maker
*
* メーカーのコードを設定します。null(未入力)も許容されます。
*/
public function setCode($code = null)
{
$this->code = $code;
return $this;
}
/**
* Get code.
*
* @return string|null
*
* メーカーのコードを取得します。
*/
public function getCode()
{
return $this->code;
}
/**
* Set createDate.
*
* @param \DateTime $createDate
*
* @return Maker
*
* メーカー情報の作成日時を設定します。
*/
public function setCreateDate($createDate)
{
$this->create_date = $createDate;
return $this;
}
/**
* Get createDate.
*
* @return \DateTime
*
* メーカー情報の作成日時を取得します。
*/
public function getCreateDate()
{
return $this->create_date;
}
/**
* Set updateDate.
*
* @param \DateTime $updateDate
*
* @return Maker
*
* メーカー情報の最終更新日時を設定します。
*/
public function setUpdateDate($updateDate)
{
$this->update_date = $updateDate;
return $this;
}
/**
* Get updateDate.
*
* @return \DateTime
*
* メーカー情報の最終更新日時を取得します。
*/
public function getUpdateDate()
{
return $this->update_date;
}
/**
* Set sortNo.
*
* @param int $sortNo
*
* @return Maker
*/
public function setSortNo($sortNo)
{
$this->sort_no = $sortNo;
return $this;
}
/**
* Get sortNo.
*
* @return int
*/
public function getSortNo()
{
return $this->sort_no;
}
}
以降のステップで、メーカーはsort_no
の大きい順に表示されるようになります。ただし、すでに登録済のメーカーに対してはsort_no
が設定されていない または 0になっていると思いますので、1から連番となるよう数値を入力しておきます。(今後、管理画面から新規で追加されるメーカーには自動で数値が割り振られるので、データベースへのアクセスは不要です。)
MakerRepositoryを修正し、sort_no関連の処理を追加する
- メーカーの取得順を
sort_no
の大きい順(降順=DESC)に - メーカーを保存するとき、
sort_no
が自動でセットされるように - メーカーを削除するとき、他の保存済メーカーの
sort_no
を詰めるように
各メソッドを修正します。ちなみに参考にしたファイルはClassNameRepository
(src/Eccube/Repository)です。
<?php
/*
* This file is part of EC-CUBE
*
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
*
* http://www.ec-cube.co.jp/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Customize\Repository;
use Doctrine\Persistence\ManagerRegistry as RegistryInterface;
use Customize\Entity\Maker;
use Eccube\Repository\AbstractRepository;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
/**
* MakerRepository
*/
class MakerRepository extends AbstractRepository
{
/**
* MakerRepository constructor.
*
* @param RegistryInterface $registry
*/
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Maker::class);
}
/**
* メーカー一覧を取得する.
*
* @return array
*/
public function getList()
{
$qb = $this->createQueryBuilder('cn')
->orderBy('cn.sort_no', 'DESC');
$Makers = $qb->getQuery()
->getResult();
return $Makers;
}
/**
* メーカーを保存する.
*
* @param Maker $maker
*/
public function save($maker)
{
if (!$maker->getId()) {
$sortNo = $this->createQueryBuilder('cn')
->select('COALESCE(MAX(cn.sort_no), 0)')
->getQuery()
->getSingleScalarResult();
$maker->setSortNo($sortNo + 1);
}
$em = $this->getEntityManager();
$em->persist($maker);
$em->flush();
}
/**
* メーカーを削除する.
*
* @param Maker $maker
*
* @throws ForeignKeyConstraintViolationException 外部キー制約違反の場合
* @throws DriverException SQLiteの場合, 外部キー制約違反が発生すると, DriverExceptionをthrowします.
*/
public function delete($maker)
{
$sortNo = $maker->getSortNo();
$this->createQueryBuilder('cn')
->update()
->set('cn.sort_no', 'cn.sort_no - 1')
->where('cn.sort_no > :sort_no')
->setParameter('sort_no', $sortNo)
->getQuery()
->execute();
$em = $this->getEntityManager();
$em->remove($maker);
$em->flush();
}
}
MakerControllerを修正し、メーカーの取得方法とmoveSortNo()メソッドを追加
- 保存済メーカーの取得は、STEP 2で新たに追加したメソッド
getList()
を使い、sort_no
が降順で取得されるようにします。- これにより、表示されるメーカーリストが
sort_no
の大きい順になります。
- これにより、表示されるメーカーリストが
- リストを並び替えたときの処理用のメソッド
moveSortNo()
を追加します。
ちなみに参考にしたファイルはClassNameController
(src/Eccube/Controller/Admin/Product)です。
<?php
/*
* This file is part of EC-CUBE
*
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
*
* http://www.ec-cube.co.jp/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Customize\Controller\Admin\Product;
use Eccube\Controller\AbstractController;
use Customize\Entity\Maker;
use Customize\Form\Type\Admin\MakerType;
use Customize\Repository\MakerRepository;
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 MakerController extends AbstractController
{
/**
* @var MakerRepository
*/
protected $makerRepository;
/**
* MakerController constructor.
*
* @param MakerRepository $makerRepository
*/
public function __construct(MakerRepository $makerRepository)
{
$this->makerRepository = $makerRepository;
}
/**
* @Route("/%eccube_admin_route%/product/maker", name="admin_product_maker")
* @Template("@admin/Product/maker.twig")
*/
public function index(Request $request)
{
$makers = $this->makerRepository->getList();
$newMaker = new Maker();
$builder = $this->formFactory
->createBuilder(MakerType::class, $newMaker);
$form = $builder->getForm();
/**
* 編集用フォーム
*/
$forms = [];
foreach ($makers as $maker) {
$id = $maker->getId();
$forms[$id] = $this->formFactory->createNamed('maker_'.$id, MakerType::class, $maker);
}
/**
* 新規登録処理
*/
if ($request->getMethod() === 'POST') {
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->makerRepository->save($newMaker);
$this->addSuccess('admin.common.save_complete', 'admin');
return $this->redirectToRoute('admin_product_maker');
}
}
/*
* 編集処理
*/
foreach ($forms as $editForm) {
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->makerRepository->save($editForm->getData());
$this->addSuccess('admin.common.save_complete', 'admin');
return $this->redirectToRoute('admin_product_maker');
}
}
$formViews = [];
foreach ($forms as $key => $value) {
$formViews[$key] = $value->createView();
}
return [
'form' => $form->createView(),
'makers' => $makers,
'newMaker' => $newMaker,
'forms' => $formViews,
];
}
/**
* @Route("/%eccube_admin_route%/product/maker/{id}/delete", requirements={"id" = "\d+"}, name="admin_product_maker_delete", methods={"DELETE"})
*/
public function delete(Request $request, Maker $maker)
{
$this->isTokenValid();
try {
$this->makerRepository->delete($maker);
$this->addSuccess('admin.common.delete_complete', 'admin');
} catch (\Exception $e) {
$message = trans('admin.common.delete_error_foreign_key', ['%name%' => $maker->getName()]);
$this->addError($message, 'admin');
}
return $this->redirectToRoute('admin_product_maker');
}
/**
* @Route("/%eccube_admin_route%/product/maker/sort_no/move", name="admin_product_maker_sort_no_move", methods={"POST"})
*/
public function moveSortNo(Request $request)
{
if (!$request->isXmlHttpRequest() && $this->isTokenValid()) {
throw new BadRequestHttpException();
}
if ($this->isTokenValid()) {
$sortNos = $request->request->all();
foreach ($sortNos as $makerId => $sortNo) {
$Maker = $this->makerRepository
->find($makerId);
$Maker->setSortNo($sortNo);
$this->entityManager->persist($Maker);
}
$this->entityManager->flush();
return new Response();
}
}
}
maker.twigを修正し、リストの入れ替え機能を実装する
- 順番を入れ替えられる、上下矢印のアイコンを設置します。
- リストをドラッグ&ドロップしたとき および 上下矢印アイコンをクリックしたときの処理を、JavaScript(JQuery)で記述します。
ちなみに参考にしたファイルはclass_name.twig
(src/Eccube/Resource/template/admin/Product)です。
{#
This file is part of EC-CUBE
Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
http://www.ec-cube.co.jp/
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
#}
{% extends '@admin/default_frame.twig' %}
{% set menus = ['product', 'maker'] %}
{% block title %}{{ 'admin.product.maker_management'|trans }}{% endblock %}
{% block sub_title %}{{ 'admin.product.product_management'|trans }}{% endblock %}
{% form_theme form '@admin/Form/bootstrap_4_horizontal_layout.html.twig' %}
{% block stylesheet %}
<style type="text/css">
.list-group-item:hover {
z-index: inherit;
}
</style>
{% endblock stylesheet %}
{% block javascript %}
<script>
$(function() {
var oldSortNos = [];
// 画面の中のsortNo一覧を保持
$('.sortable-item').each(function() {
oldSortNos.push(this.dataset.sortNo);
});
// rsort
oldSortNos.sort(function(a, b) {
return a - b;
}).reverse();
$('.sortable-container').sortable({
items: '> .sortable-item',
cursor: 'move',
update: function(e, ui) {
$('body').append($('<div class="modal-backdrop show"></div>'));
updateSortNo();
}
});
var updateSortNo = function() {
// 並び替え後にsortNoを更新
var newSortNos = {};
var i = 0;
$('.sortable-item').each(function() {
newSortNos[this.dataset.makerId] = oldSortNos[i];
i++;
});
$.ajax({
url: '{{ url('admin_product_maker_sort_no_move') }}',
type: 'POST',
data: newSortNos
}).done(function() {
// remove class disable
$('a.up.disabled').removeClass('disabled');
$('a.down.disabled').removeClass('disabled');
// First element
$('.sortable-item > li:nth-child(2) > div > div.col-auto.text-end > a.up').addClass('disabled');
}).always(function() {
redrawDisableAllows();
$('.modal-backdrop').remove();
});
};
// 最初と最後の↑↓を再描画
var redrawDisableAllows = function() {
var items = $('.sortable-item');
items.find('a.up').removeClass('disabled');
items.find('a.down').removeClass('disabled');
items.first().find('a.up').addClass('disabled');
items.last().find('a.down').addClass('disabled');
};
$('.sortable-item a.up').click(function(e) {
e.preventDefault();
var current = $(this).parents('.list-group-item');
current.prev().before(current);
$('body').append($('<div class="modal-backdrop show"></div>'));
updateSortNo();
});
$('.sortable-item a.down').click(function(e) {
e.preventDefault();
var current = $(this).parents('.list-group-item');
current.next().after(current);
$('body').append($('<div class="modal-backdrop show"></div>'));
updateSortNo();
});
// 編集
$('.sortable-item').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');
});
// 編集キャンセル
$('.sortable-item').on('click', 'button.action-edit-cancel', function(e) {
location.href = '{{ url('admin_product_maker') }}';
});
// 編集時, エラーがあれば入力欄を表示.
$('.sortable-item').find('.is-invalid').each(function(e) {
var current = $(this).parents('li');
current.find('.mode-view').addClass('d-none');
current.find('.mode-edit').removeClass('d-none');
});
// 削除モーダルのhrefとmessageの変更
$('#DeleteModal').on('shown.bs.modal', function(event) {
var target = $(event.relatedTarget);
// hrefの変更
$(this).find('[data-method="delete"]').attr('href', target.data('url'));
// messageの変更
$(this).find('p.modal-message').text(target.data('message'));
});
});
</script>
{% endblock %}
{% block main %}
<div class="c-contentsArea__cols">
<div class="c-contentsArea__primaryCol">
<div 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">
<form role="form" class="row" name="form1" id="form1" method="post" action="{{ url('admin_product_maker') }}">
<div class="col-auto align-self-center"><span>{{ 'admin.product.maker.name'|trans }}</span></div>
<div class="col-3 me-2">
{{ form_widget(form._token) }}
{{ form_widget(form.name) }}
{{ form_errors(form.name) }}
</div>
<div class="col-auto align-self-center" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ 'tooltip.product.code'|trans }}">
<span>{{ 'admin.product.maker.code'|trans }}</span>
</div>
<div class="col-3">
{{ form_widget(form.code) }}
{{ form_errors(form.code) }}
</div>
<div class="col-auto">
<button class="btn btn-ec-regular" type="submit">{{ 'admin.common.create__new'|trans }}</button>
</div>
</form>
</li>
<li class="list-group-item">
<div class="row">
<div class="col-auto"><strong> </strong></div>
<div class="col-auto"><strong>{{ 'admin.common.id'|trans }}</strong></div>
<div class="col-1"><strong>{{ 'admin.product.maker_management'|trans }}</strong></div>
</div>
</li>
{% for maker in makers %}
<li id="ex-class_name-{{ maker.id }}" class="list-group-item sortable-item" data-maker-id="{{ maker.id }}" data-sort-no="{{ maker.sortNo }}">
<div class="row justify-content-around mode-view">
<div class="col-auto d-flex align-items-center"><i class="fa fa-bars text-ec-gray"></i></div>
<div class="col-auto d-flex align-items-center">{{ maker.id }}</div>
<div class="col d-flex align-items-center">{{ maker.name }}</div>
{# 矢印アイコンと編集アイコンと削除アイコンを設置 #}
<div class="col-auto text-end">
<a class="btn btn-ec-actionIcon me-2 up {% if loop.first %}disabled{% endif %}" href="" data-bs-toggle="tooltip" data-bs-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 me-2 down {% if loop.last %}disabled{% endif %}" href="" data-bs-toggle="tooltip" data-bs-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 me-2 action-edit" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ 'admin.common.edit'|trans }}">
<i class="fa fa-pencil fa-lg text-secondary"></i>
</a>
<div class="d-inline-block me-2" data-bs-toggle="tooltip"
data-bs-placement="top" title="{{ 'admin.common.delete'|trans }}">
<a class="btn btn-ec-actionIcon" data-bs-toggle="modal" data-bs-target="#DeleteModal" data-url="{{ url('admin_product_maker_delete', {'id' : maker.id}) }}" data-message="{{ 'admin.common.delete_modal__message'|trans({ "%name%" : maker.name }) }}">
<i class="fa fa-close fa-lg text-secondary"></i>
</a>
</div>
</div>
</div>
{# 編集用のフォームを設置 #}
<form class="row d-none mode-edit" method="post" action="{{ url('admin_product_maker') }}">
{{ form_widget(forms[maker.id]._token) }}
<div class="col-auto align-self-center"><span>{{ 'admin.product.maker.name'|trans }}</span></div>
<div class="col-auto align-items-center">
{{ form_widget(forms[maker.id].name, {'attr': {'data-origin-value': forms[maker.id].name.vars.value}}) }}
{{ form_errors(forms[maker.id].name) }}
</div>
<div class="col-auto align-self-center"><span>{{ 'admin.product.maker.code'|trans }}</span></div>
<div class="col-auto align-items-center">
{{ form_widget(forms[maker.id].code, {'attr': {'data-origin-value': forms[maker.id].code.vars.value}}) }}
{{ form_errors(forms[maker.id].code) }}
</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>
</li>
{% endfor %}
</ul>
<!-- 削除モーダル -->
<div class="modal fade" id="DeleteModal" tabindex="-1" role="dialog"
aria-labelledby="DeleteModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold">
{{ 'admin.common.delete_modal__title'|trans }}
</h5>
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<div class="modal-body text-start">
<p class="text-start modal-message"><!-- jsでメッセージを挿入 --></p>
</div>
<div class="modal-footer">
<button class="btn btn-ec-sub" type="button" data-bs-dismiss="modal">
{{ 'admin.common.cancel'|trans }}
</button>
<a class="btn btn-ec-delete" href="#" {{ csrf_token_for_anchor() }}
data-method="delete" data-confirm="false">
{{ 'admin.common.delete'|trans }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p>{{ 'admin.common.drag_and_drop_description'|trans }}</p>
</div>
</div>
</div>
{% endblock %}
このコードを流用する場合は、HTMLのIDやclass名なども合わせて修正しないと動作しないので注意!
キャッシュを削除
ここまでで準備完了です。
管理画面からキャッシュを削除して、商品管理メニューのメーカー管理を確認しましょう。
冒頭の動画のような【メーカー管理】メニューが追加され、管理画面からメーカーの新規追加および追加済メーカーの「編集」「削除」「表示順入れ替え」ができるようになっているはずです!