IhavebeenthinkingabitrecentlyhowtomanagedependenciesandhowtostructureZendFrameworkbasedapplicationstomakethecodelesscoupled,moretestableandlessdependentontheglobalscope.
Idon'tmeantobenegativebutIamnottoohappyaboutthewebapplicationstructurethatmostarticlesandbookspresent.InZendFrameworkworldcontrollerseemstobetheplacewhenthingsgetdone.Controlleristheworkhorseandthisiswhereallthelogicseemstobeburied.ItalsoseemstomethatmodelinMVCisreducedtodatabaseintegrationbutthereisnoserviceslayerforsomereason.WhereeveryoulookyouwillseethesameexampleswithcontrollerdoalltheworkandmodelsbeingsimpleZend_Db_TableorZend_Db_Table_Rowinstances.Youwillnotseebusinesslogicfocusedclasses,ControllerorDBModel,that’sallyoucanchoosefrom.
TheproblemIseewiththissolutionisthatcontrollersarenotreusable.Controlleractioniscoupledtootightlywithwaytoomanythingstobereusable.YoucannotcallcontrolleractionfromacronoraRESTserver.Youcannoteasilyreusecontrolleractionfromaqueueprocessor.WhyBecauseitdependsonsession,request,cookies,response,viewsetc.Controllerhaswaytoomanythingsonitsmind,ifyouwanttoreuseityouwouldhavetosatisfyallitsdependencieswhichgetoutofcontroleasily.
Whatyoucanseeinthewildisevenworse.Peopleuse$_POSTandpassitaroundtotheviewormodels.Inresulteverythingdependsoneverythingandyouhaveatruespaghetticode.
HowtodecouplecomponentsinZendFrameworkapplications
ThefirstthingthatweshouldaimforistoreducetheamountofcodethatiscoupledwithZendFrameworkitselfandespeciallywiththerequestscope.WeshouldtrytocreatedecoupledservicesandPOPO(PlainOldPHPObjects)whichhandlebusinesslogic.Theyshouldnotbeawareofsessionnorrequest/response/cookiesetc.Theyshoulddependsolelyonwhatisprovidedinconstructorandmethodarguments.
Thelessclassdependsonthebetter.IfaclassdependsonacookieorDB,thatisalreadytoomuch,asitmeansthattheclassistightlycoupledwithboththepresentationanddatabase.Inmulti-layerarchitectureyoushouldnotallowlowerlayerstocallordependonhigherlayers.Youshouldalsonotallowdirectuseofbottomlayersbytoplayersetc.Youspendmoretimeontoachievethisseparationbutthebenefitsarehugeasyouendupwithasystemthatisstructuredandmaintainable.
AftergivingitsomethoughtIcameupwiththefollowingdiagramofhowdependenciesandscopescouldlooklikeinaZendFrameworkbasedapplication.
Therearealotofruleshererepresentedmostlybythedashedarrows.
Serviceisking
Inthisutopia-likediagramservicesarewherethejobgetsdone.ServicespresentasimpleAPItoperformallthecomplexoperationsbehindit.Itcanbeanything,forexample:
1
2
3
4
$userService->disable($user)
$captchaService->generate()
$purchaseService->completeOrder($order)
$cache->load($key)
Thepointisthatservicesarekeptawayfromthefollowing:
·request
·response
·session
·cookies
·view
·currentusercontext(besidepermissions)
·requestparameternames(associativearrayscontainingPOSTarenotcool)
Thismeansthatserviceclasshasawell-definedAPI.Itexposessimplemethodswithparametersthathavestructure(interfacesorclassesnotassociativearrays).
Italsomeansthatservicescandependoneachotherandontheexternallibrariesand/ordatabase.HereIwoulddivideservicestotwocategories.Databaseawareanddatabaseagnostic.Thedifferenceisimportantfromtestingandreusepointofview.Ifyourclassdependsontablesanddbstructureyouwon’tbeabletoreuseitortestinisolationeasily.Ifyourservicedependsonlyontheinterfacesinjectedintheconstructorthenyoucaneasilyreplacethemwithmocksorwiretheapplicationdifferentlyatruntime.
Lastimportantthingaboutservicesisthattheydonotkeepstate.Youshouldexpectaservicetodothejobandafterservicemethodreturnssomethingserviceshouldbereadytotakeanotherrequest.Thenitdoesnotmatterifyouwanttoprocessonepaymentoraqueueofmillionpayments.Yourpaymentservicetakesapaymentobjectasargumentandprocessesit.Itcansucceedorfailbutitshouldstillbereadytotakeanotherpaymentwithoutaftereffects.ItisalsoveryimportantifyouwanttoprovideRESTorSOAPAPIforyourapplication.HavingstatelessservicesyoucaneasilyinvokethemfromyourAPIlayerorcronsandyouwillnotneedanymodifications.
Servicesareclassesthatperformoperationsbasedonlyontheservicemethodarguments.Ifwecreateservicesthiswaytheycanbeeasilyinvokedfromcontrollers,plugins,commandline,queueorSoapentrypoints.Theyarealsomucheasiertotestasweknowtheyonlydependonthearguments(aconstructorinjectedservices/objects).
Databaseaware/agnosticservices,whyshouldwecare
OnthediagramIseparatethosetohighlightthatdependenciesondatabaseareheavy.Tomaketheseservicessimplerwedonotallowthemtodependon3rdpartycodedirectlyortalktoexternalservices.IfaDB-awareserviceneedstogenerateaPDFitshoulduseaPDFservice.ThencodeofDBawareservicestayssimplerandPDFservicetakesresponsibilityofPDFgeneration(whichisnottrivial).
Pleasenotethatthereisnodependencyintheoppositedirection.PayPalservicefacadedoesnotcallthedatabase-awareservice.Itshouldnot.IfPayPalneedstoexposeendpointforpaymentnotificationsitshouldbeacontrollercallingapaymentservicewhichinturnwouldaskPayPalservicetoprocessthenotification(extractreferenceidandstatus).
Whatcanmodelsdo
Startingfromthebottomweseemodelstalkingtothedatabase.Theyhavenoideawhatisarequest,theyarenotallowedtocallservicesnorcontrollers.Theycannotuseexternallibrariesnorsession,cookiesetc.Theyshouldnotbecoupledtorequestparameterseither.ModelsshouldbeprettydumbastheyallextendZend_Db_XXsoreusewillbelimited.Ithinkitisoktoletmodelsdependonothermodelsandletthemperformdifferentqueries.
Howtoincorporate3rdpartycode
InleftbottomcornerIplacedexternallibrariesasIthinkitisimportanttokeeptheminmind.
Everyapplicationusessome3rdpartycode.Ifwewanttokeepourapplicationsafefromexternalchangesweshouldseparateitwithawrapper(facade)service.Weneverknowwhatbugsarethereandifwewon'thavetoreplacetheimplementation.WedonotwanttopolluteourAPIwith3rdpartystandardsandexceptionseither.
Thebestwaywouldbetowrapexternallibrariesinformofsimplifiedservices.Theseservicessimplydelegateto3rdpartycodeorperformoperationstohidecomplexity.Interfacesthatexposeexactlyandonlywhatweneed.IfwewanttoresizeimagesletscreateafacadewithsimpleAPItoresizeimages.Ourapplicationdoesnothavetocreateanyexternalinstances,itisseparatedfromimplementationdetailsanditiseasierforustochangethebehaviorifnecessaryasit’sallintheimageresizingservice.
Servicesthatencapsulate3rdpartycodecomponentsshouldnotbeawareofrequest/response/session/databaseeither.Theyshouldfocusonbeingadaptersbetween3rdpartycodeandourserviceinterface.
Howtointegratewithexternalsystems
Inthesamelayerofservicesweprovideservicesthattalktoexternalsystems.Againtheyprovideafunctionofadaptersastheywouldnothaveanymajorbusinesslogic.
Howtosupportcrons,SOAP,commandlinetasksandQueues
Nowthatwealreadyhaveaserviceslayer.Itisveryeasyforustoaddaqueueprocessorthatpopulatesserviceargumentswithqueuedataandexecutesaservicemethod.WecanaddSAOPendpointwithmethodsthatmapSOAPargumentstoourservicemethodargumentsandweshouldbereadytogo.Cronsareequallyeasytoimplement.Wecanhavecomplextasksreusedacrossweb/soap/croninterfaces.
TheVCrelatedcode(viewandcontroller)
Ontheverytopofthediagramwehaveabunchofdifferentcomponents.WehaveFrontPlugins,Controllers,ActionHelpers,ViewHelpers,Forms,ViewsandPartialViews.Itisquitealotsolet’sbreakitdown.
FrontPlugins
Ithinkfrontpluginscannotescapecouplingtorequest,cookiesandsession.Frontpluginscanbeusedforstufflikeredirecting,geolocation,permissionsetc.Inmanycasesthesecomponentswilldependonrequest,sessionandcookies.Pluginscanalsobeinvolvedinpreparingusercontextandpermissions.
FrontpluginsdonotreallytouchtheviewsbuttheirlifecycleiscontrolledbyMVCandisquitecloselyrelatedtocontrollerssoIputtheminthesamegroupofviewrelatedclasses.
Controllers
ThereisnocontroversyabouttheroleofcontrollersanymoreIguess.Theyarethebridgebetweenuserinterfaceandservices.Controllersuserequest,response,cookiesandsessiontoassembleservicemethods'arguments.Controllersdonotcontainrealbusinesslogic.TheydonottalktoZend_Db_XXclasses,theydonotuse3rdpartycodeetc.
Controllersareusingformsandpopulatingviewparameters.Controlleractionscanalsouseactionhelpers.
Viewsandpartialviews
Ithinkthisareaissafeandsimpletoo.Viewsshouldonlyaccessvaluespopulatedbycontrollersandviewhelpers.Viewfilesshouldneveraccesssession,cookies,requestparametersnorinvokeservicemethod.Theyrendertheuserinterfaceandthisistheironlyresponsibility.
ActionhelpersandViewhelpers
Nowforthemorecontroversialcomponents,ActionhelpersandViewhelpers.Ithinkactionhelpersarecodethatshouldbereusedacrosscontrolleractionsbutstilldependsonrequest/response/session/cookies.WeshouldnothavealotofitbutIguessinanyapplicationtherewillbecomponentslikethissobettertoencapsulatethesimilaritythencopypaste.
Iamnotsurewhichcomponentswouldfitbestintoactionhelpers.ShouldtheybeallowedtouseformsOnthediagramIdidnotdrawthelinesuggestingthatactionhelperscannotuseforms.IamalsonotsureyetshouldactionhelpersbeallowedtosetviewparametersIthinktheyshouldnot,asitwouldbedifficulttokeeptrackofwhichactionhelpersetswhichvaluesandwhathastobecalledtomakesurethatviewhasallthevariablesitneeds.
ViewHelpersarethebiggestcontroversyheresolet’sspendsomemoretimeonit.
Viewhelpers,smartordumb
LookingatdifferentframeworksyouwillfindthatviewhelpersareusuallyjustsimpleHTMLpreprocessors.UsuallytheylookveryuglyhavingalotofHTMLgenerationinsideofthehelpermethod.
ButwheredoweputstandalonecomponentsinZendFrameworkMVCIfIwanttohaveauserloginbaratthetopofeverypageshouldIpopulatepartialviewvariablesininit()methodofbasecontrollerDoIhavetocallactionhelperorsetviewvariablesineveryaction
WhatifIwanttohavearefertoafriendwidgetonsomeofthepagesDoIhavetoaddvariablesintomyviewineveryactionWoulditnotbeeasiertouseaviewhelperinsteadandtreatitasacomponentWoulditnotbenicetosay“givemeafeaturedproducthere”inthelayoutorsomeviewandhavetherecommendedproductdetailsloadedforyou
ThisissueisprobablytheonlythingIamnotreallyhappyaboutwhenitcomestoMVCframeworkslikeZendFramework.Thereisnosolutiontomyproblem.IwantaminiMVCcomponentwithitsownlifecycle.Iwanttobeabletodecideintheviewshoulditbeevaluatedornot.Viewhelpersfitquitewellintothisusecase.Iassembleservicemethodarguments,callservice,getresponse,populatepartialviewandrenderit.Jobdone,featuredproductisonthepage.
MaybeitcouldbedoneonalayoutlevelsomehowIamnotsureifithastobeintheviewhelperbutitseemsflexible.
IamnotreallysureifthisisabestwaytodoitbutIthinkviewhelpersshouldbeallowedtodosimplecontroller-liketasksandaskservicesforsomedataifnecessarytorenderthewidget.Itmaybeacalltogetuser'sflickerpreferencestoshowtherightimagesinthesidebaroritcanbeaquerytogeneratemenutreebasedonpermissionsandpreferences.Ibelievethataslongastheserviceisdoingalltheheavyworkandhelperjustconsumesitthenweshouldbefine.
PuristswillsayitisaviolationofMVC,butMVCisnotawebpattern.ItwasdesignedfordesktopapplicationsandtheWebtwistonMVCisquitedifferentfromtheoriginalpattern.Whenyouthinkofitandlookatthediagramsyouwillseearrowsfromviewtomodel.Zendcallsmodelsdbtablesandrows,ourmodelsaremuchmoresophisticated,ourmodelsareservices.Somygutfeelingtellsmethatallowingviewhelperstoaccessservicemethodsisfine.
Usercontext
Thelastpieceofthepuzzleistheusercontextandtranslationwhichhavetobeavailableinalltheviewrelatedcode.Wewillmakedecisionsintheviewbasedonpermissions(showorhideoption).Wewillmakedecisionsbasedonuserprofile,preferencesetc.allthetimesohavingawelldefinedusercontextavailableinVCmakessense.
Ibelieveservicesshouldnotbeawareoftheusertokeepthemsimple,ifyouwanttoincorporatepermissionsintoservicelogicyouwillhavetocomeupwithanactorparameterthatispassedaroundtoeveryservicecallormakeitavailablesomehowviastaticscope.Butthenyoumakeyourservicesmorecoupledtotheuseranditmaybehardertotesttheminisolationorusethemfromuser-lessscopelikecronormessagequeue.
IthinkitwouldmakesensetoprepareusercontextinthefrontpluginorsomewherelikethisanddecoratetherequestobjecttomakeuserdataavailableacrossVClayer.Youruserobjectcouldhavewhateveriscommonlyused:
·permissions
·userId
·personaldetails
·preferences
Iwouldnotmaketheuserobjectresponsiblefortalkingtoservicesthough.IthinkitshouldbeadatacontainerforVClayertograbthebasicinformationfrom.
Discussiontime
WellthearticlegotabitlongerthanIintendedbutIhopeIdidnotboreyoutodeath:)Pleaseletmeknowwhatdoyouthinkandsharesomethoughtswithme.Iamespeciallyinterestedinyourexperienceswithservicesandindependentcomponents.