ECCUBE4

EC-CUBE4 離島料金設定プラグインを作ってみよう

こんにちはジャムです。

EC-CUBE4ではデフォルトで各都道府県の配送料金を設定する事ができます。

しかし、離島に関しては個別に料金を設定することができません。

そこで、本記事では離島料金を設定するプラグインの作成方法を紹介します。

機能概要

今回作成する機能としては、離島として判断するために郵便番号を使って判断します。そのため管理画面で離島料金と郵便番号を登録できるようにします。

そして、購入の際に登録している郵便番号であれば離島料金を設定するようにします。

プラグイン生成&インストール&有効化

プラグイン生成

$ bin/console e:p:g

入力するとコマンドラインが実行されます。

まず、プラグインの名前を入力します。
「離島料金プラグイン」と入力します。

 name [EC-CUBE Sample Plugin]:
 > 離島料金プラグイン

次に、プラグインコードを入力します。
「RemoteIslandFeePlugin」と入力します。

 code [Sample]:
 > RemoteIslandFeePlugin

最後にバージョンを入力します。
何も入力せずに「Enter」を押します。
何も入力しないと1.0.0となります。

 ver [1.0.0]:
 > 

プラグインのインストール&有効化

インストールします。

$ bin/console e:p:i --code=RemoteIslandFeePlugin

 // Clearing the cache for the dev environment with debug true                                                                                                                     
 [OK] Cache for the "dev" environment (debug=true) was successfully cleared.
 [OK] Installed. 

有効化します。

$ bin/console e:p:e --code=RemoteIslandFeePlugin

 // Clearing the cache for the dev environment with debug true                                                                                              
 [OK] Cache for the "dev" environment (debug=true) was successfully cleared.                                                                                                                       
 [OK] Plugin Enabled.

離島管理用のテーブルを作成

それでは、離島を管理するためのテーブルを作成します。

テーブル名やカラム構成は以下にします。

  • テーブル名:plg_remote_island_fee
  • カラム構成
    • 価格:fee
    • 郵便番号:postal_codes

でわ、Entityファイルを作成します。

<?php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Customize\Entity;

use Doctrine\ORM\Mapping as ORM;

if (!class_exists('\Customize\Entity\RemoteIslandFee')) {
    /**
     * RemoteIslandFee
     *
     * @ORM\Table(name="dtb_remote_island_fee")
     * @ORM\InheritanceType("SINGLE_TABLE")
     * @ORM\DiscriminatorColumn(name="discriminator_type", type="string", length=255)
     * @ORM\HasLifecycleCallbacks()
     * @ORM\Entity(repositoryClass="Customize\Repository\RemoteIslandFeeRepository")
     */
    class RemoteIslandFee 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|null
         *
         * @ORM\Column(name="fee", type="decimal", precision=12, scale=2)
         */
        private $fee;

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

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

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

            return $this;
        }

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

        /**
         * Set remote_island_list.
         *
         * @return this
         */
        public function setRemoteIslandList($remote_island_list)
        {
            $this->remote_island_list = $remote_island_list;

            return $this;
        }

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

        public function getPostalCodeLists()
        {
            $postalCodeList = $this->getRemoteIslandList();
            $array = explode(',', $postalCodeList);
            return $array;
        }

    }
}

データベースに反映をします。

まずは確認をしましょう。

$ bin/console d:s:u --dump-sql
The following SQL statements will be executed:
     CREATE TABLE plg_remote_island_fee (
id INT UNSIGNED AUTO_INCREMENT NOT NULL, 
fee NUMERIC(12, 2) NOT NULL, 
postal_codes LONGTEXT DEFAULT NULL,
 discriminator_type VARCHAR(255) NOT NULL, 
PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE = InnoDB

SQL文に問題なさそうであれば、実行しましょう

$ bin/console d:s:u --dump-sql --force

[OK] Database schema updated successfully!

Repositoryファイルも作成しておきましょう

<?php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Plugin\RemoteIslandFeePlugin\Repository;

use Eccube\Repository\AbstractRepository;
use Plugin\RemoteIslandFeePlugin\Entity\RemoteIslandFee;
use Symfony\Bridge\Doctrine\RegistryInterface;

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

    /**
     * @param int $id
     *
     * @return
     */
    public function get($id = 1)
    {
        $RemoteIsland = $this->find($id);

        if (null === $RemoteIsland) {
            throw new \Exception('RemoteIsland not found. id = '.$id);
        }

        return $this->find($id);
    }

}

フォームを作成しましょう

管理画面で編集・登録をするためのフォームを作成しましょう。

料金フォームと郵便番号を登録するフォームを作成します。

<?php

namespace Plugin\RemoteIslandFeePlugin\Form\Type\Admin;

use Symfony\Component\Form\AbstractType;
use Eccube\Form\Type\PriceType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class RemoteIslandFeeType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
        ->add('fee', PriceType::class, [
            'required' => false,
            'attr' => [
                'required' => 'required',
            ]
        ])
        ->add('postal_codes', TextareaType::class, []);

    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'data_class' => 'Plugin\RemoteIslandFeePlugin\Entity\RemoteIslandFee',
            ]
        );
    }

}

Controllerの作成

管理画面で離島料金や郵便番号のフォームを作成しましたので、管理画面で編集・登録できるようにまずはコントローラーを作成しましょう。

<?php

namespace Plugin\RemoteIslandFeePlugin\Controller\Admin;

use Plugin\RemoteIslandFeePlugin\Form\Type\Admin\RemoteIslandFeeType;
use Plugin\RemoteIslandFeePlugin\Repository\RemoteIslandFeeRepository;
use Eccube\Controller\AbstractController;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class RemoteIslandFeeController extends AbstractController
{
    private $remoteIslandFeeRepository;

    public function __construct
    (
        RemoteIslandFeeRepository $remoteIslandFeeRepository
    )
    {
        $this->remoteIslandFeeRepository = $remoteIslandFeeRepository;
    }

    /**
     * @Route("/%eccube_admin_route%/setting/shop/remote_island_fee", name="admin_setting_shop_remote_island_fee")
     * @Template("@RemoteIslandFeePlugin/admin/remote_island_fee.twig")
     */
    public function index(Request $request)
    {
        $RemoteIslandFee = $this->remoteIslandFeeRepository->find(1);

        $builder = $this->formFactory->createBuilder(RemoteIslandFeeType::class,$RemoteIslandFee);

        $form = $builder->getForm();

        $form->handleRequest($request);

        if ($form->isSubmitted() &&  $form->isValid()) {
            log_info('離島料金編集開始');
            $RemoteIslandFee = $form->getData();

            log_info('離島料金登録開始');
            try {
                $this->entityManager->persist($RemoteIslandFee);
                $this->entityManager->flush($RemoteIslandFee);
                log_info('離島料金登録完了');
            } catch(\Exception $e) {
                log_error('離島料金登録エラー',[
                    'err' => $e
                ]);
            }

            $this->addSuccess('admin.common.save_complete', 'admin');

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

        }

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

}

Twigファイルの作成

フォーム、コントローラーを作成したので、次にhtmlを生成するために必要なTwigを作成しましょう。

{% extends '@admin/default_frame.twig' %}

{% set menus = ['setting', 'shop', 'shop_remote_island_fee'] %}

{% block title %}{{ 'admin.setting.shop.remote_island_fee_info'|trans}}{% endblock %}
{% block sub_title %}{{ 'admin.setting.shop'|trans }}{% endblock %}

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

{% block javascript %}
{% endblock %}

{% block main %}
<form name="form1" role="form" class="form-horizontal" id="point_form" method="post"
    action="{{ url('admin_setting_shop_remote_island_fee') }}">

    {{ form_widget(form._token) }}

    <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"><span>{{ 'admin.setting.shop.remote_island_fee_info'|trans }}</span></div>
                    <div id="ex-shop-basic" class="card-body">
                        <div class="row">
                            <div class="col-2"><span>{{ 'admin.setting.shop.remote_island_fee.fee'|trans }}</span></div>
                            <div class="col mb-2">
                                {{ form_widget(form.fee) }}
                                {{ form_errors(form.fee) }}
                            </div>
                        </div>
                        <div class="row">
                            <div class="col-2"><span>{{ 'admin.setting.shop.remote_island_fee.list'|trans }}</span>
                            </div>
                            <div class="col mb-2">
                                {{ form_widget(form.postal_codes,{ 'attr' : { 'rows' : 120 }}) }}
                                {{ form_errors(form.postal_codes) }}
                            </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">
                    </div>
                </div>
                <div id="ex-conversion-action" class="col-6">
                    <div 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>
</form>
{% endblock %}

翻訳ファイルの作成

ページ上に表示する日本語をTwig上では英語で表記していますので、それを翻訳するためのファイルを作成します。

admin.setting.shop.remote_island_fee_info: 離島料金設定
admin.setting.shop.remote_island_fee.fee: 配送料金
admin.setting.shop.remote_island_fee.list: 郵便番号一覧

ナビバーの作成

フォーム、コントローラー、Twigを作成しましたので、管理画面のナビバーから移動できるようにリンクを作成しましょう。

<?php

namespace Plugin\RemoteIslandFeePlugin;

use Eccube\Common\EccubeNav;

class Nav implements EccubeNav
{
    /**
     * @return array
     */
    public static function getNav()
    {
        return [
            'setting' => [
                'children' => [
                    'shop' => [
                        'children' => [
                            'shop_remote_island_fee' => [
                                'name' => 'admin.setting.shop.remote_island_fee_info',
                                'url' => 'admin_setting_shop_remote_island_fee'
                            ]
                        ]
                    ]
                ]
            ]
        ];
    }
}

動作確認

まずはナビバーが設定されているか確認しましょう。

①設定 ②店舗設定 ③離島料金設定

次に離島料金、郵便番号が登録可能か確認します。

郵便番号はカンマ(,)区切りで登録想定です。

離島料金の条件を追加

ここまでで、離島料金と離島対象の郵便番号が登録できるようになりました。次にユーザー側のページで注文対象のユーザーが離島対象の住所であれば、料金を変更するようにしましょう。

送料設定のプロセスを追加

次のファイルを作成し、注文フローに組み込みます。

<?php

namespace Plugin\RemoteIslandFeePlugin\Service;

use Eccube\Entity\BaseInfo;
use Eccube\Entity\ItemHolderInterface;
use Eccube\Entity\OrderItem;
use Eccube\Repository\BaseInfoRepository;
use Eccube\Repository\TaxRuleRepository;
use Eccube\Service\PurchaseFlow\ItemHolderPreprocessor;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Eccube\Annotation\ShoppingFlow;
use Eccube\Service\TaxRuleService;
use Plugin\RemoteIslandFeePlugin\Repository\RemoteIslandFeeRepository;

/**
 * 離島料金判定.
 * 
 * @ShoppingFlow
 */
class DeliveryFeePreprocessor implements ItemHolderPreprocessor
{
    /** @var BaseInfo */
    protected $BaseInfo;

    /**
     * @var RemoteIslandFeeRepository
     */
    protected $remoteIslandFeeRepository;

    /**
     * @var TaxRuleRepository
     */
    protected $taxRuleService;

    /**
     * DeliveryFeePreprocessor constructor.
     *
     */
    public function __construct(
        BaseInfoRepository $baseInfoRepository,
        RemoteIslandFeeRepository $remoteIslandFeeRepository,
        TaxRuleService $taxRuleService
    ) {
        $this->BaseInfo = $baseInfoRepository->get();
        $this->remoteIslandFeeRepository = $remoteIslandFeeRepository;
        $this->taxRuleService = $taxRuleService;
    }

    /**
     * @param ItemHolderInterface $itemHolder
     * @param PurchaseContext $context
     *
     * @throws \Doctrine\ORM\NoResultException
     */
    public function process(ItemHolderInterface $itemHolder, PurchaseContext $context)
    {
        $remoteIsland = $this->remoteIslandFeeRepository->get();

        foreach ($itemHolder->getShippings() as $Shipping) {
            if (in_array($Shipping->getPostalCode(),$remoteIsland->getPostalCodeLists(),true)) {
                $deliveryFeeProduct = 0;
                if ($this->BaseInfo->isOptionProductDeliveryFee()) {
                    /** @var OrderItem $item */
                    foreach ($Shipping->getOrderItems() as $item) {
                        if (!$item->isProduct()) {
                            continue;
                        }
                        $deliveryFeeProduct += $item->getProductClass()->getDeliveryFee() * $item->getQuantity();
                    }
                }
    
                foreach ($itemHolder->getOrderItems() as $item) {
                    if ($item->getProcessorName() === \Eccube\Service\PurchaseFlow\Processor\DeliveryFeePreprocessor::class) {
                        $price = $remoteIsland->getFee() + $deliveryFeeProduct;
                        $taxRate = $item->getTaxRate();
                        $RoundingType = $item->getRoundingType();
                        $tax = $this->taxRuleService->calcTaxIncluded($price,$taxRate,$RoundingType->getId());

                        $item->setPrice($price);
                        $item->setTax($tax);
                    }
                }
            }
        }

    }

}

動作確認

対象の会員情報の郵便番号を変更してください。

管理画面で設定された、料金が反映されていれば問題なしです。

今回の記事はここまでにします。お疲れ様でした。