Вы находитесь на странице: 1из 23

GettingStartedwithEngines

Inthisguideyouwilllearnaboutenginesandhowtheycanbeusedtoprovideadditionalfunctionalityto
theirhostapplicationsthroughacleanandveryeasytouseinterface.
Afterreadingthisguide,youwillknow:
Whatmakesanengine.
Howtogenerateanengine.
Howtobuildfeaturesfortheengine.
Howtohooktheengineintoanapplication.
Howtooverrideenginefunctionalityintheapplication.
Chapters
1. Whatareengines?
2. Generatinganengine
InsideanEngine
3. Providingenginefunctionality
GeneratinganArticleResource
GeneratingaCommentsResource
4. HookingIntoanApplication
MountingtheEngine
Enginesetup
UsingaClassProvidedbytheApplication
ConfiguringanEngine
5. Testinganengine
FunctionalTests
6. Improvingenginefunctionality
OverridingModelsandControllers
OverridingViews
Routes
Assets
SeparateAssets&Precompiling
OtherGemDependencies

1 What are engines?


Enginescanbeconsideredminiatureapplicationsthatprovidefunctionalitytotheirhostapplications.A
Railsapplicationisactuallyjusta"supercharged"engine,withtheRails::Applicationclassinheritingalot
ofitsbehaviorfromRails::Engine.
Therefore,enginesandapplicationscanbethoughtofasalmostthesamething,justwithsubtle
differences,asyou'llseethroughoutthisguide.Enginesandapplicationsalsoshareacommonstructure.
Enginesarealsocloselyrelatedtoplugins.Thetwoshareacommonlibdirectorystructure,andareboth
generatedusingtherails plugin newgenerator.Thedifferenceisthatanengineisconsidereda"full
plugin"byRails(asindicatedbythe--fulloptionthat'spassedtothegeneratorcommand).We'llactually
beusingthe--mountableoptionhere,whichincludesallthefeaturesof--full,andthensome.Thisguide
willrefertothese"fullplugins"simplyas"engines"throughout.Anenginecanbeaplugin,anda
plugincanbeanengine.
Theenginethatwillbecreatedinthisguidewillbecalled"blorgh".Thisenginewillprovideblogging
functionalitytoitshostapplications,allowingfornewarticlesandcommentstobecreated.Atthebeginning
ofthisguide,youwillbeworkingsolelywithintheengineitself,butinlatersectionsyou'llseehowtohookit
intoanapplication.
Enginescanalsobeisolatedfromtheirhostapplications.Thismeansthatanapplicationisabletohavea
pathprovidedbyaroutinghelpersuchasarticles_pathanduseanenginethatalsoprovidesapathalso
calledarticles_path,andthetwowouldnotclash.Alongwiththis,controllers,modelsandtablenames
arealsonamespaced.You'llseehowtodothislaterinthisguide.
It'simportanttokeepinmindatalltimesthattheapplicationshouldalwaystakeprecedenceoverits
engines.Anapplicationistheobjectthathasfinalsayinwhatgoesoninitsenvironment.Theengine
shouldonlybeenhancingit,ratherthanchangingitdrastically.
Toseedemonstrationsofotherengines,checkoutDevise,anenginethatprovidesauthenticationforits
parentapplications,orForem,anenginethatprovidesforumfunctionality.There'salsoSpreewhich
providesanecommerceplatform,andRefineryCMS,aCMSengine.
Finally,engineswouldnothavebeenpossiblewithouttheworkofJamesAdam,PiotrSarnacki,theRails
CoreTeam,andanumberofotherpeople.Ifyouevermeetthem,don'tforgettosaythanks!
2Generatinganengine
Togenerateanengine,youwillneedtoruntheplugingeneratorandpassitoptionsasappropriatetothe
need.Forthe"blorgh"example,youwillneedtocreatea"mountable"engine,runningthiscommandina
terminal:
$ rails plugin new blorgh --mountable
Thefulllistofoptionsfortheplugingeneratormaybeseenbytyping:
$ rails plugin --help
The--mountableoptiontellsthegeneratorthatyouwanttocreatea"mountable"andnamespaceisolated
engine.Thisgeneratorwillprovidethesameskeletonstructureaswouldthe--fulloption.The--fulloption
tellsthegeneratorthatyouwanttocreateanengine,includingaskeletonstructurethatprovidesthe
following:
Anappdirectorytree
Aconfig/routes.rbfile:
Rails.application.routes.draw do
end
Afileatlib/blorgh/engine.rb,whichisidenticalinfunctiontoastandardRails
application'sconfig/application.rbfile:
module Blorgh
class Engine < ::Rails::Engine
end
end
The--mountableoptionwilladdtothe--fulloption:
Assetmanifestfiles(application.jsandapplication.css)
AnamespacedApplicationControllerstub
AnamespacedApplicationHelperstub
Alayoutviewtemplatefortheengine
Namespaceisolationtoconfig/routes.rb:
Blorgh::Engine.routes.draw do
end
Namespaceisolationtolib/blorgh/engine.rb:
module Blorgh
class Engine < ::Rails::Engine
isolate_namespace Blorgh
end
end
Additionally,the--mountableoptiontellsthegeneratortomounttheengineinsidethedummytesting
applicationlocatedattest/dummybyaddingthefollowingtothedummyapplication'sroutesfile
attest/dummy/config/routes.rb:
mount Blorgh::Engine =>
"/blorgh"
2.1InsideanEngine
2.1.1CriticalFiles
Attherootofthisbrandnewengine'sdirectorylivesablorgh.gemspecfile.Whenyouincludetheengine
intoanapplicationlateron,youwilldosowiththislineintheRailsapplication'sGemfile:
gem 'blorgh', path:
'engines/blorgh'
Don'tforgettorunbundle installasusual.ByspecifyingitasagemwithintheGemfile,Bundlerwillload
itassuch,parsingthisblorgh.gemspecfileandrequiringafilewithinthelibdirectory
calledlib/blorgh.rb.Thisfilerequirestheblorgh/engine.rbfile(locatedatlib/blorgh/engine.rb)and
definesabasemodulecalledBlorgh.
require
"blorgh/engine"

module Blorgh
end
Someengineschoosetousethisfiletoputglobalconfigurationoptionsfortheirengine.It'sarelatively
goodidea,soifyouwanttoofferconfigurationoptions,thefilewhereyourengine'smoduleisdefinedis
perfectforthat.Placethemethodsinsidethemoduleandyou'llbegoodtogo.
Withinlib/blorgh/engine.rbisthebaseclassfortheengine:
module Blorgh
class Engine < ::Rails::Engine
isolate_namespace Blorgh
end
end
ByinheritingfromtheRails::Engineclass,thisgemnotifiesRailsthatthere'sanengineatthespecified
path,andwillcorrectlymounttheengineinsidetheapplication,performingtaskssuchasadding
theappdirectoryoftheenginetotheloadpathformodels,mailers,controllers,andviews.
Theisolate_namespacemethodheredeservesspecialnotice.Thiscallisresponsibleforisolatingthe
controllers,models,routesandotherthingsintotheirownnamespace,awayfromsimilarcomponents
insidetheapplication.Withoutthis,thereisapossibilitythattheengine'scomponentscould"leak"intothe
application,causingunwanteddisruption,orthatimportantenginecomponentscouldbeoverriddenby
similarlynamedthingswithintheapplication.Oneoftheexamplesofsuchconflictsishelpers.Without
callingisolate_namespace,theengine'shelperswouldbeincludedinanapplication'scontrollers.
Itishighlyrecommendedthattheisolate_namespacelinebeleftwithintheEngineclassdefinition.
Withoutit,classesgeneratedinanenginemayconflictwithanapplication.
Whatthisisolationofthenamespacemeansisthatamodelgeneratedbyacalltobin/rails g model,
suchasbin/rails g model article,won'tbecalledArticle,butinsteadbenamespacedand
calledBlorgh::Article.Inaddition,thetableforthemodelisnamespaced,becomingblorgh_articles,
ratherthansimplyarticles.Similartothemodelnamespacing,acontroller
calledArticlesControllerbecomesBlorgh::ArticlesControllerandtheviewsforthatcontrollerwillnot
beatapp/views/articles,butapp/views/blorgh/articlesinstead.Mailersarenamespacedaswell.
Finally,routeswillalsobeisolatedwithintheengine.Thisisoneofthemostimportantpartsabout
namespacing,andisdiscussedlaterintheRoutessectionofthisguide.
2.1.2appDirectory
Insidetheappdirectoryarethe
standardassets,controllers,helpers,mailers,modelsandviewsdirectoriesthatyoushouldbe
familiarwithfromanapplication.Thehelpers,mailersandmodelsdirectoriesareempty,sotheyaren't
describedinthissection.We'lllookmoreintomodelsinafuturesection,whenwe'rewritingtheengine.
Withintheapp/assetsdirectory,therearetheimages,javascriptsandstylesheetsdirectorieswhich,
again,youshouldbefamiliarwithduetotheirsimilaritytoanapplication.Onedifferencehere,however,is
thateachdirectorycontainsasubdirectorywiththeenginename.Becausethisengineisgoingtobe
namespaced,itsassetsshouldbetoo.
Withintheapp/controllersdirectorythereisablorghdirectorythatcontainsafile
calledapplication_controller.rb.Thisfilewillprovideanycommonfunctionalityforthecontrollersofthe
engine.Theblorghdirectoryiswheretheothercontrollersfortheenginewillgo.Byplacingthemwithin
thisnamespaceddirectory,youpreventthemfrompossiblyclashingwithidenticallynamedcontrollers
withinotherenginesorevenwithintheapplication.
TheApplicationControllerclassinsideanengineisnamedjustlikeaRailsapplicationinordertomakeit
easierforyoutoconvertyourapplicationsintoengines.
BecauseofthewaythatRubydoesconstantlookupyoumayrunintoasituationwhereyourengine
controllerisinheritingfromthemainapplicationcontrollerandnotyourengine'sapplicationcontroller.Ruby
isabletoresolvetheApplicationControllerconstant,andthereforetheautoloadingmechanismisnot
triggered.SeethesectionWhenConstantsAren'tMissedoftheAutoloadingandReloading
Constantsguideforfurtherdetails.Thebestwaytopreventthisfromhappeningisto
userequire_dependencytoensurethattheengine'sapplicationcontrollerisloaded.Forexample:
# app/controllers/blorgh/articles_controller.rb:
require_dependency
"blorgh/application_controller"

module Blorgh
class ArticlesController < ApplicationController
...
end
end
Don'tuserequirebecauseitwillbreaktheautomaticreloadingofclassesinthedevelopmentenvironment
usingrequire_dependencyensuresthatclassesareloadedandunloadedinthecorrectmanner.
Lastly,theapp/viewsdirectorycontainsalayoutsfolder,whichcontainsafile
atblorgh/application.html.erb.Thisfileallowsyoutospecifyalayoutfortheengine.Ifthisengineisto
beusedasastandaloneengine,thenyouwouldaddanycustomizationtoitslayoutinthisfile,ratherthan
theapplication'sapp/views/layouts/application.html.erbfile.
Ifyoudon'twanttoforcealayoutontousersoftheengine,thenyoucandeletethisfileandreferencea
differentlayoutinthecontrollersofyourengine.
2.1.3binDirectory
Thisdirectorycontainsonefile,bin/rails,whichenablesyoutousetherailssubcommandsand
generatorsjustlikeyouwouldwithinanapplication.Thismeansthatyouwillbeabletogeneratenew
controllersandmodelsforthisengineveryeasilybyrunningcommandslikethis:
$ bin/rails g
model
Keepinmind,ofcourse,thatanythinggeneratedwiththesecommandsinsideofanenginethat
hasisolate_namespaceintheEngineclasswillbenamespaced.
2.1.4testDirectory
Thetestdirectoryiswheretestsfortheenginewillgo.Totesttheengine,thereisacutdownversionofa
Railsapplicationembeddedwithinitattest/dummy.Thisapplicationwillmounttheenginein
thetest/dummy/config/routes.rbfile:
Rails.application.routes.draw do
mount Blorgh::Engine =>
"/blorgh"
end
Thislinemountstheengineatthepath/blorgh,whichwillmakeitaccessiblethroughtheapplicationonly
atthatpath.
Insidethetestdirectorythereisthetest/integrationdirectory,whereintegrationtestsfortheengine
shouldbeplaced.Otherdirectoriescanbecreatedinthetestdirectoryaswell.Forexample,youmay
wishtocreateatest/modelsdirectoryforyourmodeltests.
3Providingenginefunctionality
Theenginethatthisguidecoversprovidessubmittingarticlesandcommentingfunctionalityandfollowsa
similarthreadtotheGettingStartedGuide,withsomenewtwists.
3.1GeneratinganArticleResource
ThefirstthingtogenerateforablogengineistheArticlemodelandrelatedcontroller.Toquicklygenerate
this,youcanusetheRailsscaffoldgenerator.
$ bin/rails generate scaffold article title:string
text:text
Thiscommandwilloutputthisinformation:
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
Thefirstthingthatthescaffoldgeneratordoesisinvoketheactive_recordgenerator,whichgeneratesa
migrationandamodelfortheresource.Notehere,however,thatthemigrationis
calledcreate_blorgh_articlesratherthantheusualcreate_articles.Thisisdueto
theisolate_namespacemethodcalledintheBlorgh::Engineclass'sdefinition.Themodelhereisalso
namespaced,beingplacedatapp/models/blorgh/article.rbratherthanapp/models/article.rbdueto
theisolate_namespacecallwithintheEngineclass.
Next,thetest_unitgeneratorisinvokedforthismodel,generatingamodeltest
attest/models/blorgh/article_test.rb(ratherthantest/models/article_test.rb)andafixture
attest/fixtures/blorgh/articles.yml(ratherthantest/fixtures/articles.yml).
Afterthat,alinefortheresourceisinsertedintotheconfig/routes.rbfilefortheengine.Thislineis
simplyresources :articles,turningtheconfig/routes.rbfilefortheengineintothis:
Blorgh::Engine.routes.draw do
resources :articles
end
NoteherethattheroutesaredrawnupontheBlorgh::Engineobjectratherthan
theYourApp::Applicationclass.Thisissothattheengineroutesareconfinedtotheengineitselfandcan
bemountedataspecificpointasshowninthetestdirectorysection.Italsocausestheengine'sroutesto
beisolatedfromthoseroutesthatarewithintheapplication.TheRoutessectionofthisguidedescribesitin
detail.
Next,thescaffold_controllergeneratorisinvoked,generatingacontroller
calledBlorgh::ArticlesController(atapp/controllers/blorgh/articles_controller.rb)anditsrelated
viewsatapp/views/blorgh/articles.Thisgeneratoralsogeneratesatestforthecontroller
(test/controllers/blorgh/articles_controller_test.rb)andahelper
(app/helpers/blorgh/articles_helper.rb).
Everythingthisgeneratorhascreatedisneatlynamespaced.Thecontroller'sclassisdefinedwithin
theBlorghmodule:
module Blorgh
class ArticlesController < ApplicationController
...
end
end
TheArticlesControllerclassinheritsfromBlorgh::ApplicationController,notthe
application'sApplicationController.
Thehelperinsideapp/helpers/blorgh/articles_helper.rbisalsonamespaced:
module Blorgh
module ArticlesHelper
...
end
end
Thishelpspreventconflictswithanyotherengineorapplicationthatmayhaveanarticleresourceaswell.
Finally,theassetsforthisresourcearegeneratedintwo
files:app/assets/javascripts/blorgh/articles.jsandapp/assets/stylesheets/blorgh/articles.css.
You'llseehowtousethesealittlelater.
Youcanseewhattheenginehassofarbyrunningbin/rails db:migrateattherootofourenginetorun
themigrationgeneratedbythescaffoldgenerator,andthenrunningrails serverintest/dummy.When
youopenhttp://localhost:3000/blorgh/articlesyouwillseethedefaultscaffoldthathasbeen
generated.Clickaround!You'vejustgeneratedyourfirstengine'sfirstfunctions.
Ifyou'dratherplayaroundintheconsole,rails consolewillalsoworkjustlikeaRailsapplication.
Remember:theArticlemodelisnamespaced,sotoreferenceityoumustcallitasBlorgh::Article.
>> Blorgh::Article.find(1)
=> #<Blorgh::Article id:
1 ...>
Onefinalthingisthatthearticlesresourceforthisengineshouldbetherootoftheengine.Whenever
someonegoestotherootpathwheretheengineismounted,theyshouldbeshownalistofarticles.This
canbemadetohappenifthislineisinsertedintotheconfig/routes.rbfileinsidetheengine:
root to:
"articles#index"
Nowpeoplewillonlyneedtogototherootoftheenginetoseeallthearticles,ratherthan
visiting/articles.Thismeansthatinsteadofhttp://localhost:3000/blorgh/articles,youonlyneedtogo
tohttp://localhost:3000/blorghnow.
3.2GeneratingaCommentsResource
Nowthattheenginecancreatenewarticles,itonlymakessensetoaddcommentingfunctionalityaswell.
Todothis,you'llneedtogenerateacommentmodel,acommentcontrollerandthenmodifythearticles
scaffoldtodisplaycommentsandallowpeopletocreatenewones.
Fromtheapplicationroot,runthemodelgenerator.TellittogenerateaCommentmodel,withtherelated
tablehavingtwocolumns:anarticle_idintegerandtexttextcolumn.
$ bin/rails generate model Comment article_id:integer
text:text
Thiswilloutputthefollowing:
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
Thisgeneratorcallwillgeneratejustthenecessarymodelfilesitneeds,namespacingthefilesunder
ablorghdirectoryandcreatingamodelclasscalledBlorgh::Comment.Nowrunthemigrationtocreate
ourblorgh_commentstable:
$ bin/rails
db:migrate
Toshowthecommentsonanarticle,editapp/views/blorgh/articles/show.html.erbandaddthisline
beforethe"Edit"link:
<h3>Comments</h3>
<%= render @article.comments %>
Thislinewillrequiretheretobeahas_manyassociationforcommentsdefinedon
theBlorgh::Articlemodel,whichthereisn'trightnow.Todefineone,
openapp/models/blorgh/article.rbandaddthislineintothemodel:
has_many :comments
Turningthemodelintothis:
module Blorgh
class Article < ApplicationRecord
has_many :comments
end
end
Becausethehas_manyisdefinedinsideaclassthatisinsidetheBlorghmodule,Railswillknowthatyou
wanttousetheBlorgh::Commentmodelfortheseobjects,sothere'snoneedtospecifythatusing
the:class_nameoptionhere.
Next,thereneedstobeaformsothatcommentscanbecreatedonanarticle.Toaddthis,putthisline
underneaththecalltorender @article.commentsinapp/views/blorgh/articles/show.html.erb:
<%= render "blorgh/comments/form"
%>
Next,thepartialthatthislinewillrenderneedstoexist.Createanewdirectory
atapp/views/blorgh/commentsandinitanewfilecalled_form.html.erbwhichhasthiscontentto
createtherequiredpartial:
<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 %>
Whenthisformissubmitted,itisgoingtoattempttoperformaPOSTrequesttoaroute
of/articles/:article_id/commentswithintheengine.Thisroutedoesn'texistatthemoment,butcanbe
createdbychangingtheresources :articleslineinsideconfig/routes.rbintotheselines:
resources :articles do
resources :comments
end
Thiscreatesanestedrouteforthecomments,whichiswhattheformrequires.
Theroutenowexists,butthecontrollerthatthisroutegoestodoesnot.Tocreateit,runthiscommand
fromtheapplicationroot:
$ bin/rails g controller
comments
Thiswillgeneratethefollowingthings:
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
TheformwillbemakingaPOSTrequestto/articles/:article_id/comments,whichwillcorrespondwith
thecreateactioninBlorgh::CommentsController.Thisactionneedstobecreated,whichcanbedone
byputtingthefollowinglinesinsidetheclassdefinition
inapp/controllers/blorgh/comments_controller.rb:
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
Thisisthefinalsteprequiredtogetthenewcommentformworking.Displayingthecomments,however,is
notquiterightyet.Ifyouweretocreateacommentrightnow,youwouldseethiserror:
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"
Theengineisunabletofindthepartialrequiredforrenderingthecomments.Railslooksfirstinthe
application's(test/dummy)app/viewsdirectoryandthenintheengine'sapp/viewsdirectory.Whenit
can'tfindit,itwillthrowthiserror.Theengineknowstolookforblorgh/comments/_commentbecause
themodelobjectitisreceivingisfromtheBlorgh::Commentclass.
Thispartialwillberesponsibleforrenderingjustthecommenttext,fornow.Createanewfile
atapp/views/blorgh/comments/_comment.html.erbandputthislineinsideit:
<%= comment_counter + 1 %>. <%= comment.text
%>
Thecomment_counterlocalvariableisgiventousbythe<%= render @article.comments %>call,
whichwilldefineitautomaticallyandincrementthecounterasititeratesthrougheachcomment.It'sused
inthisexampletodisplayasmallnumbernexttoeachcommentwhenit'screated.
Thatcompletesthecommentfunctionofthebloggingengine.Nowit'stimetouseitwithinanapplication.
4HookingIntoanApplication
Usinganenginewithinanapplicationisveryeasy.Thissectioncovershowtomounttheengineintoan
applicationandtheinitialsetuprequired,aswellaslinkingtheenginetoaUserclassprovidedbythe
applicationtoprovideownershipforarticlesandcommentswithintheengine.
4.1MountingtheEngine
First,theengineneedstobespecifiedinsidetheapplication'sGemfile.Ifthereisn'tanapplicationhandy
totestthisoutin,generateoneusingtherails newcommandoutsideoftheenginedirectorylikethis:
$ rails new unicorn
Usually,specifyingtheengineinsidetheGemfilewouldbedonebyspecifyingitasanormal,everyday
gem.
gem
'devise'
However,becauseyouaredevelopingtheblorghengineonyourlocalmachine,youwillneedtospecify
the:pathoptioninyourGemfile:
gem 'blorgh', path:
'engines/blorgh'
Thenrunbundletoinstallthegem.
Asdescribedearlier,byplacingthegemintheGemfileitwillbeloadedwhenRailsisloaded.Itwillfirst
requirelib/blorgh.rbfromtheengine,thenlib/blorgh/engine.rb,whichisthefilethatdefinesthemajor
piecesoffunctionalityfortheengine.
Tomaketheengine'sfunctionalityaccessiblefromwithinanapplication,itneedstobemountedinthat
application'sconfig/routes.rbfile:
mount Blorgh::Engine, at:
"/blog"
Thislinewillmounttheengineat/blogintheapplication.Makingitaccessible
athttp://localhost:3000/blogwhentheapplicationrunswithrails server.
Otherengines,suchasDevise,handlethisalittledifferentlybymakingyouspecifycustomhelpers(such
asdevise_for)intheroutes.Thesehelpersdoexactlythesamething,mountingpiecesoftheengines's
functionalityatapredefinedpathwhichmaybecustomizable.
4.2Enginesetup
Theenginecontainsmigrationsfortheblorgh_articlesandblorgh_commentstablewhichneedtobe
createdintheapplication'sdatabasesothattheengine'smodelscanquerythemcorrectly.Tocopythese
migrationsintotheapplicationrunthefollowingcommandfromthetest/dummydirectoryofyourRails
engine:
$ bin/rails
blorgh:install:migrations
Ifyouhavemultipleenginesthatneedmigrationscopiedover,userailties:install:migrationsinstead:
$ bin/rails
railties:install:migrations
Thiscommand,whenrunforthefirsttime,willcopyoverallthemigrationsfromtheengine.Whenrunthe
nexttime,itwillonlycopyovermigrationsthathaven'tbeencopiedoveralready.Thefirstrunforthis
commandwilloutputsomethingsuchasthis:
Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh
Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh
Thefirsttimestamp([timestamp_1])willbethecurrenttime,andthesecondtimestamp
([timestamp_2])willbethecurrenttimeplusasecond.Thereasonforthisissothatthemigrationsfor
theenginearerunafteranyexistingmigrationsintheapplication.
Torunthesemigrationswithinthecontextoftheapplication,simplyrunbin/rails db:migrate.When
accessingtheenginethroughhttp://localhost:3000/blog,thearticleswillbeempty.Thisisbecausethe
tablecreatedinsidetheapplicationisdifferentfromtheonecreatedwithintheengine.Goahead,play
aroundwiththenewlymountedengine.You'llfindthatit'sthesameaswhenitwasonlyanengine.
Ifyouwouldliketorunmigrationsonlyfromoneengine,youcandoitbyspecifyingSCOPE:
bin/rails db:migrate
SCOPE=blorgh
Thismaybeusefulifyouwanttorevertengine'smigrationsbeforeremovingit.Torevertallmigrations
fromblorghengineyoucanruncodesuchas:
bin/rails db:migrate SCOPE=blorgh
VERSION=0
4.3UsingaClassProvidedbytheApplication
4.3.1UsingaModelProvidedbytheApplication
Whenanengineiscreated,itmaywanttousespecificclassesfromanapplicationtoprovidelinksbetween
thepiecesoftheengineandthepiecesoftheapplication.Inthecaseoftheblorghengine,makingarticles
andcommentshaveauthorswouldmakealotofsense.
AtypicalapplicationmighthaveaUserclassthatwouldbeusedtorepresentauthorsforanarticleora
comment.Buttherecouldbeacasewheretheapplicationcallsthisclasssomethingdifferent,such
asPerson.Forthisreason,theengineshouldnothardcodeassociationsspecificallyforaUserclass.
Tokeepitsimpleinthiscase,theapplicationwillhaveaclasscalledUserthatrepresentstheusersofthe
application(we'llgetintomakingthisconfigurablefurtheron).Itcanbegeneratedusingthiscommand
insidetheapplication:
rails g model user name:string
Thebin/rails db:migratecommandneedstoberunheretoensurethatourapplicationhas
theuserstableforfutureuse.
Also,tokeepitsimple,thearticlesformwillhaveanewtextfieldcalledauthor_name,whereuserscan
electtoputtheirname.TheenginewillthentakethisnameandeithercreateanewUserobjectfromit,or
findonethatalreadyhasthatname.Theenginewillthenassociatethearticlewiththefoundor
createdUserobject.
First,theauthor_nametextfieldneedstobeaddedto
theapp/views/blorgh/articles/_form.html.erbpartialinsidetheengine.Thiscanbeaddedabove
thetitlefieldwiththiscode:
<div class="field">
<%= f.label :author_name %><br>
<%= f.text_field :author_name %>
</div>
Next,weneedtoupdateourBlorgh::ArticleController#article_paramsmethodtopermitthenewform
parameter:
def article_params
params.require(:article).permit(:title, :text, :author_name)
end
TheBlorgh::Articlemodelshouldthenhavesomecodetoconverttheauthor_namefieldintoan
actualUserobjectandassociateitasthatarticle'sauthorbeforethearticleissaved.Itwillalsoneedto
haveanattr_accessorsetupforthisfield,sothatthesetterandgettermethodsaredefinedforit.
Todoallthis,you'llneedtoaddtheattr_accessorforauthor_name,theassociationfortheauthorand
thebefore_validationcallintoapp/models/blorgh/article.rb.Theauthorassociationwillbehard
codedtotheUserclassforthetimebeing.
attr_accessor :author_name
belongs_to :author, class_name: "User"
before_validation :set_author

private
def set_author
self.author = User.find_or_create_by(name: author_name)
end
Byrepresentingtheauthorassociation'sobjectwiththeUserclass,alinkisestablishedbetweenthe
engineandtheapplication.Thereneedstobeawayofassociatingtherecordsin
theblorgh_articlestablewiththerecordsintheuserstable.Becausetheassociationiscalledauthor,
thereshouldbeanauthor_idcolumnaddedtotheblorgh_articlestable.
Togeneratethisnewcolumn,runthiscommandwithintheengine:
$ bin/rails g migration add_author_id_to_blorgh_articles
author_id:integer
Duetothemigration'snameandthecolumnspecificationafterit,Railswillautomaticallyknowthatyou
wanttoaddacolumntoaspecifictableandwritethatintothemigrationforyou.Youdon'tneedtotellit
anymorethanthis.
Thismigrationwillneedtoberunontheapplication.Todothat,itmustfirstbecopiedusingthiscommand:
$ bin/rails
blorgh:install:migrations
Noticethatonlyonemigrationwascopiedoverhere.Thisisbecausethefirsttwomigrationswerecopied
overthefirsttimethiscommandwasrun.
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
Runthemigrationusing:
$ bin/rails
db:migrate
Nowwithallthepiecesinplace,anactionwilltakeplacethatwillassociateanauthorrepresentedbya
recordintheuserstablewithanarticle,representedbytheblorgh_articlestablefromtheengine.
Finally,theauthor'snameshouldbedisplayedonthearticle'spage.Addthiscodeabovethe"Title"output
insideapp/views/blorgh/articles/show.html.erb:
<p>
<b>Author:</b>
<%= @article.author.name %>
</p>
4.3.2UsingaControllerProvidedbytheApplication
BecauseRailscontrollersgenerallysharecodeforthingslikeauthenticationandaccessingsession
variables,theyinheritfromApplicationControllerbydefault.Railsengines,howeverarescopedtorun
independentlyfromthemainapplication,soeachenginegetsascopedApplicationController.This
namespacepreventscodecollisions,butoftenenginecontrollersneedtoaccessmethodsinthemain
application'sApplicationController.Aneasywaytoprovidethisaccessistochangetheengine's
scopedApplicationControllertoinheritfromthemainapplication'sApplicationController.Forour
Blorghenginethiswouldbedonebychangingapp/controllers/blorgh/application_controller.rbtolook
like:
module Blorgh
class ApplicationController < ::ApplicationController
end
end
Bydefault,theengine'scontrollersinheritfromBlorgh::ApplicationController.So,aftermakingthis
changetheywillhaveaccesstothemainapplication'sApplicationController,asthoughtheywerepartof
themainapplication.
ThischangedoesrequirethattheengineisrunfromaRailsapplicationthathas
anApplicationController.
4.4ConfiguringanEngine
ThissectioncovershowtomaketheUserclassconfigurable,followedbygeneralconfigurationtipsforthe
engine.
4.4.1SettingConfigurationSettingsintheApplication
ThenextstepistomaketheclassthatrepresentsaUserintheapplicationcustomizablefortheengine.
ThisisbecausethatclassmaynotalwaysbeUser,aspreviouslyexplained.Tomakethissetting
customizable,theenginewillhaveaconfigurationsettingcalledauthor_classthatwillbeusedtospecify
whichclassrepresentsusersinsidetheapplication.
Todefinethisconfigurationsetting,youshoulduseamattr_accessorinsidetheBlorghmoduleforthe
engine.Addthislinetolib/blorgh.rbinsidetheengine:
mattr_accessor :author_class
Thismethodworkslikeitsbrothers,attr_accessorandcattr_accessor,butprovidesasetterandgetter
methodonthemodulewiththespecifiedname.Touseit,itmustbereferenced
usingBlorgh.author_class.
ThenextstepistoswitchtheBlorgh::Articlemodelovertothisnewsetting.Change
thebelongs_toassociationinsidethismodel(app/models/blorgh/article.rb)tothis:
belongs_to :author, class_name: Blorgh.author_class
Theset_authormethodintheBlorgh::Articlemodelshouldalsousethisclass:
self.author = Blorgh.author_class.constantize.find_or_create_by(name:
author_name)
Tosavehavingtocallconstantizeontheauthor_classresultallthetime,youcouldinsteadjustoverride
theauthor_classgettermethodinsidetheBlorghmoduleinthelib/blorgh.rbfiletoalways
callconstantizeonthesavedvaluebeforereturningtheresult:
def self.author_class
@@author_class.constantize
end
Thiswouldthenturntheabovecodeforset_authorintothis:
self.author = Blorgh.author_class.find_or_create_by(name: author_name)
Resultinginsomethingalittleshorter,andmoreimplicitinitsbehavior.Theauthor_classmethodshould
alwaysreturnaClassobject.
Sincewechangedtheauthor_classmethodtoreturnaClassinsteadofaString,wemustalsomodify
ourbelongs_todefinitionintheBlorgh::Articlemodel:
belongs_to :author, class_name: Blorgh.author_class.to_s
Tosetthisconfigurationsettingwithintheapplication,aninitializershouldbeused.Byusinganinitializer,
theconfigurationwillbesetupbeforetheapplicationstartsandcallstheengine'smodels,whichmay
dependonthisconfigurationsettingexisting.
Createanewinitializeratconfig/initializers/blorgh.rbinsidetheapplicationwheretheblorghengineis
installedandputthiscontentinit:
Blorgh.author_class =
"User"
It'sveryimportantheretousetheStringversionoftheclass,ratherthantheclassitself.Ifyouweretouse
theclass,Railswouldattempttoloadthatclassandthenreferencetherelatedtable.Thiscouldleadto
problemsifthetablewasn'talreadyexisting.Therefore,aStringshouldbeusedandthenconvertedtoa
classusingconstantizeintheenginelateron.
Goaheadandtrytocreateanewarticle.Youwillseethatitworksexactlyinthesamewayasbefore,
exceptthistimetheengineisusingtheconfigurationsettinginconfig/initializers/blorgh.rbtolearnwhat
theclassis.
Therearenownostrictdependenciesonwhattheclassis,onlywhattheAPIfortheclassmustbe.The
enginesimplyrequiresthisclasstodefineafind_or_create_bymethodwhichreturnsanobjectofthat
class,tobeassociatedwithanarticlewhenit'screated.Thisobject,ofcourse,shouldhavesomesortof
identifierbywhichitcanbereferenced.
4.4.2GeneralEngineConfiguration
Withinanengine,theremaycomeatimewhereyouwishtousethingssuchasinitializers,
internationalizationorotherconfigurationoptions.Thegreatnewsisthatthesethingsareentirelypossible,
becauseaRailsenginesharesmuchthesamefunctionalityasaRailsapplication.Infact,aRails
application'sfunctionalityisactuallyasupersetofwhatisprovidedbyengines!
Ifyouwishtouseaninitializercodethatshouldrunbeforetheengineisloadedtheplaceforitis
theconfig/initializersfolder.Thisdirectory'sfunctionalityisexplainedintheInitializerssectionofthe
Configuringguide,andworkspreciselythesamewayastheconfig/initializersdirectoryinsidean
application.Thesamethinggoesifyouwanttouseastandardinitializer.
Forlocales,simplyplacethelocalefilesintheconfig/localesdirectory,justlikeyouwouldinan
application.
5Testinganengine
Whenanengineisgenerated,thereisasmallerdummyapplicationcreatedinsideitattest/dummy.This
applicationisusedasamountingpointfortheengine,tomaketestingtheengineextremelysimple.You
mayextendthisapplicationbygeneratingcontrollers,modelsorviewsfromwithinthedirectory,andthen
usethosetotestyourengine.
ThetestdirectoryshouldbetreatedlikeatypicalRailstestingenvironment,allowingforunit,functional
andintegrationtests.
5.1FunctionalTests
Amatterworthtakingintoconsiderationwhenwritingfunctionaltestsisthatthetestsaregoingtobe
runningonanapplicationthetest/dummyapplicationratherthanyourengine.Thisisduetothesetup
ofthetestingenvironment;anengineneedsanapplicationasahostfortestingitsmainfunctionality,
especiallycontrollers.ThismeansthatifyouweretomakeatypicalGETtoacontrollerinacontroller's
functionaltestlikethis:
module Blorgh
class FooControllerTest < ActionDispatch::IntegrationTest
include Engine.routes.url_helpers

def test_index
get foos_url
...
end
end
end
Itmaynotfunctioncorrectly.Thisisbecausetheapplicationdoesn'tknowhowtoroutetheserequeststo
theengineunlessyouexplicitlytellithow.Todothis,youmustsetthe@routesinstancevariabletothe
engine'sroutesetinyoursetupcode:
module Blorgh
class FooControllerTest < ActionDispatch::IntegrationTest
include Engine.routes.url_helpers

setup do
@routes = Engine.routes
end

def test_index
get foos_url
...
end
end
end
ThistellstheapplicationthatyoustillwanttoperformaGETrequesttotheindexactionofthiscontroller,
butyouwanttousetheengine'sroutetogetthere,ratherthantheapplication'sone.
Thisalsoensuresthattheengine'sURLhelperswillworkasexpectedinyourtests.
6Improvingenginefunctionality
Thissectionexplainshowtoaddand/oroverrideengineMVCfunctionalityinthemainRailsapplication.
6.1OverridingModelsandControllers
EnginemodelandcontrollerclassescanbeextendedbyopenclassingtheminthemainRailsapplication
(sincemodelandcontrollerclassesarejustRubyclassesthatinheritRailsspecificfunctionality).Open
classinganEngineclassredefinesitforuseinthemainapplication.Thisisusuallyimplementedbyusing
thedecoratorpattern.
Forsimpleclassmodifications,useClass#class_eval.Forcomplexclassmodifications,consider
usingActiveSupport::Concern.
6.1.1AnoteonDecoratorsandLoadingCode
BecausethesedecoratorsarenotreferencedbyyourRailsapplicationitself,Rails'autoloadingsystemwill
notkickinandloadyourdecorators.Thismeansthatyouneedtorequirethemyourself.
Hereissomesamplecodetodothis:
# 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
Thisdoesn'tapplytojustDecorators,butanythingthatyouaddinanenginethatisn'treferencedbyyour
mainapplication.
6.1.2ImplementingDecoratorPatternUsingClass#class_eval
AddingArticle#time_since_created:

#
MyApp/app/decorators/models/blorgh/article_decorator.rb

Blorgh::Article.class_eval do
def time_since_created
Time.current - created_at
end
end
# Blorgh/app/models/article.rb

class Article < ApplicationRecord


has_many :comments
end
OverridingArticle#summary:

#
MyApp/app/decorators/models/blorgh/article_decorator.rb

Blorgh::Article.class_eval do
def summary
"#{title} - #{truncate(text)}"
end
end
# Blorgh/app/models/article.rb

class Article < ApplicationRecord


has_many :comments
def summary
"#{title}"
end
end
6.1.3ImplementingDecoratorPatternUsingActiveSupport::Concern
UsingClass#class_evalisgreatforsimpleadjustments,butformorecomplexclassmodifications,you
mightwanttoconsiderusingActiveSupport::Concern.ActiveSupport::Concernmanagesloadorderof
interlinkeddependentmodulesandclassesatruntimeallowingyoutosignificantlymodularizeyourcode.
AddingArticle#time_since_createdandOverridingArticle#summary:

# MyApp/app/models/blorgh/article.rb

class Blorgh::Article < ApplicationRecord


include Blorgh::Concerns::Models::Article

def time_since_created
Time.current - created_at
end

def summary
"#{title} - #{truncate(text)}"
end
end
# Blorgh/app/models/article.rb

class Article < ApplicationRecord


include Blorgh::Concerns::Models::Article
end
# Blorgh/lib/concerns/models/article.rb

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_validation :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
6.2OverridingViews
WhenRailslooksforaviewtorender,itwillfirstlookintheapp/viewsdirectoryoftheapplication.Ifit
cannotfindtheviewthere,itwillcheckintheapp/viewsdirectoriesofallenginesthathavethisdirectory.
WhentheapplicationisaskedtorendertheviewforBlorgh::ArticlesController'sindexaction,itwillfirst
lookforthepathapp/views/blorgh/articles/index.html.erbwithintheapplication.Ifitcannotfindit,it
willlookinsidetheengine.
Youcanoverridethisviewintheapplicationbysimplycreatinganewfile
atapp/views/blorgh/articles/index.html.erb.Thenyoucancompletelychangewhatthisviewwould
normallyoutput.
Trythisnowbycreatinganewfileatapp/views/blorgh/articles/index.html.erbandputthiscontentin
it:
<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 %>
6.3Routes
Routesinsideanengineareisolatedfromtheapplicationbydefault.Thisisdoneby
theisolate_namespacecallinsidetheEngineclass.Thisessentiallymeansthattheapplicationandits
enginescanhaveidenticallynamedroutesandtheywillnotclash.
RoutesinsideanenginearedrawnontheEngineclasswithinconfig/routes.rb,likethis:
Blorgh::Engine.routes.draw do
resources :articles
end
Byhavingisolatedroutessuchasthis,ifyouwishtolinktoanareaofanenginefromwithinanapplication,
youwillneedtousetheengine'sroutingproxymethod.Callstonormalroutingmethodssuch
asarticles_pathmayendupgoingtoundesiredlocationsifboththeapplicationandtheenginehavesuch
ahelperdefined.
Forinstance,thefollowingexamplewouldgototheapplication'sarticles_pathifthattemplatewas
renderedfromtheapplication,ortheengine'sarticles_pathifitwasrenderedfromtheengine:
<%= link_to "Blog articles", articles_path
%>
Tomakethisroutealwaysusetheengine'sarticles_pathroutinghelpermethod,wemustcallthemethod
ontheroutingproxymethodthatsharesthesamenameastheengine.
<%= link_to "Blog articles", blorgh.articles_path
%>
Ifyouwishtoreferencetheapplicationinsidetheengineinasimilarway,usethemain_apphelper:
<%= link_to "Home", main_app.root_path
%>
Ifyouweretousethisinsideanengine,itwouldalwaysgototheapplication'sroot.Ifyouweretoleaveoff
themain_app"routingproxy"methodcall,itcouldpotentiallygototheengine'sorapplication'sroot,
dependingonwhereitwascalledfrom.
Ifatemplaterenderedfromwithinanengineattemptstouseoneoftheapplication'sroutinghelper
methods,itmayresultinanundefinedmethodcall.Ifyouencountersuchanissue,ensurethatyou'renot
attemptingtocalltheapplication'sroutingmethodswithoutthemain_appprefixfromwithintheengine.
6.4Assets
Assetswithinanengineworkinanidenticalwaytoafullapplication.Becausetheengineclassinherits
fromRails::Engine,theapplicationwillknowtolookupassetsintheengine's'app/assets'and'lib/assets'
directories.
Likealloftheothercomponentsofanengine,theassetsshouldbenamespaced.Thismeansthatifyou
haveanassetcalledstyle.css,itshouldbeplacedatapp/assets/stylesheets/[engine
name]/style.css,ratherthanapp/assets/stylesheets/style.css.Ifthisassetisn'tnamespaced,thereis
apossibilitythatthehostapplicationcouldhaveanassetnamedidentically,inwhichcasetheapplication's
assetwouldtakeprecedenceandtheengine'sonewouldbeignored.
Imaginethatyoudidhaveanassetlocatedatapp/assets/stylesheets/blorgh/style.cssToincludethis
assetinsideanapplication,justusestylesheet_link_tagandreferencetheassetasifitwereinsidethe
engine:
<%= stylesheet_link_tag "blorgh/style.css"
%>
YoucanalsospecifytheseassetsasdependenciesofotherassetsusingAssetPipelinerequire
statementsinprocessedfiles:
/*
*= require blorgh/style
*/
RememberthatinordertouselanguageslikeSassorCoffeeScript,youshouldaddtherelevantlibraryto
yourengine's.gemspec.
6.5SeparateAssets&Precompiling
Therearesomesituationswhereyourengine'sassetsarenotrequiredbythehostapplication.For
example,saythatyou'vecreatedanadminfunctionalitythatonlyexistsforyourengine.Inthiscase,the
hostapplicationdoesn'tneedtorequireadmin.cssoradmin.js.Onlythegem'sadminlayoutneedsthese
assets.Itdoesn'tmakesenseforthehostapptoinclude"blorgh/admin.css"initsstylesheets.Inthis
situation,youshouldexplicitlydefinetheseassetsforprecompilation.Thistellssprocketstoaddyour
engineassetswhenbin/rails assets:precompileistriggered.
Youcandefineassetsforprecompilationinengine.rb:
initializer "blorgh.assets.precompile" do |app|
app.config.assets.precompile += %w( admin.js admin.css )
end
Formoreinformation,readtheAssetPipelineguide.
6.6OtherGemDependencies
Gemdependenciesinsideanengineshouldbespecifiedinsidethe.gemspecfileattherootoftheengine.
Thereasonisthattheenginemaybeinstalledasagem.Ifdependenciesweretobespecifiedinside
theGemfile,thesewouldnotberecognizedbyatraditionalgeminstallandsotheywouldnotbeinstalled,
causingtheenginetomalfunction.
Tospecifyadependencythatshouldbeinstalledwiththeengineduringatraditionalgem install,specifyit
insidetheGem::Specificationblockinsidethe.gemspecfileintheengine:
s.add_dependency
"moo"
Tospecifyadependencythatshouldonlybeinstalledasadevelopmentdependencyoftheapplication,
specifyitlikethis:
s.add_development_dependency
"moo"
Bothkindsofdependencieswillbeinstalledwhenbundle installisruninsideoftheapplication.The
developmentdependenciesforthegemwillonlybeusedwhenthetestsfortheenginearerunning.
Notethatifyouwanttoimmediatelyrequiredependencieswhentheengineisrequired,youshouldrequire
thembeforetheengine'sinitialization.Forexample:
require 'other_engine/engine'
require
'yet_another_engine/engine'

module MyEngine
class Engine < ::Rails::Engine
end
end

Вам также может понравиться