spree-decorators について
Rails アプリで Ruby デコレーターを使って Spree クラスを拡張する開発者向けスキルです。モデルやコントローラーのデコレーターの使い分け、生成方法、実装例に加え、ワークフローや API 向けの代替手段も説明します。
主な機能
- 01モデルとコントローラーのデコレーターを生成
- 02モデルの関連とバリデーションを追加
- 03スコープとヘルパーメソッドを拡張
- 04API コントローラーのフックを調整
- 05属性を許可しフィルターを設定
- 06別の拡張ポイントを選ぶ場面を特定
活用シーン
- 01Spree モデルにフィールド関連を追加
- 02モデルのスコープや読み取り専用ヘルパーを定義
- 03API エンドポイントの返却レコードを絞り込む
- 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