EC-CUBE4の商品一覧画面で該当するカテゴリページに画像や説明文を表示させる方法

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

EC-CUBE4では、カテゴリを設定すると、カテゴリ毎に商品一覧を表示させる機能があります。ただ、そのカテゴリ商品一覧画面ですが画像や説明文も何もありません。今回はカテゴリが選択されたら、カテゴリページに説明文や画像などのコンテンツを追加することができる方法を説明します。

EC-CUBE3をご利用の方はこちらのプラグインを利用すると同様のことが実現可能です。

www.ec-cube.net

Categoryクラスの修正

Categoryクラスに説明文と商品画像のファイル名を保存する項目を追加します。

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

/**
 * @var string|null
 *
 * @ORM\Column(name="free_area", type="text", nullable=true)
 */
private $free_area;

/**
 * @var string
 *
 * @ORM\Column(name="category_image", type="string", length=255, nullable=true)
 */
private $category_image;

/**
 * Set freeArea.
 *
 * @param string|null $freeArea
 *
 * @return Category
 */
public function setFreeArea($freeArea = null)
{
    $this->free_area = $freeArea;

    return $this;
}

/**
 * Get freeArea.
 *
 * @return string|null
 */
public function getFreeArea()
{
    return $this->free_area;
}

/**
 * Set categoryImage.
 *
 * @param string|null $categoryImage
 *
 * @return Category
 */
public function setCategoryImage($categoryImage = null)
{
    $this->category_image = $categoryImage;

    return $this;
}

/**
 * Get categoryImage.
 *
 * @return string|null
 */
public function getCategoryImage()
{
    return $this->category_image;
}

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

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

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

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

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

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

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

カテゴリ管理画面の修正

次に、カテゴリ管理画面の修正をします。以下のController、FormType、twigファイルを修正してください。

  • src/Eccube/Controller/Admin/Product/CategoryController.php
<?php
〜
〜
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
〜
〜

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

// ファイルアップロード
$file = $form['category_image']->getData();
$fs = new Filesystem();
if ($file && $fs->exists($this->getParameter('eccube_temp_image_dir').'/'.$file)) {
    $fs->rename(
        $this->getParameter('eccube_temp_image_dir').'/'.$file,
        $this->getParameter('eccube_save_image_dir').'/'.$file
    );
}


$this->categoryRepository->save($TargetCategory);

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

〜
〜

if ($editForm->isSubmitted() && $editForm->isValid()) {

    // ファイルアップロード
    $file = $editForm['category_image']->getData();
    $fs = new Filesystem();
    if ($file && $fs->exists($this->getParameter('eccube_temp_image_dir').'/'.$file)) {
        $fs->rename(
            $this->getParameter('eccube_temp_image_dir').'/'.$file,
            $this->getParameter('eccube_save_image_dir').'/'.$file
        );
    }

    $this->categoryRepository->save($editForm->getData());

    // $editFormが保存されたフォーム
    // 上の新規登録用フォームの場合とイベント名が共通のため
    // このイベントのリスナーではsubmitされているフォームを判定する必要がある


〜
〜



/**
 * @Route("/%eccube_admin_route%/product/category/image/add", name="admin_product_category_image_add")
 */
public function imageAdd(Request $request)
{
    if (!$request->isXmlHttpRequest()) {
        throw new BadRequestHttpException();
    }

    $allowExtensions = ['gif', 'jpg', 'jpeg', 'png'];
    $filename = null;

    $files = $request->files->all();
    foreach ($files as $images) {
        if (isset($images['category_file'])) {
            $image = $images['category_file'];

            //ファイルフォーマット検証
            $mimeType = $image->getMimeType();
            if (0 !== strpos($mimeType, 'image')) {
                throw new UnsupportedMediaTypeHttpException();
            }

            // 拡張子
            $extension = $image->getClientOriginalExtension();
            if (!in_array(strtolower($extension), $allowExtensions)) {
                throw new UnsupportedMediaTypeHttpException();
            }

            $filename = date('mdHis').uniqid('_').'.'.$extension;
            $image->move($this->getParameter('eccube_temp_image_dir'), $filename);
        }
    }

    return $this->json(['filename' => $filename], 200);
}
  • src/Eccube/Form/Type/Admin/CategoryType.php
<?php
〜
〜
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;

〜
〜

->add('free_area', TextareaType::class, [
    'label' => '詳細',
    'required' => false,
    'constraints' => [
        new Assert\Length([
            'max' => $this->eccubeConfig['eccube_ltext_len'],
        ]),
    ],
])
->add('category_file', FileType::class, [
    'label' => 'カテゴリ画像',
    'mapped' => false,
    'required' => false,
])
->add('category_image', HiddenType::class, [
    'required' => false,
]);

twigファイルは修正箇所が多いので、全て載せます。app/template/admin/Product/category.twigまでコピー後、以下の内容に修正します。

  • app/template/admin/Product/category.twig
{% extends '@admin/default_frame.twig' %}

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

{% block title %}{{ 'admin.product.category_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 %}
    <link rel="stylesheet" href="{{ asset('assets/css/fileupload/jquery.fileupload.css', 'admin') }}">
    <link rel="stylesheet" href="{{ asset('assets/css/fileupload/jquery.fileupload-ui.css', 'admin') }}">
    <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">

    <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 stylesheet %}

{% 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 src="{{ asset('assets/js/vendor/fileupload/vendor/jquery.ui.widget.js', 'admin') }}"></script>
    <script src="{{ asset('assets/js/vendor/fileupload/jquery.iframe-transport.js', 'admin') }}"></script>
    <script src="{{ asset('assets/js/vendor/fileupload/jquery.fileupload.js', 'admin') }}"></script>
    <script src="{{ asset('assets/js/vendor/fileupload/jquery.fileupload-process.js', 'admin') }}"></script>
    <script src="{{ asset('assets/js/vendor/fileupload/jquery.fileupload-validate.js', 'admin') }}"></script>
    <script>var bootstrapTooltip = $.fn.tooltip.noConflict();</script>
    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
    <script>
        $.fn.tooltip = bootstrapTooltip;
        $(document).on('drop dragover', function(e) {
            e.preventDefault();
        });

        $(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];
                    i++;
                });
                $.ajax({
                    url: '{{ url('admin_product_category_sort_no_move') }}',
                    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');
            });

            // 削除モーダルの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'));
            });

            var hideThumbnail = function() {
                if ($('#thumb div').length > 0) {
                    $('#icon_no_image').css('display', 'none');
                } else {
                    $('#icon_no_image').css('display', '');
                }
            };

            var proto_img = '<div class="c-form__fileUploadThumbnail" style="background-image:url(\'__path__\');">' +
                '<a class="delete-image"><i class="fa fa-times" aria-hidden="true"></i></a>' +
                '</div>';
            var category_image = $('#{{ form.category_image.vars.id }}').val();
            if (category_image != '') {
                var filename = $('#{{ form.category_image.vars.id }}').val();
                {# if (category_image == '{{ oldCategoryImage }}') { #}
                var path = '{{ asset('', 'save_image') }}' + filename;
                //} else {
                {# var path = '{{ asset('', 'temp_image') }}' + filename; #}
                //}
                var $img = $(proto_img.replace(/__path__/g, path));
                $('#{{ form.category_image.vars.id }}').val(filename);

                $('#thumb').append($img);
                hideThumbnail();
            }
            hideThumbnail();

            $('.file-upload').fileupload({
                url: "{{ url('admin_product_category_image_add') }}",
                type: 'post',
                dataType: 'json',
                dropZone: $('#upload-zone'),
                done: function(e, data) {
                    $('.progress', $(this).parent()).hide();
                    var path = '{{ asset('', 'temp_image') }}/' + data.result.filename;
                    var $img = $(proto_img.replace(/__path__/g, path));
                    $('.category-image', $(this).parent()).val(data.result.filename);
                    $('.upload-image', $(this).parent()).append($img);
                    $('img', $(this).parent()).remove();

                    hideThumbnail();
                },
                fail: function(e, data) {
                    alert('{{ 'admin.common.upload_error'|trans }}');
                },
                always: function(e, data) {
                    $('.progress').hide();
                    $('.progress .progress-bar').width('0%');
                },
                start: function(e, data) {
                    if ($('.c-form__fileUploadThumbnail').length >= 1) {
                        $.each($('.delete-image'), function(index, delete_image) {
                            delete_image.click();
                        });
                    }
                    $('.progress', $(this).parent()).show();
                    $('#thumb', $(this).parent()).find('div').remove();
                    $('#{{ form.category_image.vars.id }}').val('');
                },
                acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i,
                maxFileSize: 10000000,
                maxNumberOfFiles: 1,
                progressall: function(e, data) {
                    var progress = parseInt(data.loaded / data.total * 100, 10);
                    $('.progress .progress-bar', $(this).parent()).css(
                        'width',
                        progress + '%'
                    );
                },
                processalways: function(e, data) {
                    if (data.files.error) {
                        alert("{{ 'admin.common.upload_error'|trans }}");
                    }
                }
            });

            $('#thumb').on('click', '.delete-image', function() {
                $('#{{ form.category_image.vars.id }}').val('');
                var thumbnail = $(this).parents('div.c-form__fileUploadThumbnail');
                $(thumbnail).remove();
                hideThumbnail();
            });

            $(document).on('click', '.delete-image', function() {
                var thumbnail = $(this).parents('div.c-form__fileUploadThumbnail');
                $(thumbnail).remove();
                $(this).parent().find('img').remove();
                $(this).parent().find('.category-image').val('');
                $(this).hide();
            });
        });
    </script>
{% 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_category') }}">
                                    {{ '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_category_show', { parent_id : ParentCategory.id }) }}">
                                            {{ ParentCategory.name }}
                                        </a>
                                    </li>
                                {% endif %}
                            {% endfor %}
                        </ol>
                    </nav>
                </div>
                <div class="col-6 text-right">
                    <div class="btn-group" role="group">
                        <a class="btn btn-ec-regular" href="{{ url('admin_product_category_export') }}">
                            <i class="fa fa-cloud-download mr-1 text-secondary"></i>
                            <span>{{ 'admin.common.csv_download'|trans }}</span>
                        </a>
                        <a class="btn btn-ec-regular" href="{{ url('admin_setting_shop_csv', { id : constant('\\Eccube\\Entity\\Master\\CsvType::CSV_TYPE_CATEGORY') }) }}">
                            <i class="fa fa-cog mr-1 text-secondary"></i>
                            <span>{{ 'admin.setting.shop.csv_setting'|trans }}</span>
                        </a>
                    </div>
                </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">
                                    <form role="form" name="form1" id="form1" method="post"
                                          action="{% if TargetCategory.id %}{{ path('admin_product_category_edit', {id: TargetCategory.id}) }}{% elseif Parent %}{{ url('admin_product_category_show', {'parent_id': Parent.id}) }}{% else %}{{ url('admin_product_category') }}{% endif %}"
                                          enctype="multipart/form-data">
                                        {% if TargetCategory.hierarchy <= eccube_config.eccube_category_nest_level %}
                                            {{ form_widget(form._token) }}
                                            <div class="form-row mb-3">
                                                <div class="col-auto align-self-center mr-3"><span>カテゴリ名</span></div>
                                                <div class="col-7">
                                                    {{ form_widget(form.name) }}
                                                    {{ form_errors(form.name) }}
                                                </div>
                                                <div class="col-12 align-self-center"><span>詳細</span></div>
                                                <div class="col-12">
                                                    {{ form_widget(form.free_area) }}
                                                    {{ form_errors(form.free_area) }}
                                                </div>
                                                <div class="col-12 align-self-center"><span>カテゴリ画像</span></div>
                                                <div class="col-12 mb-2">
                                                    <div class="progress" style="display: none;">
                                                        <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
                                                    </div>
                                                    <div id="thumb" class="c-form__fileUploadThumbnails clearfix"></div>
                                                    <div class="upload-image"></div>
                                                    {{ form_widget(form.category_image, { attr : {'class': 'category-image', style : 'display:none;' } }) }}
                                                    {{ form_widget(form.category_file, { attr : {'class': 'file-upload', 'class': 'file-upload', accept : 'image/*', style : 'display:none;' } }) }}
                                                    {{ form_errors(form.category_image) }}

                                                    <a class="btn btn-ec-regular" onclick="$('#admin_category_category_file').click()">
                                                        {{ 'admin.common.file_select'|trans }}
                                                    </a>
                                                </div>
                                                <div class="col-auto">
                                                    <button class="btn btn-ec-regular" type="submit">
                                                        {{ 'admin.common.create__new'|trans }}
                                                    </button>
                                                </div>
                                            </div>
                                            {# エンティティ拡張の自動出力 #}
                                            {% for f in form if f.vars.eccube_form_options.auto_render %}
                                                {% if f.vars.eccube_form_options.form_theme %}
                                                    {% form_theme f f.vars.eccube_form_options.form_theme %}
                                                    {{ form_row(f) }}
                                                {% else %}
                                                    <div class="form-row mb-3">
                                                        <div class="col-3">
                                                            <span>{{ f.vars.label|trans }}</span>
                                                        </div>
                                                        <div class="col">
                                                            {{ form_widget(f) }}
                                                            {{ form_errors(f) }}
                                                        </div>
                                                    </div>
                                                {% endif %}
                                            {% endfor %}
                                        {% endif %}
                                    </form>
                                </li>
                                <li class="list-group-item">
                                    <div class="row">
                                        <div class="col-auto"><strong>&nbsp;</strong></div>
                                        <div class="col-auto"><strong>{{ 'admin.common.id'|trans }}</strong></div>
                                        <div class="col-2"><strong>{{ 'admin.product.category'|trans }}</strong></div>
                                    </div>
                                </li>
                                {% if Categories|length > 0 %}
                                    {% for Category in Categories %}
                                        <li id="ex-category-{{ Category.id }}" class="list-group-item sortable-item" data-id="{{ Category.id }}" data-sort-no="{{ Category.sort_no }}">
                                            {% if Category.id != TargetCategory.id %}
                                                <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">{{ Category.id }}</div>
                                                    <div class="col d-flex align-items-center">
                                                        <a href="{{ url('admin_product_category_show',  { parent_id : Category.id }) }}">{{ Category.name }}</a>
                                                        {% if Category.category_image %}
                                                            <div class="ml-5">
                                                                <img src="{{ asset(Category.category_image, 'save_image') }}" width="50px">
                                                            </div>
                                                        {% endif %}

                                                    </div>
                                                    <div class="col-auto 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: Category.id}) }}"
                                                           data-tooltip="true" data-placement="top"
                                                           title="{{ 'admin.common.edit'|trans }}">
                                                            <i class="fa fa-pencil fa-lg text-secondary"></i>
                                                        </a>
                                                        <div class="d-inline-block mr-2" data-tooltip="true" data-placement="top"
                                                             title="{{ 'admin.common.delete'|trans }}">
                                                            <a class="btn btn-ec-actionIcon{% if Category.Children|length > 0 or Category.hasProductCategories %} disabled{% endif %}"
                                                               data-toggle="modal" data-target="#DeleteModal"
                                                               data-url="{{ url('admin_product_category_delete', {id: Category.id}) }}"
                                                               data-message="{{ 'admin.common.delete_modal__message'|trans({ "%name%" : Category.name }) }}">
                                                                <i class="fa fa-close fa-lg text-secondary"></i>
                                                            </a>
                                                        </div>
                                                    </div>
                                                </div>
                                                <form class="form-row d-none mode-edit" method="POST" action="{{ (Parent and Parent.id) ? url('admin_product_category_show', {'parent_id': Parent.id}) : url('admin_product_category') }}" enctype="multipart/form-data">
                                                    {{ form_widget(forms[Category.id]._token) }}
                                                    <div class="col-auto align-self-center mr-3"><span>カテゴリ名</span></div>
                                                    <div class="col-7">
                                                        {{ form_widget(forms[Category.id].name, {'attr': {'data-origin-value': forms[Category.id].name.vars.value}}) }}
                                                        {{ form_errors(forms[Category.id].name) }}
                                                    </div>
                                                    <div class="col-12 align-self-center"><span>詳細</span></div>
                                                    <div class="col-12">
                                                        {{ form_widget(forms[Category.id].free_area, {'attr': {'data-origin-value': forms[Category.id].free_area.vars.value}}) }}
                                                        {{ form_errors(forms[Category.id].free_area) }}
                                                    </div>
                                                    <div class="col-12 align-self-center"><span>カテゴリ画像</span></div>
                                                    <div class="col-12 mb-2">
                                                        <div class="progress" style="display: none;">
                                                            <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
                                                        </div>
                                                        <div class="upload-image"></div>
                                                        {% if forms[Category.id].category_image.vars.value %}
                                                            <img src="{{ asset(forms[Category.id].category_image.vars.value, 'save_image') }}" width="150px">
                                                        {% endif %}
                                                        {{ form_widget(forms[Category.id].category_image, {'attr': {'class': 'category-image', 'data-origin-value': forms[Category.id].category_image.vars.value}}) }}
                                                        {{ form_errors(forms[Category.id].category_image) }}
                                                        {{ form_widget(forms[Category.id].category_file, { attr : { 'class': 'file-upload', accept : 'image/*', style : 'display:none;' } }) }}
                                                        <a class="btn btn-ec-regular mr-2" onclick="$('#{{ forms[Category.id].category_file.vars.id }}').click()">
                                                            {{ 'admin.common.file_select'|trans }}
                                                        </a>
                                                        <a class="btn btn-ec-regular mr-2 delete-image" {% if not forms[Category.id].category_image.vars.value %}style="display: none"{% endif %}>
                                                            削除
                                                        </a>
                                                    </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>
                                                    {# エンティティ拡張の自動出力 #}
                                                    {% for f in forms[Category.id] if f.vars.eccube_form_options.auto_render %}
                                                        <div class="col-auto align-items-center" style="width:90%; padding-top: 10px;">
                                                            <div class="row">
                                                                <div class="col-3">
                                                                    <span>{{ f.vars.label|trans }}</span>
                                                                </div>
                                                                <div class="col-9">
                                                                    {{ form_widget(f) }}
                                                                    {{ form_errors(f) }}
                                                                </div>
                                                            </div>
                                                        </div>
                                                    {% endfor %}
                                                </form>
                                            {% endif %}
                                        </li>
                                    {% endfor %}
                                {% endif %}
                            </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 font-weight-bold">
                                                {{ 'admin.common.delete_modal__title'|trans }}
                                            </h5>
                                            <button class="close" type="button" data-dismiss="modal" aria-label="Close">
                                                <span aria-hidden="true">×</span>
                                            </button>
                                        </div>
                                        <div class="modal-body text-left">
                                            <p class="text-left modal-message"><!-- jsでメッセージを挿入 --></p>
                                        </div>
                                        <div class="modal-footer">
                                            <button class="btn btn-ec-sub" type="button" data-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>

        {% 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_category_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_category') }}">{{ '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 %}

商品一覧画面の修正

src/Eccube/Resource/template/default/Product/list.twigapp/template/default/Product/list.twigまでコピーし、適切な箇所へ以下のタグを追加します。

  • app/template/default/Product/list.twig
{% if Category and Category.category_image %}
    <figure>
        <img src="{{ asset(Category.category_image|no_image_product, 'save_image') }}" alt="{{ Category.name }}">
    </figure>
{% endif %}

{% if Category and Category.free_area %}
    <div>
        {{ include(template_from_string(Category. free_area)) }}
    </div>
{% endif %}

以上でカテゴリページに説明文や画像などのコンテンツを追加する事が可能です。もしカテゴリが指定されていなかった時でも画像や説明文を表示させたいという場合、固定の画像や説明文をtwig内で分岐させて表示させる方法で十分だと思います。

もし動作しないなどがありましたらコメントください。