mcpField
S
spree-decorators

spree-decorators

github.com/spree開発10225 回インストール4 週間前に更新
リポジトリ

spree-decorators について

Rails アプリで Ruby デコレーターを使って Spree クラスを拡張する開発者向けスキルです。モデルやコントローラーのデコレーターの使い分け、生成方法、実装例に加え、ワークフローや API 向けの代替手段も説明します。

主な機能

  1. 01モデルとコントローラーのデコレーターを生成
  2. 02モデルの関連とバリデーションを追加
  3. 03スコープとヘルパーメソッドを拡張
  4. 04API コントローラーのフックを調整
  5. 05属性を許可しフィルターを設定
  6. 06別の拡張ポイントを選ぶ場面を特定

活用シーン

  1. 01Spree モデルにフィールド関連を追加
  2. 02モデルのスコープや読み取り専用ヘルパーを定義
  3. 03API エンドポイントの返却レコードを絞り込む
  4. 04業務処理の変更にデコレーターでなくワークフローフックを選択

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/spree/agent-skills --skill spree-decorators

インストール後、Claude Code で/spree-decoratorsを入力して読み込まれたか確認します。

詳細

Spree デコレーター

コマンドでは Spree CLI(spree …)を使用します。CLI のない従来の Rails アプリでは、アプリのルートから bin/rails g … を使用してください。パスは、Rails アプリが server/ にあることを前提としています。

デコレーターを使うと、Ruby の Module#prepend と Spree のファイル名規約、2 つのジェネレーターを用いて、アプリから既存の Spree クラスを変更できます。Spree の内部実装に依存するため、構造上の変更(関連付け、バリデーション、スコープ、読み取り専用のヘルパーメソッド、コントローラーのスコープの絞り込み)に使用し、振る舞いの変更には使用しないでください。

まず適切なツールを選ぶ

用途デコレーターの代わりに使用するもの
操作を拒否する(カートへの追加制限、チェックアウトルール、キャンセル/返品ポリシー)ワークフローの validate フック — spree-workflows
税金/プロモーションの計算にデータを提供するワークフローのコンテキストフック — spree-workflows
カート/注文/フルフィルメント/決済の操作に合わせて処理を行うワークフローのライフサイクルフック — spree-workflows
保存後に処理を行う/ERP と同期する/通知を送信するイベントサブスクライバー — spree-events-webhooks
フローの計算方法を置き換えるSpree.dependencies — spree-dependencies
API のレスポンス形式を変更するSpree.api.*_serializer を介したシリアライザーのサブクラス — spree-dependencies
API の書き込みで追加の属性を受け入れるModel.additional_permitted_attributes += [...](以下を参照)
q[...] でフィルターできるフィールドを追加するSpree.ransack.add_attribute(Spree::Product, :brand_id)
マイグレーションなしで追加の値を保存するカスタムフィールド(product.set_custom_field('ns.key', value))— spree-catalog
新しいエンドポイントを追加する新しい ResourceController サブクラス — spree-resource
管理画面の UI を変更するダッシュボードプラグイン/スロット — spree-dashboard-plugins
モデルに関連付け、バリデーション、スコープ、ヘルパーメソッドを追加するデコレーター(このスキル)
既存エンドポイントの scope を絞り込む、または serializer_class を差し替えるコントローラーデコレーター(このスキル)

ビジネス動詞(Order#finalize!、Order#cancel、Payment#capture!、Fulfillment#finalize!)は決してデコレートしないでください。これらはワークフローに処理を委譲する非推奨のシェルであり、実際のフロー(Spree::Carts::Complete、Spree::Orders::Cancel、Spree::Payments::Capture)からは呼び出されません。ステータス遷移にもフック可能なステートマシンコールバックはありません。ワークフローのフックを使用してください。

パターン

# server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      # class-level: associations, validations, scopes, class_attributes — called on base
    end

    # instance methods here; call super to reach the original
  end

  Product.prepend(ProductDecorator)
end

デコレーターは、config/application.rb の config.to_prepare ブロック(Spree インストーラーによって追加されます)で読み込まれます。このブロックは app/**/*_decorator*.rb を glob するため、prepend は起動時と開発時のリロードごとに実行されます。アプリにこのブロックがない場合は追加してください。Zeitwerk だけでは参照されていないデコレーターは読み込まれず、適用されないままになります。

ジェネレーター

spree generate model_decorator Spree::Product
# → server/app/models/spree/product_decorator.rb

spree generate controller_decorator Spree::Api::V3::Admin::ProductsController
# → server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb

(CLI がない場合は bin/rails g spree:model_decorator … を使用します。)モデルジェネレーターは名前を classify に渡し、単数形に変換します。複数形のクラス(Spree::Exports::Products など)では、ファイルを手動で作成してください。

モデルデコレーターのパターン

関連付けを追加する

まずマイグレーションを作成します。Spree のスタイルでは外部キー制約を付けません。

spree generate migration AddBrandIdToSpreeProducts brand_id:bigint:index
class AddBrandIdToSpreeProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :spree_products, :brand_id, :bigint
    add_index :spree_products, :brand_id
  end
end
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
      base.has_many :videos, class_name: 'Spree::Video', dependent: :destroy
      base.additional_permitted_attributes += [:brand_id]   # writable via Admin API (see below)
    end
  end

  Product.prepend(ProductDecorator)
end

Spree のモデルでは、belongs_to はデフォルトで必須です。optional: true を追加しないと、既存のすべての商品の保存で「Brand must exist」というエラーになります。常に class_name(文字列。遅延解決されるため、読み込み順序の問題を回避できます)と、has_many の dependent: を指定してください。

バリデーション、スコープ、クラスメソッド
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.validates :external_id, uniqueness: { scope: :store_id }, allow_nil: true   # plus a DB index
      base.scope :recently_added, -> { where(created_at: 30.days.ago..) }
      base.scope :tagged_featured, -> { where("metadata->>'featured' = ?", 'true') }  # jsonb on PostgreSQL
      base.extend ClassMethods
    end

    module ClassMethods
      def search_by_name(query)
        where('LOWER(name) LIKE ?', "%#{sanitize_sql_like(query.downcase)}%")
      end
    end
  end

  Product.prepend(ProductDecorator)
end

メタデータを持つモデルには、単一の metadata JSON カラムがあります(public_metadata / private_metadata はすでに存在せず、private_metadata は非推奨の Ruby エイリアスです)。metadata は内部用で、Store API では公開されません。ストアフロントに表示するデータやフィルタリング可能なデータには、実際のカラムまたはカスタムフィールドを使用してください。JSON 演算子(->>)は PostgreSQL の構文です。MySQL/SQLite では異なる SQL が必要です。

操作レベルのルール(「注文ごとに最大 10 個」「B2B 顧客のみ」)はモデルのバリデーションではありません。データ移行やスタッフによる修正を妨げないよう、ワークフローの validate フックに設定してください。コアのルールを緩和するために clear_validators! を使わないでください。node_modules/@spree/docs/dist/developer/customization/validations.md を参照してください。

メソッドをオーバーライドする(super を呼び出す)
module Spree
  module ProductDecorator
    def purchasable?
      return false if metadata[:embargoed]
      super
    end
  end

  Product.prepend(ProductDecorator)
end

super を省略すると、元の実装が完全に置き換えられます。オーバーライドは読み取り専用のメソッドや派生メソッドにとどめ、書き込みや資金の移動を伴う処理はワークフローに実装してください。

Cart と Order:メソッドは Spree::Purchase::* の concern にあります

Spree::Cart(チェックアウト前)と Spree::Order(確定済みで変更不可)は、concern を通じて動作を共有します。Spree::Purchase::Totals、::Addresses、::Taxation、::PaymentProcessing、::CheckoutSteps(Cart のみ)、::Validations などです(spree_core の app/models/concerns/spree/purchase/)。デコレートする前に、メソッドがどこで定義されているかを確認してください(Spree::Cart.instance_method(:amount).owner)。

  • 片方だけ変更する場合は、Spree::Cart または Spree::Order をデコレートしてください(prepend されたモジュールは concern の前に配置され、super は concern に到達します)。
  • 両方を変更する場合は、両方のクラスをデコレートするか、concern 自体に prepend してください(Spree::Purchase::Totals.prepend(MyTotals) — Ruby 3 では、すでに concern を include しているクラスにも反映されます)。
  • 変更したいカート/注文のロジック(アイテムの追加、合計、完了処理など)の大半は、モデルではなくワークフローにあります。フックを使用してください。
書き込み時に新しい属性を許可する
Spree::Product.additional_permitted_attributes += [:brand_id]
Spree::LineItem.additional_permitted_attributes += [:gift_note]
  • 必ず +=(または |=)を使ってください。デフォルトは凍結配列なので(FrozenError)、<< は使わないでください。また、他の拡張機能が追加した内容を消してしまう = も使わないでください。
  • 開発時にエンジンのモデルが再読み込みされても有効なように、モデルデコレーターの prepended(前述)か、Rails.application.config.to_prepare ブロックに記述してください。
  • API v3 コントローラーは、このリストを独自のリスト(resource_permitted_attributes)と統合します。Spree::PermittedAttributes はすでに存在しません。
  • ラインアイテムの場合、カートへの追加でレコードに渡されるのは Spree::LineItem.additional_permitted_attributes だけです。POST /carts/:id/items(Spree::Carts::AddItem)では、トップレベルまたは options 内に送信できます。options 内のその他の値はすべて破棄されます。また、id、variant_id、quantity はすでに検証と価格設定が済んでいるため、許可しても設定できません。
フィルタリング可能にする
# config/initializers/spree.rb
Spree.ransack.add_attribute(Spree::Product, :brand_id)
Spree.ransack.add_association(Spree::Product, :brand)
Spree.ransack.add_scope(Spree::Product, :recently_added)

これを設定しないと、q[brand_id_eq]=… は通知なく無視され、200 が返ってもフィルタリングされません。add_association が拡張するのは Admin API だけです。Store API が参照するのは storefront_ransackable_associations のみです(購入者がブランド名で絞り込めるようにする場合は、同じ initializer に Spree::Product.storefront_ransackable_associations |= %w[brand] を追加してください)。Seller API は何も参照しません。スタッフ専用の属性を購入者/販売者に見せないようにするには、private_ransackable_attributes({ store: [...], seller: [...] })を使用してください。spree-api-v3 を参照してください。

コントローラーデコレーターのパターン(API v3)

Spree に含まれるコントローラーは API 用のみです。Spree::Api::V3::Store::*、Spree::Api::V3::Admin::*(および ::Seller::*)です。デコレート対象となる Rails 管理画面や Rails ストアフロントはありません。アクションではなく、ResourceController が公開する名前付きフック(scope、serializer_class、collection_includes、scope_includes、resource_permitted_attributes、model_class)をオーバーライドしてください。

エンドポイントが返す内容を絞り込む
# server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb
module Spree::Api::V3::Admin
  module ProductsControllerDecorator
    protected

    # Chain onto super — it is already store-scoped (and ability-scoped on the
    # Admin API). Replacing it leaks other stores' records.
    # current_user is nil for secret-key (sk_) requests — guard with &.
    def scope
      return super if current_user&.spree_admin?

      super.where(discontinue_on: nil)
    end
  end
end

Spree::Api::V3::Admin::ProductsController.prepend(Spree::Api::V3::Admin::ProductsControllerDecorator)

Store API では、current_user はサインイン中の顧客(ゲストの場合は nil)です。Admin API ではスタッフメンバー(シークレットキーによるリクエストの場合は nil)です。フックメソッドは基底クラスで protected になっているため、デコレーターでも protected のままにしてください。

1つのエンドポイントのシリアライザーを差し替える
module Spree::Api::V3::Store
  module ProductsControllerDecorator
    protected

    def serializer_class
      MyStore::DetailedProductSerializer
    end
  end
end

Spree::Api::V3::Store::ProductsController.prepend(Spree::Api::V3::Store::ProductsControllerDecorator)

すべての箇所で差し替えるには、代わりに Spree.api.product_serializer = … を使用してください(spree-dependencies)。

新しいアクションには新しいコントローラーを

デコレーターを使って Spree のコントローラーにアクションを追加しないでください。リソースコントローラーをサブクラス化すると、ページネーション、Ransack によるフィルタリング、プレフィックス付き ID、エラーエンベロープ、認可を引き継げます。

# server/app/controllers/spree/api/v3/admin/product_audits_controller.rb
module Spree::Api::V3::Admin
  class ProductAuditsController < ResourceController
    scoped_resource :products   # API-key scope: read_products / write_products (required on Admin controllers)

    protected

    def model_class = MyStore::ProductAudit
    def serializer_class = MyStore::ProductAuditSerializer
    def resource_permitted_attributes = [:note]
  end
end
# config/routes.rb
Spree::Core::Engine.add_routes do
  namespace :api, defaults: { format: 'json' } do
    namespace :v3 do
      namespace :admin do
        resources :product_audits
      end
    end
  end
end

spree generate api_resource を実行すると、モデル、シリアライザー、コントローラー、ルートのひな形が生成されます。詳しくは spree-resource を参照してください。

API デコレーターでは JSON をレンダリングしてください(render_error/標準のエラーエンベロープ)。フラッシュメッセージやリダイレクトは使用しないでください。

よくある落とし穴

  • super を呼び忘れる — Spree のロジックが通知なく置き換わります。
  • prepended 内でのインスタンス変数の使用(@setting = true)— インスタンスではなくモジュール上に保持されます。base.class_attribute :setting, default: true を使用してください。
  • 起動時の定数参照 — class_name: 'Spree::Variant' を指定しない has_many :variants は、読み込み順序のエラーを引き起こす可能性があります。
  • ファイルとモジュールの不一致 — Spree::ProductDecorator は app/models/spree/product_decorator.rb に配置してください。
  • チェックアウトの動作を変更するために Spree::Order をデコレートする — チェックアウト前の状態は Spree::Cart です。注文が存在するのは Spree::Carts::Complete の後だけです。
  • optional: true を指定しない belongs_to — 既存の行で「must exist」エラーが発生します。
  • Spree.user_class — 非推奨です。Spree.customer_class(デフォルトは Spree::Customer)または Spree.admin_user_class(Spree::AdminUser)をデコレートしてください。

多数のデコレーターを整理する

server/app/models/spree/
├── product_decorator.rb        # the only file that calls prepend
└── product/
    ├── brand_decorator.rb      # plain modules
    └── seo_decorator.rb
module Spree
  module ProductDecorator
    include Product::BrandDecorator
    include Product::SeoDecorator
  end

  Product.prepend(ProductDecorator)
end

Zeitwerk はパスからネストされたモジュールを解決するため、require/require_dependency は不要です(Zeitwerk モードでは削除されています)。ネストされたファイルも *_decorator*.rb グロブに一致するため、それらのファイル内で prepend を呼び出さないようにしてください。

コールバックデコレーターを移行する

after_save :sync_to_erp を追加するデコレーターは、サブスクライバーに置き換えてください。

# server/app/subscribers/product_sync_subscriber.rb   (spree generate subscriber ProductSync)
class ProductSyncSubscriber < Spree::Subscriber
  subscribes_to 'product.updated'

  def handle(event)
    ExternalSyncJob.perform_later(event.payload['id'])
  end
end

ペイロードは変更セットではなく、シリアライズされたレコード(プレフィックス付きの id)です。登録と非同期動作については spree-events-webhooks を参照してください。操作中に金額、在庫、外部システムを扱うコールバックは、代わりにワークフローフックに含めてください。

さらに詳しく読む

  • node_modules/@spree/docs/dist/developer/customization/decorators.md (https://spreecommerce.org/docs/developer/customization/decorators)
  • node_modules/@spree/docs/dist/developer/customization/validations.md — フック、デコレーターのバリデーション、レジストリの違い
  • node_modules/@spree/docs/dist/developer/customization/api.md — カスタム API エンドポイント
  • node_modules/@spree/docs/dist/developer/tutorial/model-and-api.md — 商品にブランドを追加する手順
  • 関連スキル:spree-workflows、spree-dependencies、spree-events-webhooks、spree-resource、spree-customization

コメント