Loading...
Loading...
Use when the user wants to extend an existing Spree 6 model or API controller without forking — add an association to Spree::Product, add a validation or scope, add a method to Spree::Cart / Spree::Order, narrow what an API v3 endpoint returns, permit an extra attribute on writes. Common phrasings include "add brand to product", "decorate Spree::X", "ProductDecorator", "OrderDecorator", "Module#prepend", "spree:model_decorator", "spree:controller_decorator", "extend an existing Spree model", "add a method to Spree::Order", "override Spree behavior", "monkey patch Spree", "customize an API controller". Provides the decorator pattern, the generators, the prepended(base) idiom, API controller decorators and the gotchas — and routes behavioral changes to workflow hooks, events or dependencies instead.
npx skill4agent add spree/agent-skills spree-decoratorsCommands use the Spree CLI (). On a classic Rails app without the CLI, usespree …from the app root. Paths assume the Rails app lives inbin/rails g ….server/
Module#prepend| Use case | Use this instead of a decorator |
|---|---|
| Veto an operation (add-to-cart limit, checkout rule, cancel/return policy) | Workflow |
| Contribute data to tax / promotion calculation | Workflow context hook — |
| Do something alongside a cart/order/fulfillment/payment operation | Workflow lifecycle hook — |
| React after a save / sync to ERP / send notification | Event subscriber — |
| Replace how a flow computes | |
| Change an API response shape | Serializer subclass via |
| Accept an extra attribute on API writes | |
Make a field filterable via | |
| Store an extra value without a migration | Custom fields ( |
| Add a new endpoint | A new |
| Admin UI changes | Dashboard plugins / slots — |
| Add an association, validation, scope, or helper method to a model | Decorator (this skill) |
Narrow an existing endpoint's | Controller decorator (this skill) |
Order#finalize!Order#cancelPayment#capture!Fulfillment#finalize!Spree::Carts::CompleteSpree::Orders::CancelSpree::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)
endconfig.to_prepareconfig/application.rbapp/**/*_decorator*.rbprependspree 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.rbbin/rails g spree:model_decorator …classifySpree::Exports::Productsspree generate migration AddBrandIdToSpreeProducts brand_id:bigint:indexclass AddBrandIdToSpreeProducts < ActiveRecord::Migration[8.1]
def change
add_column :spree_products, :brand_id, :bigint
add_index :spree_products, :brand_id
end
endmodule 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)
endbelongs_tooptional: trueclass_namedependent:has_manymodule 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)
endmetadatapublic_metadataprivate_metadataprivate_metadatametadata->>validateclear_validators!node_modules/@spree/docs/dist/developer/customization/validations.mdsupermodule Spree
module ProductDecorator
def purchasable?
return false if metadata[:embargoed]
super
end
end
Product.prepend(ProductDecorator)
endsuperSpree::Purchase::*Spree::CartSpree::OrderSpree::Purchase::Totals::Addresses::Taxation::PaymentProcessing::CheckoutSteps::Validationsspree_coreapp/models/concerns/spree/purchase/Spree::Cart.instance_method(:amount).ownerSpree::CartSpree::OrdersuperSpree::Purchase::Totals.prepend(MyTotals)Spree::Product.additional_permitted_attributes += [:brand_id]
Spree::LineItem.additional_permitted_attributes += [:gift_note]+=|=<<FrozenError=prependedRails.application.config.to_prepareresource_permitted_attributesSpree::PermittedAttributesSpree::LineItem.additional_permitted_attributesoptionsPOST /carts/:id/itemsSpree::Carts::AddItemoptionsidvariant_idquantity# 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]=…add_associationstorefront_ransackable_associationsSpree::Product.storefront_ransackable_associations |= %w[brand]private_ransackable_attributes{ store: [...], seller: [...] }spree-api-v3Spree::Api::V3::Store::*Spree::Api::V3::Admin::*::Seller::*ResourceControllerscopeserializer_classcollection_includesscope_includesresource_permitted_attributesmodel_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)current_userprotectedprotectedmodule 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# 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
endspree generate api_resourcespree-resourcerender_errorsuperprepended@setting = truebase.class_attribute :setting, default: truehas_many :variantsclass_name: 'Spree::Variant'Spree::ProductDecoratorapp/models/spree/product_decorator.rbSpree::OrderSpree::CartSpree::Carts::Completebelongs_tooptional: trueSpree.user_classSpree.customer_classSpree::CustomerSpree.admin_user_classSpree::AdminUserserver/app/models/spree/
├── product_decorator.rb # the only file that calls prepend
└── product/
├── brand_decorator.rb # plain modules
└── seo_decorator.rbmodule Spree
module ProductDecorator
include Product::BrandDecorator
include Product::SeoDecorator
end
Product.prepend(ProductDecorator)
endrequirerequire_dependency*_decorator*.rbprependafter_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
endidspree-events-webhooksnode_modules/@spree/docs/dist/developer/customization/decorators.mdnode_modules/@spree/docs/dist/developer/customization/validations.mdnode_modules/@spree/docs/dist/developer/customization/api.mdnode_modules/@spree/docs/dist/developer/tutorial/model-and-api.mdspree-workflowsspree-dependenciesspree-events-webhooksspree-resourcespree-customization