EC-CUBE4でParsleyを利用したリアルタイムバリデーションの実装方法

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

EC-CUBE4でフォーム画面の入力チェックはサーバ側のチェックで行っている画面と、HTML5のrequired属性を設定して入力チェックを行っている画面の2パターン存在しています。

サーバ側で入力チェックが必須なのは当然ですが、項目のフォーカスアウト時にリアルタイムバリデーションを行うように使い勝手をもう少し良くするために、入力チェックライブラリであるParsleyを利用します。

parsleyjs.org

利用方法はサイトを見れば一目瞭然なのですが、EC-CUBE4で利用する方法を説明します。

Parsleyのダウンロード、配置

Parsley - Download the pieces

こちらよりparsley.zipファイルをダウンロードします。
parsley.zipリンクを押すとGitHubの画面へ遷移するのでSource code (zip)をダウンロード後解凍し、distディレクトリにあるi18nディレクトリとparsley.min.jsを下記のディレクトリへ配置します。

html/template/default/assets/js

Parsleyの利用方法

会員登録入力画面を参考にParsleyを設定してみます。

src/Eccube/Resource/template/default/Entry/index.twig

ファイルを開き、以下の内容をを追記します。

{% block javascript %}
    <script src="//yubinbango.github.io/yubinbango/yubinbango.js" charset="UTF-8"></script>
{% endblock javascript %}{% block javascript %}
    <script src="//yubinbango.github.io/yubinbango/yubinbango.js" charset="UTF-8"></script>
    <script src="{{ asset('assets/js/parsley.min.js') }}"></script>
    <script src="{{ asset('assets/js/i18n/ja.js') }}"></script>
    <script>
        $(function() {
            Parsley.options.trigger = 'keyup focusout change input';

            $('#form1').parsley({
                classHandler: function(ParsleyField) {
                    return ParsleyField.$element;
                },
                errorsWrapper: '<p class="ec-errorMessage"></p>',
                errorTemplate: '<span></span>'
            }).on('field:validated', function() {
                var ok = $('.parsley-error').length === 0;
                if (!ok) {
                    $('.bg-load-overlay').remove();
                    if (this.$element.attr('required')) {
                        if (this.$element.hasClass('parsley-success')) {
                            this.$element.parent('div').removeClass('error');
                        } else {
                            this.$element.parent('div').addClass('error');
                        }
                    }
                }
            });
        });
    </script>
{% endblock javascript %}
Parsley.options.trigger = 'keyup focusout change input';

という箇所は、エラーチェックを行うタイミングを指定していますので、不要な方は削除してください。

次に、formタグにid属性を付与します。

<form method="post" action="{{ url('entry') }}" novalidate class="h-adr"><form id="form1" method="post" action="{{ url('entry') }}" novalidate class="h-adr">

この設定を行う事で、フォーカスアウト時にリアルタイムバリデーションを行ってくれるようになります。

一部デザイン崩れを起こす箇所もありますが、そこは適宜修正してください。

また、独自にデザインを作成している方は、parsleyに記載されている内容をデザインに合わせて修正してください。

Bootstrap4を利用してフォーム画面を作成されている方は以下の内容を適用する事でエラー箇所がキレイに表示されます。

$('#form1').parsley({
    errorClass: 'is-invalid',
    successClass: 'is-valid',
    classHandler: function(ParsleyField) {
        return ParsleyField.$element;
    },
    errorsWrapper: '<div class="invalid-feedback"></div>',
    errorTemplate: '<div></div>'
}).on('field:validated', function() {
    var ok = $('.is-invalid').length === 0;
    if (!ok) {
        $('.bg-load-overlay').remove();
    }
});

今回は必須入力のみのチェックを行なっていますが、他にも電話番号やメールアドレス等の入力チェックもありますので詳しくはParsleyのドキュメントをご覧ください。

EC-CUBE4で利用している郵便番号ライブラリの変更

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

EC-CUBE4で会員登録等に住所補完を目的として利用している郵便番号ライブラリですが、こちらのyubinbangoライブラリを利用しています。

github.com

このライブラリは非常に便利なのですが、EC-CUBEにもissueが上がっているように一度入力した郵便番号へフォーカスを当てると、住所の項目が消えてしまいます。

github.com

通常利用している分にはそこまで大きな問題ではありませんが、どうしても不便だという方もいるため、別の郵便番号ライブラリを反映して対応するようにします。

ajaxzip3の利用

yubinbangoライブラリと同一作者が作成されているajaxzip3を利用します。

github.com

GitHubのREADMEにも記載されている通り、yubinbangoライブラリをオススメしているため、特に不都合がない方は無理に変更する必要はありません。

ソース変更箇所

郵便番号の住所補完を行なっている箇所は以下となります。

src/Eccube/Resource/template/default/Contact/index.twig
src/Eccube/Resource/template/default/Entry/index.twig
src/Eccube/Resource/template/default/Mypage/change.twig
src/Eccube/Resource/template/default/Mypage/delivery_edit.twig
src/Eccube/Resource/template/default/Shopping/nonmember.twig
src/Eccube/Resource/template/default/Shopping/shipping_edit.twig
src/Eccube/Resource/template/default/Shopping/shipping_multiple_edit.twig
src/Eccube/Resource/template/admin/Customer/delivery_edit.twig
src/Eccube/Resource/template/admin/Customer/edit.twig
src/Eccube/Resource/template/admin/Order/edit.twig
src/Eccube/Resource/template/admin/Order/shipping.twig
src/Eccube/Resource/template/admin/Setting/Shop/shop_master.twig

このtwigファイル内に記載されている箇所を以下のように変更します。

<script src="//yubinbango.github.io/yubinbango/yubinbango.js" charset="UTF-8"></script>
↓
<script src="//yubinbango.github.io/ajaxzip3/ajaxzip3.js" charset="UTF-8"></script>

次に、この郵便番号を利用できるようにfunction.jsへ処理を追加します。

処理内容はこちらを参考にさせてもらっています。

qiita.com

  • html/template/default/js/function.js
var postalHandler = function() {
    $postal = $('.p-postal-code');
    $region = $('.p-region-id');
    $locality = $('.p-locality');

    AjaxZip3.zip2addr($postal[0].name, '', $region[0].name, $locality[0].name, '', '', false);
};
$(function() {

    var postalHandler = function() {
        $postal = $('.p-postal-code');
        $region = $('.p-region-id');
        $locality = $('.p-locality');

        AjaxZip3.zip2addr($postal[0].name, '', $region[0].name, $locality[0].name, '', '', false);
    };
    $('.p-postal-code').change(function() {
        if ($(this).val().length >= 7) {
            postalHandler();
        }
    });

});
  • html/template/admin/assets/js/function.js
$(function() {

    var postalHandler = function(){
        $postal = $('.p-postal-code');
        $region = $('.p-region-id');
        $locality = $('.p-locality');

        AjaxZip3.zip2addr($postal[0].name, '', $region[0].name, $locality[0].name, '', '', false);
    };
    $('.p-postal-code').change(function(){
        if ($(this).val().length >= 7) {
            postalHandler();
        }
    });

});

また、app/template/admin/Order/edit.twigファイルのみ以下の処理を{% block javascript %}〜{% endblock javascript %}内へ記述します。

  • app/template/admin/Order/edit.twig
var postalHandler = function(id) {
    $postal = $(id + ' .p-postal-code');
    $region = $(id + ' .p-region-id');
    $locality = $(id + ' .p-locality');

    AjaxZip3.zip2addr($postal[0].name, '', $region[0].name, $locality[0].name, '', '', false);
};

$('#shippingInfo .p-postal-code').change(function() {
    if ($(this).val().length >= 7) {
        postalHandler('#shippingInfo');
    }
});
$('#ordererInfo .p-postal-code').change(function() {
    if ($(this).val().length >= 7) {
        postalHandler('#ordererInfo');
    }
});

以上でajaxzip3が利用できるようになり、郵便番号へフォーカスを当てても一度入力した住所が消えることはありません。

一点対応できていない箇所として、 p-street-addressp-extended-addressが対応できていませんので、そちらも適用させたい方は、

EC-CUBE4で郵便番号にTABでフォーカス移動すると番地が消える - Qiita

の処理をfunction.jsへ適用させるようにしてください。

EC-CUBE4でWordPressの投稿記事を簡単に出力する方法

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

EC-CUBE4でサイトを公開したが、商品紹介記事などはWordPressで作成しており、 その記事をEC-CUBE側で公開したいという要望が時々あります。

そこで簡単にWordPress側から記事を取得してEC-CUBE4へ表示させる方法を書いていきます。

今回は、

というように、同一ドメインでEC-CUBE4とWordPressが動作されている事を想定しています。

前提としてWordPress側でREST APIが有効になっている必要があります。

WordPressから記事情報を取得

トップページへアクセスした時にブログ記事を取得するパターンを想定してみます。今回は簡単にController側で処理をしてその内容を表示させます。

以下の内容をTopController.phpへ記述します。

  • src/Eccube/Controller/TopController.php
/**
 * @Route("/", name="homepage")
 * @Template("index.twig")
 */
public function index(Request $request)
{

    // ブログ情報を取得
    $posts = json_decode(file_get_contents($request->getSchemeAndHttpHost().$request->getBasePath().'/blog/wp-json/wp/v2/posts?per_page=3&_embed'));

    $blogDatas = [];
    foreach ($posts as $data) {
        $item = [];
        $item['title'] = $data->title;
        $item['date'] = $data->date;
        $item['link'] = $data->link;
        $name = 'wp:featuredmedia';
        if (isset($data->_embedded->{$name})) {
            $item['attachment'] = $data->_embedded->{$name}[0];
        }
        $name = 'wp:term';
        if (isset($data->_embedded->{$name})) {
            $item['category'] = $data->_embedded->{$name}[0];
        }
        $blogDatas[] = $item;
    }

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

ソース中にある

// ブログ情報を取得
$posts = json_decode(file_get_contents($request->getSchemeAndHttpHost().$request->getBasePath().'/blog/wp-json/wp/v2/posts?per_page=3&_embed'));

WordPress側と通信して記事を取得する部分となります。

$request->getSchemeAndHttpHost().$request->getBasePath()

ドメイン部分を構築させています。Requestクラスについて詳しくはこちらをご覧ください。 fivestar.hatenablog.com

その後に続く、

/blog/wp-json/wp/v2/posts?per_page=3&_embed

REST APIを利用した記事取得部分となります。

「per_page=xx」というパラメータをつける事でxx件まで記事のデータを取得することができます。 今回は3としているため、3件データを取得しています。

また、「&_embed」というパラメーターはアイキャッチの情報を含めたデータを取得することができます。 他のAPIの種類やパラメーターはこちらをご覧ください。

Reference | REST API Handbook | WordPress Developer Resources

これでデータが取得できましたので、twig側へ扱いやすいように整形します。

    $blogDatas = [];
    foreach ($posts as $data) {
        $item = [];
        $item['title'] = $data->title;
        $item['date'] = $data->date;
        $item['link'] = $data->link;
        $name = 'wp:featuredmedia';
        if (isset($data->_embedded->{$name})) {
            $item['attachment'] = $data->_embedded->{$name}[0];
        }
        $name = 'wp:term';
        if (isset($data->_embedded->{$name})) {
            $item['category'] = $data->_embedded->{$name}[0];
        }
        $blogDatas[] = $item;
    }

この箇所は、WordPress側から取得したデータ必要な分だけ取り出しています。タイトルや日付、リンク先など最低限必要なものを取り出しています。

これでController側の処理が終わりました。次にTwigへの記述方法を説明します。

Twigへ取得した記事情報を表示させる

twig側は特に難しい事をするわけでもなく普通にタグを記述するのみです。今回は直接src直下にあるindex.twigを修正します。

  • src/Eccube/Resource/template/default/index.twig
{% if blogDatas|length > 0 %}
    <section class="blog">
        <div class="row">
            {% for blogData in blogDatas %}
                <div class="col-3">
                    <a href="{{ blogData.link }}" target="_blank" rel="noopener">
                        {% if blogData.attachment is defined %}
                            <figure class="thumbnail">
                                <img src="{{ blogData.attachment.source_url }}" alt="{{ blogData.title.rendered }}">
                            </figure>
                        {% endif %}
                        {% if blogData.category is defined %}
                            <p class="category">{% for category in blogData.category %}{{ category.name }}{% if not loop.last %},{% endif %}{% endfor %}</p>
                        {% endif %}
                        <p class="title">{{ blogData.title.rendered }}</p>
                    </a>
                    <time>{{ blogData.date|slice(0, 10)|replace({'-': '.'}) }}</time>
                </div>
            {% endfor %}
        </div>
    </section>
{% endif %}

blogDatasはController側から渡された変数となり、記事が存在すれば表示させるという処理を行っています。

タグの記述方法はデザインに合わせて記述してください。

上記を記述後、EC-CUBE4のトップページへアクセスするとブログ記事が表示されるようになります。

以上でWordPressから記事を取得して表示させる方法となりますので参考にしてください。

GoogleショッピングへEC-CUBEに登録されている商品を登録する方法

Googleショッピングが日本でも無料で利用できることがアナウンスされています。

www.suzukikenichi.com

すでにEC-CUBEで掲載されている商品をGoolgeショッピングへ登録するには手動またはスプレッドシートで登録できますが、 バッチを利用して一括で登録できる方法を説明します。

準備としてGoogleマーチャントセンターへ登録が必要になります。

support.google.com

登録が完了したら、メニューのレンチアイコンからContent APIメニューを選択します。 f:id:amidaike:20201024095010p:plain

選択後、「認証」メニューを選択し新しいAPIキーを+アイコンを押して作成します。 f:id:amidaike:20201024095059p:plain

作成すると、content-api-key.jsonファイルがダウンロードされるのでこれを利用して連携処理を行います。

ここまで準備ができたら、今回はEC-CUBE3でGoogleショッピングの連携を行います。

Googleショッピング連携を行うにはライブラリが公開されていますので、 そのライブラリを利用して処理を行います。

ターミナルを起動し、EC-CUBE3がインストールされているディレクトリまで移動します。

以下のコマンドを実行してライブラリをインストールします。

最近、composerが2系へバージョンアップされていますので、混乱を防ぐためにもローカル環境へcomposer.pharをダウンロードし、composerコマンドを実行するようにします。

wget https://getcomposer.org/download/1.10.17/composer.phar

をしてローカル環境に落としてきた後、以下を実行します。

php composer.phar require google/apiclient:"^2.0" --ignore-platform-reqs

--ignore-platform-reqsというパラメーターは、Composerの依存性チェックでphpバージョンエラーを一時的に回避するために付与しています。

インストールが完了したら、先ほどダウンロードしたcontent-api-key.jsonファイルを、

app/config/eccube

ディレクトリへ保存します。

また、マーチャントIDが必要になるのですがこちらのIDはGoogleマーチャントセンター管理画面の右上にあるユーザー名の横にある数字となります。

次にCommandを利用したプログラムを作成します。

src/Eccube/Command/ProductUpdateCommand.php

というファイルを作成し、以下のソースを記入します。

<?php

namespace Eccube\Command;

use Eccube\Application;
use Eccube\Entity\BaseInfo;
use Eccube\Entity\Product;
use Knp\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * Class ProductUpdateCommand
 *
 * @package Eccube\Command\Mall\Google
 */
class ProductUpdateCommand extends Command
{

    protected function configure()
    {
        $this
            ->setName('google:productupdate')
            ->setDescription('Googleショッピング連携処理')
            ->setHelp(<<<EOF
The <info>%command.name%</info> command Product Update Google Shopping,
EOF
            );
    }

    /**
     * Googleショッピング連携
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return int|null|void
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $io = new SymfonyStyle($input, $output);

        /** @var Application $app */
        $app = $this->getSilexApplication();

        $io->title('Googleショッピング連携処理 Start.');

        putenv('GOOGLE_APPLICATION_CREDENTIALS='.$app['config']['root_dir'].'/app/config/eccube/content-api-key.json');

        $merchantId = 'マーチャントIDを設定';
        $url = 'サイトURLを設定'; // 例) https://example.com

        $client = new \Google_Client();
        $client->useApplicationDefaultCredentials();

        $client->addScope('https://www.googleapis.com/auth/content');

        $service = new \Google_Service_ShoppingContent($client);

        $Products = $app['eccube.repository.product']->findBy(array('Status' => 1));



        // バッチ処理
        $entries = [];
        $i = 0;
        /** @var Product $Product */
        foreach ($Products as $Product) {

            $entry = new \Google_Service_ShoppingContent_ProductsCustomBatchRequestEntry();
            $entry->setBatchId($i);
            $entry->setMerchantId($merchantId);
            $entry->setMethod('insert');

            $ShoppingProduct = new \Google_Service_ShoppingContent_Product();
            $ShoppingProduct->setOfferId($Product->getCodeMin());
            $ShoppingProduct->setTitle($Product->getName());
            $ShoppingProduct->setDescription($Product->getDescriptionDetail());
            $ShoppingProduct->setLink($url.'/products/detail/'.$Product->getId());
            $ShoppingProduct->setImageLink($url.$app['config']['image_save_urlpath'].'/'.$Product->getMainListImage());
            $ShoppingProduct->setContentLanguage('ja');
            $ShoppingProduct->setTargetCountry('JP');
            $ShoppingProduct->setChannel('online');
            $ShoppingProduct->setAvailability('in stock');
            if (!$Product->getStockFind()) {
                $ShoppingProduct->setAvailability('out of stock');
            }

            // $ShoppingProduct->setCondition('');
            $ShoppingProduct->setGoogleProductCategory('1025'); // https://www.google.com/basepages/producttype/taxonomy-with-ids.ja-JP.txt より適切なカテゴリIDを設定
            $ProductCategories = $Product->getProductCategories();
            $path = '';
            foreach ($ProductCategories as $ProductCategory) {
                $path = $ProductCategory->getCategory()->getRootCategory()->getName();
            }
            $ShoppingProduct->setBrand($path);

            // $ShoppingProduct->setGender('');
            // $ShoppingProduct->setColor('');

            $price = new \Google_Service_ShoppingContent_Price();
            $price->setValue($Product->getPrice02IncTaxMin());
            $price->setCurrency('JPY');

            $shipping_price = new \Google_Service_ShoppingContent_Price();
            $shipping_price->setValue('0'); // 配送料を設定
            $shipping_price->setCurrency('JPY');

            $shipping = new \Google_Service_ShoppingContent_ProductShipping();
            $shipping->setPrice($shipping_price);
            $shipping->setCountry('JP');
            // $shipping->setService('Standard shipping');

            $ShoppingProduct->setPrice($price);
            $ShoppingProduct->setShipping(array($shipping));

            $entry->setProduct($ShoppingProduct);

            $entries[] = $entry;

            $i++;

        }

        $batchRequest = new \Google_Service_ShoppingContent_ProductsCustomBatchRequest();
        $batchRequest->setEntries($entries);
        $result = $service->products->custombatch($batchRequest);

        $io->text('処理件数 : '.$result->count());

        $io->success('Googleショッピング連携処理 End.');

    }
}

上記ファイルを作成後、

php app/console google:productupdate

を実行するとGoogleショッピング連携処理が行われます。

処理を実行後、1時間程度待つとマーチャントセンターへ商品が登録されているのが確認できます。

正常に登録されていればGoogleショッピング連携処理が完了なので、 運用に合わせて適宜対応してください。

4系については今後公開します。

EC-CUBE4で会員登録時に会員登録ポイントを付与させる方法

EC-CUBE4ではポイント機能は標準で対応されるようになりましたが、商品購入時にしかポイントが付与されません。

EC-CUBE2系をお使いの方であれば、会員登録ポイントは付与できますので同じような機能をEC-CUBE4でも実現させてみます。

変更させる箇所は以下の通りです。

今回はDBで会員登録ポイントを管理するのではなく、設定ファイルで管理させてます。

  • app/config/eccube/packages/eccube.yaml
  eccube_point_entry_point: 200 # ポイント機能;会員登録で獲得するポイント数

記述する箇所は最下行で構いません。この例だと200ポイントを付与させるようにしています。

  • src/Eccube/Controller/EntryController.php
use Eccube\Service\PointHelper;

・
・
・
         
/**
 * @var PointHelper
 */
protected $pointHelper;

・
・
・

/**
 * EntryController constructor.
 *
 * @param CartService $cartService
 * @param CustomerStatusRepository $customerStatusRepository
 * @param MailService $mailService
 * @param BaseInfoRepository $baseInfoRepository
 * @param CustomerRepository $customerRepository
 * @param EncoderFactoryInterface $encoderFactory
 * @param ValidatorInterface $validatorInterface
 * @param TokenStorageInterface $tokenStorage
 * @param PointHelper $pointHelper
 */
public function __construct(
    CartService $cartService,
    CustomerStatusRepository $customerStatusRepository,
    MailService $mailService,
    BaseInfoRepository $baseInfoRepository,
    CustomerRepository $customerRepository,
    EncoderFactoryInterface $encoderFactory,
    ValidatorInterface $validatorInterface,
    TokenStorageInterface $tokenStorage,
    PointHelper $pointHelper
)
{
    $this->customerStatusRepository = $customerStatusRepository;
    $this->mailService = $mailService;
    $this->BaseInfo = $baseInfoRepository->get();
    $this->customerRepository = $customerRepository;
    $this->encoderFactory = $encoderFactory;
    $this->recursiveValidator = $validatorInterface;
    $this->tokenStorage = $tokenStorage;
    $this->cartService = $cartService;
    $this->pointHelper = $pointHelper;
}

・
・
・

// 会員登録ポイントの付与
case 'complete':
    log_info('会員登録開始');

    $encoder = $this->encoderFactory->getEncoder($Customer);
    $salt = $encoder->createSalt();
    $password = $encoder->encodePassword($Customer->getPassword(), $salt);
    $secretKey = $this->customerRepository->getUniqueSecretKey();

    $Customer
        ->setSalt($salt)
        ->setPassword($password)
        ->setSecretKey($secretKey)
        ->setPoint(0);

    $this->entityManager->persist($Customer);
    $this->entityManager->flush();

    // 会員登録ポイントの付与
    // 所有ポイントが更新されるので再度flushする必要あり
    $this->pointHelper->addEntryPoint($Customer);
    $this->entityManager->persist($Customer);
    $this->entityManager->flush();

PointHelperを利用して会員登録ポイントを付与させる処理を追加します。

  • src/Eccube/Service/PointHelper.php
・
・
・

/**
 * @var EccubeConfig
 */
private $eccubeConfig;

・
・
・

/**
 * PointHelper constructor.
 *
 * @param BaseInfoRepository $baseInfoRepository
 * @param EntityManagerInterface $entityManager
 */
public function __construct(BaseInfoRepository $baseInfoRepository, EntityManagerInterface $entityManager, EccubeConfig $eccubeConfig)
{
    $this->baseInfoRepository = $baseInfoRepository;
    $this->entityManager = $entityManager;
    $this->eccubeConfig = $eccubeConfig;
}

・
・
・


// 会員登録時ポイントを付与する
public function addEntryPoint(Customer $Customer)
{
    $point = $this->eccubeConfig['eccube_point_entry_point'];

    $pointHistory = new PointHistory();
    $pointHistory->setRecordType(PointHistory::TYPE_ADD);
    $pointHistory->setRecordEvent(PointHistory::EVENT_ENTRY);
    $pointHistory->setPoint($point);
    $pointHistory->setCustomer($Customer);
    $em = $this->entityManager;
    $em->persist($pointHistory);
    $em->flush($pointHistory);

    // ポイントの付与
    $Customer->setPoint($Customer->getPoint() + $point);
}

PointHelper.phpに会員登録時にポイントを付与する関数を追加します。

今回はかなり省略して変更点を記述していますので、もう少し詳しく書いて欲しければコメントしてください。

EC-CUBE4でCSVファイルを常にダブルクォーテーション付きで出力させる方法

EC-CUBE4では管理画面の商品管理や受注管理でCSV出力をする場合、項目によってはダブルクォーテーションが付いたり付かなかったりとバラバラな状態で出力されます。

理由はfputcsv関数を利用しているからです。

PHP: fputcsv - Manual

ただ、サイトによっては常にダブルクォーテーションをつけて欲しいという要望があります。 その時は以下の修正を行うことで対応が可能です。

説明は割愛して、修正した関数だけ載せておきます。 src/Eccube/Service/CsvExportService.phpファイルの以下の関数を修正します。

  • exportHeader関数の修正
<?php

/**
 * ヘッダ行を出力する.
 * このメソッドを使う場合は, 事前にinitCsvType($CsvType)で初期化しておく必要がある.
 */
public function exportHeader()
{
    if (is_null($this->CsvType) || is_null($this->Csvs)) {
        throw new \LogicException('init csv type incomplete.');
    }

    $row = [];
    foreach ($this->Csvs as $Csv) {
        $row[] = '"'.$Csv->getDispName().'"';
    }

    $str = implode($this->eccubeConfig['eccube_csv_export_separator'], $row);
    $str .= "\r\n";
    $csv = mb_convert_encoding($str, $this->eccubeConfig['eccube_csv_export_encoding'], 'UTF-8');

    $this->fopen();
    // $this->($row);
    fputs($this->fp, $csv);
    $this->fclose();
}
  • fputcsv関数の修正
<?php

/**
 * @param $row
 */
public function fputcsv($row)
{
    if (is_null($this->convertEncodingCallBack)) {
        $this->convertEncodingCallBack = $this->getConvertEncodingCallback();
    }

    // fputcsv($this->fp, array_map($this->convertEncodingCallBack, $row), $this->eccubeConfig['eccube_csv_export_separator']);

    $rows = [];
    foreach ($row as $r) {
        $rows[] = '"'.$r.'"';
    }

    $str = implode($this->eccubeConfig['eccube_csv_export_separator'], $rows);
    $str .= "\r\n";
    $csv = mb_convert_encoding($str, $this->eccubeConfig['eccube_csv_export_encoding'], 'UTF-8');

    fputs($this->fp, $csv);
}

以上の対応でCSV出力時は常にダブルクォーテーションが付くようになります。

EC-CUBE4でMySQL利用時に絵文字を利用する方法

EC-CUBE4をMySQLで利用している場合、標準のままだと絵文字をDBへ登録しようとするとシステムエラーが発生します。 それを防ぐためには文字コードを「utf8mb4」へ変更する必要があります。 前提条件としてMySQL5.7以上を対象としていますが、 MySQL5.6以下でも設定を変更する事で利用可能となります。

パッケージを解凍後EC-CUBEをインストールする前に、 最低限以下の設定をすることで絵文字が登録できるようになります。

  • app/config/eccube/packages/doctrine.yaml
parameters:
    # Adds a fallback DATABASE_URL if the env var is not set.
    # This allows you to run cache:warmup even if your
    # environment variables are not available yet.
    # You should not need to change this value.
    env(DATABASE_URL): ''
    env(DATABASE_SERVER_VERSION): ~
doctrine:
    dbal:
        driver: 'pdo_sqlite'
        server_version: "%env(DATABASE_SERVER_VERSION)%"
        charset: utf8mb4

        # for mysql only
        default_table_options:
          collate: 'utf8mb4_general_ci'

        # With Symfony 3.3, remove the `resolve:` prefix
        url: '%env(DATABASE_URL)%'

        # types
        types:
            datetime: 'Eccube\Doctrine\DBAL\Types\UTCDateTimeType'
            datetimetz: 'Eccube\Doctrine\DBAL\Types\UTCDateTimeTzType'
    orm:
        auto_generate_proxy_classes: '%kernel.debug%'
        naming_strategy: doctrine.orm.naming_strategy.underscore
        auto_mapping: true
        dql:
            string_functions:
                NORMALIZE: Eccube\Doctrine\ORM\Query\Normalize
            numeric_functions:
                EXTRACT: Eccube\Doctrine\ORM\Query\Extract
        filters:
            option_nostock_hidden:
                class: Eccube\Doctrine\Filter\NoStockHiddenFilter
                enabled: false
            incomplete_order_status_hidden:
                class: Eccube\Doctrine\Filter\OrderStatusFilter
                enabled: false

変更点は12行目と16行目にある
「utf8」「utf8_general_ci」を
「utf8mb4」「utf8mb4_general_ci」
に変更します。

  • src/Eccube/Controller/Install/InstallController.php
if (strpos($params['url'], 'mysql') !== false) {
    $params['charset'] = 'utf8mb4';
    $params['defaultTableOptions'] = [
        'collate' => 'utf8mb4_general_ci',
    ];
}

変更点は564行目と566行目にある
「utf8」「utf8_general_ci」を
「utf8mb4」「utf8mb4_general_ci」
に変更することで絵文字対応が可能となります。 前提として、MySQLから

show variables like "chara%";

を実行時に、 character_set_databaseutf8mb4 になっている必要があります。 そのため、データベースを作成時は必ず utf8mb4 で作成するようにしましょう。

create database [データベース名] default character set utf8mb4;

最後は必ず「utf8mb4」を指定してください。

もう一点環境によってはDBの型を変更する必要があります。

  • src/Eccube/Controller/Entity/Product.php

ここで4000として定義されている、 note description_list description_detai search_word

@ORM\Column(name="note", type="text", nullable=true)

というようにtext型へ変更させる必要があります。

PostgreSQLの方は試してないのですが、特に変更することなく対応できるのではないかと思います。