Skip to content
Browse files

[engines guide] basic revision/review work

  • Loading branch information...
1 parent 3b3433f commit 9ee71e2378768cf0128ff49f31065bf18e85dfe1 @radar radar committed Feb 14, 2012
Showing with 89 additions and 31 deletions.
  1. +89 −31 railties/guides/source/engines.textile
View
120 railties/guides/source/engines.textile
@@ -12,31 +12,33 @@ endprologue.
h3. What are 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 from +Rails::Engine+. Therefore, engines and applications share common functionality but are at the same time two separate beasts. Engines and applications also share a common structure, as you'll see throughout this guide.
+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+.
-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.
+Therefore, engines and applications can be thought of almost the same thing, just with very minor differences, as you'll see throughout this guide. Engines and applications also share a common structure.
-The engine that will be generated for this guide will be called "blorgh". The engine will provide blogging functionality to its host applications, allowing for new posts and comments to be created. For now, you will be working solely within the engine itself and in later sections you'll see how to hook it into an application.
+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 -- but this guide will refer to them simply as "engines" throughout. An engine *can* be a plugin, and a plugin *can* be an engine.
+
+The engine that will be created in this guide will be called "blorgh". The engine will provide blogging functionality to its host applications, allowing for new posts and comments to be created. At the beginning of this guide, you will be working solely within the engine itself, but in later sections you'll see how to hook it into an application.
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. Along with this, controllers, models and table names are also namespaced. You'll see how to do this later in this guide.
-It's important to keep in mind at all times that the application should *always* take precedence over its engines. An application is the object that has final say in what goes on in the universe, where the engine should only be enhancing it, rather than changing it.
+It's important to keep in mind at all times that the application should *always* take precedence over its engines. An application is the object that has final say in what goes on in the universe (with the universe being the application's environment) where the engine should only be enhancing it, rather than changing it drastically.
-To see demonstrations of other engines, check out "Devise":https://github.com/plataformatec/devise, an engine that provides authentication for its parent applications, or "Forem":https://github.com/radar/forem, an engine that provides forum functionality.
+To see demonstrations of other engines, check out "Devise":https://github.com/plataformatec/devise, an engine that provides authentication for its parent applications, or "Forem":https://github.com/radar/forem, an engine that provides forum functionality. There's also "Spree":https://github.com/spree/spree which provides an e-commerce platform, and "RefineryCMS":https://github.com/resolve/refinerycms, a CMS engine.
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. If you ever meet them, don't forget to say thanks!
h3. Generating an engine
-To generate an engine with Rails 3.1, you will need to run the plugin generator and pass it the +--mountable+ option. To generate the beginnings of the "blorgh" engine you will need to run this command in a terminal:
+To generate an engine with Rails 3.1, you will need to run the plugin generator and pass it the +--full+ and +--mountable+ options. To generate the beginnings of the "blorgh" engine you will need to run this command in a terminal:
<shell>
$ rails plugin new blorgh --full --mountable
</shell>
The +--full+ option tells the plugin generator that you want to create an engine (which is a mountable plugin, hence the option name), creating the basic directory structure of an engine by providing things such as the foundations of an +app+ folder, as well a +config/routes.rb+ file. This generator also provides a file at +lib/blorgh/engine.rb+ which is identical in function to an application's +config/application.rb+ file.
-The +--mountable+ option tells the generator to mount the engine inside the dummy testing application located at +test/dummy+ inside the engine. It does this by placing this line in to the dummy application's +config/routes.rb+ file:
+The +--mountable+ option tells the generator to mount the engine inside the dummy testing application located at +test/dummy+ inside the engine. It does this by placing this line in to the dummy application's +config/routes.rb+ file, located at +test/dummy/config/routes.rb+ inside the engine:
<ruby>
mount Blorgh::Engine, :at => "blorgh"
@@ -46,7 +48,7 @@ h4. Inside an engine
h5. Critical files
-At the root of the engine's directory, lives a +blorgh.gemspec+ file. When you include the engine into the application later on, you will do so with this line in a Rails application's +Gemfile+:
+At the root of this brand new engine's directory, lives a +blorgh.gemspec+ file. When you include the engine into the application later on, you will do so with this line in a Rails application's +Gemfile+:
<ruby>
gem 'blorgh', :path => "vendor/engines/blorgh"
@@ -61,6 +63,8 @@ module Blorgh
end
</ruby>
+TIP: Some engines choose to use this file to put global configuration options for their engine. It's a relatively good idea, and so if you're wanting offer configuration options, the file where your engine's +module+ is defined is perfect for that. Place the methods inside the module and you'll be good to go.
+
Within +lib/blorgh/engine.rb+ is the base class for the engine:
<ruby>
@@ -71,29 +75,39 @@ module Blorgh
end
</ruby>
-By inheriting from the +Rails::Engine+ class, this gem notifies Rails that there's an engine at the specified path, and will correctly mount the engine inside the application, performing tasks such as adding the +app+ directory of the engine to the load path for models, controllers and views.
+By inheriting from the +Rails::Engine+ class, this gem notifies Rails that there's an engine at the specified path, and will correctly mount the engine inside the application, performing tasks such as adding the +app+ directory of the engine to the load path for models, mailers, controllers and views.
The +isolate_namespace+ method here deserves special notice. This call is responsible for isolating the controllers, models, routes and other things into their own namespace, away from similar components inside hte application. Without this, there is a possibility that the engine's components could "leak" into the application, causing unwanted disruption, or that important engine components could be overriden by similarly named things within the application.
-NOTE: It is *highly* recommended that the +isolate_namespace+ line be left within the +Engine+ class definition.
+NOTE: It is *highly* recommended that the +isolate_namespace+ line be left within the +Engine+ class definition. Without it, classes generated in an engine *may* conflict with an application.
-The +isolate_namespace+ line will cause the models, controllers and views to be namespaced for the engine. What this means is that a model called +Post+ would instead be called +Blorgh::Post+, a controller called +PostsController+ would be +Blorgh::Postscontroller+ and the views for that controller would not be at +app/views/posts+, but rather +app/views/blorgh/posts+. In addition to this, the table for the model is namespaced, becoming +blorgh_posts+, rather than simply +posts+.
+What this isolation of the namespace means is that a model generated by a call to +rails g model+ such as +rails g model post+ wouldn't be called +Post+, but instead be namespaced and called +Blorgh::Post+. In addition to this, the table for the model is namespaced, becoming +blorgh_posts+, rather than simply +posts+. Similar to the model namespacing, a controller called +PostsController+ would be +Blorgh::Postscontroller+ and the views for that controller would not be at +app/views/posts+, but rather +app/views/blorgh/posts+. Mailers would be namespaced as well.
-Finally, routes will also be isolated within the engine. This is discussed later in the "Routes":#routes section of this guide.
+Finally, routes will also be isolated within the engine. This is one of the most important parts about namespacing, and is discussed later in the "Routes":#routes section of this guide.
h5. +app+ directory
-Inside the +app+ directory there lives the standard +assets+, +controllers+, +helpers+, +mailers+, +models+ and +views+ directories that you should be familiar with from an application. The +helpers+, +mailers+ and +models+ directories are empty and so aren't described in this section. We'll look more into models in a future section.
+Inside the +app+ directory there is the standard +assets+, +controllers+, +helpers+, +mailers+, +models+ and +views+ directories that you should be familiar with from an application. The +helpers+, +mailers+ and +models+ directories are empty and so aren't described in this section. We'll look more into models in a future section, when we're writing the engine.
Within the +app/assets+ directory, there is the +images+, +javascripts+ and +stylesheets+ directories which, again, you should be familiar with due to their similarities of an application. One difference here however is that each directory contains a sub-directory with the engine name. Because this engine is going to be namespaced, its assets should be too.
Within the +app/controllers+ directory there is a +blorgh+ directory and inside that a file called +application_controller.rb+. This file will provide any common functionality for the controllers of the engine. The +blorgh+ directory is where the other controllers for the engine will go. By placing them within this namespaced directory, you prevent them from possibly clashing with identically-named controllers within other engines or even within the application.
+NOTE: The +ApplicationController+ class is called as such inside an engine -- rather than +EngineController+ -- mainly due to that if you consider that an engine is really just a mini-application, it makes sense. You should also be able to convert an application to an engine with relatively little pain, and this is just one of the ways to make that process easier, albeit however so slightly.
+
Lastly, the +app/views+ directory contains a +layouts+ folder which contains file at +blorgh/application.html.erb+ which allows you to specify a layout for the engine. If this engine is to be used as a stand-alone engine, then you would add any customization to its layout in this file, rather than the applications +app/views/layouts/application.html.erb+ file.
+If you don't want to force a layout on to users of the engine, then you can delete this file and reference a different layout in the controllers of your engine.
+
h5. +script+ directory
-This directory contains one file, +script/rails+, which allows you to use the +rails+ sub-commands and generators just like you would within an application. This means that you will very easily be able to generate new controllers and models for this engine.
+This directory contains one file, +script/rails+, which enables you to use the +rails+ sub-commands and generators just like you would within an application. This means that you will very easily be able to generate new controllers and models for this engine by running commands like this:
+
+<shell>
+rails g model
+</shell>
+
+Keeping in mind, of course, that anything generated with these commands inside an engine that has +isolate_namespace+ inside the Engine class will be namespaced.
h5. +test+ directory
@@ -156,11 +170,11 @@ invoke css
create app/assets/stylesheets/scaffold.css
</shell>
-The first thing that the scaffold generator does is invoke the +active_record+ generator, which generates a migration and a model for the resource. Note here, however, that the migration is called +create_blorgh_posts+ rather than the usual +create_posts+. This is due to the +isolate_namespace+ method called in the +Blorgh::Engine+ class's definition. The model here is also namespaced, being placed at +app/models/blorgh/post.rb+ rather than +app/models/post.rb+.
+The first thing that the scaffold generator does is invoke the +active_record+ generator, which generates a migration and a model for the resource. Note here, however, that the migration is called +create_blorgh_posts+ rather than the usual +create_posts+. This is due to the +isolate_namespace+ method called in the +Blorgh::Engine+ class's definition. The model here is also namespaced, being placed at +app/models/blorgh/post.rb+ rather than +app/models/post.rb+ due to the +isolate_namespace+ call within the +Engine+ class.
Next, the +test_unit+ generator is invoked for this model, generating a unit test at +test/unit/blorgh/post_test.rb+ (rather than +test/unit/post_test.rb+) and a fixture at +test/fixtures/blorgh/posts.yml+ (rather than +test/fixtures/posts.yml+).
-After that, a line for the resource is inserted into the +config/routes.rb+ file for the engine. This line is simply +resources :posts+, turning the +config/routes.rb+ file into this:
+After that, a line for the resource is inserted into the +config/routes.rb+ file for the engine. This line is simply +resources :posts+, turning the +config/routes.rb+ file for the engine into this:
<ruby>
Blorgh::Engine.routes.draw do
@@ -169,11 +183,11 @@ Blorgh::Engine.routes.draw do
end
</ruby>
-Note here that the routes are drawn upon the +Blorgh::Engine+ object rather than the +YourApp::Application+ class. This is so that the engine routes are confined to the engine itself and can be mounted at a specific point as shown in the "test directory":#test-directory section. This is discussed further in the "Routes":#routes section of this guide.
+Note here that the routes are drawn upon the +Blorgh::Engine+ object rather than the +YourApp::Application+ class. This is so that the engine routes are confined to the engine itself and can be mounted at a specific point as shown in the "test directory":#test-directory section. This is also what causes the engine's routes to be isolated from those routes that are within the application. This is discussed further in the "Routes":#routes section of this guide.
Next, the +scaffold_controller+ generator is invoked, generating a controlled called +Blorgh::PostsController+ (at +app/controllers/blorgh/posts_controller.rb+) and its related views at +app/views/blorgh/posts+. This generator also generates a functional test for the controller (+test/functional/blorgh/posts_controller_test.rb+) and a helper (+app/helpers/blorgh/posts_controller.rb+).
-Everything this generator has generated is neatly namespaced. The controller's class is defined within the +Blorgh+ module:
+Everything this generator has created is neatly namespaced. The controller's class is defined within the +Blorgh+ module:
<ruby>
module Blorgh
@@ -185,7 +199,7 @@ end
NOTE: The +ApplicationController+ class being inherited from here is the +Blorgh::ApplicationController+, not an application's +ApplicationController+.
-The helper is also namespaced:
+The helper inside +app/helpers/blorgh/posts_helper.rb+ is also namespaced:
<ruby>
module Blorgh
@@ -224,7 +238,7 @@ One final thing is that the +posts+ resource for this engine should be the root
root :to => "posts#index"
</ruby>
-Now people will only need to go to the root of the engine to see all the posts, rather than visiting +/posts+.
+Now people will only need to go to the root of the engine to see all the posts, rather than visiting +/posts+. This means that instead of +http://localhost:3000/blorgh/posts+, you only need to go to +http://localhost:3000/blorgh+ now.
h4. Generating a comments resource
@@ -272,7 +286,7 @@ module Blorgh
end
</ruby>
-Because the +has_many+ is defined inside a class that is inside the +Blorgh+ module, Rails will know that you want to use the +Blorgh::Comment+ model for these objects.
+NOTE: Because the +has_many+ is defined inside a class that is inside the +Blorgh+ module, Rails will know that you want to use the +Blorgh::Comment+ model for these objects, so there's no need to specify that using the +:class_name+ option here.
Next, there needs to be a form so that comments can be created on a post. To add this, put this line underneath the call to +render @post.comments+ in +app/views/blorgh/posts/show.html.erb+:
@@ -293,15 +307,17 @@ Next, the partial that this line will render needs to exist. Create a new direct
<% end %>
</erb>
-This form, when submitted, is going to attempt to post to a route of +posts/:post_id/comments+ within the engine. This route doesn't exist at the moment, but can be created by changing the +resources :posts+ line inside +config/routes.rb+ into these lines:
+When this form is submitted, it is going to attempt to perform a +POST+ request to a route of +/posts/:post_id/comments+ within the engine. This route doesn't exist at the moment, but can be created by changing the +resources :posts+ line inside +config/routes.rb+ into these lines:
<ruby>
resources :posts do
resources :comments
end
</ruby>
-The route now will exist, but the controller that this route goes to does not. To create it, run this command:
+This creates a nested route for the comments, which is what the form requires.
+
+The route now exists, but the controller that this route goes to does not. To create it, run this command:
<shell>
$ rails g controller comments
@@ -430,15 +446,15 @@ When an engine is created, it may want to use specific classes from an applicati
Usually, an application would have a +User+ class that would provide the objects that would represent the posts' and comments' authors, but there could be a case where the application calls this class something different, such as +Person+. It's because of this reason that the engine should not hardcode the associations to be exactly for a +User+ class, but should allow for some flexibility around what the class is called.
-To keep it simple in this case, the application will have a class called +User+ which will represent the users of the application. It can be generated using this command:
+To keep it simple in this case, the application will have a class called +User+ which will represent the users of the application. It can be generated using this command inside the application:
<shell>
rails g model user name:string
</shell>
The +rake db:migrate+ command needs to be run here to ensure that our application has the +users+ table for future use.
-Also to keep it simple, the posts form will have a new text field called +author_name_+ where users can elect to put their name. The engine will then take this name and create a new +User+ object from it or find one that already has that name, and then associate the post with it.
+Also, to keep it simple, the posts form will have a new text field called +author_name+ where users can elect to put their name. The engine will then take this name and create a new +User+ object from it or find one that already has that name, and then associate the post with it.
First, the +author_name+ text field needs to be added to the +app/views/blorgh/posts/_form.html.erb+ partial inside the engine. This can be added above the +title+ field with this code:
@@ -550,7 +566,23 @@ The +set_author+ method also located in this class should also use this class:
self.author = Blorgh.user_class.constantize.find_or_create_by_name(author_name)
</ruby>
-To set this configuration setting within the application, an initializer should be used. By using an initializer, the configuration will be set up before the application starts and makes references to the classes of the engine which may depend on this configuration setting existing.
+To save having to call +constantize+ on the +user_class+ result all the time, you could instead just override the +user_class+ getter method inside the +Blorgh+ module in the +lib/blorgh.rb+ file to always call +constantize+ on the saved value before returning the result:
+
+<ruby>
+ def self.user_class
+ @@user_class.constantize
+ end
+</ruby>
+
+This would then turn the above code for +self.author=+ into this:
+
+<ruby>
+self.author = Blorgh.user_class.find_or_create_by_name(author_name)
+</ruby>
+
+Resulting in something a little shorter, and more implicit in its behaviour. The +user_class+ method should always return a +Class+ object.
+
+To set this configuration setting within the application, an initializer should be used. By using an initializer, the configuration will be set up before the application starts and calls the engine's models which may depend on this configuration setting existing.
Create a new initializer at +config/initializers/blorgh.rb+ inside the application where the +blorgh+ engine is installed and put this content in it:
@@ -562,13 +594,13 @@ WARNING: It's very important here to use the +String+ version of the class, rath
Go ahead and try to create a new post. You will see that it works exactly in the same way as before, except this time the engine is using the configuration setting in +config/initializers/blorgh.rb+ to learn what the class is.
-There are now no strict dependencies on what the class is, only what the class's API must be. The engine simply requires this class to define a +find_or_create_by_name+ method which returns an object of that class to be associated with a post when it's created.
+There are now no strict dependencies on what the class is, only what the class's API must be. The engine simply requires this class to define a +find_or_create_by_name+ method which returns an object of that class to be associated with a post when it's created. This object, of course, should have some sort of identifier by which it can be referenced.
h5. General engine configuration
Within an engine, there may come a time where you wish to use things such as initializers, internationalization or other configuration options. The great news is that these things are entirely possible because a Rails engine shares much the same functionality as a Rails application. In fact, a Rails application's functionality is actually a superset of what is provided by engines!
-If you wish to use an initializer -- code that should run before the engine is loaded -- the place for it is the +config/initializers+ folder. This directory's functionality is explained in the "Initializers section":http://guides.rubyonrails.org/configuring.html#initializers of the Configuring guide, and works precisely the same way as the +config/initializers+ directory inside an application.
+If you wish to use an initializer -- code that should run before the engine is loaded -- the place for it is the +config/initializers+ folder. This directory's functionality is explained in the "Initializers section":http://guides.rubyonrails.org/configuring.html#initializers of the Configuring guide, and works precisely the same way as the +config/initializers+ directory inside an application. Same goes for if you want to use a standard initializer.
For locales, simply place the locale files in the +config/locales+ directory, just like you would in an application.
@@ -578,6 +610,22 @@ When an engine is generated there is a smaller dummy application created inside
The +test+ directory should be treated like a typical Rails testing environment, allowing for unit, functional and integration tests.
+h4. Functional tests
+
+A matter worth taking into consideration when writing functional tests is that the tests are going to be running on an application -- the +test/dummy+ application -- rather than your engine. This is due to the setup of the testing environment; an engine needs an application as a host for testing its main functionality, especially controllers. This means that if you were to make a typical +GET+ to a controller in a controller's functional test like this:
+
+<ruby>
+get :index
+</ruby>
+
+It may not function correctly. This is because the application doesn't know how to route these requests to the engine unless you explicitly tell it *how*. To do this, you must pass the +:use_route+ option (as a parameter) on these requests also:
+
+<ruby>
+get :index, :use_route => :blorgh
+</ruby>
+
+This tells the application that you still want to perform a +GET+ request to the +index+ action of this controller, just that you want to use the engine's route to get there, rather than the application.
+
h3. Improving engine functionality
This section looks at overriding or adding functionality to the views, controllers and models provided by an engine.
@@ -645,16 +693,26 @@ If a template is rendered from within an engine and it's attempting to use one o
h4. Assets
-Assets within an engine work in much the same way as they do inside an application. Because the engine class inherits from +Rails::Engine+, the application will know to look up in the engine's +app/assets+ directory for potential assets.
+Assets within an engine work in an identical way to a full application. Because the engine class inherits from +Rails::Engine+, the application will know to look up in the engine's +app/assets+ directory for potential assets.
-Much like all the other components of an engine, the assets should also be namespaced. This means if you have an asset called +style.css+, it should be placed at +app/assets/stylesheets/[engine name]/style.css+, rather than +app/assets/stylesheets/style.css+. If this asset wasn't namespaced, then there is a possibility that the host application could have an asset named identically, in which case the application's asset would take precedence.
+Much like all the other components of an engine, the assets should also be namespaced. This means if you have an asset called +style.css+, it should be placed at +app/assets/stylesheets/[engine name]/style.css+, rather than +app/assets/stylesheets/style.css+. If this asset wasn't namespaced, then there is a possibility that the host application could have an asset named identically, in which case the application's asset would take precedence and the engine's one would be all but ignored.
-To include this asset inside an application, just use +stylesheet_link_tag+ like normal:
+Imagine that you did have an asset located at +app/assets/stylesheets/blorgh/style.css+ To include this asset inside an application, just use +stylesheet_link_tag+ and reference the asset as if it were inside the engine:
<erb>
<%= stylesheet_link_tag "blorgh/style.css" %>
</erb>
+You can also specify these assets as dependencies of other assets using the Asset Pipeline require statements in processed files:
+
+<css>
+/*
+ *= require blorgh/style
+*/
+</css>
+
+For more information, read the "Asset Pipeline guide":http://guides.rubyonrails.org/asset_pipeline.html
+
h4. Other gem dependencies
Gem dependencies inside an engine should be specified inside the +.gemspec+ file that's at the root of the engine. The reason for this is because the engine may be installed as a gem. If dependencies were to be specified inside the +Gemfile+, these would not be recognised by a traditional gem install and so they would not be installed, causing the engine to malfunction.

0 comments on commit 9ee71e2

Please sign in to comment.
Something went wrong with that request. Please try again.