Quantcast
Channel: EC-CUBE アーカイブ - あずみ.net
Viewing all 271 articles
Browse latest View live

【EC-CUBE4】ご注文手続きのお問い合わせ項目を必須にするプラグインを作る方法

$
0
0

EC-CUBE4のご注文手続きのお問い合わせ項目を必須にするプラグインを作る方法です。

まずは以下のコマンドでプラグインの雛形を作ります。

プラグインコードはなんでも良いのですが、今回は「ShoppingMessage」にしてください。

bin/console eccube:plugin:generate

ご注文手続きフォームの拡張

ご注文フォームを拡張します。

自動生成されたプラグイン一式内のForm/Extensionディレクトリ内に以下のOrderTypeExtention.phpファイルを設置して下さい。

<?php

namespace Plugin\ShoppingMessage\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;
use Eccube\Form\Type\Shopping\OrderType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;

/**
 * ご注文手続きのお問い合わせを必須にする
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class OrderTypeExtension extends AbstractTypeExtension {

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $options = $builder->get('message')->getOptions();
        $options['required'] = true;
        $options['constraints'][] = new Assert\NotBlank;

        $builder->add('message', TextareaType::class, $options);
    }

    public function getExtendedType()
    {
        return OrderType::class;
    }

}

上記はOrderTypeクラスで設定されているお問い合わせ(message) を必須にするように拡張しています。

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

以上で完成です。

プラグインのインストールと有効化を行うと適用されます。

このプラグインのファイル一式はこちら


【EC-CUBE4】ログインしたら何かするプラグインを作る方法

$
0
0

EC-CUBE4でログインしたら何かするプラグインを作る方法です。

まずは以下のコマンドでプラグインの雛形を作ります。

プラグインコードはなんでも良いのですが、今回は「InteractiveLogin」にしてください。

bin/console eccube:plugin:generate

ログインしたときに何かするためのイベント登録

ログイン後に何かするためには以下のように処理とイベント登録を行う必要があります。

自動生成されたプラグイン一式内にEventListenerディレクトリを作って、以下のSecurityListener.phpを設置して下さい。

<?php

namespace Plugin\InteractiveLogin\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;

/**
 * Description of SecurityListener
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class SecurityListener implements EventSubscriberInterface {

    public static function getSubscribedEvents(): array
    {
        return [
            SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin',
        ];
    }
    
    /**
     * ログインしたときに何かする
     * 
     * @param InteractiveLoginEvent $event
     */
    public function onInteractiveLogin(InteractiveLoginEvent $event)
    {
        // ログインユーザーデータを取得
        $User = $event
                ->getAuthenticationToken()
                ->getUser();
    }

}

以上で完成です。

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

プラグインのインストールと有効化を行うと動作します。

このプラグインのファイル一式はこちら

EC-CUBE4の新着情報をブラウザでPUSH通知するプラグインを作る方法

$
0
0

EC-CUBE4の新着情報をブラウザでPUSH通知するプラグインを作る方法です。

まずは以下のコマンドでプラグインの雛形を作ります。

プラグインコードはなんでも良いのですが、今回は「Push4」にしてください。

bin/console eccube:plugin:generate

プッシュ通知するためのTwigファイルを用意

プッシュ通知するために今回はPush.jsを使用します。

以下のTwigファイルをPlugin/Push4/Resource/template/default/snippet/push.twigに設置してください。

{% block javascripts %}
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/push.js/1.0.7/push.min.js"></script>
    <script type="text/javascript">
        // アクセスから30秒後に通知を受け取る許諾ポップアップを表示
        setTimeout(function () {
            Push.Permission.request(onGranted, onDenied);
        }, 30 * 1000);

        function onGranted() {
            console.log('Granted!');
        }

        function onDenied() {
            console.log('Denied');
        }

        if (Push.Permission.has()) {
            Push.create("{{ BaseInfo.shop_name }}", {
                body:    "{{ News.title }}",
                icon:    "{{ asset('assets/img/common/favicon.ico') }}",
                timeout: 4000,
                onClick: function () {
                    {% if News.url %}
                    location.href = '{{ News.url }}'
                    {% endif %}
                    this.close();
                }
            });
        }
    </script>
{% endblock %}

フロントの場合のみTwigファイルを読み込む

上記で用意したTwigファイルをフロントの場合のみ読み込むイベントを用意します。

以下の内容をEvent.phpに追記してください。

<?php

namespace Plugin\Push4;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Eccube\Request\Context;
use Eccube\Event\TemplateEvent;
use Eccube\Repository\NewsRepository;

class Event implements EventSubscriberInterface {

    /**
     * @var \Eccube\Request\Context
     */
    protected $requestContext;

    /**
     * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface 
     */
    protected $eventDispatcher;

    /**
     * @var \Eccube\Repository\NewsRepository 
     */
    protected $newsRepository;

    public function __construct(
            Context $requestContext,
            EventDispatcherInterface $eventDispatcher,
            NewsRepository $newsRepository
    )
    {
        $this->requestContext = $requestContext;
        $this->eventDispatcher = $eventDispatcher;
        $this->newsRepository = $newsRepository;
    }

    /**
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::CONTROLLER_ARGUMENTS => [['onKernelController', 100000000]],
        ];
    }

    public function onKernelController(FilterControllerEvent $event)
    {
        // フロントページでない場合はスルー
        if (!$this->requestContext->isFront()) {
            return;
        }

        if ($event->getRequest()->attributes->has('_template')) {
            $template = $event->getRequest()->attributes->get('_template');
            $this->eventDispatcher->addListener($template->getTemplate(), function (TemplateEvent $templateEvent) {
                $templateEvent->addSnippet('@Push4/default/snippet/push.twig');
                
                $News = $this->newsRepository->getList();
                $templateEvent->setParameter('News', $News->first());
            });
        }
    }

}

以上で完成です。

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

プラグインのインストールと有効化を行うと動作します。

ちなみに上記の実装ではページをリロードするたびにプッシュ通知されます。

このプラグインのファイル一式はこちら

EC-CUBE4でタイトルタグの内容を書き換える

$
0
0

EC-CUBE4でタイトルタグの内容を書き換える方法です。

まずCusotomizeディレクトリ内にEventListenerディレクトリを設置してください。

次に以下のようにTitleListner.phpを作成してEventListener内に設置してください。

<?php

namespace Customize\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Eccube\Request\Context;

/**
 * Description of TitleListner
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class TitleListner implements EventSubscriberInterface {
    
    public function __construct(
            Context $requestContext
    )
    {
        $this->requestContext = $requestContext;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => [['onKernelResponse', 100000000]],
        ];
    }
    
    public function onKernelResponse(FilterResponseEvent $event)
    {
        // フロントページでない場合はスルー
        if (!$this->requestContext->isFront()) {
            return;
        }
        
        $response = $event->getResponse();
        $content = $response->getContent();
        
        $response->setContent(preg_replace("/<title>(.*)<\/title>/", "<title>タイトル変更</title>", $content));
        $event->setResponse($response);
    }

}

以上で完成です。

これによりフロントのすべてのページのタイトルが「タイトル変更」に書き換わります。

特定のページのみ書き換えたい場合は、ルーティング名を取得して条件分岐すれば対応できると思います。

EC-CUBE4の管理画面にログインしたら自分が登録した商品しか商品一覧に表示されないようにする方法

$
0
0

EC-CUBE4の管理画面にログインしたら自分が登録した商品しか商品一覧に表示されないようにする方法です。

まずCusotomizeディレクトリ内にRepositoryディレクトリを設置してください。

次に以下のようにAdminProductListByMenberCustomizer.phpを作成してRepository内に設置してください。

<?php

namespace Customize\Repository;

use Eccube\Doctrine\Query\WhereCustomizer;
use Eccube\Doctrine\Query\WhereClause;
use Eccube\Repository\QueryKey;
use Eccube\Entity\Member;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * Description of AdminProductListByMenberCustomizer
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class AdminProductListByMenberCustomizer extends WhereCustomizer {

    /**
     * @var TokenStorageInterface
     */
    protected $tokenStorage;

    public function __construct(TokenStorageInterface $tokenStorage)
    {
        $this->tokenStorage = $tokenStorage;
    }
    
    protected function createStatements($params, $queryKey)
    {
        if (null !== $token = $this->tokenStorage->getToken()) {
            if($token->getUser() instanceof Member) {
                return [WhereClause::eq('p.Creator', ':creator_id', ['creator_id' => $token->getUser()])];
            }
        }
        
        return [];
    }

    public function getQueryKey(): string
    {
        return QueryKey::PRODUCT_SEARCH_ADMIN;
    }

}

以上で完成です。

これで他のメンバーが登録した商品は管理画面の商品一覧に表示されなくなります。

EC-CUBE4で商品送料が設定されている場合に一番高い送料を適用させる方法

$
0
0

EC-CUBE4で商品送料が設定されている場合に一番高い送料を適用させる方法です。

まずCusotomizeディレクトリ内にService/PurchaseFlow/Processorディレクトリを設置してください。

次に以下のようにHighestDeliveryFeePreprocessor.phpを作成してProcessor内に設置してください。

<?php

namespace Customize\Service\PurchaseFlow\Processor;

use Eccube\Service\PurchaseFlow\ItemHolderPreprocessor;
use Eccube\Entity\ItemHolderInterface;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Eccube\Repository\BaseInfoRepository;
use Eccube\Repository\DeliveryFeeRepository;
use Eccube\Annotation\ShoppingFlow;

/**
 * Description of HighestDeliveryFeePreprocessor
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 * 
 * @ShoppingFlow
 */
class HighestDeliveryFeePreprocessor implements ItemHolderPreprocessor {

    /** @var BaseInfo */
    protected $BaseInfo;

    /**
     * @var DeliveryFeeRepository
     */
    protected $deliveryFeeRepository;

    /**
     * DeliveryFeePreprocessor constructor.
     *
     * @param BaseInfoRepository $baseInfoRepository
     * @param DeliveryFeeRepository $deliveryFeeRepository
     */
    public function __construct(
            BaseInfoRepository $baseInfoRepository,
            DeliveryFeeRepository $deliveryFeeRepository
    )
    {
        $this->BaseInfo = $baseInfoRepository->get();
        $this->deliveryFeeRepository = $deliveryFeeRepository;
    }

    public function process(ItemHolderInterface $itemHolder, PurchaseContext $context)
    {
        $this->updateDeliveryFeeItem($itemHolder);
    }

    /**
     * @param ItemHolderInterface $itemHolder
     */
    private function updateDeliveryFeeItem(ItemHolderInterface $itemHolder)
    {
        $Order = $itemHolder;

        // 配送先毎に送料計算
        foreach ($Order->getShippings() as $Shipping) {

            // 商品ごとの送料設定が有効の場合
            if ($this->BaseInfo->isOptionProductDeliveryFee()) {

                // ここに商品毎に送料を格納する
                $deliveryFeeProducts = [];

                //明細の内容を確認
                foreach ($Shipping->getOrderItems() as $item) {
                    // 商品明細ではない場合スルー
                    if (!$item->isProduct()) {
                        continue;
                    }

                    // 商品送料に数量をかけて配列に格納
                    $deliveryFeeProducts[] = $item->getProductClass()->getDeliveryFee() * $item->getQuantity();
                }

                // 都道府県別送料を取得
                $DeliveryFee = $this->deliveryFeeRepository->findOneBy([
                    'Delivery' => $Shipping->getDelivery(),
                    'Pref' => $Shipping->getPref(),
                ]);

                // 商品送料が設定されていた場合
                if ($deliveryFeeProducts) {
                    // EC-CUBE本体で設定された送料を更新する
                    foreach ($Shipping->getOrderItems() as $item) {
                        // 送料明細の場合
                        if ($item->isDeliveryFee()) {
                            // 各商品の送料×数量で一番高い送料を取得
                            $deliveryFeeProduct = max($deliveryFeeProducts);

                            // 都道府県別送料と商品送料で高いほうの送料をセット
                            $item->setPrice(max([$DeliveryFee->getFee(), $deliveryFeeProduct]));
                        }
                    }
                }
            }
        }
    }

}

以上で完成です。

これで商品送料が設定されいる場合、その中で一番高い送料が送料明細にセットされます。

また、商品送料より都道府県別送料のほうが高い場合は都道府県別送料が適用されます。

EC-CUBE4のフロントの検索ボックスで商品タグ検索できるようにする方法

$
0
0

EC-CUBE4のフロントの検索ボックスで商品タグ検索できるようにする方法です。

まずCusotomizeディレクトリ内にRepositoryディレクトリを設置してください。

次に以下のようにTagSearchCustomizer.phpを作成してRepository内に設置してください。

<?php

namespace Customize\Repository;

use Eccube\Doctrine\Query\QueryCustomizer;
use Eccube\Doctrine\Query\WhereClause;
use Eccube\Repository\QueryKey;
use Doctrine\ORM\QueryBuilder;
use Eccube\Repository\TagRepository;

/**
 * Description of TagSearchCustomizer
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class TagSearchCustomizer implements QueryCustomizer {

    protected $tagRepository;

    public function __construct(TagRepository $tagRepository)
    {
        $this->tagRepository = $tagRepository;
    }

    public function customize(QueryBuilder $builder, $params, $queryKey)
    {
        if ($params['name']) {
            // タグテーブルから検索キーワードに部分一致するタグを取得
            $result = $this->tagRepository->createQueryBuilder('t')
                    ->where("t.name LIKE :name")
                    ->setParameter("name", '%' . $params['name'] . '%')
                    ->getQuery()
                    ->getResult();

            // QueryBuilderに対してタグを検索対象するように設定
            $builder->innerJoin("p.ProductTag", "pt");
            foreach ($result as $tag) {
                $builder->orWhere("pt.Tag = :Tag");
                $builder->setParameter("Tag", $tag->getId());
            }
        }
    }

    public function getQueryKey(): string
    {
        return QueryKey::PRODUCT_SEARCH;
    }

}

以上で完成です。

これでフロントの検索ボックスにタグを入力して検索するとマッチする商品が表示されます。

EC-CUBE4の管理画面の商品一覧で商品タグ検索できるようにする方法

$
0
0

EC-CUBE4の管理画面の商品一覧でタグ検索できるようにする方法です。

まずCusotomizeディレクトリ内にDoctrine/Queryディレクトリを設置してください。

次に以下のようにAdminProductTagSearchCustomizer.phpを作成してQuery内に設置してください。

<?php

namespace Customize\Doctrine\Query;

use Eccube\Doctrine\Query\QueryCustomizer;
use Eccube\Doctrine\Query\WhereClause;
use Eccube\Repository\QueryKey;
use Doctrine\ORM\QueryBuilder;
use Eccube\Repository\TagRepository;

/**
 * Description of AdminProductTagSearchCustomizer
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class AdminProductTagSearchCustomizer implements QueryCustomizer {

    protected $tagRepository;

    public function __construct(TagRepository $tagRepository)
    {
        $this->tagRepository = $tagRepository;
    }

    public function customize(QueryBuilder $builder, $params, $queryKey)
    {
        if ($params['id']) {
            // タグテーブルから検索キーワードに部分一致するタグを取得
            $result = $this->tagRepository->createQueryBuilder('t')
                    ->where("t.name LIKE :name")
                    ->setParameter("name", '%' . $params['id'] . '%')
                    ->getQuery()
                    ->getResult();

            // QueryBuilderに対してタグを検索対象するように設定
            $builder->innerJoin("p.ProductTag", "pt");
            foreach ($result as $tag) {
                $builder->orWhere("pt.Tag = :Tag");
                $builder->setParameter("Tag", $tag->getId());
            }
        }
    }

    public function getQueryKey(): string
    {
        return QueryKey::PRODUCT_SEARCH_ADMIN;
    }

}

以上で完成です。

これで管理画面の商品一覧にてタグを入力して検索するとマッチする商品が表示されます。


EC-CUBE4で商品一覧を更新日時順で並べ替える方法

$
0
0

EC-CUBE4で商品一覧を更新日時順で並べ替える方法です。

最初に、管理画面>設定>システム設定>マスターデータ管理の「mtb_product_list_order_by」に更新日時順を追加してください。IDは4としてください。

次にCusotomizeディレクトリ内にRepositoryディレクトリを設置してください。

そして以下のようにProductOrderByUpdate.phpを作成してRepository内に設置してください。

<?php

namespace Customize\Repository;

use Eccube\Doctrine\Query\QueryCustomizer;
use Eccube\Repository\QueryKey;
use Doctrine\ORM\QueryBuilder;

/**
 * Description of ProductOrderByUpdated
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class ProductOrderByUpdate implements QueryCustomizer {

    public function customize(QueryBuilder $builder, $params, $queryKey)
    {
        if(!empty($params["orderby"]) && $params["orderby"]->getId() == 4) {
            $builder->orderBy('p.update_date', 'DESC');
        }
        
    }

    public function getQueryKey(): string
    {
        return QueryKey::PRODUCT_SEARCH;
    }

}

以上で完成です。

EC-CUBE4の商品一覧を販売個数順で並べ替える方法

$
0
0

EC-CUBE4の商品一覧を販売個数順で並べ替える方法です。

最初に、管理画面>設定>システム設定>マスターデータ管理の「mtb_product_list_order_by」に販売個数順を追加してください。IDは4としてください。

次にCusotomizeディレクトリ内にRepositoryディレクトリを設置してください。

そして以下のようにProductOrderBySales.phpを作成してRepository内に設置してください。

<?php

namespace Customize\Repository;

use Eccube\Doctrine\Query\QueryCustomizer;
use Eccube\Repository\QueryKey;
use Doctrine\ORM\QueryBuilder;
use Eccube\Repository\OrderItemRepository;

/**
 * 販売個数順で並べ替えできるようにする
 * 
 * 管理画面>設定>システム設定>マスターデータ管理の「mtb_product_list_order_by」で販売個数順を追加する。
 * IDは4を設定
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class ProductOrderBySales implements QueryCustomizer {

    protected $orderItemRepository;


    public function __construct(OrderItemRepository $orderItemRepository)
    {
        $this->orderItemRepository = $orderItemRepository;
    }
    
    public function customize(QueryBuilder $builder, $params, $queryKey)
    {
        if(!empty($params["orderby"]) && $params["orderby"]->getId() == 4) {
            //dtb_order_itemテーブルで商品個数を集計するサブクエリ
            $qb = $this->orderItemRepository->createQueryBuilder("oi")
                    ->select("COUNT(oi.Product)")
                    ->where("oi.Product = p.id")
                    ->groupBy("oi.Product");
            
            // 上記のサブクエリをselectに追加
            $builder->addSelect(sprintf('(%s) AS HIDDEN total', $qb->getDql()))
                    ->orderBy("total", "DESC");
        }
    }

    public function getQueryKey(): string
    {
        return QueryKey::PRODUCT_SEARCH;
    }

}

以上で完成です。

EC-CUBE4の管理画面の商品一覧でショップ用メモ検索できるようにする方法

$
0
0

EC-CUBE4の管理画面の商品一覧でショップ用メモ検索できるようにする方法です。

まずCusotomizeディレクトリ内にDoctrine/Queryディレクトリを設置してください。

次に以下のようにAdminProductNoteSearchCustomizer.phpを作成してQuery内に設置してください。

<?php

namespace Customize\Repository;

use Eccube\Doctrine\Query\QueryCustomizer;
use Eccube\Doctrine\Query\WhereClause;
use Eccube\Repository\QueryKey;
use Doctrine\ORM\QueryBuilder;

/**
 * 管理画面の商品一覧でショップ用メモを検索できるようにする
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class AdminProductNoteSearchCustomizer implements QueryCustomizer {

    public function customize(QueryBuilder $builder, $params, $queryKey)
    {
        if ($params['id']) {
            // QueryBuilderに対してショップ用メモを検索対象するように設定
            $builder->orWhere("p.note LIKE :note");
            $builder->setParameter("note", '%'.$params['id'].'%');
        }
    }

    public function getQueryKey(): string
    {
        return QueryKey::PRODUCT_SEARCH_ADMIN;
    }

}

以上で完成です。

EC-CUBE4で商品毎に受注メールアドレスを設定するプラグインを作る方法

$
0
0

EC-CUBE4で商品毎に受注メールアドレスを設定するプラグインを作る方法です。

メールアドレスが登録されている商品は注文完了時そのメールアドレスにメールが送信されます。

まずは以下のコマンドでプラグインの雛形を作ります。

プラグインコードはなんでも良いのですが、今回は「MailOrderForEachProduct4」にしてください。

bin/console eccube:plugin:generate

受注受付メールアドレスを登録できるようにProductエンティティを拡張

受注受付メールアドレスを登録できるようにProductエンティティを拡張するために、ProductTraitファイルを用意します。

以下のファイルをPlugin/MailOrderForEachProduct/Entity/ProductTrait.phpに設置してください。

<?php

namespace Plugin\MailOrderForEachProduct4\Entity;

use Doctrine\ORM\Mapping as ORM;
use Eccube\Annotation as Eccube;

/**
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 * 
 * @Eccube\EntityExtension("Eccube\Entity\Product")
 */
trait ProductTrait {

    /**
     * @var string|null
     *
     * @ORM\Column(name="email", type="string", length=255, nullable=true)
     * @Eccube\FormAppend(
     *  auto_render=true,
     *  type="\Symfony\Component\Form\Extension\Core\Type\EmailType",
     *  options={
     *    "required": false,
     *    "label": "受注受付メールアドレス"
     *  }
     * )
     */
    private $email;

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

        return $this;
    }

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

}

これで商品登録・編集ページに受注受付メールアドレス項目が追加されます。

注文完了時に実行されるメール送信処理に商品毎に登録されているメールアドレスを追加

注文完了時に実行されるメール送信処理に、注文された商品に登録されているメールアドレスをBCCとして追加するイベントを用意します。

以下の内容をEvent.phpに記載してください。

<?php

namespace Plugin\MailOrderForEachProduct4;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Eccube\Event\EccubeEvents;
use Eccube\Event\EventArgs;

/**
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class Event implements EventSubscriberInterface
{
    /**
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return [
            EccubeEvents::MAIL_ORDER => 'onMailOrder'
        ];
    }
    
    /**
     * 商品に受注受付メールアドレスが登録されていた場合メール送信
     * 
     * @param EventArgs $event
     */
    public function onMailOrder(EventArgs $event) {
        $message = $event->getArgument("message");
        $Order = $event->getArgument("Order");
        
        foreach ($Order->getShippings() as $Shipping) {
            foreach ($Shipping->getOrderItems() as $Item) {
                if ($Item->isProduct()) {
                    if($Item->getProductClass()->getProduct()->getEmail()) {
                        $message->setBcc($Item->getProductClass()->getProduct()->getEmail());                        
                    }
                }
            }
        }
    }
}

以上で完成です。

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

プラグインのインストールと有効化を行うと動作します。

商品登録・編集ページに受注受付メールアドレス項目が追加されていますのでメールアドレスを登録すると注文完了時にそのメールアドレス宛にメールが送信されます。

メールアドレス登録時のバリデーション処理など細かい部分は実装していませんのでご注意ください。

このプラグインのファイル一式はこちら

EC-CUBE4でフロント側でログアウトしたときに管理画面もログアウトする問題に対応する方法

$
0
0

EC-CUBE4でフロント側でログアウトしたときに管理画面もログアウトする問題に対応する方法です。

EC-CUBE4ではフロント側と管理側の両方でログインしていた場合、片方でログアウトすると両方ログアウトしてしまう問題があります。

これはSymfonyのSessionLogoutHandlerが有効化されていて、ログアウト時にセッションが破棄されてしまうことが影響しています。

なので、以下のようにsecurity.yamlに「invalidate_session」を無効化するよう指定してあげれば解決するかと思います。

security:
    encoders:
        # Our user class and the algorithm we'll use to encode passwords
        # https://symfony.com/doc/current/security.html#c-encoding-the-user-s-password
        Eccube\Entity\Member:
          id: Eccube\Security\Core\Encoder\PasswordEncoder
        Eccube\Entity\Customer:
          id: Eccube\Security\Core\Encoder\PasswordEncoder
    providers:
        # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
        # In this example, users are stored via Doctrine in the database
        # To see the users at src/App/DataFixtures/ORM/LoadFixtures.php
        # To load users from somewhere else: https://symfony.com/doc/current/security/custom_provider.html
        member_provider:
            id: Eccube\Security\Core\User\MemberProvider
        customer_provider:
            id: Eccube\Security\Core\User\CustomerProvider
    # https://symfony.com/doc/current/security.html#initial-security-yml-setup-authentication
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin:
            pattern: '^/%eccube_admin_route%/'
            anonymous: true
            provider: member_provider
            form_login:
                check_path: admin_login
                login_path: admin_login
                csrf_token_generator: security.csrf.token_manager
                default_target_path: admin_homepage
                username_parameter: 'login_id'
                password_parameter: 'password'
                use_forward: true
                success_handler: eccube.security.success_handler
                failure_handler: eccube.security.failure_handler
            logout:
                path: admin_logout
                target: admin_login
                invalidate_session: false // セッションを維持するためここを追加
        customer:
            pattern: ^/
            anonymous: true
            provider: customer_provider
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 3600
                name: eccube_remember_me
                remember_me_parameter: 'login_memory'
            form_login:
                check_path: mypage_login
                login_path: mypage_login
                csrf_token_generator: security.csrf.token_manager
                default_target_path: homepage
                username_parameter: 'login_email'
                password_parameter: 'login_pass'
                use_forward: true
                success_handler: eccube.security.success_handler
                failure_handler: eccube.security.failure_handler
            logout:
                path: logout
                target: homepage
                invalidate_session: false // セッションを維持するためここを追加

    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: false

 

EC-CUBEにプルリク出しましたので、マージされれば次回のバージョンに反映されるかと思います。

https://github.com/EC-CUBE/ec-cube/pull/4082

EC-CUBE4で年齢確認するモーダルウィンドウを表示するプラグインを作る方法

$
0
0

EC-CUBE4で年齢確認するプラグインを作る方法です。

まずは以下のコマンドでプラグインの雛形を作ります。

プラグインコードはなんでも良いのですが、今回は「VerifyAge4」にしてください。

bin/console eccube:plugin:generate

年齢確認をするためのモーダルウィンドウを表示するTwigファイルを用意

モーダルウィンドウを表示するため今回はiziModal.jpを使用します。

以下のTwigファイルをPlugin/VerifyAge4/Resource/template/default/snippet.twigに設置してください。

<div id="modal-default" data-izimodal-title="{{ modalTitle }}">
    <p>{{ modalDescription }}</p>
</div>

<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/izimodal/1.5.1/js/iziModal.min.js"></script>
<script type="text/javascript">
    $("#modal-default").iziModal({
        autoOpen: true,
        headerColor: "#26A69A",
        width: 400,
        overlayColor: "rgba(0, 0, 0, 0.5)",
        fullscreen: true,
        transitionIn: "fadeInUp",
        transitionOut: "fadeOutDown"
    });
</script>

もう一つiziModal.jp用のcssを読み込むTwigファイルをPlugin/VerifyAge4/Resource/template/default/asset.twigに設置してください。

<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/izimodal/1.5.1/css/iziModal.min.css">

フロントの場合のみTwigファイルを読み込む

上記で用意したTwigファイルをフロントの場合のみ読み込むイベントを用意します。

以下の内容をEvent.phpに追記してください。

<?php

namespace Plugin\VerifyAge4;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Eccube\Request\Context;
use Eccube\Event\TemplateEvent;

/**
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class Event implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::CONTROLLER_ARGUMENTS => [['onKernelController', 100000000]]
        ];
    }

    /**
     * @var \Eccube\Request\Context
     */
    protected $requestContext;
    
    /**
     * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
     */
    protected $eventDispatcher;

    public function __construct(Context $requestContext, EventDispatcherInterface $eventDispatcher)
    {
        $this->requestContext = $requestContext;
        $this->eventDispatcher = $eventDispatcher;
    }

    public function onKernelController(FilterControllerEvent $event)
    {
        // フロントページではない場合スルー
        if(!$this->requestContext->isFront()) {
            return;
        }
        
        if ($event->getRequest()->attributes->has('_template')) {
            $template = $event->getRequest()->attributes->get('_template');
            $this->eventDispatcher->addListener($template->getTemplate(), function (TemplateEvent $templateEvent) {
                $templateEvent->addAsset('@VerifyAge4/default/asset.twig');
                $templateEvent->addSnippet('@VerifyAge4/default/snippet.twig');
                
                // snippet.twigに値を渡す
                // 文言を動的に操作したい場合はこのへんで調整してください
                $templateEvent->setParameter('modalTitle', "年齢確認");
                $templateEvent->setParameter('modalDescription', "20歳以上ですか?");
            });
        }
    }
}

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

プラグインのインストールと有効化を行うと動作します。

ちなみに上記の実装ではページをリロードするたびにモーダルウィンドウが表示されます。

このプラグインのファイル一式はこちら

EC-CUBE4でGoogle Analyticsコンバージョンタグを設定する方法

$
0
0

EC-CUBE4でGoogle Analyticsコンバージョンタグを設定する方法です。

管理画面のコンテンツ管理>ページ管理>商品購入/ご注文完了へ進んで以下のサンプルコードを一番下へ追記してください。

{% block javascript %}
<script>
    ga('ecommerce:addTransaction', {
        id: '{{ Order.orderNo }}', // Transaction ID - this is normally generated by your system.
        affiliation: '{{ BaseInfo.shop_name }}', // Affiliation or store name
        revenue: '{{ Order.payment_total|price }}', // Grand Total
        shipping: '{{ Order.delivery_fee_total|price }}'}); // Shipping cost

    {% for OrderItem in Order.MergedProductOrderItems %}
    ga('ecommerce:addItem', {
        id: '{{ Order.orderNo }}', // Transaction ID.
        sku: '{{ OrderItem.product_code }}', // SKU/code.
        name: '{{ OrderItem.product_name }}  {{ OrderItem.classcategory_name1 }}  {{ OrderItem.classcategory_name2 }}', // Product name.
        price: '{{ OrderItem.price_inc_tax|price }}', // Unit price.
        quantity: '{{ OrderItem.quantity|number_format }}'}); // Quantity.
    {% endfor %}
    ga('ecommerce:send');
</script>
{% endblock %}

 


EC-CUBE4でログインに失敗したときに何かする方法

$
0
0

EC-CUBE4でログインに失敗したときに何かする方法です。

まずCusotomizeディレクトリ内にEventSubscriberディレクトリを設置してください。

次に以下のようにAuthenticationFailureSubscriber.phpを作成してEventSubscriber内に設置してください。

<?php

namespace Customize\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;

/**
 * ログインに失敗したときに何かする
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class AuthenticationFailureSubscriber implements EventSubscriberInterface {

    public static function getSubscribedEvents(): array
    {
        return [
            AuthenticationEvents::AUTHENTICATION_FAILURE => "onAuthenticationFailure"
        ];
    }
    
    public function onAuthenticationFailure(AuthenticationFailureEvent $event)
    {
        $token = $event->getAuthenticationToken();
        
        switch($token->getProviderKey()) {
            case "customer":
                // 会員がログイン失敗したときに何かする
                var_dump("customer");
                $User = $token->getUser();
                break;
            case "admin":
                // メンバーがログイン失敗したときに何かする
                var_dump("admin");
                $User = $token->getUser();
                break;
        }
    }

}

以上で完成です。

これで会員またはメンバーがログインに失敗したときにメール通知などの何かしらの処理が追加できます。

投稿 EC-CUBE4でログインに失敗したときに何かする方法あずみ.net に最初に表示されました。

EC-CUBE4でログインしたときに何かする方法

$
0
0

EC-CUBE4でログインしたときに何かする方法です。

まずCusotomizeディレクトリ内にEventSubscriberディレクトリを設置してください。

次に以下のようにAuthenticationSuccessSubscriber.phpを作成してEventSubscriber内に設置してください。

<?php

namespace Customize\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;

/**
 * ログインしたときに何かする
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class AuthenticationSuccessSubscriber implements EventSubscriberInterface {

    public static function getSubscribedEvents(): array
    {
        return [
            AuthenticationEvents::AUTHENTICATION_SUCCESS => "onAuthenticationSuccess"
        ];
    }
    
    public function onAuthenticationSuccess(AuthenticationEvent $event)
    {
        $token = $event->getAuthenticationToken();
        
        if(!$token->getRoles()) {
            return;
        }
        
        switch($token->getRoles()) {
            case "ROLE_USER":
                // 会員がログインしたときに何かする
                var_dump("customer");
                $User = $token->getUser();
                break;
            case "ROLE_ADMIN":
                // メンバーがログインしたときに何かする
                var_dump("admin");
                $User = $token->getUser();
                break;
        }
    }

}

以上で完成です。

これで会員またはメンバーがログインしたときにメール通知などの何かしらの処理が追加できます。

投稿 EC-CUBE4でログインしたときに何かする方法あずみ.net に最初に表示されました。

EC-CUBE4のCustomizeディレクトリにEntityとRepositoryを自動生成するコマンドを用意する方法

$
0
0

EC-CUBE4のCustomizeディレクトリにEntityとRepositoryを自動生成するコマンドを用意する方法です。

今回はSymfonyのMakerBundleを拡張してEC-CUBE4のCustomizeディレクトリに自動生成するようにします。

まずはconfig/eccube/pakagesディレクトリ内に下記のmaker.yamlファイルを設置してください。

maker:
    root_namespace: 'Customize'

次に下記のapp/Customize/Maker/MakerEntity.phpを設置してください。

<?php

/*
 * This file is part of the Symfony MakerBundle package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Customize\Maker;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator;
use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputAwareMakerInterface;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Doctrine\EntityRegenerator;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Doctrine\EntityRelation;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;

/**
 * @author Javier Eguiluz <javier.eguiluz@gmail.com>
 * @author Ryan Weaver <weaverryan@gmail.com>
 * @author Kévin Dunglas <dunglas@gmail.com>
 */
final class MakeEntity extends AbstractMaker implements InputAwareMakerInterface
{
    private $fileManager;
    private $doctrineHelper;
    private $generator;

    public function __construct(FileManager $fileManager, DoctrineHelper $doctrineHelper, Generator $generator = null)
    {
        $this->fileManager = $fileManager;
        $this->doctrineHelper = $doctrineHelper;
        // $projectDirectory is unused, argument kept for BC

        if (null === $generator) {
            @trigger_error(sprintf('Passing a "%s" instance as 4th argument is mandatory since version 1.5.', Generator::class), E_USER_DEPRECATED);
            $this->generator = new Generator($fileManager, 'App\\');
        } else {
            $this->generator = $generator;
        }
    }

    public static function getCommandName(): string
    {
        return 'make:customize:entity';
    }

    public function configureCommand(Command $command, InputConfiguration $inputConf)
    {
        $command
            ->setDescription('Creates or updates a Doctrine entity class, and optionally an API Platform resource')
            ->addArgument('name', InputArgument::OPTIONAL, sprintf('Class name of the entity to create or update (e.g. <fg=yellow>%s</>)', Str::asClassName(Str::getRandomTerm())))
            ->addOption('api-resource', 'a', InputOption::VALUE_NONE, 'Mark this class as an API Platform resource (expose a CRUD API for it)')
            ->addOption('regenerate', null, InputOption::VALUE_NONE, 'Instead of adding new fields, simply generate the methods (e.g. getter/setter) for existing fields')
            ->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite any existing getter/setter methods')
            ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeEntity.txt'))
        ;

        $inputConf->setArgumentAsNonInteractive('name');
    }

    public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
    {
        if ($input->getArgument('name')) {
            return;
        }

        if ($input->getOption('regenerate')) {
            $io->block([
                'This command will generate any missing methods (e.g. getters & setters) for a class or all classes in a namespace.',
                'To overwrite any existing methods, re-run this command with the --overwrite flag',
            ], null, 'fg=yellow');
            $classOrNamespace = $io->ask('Enter a class or namespace to regenerate', $this->getEntityNamespace(), [Validator::class, 'notBlank']);

            $input->setArgument('name', $classOrNamespace);

            return;
        }

        $entityFinder = $this->fileManager->createFinder('app/Customize/Entity/')
            // remove if/when we allow entities in subdirectories
            ->depth('<1')
            ->name('*.php');
        $classes = [];
        /** @var SplFileInfo $item */
        foreach ($entityFinder as $item) {
            if (!$item->getRelativePathname()) {
                continue;
            }

            $classes[] = str_replace(['.php', '/'], ['', '\\'], $item->getRelativePathname());
        }

        $argument = $command->getDefinition()->getArgument('name');
        $question = $this->createEntityClassQuestion($argument->getDescription());
        $value = $io->askQuestion($question);

        $input->setArgument('name', $value);

        if (
            !$input->getOption('api-resource') &&
            class_exists(ApiResource::class) &&
            !class_exists($this->generator->createClassNameDetails($value, 'Entity\\')->getFullName())
        ) {
            $description = $command->getDefinition()->getOption('api-resource')->getDescription();
            $question = new ConfirmationQuestion($description, false);
            $value = $io->askQuestion($question);

            $input->setOption('api-resource', $value);
        }
    }

    public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
    {
        $overwrite = $input->getOption('overwrite');

        // the regenerate option has entirely custom behavior
        if ($input->getOption('regenerate')) {
            $this->regenerateEntities($input->getArgument('name'), $overwrite, $generator);
            $this->writeSuccessMessage($io);

            return;
        }

        $entityClassDetails = $generator->createClassNameDetails(
            $input->getArgument('name'),
            'Entity\\'
        );

        $classExists = class_exists($entityClassDetails->getFullName());
        if (!$classExists) {
            $entityClassGenerator = new EntityClassGenerator($generator);
            $entityPath = $entityClassGenerator->generateEntityClass(
                $entityClassDetails,
                $input->getOption('api-resource')
            );

            $generator->writeChanges();
        }

        if (!$this->doesEntityUseAnnotationMapping($entityClassDetails->getFullName())) {
            throw new RuntimeCommandException(sprintf('Only annotation mapping is supported by make:customize:entity, but the <info>%s</info> class uses a different format. If you would like this command to generate the properties & getter/setter methods, add your mapping configuration, and then re-run this command with the <info>--regenerate</info> flag.', $entityClassDetails->getFullName()));
        }

        if ($classExists) {
            $entityPath = $this->getPathOfClass($entityClassDetails->getFullName());
            $io->text([
                'Your entity already exists! So let\'s add some new fields!',
            ]);
        } else {
            $io->text([
                '',
                'Entity generated! Now let\'s add some fields!',
                'You can always add more fields later manually or by re-running this command.',
            ]);
        }

        $currentFields = $this->getPropertyNames($entityClassDetails->getFullName());
        $manipulator = $this->createClassManipulator($entityPath, $io, $overwrite);

        $isFirstField = true;
        while (true) {
            $newField = $this->askForNextField($io, $currentFields, $entityClassDetails->getFullName(), $isFirstField);
            $isFirstField = false;

            if (null === $newField) {
                break;
            }

            $fileManagerOperations = [];
            $fileManagerOperations[$entityPath] = $manipulator;

            if (\is_array($newField)) {
                $annotationOptions = $newField;
                unset($annotationOptions['fieldName']);
                $manipulator->addEntityField($newField['fieldName'], $annotationOptions);

                $currentFields[] = $newField['fieldName'];
            } elseif ($newField instanceof EntityRelation) {
                // both overridden below for OneToMany
                $newFieldName = $newField->getOwningProperty();
                $otherManipulatorFilename = $this->getPathOfClass($newField->getInverseClass());
                $otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);
                switch ($newField->getType()) {
                    case EntityRelation::MANY_TO_ONE:
                        if ($newField->getOwningClass() === $entityClassDetails->getFullName()) {
                            // THIS class will receive the ManyToOne
                            $manipulator->addManyToOneRelation($newField->getOwningRelation());

                            if ($newField->getMapInverseRelation()) {
                                $otherManipulator->addOneToManyRelation($newField->getInverseRelation());
                            }
                        } else {
                            // the new field being added to THIS entity is the inverse
                            $newFieldName = $newField->getInverseProperty();
                            $otherManipulatorFilename = $this->getPathOfClass($newField->getOwningClass());
                            $otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);

                            // The *other* class will receive the ManyToOne
                            $otherManipulator->addManyToOneRelation($newField->getOwningRelation());
                            if (!$newField->getMapInverseRelation()) {
                                throw new \Exception('Somehow a OneToMany relationship is being created, but the inverse side will not be mapped?');
                            }
                            $manipulator->addOneToManyRelation($newField->getInverseRelation());
                        }

                        break;
                    case EntityRelation::MANY_TO_MANY:
                        $manipulator->addManyToManyRelation($newField->getOwningRelation());
                        if ($newField->getMapInverseRelation()) {
                            $otherManipulator->addManyToManyRelation($newField->getInverseRelation());
                        }

                        break;
                    case EntityRelation::ONE_TO_ONE:
                        $manipulator->addOneToOneRelation($newField->getOwningRelation());
                        if ($newField->getMapInverseRelation()) {
                            $otherManipulator->addOneToOneRelation($newField->getInverseRelation());
                        }

                        break;
                    default:
                        throw new \Exception('Invalid relation type');
                }

                // save the inverse side if it's being mapped
                if ($newField->getMapInverseRelation()) {
                    $fileManagerOperations[$otherManipulatorFilename] = $otherManipulator;
                }
                $currentFields[] = $newFieldName;
            } else {
                throw new \Exception('Invalid value');
            }

            foreach ($fileManagerOperations as $path => $manipulatorOrMessage) {
                if (\is_string($manipulatorOrMessage)) {
                    $io->comment($manipulatorOrMessage);
                } else {
                    $this->fileManager->dumpFile($path, $manipulatorOrMessage->getSourceCode());
                }
            }
        }

        $this->writeSuccessMessage($io);
        $io->text([
            'Next: When you\'re ready, create a migration with <comment>make:migration</comment>',
            '',
        ]);
    }

    public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null)
    {
        $dependencies->requirePHP71();

        if (null !== $input && $input->getOption('api-resource')) {
            $dependencies->addClassDependency(
                ApiResource::class,
                'api'
            );
        }

        ORMDependencyBuilder::buildDependencies($dependencies);
    }

    private function askForNextField(ConsoleStyle $io, array $fields, string $entityClass, bool $isFirstField)
    {
        $io->writeln('');

        if ($isFirstField) {
            $questionText = 'New property name (press <return> to stop adding fields)';
        } else {
            $questionText = 'Add another property? Enter the property name (or press <return> to stop adding fields)';
        }

        $fieldName = $io->ask($questionText, null, function ($name) use ($fields) {
            // allow it to be empty
            if (!$name) {
                return $name;
            }

            if (\in_array($name, $fields)) {
                throw new \InvalidArgumentException(sprintf('The "%s" property already exists.', $name));
            }

            return Validator::validateDoctrineFieldName($name, $this->doctrineHelper->getRegistry());
        });

        if (!$fieldName) {
            return null;
        }

        $defaultType = 'string';
        // try to guess the type by the field name prefix/suffix
        // convert to snake case for simplicity
        $snakeCasedField = Str::asSnakeCase($fieldName);

        if ('_at' === $suffix = substr($snakeCasedField, -3)) {
            $defaultType = 'datetime';
        } elseif ('_id' === $suffix) {
            $defaultType = 'integer';
        } elseif (0 === strpos($snakeCasedField, 'is_')) {
            $defaultType = 'boolean';
        } elseif (0 === strpos($snakeCasedField, 'has_')) {
            $defaultType = 'boolean';
        }

        $type = null;
        $allValidTypes = array_merge(
            array_keys(Type::getTypesMap()),
            EntityRelation::getValidRelationTypes(),
            ['relation']
        );
        while (null === $type) {
            $question = new Question('Field type (enter <comment>?</comment> to see all types)', $defaultType);
            $question->setAutocompleterValues($allValidTypes);
            $type = $io->askQuestion($question);

            if ('?' === $type) {
                $this->printAvailableTypes($io);
                $io->writeln('');

                $type = null;
            } elseif (!\in_array($type, $allValidTypes)) {
                $this->printAvailableTypes($io);
                $io->error(sprintf('Invalid type "%s".', $type));
                $io->writeln('');

                $type = null;
            }
        }

        if ('relation' === $type || \in_array($type, EntityRelation::getValidRelationTypes())) {
            return $this->askRelationDetails($io, $entityClass, $type, $fieldName);
        }

        // this is a normal field
        $data = ['fieldName' => $fieldName, 'type' => $type];
        if ('string' == $type) {
            // default to 255, avoid the question
            $data['length'] = $io->ask('Field length', 255, [Validator::class, 'validateLength']);
        } elseif ('decimal' === $type) {
            // 10 is the default value given in \Doctrine\DBAL\Schema\Column::$_precision
            $data['precision'] = $io->ask('Precision (total number of digits stored: 100.00 would be 5)', 10, [Validator::class, 'validatePrecision']);

            // 0 is the default value given in \Doctrine\DBAL\Schema\Column::$_scale
            $data['scale'] = $io->ask('Scale (number of decimals to store: 100.00 would be 2)', 0, [Validator::class, 'validateScale']);
        }

        if ($io->confirm('Can this field be null in the database (nullable)', false)) {
            $data['nullable'] = true;
        }

        return $data;
    }

    private function printAvailableTypes(ConsoleStyle $io)
    {
        $allTypes = Type::getTypesMap();

        $typesTable = [
            'main' => [
                'string' => [],
                'text' => [],
                'boolean' => [],
                'integer' => ['smallint', 'bigint'],
                'float' => [],
            ],
            'relation' => [
                'relation' => 'a wizard will help you build the relation',
                EntityRelation::MANY_TO_ONE => [],
                EntityRelation::ONE_TO_MANY => [],
                EntityRelation::MANY_TO_MANY => [],
                EntityRelation::ONE_TO_ONE => [],
            ],
            'array_object' => [
                'array' => ['simple_array'],
                'json' => [],
                'object' => [],
                'binary' => [],
                'blob' => [],
            ],
            'date_time' => [
                'datetime' => ['datetime_immutable'],
                'datetimetz' => ['datetimetz_immutable'],
                'date' => ['date_immutable'],
                'time' => ['time_immutable'],
                'dateinterval' => [],
            ],
        ];

        $printSection = function (array $sectionTypes) use ($io, &$allTypes) {
            foreach ($sectionTypes as $mainType => $subTypes) {
                unset($allTypes[$mainType]);
                $line = sprintf('  * <comment>%s</comment>', $mainType);

                if (\is_string($subTypes) && $subTypes) {
                    $line .= sprintf(' (%s)', $subTypes);
                } elseif (\is_array($subTypes) && !empty($subTypes)) {
                    $line .= sprintf(' (or %s)', implode(', ', array_map(function ($subType) {
                        return sprintf('<comment>%s</comment>', $subType);
                    }, $subTypes)));

                    foreach ($subTypes as $subType) {
                        unset($allTypes[$subType]);
                    }
                }

                $io->writeln($line);
            }

            $io->writeln('');
        };

        $io->writeln('<info>Main types</info>');
        $printSection($typesTable['main']);

        $io->writeln('<info>Relationships / Associations</info>');
        $printSection($typesTable['relation']);

        $io->writeln('<info>Array/Object Types</info>');
        $printSection($typesTable['array_object']);

        $io->writeln('<info>Date/Time Types</info>');
        $printSection($typesTable['date_time']);

        $io->writeln('<info>Other Types</info>');
        // empty the values
        $allTypes = array_map(function () {
            return [];
        }, $allTypes);
        $printSection($allTypes);
    }

    private function createEntityClassQuestion(string $questionText): Question
    {
        $entityFinder = $this->fileManager->createFinder('app/Customize/Entity/')
            // remove if/when we allow entities in subdirectories
            ->depth('<1')
            ->name('*.php');
        $classes = [];
        /** @var SplFileInfo $item */
        foreach ($entityFinder as $item) {
            if (!$item->getRelativePathname()) {
                continue;
            }

            $classes[] = str_replace('/', '\\', str_replace('.php', '', $item->getRelativePathname()));
        }

        $question = new Question($questionText);
        $question->setValidator([Validator::class, 'notBlank']);
        $question->setAutocompleterValues($classes);

        return $question;
    }

    private function askRelationDetails(ConsoleStyle $io, string $generatedEntityClass, string $type, string $newFieldName)
    {
        // ask the targetEntity
        $targetEntityClass = null;
        while (null === $targetEntityClass) {
            $question = $this->createEntityClassQuestion('What class should this entity be related to?');

            $targetEntityClass = $io->askQuestion($question);

            if (!class_exists($targetEntityClass)) {
                if (!class_exists($this->getEntityNamespace().'\\'.$targetEntityClass)) {
                    $io->error(sprintf('Unknown class "%s"', $targetEntityClass));
                    $targetEntityClass = null;

                    continue;
                }

                $targetEntityClass = $this->getEntityNamespace().'\\'.$targetEntityClass;
            }
        }

        // help the user select the type
        if ('relation' === $type) {
            $type = $this->askRelationType($io, $generatedEntityClass, $targetEntityClass);
        }

        $askFieldName = function (string $targetClass, string $defaultValue) use ($io) {
            return $io->ask(
                sprintf('New field name inside %s', Str::getShortClassName($targetClass)),
                $defaultValue,
                function ($name) use ($targetClass) {
                    // it's still *possible* to create duplicate properties - by
                    // trying to generate the same property 2 times during the
                    // same make:entity run. property_exists() only knows about
                    // properties that *originally* existed on this class.
                    if (property_exists($targetClass, $name)) {
                        throw new \InvalidArgumentException(sprintf('The "%s" class already has a "%s" property.', $targetClass, $name));
                    }

                    return Validator::validateDoctrineFieldName($name, $this->doctrineHelper->getRegistry());
                }
            );
        };

        $askIsNullable = function (string $propertyName, string $targetClass) use ($io) {
            return $io->confirm(sprintf(
                'Is the <comment>%s</comment>.<comment>%s</comment> property allowed to be null (nullable)?',
                Str::getShortClassName($targetClass),
                $propertyName
            ));
        };

        $askOrphanRemoval = function (string $owningClass, string $inverseClass) use ($io) {
            $io->text([
                'Do you want to activate <comment>orphanRemoval</comment> on your relationship?',
                sprintf(
                    'A <comment>%s</comment> is "orphaned" when it is removed from its related <comment>%s</comment>.',
                    Str::getShortClassName($owningClass),
                    Str::getShortClassName($inverseClass)
                ),
                sprintf(
                    'e.g. <comment>$%s->remove%s($%s)</comment>',
                    Str::asLowerCamelCase(Str::getShortClassName($inverseClass)),
                    Str::asCamelCase(Str::getShortClassName($owningClass)),
                    Str::asLowerCamelCase(Str::getShortClassName($owningClass))
                ),
                '',
                sprintf(
                    'NOTE: If a <comment>%s</comment> may *change* from one <comment>%s</comment> to another, answer "no".',
                    Str::getShortClassName($owningClass),
                    Str::getShortClassName($inverseClass)
                ),
            ]);

            return $io->confirm(sprintf('Do you want to automatically delete orphaned <comment>%s</comment> objects (orphanRemoval)?', $owningClass), false);
        };

        $askInverseSide = function (EntityRelation $relation) use ($io) {
            if ($this->isClassInVendor($relation->getInverseClass())) {
                $relation->setMapInverseRelation(false);

                return;
            }

            // recommend an inverse side, except for OneToOne, where it's inefficient
            $recommendMappingInverse = EntityRelation::ONE_TO_ONE !== $relation->getType();

            $getterMethodName = 'get'.Str::asCamelCase(Str::getShortClassName($relation->getOwningClass()));
            if (EntityRelation::ONE_TO_ONE !== $relation->getType()) {
                // pluralize!
                $getterMethodName = Str::singularCamelCaseToPluralCamelCase($getterMethodName);
            }
            $mapInverse = $io->confirm(
                sprintf(
                    'Do you want to add a new property to <comment>%s</comment> so that you can access/update <comment>%s</comment> objects from it - e.g. <comment>$%s->%s()</comment>?',
                    Str::getShortClassName($relation->getInverseClass()),
                    Str::getShortClassName($relation->getOwningClass()),
                    Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass())),
                    $getterMethodName
                ),
                $recommendMappingInverse
            );
            $relation->setMapInverseRelation($mapInverse);
        };

        switch ($type) {
            case EntityRelation::MANY_TO_ONE:
                $relation = new EntityRelation(
                    EntityRelation::MANY_TO_ONE,
                    $generatedEntityClass,
                    $targetEntityClass
                );
                $relation->setOwningProperty($newFieldName);

                $relation->setIsNullable($askIsNullable(
                    $relation->getOwningProperty(),
                    $relation->getOwningClass()
                ));

                $askInverseSide($relation);
                if ($relation->getMapInverseRelation()) {
                    $io->comment(sprintf(
                        'A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> objects from it.',
                        Str::getShortClassName($relation->getInverseClass()),
                        Str::getShortClassName($relation->getOwningClass())
                    ));
                    $relation->setInverseProperty($askFieldName(
                        $relation->getInverseClass(),
                        Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass()))
                    ));

                    // orphan removal only applies if the inverse relation is set
                    if (!$relation->isNullable()) {
                        $relation->setOrphanRemoval($askOrphanRemoval(
                            $relation->getOwningClass(),
                            $relation->getInverseClass()
                        ));
                    }
                }

                break;
            case EntityRelation::ONE_TO_MANY:
                // we *actually* create a ManyToOne, but populate it differently
                $relation = new EntityRelation(
                    EntityRelation::MANY_TO_ONE,
                    $targetEntityClass,
                    $generatedEntityClass
                );
                $relation->setInverseProperty($newFieldName);

                $io->comment(sprintf(
                    'A new property will also be added to the <comment>%s</comment> class so that you can access and set the related <comment>%s</comment> object from it.',
                    Str::getShortClassName($relation->getOwningClass()),
                    Str::getShortClassName($relation->getInverseClass())
                ));
                $relation->setOwningProperty($askFieldName(
                    $relation->getOwningClass(),
                    Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass()))
                ));

                $relation->setIsNullable($askIsNullable(
                    $relation->getOwningProperty(),
                    $relation->getOwningClass()
                ));

                if (!$relation->isNullable()) {
                    $relation->setOrphanRemoval($askOrphanRemoval(
                        $relation->getOwningClass(),
                        $relation->getInverseClass()
                    ));
                }

                break;
            case EntityRelation::MANY_TO_MANY:
                $relation = new EntityRelation(
                    EntityRelation::MANY_TO_MANY,
                    $generatedEntityClass,
                    $targetEntityClass
                );
                $relation->setOwningProperty($newFieldName);

                $askInverseSide($relation);
                if ($relation->getMapInverseRelation()) {
                    $io->comment(sprintf(
                        'A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> objects from it.',
                        Str::getShortClassName($relation->getInverseClass()),
                        Str::getShortClassName($relation->getOwningClass())
                    ));
                    $relation->setInverseProperty($askFieldName(
                        $relation->getInverseClass(),
                        Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass()))
                    ));
                }

                break;
            case EntityRelation::ONE_TO_ONE:
                $relation = new EntityRelation(
                    EntityRelation::ONE_TO_ONE,
                    $generatedEntityClass,
                    $targetEntityClass
                );
                $relation->setOwningProperty($newFieldName);

                $relation->setIsNullable($askIsNullable(
                    $relation->getOwningProperty(),
                    $relation->getOwningClass()
                ));

                $askInverseSide($relation);
                if ($relation->getMapInverseRelation()) {
                    $io->comment(sprintf(
                        'A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> object from it.',
                        Str::getShortClassName($relation->getInverseClass()),
                        Str::getShortClassName($relation->getOwningClass())
                    ));
                    $relation->setInverseProperty($askFieldName(
                        $relation->getInverseClass(),
                        Str::asLowerCamelCase(Str::getShortClassName($relation->getOwningClass()))
                    ));
                }

                break;
            default:
                throw new \InvalidArgumentException('Invalid type: '.$type);
        }

        return $relation;
    }

    private function askRelationType(ConsoleStyle $io, string $entityClass, string $targetEntityClass)
    {
        $io->writeln('What type of relationship is this?');

        $originalEntityShort = Str::getShortClassName($entityClass);
        $targetEntityShort = Str::getShortClassName($targetEntityClass);
        $rows = [];
        $rows[] = [
            EntityRelation::MANY_TO_ONE,
            sprintf("Each <comment>%s</comment> relates to (has) <info>one</info> <comment>%s</comment>.\nEach <comment>%s</comment> can relate/has to (have) <info>many</info> <comment>%s</comment> objects", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),
        ];
        $rows[] = ['', ''];
        $rows[] = [
            EntityRelation::ONE_TO_MANY,
            sprintf("Each <comment>%s</comment> relates can relate to (have) <info>many</info> <comment>%s</comment> objects.\nEach <comment>%s</comment> relates to (has) <info>one</info> <comment>%s</comment>", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),
        ];
        $rows[] = ['', ''];
        $rows[] = [
            EntityRelation::MANY_TO_MANY,
            sprintf("Each <comment>%s</comment> relates can relate to (have) <info>many</info> <comment>%s</comment> objects.\nEach <comment>%s</comment> can also relate to (have) <info>many</info> <comment>%s</comment> objects", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),
        ];
        $rows[] = ['', ''];
        $rows[] = [
            EntityRelation::ONE_TO_ONE,
            sprintf("Each <comment>%s</comment> relates to (has) exactly <info>one</info> <comment>%s</comment>.\nEach <comment>%s</comment> also relates to (has) exactly <info>one</info> <comment>%s</comment>.", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),
        ];

        $io->table([
            'Type',
            'Description',
        ], $rows);

        $question = new Question(sprintf(
            'Relation type? [%s]',
            implode(', ', EntityRelation::getValidRelationTypes())
        ));
        $question->setAutocompleterValues(EntityRelation::getValidRelationTypes());
        $question->setValidator(function ($type) {
            if (!\in_array($type, EntityRelation::getValidRelationTypes())) {
                throw new \InvalidArgumentException(sprintf('Invalid type: use one of: %s', implode(', ', EntityRelation::getValidRelationTypes())));
            }

            return $type;
        });

        return $io->askQuestion($question);
    }

    private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite): ClassSourceManipulator
    {
        $manipulator = new ClassSourceManipulator($this->fileManager->getFileContents($path), $overwrite);
        $manipulator->setIo($io);

        return $manipulator;
    }

    private function getPathOfClass(string $class): string
    {
        return (new \ReflectionClass($class))->getFileName();
    }

    private function isClassInVendor(string $class): bool
    {
        $path = $this->getPathOfClass($class);

        return $this->fileManager->isPathInVendor($path);
    }

    private function regenerateEntities(string $classOrNamespace, bool $overwrite, Generator $generator)
    {
        $regenerator = new EntityRegenerator($this->doctrineHelper, $this->fileManager, $generator, $overwrite);
        $regenerator->regenerateEntities($classOrNamespace);
    }

    private function getPropertyNames(string $class): array
    {
        if (!class_exists($class)) {
            return [];
        }

        $reflClass = new \ReflectionClass($class);

        return array_map(function (\ReflectionProperty $prop) {
            return $prop->getName();
        }, $reflClass->getProperties());
    }

    private function doesEntityUseAnnotationMapping(string $className): bool
    {
        if (!class_exists($className)) {
            $otherClassMetadatas = $this->doctrineHelper->getMetadata(Str::getNamespace($className).'\\', true);

            // if we have no metadata, we should assume this is the first class being mapped
            if (empty($otherClassMetadatas)) {
                return false;
            }

            $className = reset($otherClassMetadatas)->getName();
        }

        $driver = $this->doctrineHelper->getMappingDriverForClass($className);

        return $driver instanceof AnnotationDriver;
    }

    private function getEntityNamespace(): string
    {
        return $this->doctrineHelper->getEntityNamespace();
    }
}

最後に下記のapp/Customize/Resources/help/MakeEntity.txtを設置してください。

The <info>%command.name%</info> command creates or updates an entity and repository class.

<info>php %command.full_name% BlogPost</info>

If the argument is missing, the command will ask for the entity class name interactively.

You can also mark this class as an API Platform resource. A hypermedia CRUD API will
automatically be available for this entity class:

<info>php %command.full_name% --api-resource</info>

You can also generate all the getter/setter/adder/remover methods
for the properties of existing entities:

<info>php %command.full_name% --regenerate</info>

You can also *overwrite* any existing methods:

<info>php %command.full_name% --regenerate --overwrite</info>

以上で完成です。

以下のコマンドを実行するとEntityとRepositoryを作成するウィザードが表示されます。

bin/console make:customize:entity

初回時Entityを作成するとエラーが表示されますが、もう一度同じEntityを作成するとフィールドを追加できるようになります。

投稿 EC-CUBE4のCustomizeディレクトリにEntityとRepositoryを自動生成するコマンドを用意する方法あずみ.net に最初に表示されました。

EC-CUBE4で新着情報を公開したときにツイッターに投稿する方法

$
0
0

EC-CUBE4で新着情報を公開したときにツイッターに投稿する方法です。

今回はツイッター認証用ライブラリのTwitterOAuthを使って投稿するようにします。

まずはライブラリのインストール。EC-CUBEが用意したcomposerじゃないとインストール出来ないのでご注意ください。

bin/console eccube:composer:require abraham/twitteroauth

次にCustomize/EventSubscriberディレクトリを作成して以下のTweetSubscriber.phpを設置してください。

<?php

namespace Customize\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Eccube\Event\EccubeEvents;
use Eccube\Event\EventArgs;
use Abraham\TwitterOAuth\TwitterOAuth;

/**
 * ライブラリのインストールが必要
 * bin/console eccube:composer:require abraham/twitteroauth
 * 
 * 新着情報を投稿・更新したときにツイートするイベント
 *
 * @author Akira Kurozumi <info@a-zumi.net>
 */
class TweetSubscriber implements EventSubscriberInterface {
    
    private const CONSUMER_KEY = "CONSUMER_KEY";
    private const CONSUMER_SECRET = "CONSUMER_SECRET";
    private const ACCESS_TOKEN = "ACCESS_TOKEN";
    private const ACCESS_TOKEN_SECRET = "ACCESS_TOKEN_SECRET";

    public static function getSubscribedEvents(): array
    {
        return [
            EccubeEvents::ADMIN_CONTENT_NEWS_EDIT_COMPLETE => 'onAdminContentNewsEditComplete',
        ];
    }
    
    public function onAdminContentNewsEditComplete(EventArgs $event)
    {
        $News = $event->getArgument("News");
        
        if($News->getVisible()) {
            $connection = new TwitterOAuth(self::CONSUMER_KEY, self::CONSUMER_SECRET, self::ACCESS_TOKEN, self::ACCESS_TOKEN_SECRET);
            $connection->post("statuses/update", ["status" => $News->getTitle()." ".$News->getUrl()]);
        }

    }

}

CONSUMER_KEYとCONSUMER_SECRET、ACCESS_TOKEN、ACCESS_TOKEN_SECRETを取得して設定すれば、新着情報を投稿するたびツイートされるようになります。

投稿 EC-CUBE4で新着情報を公開したときにツイッターに投稿する方法あずみ.net に最初に表示されました。

SymfonyのMakerBundleを拡張してEC-CUBE4に対応したEccubeMakerBundleプラグインをGitHubに公開しました

$
0
0

SymfonyのMakerBundleを拡張してEC-CUBE4に対応したEccubeMakerBundleプラグインをGitHubに公開しました。

このプラグインをインストールしてコマンドを実行すると、EntityやControllerなどのファイルの雛形がCustomizeディレクトリに自動生成されます。

上記プラグインをEC-CUBE4にインストールすると、EC-CUBE用のmakerコマンドが追加されます。

追加されるコマンドはコントローラーとエンティティの作成コマンドです。

bin/console eccube:make:controller 
bin/console eccube:make:entity

コマンドを実行するとウィザードが開始され、質問に答えていくとCustomizeディレクトにファイルが生成されます。

コントローラーをmakeした場合はコントローラーとテンプレート、エンティティをmakeした場合はエンティティとレポジトリのファイルが自動生成されます。

投稿 SymfonyのMakerBundleを拡張してEC-CUBE4に対応したEccubeMakerBundleプラグインをGitHubに公開しましたあずみ.net に最初に表示されました。

Viewing all 271 articles
Browse latest View live