Movatterモバイル変換


[0]ホーム

URL:


Skip to main content
More atrubyonrails.org:

Getting Started with Engines

In this guide you will learn about engines and how they can be used to provideadditional functionality to their host applications through a clean and veryeasy-to-use interface.

After reading this guide, you will know:

  • What makes an engine.
  • How to generate an engine.
  • How to build features for the engine.
  • How to hook the engine into an application.
  • How to override engine functionality in the application.
  • How to avoid loading Rails frameworks with Load and Configuration Hooks.

1. What are Engines?

Engines can be considered miniature applications that provide functionality totheir host applications. A Rails application is actually just a "supercharged"engine, with theRails::Application class inheriting a lot of its behaviorfromRails::Engine.

Therefore, engines and applications can be thought of as almost the same thing,just with subtle differences, as you'll see throughout this guide. Engines andapplications also share a common structure.

Engines are also closely related to plugins. The two share a commonlibdirectory structure, and are both generated using therails plugin newgenerator. The difference is that an engine is considered a "full plugin" byRails (as indicated by the--full option that's passed to the generatorcommand). We'll actually be using the--mountable option here, which includesall the features of--full, and then some. This guide will refer to these"full plugins" simply as "engines" throughout. An enginecan be a plugin,and a plugincan be an engine.

The engine that will be created in this guide will be called "blorgh". Thisengine will provide blogging functionality to its host applications, allowingfor new articles and comments to be created. At the beginning of this guide, youwill be working solely within the engine itself, but in later sections you'llsee how to hook it into an application.

Engines can also be isolated from their host applications. This means that anapplication is able to have a path provided by a routing helper such asarticles_path and use an engine that also provides a path also calledarticles_path, and the two would not clash. Along with this, controllers, modelsand table names are also namespaced. You'll see how to do this later in thisguide.

It's important to keep in mind at all times that the application shouldalways take precedence over its engines. An application is the object thathas final say in what goes on in its environment. The engine shouldonly be enhancing it, rather than changing it drastically.

To see demonstrations of other engines, check outDevise, an engine that providesauthentication for its parent applications, orThredded, an engine that provides forumfunctionality. There's alsoSpree whichprovides an e-commerce platform, andRefinery CMS, 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 evermeet them, don't forget to say thanks!

2. Generating an Engine

To generate an engine, you will need to run the plugin generator and pass itoptions as appropriate to the need. For the "blorgh" example, you will need tocreate a "mountable" engine, running this command in a terminal:

$railsplugin new blorgh--mountable

The full list of options for the plugin generator may be seen by typing:

$railsplugin--help

The--mountable option tells the generator that you want to create a"mountable" and namespace-isolated engine. This generator will provide the sameskeleton structure as would the--full option. The--full option tells thegenerator that you want to create an engine, including a skeleton structurethat provides the following:

  • Anapp directory tree
  • Aconfig/routes.rb file:

    Rails.application.routes.drawdoend
  • A file atlib/blorgh/engine.rb, which is identical in function to astandard Rails application'sconfig/application.rb file:

    moduleBlorghclassEngine<::Rails::Engineendend

The--mountable option will add to the--full option:

  • Asset manifest files (blorgh_manifest.js andapplication.css)
  • A namespacedApplicationController stub
  • A namespacedApplicationHelper stub
  • A layout view template for the engine
  • Namespace isolation toconfig/routes.rb:

    Blorgh::Engine.routes.drawdoend
  • Namespace isolation tolib/blorgh/engine.rb:

    moduleBlorghclassEngine<::Rails::Engineisolate_namespaceBlorghendend

Additionally, the--mountable option tells the generator to mount the engineinside the dummy testing application located attest/dummy by adding thefollowing to the dummy application's routes file attest/dummy/config/routes.rb:

mountBlorgh::Engine=>"/blorgh"

2.1. Inside an Engine

2.1.1. Critical Files

At the root of this brand new engine's directory lives ablorgh.gemspec file.When you include the engine into an application later on, you will do so withthis line in the Rails application'sGemfile:

gem"blorgh",path:"engines/blorgh"

Don't forget to runbundle install as usual. By specifying it as a gem withintheGemfile, Bundler will load it as such, parsing thisblorgh.gemspec fileand requiring a file within thelib directory calledlib/blorgh.rb. Thisfile requires theblorgh/engine.rb file (located atlib/blorgh/engine.rb)and defines a base module calledBlorgh.

require"blorgh/engine"moduleBlorghend

Some engines choose to use this file to put global configuration optionsfor their engine. It's a relatively good idea, so if you want to offerconfiguration options, the file where your engine'smodule is defined isperfect for that. Place the methods inside the module and you'll be good to go.

Withinlib/blorgh/engine.rb is the base class for the engine:

moduleBlorghclassEngine<::Rails::Engineisolate_namespaceBlorghendend

By inheriting from theRails::Engine class, this gem notifies Rails thatthere's an engine at the specified path, and will correctly mount the engineinside the application, performing tasks such as adding theapp directory ofthe engine to the load path for models, mailers, controllers, and views.

Theisolate_namespace method here deserves special notice. This call isresponsible for isolating the controllers, models, routes, and other things intotheir 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 enginecomponents could be overridden by similarly named things within the application.One of the examples of such conflicts is helpers. Without callingisolate_namespace, the engine's helpers would be included in an application'scontrollers.

It ishighly recommended that theisolate_namespace line be leftwithin theEngine class definition. Without it, classes generated in an enginemay conflict with an application.

What this isolation of the namespace means is that a model generated by a calltobin/rails generate model, such asbin/rails generate model article, won't be calledArticle, butinstead be namespaced and calledBlorgh::Article. In addition, the table for themodel is namespaced, becomingblorgh_articles, rather than simplyarticles.Similar to the model namespacing, a controller calledArticlesController becomesBlorgh::ArticlesController and the views for that controller will not be atapp/views/articles, butapp/views/blorgh/articles instead. Mailers, jobsand helpers are namespaced as well.

Finally, routes will also be isolated within the engine. This is one of the mostimportant parts about namespacing, and is discussed later in theRoutes section of this guide.

2.1.2.app Directory

Inside theapp directory are the standardassets,controllers,helpers,jobs,mailers,models, andviews directories that you should be familiar withfrom an application. We'll look more into models in a future section, when we're writing the engine.

Within theapp/assets directory, there are theimages andstylesheets directories which, again, you should be familiar with due to theirsimilarity to an application. One difference here, however, is that eachdirectory contains a sub-directory with the engine name. Because this engine isgoing to be namespaced, its assets should be too.

Within theapp/controllers directory there is ablorgh directory thatcontains a file calledapplication_controller.rb. This file will provide anycommon functionality for the controllers of the engine. Theblorgh directoryis where the other controllers for the engine will go. By placing them withinthis namespaced directory, you prevent them from possibly clashing withidentically-named controllers within other engines or even within theapplication.

TheApplicationController class inside an engine is named just like aRails application in order to make it easier for you to convert yourapplications into engines.

Just like forapp/controllers, you will find ablorgh subdirectory undertheapp/helpers,app/jobs,app/mailers andapp/models directoriescontaining the associatedapplication_*.rb file for gathering commonfunctionalities. By placing your files under this subdirectory and namespacingyour objects, you prevent them from possibly clashing with identically-namedelements within other engines or even within the application.

Lastly, theapp/views directory contains alayouts folder, which contains afile atblorgh/application.html.erb. This file allows you to specify a layoutfor the engine. If this engine is to be used as a stand-alone engine, then youwould add any customization to its layout in this file, rather than theapplication'sapp/views/layouts/application.html.erb file.

If you don't want to force a layout on to users of the engine, then you candelete this file and reference a different layout in the controllers of yourengine.

2.1.3.bin Directory

This directory contains one file,bin/rails, which enables you to use therails 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 thisengine very easily by running commands like this:

$bin/railsgenerate model

Keep in mind, of course, that anything generated with these commands inside ofan engine that hasisolate_namespace in theEngine class will be namespaced.

2.1.4.test Directory

Thetest 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 attest/dummy. This application will mount the engine in thetest/dummy/config/routes.rb file:

Rails.application.routes.drawdomountBlorgh::Engine=>"/blorgh"end

This line mounts the engine at the path/blorgh, which will make it accessiblethrough the application only at that path.

Inside the test directory there is thetest/integration directory, whereintegration tests for the engine should be placed. Other directories can becreated in thetest directory as well. For example, you may wish to create atest/models directory for your model tests.

3. Providing Engine Functionality

The engine that this guide covers provides submitting articles and commentingfunctionality and follows a similar thread to theGetting StartedGuide, with some new twists.

For this section, make sure to run the commands in the root of theblorgh engine's directory.

3.1. Generating an Article Resource

The first thing to generate for a blog engine is theArticle model and relatedcontroller. To quickly generate this, you can use the Rails scaffold generator.

$bin/railsgenerate scaffold article title:string text:text

This command will output this information:

invoke  active_recordcreate    db/migrate/[timestamp]_create_blorgh_articles.rbcreate    app/models/blorgh/article.rbinvoke    test_unitcreate      test/models/blorgh/article_test.rbcreate      test/fixtures/blorgh/articles.ymlinvoke  resource_route route    resources :articlesinvoke  scaffold_controllercreate    app/controllers/blorgh/articles_controller.rbinvoke    erbcreate      app/views/blorgh/articlescreate      app/views/blorgh/articles/index.html.erbcreate      app/views/blorgh/articles/edit.html.erbcreate      app/views/blorgh/articles/show.html.erbcreate      app/views/blorgh/articles/new.html.erbcreate      app/views/blorgh/articles/_form.html.erbcreate      app/views/blorgh/articles/_article.html.erbinvoke    resource_routeinvoke    test_unitcreate      test/controllers/blorgh/articles_controller_test.rbcreate      test/system/blorgh/articles_test.rbinvoke    helpercreate      app/helpers/blorgh/articles_helper.rbinvoke      test_unit

The first thing that the scaffold generator does is invoke theactive_recordgenerator, which generates a migration and a model for the resource. Note here,however, that the migration is calledcreate_blorgh_articles rather than theusualcreate_articles. This is due to theisolate_namespace method called intheBlorgh::Engine class's definition. The model here is also namespaced,being placed atapp/models/blorgh/article.rb rather thanapp/models/article.rb dueto theisolate_namespace call within theEngine class.

Next, thetest_unit generator is invoked for this model, generating a modeltest attest/models/blorgh/article_test.rb (rather thantest/models/article_test.rb) and a fixture attest/fixtures/blorgh/articles.yml(rather thantest/fixtures/articles.yml).

After that, a line for the resource is inserted into theconfig/routes.rb filefor the engine. This line is simplyresources :articles, turning theconfig/routes.rb file for the engine into this:

Blorgh::Engine.routes.drawdoresources:articlesend

Note here that the routes are drawn upon theBlorgh::Engine object rather thantheYourApp::Application class. This is so that the engine routes are confinedto the engine itself and can be mounted at a specific point as shown in thetest directory section. It also causes the engine's routes tobe isolated from those routes that are within the application. TheRoutes section of this guide describes it in detail.

Next, thescaffold_controller generator is invoked, generating a controllercalledBlorgh::ArticlesController (atapp/controllers/blorgh/articles_controller.rb) and its related views atapp/views/blorgh/articles. This generator also generates tests for thecontroller (test/controllers/blorgh/articles_controller_test.rb andtest/system/blorgh/articles_test.rb) and a helper (app/helpers/blorgh/articles_helper.rb).

Everything this generator has created is neatly namespaced. The controller'sclass is defined within theBlorgh module:

moduleBlorghclassArticlesController<ApplicationController# ...endend

TheArticlesController class inherits fromBlorgh::ApplicationController, not the application'sApplicationController.

The helper insideapp/helpers/blorgh/articles_helper.rb is also namespaced:

moduleBlorghmoduleArticlesHelper# ...endend

This helps prevent conflicts with any other engine or application that may havean article resource as well.

You can see what the engine has so far by runningbin/rails db:migrate at the rootof our engine to run the migration generated by the scaffold generator, and thenrunningbin/rails server intest/dummy. When you openhttp://localhost:3000/blorgh/articles you will see the default scaffold that hasbeen generated. Click around! You've just generated your first engine's firstfunctions.

If you'd rather play around in the console,bin/rails console will also work justlike a Rails application. Remember: theArticle model is namespaced, so toreference it you must call it asBlorgh::Article.

irb>Blorgh::Article.find(1)=>#<Blorgh::Articleid:1...>

One final thing is that thearticles resource for this engine should be the rootof the engine. Whenever someone goes to the root path where the engine ismounted, they should be shown a list of articles. This can be made to happen ifthis line is inserted into theconfig/routes.rb file inside the engine:

rootto:"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 ofhttp://localhost:3000/blorgh/articles, you only need to go tohttp://localhost:3000/blorgh now.

3.2. Generating a Comments Resource

Now that the engine can create new articles, it only makes sense to addcommenting functionality as well. To do this, you'll need to generate a commentmodel, a comment controller, and then modify the articles scaffold to displaycomments and allow people to create new ones.

From the engine root, run the model generator. Tell it to generate aComment model, with the related table having two columns: anarticle referencescolumn andtext text column.

$bin/railsgenerate model Comment article:references text:text

This will output the following:

invoke  active_recordcreate    db/migrate/[timestamp]_create_blorgh_comments.rbcreate    app/models/blorgh/comment.rbinvoke    test_unitcreate      test/models/blorgh/comment_test.rbcreate      test/fixtures/blorgh/comments.yml

This generator call will generate just the necessary model files it needs,namespacing the files under ablorgh directory and creating a model classcalledBlorgh::Comment. Now run the migration to create our blorgh_commentstable:

$bin/railsdb:migrate

To show the comments on an article, editapp/views/blorgh/articles/show.html.erb andadd this line before the "Edit" link:

<h3>Comments</h3><%=render@article.comments%>

This line will require there to be ahas_many association for comments definedon theBlorgh::Article model, which there isn't right now. To define one, openapp/models/blorgh/article.rb and add this line into the model:

has_many:comments

Turning the model into this:

moduleBlorghclassArticle<ApplicationRecordhas_many:commentsendend

Because thehas_many is defined inside a class that is inside theBlorgh module, Rails will know that you want to use theBlorgh::Commentmodel 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. Toadd this, put this line underneath the call torender @article.comments inapp/views/blorgh/articles/show.html.erb:

<%=render"blorgh/comments/form"%>

Next, the partial that this line will render needs to exist. Create a newdirectory atapp/views/blorgh/comments and in it a new file called_form.html.erb which has this content to create the required partial:

<h3>New comment</h3><%=form_withmodel:[@article,@article.comments.build]do|form|%><p><%=form.label:text%><br><%=form.textarea:text%></p><%=form.submit%><%end%>

When this form is submitted, it is going to attempt to perform aPOST requestto a route of/articles/:article_id/comments within the engine. This route doesn'texist at the moment, but can be created by changing theresources :articles lineinsideconfig/routes.rb into these lines:

resources:articlesdoresources:commentsend

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. Tocreate it, run this command from the engine root:

$bin/railsgenerate controller comments

This will generate the following things:

create  app/controllers/blorgh/comments_controller.rbinvoke  erb exist    app/views/blorgh/commentsinvoke  test_unitcreate    test/controllers/blorgh/comments_controller_test.rbinvoke  helpercreate    app/helpers/blorgh/comments_helper.rbinvoke    test_unit

The form will be making aPOST request to/articles/:article_id/comments, whichwill correspond with thecreate action inBlorgh::CommentsController. Thisaction needs to be created, which can be done by putting the following linesinside the class definition inapp/controllers/blorgh/comments_controller.rb:

defcreate@article=Article.find(params[:article_id])@comment=@article.comments.create(comment_params)flash[:notice]="Comment has been created!"redirect_toarticles_pathendprivatedefcomment_paramsparams.expect(comment:[:text])end

This is the final step required to get the new comment form working. Displayingthe comments, however, is not quite right yet. If you were to create a commentright 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 andthen in the engine'sapp/views directory. When it can't find it, it will throwthis error. The engine knows to look forblorgh/comments/_comment because themodel object it is receiving is from theBlorgh::Comment class.

This partial will be responsible for rendering just the comment text, for now.Create a new file atapp/views/blorgh/comments/_comment.html.erb and put thisline inside it:

<%=comment_counter+1%>.<%=comment.text%>

Thecomment_counter local variable is given to us by the<%= render@article.comments %> call, which will define it automatically and increment thecounter as it iterates through each comment. It's used in this example todisplay 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 useit within an application.

4. Hooking Into an Application

Using an engine within an application is very easy. This section covers how tomount the engine into an application and the initial setup required, as well aslinking the engine to aUser class provided by the application to provideownership for articles and comments within the engine.

4.1. Mounting the Engine

First, the engine needs to be specified inside the application'sGemfile. Ifthere isn't an application handy to test this out in, generate one using therails new command outside of the engine directory like this:

$railsnew unicorn

Usually, specifying the engine inside theGemfile would be done by specifying itas a normal, everyday gem.

gem"devise"

However, because you are developing theblorgh engine on your local machine,you will need to specify the:path option in yourGemfile:

gem"blorgh",path:"engines/blorgh"

Then runbundle to install the gem.

As described earlier, by placing the gem in theGemfile it will be loaded whenRails is loaded. It will first requirelib/blorgh.rb from the engine, thenlib/blorgh/engine.rb, which is the file that defines the major pieces offunctionality for the engine.

To make the engine's functionality accessible from within an application, itneeds to be mounted in that application'sconfig/routes.rb file:

mountBlorgh::Engine,at:"/blog"

This line will mount the engine at/blog in the application. Making itaccessible athttp://localhost:3000/blog when the application runs withbin/railsserver.

Other engines, such as Devise, handle this a little differently by makingyou specify custom helpers (such asdevise_for) in the routes. These helpersdo exactly the same thing, mounting pieces of the engines's functionality at apre-defined path which may be customizable.

4.2. Engine Setup

The engine contains migrations for theblorgh_articles andblorgh_commentstable which need to be created in the application's database so that theengine's models can query them correctly. To copy these migrations into theapplication run the following command from the application's root:

$bin/railsblorgh:install:migrations

If you have multiple engines that need migrations copied over, userailties:install:migrations instead:

$bin/railsrailties:install:migrations

You can specify a custom path in the source engine for the migrations by specifying MIGRATIONS_PATH.

$bin/railsrailties:install:migrationsMIGRATIONS_PATH=db_blourgh

If you have multiple databases you can also specify the target database by specifying DATABASE.

$bin/railsrailties:install:migrationsDATABASE=animals

This command, when run for the first time, will copy over all the migrationsfrom the engine. When run the next time, it will only copy over migrations thathaven't been copied over already. The first run for this command will outputsomething such as this:

Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorghCopied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh

The first timestamp ([timestamp_1]) will be the current time, and the secondtimestamp ([timestamp_2]) will be the current time plus a second. The reasonfor this is so that the migrations for the engine are run after any existingmigrations in the application.

To run these migrations within the context of the application, simply runbin/railsdb:migrate. When accessing the engine throughhttp://localhost:3000/blog, thearticles will be empty. This is because the table created inside the application isdifferent from the one created within the engine. Go ahead, play around with thenewly mounted engine. You'll find that it's the same as when it was only anengine.

If you would like to run migrations only from one engine, you can do it byspecifyingSCOPE:

$bin/railsdb:migrateSCOPE=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:

$bin/railsdb:migrateSCOPE=blorghVERSION=0

4.3. Using a Class Provided by the Application

4.3.1. Using a Model Provided by the Application

When an engine is created, it may want to use specific classes from anapplication to provide links between the pieces of the engine and the pieces ofthe application. In the case of theblorgh engine, making articles and commentshave authors would make a lot of sense.

A typical application might have aUser class that would be used to representauthors for an article or a comment. But there could be a case where theapplication calls this class something different, such asPerson. For thisreason, the engine should not hardcode associations specifically for aUserclass.

To keep it simple in this case, the application will have a class calledUserthat represents the users of the application (we'll get into making thisconfigurable further on). It can be generated using this command inside theapplication:

$bin/railsgenerate model user name:string

Thebin/rails db:migrate command needs to be run here to ensure that ourapplication has theusers table for future use.

Also, to keep it simple, the articles form will have a new text field calledauthor_name, where users can elect to put their name. The engine will thentake this name and either create a newUser object from it, or find one thatalready has that name. The engine will then associate the article with the found orcreatedUser object.

First, theauthor_name text field needs to be added to theapp/views/blorgh/articles/_form.html.erb partial inside the engine. This can beadded above thetitle field with this code:

<divclass="field"><%=form.label:author_name%><br><%=form.text_field:author_name%></div>

Next, we need to update ourBlorgh::ArticlesController#article_params method topermit the new form parameter:

defarticle_paramsparams.expect(article:[:title,:text,:author_name])end

TheBlorgh::Article model should then have some code to convert theauthor_namefield into an actualUser object and associate it as that article'sauthorbefore the article is saved. It will also need to have anattr_accessor set upfor this field, so that the setter and getter methods are defined for it.

To do all this, you'll need to add theattr_accessor forauthor_name, theassociation for the author and thebefore_validation call intoapp/models/blorgh/article.rb. Theauthor association will be hard-coded to theUser class for the time being.

attr_accessor:author_namebelongs_to:author,class_name:"User"before_validation:set_authorprivatedefset_authorself.author=User.find_or_create_by(name:author_name)end

By representing theauthor association's object with theUser class, a linkis established between the engine and the application. There needs to be a wayof associating the records in theblorgh_articles table with the records in theusers table. Because the association is calledauthor, there should be anauthor_id column added to theblorgh_articles table.

To generate this new column, run this command within the engine:

$bin/railsgenerate migration add_author_id_to_blorgh_articles author_id:integer

Due to the migration's name and the column specification after it, Railswill automatically know that you want to add a column to a specific table andwrite that into the migration for you. You don't need to tell it any more thanthis.

This migration will need to be run on the application. To do that, it must firstbe copied using this command:

$bin/railsblorgh:install:migrations

Notice that onlyone migration was copied over here. This is because the firsttwo migrations were copied over the first time this command was run.

NOTE Migration [timestamp]_create_blorgh_articles.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.NOTE Migration [timestamp]_create_blorgh_comments.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blorgh

Run the migration using:

$bin/railsdb:migrate

Now with all the pieces in place, an action will take place that will associatean author - represented by a record in theusers table - with an article,represented by theblorgh_articles table from the engine.

Finally, the author's name should be displayed on the article's page. Add this codeabove the "Title" output insideapp/views/blorgh/articles/_article.html.erb:

<p><strong>Author:</strong><%=article.author.name%></p>

4.3.2. Using a Controller Provided by the Application

Because Rails controllers generally share code for things like authenticationand accessing session variables, they inherit fromApplicationController bydefault. Rails engines, however are scoped to run independently from the mainapplication, so each engine gets a scopedApplicationController. Thisnamespace prevents code collisions, but often engine controllers need to accessmethods in the main application'sApplicationController. An easy way toprovide this access is to change the engine's scopedApplicationController toinherit from the main application'sApplicationController. For our Blorghengine this would be done by changingapp/controllers/blorgh/application_controller.rb to look like:

moduleBlorghclassApplicationController<::ApplicationControllerendend

By default, the engine's controllers inherit fromBlorgh::ApplicationController. So, after making this change they will haveaccess to the main application'sApplicationController, as though they werepart of the main application.

This change does require that the engine is run from a Rails application thathas anApplicationController.

4.4. Configuring an Engine

This section covers how to make theUser class configurable, followed bygeneral configuration tips for the engine.

4.4.1. Setting Configuration Settings in the Application

The next step is to make the class that represents aUser in the applicationcustomizable for the engine. This is because that class may not always beUser, as previously explained. To make this setting customizable, the enginewill have a configuration setting calledauthor_class that will be used tospecify which class represents users inside the application.

To define this configuration setting, you should use amattr_accessor insidetheBlorgh module for the engine. Add this line tolib/blorgh.rb inside theengine:

mattr_accessor:author_class

This method works like its siblings,attr_accessor andcattr_accessor, butprovides a setter and getter method on the module with the specified name. Touse it, it must be referenced usingBlorgh.author_class.

The next step is to switch theBlorgh::Article model over to this new setting.Change thebelongs_to association inside this model(app/models/blorgh/article.rb) to this:

belongs_to:author,class_name:Blorgh.author_class

Theset_author method in theBlorgh::Article model should also use this class:

self.author=Blorgh.author_class.constantize.find_or_create_by(name:author_name)

To save having to callconstantize on theauthor_class result all the time,you could instead just override theauthor_class getter method inside theBlorgh module in thelib/blorgh.rb file to always callconstantize on thesaved value before returning the result:

defself.author_class@@author_class.constantizeend

This would then turn the above code forset_author into this:

self.author=Blorgh.author_class.find_or_create_by(name:author_name)

Resulting in something a little shorter, and more implicit in its behavior. Theauthor_class method should always return aClass object.

Since we changed theauthor_class method to return aClass instead of aString, we must also modify ourbelongs_to definition in theBlorgh::Articlemodel:

belongs_to:author,class_name:Blorgh.author_class.to_s

To set this configuration setting within the application, an initializer shouldbe used. By using an initializer, the configuration will be set up before theapplication starts and calls the engine's models, which may depend on thisconfiguration setting existing.

Create a new initializer atconfig/initializers/blorgh.rb inside theapplication where theblorgh engine is installed and put this content in it:

Blorgh.author_class="User"

It's very important here to use theString version of the class,rather than the class itself. If you were to use the class, Rails would attemptto load that class and then reference the related table. This could lead toproblems if the table didn't already exist. Therefore, aString should beused and then converted to a class usingconstantize in the engine later on.

Go ahead and try to create a new article. You will see that it works exactly in thesame way as before, except this time the engine is using the configurationsetting inconfig/initializers/blorgh.rb to learn what the class is.

There are now no strict dependencies on what the class is, only what the API forthe class must be. The engine simply requires this class to define afind_or_create_by method which returns an object of that class, to beassociated with an article when it's created. This object, of course, should havesome sort of identifier by which it can be referenced.

4.4.2. General Engine Configuration

Within an engine, there may come a time where you wish to use things such asinitializers, internationalization, or other configuration options. The greatnews is that these things are entirely possible, because a Rails engine sharesmuch the same functionality as a Rails application. In fact, a Railsapplication's functionality is actually a superset of what is provided byengines!

If you wish to use an initializer - code that should run before the engine isloaded - the place for it is theconfig/initializers folder. This directory'sfunctionality is explained in theInitializerssection of the Configuring guide, and worksprecisely the same way as theconfig/initializers directory inside anapplication. The same thing goes if you want to use a standard initializer.

For locales, simply place the locale files in theconfig/locales directory,just like you would in an application.

5. Testing an Engine

When an engine is generated, there is a smaller dummy application created insideit attest/dummy. This application is used as a mounting point for the engine,to make testing the engine extremely simple. You may extend this application bygenerating controllers, models, or views from within the directory, and then usethose to test your engine.

Thetest directory should be treated like a typical Rails testing environment,allowing for unit, functional, and integration tests.

5.1. Functional Tests

A matter worth taking into consideration when writing functional tests is thatthe tests are going to be running on an application - thetest/dummyapplication - rather than your engine. This is due to the setup of the testingenvironment; an engine needs an application as a host for testing its mainfunctionality, especially controllers. This means that if you were to make atypicalGET to a controller in a controller's functional test like this:

moduleBlorghclassFooControllerTest<ActionDispatch::IntegrationTestincludeEngine.routes.url_helpersdeftest_indexgetfoos_url# ...endendend

It may not function correctly. This is because the application doesn't know howto route these requests to the engine unless you explicitly tell ithow. Todo this, you must set the@routes instance variable to the engine's route setin your setup code:

moduleBlorghclassFooControllerTest<ActionDispatch::IntegrationTestincludeEngine.routes.url_helperssetupdo@routes=Engine.routesenddeftest_indexgetfoos_url# ...endendend

This tells the application that you still want to perform aGET request to theindex action of this controller, but you want to use the engine's route to getthere, rather than the application's one.

This also ensures that the engine's URL helpers will work as expected in yourtests.

6. Improving Engine Functionality

This section explains how to add and/or override engine MVC functionality in themain Rails application.

6.1. Overriding Models and Controllers

Engine models and controllers can be reopened by the parent application to extend or decorate them.

Overrides may be organized in a dedicated directoryapp/overrides, ignored by the autoloader, and preloaded in ato_prepare callback:

# config/application.rbmoduleMyAppclassApplication<Rails::Application# ...overrides="#{Rails.root}/app/overrides"Rails.autoloaders.main.ignore(overrides)config.to_preparedoDir.glob("#{overrides}/**/*_override.rb").sort.eachdo|override|loadoverrideendendendend

6.1.1. Reopening Existing Classes Usingclass_eval

For example, in order to override the engine model

# Blorgh/app/models/blorgh/article.rbmoduleBlorghclassArticle<ApplicationRecord# ...endend

you just create a file thatreopens that class:

# MyApp/app/overrides/models/blorgh/article_override.rbBlorgh::Article.class_evaldo# ...end

It is very important that the overridereopens the class or module. Using theclass ormodule keywords would define them if they were not already in memory, which would be incorrect because the definition lives in the engine. Usingclass_eval as shown above ensures you are reopening.

6.1.2. Reopening Existing Classes Using ActiveSupport::Concern

UsingClass#class_eval is great for simple adjustments, but for more complexclass modifications, you might want to consider usingActiveSupport::Concern.ActiveSupport::Concern manages load order of interlinked dependent modules andclasses at run time allowing you to significantly modularize your code.

AddingArticle#time_since_created andOverridingArticle#summary:

# MyApp/app/models/blorgh/article.rbclassBlorgh::Article<ApplicationRecordincludeBlorgh::Concerns::Models::Articledeftime_since_createdTime.current-created_atenddefsummary"#{title} -#{truncate(text)}"endend
# Blorgh/app/models/blorgh/article.rbmoduleBlorghclassArticle<ApplicationRecordincludeBlorgh::Concerns::Models::Articleendend
# Blorgh/lib/concerns/models/article.rbmoduleBlorgh::Concerns::Models::ArticleextendActiveSupport::Concern# `included do` causes the block to be evaluated in the context# in which the module is included (i.e. Blorgh::Article),# rather than in the module itself.includeddoattr_accessor:author_namebelongs_to:author,class_name:"User"before_validation:set_authorprivatedefset_authorself.author=User.find_or_create_by(name:author_name)endenddefsummary"#{title}"endmoduleClassMethodsdefsome_class_method"some class method string"endendend

6.2. Autoloading and Engines

Please check theAutoloading and Reloading Constantsguide for more information about autoloading and engines.

6.3. Overriding Views

When Rails looks for a view to render, it will first look in theapp/viewsdirectory of the application. If it cannot find the view there, it will check intheapp/views directories of all engines that have this directory.

When the application is asked to render the view forBlorgh::ArticlesController'sindex action, it will first look for the pathapp/views/blorgh/articles/index.html.erb within the application. If it cannotfind it, it will look inside the engine.

You can override this view in the application by simply creating a new file atapp/views/blorgh/articles/index.html.erb. Then you can completely change whatthis view would normally output.

Try this now by creating a new file atapp/views/blorgh/articles/index.html.erband put this content in it:

<h1>Articles</h1><%=link_to"New Article",new_article_path%><%@articles.eachdo|article|%><h2><%=article.title%></h2><small>By<%=article.author%></small><%=simple_format(article.text)%><hr><%end%>

6.4. Routes

Routes inside an engine are isolated from the application by default. This isdone by theisolate_namespace call inside theEngine class. This essentiallymeans that the application and its engines can have identically named routes andthey will not clash.

Routes inside an engine are drawn on theEngine class withinconfig/routes.rb, like this:

Blorgh::Engine.routes.drawdoresources:articlesend

By having isolated routes such as this, if you wish to link to an area of anengine from within an application, you will need to use the engine's routingproxy method. Calls to normal routing methods such asarticles_path may end upgoing to undesired locations if both the application and the engine have such ahelper defined.

For instance, the following example would go to the application'sarticles_pathif that template was rendered from the application, or the engine'sarticles_pathif it was rendered from the engine:

<%=link_to"Blog articles",articles_path%>

To make this route always use the engine'sarticles_path routing helper method,we must call the method on the routing proxy method that shares the same name asthe engine.

<%=link_to"Blog articles",blorgh.articles_path%>

If you wish to reference the application inside the engine in a similar way, usethemain_app helper:

<%=link_to"Home",main_app.root_path%>

If you were to use this inside an engine, it wouldalways go to theapplication's root. If you were to leave off themain_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 theapplication'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 theapplication's routing methods without themain_app prefix from within theengine.

6.5. Assets

Assets within an engine work in an identical way to a full application. Becausethe engine class inherits fromRails::Engine, the application will know tolook up assets in the engine'sapp/assets andlib/assets directories.

Like all of the other components of an engine, the assets should be namespaced.This means that if you have an asset calledstyle.css, it should be placed atapp/assets/stylesheets/[engine name]/style.css, rather thanapp/assets/stylesheets/style.css. If this asset isn't namespaced, there is apossibility that the host application could have an asset named identically, inwhich case the application's asset would take precedence and the engine's onewould be ignored.

Imagine that you did have an asset located atapp/assets/stylesheets/blorgh/style.css. To include this asset inside anapplication, just usestylesheet_link_tag and reference the asset as if itwere inside the engine:

<%=stylesheet_link_tag"blorgh/style.css"%>

You can also specify these assets as dependencies of other assets using AssetPipeline require statements in processed files:

/* *= require blorgh/style */

Remember that in order to use languages like Sass or CoffeeScript, youshould add the relevant library to your engine's.gemspec.

6.6. Separate Assets and Precompiling

There are some situations where your engine's assets are not required by thehost application. For example, say that you've created an admin functionalitythat only exists for your engine. In this case, the host application doesn'tneed to requireadmin.css oradmin.js. Only the gem's admin layout needsthese assets. It doesn't make sense for the host app to include"blorgh/admin.css" in its stylesheets. In this situation, you shouldexplicitly define these assets for precompilation. This tells Sprockets to addyour engine assets whenbin/rails assets:precompile is triggered.

You can define assets for precompilation inengine.rb:

initializer"blorgh.assets.precompile"do|app|app.config.assets.precompile+=%w( admin.js admin.css )end

For more information, read theAsset Pipeline guide.

6.7. Other Gem Dependencies

Gem dependencies inside an engine should be specified inside the.gemspec fileat the root of the engine. The reason is that the engine may be installed as agem. If dependencies were to be specified inside theGemfile, these would notbe 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 atraditionalgem install, specify it inside theGem::Specification blockinside the.gemspec file in the engine:

s.add_dependency"moo"

To specify a dependency that should only be installed as a developmentdependency of the application, specify it like this:

s.add_development_dependency"moo"

Both kinds of dependencies will be installed whenbundle install is run insideof the application. The development dependencies for the gem will only be usedwhen the development and tests for the engine are running.

Note that if you want to immediately require dependencies when the engine isrequired, you should require them before the engine's initialization. Forexample:

require"other_engine/engine"require"yet_another_engine/engine"moduleMyEngineclassEngine<::Rails::Engineendend


Back to top
[8]ページ先頭

©2009-2025 Movatter.jp