diff options
Diffstat (limited to 'guides/source/engines.md')
-rw-r--r-- | guides/source/engines.md | 1401 |
1 files changed, 1401 insertions, 0 deletions
diff --git a/guides/source/engines.md b/guides/source/engines.md new file mode 100644 index 0000000000..24548a5b01 --- /dev/null +++ b/guides/source/engines.md @@ -0,0 +1,1401 @@ +Getting Started with Engines +============================ + +In this guide you will learn about engines and how they can be used to provide +additional functionality to their host applications through a clean and very +easy-to-use interface. + +After reading this guide, you will know: + +* What makes an engine. +* How to generate an engine. +* Building features for the engine. +* Hooking the engine into an application. +* Overriding engine functionality in the application. + +-------------------------------------------------------------------------------- + +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 a lot of its behavior +from `Rails::Engine`. + +Therefore, engines and applications can be thought of almost the same thing, +just with subtle differences, as you'll see throughout this guide. Engines and +applications also share a common structure. + +Engines are also closely related to plugins. The two share a common `lib` +directory structure, and are both generated using the `rails plugin new` +generator. The difference is that an engine is considered a "full plugin" by +Rails (as indicated by the `--full` option that's passed to the generator +command). We'll actually be using the `--mountable` option here, which includes +all the features of `--full`, and then some. This guide will refer to these +"full plugins" 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". This +engine will provide blogging functionality to its host applications, allowing +for new articles 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 +`articles_path` and use an engine also that provides a path also called +`articles_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 its environment. 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. There's also [Spree](https://github.com/spree/spree) which +provides an e-commerce platform, and +[RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. + +Finally, engines would not have been 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! + +Generating an engine +-------------------- + +To generate an engine, you will need to run the plugin generator and pass it +options as appropriate to the need. For the "blorgh" example, you will need to +create a "mountable" engine, running this command in a terminal: + +```bash +$ bin/rails plugin new blorgh --mountable +``` + +The full list of options for the plugin generator may be seen by typing: + +```bash +$ bin/rails plugin --help +``` + +The `--mountable` option tells the generator that you want to create a +"mountable" and namespace-isolated engine. This generator will provide the same +skeleton structure as would the `--full` option. The `--full` option tells the +generator that you want to create an engine, including a skeleton structure +that provides the following: + + * An `app` directory tree + * A `config/routes.rb` file: + + ```ruby + Rails.application.routes.draw do + end + ``` + + * A file at `lib/blorgh/engine.rb`, which is identical in function to a + standard Rails application's `config/application.rb` file: + + ```ruby + module Blorgh + class Engine < ::Rails::Engine + end + end + ``` + +The `--mountable` option will add to the `--full` option: + + * Asset manifest files (`application.js` and `application.css`) + * A namespaced `ApplicationController` stub + * A namespaced `ApplicationHelper` stub + * A layout view template for the engine + * Namespace isolation to `config/routes.rb`: + + ```ruby + Blorgh::Engine.routes.draw do + end + ``` + + * Namespace isolation to `lib/blorgh/engine.rb`: + + ```ruby + module Blorgh + class Engine < ::Rails::Engine + isolate_namespace Blorgh + end + end + ``` + +Additionally, the `--mountable` option tells the generator to mount the engine +inside the dummy testing application located at `test/dummy` by adding the +following to the dummy application's routes file at +`test/dummy/config/routes.rb`: + +```ruby +mount Blorgh::Engine => "/blorgh" +``` + +### Inside an Engine + +#### Critical Files + +At the root of this brand new engine's directory lives a `blorgh.gemspec` file. +When you include the engine into an application later on, you will do so with +this line in the Rails application's `Gemfile`: + +```ruby +gem 'blorgh', path: "vendor/engines/blorgh" +``` + +Don't forget to run `bundle install` as usual. By specifying it as a gem within +the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file +and requiring a file within the `lib` directory called `lib/blorgh.rb`. This +file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) +and defines a base module called `Blorgh`. + +```ruby +require "blorgh/engine" + +module Blorgh +end +``` + +TIP: Some engines choose to use this file to put global configuration options +for their engine. It's a relatively good idea, so if you want to 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 +module Blorgh + class Engine < ::Rails::Engine + isolate_namespace Blorgh + end +end +``` + +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 the 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 overridden by similarly named things within the application. +One of the examples of such conflicts is helpers. Without calling +`isolate_namespace`, the engine's helpers would be included in an application's +controllers. + +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. + +What this isolation of the namespace means is that a model generated by a call +to `bin/rails g model`, such as `bin/rails g model article`, won't be called `Article`, but +instead be namespaced and called `Blorgh::Article`. In addition, the table for the +model is namespaced, becoming `blorgh_articles`, rather than simply `articles`. +Similar to the model namespacing, a controller called `ArticlesController` becomes +`Blorgh::ArticlesController` and the views for that controller will not be at +`app/views/articles`, but `app/views/blorgh/articles` instead. Mailers are namespaced +as well. + +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. + +#### `app` Directory + +Inside the `app` directory are 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, so they 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 are the `images`, `javascripts` and +`stylesheets` directories which, again, you should be familiar with due to their +similarity to 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 that +contains 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 inside an engine is named just like a +Rails application in order to make it easier for you to convert your +applications into engines. + +Lastly, the `app/views` directory contains a `layouts` folder, which contains a +file at `blorgh/application.html.erb`. This file 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 +application's `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. + +#### `bin` Directory + +This directory contains one file, `bin/rails`, which enables you to use the +`rails` sub-commands and generators just like you would within an application. +This means that you will be able to generate new controllers and models for this +engine very easily by running commands like this: + +```bash +$ bin/rails g model +``` + +Keep in mind, of course, that anything generated with these commands inside of +an engine that has `isolate_namespace` in the `Engine` class will be namespaced. + +#### `test` Directory + +The `test` directory is where tests for the engine will go. To test the engine, +there is a cut-down version of a Rails application embedded within it at +`test/dummy`. This application will mount the engine in the +`test/dummy/config/routes.rb` file: + +```ruby +Rails.application.routes.draw do + mount Blorgh::Engine => "/blorgh" +end +``` + +This line mounts the engine at the path `/blorgh`, which will make it accessible +through the application only at that path. + +Inside the test directory there is the `test/integration` directory, where +integration tests for the engine should be placed. Other directories can be +created in the `test` directory as well. For example, you may wish to create a +`test/models` directory for your model tests. + +Providing engine functionality +------------------------------ + +The engine that this guide covers provides submitting articles and commenting +functionality and follows a similar thread to the [Getting Started +Guide](getting_started.html), with some new twists. + +### Generating an Article Resource + +The first thing to generate for a blog engine is the `Article` model and related +controller. To quickly generate this, you can use the Rails scaffold generator. + +```bash +$ bin/rails generate scaffold article title:string text:text +``` + +This command will output this information: + +``` +invoke active_record +create db/migrate/[timestamp]_create_blorgh_articles.rb +create app/models/blorgh/article.rb +invoke test_unit +create test/models/blorgh/article_test.rb +create test/fixtures/blorgh/articles.yml +invoke resource_route + route resources :articles +invoke scaffold_controller +create app/controllers/blorgh/articles_controller.rb +invoke erb +create app/views/blorgh/articles +create app/views/blorgh/articles/index.html.erb +create app/views/blorgh/articles/edit.html.erb +create app/views/blorgh/articles/show.html.erb +create app/views/blorgh/articles/new.html.erb +create app/views/blorgh/articles/_form.html.erb +invoke test_unit +create test/controllers/blorgh/articles_controller_test.rb +invoke helper +create app/helpers/blorgh/articles_helper.rb +invoke assets +invoke js +create app/assets/javascripts/blorgh/articles.js +invoke css +create app/assets/stylesheets/blorgh/articles.css +invoke css +create app/assets/stylesheets/scaffold.css +``` + +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_articles` rather than the +usual `create_articles`. 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/article.rb` rather than `app/models/article.rb` due +to the `isolate_namespace` call within the `Engine` class. + +Next, the `test_unit` generator is invoked for this model, generating a model +test at `test/models/blorgh/article_test.rb` (rather than +`test/models/article_test.rb`) and a fixture at `test/fixtures/blorgh/articles.yml` +(rather than `test/fixtures/articles.yml`). + +After that, a line for the resource is inserted into the `config/routes.rb` file +for the engine. This line is simply `resources :articles`, turning the +`config/routes.rb` file for the engine into this: + +```ruby +Blorgh::Engine.routes.draw do + resources :articles +end +``` + +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. It also causes the engine's routes to +be isolated from those routes that are within the application. The +[Routes](#routes) section of this guide describes it in detail. + +Next, the `scaffold_controller` generator is invoked, generating a controller +called `Blorgh::ArticlesController` (at +`app/controllers/blorgh/articles_controller.rb`) and its related views at +`app/views/blorgh/articles`. This generator also generates a test for the +controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper +(`app/helpers/blorgh/articles_controller.rb`). + +Everything this generator has created is neatly namespaced. The controller's +class is defined within the `Blorgh` module: + +```ruby +module Blorgh + class ArticlesController < ApplicationController + ... + end +end +``` + +NOTE: The `ApplicationController` class being inherited from here is the +`Blorgh::ApplicationController`, not an application's `ApplicationController`. + +The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced: + +```ruby +module Blorgh + module ArticlesHelper + ... + end +end +``` + +This helps prevent conflicts with any other engine or application that may have +an article resource as well. + +Finally, the assets for this resource are generated in two files: +`app/assets/javascripts/blorgh/articles.js` and +`app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little +later. + +By default, the scaffold styling is not applied to the engine because the +engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't +load it. To make the scaffold styling apply, insert this line into the `<head>` +tag of this layout: + +```erb +<%= stylesheet_link_tag "scaffold" %> +``` + +You can see what the engine has so far by running `rake db:migrate` at the root +of our engine to run the migration generated by the scaffold generator, and then +running `rails server` in `test/dummy`. When you open +`http://localhost:3000/blorgh/articles` you will see the default scaffold that has +been generated. Click around! You've just generated your first engine's first +functions. + +If you'd rather play around in the console, `rails console` will also work just +like a Rails application. Remember: the `Article` model is namespaced, so to +reference it you must call it as `Blorgh::Article`. + +```ruby +>> Blorgh::Article.find(1) +=> #<Blorgh::Article id: 1 ...> +``` + +One final thing is that the `articles` resource for this engine should be the root +of the engine. Whenever someone goes to the root path where the engine is +mounted, they should be shown a list of articles. This can be made to happen if +this line is inserted into the `config/routes.rb` file inside the engine: + +```ruby +root to: "articles#index" +``` + +Now people will only need to go to the root of the engine to see all the articles, +rather than visiting `/articles`. This means that instead of +`http://localhost:3000/blorgh/articles`, you only need to go to +`http://localhost:3000/blorgh` now. + +### Generating a Comments Resource + +Now that the engine can create new articles, it only makes sense to add +commenting functionality as well. To do this, you'll need to generate a comment +model, a comment controller and then modify the articles scaffold to display +comments and allow people to create new ones. + +From the application root, run the model generator. Tell it to generate a +`Comment` model, with the related table having two columns: a `article_id` integer +and `text` text column. + +```bash +$ bin/rails generate model Comment article_id:integer text:text +``` + +This will output the following: + +``` +invoke active_record +create db/migrate/[timestamp]_create_blorgh_comments.rb +create app/models/blorgh/comment.rb +invoke test_unit +create test/models/blorgh/comment_test.rb +create test/fixtures/blorgh/comments.yml +``` + +This generator call will generate just the necessary model files it needs, +namespacing the files under a `blorgh` directory and creating a model class +called `Blorgh::Comment`. Now run the migration to create our blorgh_comments +table: + +```bash +$ rake db:migrate +``` + +To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and +add this line before the "Edit" link: + +```html+erb +<h3>Comments</h3> +<%= render @article.comments %> +``` + +This line will require there to be a `has_many` association for comments defined +on the `Blorgh::Article` model, which there isn't right now. To define one, open +`app/models/blorgh/article.rb` and add this line into the model: + +```ruby +has_many :comments +``` + +Turning the model into this: + +```ruby +module Blorgh + class Article < ActiveRecord::Base + has_many :comments + end +end +``` + +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 an article. To +add this, put this line underneath the call to `render @article.comments` in +`app/views/blorgh/articles/show.html.erb`: + +```erb +<%= render "blorgh/comments/form" %> +``` + +Next, the partial that this line will render needs to exist. Create a new +directory at `app/views/blorgh/comments` and in it a new file called +`_form.html.erb` which has this content to create the required partial: + +```html+erb +<h3>New comment</h3> +<%= form_for [@article, @article.comments.build] do |f| %> + <p> + <%= f.label :text %><br> + <%= f.text_area :text %> + </p> + <%= f.submit %> +<% end %> +``` + +When this form is submitted, it is going to attempt to perform a `POST` request +to a route of `/articles/:article_id/comments` within the engine. This route doesn't +exist at the moment, but can be created by changing the `resources :articles` line +inside `config/routes.rb` into these lines: + +```ruby +resources :articles do + resources :comments +end +``` + +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 from the application root: + +```bash +$ bin/rails g controller comments +``` + +This will generate the following things: + +``` +create app/controllers/blorgh/comments_controller.rb +invoke erb + exist app/views/blorgh/comments +invoke test_unit +create test/controllers/blorgh/comments_controller_test.rb +invoke helper +create app/helpers/blorgh/comments_helper.rb +invoke assets +invoke js +create app/assets/javascripts/blorgh/comments.js +invoke css +create app/assets/stylesheets/blorgh/comments.css +``` + +The form will be making a `POST` request to `/articles/:article_id/comments`, which +will correspond with the `create` action in `Blorgh::CommentsController`. This +action needs to be created, which can be done by putting the following lines +inside the class definition in `app/controllers/blorgh/comments_controller.rb`: + +```ruby +def create + @article = Article.find(params[:article_id]) + @comment = @article.comments.create(comment_params) + flash[:notice] = "Comment has been created!" + redirect_to articles_path +end + +private + def comment_params + params.require(:comment).permit(:text) + end +``` + +This is the final step required to get the new comment form working. Displaying +the comments, however, is not quite right yet. If you were to create a comment +right now, you would see this error: + +``` +Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], +:formats=>[:html], :locale=>[:en, :en]}. Searched in: * +"/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" * +"/Users/ryan/Sites/side_projects/blorgh/app/views" +``` + +The engine is unable to find the partial required for rendering the comments. +Rails looks first in the application's (`test/dummy`) `app/views` directory and +then in the engine's `app/views` directory. When it can't find it, it will throw +this error. The engine knows to look for `blorgh/comments/comment` because the +model object it is receiving is from the `Blorgh::Comment` class. + +This partial will be responsible for rendering just the comment text, for now. +Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this +line inside it: + +```erb +<%= comment_counter + 1 %>. <%= comment.text %> +``` + +The `comment_counter` local variable is given to us by the `<%= render +@article.comments %>` call, which will define it automatically and increment the +counter as it iterates through each comment. It's used in this example to +display a small number next to each comment when it's created. + +That completes the comment function of the blogging engine. Now it's time to use +it within an application. + +Hooking Into an Application +--------------------------- + +Using an engine within an application is very easy. This section covers how to +mount the engine into an application and the initial setup required, as well as +linking the engine to a `User` class provided by the application to provide +ownership for articles and comments within the engine. + +### Mounting the Engine + +First, the engine needs to be specified inside the application's `Gemfile`. If +there isn't an application handy to test this out in, generate one using the +`rails new` command outside of the engine directory like this: + +```bash +$ rails new unicorn +``` + +Usually, specifying the engine inside the Gemfile would be done by specifying it +as a normal, everyday gem. + +```ruby +gem 'devise' +``` + +However, because you are developing the `blorgh` engine on your local machine, +you will need to specify the `:path` option in your `Gemfile`: + +```ruby +gem 'blorgh', path: "/path/to/blorgh" +``` + +Then run `bundle` to install the gem. + +As described earlier, by placing the gem in the `Gemfile` it will be loaded when +Rails is loaded. It will first require `lib/blorgh.rb` from the engine, then +`lib/blorgh/engine.rb`, which is the file that defines the major pieces of +functionality for the engine. + +To make the engine's functionality accessible from within an application, it +needs to be mounted in that application's `config/routes.rb` file: + +```ruby +mount Blorgh::Engine, at: "/blog" +``` + +This line will mount the engine at `/blog` in the application. Making it +accessible at `http://localhost:3000/blog` when the application runs with `rails +server`. + +NOTE: Other engines, such as Devise, handle this a little differently by making +you specify custom helpers (such as `devise_for`) in the routes. These helpers +do exactly the same thing, mounting pieces of the engines's functionality at a +pre-defined path which may be customizable. + +### Engine setup + +The engine contains migrations for the `blorgh_articles` and `blorgh_comments` +table which need to be created in the application's database so that the +engine's models can query them correctly. To copy these migrations into the +application use this command: + +```bash +$ rake blorgh:install:migrations +``` + +If you have multiple engines that need migrations copied over, use +`railties:install:migrations` instead: + +```bash +$ rake railties:install:migrations +``` + +This command, when run for the first time, will copy over all the migrations +from the engine. When run the next time, it will only copy over migrations that +haven't been copied over already. The first run for this command will output +something such as this: + +```bash +Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh +Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh +``` + +The first timestamp (`[timestamp_1]`) will be the current time, and the second +timestamp (`[timestamp_2]`) will be the current time plus a second. The reason +for this is so that the migrations for the engine are run after any existing +migrations in the application. + +To run these migrations within the context of the application, simply run `rake +db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the +articles will be empty. This is because the table created inside the application is +different from the one created within the engine. Go ahead, play around with the +newly mounted engine. You'll find that it's the same as when it was only an +engine. + +If you would like to run migrations only from one engine, you can do it by +specifying `SCOPE`: + +```bash +rake db:migrate SCOPE=blorgh +``` + +This may be useful if you want to revert engine's migrations before removing it. +To revert all migrations from blorgh engine you can run code such as: + +```bash +rake db:migrate SCOPE=blorgh VERSION=0 +``` + +### Using a Class Provided by the Application + +#### Using a Model Provided by the Application + +When an engine is created, it may want to use specific classes from an +application to provide links between the pieces of the engine and the pieces of +the application. In the case of the `blorgh` engine, making articles and comments +have authors would make a lot of sense. + +A typical application might have a `User` class that would be used to represent +authors for an article or a comment. But there could be a case where the +application calls this class something different, such as `Person`. For this +reason, the engine should not hardcode associations specifically for a `User` +class. + +To keep it simple in this case, the application will have a class called `User` +that represents the users of the application (we'll get into making this +configurable further on). It can be generated using this command inside the +application: + +```bash +rails g model user name:string +``` + +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 articles 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 either create a new `User` object from it, or find one that +already has that name. The engine will then associate the article with the found or +created `User` object. + +First, the `author_name` text field needs to be added to the +`app/views/blorgh/articles/_form.html.erb` partial inside the engine. This can be +added above the `title` field with this code: + +```html+erb +<div class="field"> + <%= f.label :author_name %><br> + <%= f.text_field :author_name %> +</div> +``` + +Next, we need to update our `Blorgh::ArticleController#article_params` method to +permit the new form parameter: + +```ruby +def article_params + params.require(:article).permit(:title, :text, :author_name) +end +``` + +The `Blorgh::Article` model should then have some code to convert the `author_name` +field into an actual `User` object and associate it as that article's `author` +before the article is saved. It will also need to have an `attr_accessor` set up +for this field, so that the setter and getter methods are defined for it. + +To do all this, you'll need to add the `attr_accessor` for `author_name`, the +association for the author and the `before_save` call into +`app/models/blorgh/article.rb`. The `author` association will be hard-coded to the +`User` class for the time being. + +```ruby +attr_accessor :author_name +belongs_to :author, class_name: "User" + +before_save :set_author + +private + def set_author + self.author = User.find_or_create_by(name: author_name) + end +``` + +By representing the `author` association's object with the `User` class, a link +is established between the engine and the application. There needs to be a way +of associating the records in the `blorgh_articles` table with the records in the +`users` table. Because the association is called `author`, there should be an +`author_id` column added to the `blorgh_articles` table. + +To generate this new column, run this command within the engine: + +```bash +$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer +``` + +NOTE: Due to the migration's name and the column specification after it, Rails +will automatically know that you want to add a column to a specific table and +write that into the migration for you. You don't need to tell it any more than +this. + +This migration will need to be run on the application. To do that, it must first +be copied using this command: + +```bash +$ rake blorgh:install:migrations +``` + +Notice that only _one_ migration was copied over here. This is because the first +two migrations were copied over the first time this command was run. + +``` +NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been +skipped. Migration with the same name already exists. NOTE Migration +[timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration +with the same name already exists. Copied migration +[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh +``` + +Run the migration using: + +```bash +$ rake db:migrate +``` + +Now with all the pieces in place, an action will take place that will associate +an author - represented by a record in the `users` table - with an article, +represented by the `blorgh_articles` table from the engine. + +Finally, the author's name should be displayed on the article's page. Add this code +above the "Title" output inside `app/views/blorgh/articles/show.html.erb`: + +```html+erb +<p> + <b>Author:</b> + <%= @article.author %> +</p> +``` + +By outputting `@article.author` using the `<%=` tag, the `to_s` method will be +called on the object. By default, this will look quite ugly: + +``` +#<User:0x00000100ccb3b0> +``` + +This is undesirable. It would be much better to have the user's name there. To +do this, add a `to_s` method to the `User` class within the application: + +```ruby +def to_s + name +end +``` + +Now instead of the ugly Ruby object output, the author's name will be displayed. + +#### Using a Controller Provided by the Application + +Because Rails controllers generally share code for things like authentication +and accessing session variables, they inherit from `ApplicationController` by +default. Rails engines, however are scoped to run independently from the main +application, so each engine gets a scoped `ApplicationController`. This +namespace prevents code collisions, but often engine controllers need to access +methods in the main application's `ApplicationController`. An easy way to +provide this access is to change the engine's scoped `ApplicationController` to +inherit from the main application's `ApplicationController`. For our Blorgh +engine this would be done by changing +`app/controllers/blorgh/application_controller.rb` to look like: + +```ruby +class Blorgh::ApplicationController < ApplicationController +end +``` + +By default, the engine's controllers inherit from +`Blorgh::ApplicationController`. So, after making this change they will have +access to the main application's `ApplicationController`, as though they were +part of the main application. + +This change does require that the engine is run from a Rails application that +has an `ApplicationController`. + +### Configuring an Engine + +This section covers how to make the `User` class configurable, followed by +general configuration tips for the engine. + +#### Setting Configuration Settings in the Application + +The next step is to make the class that represents a `User` in the application +customizable for the engine. This is because that class may not always be +`User`, as previously explained. To make this setting customizable, the engine +will have a configuration setting called `author_class` that will be used to +specify which class represents users inside the application. + +To define this configuration setting, you should use a `mattr_accessor` inside +the `Blorgh` module for the engine. Add this line to `lib/blorgh.rb` inside the +engine: + +```ruby +mattr_accessor :author_class +``` + +This method works like its brothers, `attr_accessor` and `cattr_accessor`, but +provides a setter and getter method on the module with the specified name. To +use it, it must be referenced using `Blorgh.author_class`. + +The next step is to switch the `Blorgh::Article` model over to this new setting. +Change the `belongs_to` association inside this model +(`app/models/blorgh/article.rb`) to this: + +```ruby +belongs_to :author, class_name: Blorgh.author_class +``` + +The `set_author` method in the `Blorgh::Article` model should also use this class: + +```ruby +self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name) +``` + +To save having to call `constantize` on the `author_class` result all the time, +you could instead just override the `author_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.author_class + @@author_class.constantize +end +``` + +This would then turn the above code for `set_author` into this: + +```ruby +self.author = Blorgh.author_class.find_or_create_by(name: author_name) +``` + +Resulting in something a little shorter, and more implicit in its behavior. The +`author_class` method should always return a `Class` object. + +Since we changed the `author_class` method to return a `Class` instead of a +`String`, we must also modify our `belongs_to` definition in the `Blorgh::Article` +model: + +```ruby +belongs_to :author, class_name: Blorgh.author_class.to_s +``` + +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: + +```ruby +Blorgh.author_class = "User" +``` + +WARNING: It's very important here to use the `String` version of the class, +rather than the class itself. If you were to use the class, Rails would attempt +to load that class and then reference the related table. This could lead to +problems if the table wasn't already existing. Therefore, a `String` should be +used and then converted to a class using `constantize` in the engine later on. + +Go ahead and try to create a new article. 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 API for +the class must be. The engine simply requires this class to define a +`find_or_create_by` method which returns an object of that class, to be +associated with an article when it's created. This object, of course, should have +some sort of identifier by which it can be referenced. + +#### 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](configuring.html#initializers) of the Configuring guide, and works +precisely the same way as the `config/initializers` directory inside an +application. The same thing goes 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. + +Testing an engine +----------------- + +When an engine is generated, there is a smaller dummy application created inside +it at `test/dummy`. This application is used as a mounting point for the engine, +to make testing the engine extremely simple. You may extend this application by +generating controllers, models or views from within the directory, and then use +those to test your engine. + +The `test` directory should be treated like a typical Rails testing environment, +allowing for unit, functional and integration tests. + +### 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 +``` + +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 also pass the `:use_route` option as a parameter on these +requests: + +```ruby +get :index, use_route: :blorgh +``` + +This tells the application that you still want to perform a `GET` request to the +`index` action of this controller, but you want to use the engine's route to get +there, rather than the application's one. + +Another way to do this is to assign the `@routes` instance variable to `Engine.routes` in your test setup: + +```ruby +setup do + @routes = Engine.routes +end +``` + +This will also ensure url helpers for the engine will work as expected in your tests. + +Improving engine functionality +------------------------------ + +This section explains how to add and/or override engine MVC functionality in the +main Rails application. + +### Overriding Models and Controllers + +Engine model and controller classes can be extended by open classing them in the +main Rails application (since model and controller classes are just Ruby classes +that inherit Rails specific functionality). Open classing an Engine class +redefines it for use in the main application. This is usually implemented by +using the decorator pattern. + +For simple class modifications, use `Class#class_eval`. For complex class +modifications, consider using `ActiveSupport::Concern`. + +#### A note on Decorators and Loading Code + +Because these decorators are not referenced by your Rails application itself, +Rails' autoloading system will not kick in and load your decorators. This means +that you need to require them yourself. + +Here is some sample code to do this: + +```ruby +# lib/blorgh/engine.rb +module Blorgh + class Engine < ::Rails::Engine + isolate_namespace Blorgh + + config.to_prepare do + Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| + require_dependency(c) + end + end + end +end +``` + +This doesn't apply to just Decorators, but anything that you add in an engine +that isn't referenced by your main application. + +#### Implementing Decorator Pattern Using Class#class_eval + +**Adding** `Article#time_since_created`: + +```ruby +# MyApp/app/decorators/models/blorgh/article_decorator.rb + +Blorgh::Article.class_eval do + def time_since_created + Time.current - created_at + end +end +``` + +```ruby +# Blorgh/app/models/article.rb + +class Article < ActiveRecord::Base + has_many :comments +end +``` + + +**Overriding** `Article#summary`: + +```ruby +# MyApp/app/decorators/models/blorgh/article_decorator.rb + +Blorgh::Article.class_eval do + def summary + "#{title} - #{truncate(text)}" + end +end +``` + +```ruby +# Blorgh/app/models/article.rb + +class Article < ActiveRecord::Base + has_many :comments + def summary + "#{title}" + end +end +``` + +#### Implementing Decorator Pattern Using ActiveSupport::Concern + +Using `Class#class_eval` is great for simple adjustments, but for more complex +class modifications, you might want to consider using [`ActiveSupport::Concern`] +(http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). +ActiveSupport::Concern manages load order of interlinked dependent modules and +classes at run time allowing you to significantly modularize your code. + +**Adding** `Article#time_since_created` and **Overriding** `Article#summary`: + +```ruby +# MyApp/app/models/blorgh/article.rb + +class Blorgh::Article < ActiveRecord::Base + include Blorgh::Concerns::Models::Article + + def time_since_created + Time.current - created_at + end + + def summary + "#{title} - #{truncate(text)}" + end +end +``` + +```ruby +# Blorgh/app/models/article.rb + +class Article < ActiveRecord::Base + include Blorgh::Concerns::Models::Article +end +``` + +```ruby +# Blorgh/lib/concerns/models/article + +module Blorgh::Concerns::Models::Article + extend ActiveSupport::Concern + + # 'included do' causes the included code to be evaluated in the + # context where it is included (article.rb), rather than being + # executed in the module's context (blorgh/concerns/models/article). + included do + attr_accessor :author_name + belongs_to :author, class_name: "User" + + before_save :set_author + + private + def set_author + self.author = User.find_or_create_by(name: author_name) + end + end + + def summary + "#{title}" + end + + module ClassMethods + def some_class_method + 'some class method string' + end + end +end +``` + +### Overriding Views + +When Rails looks for a view to render, it will first look in the `app/views` +directory of the application. If it cannot find the view there, it will check in +the `app/views` directories of all engines that have this directory. + +When the application is asked to render the view for `Blorgh::ArticlesController`'s +index action, it will first look for the path +`app/views/blorgh/articles/index.html.erb` within the application. If it cannot +find it, it will look inside the engine. + +You can override this view in the application by simply creating a new file at +`app/views/blorgh/articles/index.html.erb`. Then you can completely change what +this view would normally output. + +Try this now by creating a new file at `app/views/blorgh/articles/index.html.erb` +and put this content in it: + +```html+erb +<h1>Articles</h1> +<%= link_to "New Article", new_article_path %> +<% @articles.each do |article| %> + <h2><%= article.title %></h2> + <small>By <%= article.author %></small> + <%= simple_format(article.text) %> + <hr> +<% end %> +``` + +### Routes + +Routes inside an engine are isolated from the application by default. This is +done by the `isolate_namespace` call inside the `Engine` class. This essentially +means that the application and its engines can have identically named routes and +they will not clash. + +Routes inside an engine are drawn on the `Engine` class within +`config/routes.rb`, like this: + +```ruby +Blorgh::Engine.routes.draw do + resources :articles +end +``` + +By having isolated routes such as this, if you wish to link to an area of an +engine from within an application, you will need to use the engine's routing +proxy method. Calls to normal routing methods such as `articles_path` may end up +going to undesired locations if both the application and the engine have such a +helper defined. + +For instance, the following example would go to the application's `articles_path` +if that template was rendered from the application, or the engine's `articles_path` +if it was rendered from the engine: + +```erb +<%= link_to "Blog articles", articles_path %> +``` + +To make this route always use the engine's `articles_path` routing helper method, +we must call the method on the routing proxy method that shares the same name as +the engine. + +```erb +<%= link_to "Blog articles", blorgh.articles_path %> +``` + +If you wish to reference the application inside the engine in a similar way, use +the `main_app` helper: + +```erb +<%= link_to "Home", main_app.root_path %> +``` + +If you were to use this inside an engine, it would **always** go to the +application's root. If you were to leave off the `main_app` "routing proxy" +method call, it could potentially go to the engine's or application's root, +depending on where it was called from. + +If a template rendered from within an engine attempts to use one of the +application's routing helper methods, it may result in an undefined method call. +If you encounter such an issue, ensure that you're not attempting to call the +application's routing methods without the `main_app` prefix from within the +engine. + +### 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 assets in the engine's 'app/assets' and 'lib/assets' directories. + +Like all of the other components of an engine, the assets should be namespaced. +This means that 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 isn't namespaced, 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 ignored. + +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" %> +``` + +You can also specify these assets as dependencies of other assets using Asset +Pipeline require statements in processed files: + +``` +/* + *= require blorgh/style +*/ +``` + +INFO. Remember that in order to use languages like Sass or CoffeeScript, you +should add the relevant library to your engine's `.gemspec`. + +### Separate Assets & Precompiling + +There are some situations where your engine's assets are not required by the +host application. For example, say that you've created an admin functionality +that only exists for your engine. In this case, the host application doesn't +need to require `admin.css` or `admin.js`. Only the gem's admin layout needs +these assets. It doesn't make sense for the host app to include +`"blorgh/admin.css"` in its stylesheets. In this situation, you should +explicitly define these assets for precompilation. This tells sprockets to add +your engine assets when `rake assets:precompile` is triggered. + +You can define assets for precompilation in `engine.rb`: + +```ruby +initializer "blorgh.assets.precompile" do |app| + app.config.assets.precompile += %w(admin.css admin.js) +end +``` + +For more information, read the [Asset Pipeline guide](asset_pipeline.html). + +### Other Gem Dependencies + +Gem dependencies inside an engine should be specified inside the `.gemspec` file +at the root of the engine. The reason is that the engine may be installed as a +gem. If dependencies were to be specified inside the `Gemfile`, these would not +be recognized by a traditional gem install and so they would not be installed, +causing the engine to malfunction. + +To specify a dependency that should be installed with the engine during a +traditional `gem install`, specify it inside the `Gem::Specification` block +inside the `.gemspec` file in the engine: + +```ruby +s.add_dependency "moo" +``` + +To specify a dependency that should only be installed as a development +dependency of the application, specify it like this: + +```ruby +s.add_development_dependency "moo" +``` + +Both kinds of dependencies will be installed when `bundle install` is run inside +of the application. The development dependencies for the gem will only be used +when the tests for the engine are running. + +Note that if you want to immediately require dependencies when the engine is +required, you should require them before the engine's initialization. For +example: + +```ruby +require 'other_engine/engine' +require 'yet_another_engine/engine' + +module MyEngine + class Engine < ::Rails::Engine + end +end +``` |