会員グループ別価格&数量別割引マネージャー for EC-CUBE 4.2 / 4.3

開発者向けカスタマイズガイド

Twig APIを利用して、ログイン状態・会員グループ・商品タグ・カテゴリ・B2B価格ルールに応じた表示を安全にカスタマイズする方法を説明します。

EC-CUBE 4.2 / 4.3対応

はじめに

Context

ログイン・会員グループ

ゲスト、一般会員、B2B会員を判定し、現在のグループ名や日本語ラベルを取得できます。

Product

商品・タグ・カテゴリ

商品ID、タグ、カテゴリを個別または複数条件で判定し、カスタマイズ対象を絞り込めます。

Price

価格・ルール・数量別割引

現在の会員に適用される表示単価、割引額、価格ルール、数量別割引段階を参照できます。

推奨:IDで設定する

グループ名・タグ名・カテゴリ名は管理画面で変更できます。本番の判定条件にはIDを使用し、名前によるAPIは簡易カスタマイズや表示補助に使用してください。

{% set employee_group_id = 3 %}
{% if b2b_in_group_id(employee_group_id) %}
  社員向けコンテンツ
{% endif %}

テンプレートの主な配置先

商品一覧
app/template/<テンプレート名>/Product/list.twig
商品詳細
app/template/<テンプレート名>/Product/detail.twig
標準テンプレート
app/template/default/

通常はID・名前・真偽値を返すスカラーAPIを使用してください。b2b_customer()、b2b_group()、b2b_applicable_rule()、b2b_applicable_tier()はEntityを返し、b2b_price_context()にもrule・tierのEntityが含まれます。これらは信頼できるテンプレートで必要なプロパティだけを参照するための上級者向けAPIです。Entity全体をdumpしたり、会員の個人情報や内部設定を画面・ログへ出力したりしないでください。

Twig API

ログイン・会員グループ

引数を省略すると、現在ログイン中のフロント会員を参照します。管理画面ユーザーはフロント会員として扱いません。

Twig関数戻り値用途
b2b_is_logged_in()boolフロント会員がログイン中か
b2b_is_guest()boolゲストまたはフロント会員以外か
b2b_customer()Customer|null現在のフロント会員。ゲスト時はnull
b2b_customer_id()int|null現在の会員ID
b2b_group()B2BGroup|null現在のB2Bグループ
b2b_group_id()int|null現在のB2BグループID
b2b_group_name()string|null現在のB2Bグループ名
b2b_group_label()stringグループ名、一般会員、またはゲスト
b2b_has_group()boolB2Bグループに割り当て済みか
b2b_in_group_id(groupId)bool指定グループIDと一致するか
b2b_in_group_name(groupName)bool指定グループ名と一致するか
b2b_in_any_group_ids(groupIds)bool指定した複数のグループIDのいずれかと一致するか
b2b_in_any_group_names(groupNames)bool指定した複数のグループ名のいずれかと一致するか

日本語ラベルを表示

<p>会員区分:{{ b2b_group_label() }}</p>

複数グループを許可

{% if b2b_in_any_group_ids([3, 5, 8]) %}
  許可された会員向けコンテンツ
{% endif %}

Twig API

商品・タグ・カテゴリ・表示可否

Productには商品エンティティを渡します。複数条件は配列で指定できます。

Twig関数戻り値用途
b2b_product_is_id(Product, productId)bool商品IDが一致するか
b2b_product_has_tag_id(Product, tagId)boolタグIDが設定されているか
b2b_product_has_tag_name(Product, tagName)boolタグ名が設定されているか
b2b_product_has_any_tag_ids(Product, tagIds)bool指定タグIDのいずれかを持つか
b2b_product_has_any_tag_names(Product, tagNames)bool指定タグ名のいずれかを持つか
b2b_product_has_category_id(Product, categoryId)boolカテゴリIDが直接設定されているか
b2b_product_has_category_name(Product, categoryName)boolカテゴリ名が直接設定されているか
b2b_product_has_any_category_ids(Product, categoryIds)bool指定カテゴリIDのいずれかに直接所属するか
b2b_product_has_any_category_names(Product, categoryNames)bool指定カテゴリ名のいずれかに直接所属するか
b2b_product_matches(Product, criteria = {})bool商品・カテゴリ・タグの条件をまとめて判定
b2b_can_view_product(Product, allowedGroupIds, criteria = {})bool商品を表示してよいかを画面用に判定
b2b_can_purchase_product(Product, allowedGroupIds, criteria = {})bool購入UIを表示してよいかを画面用に判定

判定仕様:criteriaが空の場合は、渡した商品そのものを制限対象として扱います。条件が指定され、商品がその条件に一致しない場合は制限対象外のためtrueです。制限対象商品では、現在のグループIDがallowedGroupIdsに含まれる場合だけtrueです。

カテゴリ判定は商品に直接設定されたカテゴリとの一致です。親カテゴリ・子カテゴリの自動展開は行いません。

criteriaで使用できるキーはproduct_ids、tag_ids、tag_names、category_ids、category_namesです。各条件はOR、名前は完全一致で評価されます。ID配列には整数、名前配列には空でない文字列を指定してください。トップレベルの空条件{}はすべての商品を対象にします。一方、指定したキーの配列が空(例:{tag_ids: []})またはキー・値が不正な場合は、設定ミスによる迂回を防ぐためアクセス判定がfalseになります。

複数の対象条件をまとめる

次の例は、商品ID 100、カテゴリID 12、タグID 7または9のいずれかに一致する商品を対象にします。

{% set target = {
  product_ids: [100],
  category_ids: [12],
  tag_ids: [7, 9]
} %}

{% if b2b_product_matches(Product, target) %}
  カスタマイズ対象商品
{% endif %}

Twig API

B2B価格・ルール・数量別割引

価格計算には商品規格のProductClassと数量を渡します。ルール判定にはProductを渡します。

Twig関数戻り値用途
b2b_price_context(ProductClass, quantity = 1){ base_unit_price, unit_price, quantity, discount, rule, tier, matched }価格・ルール・割引段階を1回の整合した計算で取得(推奨)
b2b_price_result(ProductClass, quantity = 1){ discount, unit_price }数量適用後の割引総額と表示単価
b2b_display_unit_price(ProductClass, quantity = 1)int数量適用後の単価
b2b_discount_amount(ProductClass, quantity = 1)int対象明細の割引総額
b2b_applicable_rule(Product)B2BPriceRule|null現在の会員に適用可能な価格ルール
b2b_has_applicable_rule(Product)bool適用可能な価格ルールがあるか
b2b_applicable_tier(Product, quantity = 1)B2BPriceTier|null指定数量に適用される割引段階
b2b_has_tier_rule(Product)bool数量別割引ルールを持つ対象商品か

価格・ルール・割引段階をまとめて取得

同じ計算で決定された価格・ルール・割引段階を表示する場合は、個別の関数を組み合わせず、この統合ヘルパーを使用します。

{% set context = b2b_price_context(ProductClass, quantity) %}

<span>単価:{{ context.unit_price|price }}</span>
{% if context.discount > 0 %}
  <span>割引額:-{{ context.discount|price }}</span>
{% endif %}
{% if context.matched %}
  <span>適用ルール:{{ context.rule.name }}</span>
{% endif %}

適用ルール・割引段階を参照

{% set rule = b2b_applicable_rule(Product) %}
{% set tier = b2b_applicable_tier(Product, quantity) %}

{% if rule %}
  <p>適用ルール:{{ rule.name }}</p>
{% endif %}
{% if tier %}
  <p>適用開始数量:{{ tier.minQty }}</p>
{% endif %}

b2b_applicable_rule()は、現在のグループに属する有効なルールのうち、商品に一致し、割引段階を1件以上持つルールを返します。この時点では指定数量が最小数量へ到達していることまでは意味しません。

b2b_applicable_tier()は指定数量を満たす割引段階を探します。先に見つかったルールの最小数量を満たさない場合は、後続の一致ルールも確認します。ゲスト・一般会員・該当なしの場合はnullです。

価格・ルール・割引段階を同時表示する場合はb2b_price_context()を使用してください。各値が同じルールと割引段階から計算され、個別関数の重複クエリや組み合わせの不一致を避けられます。個別関数は単一情報の参照に使用できます。商品一覧では結果をTwig変数へ保存し、ループ内の重複呼び出しを避けてください。標準の商品一覧価格はプラグインが既に差し込むため、独自表示が必要な場合だけ使用してください。

Product/detail.twig・Product/list.twig

用途別の実装例

以下のIDは例です。管理画面やデータベースで実際のグループ・タグ・カテゴリIDを確認して置き換えてください。

Example 1

ゲストには価格とカートボタンを表示しない

detail.twig

標準の価格表示部分とカートフォーム部分を、次の条件で囲みます。B2Bグループ未割当の一般会員もログイン済みなので表示されます。

{% if b2b_is_logged_in() %}
  {# 既存の価格表示をここへ配置 #}
  <p class="ec-price">{{ Product.getPrice02IncTaxMin|price }}</p>

  {# 既存のカートフォームをここへ配置 #}
  {{ form_start(form, {attr: {id: 'form1'}}) }}
    {# 規格・数量・カートボタン等 #}
  {{ form_end(form) }}
{% else %}
  <p class="ec-color-red">
    価格の確認・ご購入にはログインが必要です。
  </p>
  <a href="{{ url('mypage_login') }}">ログインする</a>
{% endif %}

Example 2

商品一覧でゲスト向け価格・購入UIを隠す

list.twig

{% for Product in pagination %}内にある価格・カート部分を囲みます。商品名や画像はゲストにも表示されます。

{% for Product in pagination %}
  <div class="ec-shelfGrid__item">
    {# 商品画像・商品名は常に表示 #}
    <a href="{{ url('product_detail', {id: Product.id}) }}">
      {{ Product.name }}
    </a>

    {% if b2b_is_logged_in() %}
      {# 既存の価格表示・カートボタン #}
      <p>{{ Product.getPrice02IncTaxMin|price }}</p>
    {% else %}
      <p>ログイン後に価格をご確認いただけます。</p>
    {% endif %}
  </div>
{% endfor %}

Example 3

「社員」グループだけ購入ボタンを表示する

ID指定を推奨

例ではタグID 12を社員限定商品、グループID 3を社員グループとして扱います。対象外の商品は通常どおり購入できます。

{% set employee_group_ids = [3] %}
{% set employee_only = {tag_ids: [12]} %}
{% set can_buy = b2b_can_purchase_product(
  Product,
  employee_group_ids,
  employee_only
) %}

{% if can_buy %}
  {# 既存のカートフォーム・購入ボタン #}
  <button type="submit">カートに入れる</button>
{% elseif b2b_is_guest() %}
  <p>この商品を購入するにはログインしてください。</p>
{% else %}
  <p>この商品は社員グループ限定です。</p>
{% endif %}

Example 4

タグまたはカテゴリで対象商品を増やす

複数条件

商品を1件ずつTwigへ追加せず、管理画面で対象タグ・カテゴリを設定して運用できます。条件内のIDはいずれかに一致すると対象になります。

{% set restricted_products = {
  category_ids: [20, 21],
  tag_ids: [12, 13]
} %}

{% set can_buy = b2b_can_purchase_product(
  Product,
  [3],
  restricted_products
) %}

{% if can_buy %}
  {# 購入UI #}
{% else %}
  <p>ご契約グループでは購入できません。</p>
{% endif %}

Example 5

対象商品そのものを一覧から隠す

list.twig

購入UIだけでなく商品カード全体を隠す場合は、商品カードの外側でb2b_can_view_product()を判定します。

{% set restricted_products = {tag_ids: [12]} %}

{% for Product in pagination %}
  {% set can_view = b2b_can_view_product(
    Product,
    [3],
    restricted_products
  ) %}

  {% if can_view %}
    <div class="ec-shelfGrid__item">
      {# 商品カード全体 #}
    </div>
  {% endif %}
{% endfor %}

ページネーションの件数はEC-CUBEが取得した全商品を基準にするため、Twigでカードを隠すと表示件数が少なくなる場合があります。検索結果から完全に除外するには、商品検索クエリ側のカスタマイズが別途必要です。

Example 6

現在のB2B会員区分を表示する

共通パーツ
<dl>
  <dt>会員区分</dt>
  <dd>{{ b2b_group_label() }}</dd>

  {% if b2b_has_group() %}
    <dt>B2BグループID</dt>
    <dd>{{ b2b_group_id() }}</dd>
  {% endif %}
</dl>
状態b2b_group_label()b2b_group_id()
ゲストゲストnull
グループ未割当会員一般会員null
B2B会員設定されたグループ名グループID

Example 7

B2B価格・割引情報を独自表示する

ProductClass
{% set quantity = 5 %}
{% set context = b2b_price_context(ProductClass, quantity) %}

<p>通常単価:{{ context.base_unit_price|price }}</p>
<p>B2B単価:{{ context.unit_price|price }}</p>
{% if context.discount > 0 %}
  <p>数量{{ context.quantity }}点の割引額:-{{ context.discount|price }}</p>
{% endif %}
{% if context.rule %}
  <p>適用ルール:{{ context.rule.name }}</p>
{% endif %}
{% if context.tier %}
  <p>適用開始数量:{{ context.tier.minQty }}点</p>
{% endif %}

サーバー側でも同じ条件を検証

PHPから再利用する場合

Twig関数の内部ロジックはサービスへ分離されています。独自プラグインやイベントリスナーでは、Twigを呼び出さず対応するサービスをDIして再利用してください。

主なサービス

Service\B2BCustomerContext
現在ログイン中のストア会員・B2Bグループを解決
Service\B2BProductMatcher
商品ID・タグ・カテゴリ条件を判定
Service\B2BAccessService
現在ログイン中のストア会員について、許可グループと商品条件を組み合わせて判定
Service\B2BPriceCalculator
表示単価・割引額・適用ルールを計算

検証が必要な境界

  • 商品をカートへ追加するとき
  • カート内の商品・数量を更新するとき
  • 購入確認から注文を確定するとき
  • 独自APIや外部連携から注文を作成するとき
  • 管理画面で受注明細を変更・再計算するとき

B2BAccessServiceの使い分け

ストア画面では現在ログイン中の会員を使うメソッド、受注・バッチ・外部連携では対象会員を明示するメソッドを使用します。

PHPメソッド対象会員用途
canViewProduct(Product $Product, array $allowedGroupIds, array $criteria = [])現在のストア会員ストア画面の表示判定
canPurchaseProduct(Product $Product, array $allowedGroupIds, array $criteria = [])現在のストア会員ストア画面の購入UI判定
canCustomerViewProduct(?Customer $Customer, Product $Product, array $allowedGroupIds, array $criteria = [])引数で指定受注・バッチ等での表示ポリシー判定
canCustomerPurchaseProduct(?Customer $Customer, Product $Product, array $allowedGroupIds, array $criteria = [])引数で指定カート・注文・外部連携での購入ポリシー判定

Repositoryから会員割当を取得

特定のCustomerを明示して調べる場合は、B2BCustomerGroupRepositoryの次のメソッドを利用できます。現在のログイン会員を調べるだけなら、認証状態を安全に扱うB2BCustomerContextを推奨します。

PHPメソッド戻り値用途
findAssignmentByCustomer(?Customer $Customer)B2BCustomerGroup|null会員とグループの割当レコードを取得
findGroupByCustomer(?Customer $Customer)B2BGroup|null会員に割り当てられたグループを取得
hasGroupForCustomer(?Customer $Customer)bool会員にB2Bグループが割り当て済みか
isCustomerInGroupId(?Customer $Customer, int $groupId)bool会員が指定グループIDに属するか
isCustomerInGroupName(?Customer $Customer, string $groupName)bool会員が指定グループ名に属するか

実装上の注意

拒否時は単にボタンを隠すのではなく、サーバー側で処理を中止し、利用者向けメッセージを返してください。注文確定直前にも再検証し、ログイン状態やグループ変更後の古いカートが制限を迂回しないようにします。

B2BCustomerContextとTwig向けのcanViewProduct()・canPurchaseProduct()は、現在ログイン中のストア会員を対象にします。管理画面の受注編集・受注再計算では、ログイン中の管理者ではなくOrderの注文者を判定する必要があります。$Order->getCustomer()をcanCustomerViewProduct()またはcanCustomerPurchaseProduct()へ明示的に渡してください。

PHPでの判定例

次は、現在のストア会員を対象とするサービスの再利用方法を示す最小例です。実際にはカート・購入処理のイベントやProcessor内で、拒否結果をEC-CUBEのエラーとして返してください。

use Eccube\Entity\Product;
use Plugin\SimpleB2BPriceTierV2\Service\B2BAccessService;

final class EmployeeProductGuard
{
    private B2BAccessService $b2bAccessService;

    public function __construct(B2BAccessService $b2bAccessService)
    {
        $this->b2bAccessService = $b2bAccessService;
    }

    public function canPurchase(Product $Product): bool
    {
        return $this->b2bAccessService->canPurchaseProduct(
            $Product,
            [3],
            ['tag_ids' => [12]]
        );
    }
}

管理画面の受注では注文者を明示する

管理画面では現在のSecurityユーザーが管理者になるため、B2BCustomerContextで注文者グループを判定しないでください。対象商品の判定と、受注に紐づく会員の判定を明示的に組み合わせます。

$canPurchase = $this->b2bAccessService->canCustomerPurchaseProduct(
    $Order->getCustomer(),
    $Product,
    [3],
    ['tag_ids' => [12]]
);

変更後の確認

キャッシュ削除・反映手順

  1. 1

    バックアップ

    変更するTwigファイルをバックアップします。

  2. 2

    Twigを編集

    使用中のテンプレート配下だけを変更します。

  3. 3

    キャッシュ削除

    管理画面またはCLIからEC-CUBEキャッシュを削除します。

  4. 4

    権限別テスト

    ゲスト・一般会員・許可グループ・非許可グループで確認します。

CLIでキャッシュを削除

php bin/console cache:clear --env=prod
php bin/console cache:warmup --env=prod

最低限の確認項目

  • 商品一覧・商品詳細の両方
  • 規格あり・規格なし商品
  • 対象・対象外の商品、タグ、カテゴリ
  • 数量境界の前後と端数処理
  • 直接カート追加・カート更新・注文確定