Skip to content

Instantly share code, notes, and snippets.

@beneggett
Created July 12, 2012 15:06
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save beneggett/3098702 to your computer and use it in GitHub Desktop.
Save beneggett/3098702 to your computer and use it in GitHub Desktop.
Engines on Rails

This Presentation was built to be viewed with GistDeck

Rails Engines

Ben Eggett

beggett@savvi.com

801.946.1510

"I didn't want rails to succumb to the lure of high-level components like login systems, forums, content management, and the likes." - DHH (Apr 07)

hmm... so what about:


Devise

Forem

Refinery

Spree

"But the goal of Rails is to create a world where [engines] are neither needed or strongly desired. Obviously, we are not quite there yet." -DHH

?

"Engines have not received the blessing of the RoR core team, and I wouldn't expect any different, because it would be madness to include them in core Rails." - James Adam, 2005

http://article.gmane.org/gmane.comp.lang.ruby.rails/29166

History of Engines


James Adam: Created first 'engines' plugin

10/31/2005 (Rails 0.14.2)

It was not really that well received because it broke every time a minor (and often tiny) release was pushed.

Engine Support Today

Rails < 2.3

(~Mar 2009)

Use James' engines plugin http://github.com/lazyatom/engines

Rails 3.0

(~Aug 2010)

Actually, engines are not looking all that bad. Thanks to good guys like Piotr Sarnacki and the Rails core team.

Rails 3.1

(~May 2011)

Ya'll ever head of the Asset Pipeline? Engines just became awesome(r).

Rails 3.2

(~Jan 2012)

Not a lot has really changed, in general better namespacing practices are being followed in the community

What are engines?

(extracted from rails guide on engines )

Engines can be considered miniature applications that provide functionality to their host applications.

A Rails application is actually just a “supercharged” engine, with the Rails::Application class inheriting a lot of its behaviour from Rails::Engine.

In Rails 3.0, a Rails::Application object was introduced which is nothing more than an Engine but with the responsibility of coordinating the whole boot process.

Therefore, engines and applications can be thought of as almost the same thing...

Engines are also closely related to plugins where the two share a common lib directory structure and are both generated using the rails plugin new generator.

The difference being that an engine is considered a “full plugin” by Rails as indicated by the —full option that’s passed to the generator command.

Engines can also be isolated from their host applications.

This means that an application is able to have a path provided by a routing helper such as posts_path and use an engine also that provides a path also called posts_path, and the two would not clash.

  <%= link_to 'App posts', posts_path %>
  <%= link_to 'Engine posts', engine.posts_path %>

Along with this, controllers, models and table names are also namespaced.

It’s important to keep in mind at all times that the application should always take precedence over its engines.

Finally, engines would not have be possible without the work of James Adam, Piotr Sarnacki, the Rails Core Team, and a number of other people.

How to generate new engine:

Create a plugin

Plugin is essentially foundation for engine

  rails plugin new urug_testimonials --full --mountable
  • --full --mountable give routes, assets directories
  • as of 3.1 plugins are also a Gem by default, meaning pushing to github is simple

What's inside your engine?

Inside UrugTestimonials::Engine

  • App/ directory (assets/controllers/helpers/models/views all namespaced)
  • config: routes.rb (initializers | locales | generators & templates would go here)
  • lib: namespaced_dirs: engine modules, version | rake tasks
  • script/ directory
  • test/ directory
  • Rakefile (tasks go into lib/tasks)
  • Gemfile, .gemspec, .gitignore, MIT-LICENSE
  • No readme file is generated

Step away from the Gemfile

Gemfile should only source rubygems and gemspec

  source "http://rubygems.org"
  gemspec

All dependencies belong in the .gemspec file

  s.add_dependency "paperclip"
  s.add_development_dependency "rspec" 
  

There are some interesting things happening we should be aware of:

In lib/urug_testimonials.rb

  require 'urug_testimonials/engine'
  module UrugTestimonials
  
  end

There are some interesting things happening we should be aware of:

In lib/urug_testimonials/engine.rb

  module UrugTestimonials
    class Engine < Rails::Engine
      isolate_namespace UrugTestimonials
    end
  end
  • Rails engine tells rails app there is an engine here and to add it to the load paths

  • If constants exist, great; if not unitialized constant error; restart or define constants appropriately.

Namespacing

Avoid double names:

app/models/post.rb ≠ path/to/engine/app/models/post.rb => Conflict!

well, just call it something different?

Remember isolate_namespace?

isolate_namespace urug app/models/urug/post.rb

  UrugTestimonials::Post

isolate_namespace

also namespaces tables as:

  urug_testimonials_posts

isolate_namespace

also namespaces app/{assets/controllers/helpers/views/models}

Works so that you could replace any of the predefined methods by simply creating an app/controllers/urug_testimonials/application_controller.rb file

  • This can be assets/controllers/helpers/models/views

Drawing Routes

  Rails.application.routes.draw:
  Rails.application.routes.draw do 
    namespace :urug_testimonials do
      resources :testimonials
    end
  end

WRONG

Drawing on application routes, is generally bad.

Instead we use UrugTestinomials::Engine.routes.draw

urug_testimonials/config/routes.rb

  UrugTestimonials::Engine.routes.draw do
    resources :forums
    ...
  end

Mount the engine in our application

urug/config/routes.rb

  mount UrugTestimonials::Engine, :at => "/testimonials"
  • Routes will not interfere, at all

Namespacing Tricks and path_helpers

within an engine, one can call testimonials_path

outside of engine, prefix with engine's isolated_namespace

  urug_testimonials.testimonials_path

access app paths from within engine by prefixing with main_app

  main_app.users_path

By default Engines, have their own layout/application file.

You can remove this and make an engine load seamlessly into an existing app

There are two methods of doing this:

1.) In your engine's application controller

  require :layout => 'desired_layout'

You can specify whatever layout you want.

There are two methods of doing this:

2.) (Better way) Remove applications layout from engine and adjust the application_controller class to inherit from the Application rather than ActionController::Base.

  module UrugTestimonials
    class ApplicationController < ActionController::Base
      
    end
  end

becomes =>

  class UrugTestimonials::ApplicationController < ApplicationController
      
  end

The layout will be passed through from the application.

Conventions

I prefer namespacing my engines (or any classes within modules really) like this:

  class UrugTestimonials::ApplicationController < ApplicationController
      
  end

rather than

  module UrugTestimonials
    class ApplicationController < ActionController::Base
      
    end
  end

because it allows me to more naturally have my classes inherit from objects outside the scope of the given module (without tampering with helpers or monkey patching a solution).

Complex Engines: Spree example

Spree is like 5 engines packed into one

It wasn't always that way; it was just a standalone application you would customize to fit your ecommerce needs.

Auth | Core | API | Dash | Promo => App

  Spree::Core::Engine.routes.draw
  Spree::Auth::Engine.routes.draw
  Spree::API::Engine.routes.draw
  Spree::Dash::Engine.routes.draw
  Spree::Promo::Engine.routes.draw
  mount Spree::Core::Engine, at => '/'
  mount Spree::Auth::Engine, at => '/'
  mount Spree::API::Engine, at => '/'
  mount Spree::Dash::Engine, at => '/'
  mount Spree::Promo::Engine, at => '/'
  mount Spree::YourExtension1::Engine, at => '/'
  mount Spree::YourExtension2::Engine, at => '/'
  mount Spree::YourExtension3::Engine, at => '/'

But how can we expect people to know that

  spree_core.root_path
  spree_auth.login_path

mount Spree::Core::Engine, at: '/'

Draw all routes onto core, using prepend

  Spree::Core::Engine.routes.prepend do 
    ...
  end

Mount core onto app

Fix everything, lots of broken tests mainly pass { :use_route => :spree }

  def spree_user
    current_user
  end

config/initializers/spree.rb

  Spree.user_class = "User"

The End

Ben Eggett | Director of Software Dev @ Savvi

beggett@savvi.com | 801.946.1510


(prelude to @blowmage)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment