EC-CUBE4でお問い合わせ内容をDBへ保存する方法

EC-CUBE Advent Calendar 2020 11日目の記事です。

EC-CUBE4のお問い合わせフォームの内容は標準ではメールアドレスしか送られません。

ただ、お客様によっては時々お問い合わせフォームの内容をDBへ保存しておきたいという方がいます。

今回はお問い合わせフォームの内容をDBへ保存する方法を説明します。

dtb_contactテーブルの作成

お問い合わせ内容を保存するためにdtb_contactテーブルを作成します。

画面項目に合わせて、src/Eccube/Entity直下に以下の内容を保存します。

  • src/Eccube/Entity/Contact.php
<?php

namespace Eccube\Entity;

use Doctrine\ORM\Mapping as ORM;

if (!class_exists('\Eccube\Entity\Contact')) {
    /**
     * Contact
     *
     * @ORM\Table(name="dtb_contact")
     * @ORM\InheritanceType("SINGLE_TABLE")
     * @ORM\DiscriminatorColumn(name="discriminator_type", type="string", length=255)
     * @ORM\HasLifecycleCallbacks()
     * @ORM\Entity(repositoryClass="Eccube\Repository\ContactRepository")
     */
    class Contact extends \Eccube\Entity\AbstractEntity
    {
        /**
         * @var int
         *
         * @ORM\Column(name="id", type="integer", options={"unsigned":true})
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="IDENTITY")
         */
        private $id;

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

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

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

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

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

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

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

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

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

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

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

        /**
         * @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 \Eccube\Entity\Customer
         *
         * @ORM\ManyToOne(targetEntity="Eccube\Entity\Customer")
         * @ORM\JoinColumns({
         *   @ORM\JoinColumn(name="customer_id", referencedColumnName="id")
         * })
         */
        private $Customer;

        /**
         * @var \Eccube\Entity\Master\Pref
         *
         * @ORM\ManyToOne(targetEntity="Eccube\Entity\Master\Pref")
         * @ORM\JoinColumns({
         *   @ORM\JoinColumn(name="pref_id", referencedColumnName="id")
         * })
         */
        private $Pref;


        /**
         * Get id.
         *
         * @return int
         */
        public function getId()
        {
            return $this->id;
        }

        /**
         * Set name01.
         *
         * @param string $name01
         *
         * @return Contact
         */
        public function setName01($name01)
        {
            $this->name01 = $name01;

            return $this;
        }

        /**
         * Get name01.
         *
         * @return string
         */
        public function getName01()
        {
            return $this->name01;
        }

        /**
         * Set name02.
         *
         * @param string $name02
         *
         * @return Contact
         */
        public function setName02($name02)
        {
            $this->name02 = $name02;

            return $this;
        }

        /**
         * Get name02.
         *
         * @return string
         */
        public function getName02()
        {
            return $this->name02;
        }

        /**
         * Set kana01.
         *
         * @param string|null $kana01
         *
         * @return Contact
         */
        public function setKana01($kana01 = null)
        {
            $this->kana01 = $kana01;

            return $this;
        }

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

        /**
         * Set kana02.
         *
         * @param string|null $kana02
         *
         * @return Contact
         */
        public function setKana02($kana02 = null)
        {
            $this->kana02 = $kana02;

            return $this;
        }

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

        /**
         * Set companyName.
         *
         * @param string|null $companyName
         *
         * @return Contact
         */
        public function setCompanyName($companyName = null)
        {
            $this->company_name = $companyName;

            return $this;
        }

        /**
         * Get companyName.
         *
         * @return string|null
         */
        public function getCompanyName()
        {
            return $this->company_name;
        }

        /**
         * Set postalCode.
         *
         * @param string|null $postalCode
         *
         * @return Contact
         */
        public function setPostalCode($postalCode = null)
        {
            $this->postal_code = $postalCode;

            return $this;
        }

        /**
         * Get postalCode.
         *
         * @return string|null
         */
        public function getPostalCode()
        {
            return $this->postal_code;
        }

        /**
         * Set addr01.
         *
         * @param string|null $addr01
         *
         * @return Contact
         */
        public function setAddr01($addr01 = null)
        {
            $this->addr01 = $addr01;

            return $this;
        }

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

        /**
         * Set addr02.
         *
         * @param string|null $addr02
         *
         * @return Contact
         */
        public function setAddr02($addr02 = null)
        {
            $this->addr02 = $addr02;

            return $this;
        }

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

        /**
         * Set email.
         *
         * @param string $email
         *
         * @return Contact
         */
        public function setEmail($email)
        {
            $this->email = $email;

            return $this;
        }

        /**
         * Get email.
         *
         * @return string
         */
        public function getEmail()
        {
            return $this->email;
        }

        /**
         * Set phoneNumber.
         *
         * @param string|null $phoneNumber
         *
         * @return Contact
         */
        public function setPhoneNumber($phoneNumber = null)
        {
            $this->phone_number = $phoneNumber;

            return $this;
        }

        /**
         * Get phoneNumber.
         *
         * @return string|null
         */
        public function getPhoneNumber()
        {
            return $this->phone_number;
        }

        /**
         * Set contents.
         *
         * @param string $contents
         *
         * @return Contact
         */
        public function setContents($contents)
        {
            $this->contents = $contents;

            return $this;
        }

        /**
         * Get contents.
         *
         * @return string
         */
        public function getContents()
        {
            return $this->contents;
        }

        /**
         * Set createDate.
         *
         * @param \DateTime $createDate
         *
         * @return Contact
         */
        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 Contact
         */
        public function setUpdateDate($updateDate)
        {
            $this->update_date = $updateDate;

            return $this;
        }

        /**
         * Get updateDate.
         *
         * @return \DateTime
         */
        public function getUpdateDate()
        {
            return $this->update_date;
        }

        /**
         * Set customer.
         *
         * @param \Eccube\Entity\Customer|null $customer
         *
         * @return Contact
         */
        public function setCustomer(\Eccube\Entity\Customer $customer = null)
        {
            $this->Customer = $customer;

            return $this;
        }

        /**
         * Get customer.
         *
         * @return \Eccube\Entity\Customer|null
         */
        public function getCustomer()
        {
            return $this->Customer;
        }

        /**
         * Set pref.
         *
         * @param \Eccube\Entity\Master\Pref|null $pref
         *
         * @return Contact
         */
        public function setPref(\Eccube\Entity\Master\Pref $pref = null)
        {
            $this->Pref = $pref;

            return $this;
        }

        /**
         * Get pref.
         *
         * @return \Eccube\Entity\Master\Pref|null
         */
        public function getPref()
        {
            return $this->Pref;
        }
    }
}

src/Eccube/Repository直下ににreposityクラスも作成します。記述されている内容は後ほど作成する管理画面で、メール本文の内容を検索できるための関数となります。

  • src/Eccube/Repository/ContactRepository.php
<?php

namespace Eccube\Repository;

use Eccube\Entity\Contact;
use Eccube\Util\StringUtil;
use Symfony\Bridge\Doctrine\RegistryInterface;

/**
 * ContactRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class ContactRepository extends AbstractRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Contact::class);
    }

    public function getQueryBuilderBySearchData($searchData)
    {
        $qb = $this->createQueryBuilder('c')
            ->select('c');

        if (isset($searchData['multi']) && StringUtil::isNotBlank($searchData['multi'])) {
            //スペース除去
            $clean_key_multi = preg_replace('/\s+|[ ]+/u', '', $searchData['multi']);
            $id = preg_match('/^\d{0,10}$/', $clean_key_multi) ? $clean_key_multi : null;
            $qb
                ->andWhere('c.id = :id OR CONCAT(c.name01, c.name02) LIKE :name OR CONCAT(c.kana01, c.kana02) LIKE :kana OR c.email LIKE :email')
                ->setParameter('id', $id)
                ->setParameter('name', '%'.$clean_key_multi.'%')
                ->setParameter('kana', '%'.$clean_key_multi.'%')
                ->setParameter('email', '%'.$clean_key_multi.'%');
        }


        // お問い合わせ内容
        if (isset($searchData['contents']) && StringUtil::isNotBlank($searchData['contents'])) {
            $qb
                ->andWhere('c.contents LIKE :contents')
                ->setParameter('contents', '%'.$searchData['contents'].'%');
        }

        // contact_id_start
        if (isset($searchData['contact_id_start']) && StringUtil::isNotBlank($searchData['contact_id_start'])) {
            $qb
                ->andWhere('c.id >= :contact_id_start')
                ->setParameter('contact_id_start', $searchData['contact_id_start']);
        }

        // contact_id_end
        if (isset($searchData['contact_id_end']) && StringUtil::isNotBlank($searchData['contact_id_end'])) {
            $qb
                ->andWhere('c.id >= :contact_id_end')
                ->setParameter('contact_id_end', $searchData['contact_id_end']);
        }

        // name
        if (isset($searchData['name']) && StringUtil::isNotBlank($searchData['name'])) {
            $qb
                ->andWhere('CONCAT(c.name01, c.name02) LIKE :name')
                ->setParameter('name', '%'.$searchData['name'].'%');
        }

        // kana
        if (isset($searchData['kana']) && StringUtil::isNotBlank($searchData['kana'])) {
            $qb
                ->andWhere('CONCAT(c.kana01, c.kana02) LIKE :kana')
                ->setParameter('kana', '%'.$searchData['kana'].'%');
        }

        // email
        if (isset($searchData['email']) && StringUtil::isNotBlank($searchData['email'])) {
            $qb
                ->andWhere('c.email like :email')
                ->setParameter('email', '%'.$searchData['email'].'%');
        }

        // contact_date
        if (!empty($searchData['contact_date_start']) && $searchData['contact_date_start']) {
            $date = $searchData['contact_date_start'];
            $qb
                ->andWhere('c.create_date >= :contact_date_start')
                ->setParameter('contact_date_start', $date);
        }
        if (!empty($searchData['contact_date_end']) && $searchData['contact_date_end']) {
            $date = clone $searchData['contact_date_end'];
            $date = $date
                ->modify('+1 days');
            $qb
                ->andWhere('c.create_date < :contact_date_end')
                ->setParameter('contact_date_end', $date);
        }


        // Order By
        $qb->addOrderBy('c.create_date', 'DESC');

        return $qb;
    }
}

作成すれば、dtb_contactテーブルをDBへ作成します。create文でテーブルを作成しても良いのですが、doctrineコマンドを利用して作成します。

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

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

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

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

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

コマンド実行後、エラーが発生しなければDBにdtb_contactテーブルが作成されます。

ContactControllerの変更

次に、お問い合わせフォームの内容をDBへ保存する処理を作成します。

src/Eccube/ContactController.phpの内容を変更します。

  • src/Eccube/ContactController.php
<?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 Eccube\Controller;

use Eccube\Entity\Contact;
use Eccube\Entity\Customer;
use Eccube\Event\EccubeEvents;
use Eccube\Event\EventArgs;
use Eccube\Form\Type\Front\ContactType;
use Eccube\Repository\ContactRepository;
use Eccube\Service\MailService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class ContactController extends AbstractController
{
    /**
     * @var MailService
     */
    protected $mailService;

    /**
     * @var ContactRepository
     */
    protected $contactRepository;

    /**
     * ContactController constructor.
     *
     * @param MailService $mailService
     * @param ContactRepository $contactRepository
     */
    public function __construct(MailService $mailService, ContactRepository $contactRepository)
    {
        $this->mailService = $mailService;
        $this->contactRepository = $contactRepository;
    }

    /**
     * お問い合わせ画面.
     *
     * @Route("/contact", name="contact")
     * @Template("Contact/index.twig")
     */
    public function index(Request $request)
    {
        $builder = $this->formFactory->createBuilder(ContactType::class);

        if ($this->isGranted('ROLE_USER')) {
            /** @var Customer $user */
            $user = $this->getUser();
            $Contact = new Contact();

            $Contact
                ->setName01($user->getName01())
                ->setName02($user->getName02())
                ->setKana01($user->getKana01())
                ->setKana02($user->getKana02())
                ->setPostalCode($user->getPostalCode())
                ->setPref($user->getPref())
                ->setAddr01($user->getAddr01())
                ->setAddr02($user->getAddr02())
                ->setPhoneNumber($user->getPhoneNumber())
                ->setEmail($user->getEmail())
                ->setCustomer($user);

            $builder->setData($Contact);
        }

        // FRONT_CONTACT_INDEX_INITIALIZE
        $event = new EventArgs(
            [
                'builder' => $builder,
            ],
            $request
        );
        $this->eventDispatcher->dispatch(EccubeEvents::FRONT_CONTACT_INDEX_INITIALIZE, $event);

        $form = $builder->getForm();
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            switch ($request->get('mode')) {
                case 'confirm':

                    return $this->render('Contact/confirm.twig', [
                        'form' => $form->createView(),
                    ]);

                case 'complete':

                    $data = $form->getData();

                    $event = new EventArgs(
                        [
                            'form' => $form,
                            'data' => $data,
                        ],
                        $request
                    );
                    $this->eventDispatcher->dispatch(EccubeEvents::FRONT_CONTACT_INDEX_COMPLETE, $event);

                    $data = $event->getArgument('data');

                    $this->contactRepository->save($data);
                    $this->entityManager->flush();

                    // メール送信
                    $this->mailService->sendContactMail($data);

                    return $this->redirect($this->generateUrl('contact_complete'));
            }
        }

        return [
            'form' => $form->createView(),
        ];
    }

    /**
     * お問い合わせ完了画面.
     *
     * @Route("/contact/complete", name="contact_complete")
     * @Template("Contact/complete.twig")
     */
    public function complete()
    {
        return [];
    }

}

ContactControllerクラス以外に、ContactTypeクラスも変更します。

  • src/Eccube/Form/Type/Front/ContactType.php
<?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 Eccube\Form\Type\Front;

use Eccube\Common\EccubeConfig;
use Eccube\Form\Type\AddressType;
use Eccube\Form\Type\KanaType;
use Eccube\Form\Type\NameType;
use Eccube\Form\Type\PhoneNumberType;
use Eccube\Form\Type\PostalType;
use Eccube\Form\Validator\Email;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;

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

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

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', NameType::class, [
                'required' => true,
            ])
            ->add('kana', KanaType::class, [
                'required' => false,
            ])
            ->add('postal_code', PostalType::class, [
                'required' => false,
            ])
            ->add('address', AddressType::class, [
                'required' => false,
            ])
            ->add('phone_number', PhoneNumberType::class, [
                'required' => false,
            ])
            ->add('email', EmailType::class, [
                'constraints' => [
                    new Assert\NotBlank(),
                    new Email(['strict' => $this->eccubeConfig['eccube_rfc_email_check']]),
                ],
            ])
            ->add('contents', TextareaType::class, [
                'constraints' => [
                    new Assert\NotBlank(),
                ],
            ]);
    }

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

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'contact';
    }
}

元々の内容から、

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

を追加します。

上記の内容に変更することで、Contactクラスに対してお問い合わせフォームの内容が設定されDBへ保存されるようになります。

管理画面でお問い合わせ内容を確認

お問い合わせ内容をDBへ保存することが出来たので、次に管理画面でお問い合わせ内容を閲覧できるように作成します。

作成するものは、Controllerクラス、twigファイルとなります。 また、管理者メニューにお問い合わせ管理というメニューを表示させるための変更も行います。

先ずはContollerクラスを作成します。src/Eccube/Controller/Admin/Contactディレクトリを作成し、Controllerクラスを作成してください。

  • src/Eccube/Controller/Admin/Contact/ContactController.php
<?php

namespace Eccube\Controller\Admin\Contact;

use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\ORM\QueryBuilder;
use Eccube\Common\Constant;
use Eccube\Controller\AbstractController;
use Eccube\Entity\Contact;
use Eccube\Form\Type\Admin\SearchContactType;
use Eccube\Repository\ContactRepository;
use Eccube\Repository\Master\PageMaxRepository;
use Eccube\Util\FormUtil;
use Knp\Component\Pager\Paginator;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class ContactController extends AbstractController
{

    /**
     * @var PageMaxRepository
     */
    protected $pageMaxRepository;

    /**
     * @var ContactRepository
     */
    protected $contactRepository;

    /**
     * ContactController constructor.
     *
     * @param PageMaxRepository $pageMaxRepository
     * @param ContactRepository $contactRepository
     */
    public function __construct(PageMaxRepository $pageMaxRepository, ContactRepository $contactRepository)
    {
        $this->pageMaxRepository = $pageMaxRepository;
        $this->contactRepository = $contactRepository;
    }


    /**
     * @Route("/%eccube_admin_route%/contact", name="admin_contact")
     * @Route("/%eccube_admin_route%/contact/page/{page_no}", requirements={"page_no" = "\d+"}, name="admin_contact_page")
     * @Template("@admin/Contact/contact.twig")
     */
    public function index(Request $request, $page_no = null, Paginator $paginator)
    {
        $session = $this->session;
        $builder = $this->formFactory->createBuilder(SearchContactType::class);

        $searchForm = $builder->getForm();

        $pageMaxis = $this->pageMaxRepository->findAll();
        $pageCount = $session->get('eccube.admin.contact.search.page_count', $this->eccubeConfig['eccube_default_page_count']);
        $pageCountParam = $request->get('page_count');
        if ($pageCountParam && is_numeric($pageCountParam)) {
            foreach ($pageMaxis as $pageMax) {
                if ($pageCountParam == $pageMax->getName()) {
                    $pageCount = $pageMax->getName();
                    $session->set('eccube.admin.contact.search.page_count', $pageCount);
                    break;
                }
            }
        }

        if ('POST' === $request->getMethod()) {
            $searchForm->handleRequest($request);
            if ($searchForm->isValid()) {
                $searchData = $searchForm->getData();
                $page_no = 1;

                $session->set('eccube.admin.contact.search', FormUtil::getViewData($searchForm));
                $session->set('eccube.admin.contact.search.page_no', $page_no);
            } else {
                return [
                    'searchForm' => $searchForm->createView(),
                    'pagination' => [],
                    'pageMaxis' => $pageMaxis,
                    'page_no' => $page_no,
                    'page_count' => $pageCount,
                    'has_errors' => true,
                ];
            }
        } else {
            if (null !== $page_no || $request->get('resume')) {
                if ($page_no) {
                    $session->set('eccube.admin.contact.search.page_no', (int)$page_no);
                } else {
                    $page_no = $session->get('eccube.admin.contact.search.page_no', 1);
                }
                $viewData = $session->get('eccube.admin.contact.search', []);
            } else {
                $page_no = 1;
                $viewData = FormUtil::getViewData($searchForm);
                $session->set('eccube.admin.contact.search', $viewData);
                $session->set('eccube.admin.contact.search.page_no', $page_no);
            }
            $searchData = FormUtil::submitAndGetData($searchForm, $viewData);
        }

        /** @var QueryBuilder $qb */
        $qb = $this->contactRepository->getQueryBuilderBySearchData($searchData);

        $pagination = $paginator->paginate(
            $qb,
            $page_no,
            $pageCount
        );

        return [
            'searchForm' => $searchForm->createView(),
            'pagination' => $pagination,
            'pageMaxis' => $pageMaxis,
            'page_no' => $page_no,
            'page_count' => $pageCount,
            'has_errors' => false,
        ];
    }

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

        log_info('お問い合わせ削除開始', [$id]);

        $page_no = intval($this->session->get('eccube.admin.contact.search.page_no'));
        $page_no = $page_no ? $page_no : Constant::ENABLED;

        $Contact = $this->contactRepository->find($id);

        if (!$Contact) {
            $this->deleteMessage();

            return $this->redirect($this->generateUrl('admin_contact_page', ['page_no' => $page_no]).'?resume='.Constant::ENABLED);
        }

        try {
            $this->entityManager->remove($Contact);
            $this->entityManager->flush();
            $this->addSuccess('admin.common.delete_complete', 'admin');
        } catch (ForeignKeyConstraintViolationException $e) {
            log_error('お問い合わせ削除失敗', [$e]);
            $this->addError(trans('admin.common.delete_error_foreign_key', ['%name%' => $Contact->getId()]), 'admin');

            return $this->redirectToRoute('admin_contact');

        }

        return $this->redirect($this->generateUrl('admin_contact_page', ['page_no' => $page_no]).'?resume='.Constant::ENABLED);
    }


    /**
     * @Route("/%eccube_admin_route%/contact/{id}/edit", requirements={"id" = "\d+"}, name="admin_contact_edit")
     * @Template("@admin/Contact/contact_edit.twig")
     *
     * @param Request $request
     * @param Contact $Contact
     * @return array|\Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function edit(Request $request, Contact $Contact)
    {

        return [
            'Contact' => $Contact,
        ];
    }

}

次に、FormTypeクラスを作成します。

  • /src/Eccube/Form/Type/Admin/SearchContactType.php
<?php

namespace Eccube\Form\Type\Admin;

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

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

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


    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // 問い合わせ番号・お名前・メールアドレス
            ->add('multi', TextType::class, [
                'required' => false,
                'constraints' => [
                    new Assert\Length(['max' => $this->eccubeConfig['eccube_stext_len']]),
                ],
            ])
            ->add('contents', TextType::class, [
                'label' => 'お問い合わせ内容',
                'required' => false,
                'constraints' => [
                    new Assert\Length(['max' => $this->eccubeConfig['eccube_stext_len']]),
                ],
            ])
            ->add('contact_id_start', IntegerType::class, [
                'required' => false,
                'constraints' => [
                    new Assert\Range(['min' => 1]),
                ],
            ])
            ->add('contact_id_end', IntegerType::class, [
                'required' => false,
                'constraints' => [
                    new Assert\Range(['min' => 1]),
                ],
            ])
            ->add('name', TextType::class, [
                'label' => 'お名前',
                'required' => false,
            ])
            ->add($builder
                ->create('kana', TextType::class, [
                    'label' => 'お名前(カナ)',
                    'required' => false,
                    'constraints' => [
                        new Assert\Regex([
                            'pattern' => '/^[ァ-ヶヲ-゚ー]+$/u',
                            'message' => 'form_error.kana_only',
                        ]),
                    ],
                ])
                ->addEventSubscriber(new \Eccube\Form\EventListener\ConvertKanaListener('CV')
                ))
            ->add('email', TextType::class, [
                'label' => 'admin.common.mail_address',
                'required' => false,
            ])
            ->add('contact_date_start', DateType::class, [
                'label' => '問い合わせ日',
                'required' => false,
                'input' => 'datetime',
                'widget' => 'single_text',
                'format' => 'yyyy-MM-dd',
                'placeholder' => ['year' => '----', 'month' => '--', 'day' => '--'],
                'attr' => [
                    'class' => 'datetimepicker-input',
                    'data-target' => '#'.$this->getBlockPrefix().'_contact_date_start',
                    'data-toggle' => 'datetimepicker',
                ],
            ])
            ->add('contact_date_end', DateType::class, [
                'label' => '問い合わせ日',
                'required' => false,
                'input' => 'datetime',
                'widget' => 'single_text',
                'format' => 'yyyy-MM-dd',
                'placeholder' => ['year' => '----', 'month' => '--', 'day' => '--'],
                'attr' => [
                    'class' => 'datetimepicker-input',
                    'data-target' => '#'.$this->getBlockPrefix().'_contact_date_end',
                    'data-toggle' => 'datetimepicker',
                ],
            ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'admin_search_contact';
    }
}

以上で処理部分が作成されましたので、次にtwigファイルを作成します。 twigファイルは検索画面と詳細画面の2画面作成します。

先ずは検索画面を作成します。twigファイルの配置場所はapp/template/admin/Contactディレクトリを作成して配置します。

  • app/template/admin/Contact/contact.twig
{% extends '@admin/default_frame.twig' %}

{% set menus = ['contact', 'contact_master'] %}

{% block title %}お問い合わせ一覧{% endblock %}
{% block sub_title %}お問い合わせ管理{% endblock %}

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

{% block stylesheet %}
    <link rel="stylesheet" href="{{ asset('assets/css/tempusdominus-bootstrap-4.min.css', 'admin') }}">
    <style type="text/css">
        .datepicker-days th.dow:first-child,
        .datepicker-days td:first-child {
            color: #f00;
        }

        .datepicker-days th.dow:last-child,
        .datepicker-days td:last-child {
            color: #00f;
        }
    </style>
{% endblock stylesheet %}

{% block javascript %}
    <script>
        $(function() {
            $.when(
                $.getScript("{{ asset('assets/js/vendor/moment.min.js', 'admin') }}"),
                $.getScript("{{ asset('assets/js/vendor/moment-with-locales.min.js', 'admin') }}"),
                $.getScript("{{ asset('assets/js/vendor/tempusdominus-bootstrap-4.min.js', 'admin') }}")
            ).done(function() {
                // datetimepicker で value が消えてしまうので data-value に保持しておく
                $('input.datetimepicker-input').each(function() {
                    $(this).data('value', $(this).val());
                });

                $('input.datetimepicker-input').datetimepicker({
                    locale: '{{ eccube_config.locale }}',
                    format: 'YYYY-MM-DD HH:mm:ss',
                    useCurrent: false,
                    buttons: {
                        showToday: true,
                        showClose: true
                    },
                });

                // datetimepicker で value が消えてしまうので更新
                $('input.datetimepicker-input').each(function() {
                    $(this).val($(this).data('value'));
                });
            });

        });

    </script>
{% endblock javascript %}

{% block main %}
    <form name="search_form" id="search_form" method="post" action="">
        {{ form_widget(searchForm._token) }}
        <div class="c-outsideBlock">
            <div class="c-outsideBlock__contents">
                <div class="row justify-content-start">
                    <div class="col-6">
                        <div class="mb-2">
                            <label class="col-form-label" data-tooltip="true" data-placement="top" title="問い合わせ番号・お名前・メールアドレス">問い合わせ番号・お名前・メールアドレス<i class="fa fa-question-circle fa-lg ml-1"></i></label>
                            {{ form_widget(searchForm.multi) }}
                            {{ form_errors(searchForm.multi) }}
                        </div>
                        <div class="d-inline-block mb-3 collapsed" data-toggle="collapse" href="#searchDetail" aria-expanded="false" aria-controls="searchDetail"><a><i class="fa font-weight-bold mr-1 fa-plus-square-o"></i><span class="font-weight-bold">{{ 'admin.common.search_detail'|trans }}</span></a></div>
                    </div>
                </div>
            </div>
            <div class="c-subContents ec-collapse collapse{{ has_errors ? ' show' }}" id="searchDetail">
                <div class="row mb-2">
                    <div class="col">
                        <div>
                            <label>お問い合わせ内容</label>
                            {{ form_widget(searchForm.contents) }}
                            {{ form_errors(searchForm.contents) }}
                        </div>
                    </div>
                </div>
                <div class="row mb-2">
                    <div class="col">
                        <div>
                            <label>番号</label>
                            <div class="form-row align-items-center">
                                <div class="col">
                                    {{ form_widget(searchForm.contact_id_start) }}
                                    {{ form_errors(searchForm.contact_id_start) }}
                                </div>
                                <div class="col-auto text-center"><span>{{ 'admin.common.separator__range'|trans }}</span></div>
                                <div class="col">
                                    {{ form_widget(searchForm.contact_id_end) }}
                                    {{ form_errors(searchForm.contact_id_end) }}
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="row mb-2">
                    <div class="col">
                        <div>
                            <label>お名前</label>
                            {{ form_widget(searchForm.name) }}
                            {{ form_errors(searchForm.name) }}
                        </div>
                    </div>
                    <div class="col">
                        <div>
                            <label>お名前(カナ)</label>
                            {{ form_widget(searchForm.kana) }}
                            {{ form_errors(searchForm.kana) }}
                        </div>
                    </div>
                </div>
                <div class="row mb-2">
                    <div class="col">
                        <div>
                            <label>メールアドレス</label>
                            {{ form_widget(searchForm.email) }}
                            {{ form_errors(searchForm.email) }}
                        </div>
                    </div>
                    <div class="col">
                        <div>
                            <label>お問い合わせ日</label>
                            <div class="form-row align-items-center">
                                <div class="col">
                                    {{ form_widget(searchForm.contact_date_start) }}
                                    {{ form_errors(searchForm.contact_date_start) }}
                                </div>
                                <div class="col-auto text-center"><span>{{ 'admin.common.separator__range'|trans }}</span></div>
                                <div class="col">
                                    {{ form_widget(searchForm.contact_date_end) }}
                                    {{ form_errors(searchForm.contact_date_end) }}
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="c-outsideBlock__contents mb-5">
            <button type="submit" class="btn btn-ec-conversion px-5">{{ 'admin.common.search'|trans }}</button>
            {% if pagination %}
                <span class="font-weight-bold ml-2">{{ 'admin.common.search_result'|trans({'%count%':pagination.totalItemCount}) }}</span>
            {% endif %}
        </div>
        <div class="c-outsideBlock__contents mb-5">
            {{ include('@admin/search_items.twig', { 'form': searchForm }, ignore_missing = true) }}
        </div>
        <div class="c-contentsArea__cols">
            <div class="c-contentsArea__primaryCol">
                <div class="c-primaryCol">
                    {% if pagination and pagination.totalItemCount %}
                        <div class="row justify-content-between mb-2">
                            <div class="col-6"></div>

                            <div class="col-5 text-right">
                                {#Dropdown page count#}
                                <div class="d-inline-block mr-2">
                                    <select class="custom-select" onchange="location = this.value;">
                                        {% for pageMax in pageMaxis %}
                                            <option {% if pageMax.name == page_count %} selected {% endif %}
                                                    value="{{ path('admin_customer_page', {'page_no': 1, 'page_count': pageMax.name }) }}">
                                                {{ 'admin.common.count'|trans({ '%count%': pageMax.name }) }}</option>
                                        {% endfor %}
                                    </select>
                                </div>

                            </div>
                        </div>
                        <div class="card rounded border-0 mb-4 d-block">
                            <div class="card-body p-0">
                                <table class="table">
                                    <thead>
                                    <tr>
                                        <th class="border-top-0 pt-2 pb-3 pl-3 text-nowrap">番号</th>
                                        <th class="border-top-0 pt-2 pb-3">会員ID</th>
                                        <th class="border-top-0 pt-2 pb-3">名前</th>
                                        <th class="border-top-0 pt-2 pb-3">内容</th>
                                        <th class="border-top-0 pt-2 pb-3">日時</th>
                                        <th class="border-top-0 pt-2 pb-3 pr-3">&nbsp;</th>
                                    </tr>
                                    </thead>
                                    <tbody>
                                    {% for Contact in pagination %}
                                        <tr>
                                            <td class="align-middle pl-3"><a href="{{ url('admin_contact_edit', { 'id': Contact.id}) }}">{{ Contact.id }}</a></td>
                                            <td class="align-middle">{{ Contact.Customer ? Contact.Customer.id : '' }}</td>
                                            <td class="align-middle"><a href="{{ url('admin_contact_edit', { 'id': Contact.id}) }}">{{ Contact.name01 }}&nbsp;{{ Contact.name02 }}</a></td>
                                            <td class="align-middle">{{ Contact.contents }}</td>
                                            <td class="align-middle">{{ Contact.create_date|date_sec }}</td>
                                            <td class="align-middle"></td>
                                            <td class="align-middle pr-3">
                                                <div class="text-right">
                                                    <div class="px-1 d-inline-block" data-tooltip="true" data-placement="top" title="{{ 'admin.common.delete'|trans }}">
                                                        <a class="btn btn-ec-actionIcon" data-toggle="modal" data-target="#discontinuance-{{ Contact.id }}">
                                                            <i class="fa fa-close fa-lg text-secondary" aria-hidden="true"></i>
                                                        </a>
                                                    </div>
                                                    <div class="modal fade" id="discontinuance-{{ Contact.id }}" tabindex="-1" role="dialog" aria-labelledby="discontinuance" 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">
                                                                        {{ 'admin.common.delete_modal__message'|trans({ '%name%' : Contact.email }) }}</p>
                                                                </div>
                                                                <div class="modal-footer">
                                                                    <button class="btn btn-ec-sub" type="button" data-dismiss="modal">
                                                                        {{ 'admin.common.cancel'|trans }}
                                                                    </button>
                                                                    <a href="{{ url('admin_contact_delete', {'id' : Contact.id}) }}" class="btn btn-ec-delete"{{ csrf_token_for_anchor() }} data-method="delete" data-confirm="false">
                                                                        {{ 'admin.common.delete'|trans }}
                                                                    </a>
                                                                </div>
                                                            </div>
                                                        </div>
                                                    </div>
                                                </div><!-- /.text-right -->
                                            </td>
                                        </tr>
                                    {% endfor %}
                                    </tbody>
                                </table>
                                <div class="row justify-content-md-center mb-4">
                                    {% if pagination.totalItemCount > 0 %}
                                        {% include "@admin/pager.twig" with { 'pages' : pagination.paginationData, 'routes' : 'admin_contact_page' } %}
                                    {% endif %}
                                </div>
                            </div>
                        </div>
                    {% elseif has_errors %}
                        <div class="card rounded border-0">
                            <div class="card-body p-4">
                                <div class="text-center text-muted mb-4 h5">{{ 'admin.common.search_invalid_condition'|trans }}</div>
                                <div class="text-center text-muted">{{ 'admin.common.search_try_change_condition'|trans }}</div>
                            </div>
                        </div>
                    {% else %}
                        <div class="card rounded border-0">
                            <div class="card-body p-4">
                                <div class="text-center text-muted mb-4 h5">{{ 'admin.common.search_no_result'|trans }}</div>
                                <div class="text-center text-muted">{{ 'admin.common.search_try_change_condition'|trans }}</div>
                                <div class="text-center text-muted">{{ 'admin.common.search_try_advanced_search'|trans }}</div>
                            </div>
                        </div>
                    {% endif %}
                </div>
            </div>
        </div>
    </form>
{% endblock %}

次に詳細画面を作成します。

  • app/template/admin/Contact/contact_edit.twig
{% extends '@admin/default_frame.twig' %}

{% set menus = ['contact', 'customer_edit'] %}

{% block title %}お問い合わせ詳細{% endblock %}
{% block sub_title %}お問い合わせ管理{% 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-header">
                        <div class="row">
                            <div class="col-8"><span class="card-title">お問い合わせ情報</span>
                            </div>
                            <div class="col-4 text-right">
                                <a data-toggle="collapse" href="#contactInfo"
                                   aria-expanded="false" aria-controls="contactInfo">
                                    <i class="fa fa-angle-up fa-lg"></i>
                                </a>
                            </div>
                        </div>
                    </div>
                    <div class="collapse show ec-cardCollapse" id="contactInfo">
                        <div class="card-body">
                            <div class="row mb-2">
                                <div class="col-3">
                                    <span>お問い合わせ番号</span>
                                </div>
                                <div class="col">
                                    <p>{{ Contact.id }}</p>
                                </div>
                            </div>
                            <div class="row mb-2">
                                <div class="col-3">
                                    <span>お問い合わせ日時</span>
                                </div>
                                <div class="col">
                                    <p>{{ Contact.create_date|date_sec }}</p>
                                </div>
                            </div>
                            <div class="row mb-2">
                                <div class="col-3">
                                    <span>会員ID</span>
                                </div>
                                <div class="col">
                                    <p>{{ Contact.Customer ? Contact.Customer.id : '' }}</p>
                                </div>
                            </div>
                            <div class="row mb-2">
                                <div class="col-3">
                                    <span>お名前</span>
                                </div>
                                <div class="col">
                                    <p>{{ Contact.name01 }} {{ Contact.name02 }}</p>
                                </div>
                            </div>
                            <div class="row mb-2">
                                <div class="col-3">
                                    <span>お名前(カナ)</span>
                                </div>
                                <div class="col">
                                    <p>{{ Contact.kana01 }} {{ Contact.kana02 }}</p>
                                </div>
                            </div>
                            <div class="row mb-2">
                                <div class="col-3">
                                    <span>メールアドレス</span>
                                </div>
                                <div class="col">
                                    <p>{{ Contact.email }}</p>
                                </div>
                            </div>
                            <div class="row mb-2">
                                <div class="col-3">
                                    <span>お問い合わせ内容</span>
                                </div>
                                <div class="col">
                                    <p>{{ Contact.contents|nl2br }}</p>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

            </div>

        </div>
    </div>

    <div class="c-conversionArea">
        <div class="c-conversionArea__container">
            <div class="row justify-content-between align-items-center">
                <div class="col-6">
                    <div class="c-conversionArea__leftBlockItem">
                        <a class="c-baseLink"
                           href="{{ url('admin_contact_page', { page_no : app.session.get('eccube.admin.contact.search.page_no')|default('1') }) }}?resume=1">
                            <i class="fa fa-backward" aria-hidden="true"></i>
                            <span>お問い合わせ一覧</span>
                        </a>
                    </div>
                </div>
                <div class="col-6">
                    <div id="ex-conversion-action" class="row align-items-center justify-content-end">
                        <div class="col-auto">
                            <button class="btn btn-ec-conversion px-5" type="submit">{{ 'admin.common.registration'|trans }}</button>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

これでtwigファイルが用意できました。

最後に、管理画面のメニューへ表示させるため、app/config/eccube/packages/eccube_nav.yamlファイルを変更します。

メニューを追加する場所はどこでも良いのですが、会員管理の下へお問い合わせ管理というメニューを追加させます。

        contact:
            name: お問い合わせ管理
            icon: fa-comment
            children:
                contact_master:
                     name: お問い合わせ一覧
                     url: admin_contact

yamlファイルを設定する時はスペースの数にも気をつけてください。

以上で管理画面からお問い合わせの内容が確認できるようになりました。

ここからもう少し拡張して返信機能を作成したりすることも可能ですので頑張ってみてください。