All of Spree’s models, controllers, helpers, etc can easily be extended or overridden to meet your exact requirements using standard Ruby idioms.Standard practice for including such changes in your application or extension is to create a file within the relevant app/models/spree or app/controllers/spree directory with the original class name with _decorator appended.
When working with Spree, you’ll often need to add functionality to existing models like Spree::Product or Spree::Order. However, you shouldn’t modify these files directly because:
Upgrades - Your changes would be lost when updating Spree
Maintainability - It’s hard to track what you’ve customized
Conflicts - Direct modifications can conflict with Spree’s code
Instead, we use decorators - a Ruby pattern that lets you add or modify behavior of existing classes without changing their original source code.
In Ruby, classes are “open” - you can add methods to them at any time. Decorators leverage this by:
Creating a module with your new methods
Using Module#prepend to inject your module into the class’s inheritance chain
Your methods run first, and can call super to invoke the original method
Copy
Ask AI
# This is the basic patternmodule Spree module ProductDecorator # Add a new method def my_new_method "Hello from decorator!" end # Override an existing method def existing_method # Do something before result = super # Call the original method # Do something after result end end Product.prepend(ProductDecorator)end
The key line is Product.prepend(ProductDecorator) - this inserts your module at the beginning of the method lookup chain, so your methods are found first.
bin/rails g spree:controller_decorator Spree::Admin::ProductsController
This creates app/controllers/spree/admin/products_controller_decorator.rb:
Copy
Ask AI
module Spree module Admin module ProductsControllerDecorator def self.prepended(base) # Class-level configurations go here end end ProductsController.prepend(ProductsControllerDecorator) endend
The most common use case is changing the behavior of existing methods. When overriding a method, you can call super to invoke the original implementation:
app/models/spree/product_decorator.rb
Copy
Ask AI
module Spree module ProductDecorator def available? # Add custom logic before return false if discontinued? # Call the original method super end end Product.prepend(ProductDecorator)end
Always consider whether you need to call super when overriding methods. Omitting it completely replaces the original behavior, which may break functionality.
module Spree module ProductDecorator def self.prepended(base) base.before_validation :normalize_name base.after_save :sync_to_external_service base.after_destroy :cleanup_external_data end private def normalize_name self.name = name.strip.titleize if name.present? end def sync_to_external_service ExternalSyncJob.perform_later(self) if saved_change_to_name? end def cleanup_external_data ExternalCleanupJob.perform_later(id) end end Product.prepend(ProductDecorator)end
Use extend within the prepended callback to add class methods:
app/models/spree/product_decorator.rb
Copy
Ask AI
module Spree module ProductDecorator def self.prepended(base) base.extend ClassMethods end module ClassMethods def search_by_name(query) where('LOWER(name) LIKE ?', "%#{query.downcase}%") end end end Product.prepend(ProductDecorator)end
module Spree module Admin module ProductsControllerDecorator def create # Add custom logic before log_product_creation_attempt # Call original method super # Add custom logic after notify_team_of_new_product if @product.persisted? end private def log_product_creation_attempt Rails.logger.info "Product creation attempted by #{current_spree_user.email}" end def notify_team_of_new_product ProductNotificationJob.perform_later(@product) end end ProductsController.prepend(ProductsControllerDecorator) endend
module Spree module CheckoutControllerDecorator def self.prepended(base) base.before_action :check_minimum_order, only: [:update] end private def check_minimum_order if @order.total < 25.0 && params[:state] == 'payment' flash[:error] = 'Minimum order amount is $25' redirect_to checkout_state_path(@order.state) end end end CheckoutController.prepend(CheckoutControllerDecorator)end
# ❌ Bad - completely replaces original behaviordef available? in_stock? && active?end# ✅ Good - extends original behaviordef available? super && custom_availability_checkend
# ❌ Bad - instance variables don't work in prependeddef self.prepended(base) @custom_setting = true # This won't work as expectedend# ✅ Good - use class attributes or methodsdef self.prepended(base) base.class_attribute :custom_setting, default: trueend
# ❌ Bad - can cause loading issues# product_decorator.rbdef self.prepended(base) base.has_many :variants # Variant decorator might not be loaded yetend# ✅ Good - use strings for class namesdef self.prepended(base) base.has_many :variants, class_name: 'Spree::Variant'end