Spree and Software Development: Git and Ruby techniques
Having tackled a few interesting Spree projects lately, I thought I’d share some software evelopment tips I’ve picked up along the way.
Gem or Source?
The first decision you may need to make is whether to run Spree from a gem or source. Directions for both are included at the Spree Quickstart Guide, but the guide doesn’t touch on motivation from running from a gem versus source. The Spree documentation does address the question, but I wanted to comment based on recent experience. I’ve preferred to build an application running from the gem for most client projects. The only times I’ve decided to work against Spree source code was when the Spree edge code had a major change that wasn’t available in a released gem, or if I wanted to troubleshoot the internals of Spree, such as the extension loader or localization functionality.
If you follow good code organization practices and develop modular and abstracted functionality, it should be quite easy to switch back and forth between gem and source. However, switching back and forth between Spree gem and source may not be cleanly managed from a version control perspective.
git rebase
Git rebase is lovely. Ethan describes some examples of using git rebase here. When working with several other developers and even when I’m the sole developer, I’ve included rebasing in my pull and push workflow.
.gitmodules
Git submodules are lovely, also. An overview on git submodules with contributions from Brian Miller and David Christensen can be read here. Below is an example of a .gitmodules from a recent project that includes several extensions written by folks in the Spree community:
[submodule "vendor/extensions/faq"]
path = vendor/extensions/faq
url = git://github.com/joshnuss/spree-faq.git
[submodule "vendor/extensions/multi_domain"]
path = vendor/extensions/multi_domain
url = git://github.com/railsdog/spree-multi-domain.git
[submodule "vendor/extensions/paypal_express"]
path = vendor/extensions/paypal_express
url = git://github.com/railsdog/spree-paypal-express.git
.gitignore
This should apply to software development for other applications as well, but it’s important to setup .gitignore correctly at the beginning of the project. I typically ignore database, log, and tmp files. Occasionally, I ignore some public asset files (stylesheets, javascripts, images) if they are copied over from an extension upon server restart, which is standard in Spree.
Overriding modules, controllers, and views
Now, the good stuff! So, let’s assume the Spree core is missing functionality that you need. Options for expanding from the Spree core include overriding or extending existing models, controllers, or views, or writing and including new models, controllers, or views. Spree’s Extension Tutorial covers adding new controllers, models, and views, so I’ll discuss extending and overriding existing models, views and controllers below.
Extend an Existing Controller
To extend an existing controller, I’ve typically included a module with the extended behavior in the *_extension.rb file. For all examples, let’s assume that my extension is named “Site”, another standard in Spree. The code below shows the module include in site_extension.rb:
...
def activate
ProductsController.send(:include, Spree::Site::ProductsController)
end
...
My ProductsController module, inside the Spree::Site namespace, includes the following to define a before filter in the Spree core products controller:
module Spree::Site::ProductsController
def self.included(controller)
controller.class_eval do
controller.append_before_filter :do_stuff
end
end
def do_stuff
#doing stuff
end
end
Override an Existing Controller Method
Next, to override a method in an existing controller, I’ve started the same way as before, by including a module in site_extension.rb:
...
def activate
CheckoutsController.send(:include, Spree::Site::CheckoutsController)
end
...
The Spree::Site::CheckoutsController module will contain:
module Spree::Site::CheckoutsController
def self.included(target)
target.class_eval do
alias :spree_rate_hash :rate_hash
def rate_hash; site_rate_hash; end
end
end
def site_rate_hash
# compute new rate_hash
end
end
In this example, the core rate_hash method is aliased for later use. And the rate_hash method is redefined inside the class_eval block. This example demonstrates how to override the core shipping rate computation during checkout.
Extend an Existing Model
Next, I’ll provide an example of extending an existing Spree model. site_extension.rb will include the following:
...
def activate
Product.send(:include, Spree::Site::Product)
end
...
And Spree::Site::Product module contains:
module Spree::Site::Product
def new_product_method
# new product instance method
end
end
In the situation where you may want to create a class object method rather than an instance object method, you may include the following in Spree::Site::Product:
module Spree::Site::Product
def self.included(target)
def target.do_something_special
'Something Special!'
end
end
end
The above example adds a method to the Product class object to be called from a view, for example <%= Product.do_something_special %> will return ‘Something Special!’.
Override an Existing Model Method
To override a method from an existing model, I start with a module include in site_extension.rb:
...
def activate
Product.send(:include, Spree::Site::Product)
end
...
And Spree::Site::Product contains the following:
module Spree::Site::Product
def self.included(model)
model.class_eval do
alias :spree_master_price :master_price
def master_price; site_master_price; end
end
end
def site_master_price
'1 billion dollars'
end
end
And from the view, the two methods can be called within the following block:
<% @products.each do |product| -%>
<%= product.master_price %> vs <%= product.spree_master_price.to_s %>
<% end -%>
Extend an Existing View
I previously discussed the introduction of hooks in depth here and here. To extend an existing view that has a hook wrapped around the content you intend to modify, you may add something similar to the following to *_hooks.rb, where * is the extension name:
insert_after :homepage_products, 'shared/promo'
The above code inserts the ‘shared/promo’ view to be rendered above the homepage_products hook in the Spree gem or Spree source ~/app/views/products.index.html.erb view. Other hook actions include insert_before, replace, or remove.
Override an Existing View
Before the introduction of hooks, the standard method of overriding or extending core views was to copy the core view into your extension view directory, and apply changes. In some cases, hooks are not always in the desired location. To override the footer view since there is no footer hook, I copy the Spree gem footer view to the extension view directory. The diff below compares the Spree gem view and my extension footer view:
- <div id="footer">
- <div class="left">
- <p>
- <%= t("powered_by") %> <a href="http://spreecommerce.com/">Spree</a>
- </p>
- </div>
- <div class="right">
- <%= render 'shared/language_bar' if Spree::Config[:allow_locale_switching] %>
- </div>
- </div>
<%= render 'shared/google_analytics' %>
+ <p><a href="https://www.endpointdev.com/">End Point</a></p>
Sample data
A final tip that I’ve found helpful when developing with Spree is to create sample data files in the extension db directory to maintain data consistency between developers. In a recent project, I’ve created the following stores.yml data to initiate several stores for the multi domain extension:
store_1:
name: Store1
code: store1
domains: store1.mysite.com
default: false
store_2:
name: Store2
code: store2
domains: store2.mysite.com
default: true
store_3:
name: Store3
code: store3
domains: store3.mysite.com
default: false
Many of these tips apply to general to software development. The tips specific to development in Spree (and possibly other Rails platforms) include the sample data syntax and the described Ruby techniques to extend and override class and model functionality.
Comments