【EC-CUBE4】よくある質問ページを作ってみた(2)

目次

『4』よくある質問を管理画面にて登録・操作する

管理画面に「FAQ管理」というメニューを追加し、よくある質問の登録や編集、表示順の変更が行えるようにします。
実装後は以下動画の通り。

なお、管理画面のカスタマイズについては以下記事にて解説しています。

STEP

「app/config/eccube/packages/eccube_nav.yaml」の修正

「app/config/eccube/packages」にある「eccube_nav.yaml」で、ブロック管理blockの下(キャッシュ管理cacheの上)に以下コードを挿入します。

                faq:
                    name: admin.content.faq_management
                    url: admin_content_faq
STEP

「app/Customize/Resource/locale/messages.ja.yaml」の作成とアップロード

続いて、「app/Customize/Resource/locale」に以下のYAMLファイルをアップします。

admin.content.faq_management: FAQ管理
admin.content.faq.id: ID
admin.content.faq.question: 質問
admin.content.faq.answer: 答え
admin.content.faq.update_date: 更新日
admin.content.faq.faq_registration: FAQ登録
STEP

「app/Customize/Repository/Admin/Content/FaqRepository.php」の作成とアップロード

以下のリポジトリを作成し、「app/Customize/Repository/Admin/Content」にアップします。

<?php

namespace Customize\Repository\Admin\Content;

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

/**
 * FaqRepository
 */
class FaqRepository extends AbstractRepository
{
    /**
     * FaqRepository constructor.
     *
     * @param RegistryInterface $registry
     */
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Faq::class);
    }

    /**
     * FAQ一覧を取得する.
     *
     * @return array
     */
    public function getList()
    {
        $qb = $this->createQueryBuilder('cn')
            ->orderBy('cn.sort_no', 'DESC');
        $Faqs = $qb->getQuery()
            ->getResult();

        return $Faqs;
    }

     /**
     * FAQを保存する.
     *
     * @param Faq $faq
     */
    public function save($faq)
    {
        if (!$faq->getId()) {
          $sortNo = $this->createQueryBuilder('cn')
              ->select('COALESCE(MAX(cn.sort_no), 0)')
              ->getQuery()
              ->getSingleScalarResult();
          $faq->setSortNo($sortNo + 1);
        }

        $em = $this->getEntityManager();
        $em->persist($faq);
        $em->flush();
    }

    /**
     * FAQを削除する.
     *
     * @param Faq $faq
     * 
     * @throws ForeignKeyConstraintViolationException 外部キー制約違反の場合
     * @throws DriverException SQLiteの場合, 外部キー制約違反が発生すると, DriverExceptionをthrowします.
     */
    public function delete($faq)
    {
        $sortNo = $faq->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($faq);
        $em->flush();
    }
}

データベースに保存されているFAQの取得、保存(更新)、削除を行うメソッドが定義されています。

STEP

「Customize\Form\Type\Admin\FaqType.php」を作成とアップロード

以下のフォームタイプを作成し、「app/Customize/Form/Type/Admin」にアップします。

<?php

namespace Customize\Form\Type\Admin;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;
use Customize\Entity\FAQ;

class FaqType extends AbstractType
{
      public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('question', TextareaType::class, [
                'required' => true,
                'attr' => [
                    'rows' => 2,
                ],
                'constraints' => [
                    new Assert\NotBlank(),
                ],
            ])
            ->add('answer', TextareaType::class, [
                'required' => true,
                'attr' => [
                    'rows' => 2,
                ],
                'constraints' => [
                    new Assert\NotBlank(),
                ],
            ])
            ->add('status', CheckboxType::class, [
              'label'    => '表示',
              'required' => false,
              'data'     => true,
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => FAQ::class,
        ]);
    }

    public function getBlockPrefix()
    {
        return 'admin_faq';
    }
}

管理画面からFAQを登録・修正するためのフォームが定義されています。

STEP

「app/Customize/Controller/Admin/Content/FaqController.php」の作成とアップロード

以下のコントローラを作成し、「app/Customize/Controller/Admin/Content」にアップします。

<?php

namespace Customize\Controller\Admin\Content;

use Eccube\Controller\AbstractController;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Customize\Entity\FAQ;
use Customize\Repository\Admin\Content\FaqRepository;
use Customize\Form\Type\Admin\FaqType;

class FaqController extends AbstractController
{
    /**
     * @var FaqRepository
     */
    protected $faqRepository;

    /**
     * faqController constructor.
     *
     * @param FaqRepository $faqRepository
     */
    public function __construct(FaqRepository $faqRepository)
    {
        $this->faqRepository = $faqRepository;
    }

    /**
     * @Route("/%eccube_admin_route%/content/faq", name="admin_content_faq")
     * @Template("@admin/Content/faq.twig")
     */
    public function index(Request $request)
    {
        $faqs = $this->faqRepository->getList();

        $newfaq = new Faq();

        $builder = $this->formFactory
            ->createBuilder(FaqType::class, $newfaq);

        $form = $builder->getForm();

        /**
         * 編集用フォーム
         */
        $forms = [];
        foreach ($faqs as $faq) {
            $id = $faq->getId();
            $forms[$id] = $this->formFactory->createNamed('FAQ_'.$id, faqType::class, $faq);
        }

        /**
         * 新規登録処理
         */
        if ($request->getMethod() === 'POST') {
            $form->handleRequest($request);
            if ($form->isSubmitted() && $form->isValid()) {
                $this->faqRepository->save($newfaq);
                $this->addSuccess('admin.common.save_complete', 'admin');
                return $this->redirectToRoute('admin_content_faq');
            }
        }

        /*
         * 編集処理
         */
        foreach ($forms as $editForm) {
            $editForm->handleRequest($request);
            if ($editForm->isSubmitted() && $editForm->isValid()) {
                $this->faqRepository->save($editForm->getData());
                $this->addSuccess('admin.common.save_complete', 'admin');
                return $this->redirectToRoute('admin_content_faq');
            }
        }

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

        return [
            'form' => $form->createView(),
            'faqs' => $faqs,
            'newfaq' => $newfaq,
            'forms' => $formViews,
        ];
    }

    /**
     * @Route("/%eccube_admin_route%/content/faq/{id}/delete", requirements={"id" = "\d+"}, name="admin_content_faq_delete", methods={"DELETE"})
     */
    public function delete(Request $request, Faq $faq)
    {
        $this->isTokenValid();

        try {
            $this->faqRepository->delete($faq);
            $this->addSuccess('admin.common.delete_complete', 'admin');
        } catch (\Exception $e) {
            $message = trans('admin.common.delete_error_foreign_key', ['%name%' => $faq->getQuestion()]);
            $this->addError($message, 'admin');
        }

        return $this->redirectToRoute('admin_content_faq');
    }

    /**
     * @Route("/%eccube_admin_route%/content/faq/sort_no/move", name="admin_content_faq_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 $faqId => $sortNo) {
              $faq = $this->faqRepository
                  ->find($faqId);
              $faq->setSortNo($sortNo);
              $this->entityManager->persist($faq);
          }
          $this->entityManager->flush();

          return new Response();
      }
    }
}
STEP

「app/Template/admin/Content/faq.twig」の作成とアップロード

以下のTwigテンプレートを作成し、「app/Template/admin/Content」にアップします。

{#
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 = ['content', 'faq'] %}

{% block title %}{{ 'admin.content.faq_management'|trans }}{% endblock %}
{% block sub_title %}{{ 'admin.content.contents_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.faqId] = oldSortNos[i];
                    i++;
                });

                $.ajax({
                    url: '{{ url('admin_content_faq_sort_no_move') }}',
                    type: 'POST',
                    data: newSortNos
                }).done(function() {
                    // remove faq 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_content_faq') }}';
            });

            // 編集時, エラーがあれば入力欄を表示.
            $('.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_content_faq') }}">
                                        <div class="col-auto align-self-center">
                                          <span>{{ 'admin.content.faq.question'|trans }}</span></div>
                                        <div class="col-3 me-2">
                                            {{ form_widget(form._token) }}
                                            {{ form_widget(form.question) }}
                                            {{ form_errors(form.question) }}
                                        </div>
                                        <div class="col-auto align-self-center">
                                          <span>{{ 'admin.content.faq.answer'|trans }}</span>
                                        </div>
                                        <div class="col-3">
                                            {{ form_widget(form.answer) }}
                                            {{ form_errors(form.answer) }}
                                        </div>
                                        <div class="col-3 align-self-center">
                                            {{ form_widget(form.status) }}
                                            {{ form_errors(form.status) }}
                                        </div>
                                        <div class="col-auto align-self-center">
                                            <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.content.faq_management'|trans }}</strong></div>
                                    </div>
                                </li>
                                {% for faq in faqs %}
                                    <li id="ex-faq_name-{{ faq.id }}" class="list-group-item sortable-item" data-faq-id="{{ faq.id }}" data-sort-no="{{ faq.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">{{ faq.id }}</div>
                                            <div class="col d-flex align-items-center">{{ faq.question }}</div>
                                            <div class="col d-flex align-items-center">{{ faq.answer }}</div>
                                            {% if (faq.status) %}
                                              <div class="col d-flex align-items-center">{{ '公開' }}</div>
                                            {% else %}
                                              <div class="col d-flex align-items-center">{{ '非公開' }}</div>
                                            {% endif %}
                                            {# 矢印アイコンと編集アイコンと削除アイコンを設置 #}
                                            <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_content_faq_delete', {'id' : faq.id}) }}" data-message="{{ 'admin.common.delete_modal__message'|trans({ '%name%' : faq.question }) }}">
                                                        <i class="fa fa-times fa-lg text-secondary"></i>
                                                    </a>
                                                </div>
                                            </div>
                                        </div>
                                        {# 編集用のフォームを設置 #}
                                        <form class="row d-none mode-edit" method="post" action="{{ url('admin_content_faq') }}">
                                            {{ form_widget(forms[faq.id]._token) }}
                                            <div class="col-auto align-self-center"><span>{{ 'admin.content.faq.question'|trans }}</span></div>
                                            <div class="col-md-2 align-self-center">
                                                {{ form_widget(forms[faq.id].question, {'attr': {'data-origin-value': forms[faq.id].question.vars.value}}) }}
                                                {{ form_errors(forms[faq.id].question) }}
                                            </div>
                                            <div class="col-auto align-self-center"><span>{{ 'admin.content.faq.answer'|trans }}</span></div>
                                            <div class="col-md-4 align-self-center">
                                                {{ form_widget(forms[faq.id].answer, {'attr': {'data-origin-value': forms[faq.id].answer.vars.value}}) }}
                                                {{ form_errors(forms[faq.id].answer) }}
                                            </div>
                                            <div class="col-auto align-self-center">
                                                {{ form_widget(forms[faq.id].status, {'attr': {'data-origin-value': forms[faq.id].status.vars.value}}) }}
                                                {{ form_errors(forms[faq.id].status) }}
                                            </div>
                                            <div class="col-auto align-self-center">
                                                <button class="btn btn-ec-conversion w-100" type="submit">{{ 'admin.common.decision'|trans }}</button>
                                            </div>
                                            <div class="col-auto align-self-center">
                                                <button class="btn btn-ec-sub action-edit-cancel w-100" 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 %}

以上で実装完了です。コンテンツ管理 → FAQ管理より、FAQの追加・修正・削除・順番変更が行えるようになるはずです!

EC CUBEのバージョンによって、メソッドの定義の仕方やクラス名などが異なります。(本記事のバージョンは「4.2.2」です。)
上手く動作しない場合は既存ファイルを確認し、修正してみてください。

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