Post on 31-Jul-2020
Magento2DevelopmentQuickStartGuide
BuildbetterstoresbyextendingMagento
BrankoAjzele
BIRMINGHAM-MUMBAI
Magento2DevelopmentQuickStartGuideCopyright©2018PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishingoritsdealersanddistributors,willbeheldliableforanydamagescausedorallegedtohavebeencauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
CommissioningEditor:AmarabhaBanerjeeAcquisitionEditor:ReshmaRamanContentDevelopmentEditor:KirkDsouzaTechnicalEditor:VaibhavDwivediCopyEditor:SafisEditingProjectCoordinator:HardikBhindeProofreader:SafisEditingIndexer:AishwaryaGangawaneGraphics:AlishonMendonsaProductionCoordinator:DeepikaNaik
Firstpublished:September2018
Productionreference:1180918
PublishedbyPacktPublishingLtd.LiveryPlace35LiveryStreetBirminghamB32PB,UK.
ISBN978-1-78934-344-1
www.packtpub.com
mapt.io
Maptisanonlinedigitallibrarythatgivesyoufullaccesstoover5,000booksandvideos,aswellasindustryleadingtoolstohelpyouplanyourpersonaldevelopmentandadvanceyourcareer.Formoreinformation,pleasevisitourwebsite.
Whysubscribe?SpendlesstimelearningandmoretimecodingwithpracticaleBooksandVideosfromover4,000industryprofessionals
ImproveyourlearningwithSkillPlansbuiltespeciallyforyou
GetafreeeBookorvideoeverymonth
Maptisfullysearchable
Copyandpaste,print,andbookmarkcontent
Packt.comDidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.packt.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusatcustomercare@packtpub.comformoredetails.
Atwww.packt.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewsletters,andreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
Contributors
AbouttheauthorBrankoAjzeleisarespectedandhighlyaccomplishedsoftwaredeveloper,bookauthor,solutionspecialist,consultant,andteamleader.HecurrentlyworksforInteractiveWebSolutionsLtd(iWeb),whereheholdstheroleofseniordeveloperandisthedirectorofiWeb'sCroatiaoffice.
BrankoholdsseveralrespectedITcertifications,includingZendCertifiedPHPEngineer,MagentoCertifiedDeveloper,MagentoCertifiedDeveloperPlus,MagentoCertifiedSolutionSpecialist,Magento2CertifiedSolutionSpecialist,Magento2CertifiedProfessionalDeveloper,tomentionjustafew.
Hewascrownedthee-commerceDeveloperoftheYearbytheDigitalEntrepreneurAwardsinOctober2014forhisexcellentknowledgeandexpertiseine-commercedevelopment.
Specialthankstomysupportivewife,Ivana,forherunderstandingwhenItookquiteabitofourfamilytimeforthisendeavor.
Aboutthereviewer
Andrew"Pembo"PembertonisaCertifiedMagentoDeveloperwithover20years'experiencebuildingwebsites.HeisbasedinStoke-on-Trent,UKandstartedbuildingwebsitesfromtheyoungageof13.HehasadegreeincomputersciencefromStaffordshireUniversity.
AndrewisnowthedevelopmentdirectoratiWeb(basedinStafford,UK),which,forover20years,hascreatedindustry-leadingwebsitesandnowspecializesinlargescaleMagentosolutionsandPIM-basedprojectsforawiderangeofclients.
Outsideofhisdigitallife,Andrewenjoysspendingtimewithhisfamilyofpets,travelingwithhiswife,andbeinganavidgamer.
PacktissearchingforauthorslikeyouIfyou'reinterestedinbecominganauthorforPackt,pleasevisitauthors.packtpub.comandapplytoday.Wehaveworkedwiththousandsofdevelopersandtechprofessionals,justlikeyou,tohelpthemsharetheirinsightwiththeglobaltechcommunity.Youcanmakeageneralapplication,applyforaspecifichottopicthatwearerecruitinganauthorfor,orsubmityourownidea.
TableofContentsTitlePageCopyrightandCredits
Magento2DevelopmentQuickStartGuidePacktUpsell
Whysubscribe?Packt.com
ContributorsAbouttheauthorAboutthereviewerPacktissearchingforauthorslikeyou
PrefaceWhothisbookisforWhatthisbookcoversTogetthemostoutofthisbook
DownloadtheexamplecodefilesCodeinAction
ConventionsusedGetintouch
Reviews1. UnderstandingtheMagentoArchitecture
TechnicalrequirementsInstallingMagentoModesAreasRequestflowprocessingModules
CreatingtheminimalmoduleCacheDependencyinjection
ArgumentinjectionVirtualtypesProxiesFactories
PluginsThebeforepluginThearoundpluginTheafterplugin
EventsandobserversConsolecommandsCronjobsSummary
2. WorkingwithEntitiesTechnicalrequirementsUnderstandingtypesofmodels
CreatingasimplemodelMethodsworthmemorizing
WorkingwithsetupscriptsThe InstallSchemascriptThe UpgradeSchemascriptTheRecurringscriptThe InstallDatascriptThe UpgradeDatascriptTheRecurringDatascript
ExtendingentitiesCreatingextensionattributes
Summary3. UnderstandingWebAPIs
TechnicalrequirementsTypesofusersTypesofauthenticationTypesofAPIsUsingexistingwebAPIsCreatingcustomwebAPIsUnderstandingsearchcriteriaSummary
4. BuildingandDistributingExtensionsTechnicalrequirementsBuildingashippingextensionDistributingviaGitHubDistributingviaPackagistSummary
5. DevelopingforAdminTechnicalrequirementsUsingthelistingcomponentUsingtheformcomponentSummary
6. DevelopingforStorefrontTechnicalrequirementsSettinguptheplaygroundCallingandinitializingJScomponentsMeetRequireJSReplacingjQuerywidgetcomponentsExtendingjQuerywidgetcomponentsCreatingjQuerywidgetscomponentsCreatingUI/KnockoutJScomponentsExtendingUI/KnockoutJScomponentsSummary
7. CustomizingCatalogBehaviorTechnicalrequirementsCreatingthesizeguideCreatingthesamedaydeliveryFlaggingnewproductsSummary
8. CustomizingCheckoutExperiencesTechnicalrequirementsPassingdatatothecheckoutAddingordernotestothecheckoutSummary
9. CustomizingCustomerInteractionsTechnicalrequirementsUnderstandingthesectionmechanismAddingcontactpreferencestocustomeraccountsAddingcontactpreferencestothecheckoutSummary
OtherBooksYouMayEnjoyLeaveareview-letotherreadersknowwhatyouthink
PrefaceMagentoisapopularopensourcee-commerceplatformwritteninPHP.Itisusedprimarilyforbuildingwebshops,thoughitcaneasilybeusedforothertypesofwebsitesaswell.WiththehelpofitspowerfulwebAPI,wecanbuildrobustsolutionsthatsatisfymodern-dayapplicationrequirements.
Bytheendofthisbook,thereadershouldbefamiliarwithconfigurationfiles,models,collections,blocks,controllers,events,observers,plugins,UIcomponentsandotherbuildingelementsofMagento.
WhothisbookisforThisbookisintendedforPHPdevelopersgettingstartedwithMagentov2.xdevelopment.Thoughcompactintermsofpagenumbers,thebookcoversawiderangeoffunctionality,allowingthereadertomasterday-to-dayMagentoskillsinaclearandconciseway.NopreviousMagentoknowledgeisrequired.
WhatthisbookcoversChapter1,UnderstandingtheMagentoArchitecture,takesalookatsomeofthekeyMagentocomponents.WewillgothroughpluginsandeventobserversandlearnhowtheyprovideapowerfulwayofextendingMagento,eitherbychangingthebehaviorofexistingfunctionsorbyrunningsomefollow-upcodeinresponsetocertainevents.
Chapter2,WorkingwithEntities,demonstrateshowtodifferentiatebetweenthethreetypesofMagentomodels:non-persistable,persistablesimple,andpersistableEAV.Wewilltakealookatthesixdifferentsetupscriptsandhowtheyallowusagreatdealofflexibilityforschemaanddatamanagement.
Chapter3,UnderstandingWebAPI,showsthereaderhowtodifferentiatebetweentypesofwebAPIusers,authentication,andmethodsitprovides.WewillalsotakealookathoweasyitistocreateourownAPIswithjustafewlinesofXML.WewillseehowtheroutedefinitionallowsforeasybindingbetweenwhatarrivesviaHTTPrequestsandwhatisexecutedincode,respectingtheaccesslistpermissionsintheprocess.
Chapter4,BuildingandDistributingExtensions,discusseshowtocreateasimpleshippingmodule.Weshalltakealookathoweasyitistoaddspecificshippingcalculationsaspartofofflineshippingmethods.WewillthenpackagethismoduleanddistributeitviaPackagist.Thismakesiteasyfortheendconsumertouseourmodulewithjustafewsimpleconsolecommands.
Chapter5,DevelopingforAdmin,walksthereaderthroughbuildingtwoverydifferentscreensintheMagentoadminarea.Oneutilizesthelistingcomponent,whereastheotherutilizestheformcomponent.
Chapter6,DevelopingforStorefront,coversthebitsandpiecesinvolvedinstorefrontdevelopment,whichJScomponentsmakethemostchallengingpartof.Wewillunderstandhowtowritenewcomponents,aswellashowtooverrideorbypassexistingones–anessentialskillforanyMagentodeveloper,beitbackendorfrontend.
Chapter7,CustomizingCatalogBehavior,demonstratesbuildingthreedistinctivefunctionalities,allofwhichrelatetothecatalogpartofMagento.TheydemonstratehoweasilyMagentocanbeextendedwithnewfeatureswithoutreallyoverridinganyofthecorefiles.UsingpluginsandJScomponentsarejustsomeoftheapproacheswemighttake.
Chapter8,CustomizingCheckoutExperience,demonstrateswritingasmallbutfunctionalordernotesmodule.Thiswillallowustofamiliarizeourselveswithanimportantaspectofcustomizingthecheckoutexperience,thegistofwhichliesinunderstandingthecheckout_index_indexlayouthandle,theJavaScriptwindow.checkoutConfigobject,anduiComponent.
Chapter9,CustomizingCustomerInteractions,walksthereaderthroughbuildingasmallmodulethatallowsustogetagreaterinsightintoMagento'scustomerdataandsectionsmechanism.Wewilllearnhowtomanageandbuildasinglecomponent,whichwillgetusedbothonthecustomer'sMyAccountpage,aswellasatthecheckout.
TogetthemostoutofthisbookTogetthemostoutofthebook,thereaderisexpectedtohave:
AdegreeofPHPobject-orientedprogramming(OOP)knowledgeAbasicunderstandingofJavaScriptandXML
DownloadtheexamplecodefilesYoucandownloadtheexamplecodefilesforthisbookfromyouraccountatwww.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisitwww.packtpub.com/supportandregistertohavethefilesemaileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregisteratwww.packtpub.com.2. SelecttheSUPPORTtab.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchboxandfollowtheonscreen
instructions.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
ThecodebundleforthebookisalsohostedonGitHubathttps://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.Incasethere'sanupdatetothecode,itwillbeupdatedontheexistingGitHubrepository.
Wealsohaveothercodebundlesfromourrichcatalogofbooksandvideosavailableathttps://github.com/PacktPublishing.Checkthemout!
CodeinActionVisitthefollowinglinktocheckoutvideosofthecodebeingrun:
http://bit.ly/2D98D8q
ConventionsusedThereareanumberoftextconventionsusedthroughoutthisbook.
CodeInText:Indicatescodewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandles.Hereisanexample:"Thedefaultareaisthefrontend,asdefinedbythedefaultargumentundermodulestore/etc/di.xml."
Ablockofcodeissetasfollows:
constAREA_GLOBAL='global';
constAREA_FRONTEND='frontend';
constAREA_ADMINHTML='adminhtml';
constAREA_DOC='doc';
constAREA_CRONTAB='crontab';
constAREA_WEBAPI_REST='webapi_rest';
constAREA_WEBAPI_SOAP='webapi_soap';
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
constAREA_GLOBAL='global';
constAREA_FRONTEND='frontend';
constAREA_ADMINHTML='adminhtml';
constAREA_DOC='doc';
constAREA_CRONTAB='crontab';
constAREA_WEBAPI_REST='webapi_rest';
constAREA_WEBAPI_SOAP='webapi_soap';
Anycommand-lineinputoroutputiswrittenasfollows:
phpbin/magentosetup:install\
--db-host="/Applications/MAMP/tmp/mysql/mysql.sock"\
--db-name=magelicious\
Bold:Indicatesanewterm,animportantword,orwordsthatyouseeonscreen.Forexample,wordsinmenusordialogboxesappearinthetextlikethis.Hereisanexample:"Thetabelementofthefile,whichisusedtoprovideasidebarmenupresenceunderMagentoadminStores|Settings|Configuration,isaniceexample."
Warningsorimportantnotesappearlikethis.
Tipsandtricksappearlikethis.
GetintouchFeedbackfromourreadersisalwayswelcome.
Generalfeedback:Emailfeedback@packtpub.comandmentionthebooktitleinthesubjectofyourmessage.Ifyouhavequestionsaboutanyaspectofthisbook,pleaseemailusatquestions@packtpub.com.
Errata:Althoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyouhavefoundamistakeinthisbook,wewouldbegratefulifyouwouldreportthistous.Pleasevisitwww.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetails.
Piracy:IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,wewouldbegratefulifyouwouldprovideuswiththelocationaddressorwebsitename.Pleasecontactusatcopyright@packtpub.comwithalinktothematerial.
Ifyouareinterestedinbecominganauthor:Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,pleasevisitauthors.packtpub.com.
ReviewsPleaseleaveareview.Onceyouhavereadandusedthisbook,whynotleaveareviewonthesitethatyoupurchaseditfrom?Potentialreaderscanthenseeanduseyourunbiasedopiniontomakepurchasedecisions,weatPacktcanunderstandwhatyouthinkaboutourproducts,andourauthorscanseeyourfeedbackontheirbook.Thankyou!
FormoreinformationaboutPackt,pleasevisitpacktpub.com.
UnderstandingtheMagentoArchitectureBuildingwebshopsisachallengingandtediousjob,andevenmoresoifaplatformyouareworkingonislimitedviafeatures,extensibility,andtheoverallecosystemitprovides.Choosingtherightplatformcanoftenmakethedifferencebetweenaproject'ssuccessorfailure.Theabundanceofavailablee-commercesoftware,fromSaaStoself-hostedsolutions,doesnotreallymakeitaneasychoice.
TheMagentoe-commerceplatformhasbeenaroundforover10yearsnow.WithitsfirststablereleasedatingbacktoMarch2008,itimmediatelycaughttheattentionofdevelopersasanextensibleandfeature-richopensourceplatform.Overtime,Magentoestablisheditselfasnotjustastunningtechnicalandfeature-richplatform,butasarobustecosystemaswell.Byallowingdeveloperstovalidatetheirreal-worldskillsthroughtheMagentocertificationprogram,certainstandardshavebeenputintoeffect,makingiteasierformerchantstobetterrecognizetheirsolutionpartners.Trainingcourseshavebeenfurtherprovidedforotherrolesine-commercebusinessaswell,suchasmerchants,marketers,systemadministrators,andbusinessanalysts.
Inthischapter,wewilltakealookatsomeofthekeymust-knowsaboutMagento:
InstallingMagentoModesAreasRequestflowprocessingModulesCacheDependencyinjectionPluginsEventsandobserversConsolecommandsCronjobs
Tokeepthingscompactaswemoveforward,let'sassumethefollowingthroughoutthisbook:
Weareworkingonthemagelicious.locprojectWearereferringtoourprojectrootdirectoryas<PROJECT_DIR>Wearereferringtothe<PROJECT_DIR>/app/code/Mageliciousdirectoryas<MAGELICIOUS_DIR>
WearereferringtoMagento'svendor/magentodirectoryas<MAGENTO_DIR>WehavearunningLAMP/MAMP/WAMPstack(Apache,MySQL,PHP)thatiscompliantwithMagento'srequirementsWehaveaComposerpackagemanagerinstalledWehaveaccesstocrontab(Linux,MacOS)orTaskScheduler(Windows)
AMPPSisaneasytouse,allinoneLAMP/MAMP/WAMPsoftwarestackfromSoftaculous,whichenablesApache,MySQL,andPHP.WithAMPPS,youcaneveninstallMagento2.xbytheclickofabutton,whichmeansitcomesloadedwithalltherightPHPextensions.Whileitisn'tsuitedforproductionpurposes,itcomesinhandyforquicklykickingthedevelopmentenvironment.Seehttp://www.ampps.com/formoreinformation.Consultthedevdocs(https://devdocs.magento.com)forMagentotechnologystackrequirements.
TechnicalrequirementsYouwillneedtohavebasicknowledgeofPHP,OOP,JavaScript,andXML.YouwillalsoneedApache,MySQL,andAMPPSinstalledonyoursystemtoexecutethecodes.
ThecodefilesofthischaptercanbefoundonGitHub:https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
CheckoutthefollowingvideotoseetheCodeinAction:
http://bit.ly/2D8kOlF.
InstallingMagentoTheMagentoplatformcomesintwoflavors:
MagentoOpenSource:Thefreeversion,targetingsmallbusinessesMagentoCommerce:Thecommercialversion,targetingsmall,medium,orenterprisebusinesses
ThedifferencebetweenthetwocomesmainlyintheformofextramodulesthatwereaddedtotheCommerceversion,whereasallthecodingconceptsandcorefeaturesremainthesame.ItgoestosaythatanyknowledgeweobtainthroughfollowingMagentoOpenSourceexamplesisfullyapplicabletoanyoneworkingonMagentoCommerce.
ThereareseveralwaysthatwecanobtainsourcefilesforMagentoOpenSource:
Sourcefilearchive(.zip,.tar.gz,.tar.bz2),availableathttps://magento.comGitrepository,availableathttps://github.com/magento/magento2Composerrepository,availableathttps://repo.magento.com
ObtainingsourcefilesviaaCLIfromthecomposerrepositoryisourpreferredmethod.Assumingwearewithintheempty<PROJECT_DIR>directory,wecankickoffthisprocessviathefollowingcommand:
composercreate-project--repository-url=https://repo.magento.com/magento/project-community-edition.
Thedot(.)attheendofthiscommandthistellsthecomposertopullthefilesintoacurrentdirectory.
OncetheComposerprocessisfinished,wecanstartinstallingMagento.TherearetwowayswecaninstallMagento:
ViatheWebSetupWizard:Thegraphical,browser-basedprocessViathecommandline:Thecommand-line-basedprocess
KnowinghowtoinstallMagentoviathecommandlineisanessentialskillinday-to-daydevelopment,asthemajorityofdevelopmentrequiresthedeveloper
totacklevariousbin/magentocommands—nottomentionthecommandlineapproachissomewhatfasterandeasilyscripted.
Let'sinstallMagentowiththebuilt-inphpbin/magentosetup:installcommandandafewoftherequiredinstallationoptionsasfollows:
phpbin/magentosetup:install\
--db-host="/Applications/MAMP/tmp/mysql/mysql.sock"\
--db-name=magelicious\
--db-user=root
--db-password=root\
--admin-firstname=John\
--admin-lastname=Doe\
--admin-email=john@magelicious.loc\
--admin-user=john\
--admin-password=jrdJ%0i9a69n
Aftertheprecedingcommandhasbeenexecuted,weshouldbegintoseeconsoleprogress,startingwithsomethinglikethefollowing:
StartingMagentoinstallation:
Filepermissionscheck...
[Progress:1/513]
Requiredextensionscheck...
[Progress:2/513]
EnablingMaintenanceMode...
[Progress:3/513]
Installingdeploymentconfiguration...
[Progress:4/513]
Installingdatabaseschema:
Schemacreation/updates:
Module'Magento_Store':
[Progress:5/513]
Whileitmighttakeuptoafewminutes,asuccessfulinstallationshouldendwithamessagethat'ssimilartothefollowing:
[Progress:508/513]
Installingadminuser...
[Progress:509/513]
Cachesclearing:
Cacheclearedsuccessfully
[Progress:510/513]
DisablingMaintenanceMode:
[Progress:511/513]
Postinstallationfilepermissionscheck...
Forsecurity,removewritepermissionsfromthesedirectories:'/Users/branko/Projects/magelicious/app/etc'
[Progress:512/513]
Writeinstallationdate...
[Progress:513/513]
[SUCCESS]:Magentoinstallationcomplete.
[SUCCESS]:MagentoAdminURI:/admin_mxq00c
Nothingtoimport.
Rightafterinstallation,ourfirststepshouldbetosetMagentotodevelopermodebyusingthefollowingcommand:
phpbin/magentodeploy:mode:setdeveloper
WewilltakeacloserlookatMagentomodessoon;fornow,thisistobetakenasis.
MagentoautomaticallyassignsanadminURLduringconsoleinstallation,unlessexplicitlyspecifiedthroughtheinstallcommandviathe--backend-frontnameoption.Outofalltheinstallationoptionslisted,onlythefollowingareactuallyrequired:--admin-firstname,--admin-lastname,--admin-email,--admin-user,and--admin-password.ItisworthtakingsometimetoreadthroughtheofficialMagentodocumentation(https://devdocs.magento.com)andlookingatwhattherestoftheinstallationoptionshavetooffer.
IfallwentwellduringtheMagentoinstallation,weshouldbeabletoopenthestorefrontandadmininourbrowser.
ModesModesplayacrucialroleinMagento'sdevelopmentanddeploymentprocesses.Theyarehandledbythedeploymodule,whichcanbefoundunderthe<MAGENTO_DIR>/module-deploydirectory.
Thebuilt-inphpbin/magentocommandprovidesuswiththefollowingdeploycommands:
deploy
deploy:mode:setSetapplicationmode.
deploy:mode:showDisplayscurrentapplicationmode.
Wealreadyusedthedeploy:mode:setdevelopercommandtoswitchfromdefaulttodevelopermode.
Magentodifferentiatesbetweenfollowingthreemodes:
default:Thedefaultafter-installmode:NotoptimizedforproductionSymlinkstostaticviewfilesarepublishedtothepub/staticdirectoryErrorsandexceptionsarenotshowntotheuser,astheyareloggedtothefilesystemShouldavoidusingit
developer:Fordevelopmentsystemsonly:Symlinkstostaticviewfilesarepublishedtothepub/staticdirectoryProvidesverboseloggingEnablesautomaticcodecompilationEnablesenhanceddebuggingSlowestperformance
production:Forproductionsystems:Errorsandexceptionsarenotshowntotheuser,astheyareloggedtothefilesystemStaticviewfilesarenotmaterialized,astheyareservedfromthecacheonlyAutomaticcodefilecompilationisdisabled,asneworupdatedfilesarenotwrittentothefilesystemEnablinganddisablingthecachetypesisnotpossiblefromthe
MagentoadminFastestperformance
Carefullybalancingdevelopermodewithsomeofthecachetypesbeingenabled/disabledcanprovideoptimalperformanceduringdevelopment.
AreasTheareaisalogicalcomponentthatorganizescodeforoptimizedrequestprocessing.Whilethemajorityofthetimewedon'treallyhavetocodeanythingspecificregardingareas,understandingthemiskeytounderstandingMagento.
TheMagento\Framework\App\AreaclassAREA_*constantshintatthefollowingareas:
constAREA_GLOBAL='global';
constAREA_FRONTEND='frontend';
constAREA_ADMINHTML='adminhtml';
constAREA_DOC='doc';
constAREA_CRONTAB='crontab';
constAREA_WEBAPI_REST='webapi_rest';
constAREA_WEBAPI_SOAP='webapi_soap';
Bydoingalookupforthe<argumentname="areas"stringacrossallofthe<MAGENTO_DIR>di.xmlfiles,wecanseethatfiveoftheseareashavebeenexplicitlyaddedtotheareasargumentoftheMagento\Framework\App\AreaListclass:
adminhtmlvia<MAGENTOI_DIR>/module-backend/etc/di.xmlwebapi_restvia<MAGENTOI_DIR>/module-webapi/etc/di.xmlwebapi_soapvia<MAGENTOI_DIR>/magento/module-webapi/etc/di.xmlfrontendvia<MAGENTOI_DIR>/magento/module-store/etc/di.xmlcrontabvia<MAGENTOI_DIR>/magento/module-cron/etc/di.xml
Thedefaultareaisfrontend,asdefinedbythedefaultargumentundermodule-store/etc/di.xml.Theglobalareaisusedasafallbackforfilesthatareabsentintheadminhtmlandfrontendareas.
Let'stakeacloserlookatthe<MAGENTO_DIR>/module-webapi/etc/di.xmlfile:
<typename="Magento\Framework\App\AreaList">
<arguments>
<argumentname="areas"xsi:type="array">
<itemname="webapi_rest"xsi:type="array">
<itemname="frontName"xsi:type="string">rest</item>
</item>
<itemname="webapi_soap"xsi:type="array">
<itemname="frontName"xsi:type="string">soap</item>
</item>
</argument>
</arguments>
</type>
ThefrontNameiswhatsometimesappearsatthefrontoftheURL,whereastheareanameisusedinternallytorefertotheareainconfigurationfiles.DifferentareasdefinedbyMagentocancontaindifferentcodeforprocessingURLsandrequests.ThisallowsMagentotoloadonlythedependentcodeforthespecifiedarea.
Whendevelopingmodules,wedefinewhichresourcesarevisibleandaccessibleinagivenarea.Thisway,wegettocontrolthespecificareabehaviorifneeded.Anexampleofonesuchbehaviormightbethedefinitionoftheeventobserverunderthefrontendareaforcustomer_save_afterevent.Thisobserverwouldonlytriggeroncustomersaveoperationsthataretriggeredfromthestorefront,whichusuallyindicatesacustomerregisteraction.Theadminhtmlareaoperations,suchasMagentoadminmanuallycreatingacustomer,wouldfailtotriggerthisobserver,asitwasdefinedunderthefrontendarea.
Onoccasion,wemightneedtorunsomecodethatonlyexecutesundercertainareas.Insuchcases,emulationhelpsusemulateanystoreprogrammatically.TheMagento\Store\Model\App\EmulationclassprovidesthestartEnvironmentEmulationandstopEnvironmentEmulationmethods,whichwecanuseforthispurpose,asperthefollowingpartialexample:
protected$storeRepository;
protected$emulation;
publicfunction__construct(
\Magento\Store\Api\StoreRepositoryInterface$storeRepository,
\Magento\Store\Model\App\Emulation$emulation
){
$this->storeRepository=$storeRepository;
$this->emulation=$emulation;
}
publicfunctiontest(){
$store=$this->storeRepository->get('store-to-emulate');
$this->emulation->startEnvironmentEmulation(
$store->getId(),
\Magento\Framework\App\Area::AREA_FRONTEND
);
//Codetoexecuteinemulatedenvironment
$this->emulation->stopEnvironmentEmulation();
}
Whileitisnotacommonthingtodo,wecanfurtherregisternewareasourselves.Thisiseasilydonebyusingthemodule'sdi.xml.
RequestflowprocessingURLsinMagentohavetheformatof<AreaFrontName>/<VendorName>/<ModuleName>/<ControllerName>/<ActionName>,butthisdoesnotmeanthatweactuallyusethearea,vendor,ormodulenameintheURLanytimewewishtoaccessacertaincontroller.Forexample,theareaforarequestisdefinedbythefirstrequestpathsegment,suchasadminforadminhtmlarea,andnoneforfrontendarea.
WeusetherouterclasstoassignaURLtoacorrespondingcontrolleranditsaction.Therouter'smatchmethodfindsamatchingcontroller,whichisdeterminedbyanincomingrequest.
Conceptually,creatinganewrouterisassimpleasdoingthefollowing:
1. InjectthenewitemundertherouterListargumentoftheMagento\Framework\App\RouterListtypeviathedi.xmlfile.
2. Createarouterfile(byusingthematchmethod,whichimplements\Magento\Framework\App\RouterInterface).
3. Returnaninstanceof\Magento\Framework\App\ActionInterface.
Bydoingalookupforthename="routerList"stringacrossallofthe<MAGENTO_DIR>di.xmlfiles,wecanseethefollowingrouterdefinitions:
Magento\Robots\Controller\Router(robots)
Magento\Cms\Controller\Router(cms)
Magento\UrlRewrite\Controller\Router(urlrewrite)
Magento\Framework\App\Router\Base(standard)
Magento\Framework\App\Router\DefaultRouter(default)
Magento\Backend\App\Router(admin)
Let'stakeacloserlookattherobotsrouterunder<MAGENTO_DIR>/module-robots.etc/frontend/di.xmlinjectsthenewitemundertherouterListargumentasfollows:
<typename="Magento\Framework\App\RouterList">
<arguments>
<argumentname="routerList"xsi:type="array">
<itemname="robots"xsi:type="array">
<itemname="class"xsi:type="string">Magento\Robots\Controller\Router</item>
<itemname="disable"xsi:type="boolean">false</item>
<itemname="sortOrder"xsi:type="string">10</item>
</item>
</argument>
</arguments>
</type>
TheMagento\Robots\Controller\Routerclasshasbeenfurtherdefinedasperthefollowingpartialextract:
classRouterimplements\Magento\Framework\App\RouterInterface{
//Magento\Framework\App\ActionFactory
private$actionFactory;
//Magento\Framework\App\Router\ActionList
private$actionList;
//Magento\Framework\App\Route\ConfigInterface
private$routeConfig;
publicfunctionmatch(\Magento\Framework\App\RequestInterface$request){
$identifier=trim($request->getPathInfo(),'/');
if($identifier!=='robots.txt'){
returnnull;
}
$modules=$this->routeConfig->getModulesByFrontName('robots');
if(empty($modules)){
returnnull;
}
$actionClassName=$this->actionList->get($modules[0],null,'index','index');
$actionInstance=$this->actionFactory->create($actionClassName);
return$actionInstance;
}
}
Thematchmethodchecksiftherobots.txtfilewasrequestedandreturnstheinstanceofthematched\Magento\Framework\App\ActionInterfacetype.Byfollowingthissimpleimplementation,wecaneasilycreatetherouteofourown.
Conceptually,creatinganewcontrollerisassimpleasdoingthefollowing:
1. Registerarouteviarouter.xml.2. Createanabstractcontrollerfile(asanabstractclass,whichextends
\Magento\Framework\App\Action\Action).
3. Createanactioncontrollerfile(whichextendsthemaincontrollerfilewiththeexecutemethod,andimplements\Magento\Framework\App\ActionInterface).
4. Returnaninstanceof\Magento\Framework\Controller\ResultInterface.
Theseparationofthecontrollerintothemainandactioncontrollerfilesisnotatechnicalrequirement,butratherarecommendedorganizationalone.Magentodoesthisacrossthe
majorityofitsmodules.
Bydoingalookupforthe<routestringacrossthe<MAGENTO_DIR>routes.xmlfiles,wecanseethatMagentouseshundredsofroutedefinitions,whicharespreadacrossitsmodules.Eachrouterepresentsonecontroller.
Let'stakeacloserlookatoneofMagento'scontrollers,<MAGENTO_DIR>/module-customer,whichmapstothehttp://magelicious.loc/customer/address/formURL.Therouteitselfisregisteredviafrontend/di.xmlunderthestandardrouterwithacustomerIDandacustomerfrontName,asfollows:
<routerid="standard">
<routeid="customer"frontName="customer">
<modulename="Magento_Customer"/>
</route>
</router>
TheabstractcontrollerfileController/Address.phpisdefinedpartiallyasfollows:
abstractclassAddressextends\Magento\Framework\App\Action\Action{
//Therestofthecode...
}
Theabstractcontrolleriswherewewanttoaddfunctionalityanddependenciesthataresharedacrossallofthechildactioncontrollers.
Wecanfurtherseethreedifferentactioncontrollersdefinedwithinthesubdirectorywhichhasthesamenameastheabstractclass.TheController/Addressdirectorycontainssixactioncontrollers:Delete.php,Edit.php,Form.php,FormPost.php,Index.php,andNewAction.php.Let'stakeacloserlookatthefollowingpartialForm.phpfile'scontent:
classFormextends\Magento\Customer\Controller\Address{
publicfunctionexecute(){
/**@var\Magento\Framework\View\Result\Page$resultPage*/
$resultPage=$this->resultPageFactory->create();
$navigationBlock=$resultPage->getLayout()->getBlock('customer_account_navigation');
if($navigationBlock){
$navigationBlock->setActive('customer/address');
}
return$resultPage;
}
}
TheexamplehereusesthecreatemethodoftheinjectedMagento\Framework\View\Result\PageFactorytypetocreateanewpageresult.Thevarioustypesofcontrollerresultscanbefoundwithinthe<MAGENTO_DIR>/framework
directory:
Magento\Framework\Controller\Result\Json
Magento\Framework\Controller\Result\Raw
Magento\Framework\Controller\Result\Redirect
Magento\Framework\Controller\Result\Forward
Magento\Framework\View\Result\Layout
Magento\Framework\View\Result\Page
Wecantaketheunifiedwayofcreatingresultinstancesbyusingthecreatemethodof\Magento\Framework\Controller\ResultFactory.TheResultFactorydefinestheTYPE_*constantforeachofthementionedcontrollerresulttypes:
constTYPE_JSON='json';
constTYPE_RAW='raw';
constTYPE_REDIRECT='redirect';
constTYPE_FORWARD='forward';
constTYPE_LAYOUT='layout';
constTYPE_PAGE='page';
Keepingtheseconstantsinmind,wecaneasilywriteouractioncontrollercodeasfollows:
$resultRedirect=$this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
$resultRedirect->setPath('adminhtml/*/index');
return$resultRedirect;
Aquicklookupforthe$this->resultFactory->createstring,acrosstheentire<MAGENTO_DIR>directory,cangiveuslotsofexamplesofhowtousetheResultFactoryforourowncode.
ModulesThetop-levelMagentostructureisrathersimple.Whenwestripaway(seemingly)non-relevantfilessuchaslicenses,samplefiles,andchangelogs,whatremainslooksmuchlikethefollowing:
app/
code/
design/
etc/
config.php
env.php
bin/
composer.json
composer.lock
dev/
generated/
index.php
lib/
phpserver/
pub/
static/
adminhtml/
frontend/
setup/
update/
var/
cache/
log/
page_cache/
view_preprocessed/
pub/
static/
adminhtml/
frontend/
vendor/
composer/
magento/
symfony/
Theapp/code/<VendorName>/<ModuleName>directory,<MAGELICIOUS_DIR>forshort,iswhereourcustomcodewillreside.
Whendevelopermodeisenabled,wecanmanuallycleanthecache,compilation,andstaticfilesviatherm-rfvar/cache/*&&rm-rfvar/page_cache/*&&rm-rfvar/view_preprocessed/*&&rm-rfgenerated/*&&rm-rfpub/static/*command.Underlimitedusecases,thiscanprovideafasterdevelopmentworkflow.
Thevendor/magentodirectory,<MAGENTO_DIR>forshort,iswhereMagentosourcecoderesides,asperthefollowingpartiallisting:
vendor/
magento/
composer/
framework/
language-de_de/
language-en_us/
magento-composer-installer/
magento2-base/
module-catalog/
module-checkout/
theme-adminhtml-backend/
theme-frontend-blank/
theme-frontend-luma/
Theindividualmoduledirectoryiswherethingsgetinteresting.Let'stakeaquicklookatthestructureofoneofthesimplerMagentomodules,<MAGENTO_DIR>/module-contact:
Block/
Controller/
etc/
Helper/
i18n/
Model/
Test/
view/
composer.json
LICENSE.txt
LICENSE_AFL.txt
README.md
registration.php
Thisisbynomeansthefinalstructureoftheindividualmodule.Thereareotherdirectoriesthemodulecandefine,aswewillseeaswemoveforwardthroughoutthisbook.
CreatingtheminimalmoduleLet'screatethemostminimalmodulethereisinMagento.OurmodulewillbecalledCoreanditwillbelongtotheMageliciousvendor.Theformulafordeterminingthedirectoryofcustommodulesisapp/code/<VendorName>/<ModuleName>,orinourcase<MAGELICIOUS_DIR>/Core.
Westartoffbycreatingthe<MAGELICIOUS_DIR>/Core/registration.phpfilewiththefollowingcontent:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Magelicious_Core',
__DIR__
);
Theregistration.phpfileisessentiallytheentrypointofourmodule.TheregistermethodoftheMagento\Framework\Component\ComponentRegistrarclassprovidestheabilitytostaticallyregistercomponents,whereasacomponentcanbemorethanjustamodule,asdefinedviathefollowingconstants:
Magento\Framework\Component\ComponentRegistrar::MODULE
Magento\Framework\Component\ComponentRegistrar::LIBRARY
Magento\Framework\Component\ComponentRegistrar::THEME
Magento\Framework\Component\ComponentRegistrar::LANGUAGE
Next,wewillcreatethe<MAGELICIOUS_DIR>/Core/etc/module.xmlfilewiththefollowingcontent:
<config>
<modulename="Magelicious_Core"setup_version="1.0.0">
<sequence>
<modulename="Magento_Store"/>
<modulename="Magento_Backend"/>
<modulename="Magento_Config"/>
</sequence>
</module>
</config>
Thenameandsetup_versionattributesofamoduleelementareconsideredrequired.Thesequence,ontheotherhand,isoptional.WeuseittodefineanypotentialdependenciesaroundotherMagentomodules.
Finally,weaddcomposer.jsonwiththefollowingcontent:
{
"name":"magelicious/module-core",
"description":"Thecoremodule",
"require":{
"php":"^7.0.0"
},
"type":"magento2-module",
"version":"1.0.0",
"license":[
"OSL-3.0",
"AFL-3.0"
],
"autoload":{
"files":[
"registration.php"
],
"psr-4":{
"Magelicious\\Core\\":""
}
}
}
Magentosupportsthefollowingcomposer.jsontypes:
magento2-moduleformodulesmagento2-themeforthemesmagento2-languageforlanguagepackagesmagento2-componentforgeneralextensionsthatdonotfitanyoftheothertypes
Thoughcomposer.jsonisnotrequiredforourcustommoduletobeseenbyMagento,itisrecommendedtoaddittoanycomponentwearebuilding.
Youcantriggermoduleinstallationbyrunningthephpbin/magentomodule:enableMagelicious_Corecommand,likeso:
$phpbin/magentomodule:enableMagelicious_Core
Thefollowingmoduleshavebeenenabled:
-Magelicious_Core
Tomakesurethattheenabledmodulesareproperlyregistered,run'setup:upgrade'.
Cacheclearedsuccessfully.
Generatedclassesclearedsuccessfully.Pleaserunthe'setup:di:compile'commandtogenerateclasses.
Info:Somemodulesmightrequirestaticviewfilestobecleared.Todothis,run'module:enable'withthe--clear-static-contentoptiontoclearthem.
Youcanrunthephpbin/magentosetup:upgradecommandtotriggeranyinstalland/orupdatescriptsthatneedtobetriggered:
Cacheclearedsuccessfully
Filesystemcleanup:
generated/code/Composer
generated/code/Magento
generated/code/Symfony
Updatingmodules:
Schemacreation/updates:
Module'Magento_Store':
...
Module'Magento_CmsUrlRewrite':
Module'Magelicious_Core':
Module'Magento_ConfigurableImportExport':
...
Nothingtoimport.
Thisfinishesourmoduleinstallation.
Creatingthe<VendorName>/Coremoduleisoftenagoodpracticewhenworkingonaprojectwithlotsofcustom<VendorName>modules.Usedcarefully,theCoremodulecanprovidecommonbitsthataresharedacrossseveralothermodules.Thetabelementofthesystem.xmlfile,whichisusedtoprovideasidebarmenupresenceunderMagento'sadminStores|Settings|Configuration,isaniceexample.Similarly,wecanuseittoprovidetop-levelaccessresourcesforothermodulestouse.
Toconfirmourmodulewasinstalledcorrectly,performthefollowing:
Checkthe<PROJECT_DIR>/app/etc/config.phpfileforthe'Magelicious_Core'=>1entryCheckthesetup_moduletablefortheMagelicious_Core1.0.01.0.0entry
Atthemoment,ourmoduledoesabsolutelynothing,asidefromjustsittingthere.However,thesefewsimplestepsarethebasisforusmovingforwardwithMagentodevelopment,becausethemajorityofthingsinMagentoaredoneviaamodule,alongsideothertypesofcomponents,whichwehavealreadymentioned.
CacheMagentomakesextensiveuseofcaching.TheSystem|Tools|CacheManagementsectionenablesustoEnable|Disable|Refreshthecachefromthecomfortofthegraphicalinterface.Duringdevelopment,theuseoftheconsoleismoreconvenientandfaster.
Thefollowingcache-relatedcommandsaresupported:
cache
cache:cleanCleanscachetype(s)
cache:disableDisablescachetype(s)
cache:enableEnablescachetype(s)
cache:flushFlushescachestorageusedbycachetype(s)
cache:statusCheckscachestatus
Outofthebox,MagentoOpenSourcecomeswith14differentcachetypes.Wecaneasilygetthestatusofeachcachetypebyrunningthephpbin/magentocache:statuscommand,whichgivesthefollowingoutput:
Currentstatus:
config:0
layout:0
block_html:0
collections:0
reflection:0
db_ddl:0
eav:0
customer_notification:0
the_custom_cache:1
config_integration:0
config_integration_api:0
full_page:0
translate:0
config_webservice:0
Wecanusetheenable|disable|cleancachecommandstoimpactoneormorecachetypesatonce.
Disabledcachetypesarenotcleaned.Usethecache:flushcommandwithcare,asflushingthecachetypepurgestheentirecachestorage.This,inturn,mightaffectotherapplicationsthatareusingthesamestorage.
Ifbuilt-incachetypesarenotenough,wecanalwayscreateourown.
CreatinganewcachetypeinMagentoisaseasyasdoingthefollowing:
Createthe<MAGELICIOUS_DIR>/Core/etc/cache.xmlfilewiththefollowingcontent:
<config>
<typename="the_custom_cache"translate="label,description"instance="Magelicious\Core\Model\Cache\TheCustomCache">
<label>TheCustomCache</label>
<description>Ourcustomcachetype</description>
</type>
</config>
Createthe<MAGELICIOUS_DIR>/Core/Model/Cache/TheCustomCache.phpfilewiththefollowingcontent:
classTheCustomCacheextends\Magento\Framework\Cache\Frontend\Decorator\TagScope{
constTYPE_IDENTIFIER='the_custom_cache';
constCACHE_TAG='THE_CUSTOM_CACHE';
publicfunction__construct(\Magento\Framework\App\Cache\Type\FrontendPool$cacheFrontendPool){
parent::__construct($cacheFrontendPool->get(self::TYPE_IDENTIFIER),self::CACHE_TAG);
}
}
TheTYPE_IDENTIFIERisusedinternallyasacachetypecodethatisuniqueamongallcachetypes.TheCACHE_TAGisacachetagthat'susedtodistinguishthecachetypefromallothercaches.Runningcache:statusshouldnowshowourcustomcachetypeonthelist.
WecanusetheinstanceofMagento\Framework\App\Cache\TypeListInterfacetoinvalidatethecache,asfollows:
$this->typeList->invalidate(\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER);
WecanusetheinstanceofMagento\Framework\App\Cache\Manager$cacheManagertoprogrammaticallyexecutethesameenable|disable|cleanoperationsasperthefollowingexample:
$cacheManager->setEnabled(
[\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER],
true
);
$cacheManager->clean([\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER]);
$cacheManager->flush([\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER]);
Savingdatatocacherequiresserialization,asperthefollowingexample:
//\Magento\Framework\Config\CacheInterface$cache
//\Magento\Framework\Serialize\SerializerInterface$serializer
//\Magento\Framework\App\Cache\StateInterface$cacheState
$isCacheEnabled=$cacheState->isEnabled(\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER);
$cacheId='some-unique-identifier';
if($isCacheEnabled){
$cache->save(
$serializer->serialize('some-data'),
$cacheId,
[
\Magelicious\Core\Model\Cache\TheCustomCache::CACHE_TAG
]
);
}
Readingdatafromthecacheisaseasyasperthefollowingexample:
if($cacheData=$this->cache->load($cacheId);){
$someData=$this->getSerializer()->unserialize($cacheData);
}else{
$someData=$this->fetchSomeData();
}
DependencyinjectionDependencyinjectionhasbecomeadefactostandardofmodern-daysoftware.Magentomakesheavyuseofthistechnique,basedonmappingsfoundindi.xmlfiles.TheworkloadofMagento'sdependencyinjectionishandledbytheMagento\Framework\ObjectManager\ObjectManagerinstance,whichimplementsthelightweightMagento\Framework\ObjectManagerInterface.
Thedi.xmlfileconfigurestheobjectmanager,tellingithowtohandlethefollowing:
ArgumentinjectionVirtualtypesProxiesFactoriesPlugins
Thesefeaturesallowforagreatdegreeofflexibilityandextensibility,aswewillsoonsee.
Everymodulecanhaveaglobalandarea-specificdi.xmlfile.
Magentoloadsconfigurationfilesinthefollowingorder:
Initial(app/etc/di.xml)Global(<ModuleDir>/etc/di.xml)Area-specific(<ModuleDir>/etc/<area>/di.xml)
WhenMagentoreadsalloftheseconfigurationfiles,itmergesthemalltogetherbyappendingallnodes.
ArgumentinjectionArgumentinjectionisdoneviapreferenceandtypedefinitionswithinthedi.xml.
Byperformingalookupforthe<preferencestringacrosstheentire<MAGENTO_DIR>directory'sdi.xmlfiles,wecanseethatMagentouseshundredsofpreferencedefinitions,spreadacrossthemajorityofitsmodules.
Let'stakeaquicklookatoneofthe__constructmethod,ofthetypeMagento\Eav\Model\Attribute\Data\AbstractData:
publicfunction__construct(
\Magento\Framework\Stdlib\DateTime\TimezoneInterface$localeDate,
\Psr\Log\LoggerInterface$logger,
\Magento\Framework\Locale\ResolverInterface$localeResolver
){
$this->_localeDate=$localeDate;
$this->_logger=$logger;
$this->_localeResolver=$localeResolver;
}
Wecanfindthepreferencedefinitionsfortheseinterfacesunderthe<MAGENTO_DIR>/magento2-base/app/etc/di.xmlfile:
<preferencefor="Magento\Framework\Stdlib\DateTime\TimezoneInterface"type="Magento\Framework\Stdlib\DateTime\Timezone"/>
<preferencefor="Psr\Log\LoggerInterface"type="Magento\Framework\Logger\Monolog"/>
<preferencefor="Magento\Framework\Locale\ResolverInterface"type="Magento\Framework\Locale\Resolver"/>
Theoretically,wecanusetheobjectmanagerdirectly,asfollows:
classType{
protected$objectManager;
publicfunction__construct(
\Magento\Framework\ObjectManagerInterface$objectManager
){
$this->objectManager=$objectManager;
}
publicfunctionexample(){
$this->objectManager->create(\Fully\Qualified\Class\Name::class);
$this->objectManager->get(\Fully\Qualified\Class\Name::class);
\Magento\Framework\App\ObjectManager::getInstance()
->create(\Fully\Qualified\Class\Name::class);
\Magento\Framework\App\ObjectManager::getInstance()
->get(\Fully\Qualified\Class\Name::class);
}
}
ThedirectuseoftheobjectManagerishighlydiscouraged,asitpreventstypevalidationandtype
hintingthatafactoryclassprovides.
Bydoingalookupforthe<typestringacrosstheentire<MAGENTO_DIR>directory'sdi.xmlfiles,wecanseethatMagentousesoverathousandtypedefinitions,spreadacrossthemajorityofitsmodules.
Hereisaverysimpleexample,takenfromthe<MAGENTO_DIR>/module-customer/etc/di.xmlfile:
<typename="Magento\Customer\Model\Visitor">
<arguments>
<argumentname="ignoredUserAgents"xsi:type="array">
<itemname="google1"xsi:type="string">Googlebot/1.0(googlebot@googlebot.comhttp://googlebot.com/)</item>
<itemname="google2"xsi:type="string">Mozilla/5.0(compatible;Googlebot/2.1;+http://www.google.com/bot.html)</item>
<itemname="google3"xsi:type="string">Googlebot/2.1(+http://www.googlebot.com/bot.html)</item>
</argument>
</arguments>
</type>
LookingintothesourceoftheMagento\Customer\Model\Visitorclass,wecanseethatithasitsconstructordefinedbythe$ignoredUserAgents=[]array.Usingthetypeelement,theprecedingexampleinjectstheignoredUserAgentsargumentwiththegivenarrayvalues.
Whenconfigurationfilesforagivenscopegetmerged,arrayargumentswiththesamenamegetmergedintoanewarray.However,ifanynewconfigurationisloadedatalatertime,eitherbyamorespecificscopeorthroughthecode,thenanyarraydefinitionsinthenewconfigurationwillreplacetheloadedconfigurationinsteadofmerging.
Thelistofavailableitemtypevaluesgoeswellbeyondjustastring,andincludesthefollowing:
boolean
string
number
null
object
const
init_parameter
array
See<MAGENTO_DIR>/framework/Data/etc/argument/types.xsdand
<MAGENTO_DIR>/framework/ObjectManager/etc/config.xsdforspecifictypedefinitions.
Argumentinjectionoftengoeshandinhandwithvirtualtypes,aswewillsoonsee.
VirtualtypesVirtualtypesareaveryneatfeatureofMagentothatallowustochangetheargumentsofaspecificinjectabledependencyandthuschangethebehaviorofaparticularclasstype.
The<MAGENTO_DIR>/module-checkout/etc/di.xmlfileprovidesasimpleexampleofvirtualTypeanditsusage:
<virtualTypename="Magento\Checkout\Model\Session\Storage"type="Magento\Framework\Session\Storage">
<arguments>
<argumentname="namespace"xsi:type="string">checkout</argument>
</arguments>
</virtualType>
<typename="Magento\Checkout\Model\Session">
<arguments>
<argumentname="storage"xsi:type="object">Magento\Checkout\Model\Session\Storage</argument>
</arguments>
</type>
ThevirtualTypehere(virtually)extendsMagento\Framework\Session\Storagebyrewritingitsconstructor's$namespace='default'argumentto$namespace='checkout'.However,thischangedoesnotkickinonitsown,astheMagento\Checkout\Model\Session\Storagevirtualtypemustbeusedfirst.Itisusedinthiscase,viaatypedefinition,wherethestorageargumentisreplacedentirelybythevirtualtype.
MostofthevirtualTypenameattributesacrossMagentotaketheformofanon-existingfullyqualifiedclassname,thoughthiscanbeanycharactercombinationthat'sallowedinPHParraykeys.
Bydoingalookupforthe<virtualTypestringacrosstheentire<MAGENTO_DIR>directory'sdi.xmlfiles,wecanseethatMagentouseshundredsofvirtualtypesacrossthemajorityofitsmodules.
AmorecomplexexampleofvirtualtypeusagecanbefoundundertheMagento_LayeredNavigationmodule.
The<MAGENTO_DIR>/module-layered-navigation/etc/frontend/di.xmlfiledefinestwovirtualtypes,asfollows:
<virtualTypename="Magento\LayeredNavigation\Block\Navigation\Category"type="Magento\LayeredNavigation\Block\Navigation">
<arguments>
<argumentname="filterList"xsi:type="object">categoryFilterList</argument>
</arguments>
</virtualType>
<virtualTypename="Magento\LayeredNavigation\Block\Navigation\Search"type="Magento\LayeredNavigation\Block\Navigation">
<arguments>
<argumentname="filterList"xsi:type="object">searchFilterList</argument>
</arguments>
</virtualType>
Here,wehavetwovirtualtypesdefined,eachchangingthefilterListargumentoftheMagento\LayeredNavigation\Block\Navigationclass.categoryFilterListandsearchFilterListarethenamesoftwoothervirtualtypesthataredefinedin<MAGENTO_DIR>/module-catalog-search/etc/di.xml,asvisiblehere:https://github.com/magento/magento2/blob/2.2/app/code/Magento/CatalogSearch/etc/di.xml.
TheMagento\LayeredNavigation\Block\Navigation\CategoryandMagento\LayeredNavigation\Block\Navigation\Searchvirtualtypesarethenusedinlayoutfilesforblockdefinition,asfollows:
<!--view/frontend/layout/catalog_category_view_type_layered.xml-->
<referenceContainername="sidebar.main">
<blockclass="Magento\LayeredNavigation\Block\Navigation\Category"...
</referenceContainer>
<!--view/frontend/layout/catalogsearch_result_index.xml-->
<referenceContainername="sidebar.main">
<blockclass="Magento\LayeredNavigation\Block\Navigation\Search"...
</referenceContainer>
WhatthiseffectivelydoesistellMagentothat,forthecategoryviewpageandsearchpage,usethevirtualtypeforclass,thusinstructingittogothroughalltheargumentchangesspecifiedinthevirtualtype.
Thisisaninterestingexampleasitrevealsthepotentialcomplexityofusingvirtualtypes.Basically,wehaveonevirtualtype(Magento\LayeredNavigation\Block\Navigation\Search)changingthesinglefilterListargumentofarealtype(Magento\LayeredNavigation\Block\Navigation)withanothervirtualtype(categoryFilterList).Likewise,wejustlearnedhowtheclasspropertyoftheblockelementiscapableofnotjustacceptingthefullyqualifiedclassname,butthevirtualTypenameaswell.
ProxiesProxyclassesareusedwhenobjectcreationisexpensiveandaclass'constructorisunusuallyresource-intensive.Toavoidunnecessaryperformanceimpact,MagentousesProxyclassestoturngiventypesintobecominglazy-loadedversionsofthem.
Aquicklookupforthe\Proxy</argument>stringacrossallMagentodi.xmlfilesrevealsoverahundredoccurrencesofthisstring.ItgoestosaythatMagentoextensivelyusesproxiesacrossitscode.
Thetypedefinitionunder<MAGENTO_DIR>/module-customer/etc/di.xmlisaniceexampleofusingproxies:
<typename="Magento\Customer\Model\Session">
<arguments>
<argumentname="configShare"xsi:type="object">Magento\Customer\Model\Config\Share\Proxy</argument>
<argumentname="customerUrl"xsi:type="object">Magento\Customer\Model\Url\Proxy</argument>
<argumentname="customerResource"xsi:type="object">Magento\Customer\Model\ResourceModel\Customer\Proxy</argument>
<argumentname="storage"xsi:type="object">Magento\Customer\Model\Session\Storage</argument>
<argumentname="customerRepository"xsi:type="object">Magento\Customer\Api\CustomerRepositoryInterface\Proxy</argument>
</arguments>
</type>
IfwelookattheconstructoroftheMagento\Customer\Model\Sessiontype,wecanseethatnoneofthefourarguments(configShare,customerUrl,customerResource,andcustomerRepository)weredeclaredasProxywithinthePHPfile.Theywhererewrittenthroughdi.xml.TheseProxytypesdonotreallyexistjustyet,astheMagentodependencyinjection(di)compilationprocesscreatesthem.Theyareautomaticallygeneratedunderthegenerateddirectory.
Onceitiscompiled,theMagento\Customer\Model\Url\Proxytypecaneasilybefoundunderthegenerated/code/Magento/Customer/Model/Url/Proxy.phpfile.Let'stakeapartiallookatit:
classProxyextends\Magento\Customer\Model\Url
implements\Magento\Framework\ObjectManager\NoninterceptableInterface{
publicfunction__construct(
\Magento\Framework\ObjectManagerInterface$objectManager,
$instanceName='\\Magento\\Customer\\Model\\Url',
$shared=true){
$this->_objectManager=$objectManager;
$this->_instanceName=$instanceName;
$this->_isShared=$shared;
}
publicfunction__sleep(){
return['_subject','_isShared','_instanceName'];
}
publicfunction__wakeup(){
$this->_objectManager=\Magento\Framework\App\ObjectManager::getInstance();
}
publicfunction__clone(){
$this->_subject=clone$this->_getSubject();
}
protectedfunction_getSubject(){
if(!$this->_subject){
$this->_subject=true===$this->_isShared
?$this->_objectManager->get($this->_instanceName)
:$this->_objectManager->create($this->_instanceName);
}
return$this->_subject;
}
publicfunctiongetLoginUrl(){
return$this->_getSubject()->getLoginUrl();
}
publicfunctiongetLoginUrlParams(){
return$this->_getSubject()->getLoginUrlParams();
}
}
ThecompositionoftheProxyclassshowsthemechanismbywhichitwrapsaroundtheoriginalMagento\Customer\Model\Urltype.Thisnowmeansthat,acrossMagento,everytimetheMagento\Customer\Model\Urltypeisrequested,theMagento\Customer\Model\Url\Proxyisgoingtogetpassedinstead.Unliketheoriginaltype's__constructmethodwhichmightbeperformanceheavy,theautogeneratedProxy's__constructmethodisalightweightone.Thiseliminatespossibleperformancebottlenecks.The_getSubjectmethodisusedtoinstantiate/lazyloadtheoriginaltypewheneveranyoftheoriginaltypepublicmethodsarecalled.Forexample,thecalltothegetLoginUrlmethodwouldgothroughtheproxy.
EveryproxygeneratedbyMagentoimplementsMagento\Framework\ObjectManager\NoninterceptableInterface.Thoughtheinterfaceitselfisempty,itisusedasamarkertoidentifyproxiesforwhichwedon'tneedtogenerateinterceptors(plugins).
Whenwritingcustomtypes,suchasMagelicious\Core\Model\Customer,wecouldeasilyspecifytheproxyrightthereintheconstructor:
classCustomer{
publicfunction__construct(
\Magento\Customer\Model\Url\Proxy$customerUrl
){
//...
}
}
Thisapproach,however,isabadpractice.Thewaytodothisproperlyistospecify__constructwithanoriginalMagento\Customer\Model\Urltypeandthenaddthedi.xmlasfollows:
<typename="Magelicious\Core\Model\Customer">
<arguments>
<argumentname="customerUrl"xsi:type="object">Magento\Customer\Model\Url\Proxy</argument>
</arguments>
</type>
FactoriesFactoriesareclassesthatcreateotherclasses—muchliketheobjectmanager,exceptthistimeweareencouragedtousethemdirectly.Theirpurposeistoinstantiatethenon-injectableclasses—thosethatweshouldnotinjectdirectlyinto__construct.Thebeautyofusingfactoriesisthat,mostofthetime,wedon'tevenhavetowritethem,astheyareautomaticallygeneratedbyMagentounlessweneedtoimplementsomesortofspecificbehaviorforourfactoryclasses.
BydoingalookupfortheFactory$stringacrosstheentire<MAGENTO_DIR>directory's*.phpfiles,wecanseethousandsoffactoryexamples,spreadacrossthemajorityofMagento'smodules.
Whileagreatdealofthesefactoriesactuallyexist,othersareautomaticallygeneratedwhenneeded.
Let'stakeaquicklookatoneautomaticallygeneratedfactory,thatofMagento\Newsletter\Model\SubscriberFactory,whichisusedinseveralMagentomodulessuchasthenewsletter,subscriber,andreviewmodules:
classSubscriberFactory{
protected$_objectManager=null;
protected$_instanceName=null;
publicfunction__construct(
\Magento\Framework\ObjectManagerInterface$objectManager,
$instanceName='\\Magento\\Newsletter\\Model\\Subscriber'
){
$this->_objectManager=$objectManager;
$this->_instanceName=$instanceName;
}
publicfunctioncreate(array$data=array()){
return$this->_objectManager->create($this->_instanceName,$data);
}
}
Theautogeneratedfactorycodeisessentiallyjustathinwrapperontopofanobjectmanagercreatemethod.
Factoriesworkwellwiththedi.xmlpreferencemechanism,whichmeanswecaneasilypassinterfacesintotheconstructor,likeso:
publicfunction__construct(
\Magento\CatalogInventory\Api\StockItemRepositoryInterface$stockItemRepository,
\Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory$stockItemCriteriaFactory
){
$this->stockItemRepository=$stockItemRepository;
$this->stockItemCriteriaFactory=$stockItemCriteriaFactory;
}
//$criteria=$this->stockItemCriteriaFactory->create();
//$result=$this->stockItemRepository->getList($criteria);
Thepreferencemechanismmakessurethatconcreteimplementationsgetpassedtotheobjectinstancewhenitsconstructorisinvoked.
Whileindevelopermode,Magentoperformsautomaticcompilation,meaningthatchangestodi.xmlareautomaticallypickedup.Sometimes,however,ifwestumbleuponunexpectedresults,runningthebin/magentosetup:di:compileconsolecommandorevenmanuallyclearingthegeneratedfolder(rm-rfgenerated/*)mighthelpsortouttheissues.
PluginsPluginsarelikelyoneofthemostpowerfulfeaturesofMagento.Theyallowustomodifythebehaviorofpublicclassfunctionsbyinterceptingafunctioncallandrunningcodebefore,after,oraroundthatfunctioncall.
Beforeweeagerlystartusingthem,itisworthemphasizinghowpluginscan'tbeusedonthefollowing:
FinalmethodsFinalclassesNon-publicmethodsClassmethods(suchasstaticmethods)__construct
VirtualtypesObjectsthatareinstantiatedbeforeMagentoorFramework\Interceptionisbootstrapped
Pluginscanbeusedonthefollowing:
ClassesInterfacesAbstractclassesParentclasses
Bydoingalookupforthe<pluginstringacrosstheentire<MAGENTO_DIR>directory'sdi.xmlfiles,wecanseehundredsofpluginexamplesspreadacrossthemajorityofMagento'smodules.
ThebeforepluginThebeforeplugin,asitsnamesuggests,runsbeforetheobservedmethod.
Whenwritingabeforeplugin,thereareafewkeypointstoremember:
1. Thebeforekeywordisappendedtotheobservedinstancemethod.IftheobservedmethodiscalledgetSomeValue,thenthepluginmethodiscalledbeforeGetSomeValue.
2. Thefirstparameterofthebeforepluginmethodisalwaystheobservedinstancetype,oftenabbreviatedas$subjectordirectlybytheclasstype–whichis$processorinourexample.Wecantypecastitforgreaterreadability.
3. Allotherparametersofthepluginmethodmustmatchtheparametersoftheobservedmethod.
4. Thepluginmethodmustreturnanarraywiththesametypeandnumberofparametersastheobservedmethod'sinputparameters.
Let'stakealookatoneofMagento'sbeforepluginimplementations,theonespecifiedinthe<MAGENTO_DIR>module-payment/etc/frontend/di.xmlfile:
<typename="Magento\Checkout\Block\Checkout\LayoutProcessor">
<pluginname="ProcessPaymentConfiguration"
type="Magento\Payment\Plugin\PaymentConfigurationProcess"/>
</type>
TheoriginalmethodthispluginistargetingistheprocessmethodoftheMagento\Checkout\Block\Checkout\LayoutProcessorclass:
publicfunctionprocess($jsLayout){
//Therestofthecode...
return$jsLayout;
}
TheimplementationofthebeforepluginisprovidedviathebeforeProcessmethodoftheMagento\Payment\Plugin\PaymentConfigurationProcessclass,asperthefollowingpartialexample:
publicfunctionbeforeProcess(
\Magento\Checkout\Block\Checkout\LayoutProcessor$processor,
$jsLayout){
//Therestofthecode...
return[$jsLayout];
}
ThearoundpluginThearoundpluginrunsaroundtheobservedmethodinawaythatallowsustorunsomecodebeforeandaftertheoriginalmethodcall.Thisisaverypowerfulconcept,aswegettochangetheincomingparametersaswellasthereturnvalueofafunction.
Whenwritingthearoundplugin,thereareafewkeypointstoremember:
1. Thefirstparametercomingintothepluginistheobservedtypeinstance.2. Thesecondparametercomingintothepluginisacallable/Closure.Usually,
thisparameteristypedandnamedascallable$proceed.Wemustmakesuretoforwardthesameparameterstothiscallableastheoriginalmethodsignature.
3. Allotherparametersareparametersoftheoriginalobservedmethod.4. Thepluginmustreturnthesamevalueastheoriginalfunction,ideallyreturn
$proceed(…)or$returnValue=$proceed();followedby$returnValue;forcaseswhereweneedtomodifythe$returnValue.
Let'stakealookatoneofMagento'saroundpluginimplementations,theonespecifiedinthe<MAGENTO_DIR>module-grouped-product/etc/di.xmlfile:
<typename="Magento\Catalog\Model\ResourceModel\Product\Link">
<pluginname="groupedProductLinkProcessor"type="Magento\GroupedProduct\Model\ResourceModel\Product\Link\RelationPersister"/>
</type>
TheoriginalmethodofthispluginistargetingthedeleteProductLinkmethodoftheMagento\Catalog\Model\ResourceModel\Product\Linkclass:
publicfunctiondeleteProductLink($linkId){
return$this->getConnection()
->delete($this->getMainTable(),['link_id=?'=>$linkId]);
}
TheimplementationofthearoundpluginisprovidedviathearoundDeleteProductLinkmethodoftheMagento\GroupedProduct\Model\ResourceModel\Product\Link\RelationPersisterclass,asperthefollowingpartialexample:
publicfunctionaroundDeleteProductLink(
\Magento\GroupedProduct\Model\ResourceModel\Product\Link$subject,
\Closure$proceed,$linkId){
//Therestofthecode...
$result=$proceed($linkId);
//Therestofthecode...
return$result;
}
TheafterpluginTheafterplugin,asitsnamesuggests,runsaftertheobservedmethod.
Whenwritingtheafterplugin,thereareafewkeypointstoremember:
1. Thefirstparametercomingintothepluginisanobservedtypeinstance.2. Thesecondparametercomingintothepluginistheresultoftheobserved
method,oftencalled$resultorcalledafterthevariablereturnedfromtheobservedmethod(asinthefollowingexample:$data).
3. Allotherparametersareparametersoftheobservedmethod.4. Thepluginmustreturnthesame$result|$datavariableofthesametype,as
wearefreetomodifythevalue.
Let'stakealookatoneofMagento'safterpluginimplementations,theonespecifiedinthemodule-catalog/etc/di.xmlfile:
<typename="Magento\Indexer\Model\Config\Data">
<pluginname="indexerProductFlatConfigGet"
type="Magento\Catalog\Model\Indexer\Product\Flat\Plugin\IndexerConfigData"/>
</type>
TheoriginalmethodthispluginistargetingisthegetmethodoftheMagento\Indexer\Model\Config\Dataclass:
publicfunctionget($path=null,$default=null){
//Therestofthecode...
return$data;
}
TheimplementationoftheafterpluginisprovidedviatheafterGetmethodoftheMagento\Catalog\Model\Indexer\Product\Flat\Plugin\IndexerConfigDataclass,asperthefollowingpartialexample:
publicfunctionafterGet(Magento\Indexer\Model\Config\Data,$data,$path=null,$default=null){
//Therestofthecode...
return$data;
}
Specialcareshouldbetakenwhenusingplugins.Whiletheyprovidegreatflexibility,theyalsomakeiteasytoinducebugs,performancebottlenecks,andotherlessobvioustypesofinstabilities–evenmoresoifseveralpluginsare
observingthesamemethod.
EventsandobserversMagentohasaneatpublish-subscribepatternimplementationthatwecalleventsandobservers.Bydispatchingeventswhencertainactionsaretriggered,wecanrunourcustomcodeinresponsetothetriggeredevent.TheeventsaredispatchedusingtheMagento\Framework\Event\Managerclass,whichimplementsMagento\Framework\Event\ManagerInterface.
Todispatchanevent,wesimplycallthedispatchmethodoftheeventmanagerinstance,providingitwiththenameoftheeventwearedispatchingwithanoptionalarrayofdatawewishtopassontotheobservers,asperthefollowingexampletakenfromthe<MAGENTO_DIR>/module-customer/Controller/Account/CreatePost.phpfile:
$this->_eventManager->dispatch(
'customer_register_success',
['account_controller'=>$this,'customer'=>$customer]
);
Observersareregisteredviaanevents.xmlfile,asperthefollowingexamplefromthe<MAGENTO_DIR>/module-persistent/etc/frontend/events.xmlfile:
<eventname="customer_register_success">
<observername="persistent"instance="Magento\Persistent\Observer\RemovePersistentCookieOnRegisterObserver"/>
</event>
BydoingalookupfortheeventManager->dispatchstringacrosstheentire<MAGENTO_DIR>directory's*.phpfiles,wecanseehundredsofeventsexamples,spreadacrossthemajorityofMagento'smodules.Whilealloftheseeventsareofthesametechnicalimportance,wemightsaythatsomearelikelytobeusedmoreonadaytodaybasisthanothers.
Thismakesitworthtakingsometimetostudythefollowingclassesandtheeventstheydispatch:
TheMagento\Framework\App\Action\Actionclass,withthefollowingevents:controller_action_predispatch
'controller_action_predispatch_'.$request->getRouteName()
'controller_action_predispatch_'.$request->getFullActionName()
'controller_action_postdispatch_'.$request->getFullActionName()
'controller_action_postdispatch_'.$request->getRouteName()
controller_action_postdispatch
TheMagento\Framework\Model\AbstractModelclass,withthefollowingevents:model_load_before
$this->_eventPrefix.'_load_before'
model_load_after
$this->_eventPrefix.'_load_after'
model_save_commit_after
$this->_eventPrefix.'_save_commit_after'
model_save_before
$this->_eventPrefix.'_save_before'
model_save_after
clean_cache_by_tags
$this->_eventPrefix.'_save_after'
model_delete_before
$this->_eventPrefix.'_delete_before'
model_delete_after
clean_cache_by_tags
$this->_eventPrefix.'_delete_after'
model_delete_commit_after
$this->_eventPrefix.'_delete_commit_after'
$this->_eventPrefix.'_clear'
TheMagento\Framework\Model\ResourceModel\Db\Collectionclass,withthefollowingevents:
core_collection_abstract_load_before
$this->_eventPrefix.'_load_before'
core_collection_abstract_load_after
$this->_eventPrefix.'_load_after'
Somemoreimportanteventscanbefoundinafewofthetypesdefinedunderthe<MAGENTO_DIR>/framework/Viewdirectory:
view_block_abstract_to_html_before
view_block_abstract_to_html_after
view_message_block_render_grouped_html_after
layout_render_before
'layout_render_before_'.$this->request->getFullActionName()
core_layout_block_create_after
layout_load_before
layout_generate_blocks_before
layout_generate_blocks_after
core_layout_render_element
Let'stakeacloserlookatoneoftheseevents,theonefoundinthe<MAGENTO_DIR>/framework/Model/AbstractModel.phpfile:
publicfunctionafterCommitCallback(){
$this->_eventManager->dispatch('model_save_commit_after',['object'=>$this]);
$this->_eventManager->dispatch($this->_eventPrefix.'_save_commit_after',$this->_getEventData());
return$this;
}
protectedfunction_getEventData(){
return[
'data_object'=>$this,
$this->_eventObject=>$this,
];
}
The$_eventPrefixand$_eventObjecttypepropertiesareparticularlyimportanthere.IfweglimpseovertypessuchasMagento\Catalog\Model\Product,Magento\Catalog\Model\Category,Magento\Customer\Model\Customer,Magento\Quote\Model\Quote,Magento\Sales\Model\Order,andothers,wecanseethatagreatdealoftheseentitytypesareessentiallyextendingfromMagento\Framework\Model\AbstractModelandprovidetheirownvaluestoreplace$_eventPrefix='core_abstract'and$_eventObject='object'.Whatthismeansisthatwecanuseeventssuchas$this->_eventPrefix.'_save_commit_after'tospecifyobserversviaevents.xml.
Let'stakealookatthefollowingexample,takenfromthe<MAGENTO_DIR>/module-downloadable/etc/events.xmlfile:
<config>
<eventname="sales_order_save_commit_after">
<observername="downloadable_observer"instance="Magento\Downloadable\Observer\SetLinkStatusObserver"/>
</event>
</config>
Observersareplacedinsidethe<ModuleDir>/Observerdirectory.EveryobserverimplementsasingleexecutemethodontheMagento\Framework\Event\ObserverInterfaceclass:
classSetLinkStatusObserverimplements\Magento\Framework\Event\ObserverInterface{
publicfunctionexecute(\Magento\Framework\Event\Observer$observer){
$order=$observer->getEvent()->getOrder();
}
}
Muchlikeplugins,badlyimplementedobserverscaneasilycausebugsorevenbreaktheentireapplication.Thisiswhyweneedtokeepourobserversmallandcomputationallyefficient—toavoidperformancebottlenecks.
Thecyclicaleventloopisatrapthat'seasytofallinto.Thishappenswhenanobserver,atsomepoint,isdispatchingthesameeventthatitlistensto.Forexample,ifanobserverlistenstothemodel_save_beforeevent,andthentriestosavethesameentityagainwithintheobserver,thiswouldtriggeracyclicaleventloop.
Tomakeourobserversasspecificaspossible,weneedtodeclaretheminanappropriatescope:
Forobservingfrontendonlyevents,youcandeclareobserversin<ModuleDir>/etc/frontend/events.xml
Forobservingbackendonlyevents,youcandeclareobserversin<ModuleDir>/etc/adminhtml/events.xml
Forobservingglobalevents,youcandeclareobserversin<ModuleDir>/etc/events.xml
Unlikeplugins,observersareusedfortriggeringthefollow-upfunctionality,ratherthanchangingthebehavioroffunctionsordatawhichispartoftheeventtheyareobserving.
ConsolecommandsThebuilt-inbin/magentotoolplaysamajorrole–notjustinMagentodevelopment,butinproductiondeploymentsaswell.
Rightoutofthebox,itprovidesadozencommandsthatwecanusetomanagecaches,indexers,dependencycompilation,deployingstaticviewfiles,creatingCSSfromLESS,puttingourstoretomaintenance,installingmodules,andmore.
Quiteeasily,MagentoenablesustoaddourowncommandstoitsSymfony-likecommand-lineinterface(CLI).TheMagentoCLIessentiallyextendsfromSymfony\Component\Console\Command.
Therealvalueincreatingourowncommandliesintheargumentsandoptionsthatwecanmakeavailable,thuspassingdynamicinformationtothecommand.
Magentoconsolecommandsresideunderthe<ModuleName>/Consoledirectory,whichcanfurtherbeorganizedtobetteraccommodateourcommands.Magentomostlyusesthe<ModuleName>/Console/CommanddirectorytoplacetheactualCLIcommandclass,whereasvariousoptionsandotheraccompanyingclassesresideinthe<ModuleName>/Consoledirectory.
Conceptually,creatinganewCLIcommandisaseasyasdoingthefollowing:
1. Creatingthecommandclass2. Wiringitupviadi.xml3. Clearingthecacheandcompileddirectories
Let'screateourownsimpleconsolecommand.Wewillstartoffbycreatingthe<MAGELICIOUS_DIR>/Core/Console/Command/RunStockImportCommand.phpfilewiththefollowingcontent:
useSymfony\Component\Console\Command\Command;
useSymfony\Component\Console\Input\InputArgument;
useSymfony\Component\Console\Input\InputOption;
useSymfony\Component\Console\Input\InputInterface;
useSymfony\Component\Console\Output\OutputInterface;
classRunStockImportCommandextendsCommand{
constORDER_ID_ARGUMENT='order_id';
constDAYS_BACK_OPTION='days_back';
protectedfunctionconfigure(){
$this->setName('magelicious:stock:import')
->setDescription('TheMageliciousStockImport.')
->setDefinition([
newInputArgument(
self::ORDER_ID_ARGUMENT,/*name*/
InputArgument::REQUIRED,/*modeREQUIREDorOPTIONAL*/
'Theargumenttoset.',/*description*/
null/*default*/
),
newInputOption(
self::DAYS_BACK_OPTION,/*name*/
null,/*shortcut*/
InputOption::VALUE_OPTIONAL,/*VALUE_NONEorVALUE_REQUIREDorVALUE_OPTIONALorVALUE_IS_ARRAY*/
'Theoptiontoset.'/*description*/
)
]);
parent::configure();
}
protectedfunctionexecute(InputInterface$input,OutputInterface$output){
try{
$output->setDecorated(true);
//$input->getArgument(self::ORDER_ID_ARGUMENT);
//$input->getOption(self::DAYS_BACK_OPTION);
//greentext
$output->writeln('<info>Theinfomessage.</info>');
//yellowtext
$output->writeln('<comment>Thecommentmessage.</comment>');
//blacktextonacyanbackground
$output->writeln('<question>Thequestionmessage.</question>');
return\Magento\Framework\Console\Cli::RETURN_SUCCESS;
}catch(\Exception$e){
//whitetextonaredbackground
$output->writeln('<error>'.$e->getMessage().'</error>');
if($output->getVerbosity()>=OutputInterface::VERBOSITY_VERBOSE){
$output->writeln($e->getTraceAsString());
}
return\Magento\Framework\Console\Cli::RETURN_FAILURE;
}
}
}
Wethenwireitupvia<MAGELICIOUS_DIR>/etc/di.xml,asfollows:
<typename="Magento\Framework\Console\CommandListInterface">
<arguments>
<argumentname="commands"xsi:type="array">
<itemname="runStockImport"xsi:type="object">Magelicious\Core\Console\Command\RunStockImportCommand</item>
</argument>
</arguments>
</type>
Wecannowclearthecacheandthecompileddirectorieseitherbyrunningthephpbin/magentocache:cleanconfigfollowedbyphpbin/magentosetup:di:compile,orbyrunningrm-rfgenerated/*andrm-rfvar/cache/*.
Now,ifwerunthephpbin/magentocommand,weshouldseeourcommandonthelist:
magelicious
magelicious:stock:importTheMageliciousStockImport.
Ifwenowtestourmethodbyrunningphpbin/magentomagelicious:stock:import,thisshouldimmediatelytriggeranerror,asfollows:
[Symfony\Component\Console\Exception\RuntimeException]
Notenougharguments(missing:"order_id").
magelicious:stock:import[--days_back[DAYS_BACK]][--]<order_id>
Eitherofthefollowingcallsshouldwork:
phpbin/magentomagelicious:stock:import000000060
phpbin/magentomagelicious:stock:import000000060--days_back=7
CronjobsCreatinganewcronjobisaseasyasdoingthefollowing:
1. Creatingajobdefinitionunderthe<ModuleName>/etc/crontab.xmlfile2. Creatingaclasswithapublicmethodthathandlesthejobexecution
Let'screateasimplecronjob.Wewillstartoffbycreatingthe<MAGELICIOUS_DIR>/Core/etc/crontab.xmlfilewiththefollowingcontent:
<groupid="default">
<jobname="the_job"instance="Magelicious\Core\Cron\TheJob"method="execute">
<schedule>*/15****</schedule>
</job>
</group>
Theinstanceandmethodvaluesmaptotheclassandmethodwithinthatclass,whichwillbeexecutedwhencronjobisrun.Thescheduleisacron,liketheexpressionforwhenthejobistobeexecuted.Unlesstherearespecificrequirementstellingusotherwise,wecansafelyusethedefaultgroup.
Wethencreatethe<MAGELICIOUS_DIR>/Core/Cron/TheJob.phpfilewiththefollowingcontent:
classTheJob{
publicfunctionexecute(){
//...
}
}
TheMagentoconsolecommandsupportsseveralconsolecommands:
cron
cron:installGeneratesandinstallscrontabforcurrentuser
cron:removeRemovestasksfromcrontab
cron:runRunsjobsbyschedule
Togetourcronjobrunning,weneedtomakesurethatcrontabisinstalled,byrunningphpbin/magentocron:install.Thiscommandgeneratesandinstallscrontabforthecurrentuser.Wecanconfirmthatbyfollowingupwiththecrontab-ecommand,likeso:
#~MAGENTOSTART6f7c468a10aea2972eab1da53c8d2fce
*****/bin/php/magelicious/bin/magentocron:run2>&1|grep-v"Ranjobsbyschedule">>/magelicious/var/log/magento.cron.log
*****/bin/php/magelicious/update/cron.php>>/magelicious/var/log/update.cron.log
*****/bin/php/magelicious/bin/magentosetup:cron:run>>/magelicious/var/log/setup.cron.log
#~MAGENTOEND6f7c468a10aea2972eab1da53c8d2fce
Now,ifweexecutephpbin/magentocron:run,the_jobshouldfinditswayunderthecron_scheduletable.
Dependingontheschedule_generate_everyandschedule_ahead_foroptionsforaparticularcrongroup,wemightnotseesomecronjobsinstantlyshowingupinthecron_scheduletable.
MagentoOpenSourceprovidestwocrongroups:defaultandindex.Whilethemajorityoftimesourcronjobswillbeplacedunderthedefaultgroup,theremightbeaneedtocreateacompletelynewcrongroup.Luckily,thisisquiteeasy.
Tocreateanewcrongroup,allweneedisa<MAGELICIOUS_DIR>/etc/cron_groups.xmlfilewiththefollowingcontent:
<config>
<groupid="magelicious">
<schedule_generate_every>15</schedule_generate_every>
<schedule_ahead_for>20</schedule_ahead_for>
<schedule_lifetime>15</schedule_lifetime>
<history_cleanup_every>10</history_cleanup_every>
<history_success_lifetime>10080</history_success_lifetime>
<history_failure_lifetime>10080</history_failure_lifetime>
<use_separate_process>0</use_separate_process>
</group>
</config>
Whilegroupinformationisnotstoredinthecron_scheduletable,wecanuseitviatheMagentoCLItorunjobsthatarespecifictoacertaingroup:
phpbin/magentocron:run--group=default
SummaryInthischapter,wetoucheduponsomeofMagento'skeyscomponents.PluginsandeventobserversprovideapowerfulwayofextendingMagento,eitherbychangingthebehaviorofexistingfunctionsorbyrunningsomefollow-upcodeinresponsetocertainevents.
Movingforward,wewilldeepenourMagentoknowledgefurtherbylookingintotheinstallandupdatescripts,theEntity–Attribute–Valuemodel(EAV),creatingnewEAVtypes,indexers,extension,andcustomattributes.
WorkingwithEntitiesEveryMagentomodulehostsitsmodelswithintheModelsdirectory.Someofthesemodelsarepersistable,whileothersarenon-persistable.Agreatdealofcustom,third-party,andcoreMagentomodulespersistdatatothedatabase.DatapersistenceisoneofthekeyfunctionalitiesthatplatformslikeMagentoneedtodealwith.Terminology-wise,Magentousestermssuchasmodel,resourcemodel,andcollectionforagroupofthreeclassesthatdealwithdatapersistence,thatis,create,read,update,anddelete(CRUD)operations.
Tobetterunderstandtheoverallmechanismofentities,wearegoingtotakeacloserlookatthefollowing:
Understandingtypesofmodels:CreatingasimplemodelMethodsworthmemorizing
Workingwithsetupscripts:TheInstallSchemascriptTheUpgradeSchemascriptTheRecurringscriptTheInstallDatascriptTheUpgradeDatascriptTheRecurringDatascript
Creatingextensionattributes
TechnicalrequirementsYouwillneedtohavebasicknowledgeofPHP,OOP,JavaScript,andXML.YouwillalsoneedApache,MySQL,andAMPPSinstalledonyoursystemtoexecutethecodes.
ThecodefilesofthischaptercanbefoundonGitHub:https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
CheckoutthefollowingvideotoseetheCodeinAction:
http://bit.ly/2PKYvUx.
UnderstandingtypesofmodelsTherearetwotypesofpersistencemodelsinMagento:simpleandEntity–attribute–value(EAV).Thetermentityistossedaroundinterchangeablybetweenthetwotypesofmodels.Wecanthinkofanentityasanypersistablemodel.
TheSubscriberentityoftheMagento_Newslettermoduleisanexampleofasimplemodel.Wecanseethatit'scomprisedofthefollowing:
AmodeloftypeMagento\Newsletter\Model\SubscriberextendsMagento\Framework\Model\AbstractModel
AresourcemodeloftypeMagento\Newsletter\Model\ResourceModel\SubscriberextendsMagento\Framework\Model\ResourceModel\Db\AbstractDbAcollectionoftypeMagento\Newsletter\Model\ResourceModel\Subscriber\CollectionextendsMagento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
TheCustomerentityoftheMagento_CustomermoduleisanexampleofanEAVmodel.Wecanseethatit'scomprisedofthefollowing:
AmodeloftypeMagento\Customer\Model\CustomerextendsMagento\Framework\Model\AbstractModelAresourcemodeloftypeMagento\Customer\Model\ResourceModel\CustomerextendsMagento\Eav\Model\Entity\VersionControl\AbstractEntityAcollectionoftypeMagento\Customer\Model\ResourceModel\Customer\CollectionextendsMagento\Eav\Model\Entity\Collection\VersionControl\AbstractCollection
WhatdifferentiatesEAVfromsimplemodelsisessentiallytheirresourcemodelandcollectionclasses.Theresourcemodelisourlinktothedatabase—ourpersistor,ifyouwill.
Whenasubscriberissaved,itsdatagetssavedhorizontallyinthedatabase.Datafromthesubscribermodelgetsdumpedintothesinglenewsletter_subscribertable.
Whenacustomerissaved,itsdatagetssavedverticallyinthedatabase.Data
fromthecustomermodelgetsdumpedintothefollowingtables:
customer_entity
customer_entity_datetime
customer_entity_decimal
customer_entity_int
customer_entity_text
customer_entity_varchar
Thedecisionastowheretostoreavalueforanindividualattributeiscontainedintheeav_attribute.backend_typecolumn.TheSELECTDISTINCTbackend_typeFROMeav_attribute;queryrevealsthefollowing:
Thestaticattributevaluegetsstoredinthe<entityName>_entitytableThevarcharattributevaluegetsstoredinthe<entityName>_entity_varchartableTheintattributevaluegetsstoredinthe<entityName>_entity_inttableThetextattributevaluegetsstoredinthe<entityName>_entity_texttableThedatetimeattributevaluegetsstoredinthe<entityName>_entity_datetimetableThedecimalattributevaluegetsstoredinthe<entityName>_entity_decimaltable
Nexttotheeav_attributetable,theremainingrelevantinformationisscatteredaroundthedozenofothereav_*tables,themostimportantbeingtheeav_attribute_*tables:
eav_attribute
eav_attribute_group
eav_attribute_label
eav_attribute_option
eav_attribute_option_swatch
eav_attribute_option_value
eav_attribute_set
TheSELECTentity_type_code,entity_modelFROMeav_entity_type;queryindicatesthatthefollowingMagentoentitiesarefromanEAVmodel:
customer:Magento\Customer\Model\ResourceModel\Customercustomer_address:Magento\Customer\Model\ResourceModel\Addresscatalog_category:Magento\Catalog\Model\ResourceModel\Categorycatalog_product:Magento\Catalog\Model\ResourceModel\Product
order:Magento\Sales\Model\ResourceModel\Orderinvoice:Magento\Sales\Model\ResourceModel\Order\Invoicecreditmemo:Magento\Sales\Model\ResourceModel\Order\Creditmemoshipment:Magento\Sales\Model\ResourceModel\Order\Shipment
However,notallofthemusetheEAVmodeltoitsfullextent,asindicatedbytheSELECTDISTINCTentity_type_idFROMeav_attribute;query,whichpointsonlytothefollowing:
customer
customer_address
catalog_category
catalog_product
WhatthismeansisthatonlyfourmodelsinMagentoOpenSourcereallyuseEAVmodelsformanagingtheirattributesandstoringdataverticallythroughEAVtables.Therestareallflattables,asallattributesandtheirvaluesareinasingletable.
TheEAVmodelsareinherentlymorecomplextoworkwith.Theycomeinhandyforcaseswheredynamicattributecreationisneeded,ideallyviaanadmininterface,asisthecasewithproducts.Themajorityofthetime,however,simplemodelswilldothejob.
CreatingasimplemodelUnlikeEAVmodels,creatingsimplemodelsisprettystraightforward.Let'sgoaheadandcreateamodel,resourcemodel,andacollectionforaLogentity.
Wewillstartoffbycreatingthe<MAGELICIOUS_DIR>/Core/Model/Log.phpfilewiththefollowingcontent:
classLogextends\Magento\Framework\Model\AbstractModel{
protected$_eventPrefix='magelicious_core_log';
protected$_eventObject='log';
protectedfunction_construct(){
$this->_init(\Magelicious\Core\Model\ResourceModel\Log::class);
}
}
Theuseof$_eventPrefixand$_eventObjectisnotmandatory,butitishighlyrecommended.ThesevaluesareusedbytheMagento\Framework\Model\AbstractModeleventdispatcherandaddtothefutureextensibilityofourmodule.WhileMagentousesthe<ModuleName>_<ModelName>conventionfor$_eventPrefixnaming,wemightbesaferusing<VendorName>_<ModuleName>_<ModelName>.The$_eventObject,byconvention,usuallybearsthenameofthemodelitself.
Wethencreatethe<MAGELICIOUS_DIR>/Core/Model/ResourceModel/Log.phpfilewiththefollowingcontent:
classLogextends\Magento\Framework\Model\ResourceModel\Db\AbstractDb{
protectedfunction_construct(){
$this->_init('magelicious_core_log','entity_id');
}
}
The_initmethodheretakestwoarguments:themagelicious_core_logvalueforthe$mainTableargumentandtheentity_idvalueforthe$idFieldNameargument.The$idFieldNameisthenameoftheprimarycolumninthedesignateddatabase.It'sworthnotingthatthemagelicious_core_logtablestilldoesn'texist,butwewilladdressthatinabit.
Wewillthencreatethe<MAGELICIOUS_DIR>/Core/Model/ResourceModel/Log/Collection.phpfilewiththefollowingcontent:
classCollectionextends\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection{
protectedfunction_construct(){
$this->_init(
\Magelicious\Core\Model\Log::class,
\Magelicious\Core\Model\ResourceModel\Log::class
);
}
}
The_initmethodheretakestwoarguments:thestringnamesof$modeland$resourceModel.Magentousesthe<FULLY_QUALIFIED_CLASS_NAME>::classsyntaxforthis,asitusesaniftysolutioninsteadofpassingclassstringsaround.
MethodsworthmemorizingBothEAVandsimplemodelsextendfromtheMagento\Framework\Model\AbstractModelclass,whichfurtherextendsfromMagento\Framework\DataObject.TheDataObjecthassomeneatmethodsworthmemorizing.
Groupofthefollowingmethodsdealwithdatatransformation:
toArray:Convertsanarrayofobjectdatatoanarraywithkeysrequestedinthe$keysarraytoXml:ConvertsobjectdataintoanXMLstringtoJson:ConvertsobjectdatatoJSONtoString:Convertsobjectdataintoastringwithapredefinedformatserialize:Convertsobjectdataintoastringwithdefinedkeysandvalues
Theothergroupsofthesemethods,implementedthroughthemagic__callmethod,enablesthefollowingneatsyntax:
get<AttributeName>,forexample,$object->getPackagingOption()set<AttributeName>,forexample,$object->setPackagingOption('plastic_bag')uns<AttributeName>,forexample,$object->unsPackagingOption()has<AttributeName>,forexample,$object->hasPackagingOption()
Toquicklyputthismagicintoperspective,let'smanuallycreatethemagelicious_core_logtableasfollows:
CREATETABLE`magelicious_core_log`(
`entity_id`int(10)unsignedNOTNULLAUTO_INCREMENT,
`severity_level`varchar(24)NOTNULL,
`note`textNOTNULL,
`created_at`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,
PRIMARYKEY(`entity_id`)
)ENGINE=InnoDBDEFAULTCHARSET=utf8;
WiththemagicofDataObject,ouremptyMagelicious\Core\Model\Logmodelwillstillbeabletosaveitsdata,asfollows:
$log->setCreatedAt(new\DateTime());
$log->setSeverityLevel('info');
$log->setNote('JustSomeNote');
$log->save();
Whilethisexamplewouldwork,thereisfarmoretoitthanthis.Creatingtablesmanuallyisnotaviableoptionforbuildingmodules.Magentohasjusttherightmechanismforthis,whichiscalledsetupscripts.
WorkingwithsetupscriptsEverytimeamoduleisinstalledviaaphpbin/magentomodule:enablecommand,Magentoshowsthefollowingmessage:Tomakesurethattheenabledmodulesareproperlyregistered,run'setup:upgrade'.Thephpbin/magentosetup:upgradecommandupgradestheMagentoapplication,databasedata,andschema.Oncetriggered,theupgradecommandinstantiatesMagento\Setup\Model\Installer,whichthengoesthroughaseriesofmethods.ItsgetSchemaDataHandlermethodrevealsthetypesofavailablesetupscripts:
InstallSchema.php
UpgradeSchema.php
Recurring.php
InstallData.php
UpgradeData.php
RecurringData.php
Thesescriptsliveunderthe<VendorName>/<ModuleName>/Setupdirectory.
Oncesuccessfullyfinished,thesetup:upgradecommandmakesanewentry,orupdatesanexistingone,inthesetup_moduletable.There,wecanseetheschema_versionanddata_versionvaluesloggedagainsteachmodule.
Whentestingoutsetupscripts,wecanmanuallydeleteandadjustourmoduleentriesunderthesetup_moduletabletotriggerindividualtypeofsetupscript.Forexample,wecanleaveschema_versionasis,whilechangingthedata_version.
Let'stakeacloserlookatwritingeachofthosescripts.
TheInstallSchemascriptTheInstallSchemascriptisusedwhenwewishtoaddnewcolumnstoexistingtablesorcreatenewtables.Thisscriptisrunonlywhenamoduleisenabled.Onceenabled,themodulegetsacorrespondingentryunderthesetup_module.schema_versiontablecolumn.ThisentrypreventstheInstallSchemascriptrunningonanysubsequentsetup:upgradecommandwherethemodule'ssetup_versionremainsthesame.
Let'sgoaheadandcreatethe<MAGELICIOUS_DIR>/Core/Setup/InstallSchema.phpfilewiththefollowingcontent:
use\Magento\Framework\Setup\InstallSchemaInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\SchemaSetupInterface;
classInstallSchemaimplementsInstallSchemaInterface{
publicfunctioninstall(SchemaSetupInterface$setup,ModuleContextInterface$context){
$setup->startSetup();
echo'InstallSchema->install()'.PHP_EOL;
$setup->endSetup();
}
}
Theuseof$setup->startSetup();and$setup->endSetup();isacommonpracticeamongthemajorityofsetupscripts.Theimplementationofthesetwomethodsdealswithrunningadditionalenvironmentsetupsteps,suchassettingSQL_MODEandFOREIGN_KEY_CHECKS,ascanbeseenunderMagento\Framework\DB\Adapter\Pdo\Mysql.
Tomakesomethingusefuloutofit,let'sgoaheadandreplacetheecholinewiththecodethatactuallycreatesourmagelicious_core_logtable:
$table=$setup->getConnection()
->newTable($setup->getTable('magelicious_core_log'))
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity'=>true,'unsigned'=>true,'nullable'=>false,'primary'=>true],
'EntityID'
)->addColumn(
'severity_level',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
24,
['nullable'=>false],
'SeverityLevel'
)->addColumn(
'note',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
null,
['nullable'=>false],
'Note'
)->addColumn(
'created_at',
\Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
null,
['nullable'=>false],
'CreatedAt'
)->setComment('MageliciousCoreLogTable');
$setup->getConnection()->createTable($table);
$setup->getConnection()getsusthedatabaseadapterinstance.Fromthereon,wegetaccesstomethodsthatareneededfordatabasetablecreation.WhenitcomestoInstallSchemascripts,themajorityofthetime,thefollowingmethodswilldothejob:
newTable:RetrievesaDDLobjectforthenewtableaddColumn:AddscolumnstothetableaddIndex:AddsanindextothetableaddForeignKey:AddsaforeignkeytothetablesetComment:SetsacommentforthetablecreateTable:CreatesatablefromaDDLobject
Themagelicious_core_logtablehereisessentiallystoragebehindourMagelicious\Core\Model\Logsimplemodel.IfourmodelwasanEAVmodel,wewouldbeusingthesameInstallSchemascripttocreatetablessuchasthefollowing:
log_entity
log_entity_datetime
log_entity_decimal
log_entity_int
log_entity_text
log_entity_varchar
However,inthecaseoftheEAVmodel,theactualattributesseverity_levelandnotewouldthenlikelybeaddedviaanInstallDatascript.Thisisbecauseattributesdefinitionsareessentiallydataundertheeav_attribute_*tables—primarilytheeav_attributetable.Therefore,attributesarecreatedinsideoftheInstallDataandUpgradeDatascripts.
TheUpgradeSchemascriptTheUpgradeSchemascriptisusedwhenwewishtocreatenewtablesoraddcolumnstoexistingtables.Giventhatitisrunoneverysetup:upgrade,wheresetup_module.schema_versionislowerthansetup_versionunder<VendorName>/<ModuleName>/etc/module.xml,weareinchargeofcontrollingthecodeforaspecificversion.Thisisusuallydoneviatheif-edversion_compareapproach.
Tobetterdemonstratethis,let'screatethe<MAGELICIOUS_DIR>/Core/Setup/UpgradeSchema.phpfilewiththefollowingcontent:
use\Magento\Framework\Setup\UpgradeSchemaInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\SchemaSetupInterface;
classUpgradeSchemaimplementsUpgradeSchemaInterface{
publicfunctionupgrade(SchemaSetupInterface$setup,ModuleContextInterface$context){
$setup->startSetup();
if(version_compare($context->getVersion(),'2.0.2')<0){
$this->upgradeToVersionTwoZeroTwo($setup);
}
$setup->endSetup();
}
privatefunctionupgradeToVersionTwoZeroTwo(SchemaSetupInterface$setup){
echo'UpgradeSchema->upgradeToVersionTwoZeroTwo()'.PHP_EOL;
}
}
Theif-edversion_compareherereadsasfollows:ifthecurrentmoduleversionisequalto2.0.2,thenexecutetheupgradeToVersionTwoZeroTwomethod.Ifweweretoreleaseanupdatedversionofourmodule,wewouldneedtoproperlybumpupthesetup_versionof<VendorName>/<ModuleName>/etc/module.xml,orelseUpgradeSchemadoesnotmakealotofsense.Likewise,weshouldalwaysbesuretotargetaspecificmoduleversion,thusavoidingcodethatexecutesoneveryversionchange.
WhenitcomestoUpgradeSchemascripts,thefollowingmethodsofadatabaseadapterinstance,alongsidethepreviouslymentionedone,willbeofinterest:
dropColumn:DropsthecolumnfromatabledropForeignKey:DropstheforeignkeyfromatabledropIndex:Dropstheindexfromatable
dropTable:DropsthetablefromadatabasemodifyColumn:Modifiesthecolumndefinition
TheRecurringscriptTheRecurringscriptsexecutesoneachandeverysetup:upgradecommand,regardlessoftheschema_versionordata_versionloggedagainstthesetup_moduletable.
Let'screatethe<MAGELICIOUS_DIR>/Core/Setup/Recurring.phpfilewiththefollowingcontent:
useMagento\Framework\Setup\InstallSchemaInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\SchemaSetupInterface;
classRecurringimplementsInstallSchemaInterface{
publicfunctioninstall(SchemaSetupInterface$setup,ModuleContextInterface$context){
$setup->startSetup();
echo'Recurring->install()'.PHP_EOL;
$setup->endSetup();
}
}
Thoughinteresting,theRecurringscriptsarerarelyusedinMagento.Onlyahandfulofthemareused,andthatismostlyforinstallingexternalforeignkeys.Thisisnottosaythatwecannotusethemforourpurposes–itisjustthattheirusecaseisquitelimitedwhenwethinkaboutit.
TheInstallDatascriptTheInstallDatascriptisusedwhenwewishtoaddnewdatatoexistingtables.Thisscriptisrunonlywhenamoduleisenabled.Onceenabled,themodulegetsacorrespondingentryunderthesetup_module.data_versiontablecolumn.ThisentrypreventstheInstallDatascripttorunonanysubsequentsetup:upgradecommandexecution,wherethemodule'ssetup_versionremainsthesame.
Let'screatethe<MAGELICIOUS_DIR>/Core/Setup/InstallData.phpfilewiththefollowingcontent:
use\Magento\Framework\Setup\InstallDataInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\ModuleDataSetupInterface;
classInstallDataimplementsInstallDataInterface{
publicfunctioninstall(ModuleDataSetupInterface$setup,ModuleContextInterface$context){
$setup->startSetup();
echo'InstallData->install()'.PHP_EOL;
$setup->endSetup();
}
}
Chancesare,wewillbeinteractingwiththistypeofscriptmoreoftenthannot.ReplacingtheecholinewithmodifiedpiecesoftheequivalentMagentoInstallDatascriptmightgiveusabetterunderstandingofthepossibilitiesbehindthesescripts.
TheUpgradeDatascriptLet'screatethe<MAGELICIOUS_DIR>/Core/Setup/UpgradeData.phpfilewiththefollowingcontent:
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\ModuleDataSetupInterface;
classUpgradeDataimplements\Magento\Framework\Setup\UpgradeDataInterface{
publicfunctionupgrade(ModuleDataSetupInterface$setup,ModuleContextInterface$context){
$setup->startSetup();
if(version_compare($context->getVersion(),'2.0.2')<0){
$this->upgradeToVersionTwoZeroTwo($setup);
}
$setup->endSetup();
}
privatefunctionupgradeToVersionTwoZeroTwo(ModuleDataSetupInterface$setup){
echo'UpgradeData->upgradeToVersionTwoZeroTwo()'.PHP_EOL;
}
}
Let'sgoaheadandreplacetheecholinewithsomethingpractical,likeaddinganewcolumntoanexistingtable:
$salesSetup=$this->salesSetupFactory->create(['setup'=>$setup]);
$salesSetup->addAttribute('order','merchant_note',[
'type'=>\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
'visible'=>false,
'required'=>false
]);
Here,weusedtheinstanceofMagento\Sales\Setup\SalesSetupFactory,injectedthrough__construct.ThisfurthercreatesaninstanceoftheMagento\Sales\Setup\SalesSetupclass.WeneedthisclassinordertocreatesalesEAVattributes.Theorderentityissomewhatofastrangemix;whileitisregisteredasanEAVtypeofentityundertheeav_entity_typetable,itdoesnotreallyuseeav_attribute_*tables–itusesasinglesales_ordertabletostoreitsattributes.Wecouldhaveeasilyused(Install|Upgrade)Schemascriptstosimplyaddanewcolumnvia$setup->getConnection()->addColumn().Onceexecuted,thiscodeaddsthemerchant_notecolumntothesales_ordertable.Wewillusethiscolumnlateron,aswereachtheExtendingentitiessection.
TheRecurringDatascriptMuchlikerecurringscripts,theRecurringDatascriptsarerarelyusedinMagento.Theyalsoexecuteoneachandeverysetup:upgradecommand,regardlessoftheschema_versionordata_versionloggedagainstthesetup_moduletable.MagentoOpenSourceusesmerelythreeRecurringDatascriptsthroughoutitscodebase.
Let'screatethe<MAGELICIOUS_DIR>/Core/Setup/RecurringData.phpfilewiththefollowingcontent:
useMagento\Framework\Setup\InstallDataInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\ModuleDataSetupInterface;
classRecurringDataimplementsInstallDataInterface{
publicfunctioninstall(ModuleDataSetupInterface$setup,ModuleContextInterface$context){
$setup->startSetup();
echo'RecurringData->install()'.PHP_EOL;
$setup->endSetup();
}
}
Thesetupscriptsprovideawayforustomanagethedataanditsrepresentationinthedatabase.Whereasaddinganewattributetosimplemodelislikelyacaseofextendingitstablebyanextracolumn(*Schemascripts),addinganewattributetoanEAVmodelisamatterofaddingnewdataundertheeav_attributetable(*Datascripts).
ExtendingentitiesWeextendentitiesbyaddingadditionalattributestothem.ReferringbacktothemagicalgetterandsettermethodsmentionedinthecontextofMagento\Framework\DataObject,thelogicalthinkingmightbe:what'sthebigdeal;can'twejustaddnewdatabasecolumnsviaUpgradeSchemaandusemagicalgetterandsettermethodstogoaroundit?Theanswerisbothyesandno,butmainlyleaningtowardno–wewillsoonlearnwhy.
Tobetterexplainthis,let'stakealookatMagento\Sales\Model\Order,theentitymodel.ThismodelimplementstheMagento\Sales\Api\Data\OrderInterfaceinterface,whichfurtherextendsMagento\Framework\Api\ExtensibleDataInterface.Here,wecanseeaconstantdefiningakeyfortheextensionattributesobject.Thisissomewhatofastartingpointforextendingentities.Sufficetosay,thereisanextraabstractionlayerontopofsomeofthemodels.Thisabstractionlayer,calledservicecontracts,isasetofPHPinterfacesthatensureawell-defined,durableAPIthatothermodulesandthird-partyextensionsmightimplement.
This,however,iseasiersaidthandone.Whenyouthinkaboutit,ifwehadamodulethat'salreadyheavilyinuse,addingevenasimpleattributetooneofitsentitymodelsmightbreakitsfunctionality.Thisiswhereextensionattributescomeintothepicture.
CreatingextensionattributesCreatinganewextensionattributeforanexistingentityisusuallyacaseofdoingthefollowing:
1. Usingsetupscriptstosettheattribute,column,ortableforpersistence2. Definingtheextensionattributevia
<VendorName>/<ModuleName>/etc/extension_attributes.xml
3. Addinganafterand/orbeforeplugintothesave,get,andgetListmethodsofanentityrepository
Movingforward,wearegoingtocreateextensionattributesfortheorderentity,thatis,customer_noteandmerchant_note.
Wecanimaginecustomer_noteasanattributethatdoesnotpersistitsvalue(s)inthesales_ordertableasorderentitydoes,whereasmerchant_noteattributedoes.Thisiswhywecreatedthesales_order.merchant_notecolumnearlierviatheUpgradeData
script.
Let'sgoaheadandcreatethe<MAGELICIOUS_DIR>/Core/Api/Data/CustomerNoteInterface.phpfilewiththefollowingcontent:
interfaceCustomerNoteInterfaceextends\Magento\Framework\Api\ExtensibleDataInterface
{
constCREATED_BY='created_by';
constNOTE='note';
publicfunctionsetCreatedBy($createdBy);
publicfunctiongetCreatedBy();
publicfunctionsetNote($note);
publicfunctiongetNote();
}
Thecustomer_noteattributeisgoingtobeafull-blownobject,sowewillcreateaninterfaceforit.
Whileomittedintheexample,besuretosetthedocblocksoneachmethod,otherwisetheMagentowebAPIwillthrowanEachgettermusthaveadocblockerroroncewehookupthepluginmethods.
Wewillthencreatethe<MAGELICIOUS_DIR>/Core/Model/CustomerNote.phpfilewiththe
followingcontent:
classCustomerNoteextends\Magento\Framework\Model\AbstractExtensibleModelimplements\Magelicious\Core\Api\Data\CustomerNoteInterface
{
publicfunctionsetCreatedBy($createdBy){
return$this->setData(self::CREATED_BY,$createdBy);
}
publicfunctiongetCreatedBy(){
return$this->getData(self::CREATED_BY);
}
publicfunctiongetNote(){
return$this->getData(self::NOTE);
}
publicfunctionsetNote($note){
return$this->setData(self::NOTE,$note);
}
}
Thisclassisessentiallyourcustomer_noteentitymodel.Tokeepthingsminimal,wewilljustimplementtheCustomerNoteInterface,withoutanyextralogic.
Wewillthengoaheadandcreatethe<MAGELICIOUS_DIR>/Core/etc/extension_attributes.xmlfilewiththefollowingcontent:
<?xmlversion="1.0"?>
<config>
<extension_attributesfor="Magento\Sales\Api\Data\OrderInterface">
<attributecode="customer_note"type="Magelicious\Core\Api\Data\CustomerNoteInterface"/>
<attributecode="merchant_note"type="string"/>
</extension_attributes>
</config>
Theextension_attributes.xmlfileiswhereweregisterourextensionattributes.Thetypeargumentallowsustoregistereithercomplextypes,suchasaninterface,orscalartypes,suchasastringorinteger.Withtheextensionattributesregistered,itistimetoregisterthecorrespondingplugins.Thisisdoneviathedi.xmlfile.
Let'sgoaheadandcreatethe<MAGELICIOUS_DIR>/Core/etc/di.xmlfilewiththefollowingcontent:
<?xmlversion="1.0"?>
<config>
<preferencefor="Magelicious\Core\Api\Data\CustomerNoteInterface"type="Magelicious\Core\Model\CustomerNote"/>
<typename="Magento\Sales\Api\OrderRepositoryInterface">
<pluginname="customerNoteToOrderRepository"type="Magelicious\Core\Plugin\CustomerNoteToOrderRepository"/>
<pluginname="merchantNoteToOrderRepository"type="Magelicious\Core\Plugin\MerchantNoteToOrderRepository"/>
</type>
</config>
Thereasonforregisteringpluginsinthefirstplaceistohaveourcustomer_noteandmerchant_noteattributesavailableonthegetList,get,andsavemethodsoftheMagento\Sales\Api\OrderRepositoryInterfaceinterface.TherepositoryinterfacesarethemainwayofCRUD-ingentitiesunderservicecontracts.Withoutproperplugins,Magentosimplywouldnotseeourattributes.
Let'screatethe<MAGELICIOUS_DIR>/Core/Plugin/CustomerNoteToOrderRepository.phpfilewiththefollowingcontent:
classCustomerNoteToOrderRepository{
protected$orderExtensionFactory;
protected$customerNoteInterfaceFactory;
publicfunction__construct(
\Magento\Sales\Api\Data\OrderExtensionFactory$orderExtensionFactory,
\Magelicious\Core\Api\Data\CustomerNoteInterfaceFactory$customerNoteInterfaceFactory
){
$this->orderExtensionFactory=$orderExtensionFactory;
$this->customerNoteInterfaceFactory=$customerNoteInterfaceFactory;
}
privatefunctiongetCustomerNoteAttribute(
\Magento\Sales\Api\Data\OrderInterface$resultOrder
){
$extensionAttributes=$resultOrder->getExtensionAttributes()?:$this->orderExtensionFactory->create();
//TODO:Getcustomernotefromsomewhere(belowwefakeit)
$customerNote=$this->customerNoteInterfaceFactory->create()
->setCreatedBy('Mark')
->setNote('Thenote'.\time());
$extensionAttributes->setCustomerNote($customerNote);
$resultOrder->setExtensionAttributes($extensionAttributes);
return$resultOrder;
}
privatefunctionsaveCustomerNoteAttribute(
\Magento\Sales\Api\Data\OrderInterface$resultOrder
){
$extensionAttributes=$resultOrder->getExtensionAttributes();
if($extensionAttributes&&$extensionAttributes->getCustomerNote()){
//TODO:Save$extensionAttributes->getCustomerNote()somewhere
}
return$resultOrder;
}
}
Rightnow,therearenopluginmethodsdefined.getCustomerNoteAttributeandsaveCustomerNoteAttributeareessentiallyhelpermethodsthatwewillsoonuse.
Let'sextendourCustomerNoteToOrderRepositoryclassbyaddingtheafterpluginforthegetListmethod,asfollows:
publicfunctionafterGetList(
\Magento\Sales\Api\OrderRepositoryInterface$subject,
\Magento\Sales\Model\ResourceModel\Order\Collection$resultOrder
){
foreach($resultOrder->getItems()as$order){
$this->afterGet($subject,$order);
}
return$resultOrder;
}
Now,let'sextendourCustomerNoteToOrderRepositoryclassbyaddingtheafterpluginforthegetmethod,asfollows:
publicfunctionafterGet(
\Magento\Sales\Api\OrderRepositoryInterface$subject,
\Magento\Sales\Api\Data\OrderInterface$resultOrder
){
$resultOrder=$this->getCustomerNoteAttribute($resultOrder);
return$resultOrder;
}
Finally,let'sextendourCustomerNoteToOrderRepositoryclassbyaddingtheafterpluginforthesavemethod,asfollows:
publicfunctionafterSave(
\Magento\Sales\Api\OrderRepositoryInterface$subject,
\Magento\Sales\Api\Data\OrderInterface$resultOrder
){
$resultOrder=$this->saveCustomerNoteAttribute($resultOrder);
return$resultOrder;
}
Withthepluginsforcustomer_notesorted,let'sgoaheadandaddressthepluginsformerchant_note.Wewillcreatethe<MAGELICIOUS_DIR>/Core/Plugin/MerchantNoteToOrderRepository.phpfilewiththefollowingcontent:
classMerchantNoteToOrderRepository{
protected$orderExtensionFactory;
publicfunction__construct(
\Magento\Sales\Api\Data\OrderExtensionFactory$orderExtensionFactory
){
$this->orderExtensionFactory=$orderExtensionFactory;
}
privatefunctiongetMerchantNoteAttribute(
\Magento\Sales\Api\Data\OrderInterface$order
){
$extensionAttributes=$order->getExtensionAttributes()?:$this->orderExtensionFactory->create();
$extensionAttributes->setMerchantNote($order->getData('merchant_note'));
$order->setExtensionAttributes($extensionAttributes);
return$order;
}
privatefunctionsaveMerchantNoteAttribute(
\Magento\Sales\Api\Data\OrderInterface$order
){
$extensionAttributes=$order->getExtensionAttributes();
if($extensionAttributes&&$extensionAttributes->getMerchantNote()){
$order->setData('merchant_note',$extensionAttributes->getMerchantNote());
}
return$order;
}
}
Rightnow,therearenopluginmethodsdefined.getMerchantNoteAttributeandsaveMerchantNoteAttributeareessentiallyhelpermethodsthatwewillsoonuse.
Let'sextendourMerchantNoteToOrderRepositoryclassbyaddingtheafterpluginforthegetListmethod,asfollows:
publicfunctionafterGetList(
\Magento\Sales\Api\OrderRepositoryInterface$subject,
\Magento\Sales\Model\ResourceModel\Order\Collection$order
){
foreach($order->getItems()as$_order){
$this->afterGet($subject,$_order);
}
return$order;
}
Now,let'sextendourMerchantNoteToOrderRepositoryclassbyaddingtheafterpluginforthegetmethod,asfollows:
publicfunctionafterGet(
\Magento\Sales\Api\OrderRepositoryInterface$subject,
\Magento\Sales\Api\Data\OrderInterface$order
){
$order=$this->getMerchantNoteAttribute($order);
return$order;
}
Finally,let'sextendourMerchantNoteToOrderRepositoryclassbyaddingthebeforepluginforthesavemethod,asfollows:
publicfunctionbeforeSave(
\Magento\Sales\Api\OrderRepositoryInterface$subject,
\Magento\Sales\Api\Data\OrderInterface$order
){
$order=$this->saveMerchantNoteAttribute($order);
return[$order];
}
Theobviousdifferencehereisthat,withMerchantNoteToOrderRepository,weareusingbeforeSave,whereasweusedafterSavewithCustomerNoteToOrderRepository.Thereasonforthisisthatmerchant_noteistobesaveddirectlyontheentitywhoserepositoryweareplugginginto,thatis,itstableinthesales_orderdatabase.This
way,weuseitsMagento\Framework\DataObjectpropertiesofsetDatatofetchwhatwasassuminglynotealreadysetviaextensionattributesandpassitontotheobject'smerchant_notepropertybeforeitissaved.Magento'sbuilt-insavemechanismthentakesoverandstorestheproperty,aslongasthecorrespondingcolumnexistsinthedatabase.
Withthepluginsinplace,ourattributesshouldnowbevisibleandpersistablewhenusedthroughtheOrderRepositoryInterface.WithoutgettingtoodeepintothewebAPIatthispoint,wecanquicklytestthisviaperformingthefollowingRESTrequest:
GET/index.php/rest/V1/orders?searchCriteria[filter_groups][0][filters][0][field]=entity_id&searchCriteria[filter_groups][0][filters][0][value]=1
Host:magelicious.loc
Content-Type:application/json
Authorization:Bearer0vq6d4kabpxgc5kysb2sybf3n4ct771x
WhereastheBearertokenissomethingwegetbyrunningthefollowingRESTloginaction:
POST/index.php/rest/V1/integration/admin/token
Host:magelicious.loc
Content-Type:application/json
{"username":"john","password":"grdM%0i9a49n"}
ThesuccessfulresponseofGET/V1/ordersshouldyieldaresultofthefollowingpartialstructure:
{
"items":[
{
"extension_attributes":{
"shipping_assignments":[...],
"customer_note":{
"created_by":"Mark",
"note":"NoteABC"
},
"merchant_note":"NoteXYZ"
}
}
]
}
Wecanseethatourtwoattributesarenicelynestedwithintheextension_attributeskey.
Postman,theAPIdevelopmenttool,makesiteasytotestAPIs.Seehttps://www.getpostman.comformoreinformation.
TheOrderRepositoryInterfacetowebAPIRESTrelationshipmapsoutasfollows:
getList:GET/V1/orders(plusthesearchcriteriapart)get:GET/V1/orders/:idsave:POST/V1/orders/create
WewilllearnmoreaboutthewebAPIinthenextchapter.Theexamplegivenherewasmerelyforthepurposeofvisualizingtheworkwehavedonearoundplugins.Usingextensionattributes,withthehelpofplugins,wehaveessentiallyextendedtheMagentowebAPI.
SummaryThroughoutthischapter,welearnedhowtodifferentiatethethreetypesofMagentomodels:non-persistable,persistablesimple,andpersistableEAV.TheinnersofEAVmodelsareleftoutofscopeduetotheirinherentlycomplexnature.Wethentookalookthroughsixdifferentsetupscripts.Thesegiveusagreatdealofflexibilityoverschemaanddatamanagement.Combinedwithextensionattributes,wegetapowerfulmechanismforextendingbuilt-inentities.Thoughsomewhattedious,theextensionattributesmechanismuseofinterfacesensuresthatintegratorscanextendthisbuilt-infunctionalitywithcomplexdatatypes.
Movingforward,wearegoingtotakealookatthepowerfulwebAPIthat'simplementedinMagento.
UnderstandingWebAPIsWebapplicationprogramminginterfaces(API)playamajorroleinmodernapplicationdevelopment.Theyallowvariousthird-partyintegratorstointeractwithapplicationsthroughtheHTTPlayer.MagentosupportsbothRepresentationalStateTransfer(REST)andSimpleObjectAccessProtocol(SOAP)APIs.ItswebAPIframeworkisbasedonthecreate,read,update,delete(CRUD)andsearch(searchcriteria)models.ThescopeoffunctionalitythatAPIsofferisquitebig,allowingustousethemforawiderangeoftasks,suchascreatingacompletelynewshoppingapplication,integratingwithcustomerrelationshipmanagement(CRM)systems,enterpriseresourceplanning(ERP)systems,andcontentmanagementsystems(CMS),aswellascreatingJavaScriptwidgetsintheMagentostorefrontitself.
Movingforward,wearegoingtotakeacloserlookatthefollowingwebAPIsections:
TypesofusersTypesofauthenticationTypesofendpointsUsingexistingWebAPIsCreatingcustomWebAPIsUnderstandingsearchcriteria
TechnicalrequirementsYouwillneedtohavebasicknowledgeofPHP,OOP,JavaScript,andXML.YouwillalsoneedApache,MySQL,andAMPPSinstalledonyoursystemtoexecutethecodes.
ThecodefilesofthischaptercanbefoundonGitHub:https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
CheckoutthefollowingvideotoseetheCodeinAction:
http://bit.ly/2Oz3Gqs.
TypesofusersTheMagentowebAPIframeworkdifferentiatesthreefundamentaltypesofusers:
Guest:Authorizedagainstananonymousresource:
<resources>
<resourceref="anonymous"/>
</resources>
Customer:Authorizedagainstaselfresource:
<resources>
<resourceref="self"/>
</resources>
Integrator:Authorizedagainstaspecificresourcedefinedinacl.xml:
<resources>
<resourceref="Magento_Cms::save""/>
</resources>
Tofurtherunderstandwhatthismeans,weneedtounderstandthelinkbetween<VendorName>/<ModuleName>/acl.xmland<VendorName>/<ModuleName>/webapi.xml.
Theacl.xmliswherewedefineouraccessresources.Let'stakeacloserlookatthepartialextractofonesuchresource,definedinthe<MAGENTO_DIR>/module-cms/etc/acl.xmlfile:
<config>
<acl>
<resources>
<resourceid="Magento_Backend::admin">
<resourceid="Magento_Backend::content">
<resourceid="Magento_Backend::content_elements">
<resourceid="Magento_Cms::page"title="Pages">
<resourceid="Magento_Cms::save"title="SavePage"/>
</resource>
</resource>
</resource>
</resource>
</resources>
</acl>
</config>
OurfocushereisontheMagento_Cms::saveresource.Magentomergesallofthese
individualacl.xmlfilesintoonebigACLtree.WecanseethistreeintwoplacesintheMagentoadminarea:
TheRoleResourcetaboftheSystem|Permissions|UserRoles|Edit|AddNewRolescreenTheAPItaboftheSystem|Extensions|Integrations|Edit|AddNewIntegrationscreen:
ThesearethetwoscreenswherewedefineaccesspermissionsforastandardadminuserandaspecialwebAPIintegratoruser.ThisisnottosaythatastandardadminusercannotexecutewebAPIcalls.ThedifferencewillbecomemoreobviouswhenwegettotheTypesofauthenticationsection.
Tothispoint,theseresourcesdon'treallydoanythingontheirown.Simplydefiningthemwithinacl.xmlwon'tmagicallymakeaCMSpageinthiscaseaccess-protected,oranythinglikethat.Thisiswherecontrollerscomeintothemix,asoneexampleofanaccess-controllingmechanism.AquicklookupagainstMagento_Cms::savestringusage,revealsaMagento\Cms\Controller\Adminhtml\Page\EditclassusingitaspartofitsconstADMIN_RESOURCE='Magento_Cms::save'definition.
TheADMIN_RESOURCEconstantisdefinedfurtherdowntheinheritancechain,onthe\Magento\Backend\App\AbstractActionasconstADMIN_RESOURCE='Magento_Backend::admin'.Thisisfurtherusedbythe_isAllowedmethodimplementationasfollows:
protectedfunction_isAllowed()
{
return$this->_authorization->isAllowed(static::ADMIN_RESOURCE);
}
TheAbstractActionclasshereisthebasisforprettymuchanyMagentoadmincontroller.Thismeansthatthecontrolleristheonethatutilizestheresourcedefinedinacl.xml,whereasdefinitionsinacl.xmlservethepurposeofbuildingtheACLtree,whichwecanmanagefromtheMagentoadmininterface.Thismeansthatanyonetryingtoaccessthecms/page/editURLinadminmusthaveaMagento_Cms::saveresourcepermissiontodoso.Otherwise,the_isAllowedmethod,readingtheADMIN_RESOURCEvalue,willreturnfalseandforbidaccesstothepage.
WebAPIs,ontheotherhand,don'tusecontrollers,sothereisnoaccesstotheADMIN_RESOURCEconstantandthe_isAllowedmethod.APIsusewebapi.xmltodefineroutes.Let'sfollowupwiththeCMSpagesaveanalogue,asperthe<MAGENTO_DIR>/module-cms/etc/webapi.xmlfile:
<routes>
<routeurl="/V1/cmsPage"method="POST">
<serviceclass="Magento\Cms\Api\PageRepositoryInterface"method="save"/>
<resources>
<resourceref="Magento_Cms::page"/>
</resources>
</route>
<routeurl="/V1/cmsPage/:id"method="PUT">
<serviceclass="Magento\Cms\Api\PageRepositoryInterface"method="save"/>
<resources>
<resourceref="Magento_Cms::page"/>
</resources>
</route>
</routes>
Theindividualroutedefinitionbindstogetherafewthings.TheurlandmethodargumentofarouteelementspecifywhatURLwilltriggerthisroute.Theclassandmethodargumentsofaserviceelementspecifywhichinterfaceandmethodonthatinterfacewillexecuteoncetherouteistriggered.Finally,therefargumentofaresourceelementspecifiesthesecuritychecktobeexecuted.IfauserexecutingawebAPIcallisunauthenticatedorauthenticatedwitharolethatdoesnothaveMagento_Cms::page,therequestwon'texecutetheservicemethodspecified.
Thecustomertypeofuseristhemostconvenientforworkingwithwidgets.The
Magentocheckoutisanexcellentexampleofthat.ThewholecheckoutisafullyAJAX-enabledapponitsown,separatefromtheusualMagentostorefront,suchasitsCMS,category,andproductpages.
TypesofauthenticationMagentosupportsthreedifferenttypesofauthenticationmethods:
Session-basedauthentication:BestsuitedforJavaScriptwidgetapplicationsrunningaspartoftheMagentostorefrontitself.Magentousesthelogged-instateofanadminuserorcustomertoverifytheiridentityandauthorizeaccesstotherequestedresource.Token-basedauthentication:Bestsuitedformobileorothertypesofapplicationsthatwishtoavoidthecomplexitiesoffull-blownOAuth-basedauthentication.Toobtainthetoken(withREST),oneinitiallyusesthePOST/V1/integration/customer/tokenorthePOST/V1/integration/admin/token.Asuccessfulresponsereturnsarandom32-character-longstring,forexample,8pcvbwrp97l5m1pvcdnis6e3930n4rsj.Thisisourtoken,usedforanysubsequentAPIcalls,viaaheadergivenasAuthorization:Bearer<token>.Thesimplicitybehindthisauthenticationmakesitanappealingchoicefordevelopers.OAuth-basedauthentication:Bestsuitedforthird-partyapplicationsthatintegratewithMagentoonbehalfoftheuser,withoutrevealingorstoringanyuserIDsorpasswords.ThestartingpointforsettingupOAuth-basedauthenticationisforaMagentoadminusertocreateintegration,undertheSystem|Extensions|Integration|AddNewIntegrationscreen.HerewecanprovideoptionssuchasCallbackURLandIdentitylinkURL,whichdefinetheexternalapplicationendpointthatreceivestheOAuthcredentials.Ifgiven,thevaluesoftheselinkspointtotheexternalappthatstandsastheOAuthclient.SuccessfullysavedintegrationgeneratesthekeyOAuthartefacts,suchasConsumerKey,ConsumerSecret,AccessToken,andAccessTokenSecret.
UsingOAuth-basedauthenticationexceedsthescopeofthisbook,whichiswhymovingforward,allofourexampleswillusesimplertoken-basedauthentication.
TypesofAPIsMagentosupportstwotypesofAPIs:
RepresentationalStateTransfer(REST):TheendpointsforAPIsdependonwebapi.xmlandtheindividualurlargumentsofeachrouteelement,aswewillsoonsee.Theauthenticationiscarriedoverinarequest'sheaderviaaBearertoken.SimpleObjectAccessProtocol(SOAP):TheWebServicesDescriptionLanguage(WSDL)isavailableviaaURLsuchashttp://magelicious.loc/soap/default?wsdl&services=catalogProductRepositoryV1.Whereasthedefaultstringisoptional,anditmatchesthecodenameoftheMagentostoreinthiscase,ifomitted,Magentowilldefaulttoadefaultstore,whateveritscodemightbe.Likewise,theservicesparameteracceptsoneormore(comma-separated)listsofservices.ThefulllistofavailableservicescanbeobtainedviaaURLsuchashttp://magelicious.loc/soap/default?wsdl_list.Withoutgoingintothedetailsofit,sufficeittosaythatMagentogeneratestheservicenamesautomaticallybasedonmoduleandinterfacenames.MuchlikewithRESTAPIs,theauthenticationiscarriedoverinarequest'sheaderviaaBearertoken.
Thegreatthingaboutthesetwoisthatwedon'tgettowritetwodifferentAPIsinMagento.TheapproachtowritingAPIsisunified,sotospeak.Wedefinesomeinterfaces,classes,andconfigurations,andMagentothengeneratestheAPIendpointsforbothRESTandSOAPonitsown.Thus,theRESTvs.SOAPchoicereallyonlybecomesaquestionwhenweconsumeAPIs,notwhilewewritethem.
UsingSOAPservicesexceedsthescopeofthisbook,whichiswhymovingforward,allofourexampleswilluseRESTAPIs.
UsingexistingwebAPIsTheCRUDandsearchmodelsofwebAPIsareimplementedthroughasetof*RepositoryInterfaceinterfaces,foundinthe<VendorName>/<ModuleName>/Api/<EntityName>RepositoryInterface.phpfiles.
Themajorityoftheserepositoryinterfacesdefineaspecificsetofcommonmethods:
save
get
getById
getList
delete
deleteById
Thedatatypethatflowsthroughthesemethodsfollowsacertainpattern,whereeachentitypassingthroughanAPIhasadatainterfacedefinedina<VendorName>/<ModuleName>/Api/Data/<EntityName>Interface.phpfile.
Let'stakeacloserlookat<MAGENTO_DIR>/module-cms/Api/BlockRepositoryInterface.php:
interfaceBlockRepositoryInterface
{
publicfunctionsave(
\Magento\Cms\Api\Data\BlockInterface$block
);
publicfunctiongetById($blockId);
publicfunctiongetList(
\Magento\Framework\Api\SearchCriteriaInterface$searchCriteria
);
publicfunctiondelete(
\Magento\Cms\Api\Data\BlockInterface$block
);
publicfunctiondeleteById($blockId);
}
Theconcreteimplementationsofrepositoryinterfacescanusuallybefoundinthe<VendorName>/<ModuleName>/Model/<EntityName>Repository.phporthe<VendorName>/<ModuleName>/Model/ResourceModel/<EntityName>Repository.phpfiles.Theexact
locationisnotthatrelevant,aswebapi.xmlshouldalwaysuseaninterfaceforaclassargumentforitsserviceelementdefinition.Themappingbetweentheinterfaceandconcreteimplementationthenhappensinthemodule'sdi.xmlfileviaapreferencedefinition.Fromanintegrator'spointofview,usingAPIsdoesnotrequireanyknowledgeofconcreteimplementations.
ThePHPDoc@returntagisarequirementforeverygettermethodonanAPIinterface,otherwise,Eachgettermusthaveadocblockerroristhrown.
TheSwaggerURL,http://magelicious.loc/swagger,willgenerateaSwaggerUIinterface,thatallowsustovisualizeandinteractwiththeAPI'sresources:
Bydefault,documentationreturnedhereislimitedtoanonymoususersonly.GeneratingavalidAPIkey,viathePOST/V1/integration/customer/tokenorPOST/V1/integration/admin/tokenwillunlockthedocumentationforalltheresourcesavailabletoagivenuser.WhileSwaggercertainlyhasitsplaceindevelopmentworkflows,oftentimesthePostmantoolisamorerobustsolutionforthoseworkingextensivelywithAPIs.
Let'sgoaheadandCRUDourwaythroughthecmsBlockinterface,usingRESTendpoints:
save(createanewblock)POST/V1/cmsBlocksave(updateanexistingblockbyid)PUT/V1/cmsBlock/:idgetById(getanexistingblockbyid)GET/V1/cmsBlock/:blockIddeleteById(deleteanexistingblock)DELETE/V1/cmsBlock/:blockIdgetList(getanarrayofexistingblocks)GET/V1/cmsBlock/search
Wewillbeusingtheintegratortypeofuser.ThiswillbeourMagentoadminuser,assignedeitherfullresources,oratleasttheResources|Content|Elements|BlocksresourceundertheRoleResourcetaboftheSystem|Permissions|UserRoles|Edit|AddNewRolescreen.
Westartwiththeadminloginrequest,inordertoobtainatokenforlaterrequests:
POST/index.php/rest/V1/integration/admin/tokenHTTP/1.1
Host:magelicious.loc
Content-Type:application/json
{
"username":"branko",
"password":"jrdJ%0i9a69n"
}
ThesuccessfulJSONresponseshouldcontainourAPItoken,whichwewillbeusingforanysubsequentAPIcalls.Thetokenitselfisstoredintheoauth_tokentable,underthetokencolumn.Wefurtherhaveconsumer_id,admin_id,andcustomer_idcolumnsinthattable.Thesegetfilleddependingontheusertypeweusedtologin.Bothconsumer_idandadmin_idareoftheintegratortype.Thesecolumnsgetfilledaccordinglydependingontheuserandauthenticationtypesused;asincustomerversusintegrator,andtoken-basedvsOAuth-basedvssession-basedauthentication.
Nowlet'screateanewblockviaPOST/V1/cmsBlock;thistriggersthesavemethod:
POST/rest/V1/cmsBlockHTTP/1.1
Host:magelicious.loc
Content-Type:application/json
Authorization:Bearer8pcvbwrp97l5m1pvcdnis6e3930n4rsj
{
"block":{
"identifier":"x-block",
"title":"TheXBlock",
"content":"<p>The<strong>XBlock</strong>Content...</p>",
"active":true
}
}
ThesuccessfulJSONresponseshouldreturnournewlycreatedblock:
{
"id":1,
"identifier":"x-block",
"title":"TheXBlock",
"content":"<p>The<strong>XBlock</strong>Content...</p>",
"active":true
}
Nowlet'supdatetheexistingcmsBlockviaPUT/V1/cmsBlock/:id;thistriggersthesavemethod:
PUT/rest/V1/cmsBlock/1HTTP/1.1
Host:magelicious.loc
Content-Type:application/json
Authorization:Bearer8pcvbwrp97l5m1pvcdnis6e3930n4rsj
{
"block":{
"identifier":"y-block",
"title":"TheYBlock",
"content":"<p>The<strong>YBlock</strong>Content...</p>",
"active":true
}
}
ThesuccessfulJSONresponseshouldreturntheupdatedblock:
{
"id":1,
"identifier":"y-block",
"title":"TheYBlock",
"content":"<p>The<strong>YBlock</strong>Content...</p>",
"active":true
}
Let'snowfetchoneoftheexistingblocksviaGET/V1/cmsBlock/:blockId;thistriggersthegetByIdmethod:
GET/rest/V1/cmsBlock/1HTTP/1.1
Host:magelicious.loc
Content-Type:application/json
Authorization:Bearer8pcvbwrp97l5m1pvcdnis6e3930n4rsj
ThesuccessfulJSONresponseisstructurallyidenticaltothatofthesavemethod.
Now,let'strydeletingoneoftheblocksviaDELETE/V1/cmsBlock/:blockId;thistriggersthedeleteByIdmethod:
DELETE/rest/V1/cmsBlock/2HTTP/1.1
Host:magelicious.loc
Content-Type:application/json
Authorization:Bearer8pcvbwrp97l5m1pvcdnis6e3930n4rsj
ThesuccessfulJSONresponsereturnsasingletrueorfalse.
Finally,let'stryfetchingthelistofblocksviaGET/V1/cmsBlock/search;thistriggersthegetListmethod:
GET/rest/V1/cmsBlock/search?searchCriteria[filter_groups][0][filters][0][field]=title&searchCriteria[filter_groups][0][filters][0][value]=%Block%&searchCriteria[filter_groups][0][filters][0][condition_type]=likeHTTP/1.1
Host:magelicious.loc
Content-Type:application/json
Authorization:Bearer8pcvbwrp97l5m1pvcdnis6e3930n4rsj
Sadly,theGETrequestdoesnotallowforthebody,so?searchCriteria...hastobepassedviaaURL.
ThesuccessfulJSONresponsereturnsanobjectcomprisedofitems,search_criteria,andtotal_counttop-levelkeys:
{
"items":[
{
"id":4,
"identifier":"x-block",
"title":"TheXBlock",
"content":"The<strong>XBlock</strong>Content...",
"creation_time":"2018-06-2307:30:06",
"update_time":"2018-06-2307:30:06",
"active":true
},
{
"id":5,
"identifier":"y-block",
"title":"TheYBlock",
"content":"The<strong>YBlock</strong>Content...",
"creation_time":"2018-06-2307:30:14",
"update_time":"2018-06-2307:30:14",
"active":true
}
],
"search_criteria":{...},
"total_count":2
}
Wewilladdressthesearch_criteriainmoredetaillateron.
CreatingcustomwebAPIsLet'sgoaheadandcreateaminiature,yetfull-blownMagentomoduleMagelicious_BoxythatdemonstratestheentireflowofcreatingacustomwebAPI.
Westartoffbydefiningamodule<MAGELICIOUS_DIR>/Boxy/registration.phpasfollows:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Magelicious_Boxy',
__DIR__
);
Wethendefinethe<MAGELICIOUS_DIR>/Boxy/etc/module.xmlasfollows:
<config>
<modulename="Magelicious_Boxy"setup_version="2.0.2"/>
</config>
Wethendefinethe<MAGELICIOUS_DIR>/Boxy/Setup/InstallSchema.phpthataddsthefollowingtable:
$table=$setup->getConnection()
->newTable($setup->getTable('magelicious_boxy_box'))
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,[
'identity'=>true,
'unsigned'=>true,
'nullable'=>false,
'primary'=>true
],'EntityID'
)
->addColumn(
'title',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
32,
['nullable'=>false],'Title'
)
->addColumn(
'content',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
null,
['nullable'=>false],'Content'
)
->setComment('MageliciousBoxyBoxTable');
$setup->getConnection()->createTable($table);
Wethendefine<MAGELICIOUS_DIR>/Boxy/Api/Data/BoxInterface.phpasfollows:
interfaceBoxInterface{
constBOX_ID='box_id';
constTITLE='title';
constCONTENT='content';
publicfunctiongetId();
publicfunctiongetTitle();
publicfunctiongetContent();
publicfunctionsetId($id);
publicfunctionsetTitle($title);
publicfunctionsetContent($content);
}
Wethendefine<MAGELICIOUS_DIR>/Boxy/Api/Data/BoxSearchResultsInterface.phpasfollows:
interfaceBoxSearchResultsInterfaceextends\Magento\Framework\Api\SearchResultsInterface
{
publicfunctiongetItems();
publicfunctionsetItems(array$items);
}
Wethenaddthe<MAGELICIOUS_DIR>/Boxy/Api/BoxRepositoryInterface.phpasfollows:
interfaceBoxRepositoryInterface
{
publicfunctionsave(\Magelicious\Boxy\Api\Data\BoxInterface$box);
publicfunctiongetById($boxId);
publicfunctiongetList(\Magento\Framework\Api\SearchCriteriaInterface$searchCriteria);
publicfunctiondelete(\Magelicious\Boxy\Api\Data\BoxInterface$box);
publicfunctiondeleteById($boxId);
}
Wethendefinethe<MAGELICIOUS_DIR>/Boxy/Model/Box.phpasfollows:
classBoxextends\Magento\Framework\Model\AbstractModelimplements\Magelicious\Boxy\Api\Data\BoxInterface
{
protectedfunction_construct(){
$this->_init(\Magelicious\Boxy\Model\ResourceModel\Box::class);
}
publicfunctiongetId(){
return$this->getData(self::BOX_ID);
}
publicfunctiongetTitle(){
return$this->getData(self::TITLE);
}
publicfunctiongetContent(){
return$this->getData(self::CONTENT);
}
publicfunctionsetId($id){
return$this->setData(self::BOX_ID,$id);
}
publicfunctionsetTitle($title){
return$this->setData(self::TITLE,$title);
}
publicfunctionsetContent($content){
return$this->setData(self::CONTENT,$content);
}
}
Wethendefinethe<MAGELICIOUS_DIR>/Boxy/Model/ResourceModel/Box.phpasfollows:
classBoxextends\Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
protectedfunction_construct(){
$this->_init('magelicious_boxy_box','entity_id');
}
}
Wethendefinethe<MAGELICIOUS_DIR>/Boxy/Model/ResourceModel/Box/Collection.phpasfollows:
classCollection
{
protectedfunction_construct(){
$this->_init(
\Magelicious\Boxy\Model\Box::class,
\Magelicious\Boxy\Model\ResourceModel\Box::class
);
}
}
Wethendefinethe<MAGELICIOUS_DIR>/Boxy/Model/BoxRepository.phpasfollows:
classBoxRepositoryimplements\Magelicious\Boxy\Api\BoxRepositoryInterface
{
protected$boxFactory;
protected$boxResourceModel;
protected$searchResultsFactory;
protected$collectionProcessor;
publicfunction__construct(
\Magelicious\Boxy\Api\Data\BoxInterfaceFactory$boxFactory,
\Magelicious\Boxy\Model\ResourceModel\Box$boxResourceModel,
\Magelicious\Boxy\Api\Data\BoxSearchResultsInterfaceFactory$searchResultsFactory,
\Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface$collectionProcessor
)
{
$this->boxFactory=$boxFactory;
$this->boxResourceModel=$boxResourceModel;
$this->searchResultsFactory=$searchResultsFactory;
$this->collectionProcessor=$collectionProcessor;
}
//Todo...
}
Let'sgoaheadandamendtheBoxRepositorywiththesavemethodasfollows:
publicfunctionsave(\Magelicious\Boxy\Api\Data\BoxInterface$box)
{
try{
$this->boxResourceModel->save($box);
}catch(\Exception$e){
thrownew\Magento\Framework\Exception\CouldNotSaveException(__($e->getMessage()));
}
return$box;
}
Let'sgoaheadandamendtheBoxRepositorywiththegetByIdmethodasfollows:
publicfunctiongetById($boxId){
$box=$this->boxFactory->create();
$this->boxResourceModel->load($box,$boxId);
if(!$box->getId()){
thrownew\Magento\Framework\Exception\NoSuchEntityException(__('Boxwithid"%1"doesnotexist.',$boxId));
}
return$box;
}
Let'sgoaheadandamendtheBoxRepositorywiththegetListmethodasfollows:
publicfunctiongetList(\Magento\Framework\Api\SearchCriteriaInterface$searchCriteria){
$collection=$this->boxCollectionFactory->create();
$this->collectionProcessor->process($searchCriteria,$collection);
$searchResults=$this->searchResultsFactory->create();
$searchResults->setSearchCriteria($searchCriteria);
$searchResults->setItems($collection->getItems());
$searchResults->setTotalCount($collection->getSize());
return$searchResults;
}
Let'sgoaheadandamendtheBoxRepositorywiththedeletemethodasfollows:
publicfunctiondelete(\Magelicious\Boxy\Api\Data\BoxInterface$box){
try{
$this->boxResourceModel->delete($box);
}catch(\Exception$e){
thrownew\Magento\Framework\Exception\CouldNotDeleteException(__($e->getMessage()));
}
returntrue;
}
Let'sgoaheadandamendtheBoxRepositorywiththedeleteByIdmethodasfollows:
publicfunctiondeleteById($boxId){
return$this->delete($this->getById($boxId));
}
Wethendefinethe<MAGELICIOUS_DIR>/Boxy/etc/di.xmlasfollows:
<config>
<preferencefor="Magelicious\Boxy\Api\Data\BoxInterface"type="Magelicious\Boxy\Model\Box"/>
<preferencefor="Magelicious\Boxy\Api\Data\BoxSearchResultsInterface"type="Magento\Framework\Api\SearchResults"/>
<preferencefor="Magelicious\Boxy\Api\BoxRepositoryInterface"type="Magelicious\Boxy\Model\BoxRepository"/>
</config>
Wethendefinethe<MAGELICIOUS_DIR>/Boxy/etc/acl.xmlasfollows:
<config>
<acl>
<resources>
<resourceid="Magento_Backend::admin">
<resourceid="Magento_Sales::sales">
<resourceid="Magento_Sales::sales_operation">
<resourceid="Magento_Sales::shipment">
<resourceid="Magelicious_Boxy::box"title="BoxyBox">
<resourceid="Magelicious_Boxy::box_get"title="Get"/>
<resourceid="Magelicious_Boxy::box_search"title="Search"/>
<resourceid="Magelicious_Boxy::box_save"title="Save"/>
<resourceid="Magelicious_Boxy::box_update"title="Update"/>
<resourceid="Magelicious_Boxy::box_delete"title="Delete"/>
</resource>
</resource>
</resource>
</resource>
</resource>
</resources>
</acl>
</config>
Wethendefinethe<MAGELICIOUS_DIR>/Boxy/etc/webapi.xmlasfollows:
<routes>
<routeurl="/V1/boxyBox/:boxId"method="GET">
<serviceclass="Magelicious\Boxy\Api\BoxRepositoryInterface"method="getById"/>
<resources>
<resourceref="Magelicious_Boxy::box_get"/>
</resources>
</route>
<routeurl="/V1/boxyBox/search"method="GET">
<serviceclass="Magelicious\Boxy\Api\BoxRepositoryInterface"method="getList"/>
<resources>
<resourceref="Magelicious_Boxy::box_search"/>
</resources>
</route>
<routeurl="/V1/boxyBox"method="POST">
<serviceclass="Magelicious\Boxy\Api\BoxRepositoryInterface"method="save"/>
<resources>
<resourceref="Magelicious_Boxy::box_save"/>
</resources>
</route>
<routeurl="/V1/boxyBox/:id"method="PUT">
<serviceclass="Magelicious\Boxy\Api\BoxRepositoryInterface"method="save"/>
<resources>
<resourceref="Magelicious_Boxy::box_update"/>
</resources>
</route>
<routeurl="/V1/boxyBox/:boxId"method="DELETE">
<serviceclass="Magelicious\Boxy\Api\BoxRepositoryInterface"method="deleteById"/>
<resources>
<resourceref="Magelicious_Boxy::box_delete"/>
</resources>
</route>
</routes>
Withallthesebitsinplace,ourAPIisnowready.Weshouldnowbeableto
CRUDourwaythroughBoxyBoxthesamewaywedidwiththeCMSblock.Whiletherecertainlyisagreatdealofboilerplatecodetogoaround,ourAPIisnowbothREST-andSOAP-ready.
UnderstandingsearchcriteriaThesearchCriteriaparameterofaGETrequestallowsforsearchresultsfiltering.Thekeytousingitcomesdowntounderstandingitsstructureandtheavailableconditiontypes.
Observingthe\Magento\Framework\Api\SearchCriteriaInterfaceinterface,andtheMagento\Framework\Api\SearchCriteriaclassasitsconcreteimplementation,wecaneasilyconcludethefollowingsearch_criteriastructure:
"search_criteria":{
"filter_groups":[],
"current_page":1,
"page_size":10,
"sort_orders":[]
}
Whereasthemandatoryfilter_groupsparameteranditsstructureareshownasfollows:
"filter_groups":[
{
"filters":[
{
"field":"fieldOrAttrName",
"value":"fieldOrAttrValue",
"condition_type":"eq"
},
{
//LogicalOR
}
]
},
{
//LogicalAND
}
],
Conditionsnestedundertheindividualfilterskey,correspondtotheLogicalORcondition.
Thelistofcondition_typevaluesincludes:
eq:Equalsfinset:Avaluewithinasetofvaluesfrom:Thebeginningofarange,mustbeusedwithatoconditiontype
gt:Greaterthangteq:Greaterthanorequalin:In,thevaluecancontainacomma-separatedlistofvalueslike:Like,thevaluecancontaintheSQLwildcardcharacterslt:Lessthanlteq:Lessthanorequaltomoreq:Moreorequaltoneq:Notequaltonin:Notin;thevaluecancontainacomma-separatedlistofindividualvaluesnotnull:Notnullnull:Null
Combiningtheseconditiontypeswillallowustofiltersearchresultsprettymuchanywaywewant.
Theoptionalsort_ordersparameteranditsstructureunfoldasfollows:
"sort_orders":[
{
"field":"fieldOrAttrName",
"direction":"ASC"
}
]
ThelistofdirectionvaluesincludesASCforascendingandDESCfordescendingsortorders.
ThesearchCriteriaisseeminglythemostcomplex,yetmostpowerfulaspectofasearchAPI.Understandinghowitworksisessentialforeffectivequerying.
SummaryInthischapter,wehavecoveredvaluablewebAPIelements.WelearnedhowtodifferentiatebetweentypesofwebAPIusers,andtheauthenticationandmethodsprovidedtodoso.WealsolearnedhoweasyitistocreateourownAPIswithjustafewlinesofXML.WesawhowtheroutedefinitionallowsforeasybindingbetweenwhatcomesviaanHTTPrequesttowhatexecutesincode,respectingtheaccesslistpermissionsintheprocess.ThevalueofbuildingAPIsaspartofourdistributablemodulesliesintheirextensibility.APIsforceustoembracetheinterfacewayofthinking,thusallowingotherstouseandextendourcodeeasilyandsecurely.Thepreferencemechanismweintroducedinpreviouschapters,throughdi.xmlfiles,allowsotherstochangethebehaviorbehindtheinterfaceeasily.
Movingforward,wearegoingtotakeamorethoroughandroundedlookatbuildinganddistributingourextensionsviaComposerandPackagist.
BuildingandDistributingExtensionsAttheverystartofourjourney,wementionedMagentosourcefilesbeingdistributedviathreedifferentchannels:asourcefilearchive,aGitrepository,andaComposerrepository.TheComposerapproachisthepreferredway.Whetherwearecodingamodule,library,themeorlanguagecomponent,usingtheComposerallowsforaneasyandautomateddependencymanagement,whichisnotpossibleotherwise.Magento'sbuilt-inComponentManagercanupdate,uninstall,enable,ordisableextensionsinstalledviaComposer.ThisimpliessourcesfromPackagist,MagentoMarketplace,orothercomposersources,aslongastheyhaveacomposer.jsonfile.
Movingforward,wearegoingtotakeacloserlookatthefollowingtopics:
BuildingashippingextensionDistributingviaGitHubDistributingviaPackagist
Thetermsmodule,extension,package,andcomponentareusedsomewhatinterchangeablyinMagento.Whiledeveloping,themodule.xmlimpliesmoduleterminology,andregistration.phpimpliescomponentterminology.However,distributingthemviaPackagistandMagentomarketplaceoftenimpliespackageandextensionterminologies.Magento-wise,toallintentsandpurposes,theyrefertothesamething.
TechnicalrequirementsYouwillneedtohavebasicknowledgeofPHP,OOP,JavaScript,andXML.YouwillalsoneedApache,MySQL,andAMPPSinstalledonyoursystemtoexecutethecodes.
ThecodefilesofthischaptercanbefoundonGitHub:https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
CheckoutthefollowingvideotoseetheCodeinAction:
http://bit.ly/2xoS5ms.
BuildingashippingextensionOutofthebox,Magentoprovidesseveralshippingmethodsofitsown.Unlikepaymentmethods,whichtendtobelessdiverseamongdifferentwebshops,shippingmethodsareoftenanareaofcustomizationamongmerchants,whichiswhybuildingacustomizedshippingextensionisanessentialskillforeveryMagentodeveloper.
Therearetwotypesofshippingmethods:
online:Theseshippingmethodsbasetheirshippingcalculationontheshippingservicetheyconnectto.TheMagentoOpenSourceincludesfollowingmodulesthatprovideonlineshippingmethods:Magento_Ups,Magento_Usps,Magento_Fedex,Magento_Dhl.offline:Theseshippingmethodsdotheirownshippingcalculation,withoutconnectingtoanexternalservice.TheMagentoOpenSourceincludesabuilt-inMagento_OfflineShippingmodule,whichprovidesFlatRate,TableRate,Free,andStorePickupshippingmethods.
Let'sgoaheadandcreateaMagentoshippingextensionMagelicious_RoyalTrek.TheextensionassumesanimaginaryRoyalTrekcarrier,withtwoofflineshippingmethods:RoyalTrekStandardandRoyalTrek48h.
Wewillstartoffbydefining<MAGELICIOUS_DIR>/RoyalTrek/registration.phpasfollows:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Magelicious_RoyalTrek',
__DIR__
);
Wecanthendefinethe<MAGELICIOUS_DIR>/RoyalTrek/etc/module.xmlasfollows:
<config>
<modulename="Magelicious_RoyalTrek"setup_version="1.0.0"/>
</config>
Withthesetwofilesinplace,Magentoshouldalreadyseeourmodule,whenenabled.
Wecanthengoaheadanddefinethe<MAGELICIOUS_DIR>/RoyalTrek/composer.jsonasfollows:
{
"name":"magelicious/module-royal-trek",
"description":"TheRoyalTrekshipping",
"require":{
"php":"7.0.2|7.0.4|~7.0.6|~7.1.0"
},
"type":"magento2-module",
"version":"1.0.0",
"license":[
"OSL-3.0",
"AFL-3.0"
],
"autoload":{
"files":[
"registration.php"
],
"psr-4":{
"Magelicious\\RoyalTrek\\":""
}
}
}
Wecanthendefinethe<MAGELICIOUS_DIR>/RoyalTrek/etc/adminhtml/system.xmlasfollows:
<config>
<system>
<sectionid="carriers">
<groupid="royaltrek">
<label>RoyalTrekShipping</label>
<fieldid="active"type="select">
<label>Enabled</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<fieldid="title"type="text">
<label>Title</label>
</field>
<fieldid="sallowspecific"type="select">
<label>ShiptoApplicableCountries</label>
<frontend_class>shipping-applicable-country</frontend_class>
<source_model>Magento\Shipping\Model\Config\Source\Allspecificcountries</source_model>
</field>
<fieldid="specificcountry"type="multiselect">
<label>ShiptoSpecificCountries</label>
<can_be_empty>1</can_be_empty>
<source_model>Magento\Directory\Model\Config\Source\Country</source_model>
</field>
<fieldid="showmethod"type="select"">
<label>ShowMethodifNotApplicable</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<fieldid="specificerrmsg"type="textarea">
<label>DisplayedErrorMessage</label>
</field>
<fieldid="sort_order"type="text">
<label>SortOrder</label>
<validate>validate-numbervalidate-zero-or-greater</validate>
</field>
</group>
<!--todo...-->
</section>
</system>
</config>
Thissetsthegeneralconfigurationoptionsforourshippingmethods.Thesallowspecific,specificcountry,showmethod,specificerrmsgand,sort_orderarecommonconfigurationelementsofeachshippingmethod,asseenbyexaminingtheMagento\Shipping\Model\Carrier\AbstractCarrierclass.
Wecanthenextendthe<MAGELICIOUS_DIR>/RoyalTrek/etc/adminhtml/system.xmlwiththefollowinggroup:
<!--The"RoyalTrekStandard"specificoptions-->
<groupid="royaltrekstandard">
<label><![CDATA[The"RoyalTrekStandard"shippingmethod]]></label>
<fieldset_css>complex</fieldset_css>
<fieldid="title"type="text">
<label><![CDATA[Title]]></label>
</field>
<fieldid="shippingcost"type="text">
<label><![CDATA[ShippingCost]]></label>
<validate>validate-numbervalidate-zero-or-greater</validate>
</field>
</group>
Weareintroducinganadditionalsetofconfigurationoptionshere,tobeusedwithourRoyalTrekStandardmethod.
So,wethenextendthe<MAGELICIOUS_DIR>/RoyalTrek/etc/adminhtml/system.xmlwiththefollowinggroup:
<!--The"RoyalTrek48h"specificoptions-->
<groupid="royaltrek48hr">
<label><![CDATA[The"RoyalTrek48h"shippingmethod]]></label>
<fieldset_css>complex</fieldset_css>
<fieldid="title"type="text">
<label><![CDATA[Title]]></label>
</field>
<fieldid="shippingcost"type="text">
<label><![CDATA[ShippingCost]]></label>
<validate>validate-numbervalidate-zero-or-greater</validate>
</field>
</group>
Weareintroducinganadditionalsetofconfigurationoptionshere,tobeusedwithourRoyalTrek48hmethod.
Wethendefinethe<MAGELICIOUS_DIR>/RoyalTrek/etc/config.xmlasfollows:
<config>
<default>
<carriers>
<royaltrek>
<!--DEFAULTSHERE-->
</royaltrek>
</carriers>
</default>
</config>
Theconfig>default>carriers>royaltreknestingpathmatchesthenestingpathofthesystem.xmlelements.Wethenreplacethe<!--DEFAULTSHERE-->withfollowing:
<active>1</active>
<title>RoyalTrekShipping</title>
<sallowspecific>0</sallowspecific>
<showmethod>0</showmethod>
<specificerrmsg>TheRoyalTrekshippingisnotavailable.</specificerrmsg>
<sort_order>10</sort_order>
<model>Magelicious\RoyalTrek\Model\Carrier\RoyalTrek</model>
<royaltrekstandard>
<title><![CDATA[RoyalTrekStandard]]></title>
<shippingcost>4.99</shippingcost>
</royaltrekstandard>
<royaltrek48hr>
<title><![CDATA[RoyalTrek48h]]></title>
<shippingcost>9.99</shippingcost>
</royaltrek48hr>
Withthis,wecansetthedefaultvaluesforeachoftheconfigurationoptionsmadeavailableviasystem.xml.
Wethendefinethe<MAGELICIOUS_DIR>/Model/Carrier/RoyalTrek.phpasfollows:
<?php
namespaceMagelicious\RoyalTrek\Model\Carrier;
classRoyalTrekextends\Magento\Shipping\Model\Carrier\AbstractCarrierimplements
\Magento\Shipping\Model\Carrier\CarrierInterface{
constCARRIER_CODE='royaltrek';
constROYAL_TREK_STANDARD='royaltrekstandard';
constROYAL_TREK_48HR='royaltrek48hr';
protected$_code=self::CARRIER_CODE;
protected$_isFixed=true;
protected$_rateResultFactory;
protected$_rateMethodFactory;
publicfunction__construct(
\Magento\Framework\App\Config\ScopeConfigInterface$scopeConfig,
\Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory$rateErrorFactory,
\Psr\Log\LoggerInterface$logger,
\Magento\Shipping\Model\Rate\ResultFactory$rateResultFactory,
\Magento\Quote\Model\Quote\Address\RateResult\MethodFactory$rateMethodFactory,
array$data=[]
){
$this->_rateResultFactory=$rateResultFactory;
$this->_rateMethodFactory=$rateMethodFactory;
parent::__construct($scopeConfig,$rateErrorFactory,$logger,$data);
}
publicfunctioncollectRates(\Magento\Quote\Model\Quote\Address\RateRequest$request){
if(!$this->getConfigFlag('active')){
returnfalse;
}
$result=$this->_rateResultFactory->create();
//Todo...
return$result;
}
publicfunctiongetAllowedMethods(){
return[
self::ROYAL_TREK_STANDARD=>$this->getConfigData(self::ROYAL_TREK_STANDARD.'/title'),
self::ROYAL_TREK_48HR=>$this->getConfigData(self::ROYAL_TREK_48HR.'/title'),
];
}
privatefunctiongetMethodTitle($method){
return$this->getConfigData($method.'/title');
}
privatefunctiongetMethodPrice($method){
return$this->getMethodCost($method);
}
privatefunctiongetMethodCost($method){
return$this->getConfigData($method.'/shippingcost');
}
}
ThebasicimplementationoftheMagelicious\RoyalTrek\Model\Carrier\RoyalTrekclassishighlydeterminedbytheimplementationofitsunderlyingMagento\Shipping\Model\Carrier\AbstractCarrierparentclassandMagento\Shipping\Model\Carrier\CarrierInterfaceinterface.Thebareminimumimpliessettingupthe$_codevalueandimplementingthecollectRatesmethod.The$_codevalueisanextremelyimportantbitofinformationhere.Weneedtomakesureitisuniqueamongalloftheenabledshippingextensions.ThecollectRatesmethodiswheretheactualshippingcalculationimplementationhappens.
Let'sgoaheadandextendthe<MAGELICIOUS_DIR>/Model/Carrier/RoyalTrek.phpwiththefollowing:
$method=$this->_rateMethodFactory->create();
$method->setCarrier($this->_code);
$method->setCarrierTitle($this->getConfigData('title'));
$method->setMethod(self::ROYAL_TREK_STANDARD);
$method->setMethodTitle($this->getMethodTitle($method->getMethod()));
$method->setPrice($this->getMethodPrice($method->getMethod()));
$method->setCost($this->getMethodCost($method->getMethod()));
$method->setErrorMessage(__('The%1methoderrormessagehere.'));
$result->append($method);
Usingthefactory,wecancreateaninstanceofMagento\Quote\Model\Quote\Address\RateResult\Method.Thisistheindividualshippingmethodthatwewishtomakeavailableasachoiceduringcheckout.Wethensettherequiredvaluesforthecarrier:method,price,cost,andpossibleerrormessage.Withourroyaltrekstandardmethodproperlyset,wefinallypassitontothe$resultobject.
Let'sfurtherextendthe<MAGELICIOUS_DIR>/Model/Carrier/RoyalTrek.phpwiththefollowing:
$method=$this->_rateMethodFactory->create();
$method->setCarrier($this->_code);
$method->setCarrierTitle($this->getConfigData('title'));
$method->setMethod(self::ROYAL_TREK_48HR);
$method->setMethodTitle($this->getMethodTitle($method->getMethod()));
$method->setPrice($this->getMethodPrice($method->getMethod()));
$method->setCost($this->getMethodCost($method->getMethod()));
$method->setErrorMessage(__('The%1methoderrormessagehere.'));
$result->append($method);
Muchlikewiththepreviousexample,hereweshouldaddourroyaltrek48hrtothe$resultobject.
TheendresultshouldbringforthourtwoRoyalTrekshippingmethodstothestorefrontcheckoutShippingstep,asfollows:
TheOrderSummarysectionoftheReview&PaymentsstepshouldalsoreflectonthemethodselectedintheShippingstep,asfollows:
Likewise,theadminCreateNewOrderscreensshouldalsoshowourRoyalTrekshippingmethodsasfollows:
Finally,thesuccessfullymadeordershouldreflecttheRoyalTrek48hshippingmethodselectioninitsneworderemail,andthecustomer'sMyAccountarea,asfollows:
Withourshippingmethodsconfirmedasworking,let'sgoaheadandlookforawayofdistributingit.
DistributingviaGitHubBydefault,thePackagistrepositoryistheonlyregisteredrepositoryinComposer.WecanaddmorerepositoriestoourMagentoprojectbydeclaringthemincomposer.json.Thiswaywegettoregisterourowngitrepositoryasasourceofpackages,asfollows:
composerconfigrepositories.magelicious-royal-trekgitgit@github.com:foggyline/Magelicious_RoyalTrek.git
Thiscommandresultsinthemodifiedcomposer.jsonfile,withtherepositorieskeyamendedasfollows:
"repositories":{
"0":{
"type":"composer",
"url":"https://repo.magento.com/"
},
"magelicious-royal-trek":{
"type":"git",
"url":"git@github.com:foggyline/Magelicious_RoyalTrek.git"
}
},
Wecanseeourmagelicious-royal-trekentryaddedinthere.ThegitvalueusedforthetypekeytellstheComposerweareusingthegitrepository,locatedattheURLprovidedviatheurlkey.Thecomposerandgitarenottheonlytwovaluessupportedforthetype.Theactualtypevaluecouldhaveeasilybeenanyothertypeofsupportedversioncontrolsystem:
Git(git-scm.com)Subversion(subversion.apache.org)Mercurial(mercurial-scm.org)Fossil(fossil-scm.org)
Wecouldalsohavesimplyusedthevcsvalueforthetypekey,andreliedonComposer'sVCSdrivertoautomaticallydetectthetypebasedurlvalue.
Ifwenowexecutecomposerrequiremagelicious/royal-trek:dev-master,Composerwillinstallourshippingmodule.Whilethisnewrepositoriesapproachworkswell,itissomewhatmoresuitedfordistributingprivateMagentoextensions.Wheneverwewishtodistributeourextensionpublicly,aPackagistisamoreconvenient
waytogo.
DistributingviaPackagistPackagistisafreeonlinerepositoryserviceforComposerpackages.WecanuseittoeasilydistributeourfreeMagentomodules.ThefactthatPackagistisadefaultComposerrepository,makesitthedefactorepositoryforanyComposeruser.ThisiswhyhavingourfreeMagentomodulesavailableviaPackagistisapreferredwayofdistribution.
PushingourMagentomoduletoPackagistisquiteeasy.Assumingwehaveouraccountcreated,weshouldstartbyclickingontheSubmitbutton,whichwilllandusonthefollowingscreen:
WeneedtoprovidealinktoourGitrepositoryhere,andclicktheCheckbutton,followedbytheSubmitbutton,ifthevalidrepositorywasfound.Thisshouldcreateourpackage,asperthefollowingscreen:
ThePackagistsaysthatourcreatedpackageisnowavailableforuseviathecomposerrequiremagelicious/module-royal-trekcommand.However,runningthiscommandnowwouldbelikelytogiveusthefollowingerror:
[InvalidArgumentException]
Couldnotfindamatchingversionofpackagemagelicious/module-royal-trek.Checkthepackagespelling,yourversionconstraintandthatthepackageisavailableinastabilitywhichmatchesyourminimum-stability(stable).
Noticethedev-masterlabelonourPackagistscreen.OurbranchesautomaticallyappearasdevversionsinPackagist.Therefore,wecanusethecomposerrequiremagelicious/module-royal-trek:dev-mastercommandtofetchthepackage.Tochangethat,weneedtospecificallytagourgitcommits,asfollows:
gitadd.
gitcommit-a-m'TheRoyalTrekshippingmodule,firstversion.'
gittag1.0.0
gitpushorigin1.0.0
Oncewehavedonethat,wecangobacktothePackagistpackagescreenandhittheUpdatebutton.Thisshouldnowshowour1.0.0version:
Ifwespecifyaversionwhenrequiringthepackage,Composerfetchesthelatesttaggedversionfromthemaster.Forexample,composerrequiremagelicious/module-royal-trek:2.4.xtakesthelatest2.4taggedversionfromthemasterbranch.
Whenitcomestoversioning,itisworthnotingthatsetup_versionfoundinmodule.xml,andversionfoundincomposer.jsonaretwodifferenttypesofversioning.Magentoreferstothemasmarketingversionandcomposerversion.Marketingversionmightbethoughtofassomethingthemerchantinteractswith,whileComposerversionissomethingthatdevelopersinteractwith.TheMagento_Catalogmodule,forexample,usesthe2.2.4marketingversionformarketing,whereasitsComposerversionis102.0.4.Thisisnottosaythatwecannotusethesameversioningforboth,aslongaswerememberthatthesetup_version,foundinmodule.xml,iswhatdrivesoursetupscripts.
DistributingfuturenewversionsofourMagelicious_RoyalTrekmodulewould,therefore,comedownto:
1. Bumpingupthesetup_versionfoundinmodule.xml2. Bumpinguptheversionfoundincomposer.json
3. AddressinganynecessaryMagentosetupscripts4. CommittingourchangestoGit,withproperversiontagging5. MakingsuretheUpdateistriggeredonthePackagistscreenofourmodule
editscreen
UsingthePackagist'sservicehookwecanensurethatourpackagewillalwaysbeupdatedautomatically.Seehttps://packagist.org/about#how-to-update-packagesformoreinformation.
SummaryInthischapter,welearnedhowtocreateasimpleshippingmodule.Wesawhoweasyitistoaddspecificshippingcalculationsaspartofofflineshippingmethods.WethenpackagedthismoduleanddistributeditviaPackagist.Thismadeiteasyfortheendconsumertouseourmodule,withjustafewsimpleconsolecommands.Likewise,anyfutureupdatestoourmoduleshouldbefrictionlessfortheendconsumer,ascomposercaneasilyhandlethoseviasimplecomposerupdatecommands.
Movingforward,wearegoingtotakealookatsomeofthespecificsofMagentoadminareadevelopment.
DevelopingforAdminAttheverybeginningofourjourney,backinChapter1,UnderstandingMagentoArchitecture,wementionedhowMagentoconsistsofdifferentareas.DevelopingforMagentoadminimpliesdevelopingfortheadminhtmlarea.Whilethemajorityofcodeisapplicableacrossdifferentareas,therearecertainsubtledifferences.UnlikefrontendwhichismostlybuiltviaHTML(.phtml,.html),theMagentoadminhtmlareaismostlybuiltviaUIcomponentswhicharereferenced,stacked,andconfiguredthrough.xmlfiles.Thisisnottosaythatthesamecomponentscannotbeusedbothforfrontendandadmin,becauseallUIcomponentscanbeconfiguredforbothoftheseareas;wejustneedtoconfigurestylesmanuallyforcomponentsonthefrontend.
TherearetwobasicUIcomponentsinMagento:listingandform.Therestaresecondarycomponents,whichserveasextensionsofbasiccomponents:listingToolbar,columns,filters,column,form,andfield.
Togetabetterunderstandingoftheadminhtmlarea,wearegoingtobuildaMagelicious_Minventorymodule,usingsomeofthesecomponents.Theideabehindthemoduleistoprovideacustomlistinginterfaceforalimitedsetofusers,wheretheycaneasilybumpuptheproductstockincertainincrementswithoutevergettingaccesstootherareasoftheMagentoadmin.
Ourworkherewillconsistoftwomajorparts:
UsingthelistingcomponentUsingtheformcomponent
Tokeepthingscompact,wewillusethe<MODULE_DIR>toreferencetheMAGELICIOUS_DIR>/Minventorydirectory.
TechnicalrequirementsYouwillneedtohavebasicknowledgeofPHP,OOP,JavaScript,andXML.YouwillalsoneedApache,MySQL,andAMPPSinstalledonyoursystemtoexecutethecodes.
ThecodefilesofthischaptercanbefoundonGitHub:https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
CheckoutthefollowingvideotoseetheCodeinAction:
http://bit.ly/2xuoFDL.
UsingthelistingcomponentThelistingisabasiccomponentresponsibleforrenderinggrids,lists,andtiles,providingfiltering,pagination,sorting,andotherfeatures.ThelistingElementsgroupreferencedinthevendor/magento/module-ui/etc/ui_configuration.xsdfileprovidesanicelistofbothprimaryandsecondarylistingcomponents:
actions component file massaction select
actionsColumn container filters modal selectionsColumn
bookmark dataSource form multiline tab
boolean dataProvider hidden multiselect text
button date htmlContent nav textarea
checkbox dynamicRows input number wysiwyg
checkboxset email insertForm paging
column exportButton insertListing price
columns field listing range
columnsControls fieldset listingToolbar radioset
Thekeytousingallofthesecomponentsistounderstand:
Whatparametersindividualcomponentsaccept—furtherrevealedbydefinitionsfoundinthevendor/magento/moduleui/view/base/ui_component/etc/definitiondirectoryWhatchildcomponentsindividualcomponentsaccept—forexample,theemailcomponentcannotbenestedwithinthedataProvidercomponent
Movingforward,wewillusethelistingcomponent,andafewofitssecondarycomponentstocreatetheMicroInventoryscreenasshown:
ThegriditselfistoconsistofID,SKU,Status,Quantity,andActioncolumns.TheResupplyactionwilltriggerredirectiontoacustomStockResupplyscreen,whichwewilladdressinthenextsection.TheActionsselectorintheupperleftcorneristoconsistoftwocustomactions,allowingforfixedproductstockincreases.
Assumingwehavedefinedourbasicregistration.php,composer.json,andetc/module.xmlfiles,wecanstartdealingwiththespecificsofourmodule.
Let'sstartbydefiningthe<MODULE_DIR>/etc/acl.xmlasfollows:
<config>
<acl>
<resources>
<resourceid="Magento_Backend::admin">
<resourceid="Magento_Catalog::catalog">
<resourceid="Magento_Catalog::catalog_inventory">
<resourceid="Magelicious_Minventory::minventory"title="MicroInventory"/>
</resource>
</resource>
</resource>
</resources>
</acl>
</config>
Therequirementofourmodulewastoprovideacustomlistinginterfaceforalimitedsetofusers.Theaccesslistentry,laterreferencedbyouradmincontroller,ensuresjustthat.ThechoicetonestourMagelicious_Minventory::minventoryasachildofMagento_Catalog::catalog_inventoryisbasedmerelyonlogicalgrouping,asourmoduledealswithinventorystock.We
shouldnowbeabletoseeMicroInventoryunderRolesResourcesasshown:
Wethendefinethe<MODULE_DIR>/etc/adminhtml/routes.xmlasfollows:
<config>
<routerid="admin">
<routeid="minventory"frontName="minventory">
<modulename="Magelicious_Minventory"/>
</route>
</router>
</config>
Thiswillallowustoaccessourcontrolleractionslateronviahttp://magelicious.loc/index.php/<admin>/minventory/<controller>/<action>links.
Wethendefinethe<MODULE_DIR>/etc/adminhtml/menu.xmlasfollows:
<config>
<menu>
<addid="Magelicious_Minventory::minventory"
title="MicroInventory"translate="title"
module="Magelicious_Minventory"sortOrder="100"
parent="Magento_Catalog::inventory"
action="minventory/product/index"
resource="Magelicious_Minventory::minventory"/>
</menu>
</config>
ThispositionsourMicroInventorymenurightunderthemainCatalog|CATALOGUEmenu,asshown:
Whenclicked,themenu'sminventory/product/indexactionwillthrowusat<MODULE_DIR>/Controller/Adminhtml/Product/Index.php,whichwillbeaddressedlateron.
Wethendefinethe<MODULE_DIR>/Model/Resupply.phpasfollows:
namespaceMagelicious\Minventory\Model;
classResupply
{
protected$productRepository;
protected$collectionFactory;
protected$stockRegistry;
publicfunction__construct(
\Magento\Catalog\Api\ProductRepositoryInterface$productRepository,
\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory$collectionFactory,
\Magento\CatalogInventory\Api\StockRegistryInterface$stockRegistry
)
{
$this->productRepository=$productRepository;
$this->collectionFactory=$collectionFactory;
$this->stockRegistry=$stockRegistry;
}
publicfunctionresupply($productId,$qty)
{
$product=$this->productRepository->getById($productId);
$stockItem=$this->stockRegistry->getStockItemBySku($product->getSku());
$stockItem->setQty($stockItem->getQty()+$qty);
$stockItem->setIsInStock((bool)$stockItem->getQty());
$this->stockRegistry->updateStockItemBySku($product->getSku(),$stockItem);
}}
Thisclasswillserveasacentralizedstockupdaterforourmodule,whichwillbeupdatingstockfromtheActionsselectorfoundontheMicroInventoryscreen,aswellasfromtheSavebuttonactiontriggeredontheStockResupplyscreen.
Wethendefinethe<MODULE_DIR>/Controller/Adminhtml/Product.phpasfollows:
namespaceMagelicious\Minventory\Controller\Adminhtml;
abstractclassProductextends\Magento\Backend\App\Action
{
constADMIN_RESOURCE='Magelicious_Minventory::minventory';
}
Thisisourcontrollerfile,theparentofthecontrolleractionsthatwewillsoondefine.WesetthevalueofitsADMIN_RESOURCEconstanttothatdefinedinouracl.xmlfile.Thiswillempowerourcontrollertoonlyallowaccesstouserswithproperresourceroles.
Wethendefinethe<MODULE_DIR>/Controller/Adminhtml/Product/Index.phpasfollows:
namespaceMagelicious\Minventory\Controller\Adminhtml\Product;
use\Magento\Framework\Controller\ResultFactory;
classIndexextends\Magelicious\Minventory\Controller\Adminhtml\Product
{
publicfunctionexecute()
{
$resultPage=$this->resultFactory->create(ResultFactory::TYPE_PAGE);
$resultPage->getConfig()->getTitle()->prepend((__('MicroInventory')));
return$resultPage;
}
}
Thiscontrolleractiondoesnotreallydoanythingspecial.Asidefromsettingupthescreentitle,itmerelyprovidesamechanismforloadingtheminventory_product_index.xmlthatwewilladdresslateron.
Wethendefinethe<MODULE_DIR>/Controller/Adminhtml/Product/MassResupply.phpasfollows:
namespaceMagelicious\Minventory\Controller\Adminhtml\Product;
use\Magento\Framework\Controller\ResultFactory;
classMassResupplyextends\Magelicious\Minventory\Controller\Adminhtml\Product
{
protected$filter;
protected$collectionFactory;
protected$resupply;
publicfunction__construct(
\Magento\Backend\App\Action\Context$context,
\Magento\Ui\Component\MassAction\Filter$filter,
\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory$collectionFactory,
\Magelicious\Minventory\Model\Resupply$resupply
)
{
parent::__construct($context);
$this->filter=$filter;
$this->collectionFactory=$collectionFactory;
$this->resupply=$resupply;
}
publicfunctionexecute()
{
$redirectResult=$this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
$qty=$this->getRequest()->getParam('qty');
$collection=$this->filter->getCollection($this->collectionFactory->create());
$productResupplied=0;
foreach($collection->getItems()as$product){
$this->resupply->resupply($product->getId(),$qty);
$productResupplied++;
}
$this->messageManager->addSuccessMessage(__('Atotalof%1record(s)havebeenresupplied.',$productResupplied));
return$redirectResult->setPath('minventory/product/index');
}
}
ThiscontrolleractionwillbetriggeredbytheResupply+10andResupply+50actionsfromtheMicroInventoryscreen.WecanseeitusingtheMagento\Ui\Component\MassAction\Filtertoprocessthemassselectoptions,bindingtheminternallytoproductcollectioninordertofilterproductswehaveselectedproperly.
Wethendefinethe<MODULE_DIR>/view/adminhtml/layout/minventory_product_index.xmlasfollows:
<page>
<updatehandle="styles"/>
<body>
<referenceContainername="content">
<uiComponentname="minventory_listing"/>
</referenceContainer>
</body>
</page>
Thisisthelayoutfilethatgetstriggeredwhenwelandon<MODULE_DIR>/Controller/Adminhtml/Product/Index.php.Thenameofthefilematchesthe<routeName>/<controllerName>/<controllerActionName>path.Theactuallayoutheremerelyreferencesthecontentcontainer,towhichitaddstheminventory_listingcomponentusingtheuiComponentelement.
Wethendefinethe<MODULE_DIR>/view/adminhtml/ui_component/minventory_listing.xmlasfollows:
<listing>
<argumentname="data"xsi:type="array">
<itemname="js_config"xsi:type="array">
<itemname="provider"xsi:type="string">minventory_listing.minventory_listing_data_source</item>
</item>
</argument>
<settings>
<spinner>minventory_columns</spinner>
<deps>
<dep>minventory_listing.minventory_listing_data_source</dep>
</deps>
</settings>
<!--dataSource-->
<!--listingToolbar-->
<!--columns-->
</listing>
Thisisourlistingcomponent.Theminventory_listing.minventory_listing_data_sourceisourdatasourcedefinedunderthedataSourceelement.
Wethenmodifytheminventory_listing.xmlbyreplacingthe<!--dataSource-->withthefollowing:
<dataSourcename="minventory_listing_data_source"component="Magento_Ui/js/grid/provider">
<settings>
<storageConfig>
<paramname="indexField"xsi:type="string">entity_id</param>
</storageConfig>
<updateUrlpath="mui/index/render"/>
</settings>
<dataProviderclass="Magelicious\Minventory\Ui\DataProvider\Product\ProductDataProvider"name="minventory_listing_data_source">
<settings>
<requestFieldName>id</requestFieldName>
<primaryFieldName>entity_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
ThemostimportantpartofthedataSourcecomponentisitsdataProvider.WesetitsvaluetoMagelicious\Minventory\Ui\DataProvider\Product\ProductDataProvider.TherequestFieldNameandprimaryFieldNamearenotreallythatimportantinourcase,aswearenotreallyoperatingwithfullCRUDontheproductentity,sincewearemerelyfocusingonupdatingthequantitythroughafewlinesofcustomcode.Still,thecomponentitselfrequiresacertainminimalconfiguration,soweusewhatwewouldnormallyuseforaproductentity,butthesecanreallybeanyvaluesfoundonanentity.
Wethendefinethe<MODULE_DIR>/Ui/DataProvider/Product/ProductDataProvider.phpasfollows:
classProductDataProviderextends\Magento\Ui\DataProvider\AbstractDataProvider{
protected$collection;
publicfunction__construct(
string$name,
string$primaryFieldName,
string$requestFieldName,
\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory$collectionFactory,
array$meta=[],
array$data=[]
){
parent::__construct(
$name,
$primaryFieldName,
$requestFieldName,
$meta,
$data
);
$this->collection=$collectionFactory->create();
}
publicfunctiongetData(){
if(!$this->getCollection()->isLoaded()){
$this->getCollection()->load();
}
$items=$this->getCollection()->toArray();
return[
'totalRecords'=>$this->getCollection()->getSize(),
'items'=>array_values($items),
];
}
}
ThecollectionpropertyissetmandatorilybytheparentMagento\Ui\DataProvider\AbstractDataProvider,sowehavetosetitsvaluetosomekindofcollection.Sinceweareworkingwithproducts,itonlymakessensetosetittoanexistingMagento\Catalog\Model\ResourceModel\Product\Collection,thusavoidingcreatingourowncollection.ThekeymethodforourlistingcomponentisgetData.Thismethodfeedsthelistingcomponentwiththenumberofrecordsinthedatacollection,aswellasthedatacollectionitself.
WethenextendtheProductDataProvider.phpwiththefollowing:
protectedfunctionjoinQty(){
if($this->getCollection()){
$this->getCollection()->joinField(
'qty',
'cataloginventory_stock_item',
'qty',
'product_id=entity_id'
);
}
}
Theqtyfieldisnotpartofthedefaultproductscollection,sowehavetojointheqtyinformationfromthecataloginventory_stock_itemtabletoit.Wemustmakesuretocallthismethodbeforeourcollectionisloaded.
Wethenmodifytheminventory_listing.xmlbyreplacingthe<!--listingToolbar-->
withthefollowing:
<listingToolbarname="listing_top">
<bookmarkname="bookmarks"/>
<columnsControlsname="columns_controls"/>
<filtersname="listing_filters"/>
<pagingname="listing_paging"/>
<--massaction-->
</listingToolbar>
ThelistingToolbarcomponentisessentiallyacontainerforthelisting-relatedelementslikepaging,massactions,filters,andbookmarks.Thebookmarkcomponentstorestheactiveandchangedstatesofdatagrids.Thepagingcomponentprovidesnavigationthroughthepagesofthecollection,otherwise,wewouldbeforcedtoviewtheentirecollectionatonce,whichwouldnotreallybeaperformance-efficientapproach.Thefilterscomponentisresponsibleforrenderingfilters'interfacesandapplyingtheactualfiltering.Thisincludesthestatesoffilters,columns'positions,appliedsorting,pagination,andsoon.
ThecolumnsControlscomponentallowsustomodifythevisibilityofthelistingcolumns,shownasfollows:
ThepossibilityoffilteringbyStoreView,asshownintheprecedingscreenshot,iseasilyaddedbymodifyingtheminventory_listing.xmlasfollows:
<filtersname="listing_filters">
<filterSelectname="store_id"provider="${$.parentName}">
<settings>
<optionsclass="Magento\Store\Ui\Component\Listing\Column\Store\Options"/>
<captiontranslate="true">AllStoreViews</caption>
<labeltranslate="true">StoreView</label>
<dataScope>store_id</dataScope>
</settings>
</filterSelect>
</filters>
HereweusedthefilterSelectcomponent,withtheMagento\Store\Ui\Component\Listing\Column\Store\Optionsclasspassedasanoptionsparameter.ThisshowshoweasyitistocombinevariouscomponentsandtopulldatafromPHPclasses.
Let'smodifytheminventory_listing.xmlfurtherbyreplacingthe<--massaction-->withthefollowing:
<massactionname="listing_massaction"component="Magento_Ui/js/grid/tree-massactions">
<actionname="resupply">
<settings>
<type>resupply</type>
<labeltranslate="true">Resupply</label>
<actions>
<actionname="0">
<type>resupply_10</type>
<labeltranslate="true">Resupply+10</label>
<urlpath="minventory/product/massResupply">
<paramname="qty">10</param>
</url>
</action>
<actionname="1">
<type>resupply_50</type>
<labeltranslate="true">Resupply+50</label>
<urlpath="minventory/product/massResupply">
<paramname="qty">50</param>
</url>
</action>
</actions>
</settings>
</action>
</massaction>
Usingtheactioncomponent,wedefinetheResupply+10andResupply+50actionsusedinthescopeofthemassactioncomponent.
Wethenmodifytheminventory_listing.xmlbyreplacingthe<!--columns-->withthefollowing:
<columnsname="minventory_columns"class="Magento\Catalog\Ui\Component\Listing\Columns">
<settings>
<childDefaults>
<paramname="fieldAction"xsi:type="array">
<itemname="provider"xsi:type="string">minventory_listing.minventory_listing.minventory_columns.actions</item>
<itemname="target"xsi:type="string">applyAction</item>
<itemname="params"xsi:type="array">
<itemname="0"xsi:type="string">resupply</item>
<itemname="1"xsi:type="string">${$.$data.rowIndex}</item>
</item>
</param>
</childDefaults>
</settings>
<!--columns#2-->
</columns>
Thecolumnscomponentdefinition,alongwithitschildcomponents,islikelytotakethebiggestchunkofourlistingconfiguration.Thisiswhereweaddourselectioncolumns,regularcolumns,andactioncolumns.
Todemonstratethatfurther,wereplacethe<!--columns#2-->withthefollowing:
<selectionsColumnname="ids"sortOrder="0">
<settings>
<indexField>entity_id</indexField>
</settings>
</selectionsColumn>
<columnname="entity_id"sortOrder="10">
<settings>
<filter>textRange</filter>
<labeltranslate="true">ID</label>
<sorting>asc</sorting>
</settings>
</column>
<columnname="sku"sortOrder="20">
<settings>
<filter>text</filter>
<labeltranslate="true">SKU</label>
</settings>
</column>
<columnname="qty"sortOrder="30">
<settings>
<addField>true</addField>
<filter>textRange</filter>
<labeltranslate="true">Quantity</label>
</settings>
</column>
<actionsColumnname="resupply"class="Magelicious\Minventory\Ui\Component\Listing\Columns\Resupply"sortOrder="40">
<settings>
<indexField>entity_id</indexField>
</settings>
</actionsColumn>
TheactionsColumnpointstoacustomMagelicious\Minventory\Ui\Component\Listing\Columns\Resupplyclass,whichwedefineunder<MODULE_DIR>/Ui/Component/Listing/Columns/Resupply.phpasfollows:
classResupplyextends\Magento\Ui\Component\Listing\Columns\Column{
protected$urlBuilder;
publicfunction__construct(
\Magento\Framework\View\Element\UiComponent\ContextInterface$context,
\Magento\Framework\View\Element\UiComponentFactory$uiComponentFactory,
\Magento\Framework\UrlInterface$urlBuilder,
array$components=[],
array$data=[]
){
$this->urlBuilder=$urlBuilder;
parent::__construct($context,$uiComponentFactory,$components,$data);
}
publicfunctionprepareDataSource(array$dataSource){
if(isset($dataSource['data']['items'])){
$storeId=$this->context->getFilterParam('store_id');
foreach($dataSource['data']['items']as&$item){
$item[$this->getData('name')]['resupply']=[
'href'=>$this->urlBuilder->getUrl(
'minventory/product/resupply',
['id'=>$item['entity_id'],'store'=>$storeId]
),
'label'=>__('Resupply'),
'hidden'=>false,
];
}
}
return$dataSource;
}
}
TheprepareDataSourcemethodiswhereweinjectourmodifications.Wetraversethe$dataSource['data']['items']structureuntilwecomeacrossourcolumn,andthenmodifyitaccordinglywithaproperhrefvalue.This,inturn,rendersourresupplyactionscolumnaspertheMicroInventoryscreen.
WiththeMicroInventoryscreennowsortedviathelistingcomponent,let'sshiftourfocusontotheStockResupplyscreenbuiltviatheformcomponent.
UsingtheformcomponentTheformisabasiccomponentresponsibleforperformingCRUDoperationsonanentity.ThelistingElementsgroupreferencedundervendor/magento/module-ui/etc/ui_configuration.xsdfileprovidesanicelistofbothprimaryandsecondaryformcomponents:
bookmark dataProvider fileUploader massaction range
boolean date form modal radioset
button dynamicRows hidden multiline select
checkbox email htmlContent multiselect tab
checkboxset exportButton input nav text
component field insertForm number textarea
container fieldset insertListing paging wysiwyg
dataSource file listing price
Movingforward,wewillusetheformcomponent,andafewofitssecondarycomponentstocreatetheStockResupplyscreenasshown:
TheformitselfistoconsistofStockand+Qtyfields.TheStockfieldwillbearead-onlyfieldconsistingofanSKU+currentqtystring.TheBackbuttonwilltakeusbacktotheMicroInventorylisting,whereastheSavebuttonwillposttheformtoaspecialResupplycontrolleraction,whichwillthenincreasethestockbyagiven+Qtyamount.TheActionsselectorintheupperleftcorneristoconsistoftwocustomactions,allowingforfixedproductstockincreases.
Westartoffbydefiningthe<MODULE_DIR>/Controller/Adminhtml/Product/Resupply.phpasfollows:
use\Magento\Framework\Controller\ResultFactory;
classResupplyextends\Magelicious\Minventory\Controller\Adminhtml\Product{
protected$stockRegistry;
protected$productRepository;
protected$resupply;
publicfunction__construct(
\Magento\Backend\App\Action\Context$context,
\Magento\Catalog\Api\ProductRepositoryInterface$productRepository,
\Magento\CatalogInventory\Api\StockRegistryInterface$stockRegistry,
\Magelicious\Minventory\Model\Resupply$resupply
){
parent::__construct($context);
$this->productRepository=$productRepository;
$this->stockRegistry=$stockRegistry;
$this->resupply=$resupply;
}
publicfunctionexecute(){
if($this->getRequest()->isPost()){
$this->resupply->resupply(
$this->getRequest()->getParam('id'),
$_POST['minventory_product']['qty']
);
$this->messageManager->addSuccessMessage(__('Successfullyresupplied'));
$redirectResult=$this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
return$redirectResult->setPath('minventory/product/index');
}else{
$resultPage=$this->resultFactory->create(ResultFactory::TYPE_PAGE);
$resultPage->getConfig()->getTitle()->prepend((__('StockResupply')));
return$resultPage;
}
}
}
Giventhesimplicityofourform,usingtheisPost()checkontherequestobject,weallowourselvestousethesamecontrolleractionforrenderingtheStockResupplyscreen,aswellassubmittingthesaveactiontoit.
Withcontrolleractioninplace,wethendefinethe<MODULE_DIR>/view/adminhtml/layout/minventory_product_resupply.xmlasfollows:
<page>
<updatehandle="styles"/>
<body>
<referenceContainername="content">
<uiComponentname="minventory_resupply_form"/>
</referenceContainer>
</body>
</page>
Muchlikewiththeformlisting,thislayoutfilemerelycallstheminventory_resupply_formcomponent,whichiswhereallourvisualelementsoftheStockResupplyscreenreside.
Wethendefinethe<MODULE_DIR>/view/adminhtml/ui_component/minventory_resupply_form.xmlasfollows:
<form>
<argumentname="data"xsi:type="array">
<itemname="js_config"xsi:type="array">
<itemname="provider"xsi:type="string">minventory_resupply_form.minventory_resupply_form_data_source</item>
<itemname="deps"xsi:type="string">minventory_resupply_form.minventory_resupply_form_data_source</item>
</item>
<itemname="layout"xsi:type="array">
<itemname="type"xsi:type="string">tabs</item>
</item>
</argument>
<settings>
<buttons>
<buttonname="save"class="Magelicious\Minventory\Block\Adminhtml\Product\Edit\Button\Save"/>
<buttonname="back"class="Magelicious\Minventory\Block\Adminhtml\Product\Edit\Button\Back"/>
</buttons>
</settings>
<!--dataSource-->
<!--fieldset-->
</form>
Muchlikethelistingcomponent,theformcomponentalsorequiresadataprovider.
Wethenmodifytheminventory_resupply_form.xmlbyreplacingthe<!--dataSource-->withfollowing:
<dataSourcename="minventory_resupply_form_data_source">
<argumentname="data"xsi:type="array">
<itemname="js_config"xsi:type="array">
<itemname="component"xsi:type="string">Magento_Ui/js/form/provider</item>
</item>
</argument>
<dataProviderclass="Magelicious\Minventory\Ui\DataProvider\Product\Form\ProductDataProvider"name="minventory_resupply_form_data_source">
<settings>
<requestFieldName>id</requestFieldName>
<primaryFieldName>entity_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
Herewesetthedataprovider,whichpointstoourcustomclass,Magelicious\Minventory\Ui\DataProvider\Product\Form\ProductDataProvider.
Wefurthermodifytheminventory_resupply_form.xmlbyreplacingthe<!--fieldset-->withthefollowing:
<fieldsetname="minventory_product">
<argumentname="data"xsi:type="array">
<itemname="config"xsi:type="array">
<itemname="label"xsi:type="string"translate="true">General</item>
</item>
</argument>
<fieldname="stock">
<argumentname="data"xsi:type="array">
<itemname="config"xsi:type="array">
<itemname="label"xsi:type="string">Stock</item>
<itemname="visible"xsi:type="boolean">true</item>
<itemname="dataType"xsi:type="string">text</item>
<itemname="formElement"xsi:type="string">input</item>
<itemname="disabled"xsi:type="string">true</item>
</item>
</argument>
</field>
<fieldname="qty">
<argumentname="data"xsi:type="array">
<itemname="config"xsi:type="array">
<itemname="label"xsi:type="string">+Qty</item>
<itemname="visible"xsi:type="boolean">true</item>
<itemname="dataType"xsi:type="string">text</item>
<itemname="formElement"xsi:type="string">input</item>
<itemname="focused"xsi:type="string">true</item>
<itemname="validation"xsi:type="array">
<itemname="required-entry"xsi:type="boolean">true</item>
<itemname="validate-zero-or-greater"xsi:type="boolean">true</item>
</item>
</item>
</argument>
</field>
</fieldset>
HerewedefinedfieldsetwithaGeneraltitle,andtwofields:stockandqty.Thestockfieldwasdefinedasdisabled,asitspurposewillbemerelytomergethe<SKU>|<qty>valuesforinformationalpurposes.Thestructureoftheindividualfielddefinitionmightseemoverwhelmingatfirst,butwecaneasilydetermineavailableargumentsbyobservingthe<componentname="column"definitionunderthe<MAGENTO_DIR>/module-ui/view/base/ui_component/etc/definition.map.xml.
Wethendefine<MODULE_DIR>/Ui/DataProvider/Product/Form/ProductDataProvider.phpasfollows:
classProductDataProviderextends\Magento\Ui\DataProvider\AbstractDataProvider{
protected$loadedData;
protected$productRepository;
protected$stockRegistry;
protected$request;
publicfunction__construct(
string$name,
string$primaryFieldName,
string$requestFieldName,
\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory$collectionFactory,
\Magento\Catalog\Api\ProductRepositoryInterface$productRepository,
\Magento\CatalogInventory\Api\StockRegistryInterface$stockRegistry,
\Magento\Framework\App\RequestInterface$request,
array$meta=[],array$data=[]
){
parent::__construct($name,$primaryFieldName,$requestFieldName,$meta,$data);
$this->collection=$collectionFactory->create();
$this->productRepository=$productRepository;
$this->stockRegistry=$stockRegistry;
$this->request=$request;
}
publicfunctiongetData(){
if(isset($this->loadedData)){
return$this->loadedData;
}
$id=$this->request->getParam('id');
$product=$this->productRepository->getById($id);
$stockItem=$this->stockRegistry->getStockItemBySku($product->getSku());
$this->loadedData[$product->getId()]['minventory_product']=[
'stock'=>__('%1|%2',$product->getSku(),$stockItem->getQty()),
'qty'=>10
];
return$this->loadedData;
}
}
OurdataproviderisexpectedtoimplementthegetDatamethod.Thisreturnsanarrayofdatathatfeedstheformwithpropervalues.Thestructureofthearraymightbedifficulttograspatfirst,soithelpstoglossoversomeofMagento'sdataproviders.Thestockandqtyentriesherewillprovidevaluesforthefieldsdefinedviaminventory_resupply_form.xml.
Wethendefine<MODULE_DIR>/Block/Adminhtml/Product/Edit/Button/Back.phpasfollows:
classBackextends\Magento\Backend\Block\Templateimplements\Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface
{
publicfunctiongetButtonData(){
return[
'label'=>__('Back'),
'on_click'=>sprintf("location.href='%s';",$this->getBackUrl()),
'class'=>'back',
'sort_order'=>10
];
}
publicfunctiongetBackUrl(){
return$this->getUrl('*/*/');
}
}
TheButtonProviderInterfacerequiresthegetButtonDatamethodimplementation.ThestructureofthereturnarrayissomewhatblurryuntilweglossoversomeoftheotherbuttonsthataredefinedacrossMagento.ThisrendersourBackbuttonasfollows:
<buttonid="back"title="Back"type="button"class="action-scalableback"onclick="location.href='...strippedaway...';"data-ui-id="back-button">
<span>Back</span>
</button>
TheBackbuttonprovidesagobacktopreviouspagefunctionality,whichinourcaseisdeterminedbythevalueofthegetBackUrlmethodresponse.
Wethendefine<MODULE_DIR>/Block/Adminhtml/Product/Edit/Button/Save.phpasfollows:
classSaveextends\Magento\Backend\Block\Templateimplements\Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface
{
publicfunctiongetButtonData(){
return[
'label'=>__('Save'),
'class'=>'saveprimary',
'data_attribute'=>[
'mage-init'=>['button'=>['event'=>'save']],
'form-role'=>'save',
],
'sort_order'=>20,
];
}
}
Muchlikewiththepreviousbutton,weuseasimilararraystructureforourbuttonhere.Thedifferenceisthatthistimewearepassingthedata_attributeaswell.ThisrendersourSavebuttonasfollows:
<buttonid="save"title="Save"type="button"class="action-scalablesaveprimaryui-buttonui-widgetui-state-defaultui-corner-allui-button-text-only"onclick="location.href='...strippedaway...';"data-form-role="save"data-ui-id="save-button"role="button"aria-disabled="false"><spanclass="ui-button-text">
<span>Save</span>
</span></button>
Themage-initpartmightseemconfusingatthemoment.Sufficeittosaythatit'sawayofinitializingaJScomponent,whichissomethingwewilladdressinmoredetailinthenextchapter.OurSaveessentiallytriggerstheform'ssubmission.
Withthiswehavefinishedourformcomponentdefinition,makingthewholeStockResupplyscreenfunctional.
SummaryInthischapter,webuilttwoverydifferentscreensintheMagentoadminarea.Oneutilizedthelistingcomponent,whereastheotherutilizedtheformcomponent.Agreatdealofourworkinvolvedconfigurationratherthancoding,whichstandstoprovehowpowerfulMagentoUIcomponentscanbe.Whiletheamountofconfigurationmightseemoverwhelmingatfirst,gettingagriponindividualcomponentconfigurationsallowsustobuildcomplexinterfacesquickly.
Movingforward,wearegoingtotakealookatsomeofthespecificsbehinddevelopingforthestorefrontarea.
DevelopingforStorefrontTheMagentostorefrontisthecustomer-facingviewofaMagentoe-commerceplatform.Developingforstorefrontimpliesdevelopingforthefrontendarea.WhereastheadminhtmlareaisprimarilybuiltviameansofUIcomponents,thefrontendareamakesheavyuseofJavaScript(JS)componentsthatcomeinformofjQuerywidgetsandUI/KnockoutJScomponents.AsidefromJScomponents,therearelotsofotherbitsandpiecesinvolvedinstorefrontdevelopment,suchasthemes,layouts,templates,languagepackages,andCSS/LESS.Ourfocus,however,throughoutthischapterwillbeonJScomponents,astheyseemtobethemostconfusingandchallengingpartoftheMagentofrontendtoovercome.
Movingforward,wearegoingtolookintothefollowingsections:
SettinguptheplaygroundInitializingJScomponentsMeetRequireJSReplacingjQuerywidgetcomponentsExtendingjQuerywidgetscomponentsCreatingjQuerywidgetscomponentsExtendingUI/KnockoutJScomponentsCreatingUI/KnockoutJScomponents
TechnicalrequirementsYouwillneedtohavebasicknowledgeofPHP,OOP,JavaScript,andXML.YouwillalsoneedApache,MySQL,andAMPPSinstalledonyoursystemtoexecutethecodes.
ThecodefilesofthischaptercanbefoundonGitHub:https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
CheckoutthefollowingvideotoseetheCodeinAction:
http://bit.ly/2D6oMLz.
SettinguptheplaygroundTogetabetterunderstandingofthefrontendarea,wearegoingtobuildaverylightweightMagelicious_Jscomodule,toserveasaplaygroundforourJScomponentexploration.
Tothispoint,weshouldalreadybeprettyfamiliarwiththeflowofcreatinganewmodule.Assumingwehavedefinedourbasicregistration.php,composer.json,andetc/module.xmlfiles,wecanstartdealingwiththespecificsofourMagelicious_Jscomodule.
Let'sstartbydefining<MODULE_DIR>/etc/frontend/routes.xml,asfollows:
<config>
<routerid="standard">
<routeid="jsco"frontName="jsco">
<modulename="Magelicious_Jsco"/>
</route>
</router>
</config>
Wethencreate<MODULE_DIR>/Controller/Playground.php,asfollows:
namespaceMagelicious\Jsco\Controller;
abstractclassPlaygroundextends\Magento\Framework\App\Action\Action
{
}
Wethencreate<MODULE_DIR>/Controller/Playground/Index.php,asfollows:
namespaceMagelicious\Jsco\Controller\Playground;
useMagento\Framework\Controller\ResultFactory;
classIndexextends\Magelicious\Jsco\Controller\Playground
{
publicfunctionexecute(){
$resultPage=$this->resultFactory->create(ResultFactory::TYPE_PAGE);
$resultPage->getConfig()->getTitle()->set(__('Playground'));
return$resultPage;
}
}
There'snothingreallynewtothispoint.Wehavemerelycreatedaroute,controller,andcontrolleractiontosupportapagethatwecanaccessviatheURL,suchashttp://magelicious.loc/jsco/playground.ButthepageitselfisdefinedviaXMLlayout,andwefurthercreate
<MODULE_DIR>/view/frontend/layout/jsco_playground_index.xml,asfollows:
<pagexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"layout="empty"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainername="content">
<blockclass="Magelicious\Jsco\Block\Test"
name="jsco_test"
template="Magelicious_Jsco::playground.phtml">
</block>
</referenceContainer>
</body>
</page>
Notelayout="empty"he
re;thisistolimitourselvestoanearlyemptypagetoworkwith.
Finally,wecreateanempty<MODULE_DIR>view/frontend/templates/playground.phtmlpage.Ifweweretonowopenalink,suchashttp://magelicious.loc/jsco/playground,thatwouldopenapagewiththePlaygroundtitleshown.playground.phtmliswhereallofoursamplecodewillgoin,aswecontinueexploringthischapter.
CallingandinitializingJScomponentsCallingandinitializingJScomponentsmightseemabitchallengingatfirst.TherearetwotypesofsyntaxnotationsusedwithMagentoJScomponents:
Declarative:Usingthedata-mage-initattributeUsingthe<scripttype="text/x-magento-init"/>tag
Imperative:Usingthe<script>tag,withoutthetype="text/x-magento-init"attribute
Tobetterunderstandthedata-mage-initnotation,let'stakealookatapartial<PROJECT_DIR>/lib/web/mage/redirect-url.jsfileextract:
define([
'jquery',
'jquery/ui'
],function($){
'usestrict';
$.widget('mage.redirectUrl',{
options:{
event:'click',
url:undefined
},
_bind:function(){/*...*/},
_create:function(){/*...*/},
_onEvent:function(){/*...*/}
});
return$.mage.redirectUrl;
});
ThishereisajQuerywidgetwrappedasanAMDmodule;moreonthatlateron.data-mage-initknowshowtointerpretmage.redirectUrlasaredirectUrlcomponent.BystudyingtheredirectUrlwidgetcode,wecanseeitcanbeusednotonlywiththebuttonandthelinktypeofelementsbutwiththeselecttypeaswell.Let'sgoaheadandappendourplayground.phtmlfilewiththefollowing:
<adata-mage-init='{"redirectUrl":{"url":"http://test.url"}}'>
<span><?=__('Test')?></span>
</a>
<buttontype="button"
data-mage-init='{"redirectUrl":{"url":"http://test.url"}}'>
<span><?=__('Test')?></span>
</button>
<selectdata-mage-init='{"redirectUrl":{"event":"change"}}'>
<optionvalue="http://test.url/1">Test#1</option>
<optionvalue="http://test.url/2">Test#2</option>
<optionvalue="http://test.url/3">Test#3</option>
</select>
Whiletheclickeventworksperfectlyforlinkandbuttonelements,theselectelementreliesonamorespecificchangeevent.Therefore,ourselectelementexploitsthefactthattheredirectUrlcomponentacceptstheeventconfigurationoption.Thismakesforaniceandcleanlittleexampleofreusingasinglecomponentmultipletime.
Tobetterunderstandthe<scripttype="text/x-magento-init"/>notation,let'stakealookatapartial<MAGENTO_DIR>/module-cookie/view/frontend/web/js/notices.jsfileextract:
define([
'jquery',
'jquery/ui',
'mage/cookies'
],function($){
'usestrict';
$.widget('mage.cookieNotices',{
_create:function(){
//...
}
});
return$.mage.cookieNotices;
});
Justlikeinourfirstexample,thisisjustanotherjQuerywidgetessentially.WhatthecookieNoticeswidgetdoesistakethegivencontentanddisplayitascookienoticealerttotheuser,doingsountiltheuserfinallyhitstheAllowCookiesbutton.Wecaneasilyreusethiswidgettoinjectourowncontent.WhilebothcookieNoticesandredirectUrlarejQuerywidgets,thewaytheyareusedinMagentodiffers.
Let'sgoaheadandappendourplayground.phtmlfilewiththefollowingHTMLbits:
<divid="playgroundCookieBlock"class="messageglobalcookie"
style="display:none;">
<p>
<strong><?=$block->escapeHtml(__('Weusecookiestomakeyourexperiencebetter.'))?></strong>
<span><?=$block->escapeHtml(__('Tocomplywiththenewe-Privacydirective,weneedtoaskforyourconsenttosetthecookies.'))?></span>
<?=$block->escapeHtml(__('<ahref="%1">Learnmore</a>.','http://magelicious.loc/privacy'),['a'])?>
</p>
<divclass="actions">
<buttonid="btn-cookie-allow"class="actionallowprimary">
<span><?=$block->escapeHtml(__('AllowCookies'))?></span>
</button>
</div>
</div>
Thisistosimulateourintentforacustomcookiewidget,withspecialcontentandacookiename.Let'sfurtherappendtheplayground.phtmlfilewithadeclarativecalltocookieNoticesJScomponent:
<scripttype="text/x-magento-init">
{
"#playgroundCookieBlock":{
"cookieNotices":{
"cookieAllowButtonSelector":"#btn-cookie-allow",
"cookieName":"playgroundCookie",
"cookieValue":"playgroundCookieValue",
"cookieLifetime":"300",
"noCookiesUrl":"http://magelicious.loc/no-cookies"
}
}
}
</script>
UnliketheredirectUrlwidget,whichhadanicelistofoptionsdefinedattheverystartofthewidgetdefinition,thecookieNoticeswidgetdoesnothavethose.Itmerelyreferencesthoseoptionsthroughoutthecode,viathis.options.<optionPushedViaMagentoInit>calls.ThisisreallyadefaultjQuerywidgetoptionsobject.Thereasonwearebringingitupismerelytounderstandhow,mostofthetime,oneneedstotakeamoreinvolvedapproachtowardinspectingexistingJavaScriptcomponentscode,insteadofjustfocusingonthesetofpossibledefaultoptions.
Tobetterunderstandthe<script>tagnotation,let'stakealookatapartial<MAGENTO_DIR>/module-ui/view/base/web/js/modal/modal.jsfileextract:
define([
/*...*/
],function(/*...*/){
'usestrict';
//...
$.widget('mage.modal',{
//...
});
return$.mage.modal;
});
Asintheprevioustwoexamples,thisagainisjustajQuerywidget.Nowlet'sgoaheadandappendourplayground.phtmlfilewiththefollowingHTMLbits:
<div>
<ahref="#"id="playgroundModalLink">Showmodal!</a>
</div>
<divid="playgroundModal">
<p>Content...</p>
</div>
Thisistosimulateourintentofcreatingamodalbox,withspecialcontent.Now,let'susethemodalwidgettoturnthisintoanactualmodal.Wefurtherappendourplayground.phtmlfile,asfollows:
<script>
require([
'jquery',
'mage/translate',
'Magento_Ui/js/modal/modal'
],function($,$t,modal){
varoptions={
title:'PlaygroundModal',
buttons:[{
text:$t('Continue'),
click:function(){
this.closeModal();
}
}]
};
modal(options,$('#playgroundModal'));
$('#playgroundModalLink').on('click',function(){
$('#playgroundModal').modal('openModal');
});
}
);
</script>
Thistimeweareusingthe<script>tagapproachtoutilizetheJScomponent.
Toensureourcodeevaluatesonpageload,wecanfurtherwrapourmodalwidgetrelatedcodeintoafunction,asfollows:
<script>
require([
/*libraries...*/
],function(/*params...*/){
$(function(){
//RawJScode...
});
}
);
</script>
Likewise,wecanuseaRequireJSdomReadymoduletoexecuteourJScodeonDOM:
<script>
require([
'jquery',
'mage/translate',
'domReady!'
],function($,$t){
//RawJScode...
});
</script>
The!characterusedindomReady!isasyntaxreservedforplugins.Whilethereismoretoit,sufficetosaythatinacaseofdomReady!thepluginexistssimplyasawayofwaitinguntilDOMgetsloadedbeforeinvokingourfunction.
ThechoiceofcallingandinitializingJScomponentsdependsonhowtheyarewrittenandhowtheyareintendedtobeused.Weusethedeclarativenotationwhenourcomponentrequiresinitialization.Theconfigurationispreparedonthebackendandsimplyoutputtedtothepage.WeusetheimperativenotationonthepagesthatuserawJScode;thisallowsustoexecuteparticularbusinesslogic.
MeetRequireJSTothispoint,wehavebeenusingthingslikeredirectUrlandcookieNoticesoutofthinair,buthowexactlydothesecomponentsbecomeavailabletoourcode?Theansweris,viaRequireJS,alibrarythatunderliesnearlyeveryotherJSfeaturebuiltintoMagento.TheoverallroleofRequireJSissimple;itisaJSmodulesystemthatimplementstheAsynchronousModuleDefinition(AMD)standard,whichservesasanimprovementovertheweb'scurrentglobalsandscripttags.
WehavealreadyseentheformatoftheseAMDmodulesintheprecedingexamples,whichcomesdownthefollowing:
define(['dep1','dep2'],function(dep1,dep2){
returnfunction(){
//Modulevaluetoreturn
};
});
ThegistofAMDmodulesfunctionalitycomesdowntoeachmodulebeingableto:
RegisterthefactoryfunctionviadefineInjectdependencies,insteadofusingglobalsExecutethefactoryfunctionwhenalldependenciesbecomeaccessiblePassdependentmodulesasargumentstothefactoryfunction
Thisstrategysolvesmanyoftheconventionaldependencyissues,wheredependenciesareassumedtobeimmediatelyavailablewhenthefunctionexecutes,whichisnotalwaysthecase.
IfweweretodoaViewPageSourceonourPlaygroundpageinabrowser,wewouldseethree<scripttype="text/javascript"src="...">tagswiththeirsrcattributespointingtothefollowingJSfiles:
frontend/Magento/luma/en_US/requirejs/require.js
frontend/Magento/luma/en_US/mage/requirejs/mixins.js
frontend/Magento/luma/en_US/requirejs-config.js
Aquicklookatthepartialrequirejs-config.jsfilerevealshowthesegetloaded:
(function(require){
/*...*/
(function(){
varconfig={
map:{
'*':{
'redirectUrl':'mage/redirect-url',
}
}
};
require.config(config);
})();
/*...*/
(function(){
varconfig={
map:{
'*':{
cookieNotices:'Magento_Cookie/js/notices'
}
}
};
require.config(config);
})();
/*...*/
})(require);
Thesetwomappingsbreakdownasfollows:
Theleft-handsidepointstothefreelygivennameofourJScomponent,whichessentiallytellsconsumershowtoreferenceit.ThisiswhywewereabletousethesetwocomponentssimplybyreferencingthemviaredirectUrlandcookieNotices.Theright-handsidepointstothelocationofourJScomponent:
mage/redirect-url,wheremagepointstothe<PROJECT_DIR>/lib/web/magedirectory,andredirect-urlfurtherpointstotheredirect-url.jsfilewithinthatdirectoryMagento_Cookie/js/notices,whereMagento_Cookiepointstothe<MAGENTO_DIR>/module-cookie/view/frontend/webdirectory,andjs/noticesfurtherpointstothejs/notices.jsfilewithinthatdirectory
Furtherobservingtherequirejs-config.jsfile,asidefrommap,thereareafewotherimportantkeyswhoserolesareworthknowing:
varconfig={
map:{
'*':{
/*...*/
}
},
paths:{
/*...*/
},
shim:{
/*...*/
},
deps:[
/*...*/
],
config:{
mixins:{
/*...*/
}
}
};
Thesebreakdownasfollows:
map:Forthegivenmoduleprefix;insteadofloadingthemodulewiththegivenID,substituteadifferentmoduleIDpaths:PathmappingsformodulenamesnotfounddirectlyunderbaseUrlshim:Configurethedependencies,exports,andcustominitializationforolderbrowserglobalsscriptsthatdonotusedefinefordeclaringthedependenciesandsettingthemodulevaluedeps:Anarrayofdependenciestoloadconfig/mixins:ListofJSclassmappings,forclasseswhosemethodsareaddedto,ormixedin,withotherJSclasses
Seehttps://requirejs.org/docs/api.htmlformoreinformationontheRequireJSAPI.
Thetakeawayhereisthatourownmodulescandefinetherequirejs-config.jsfileontheirown,underthe<MODULE_DIR>/view/frontenddirectory,allowingustohookintothefinalMagentorequirejs-config.jsfilethatgetsgeneratedforthebrowser.This,inturn,allowsustoeasilyregisterourowncomponents,overrideexistingmappings,paths,andotherthings.
ReplacingjQuerywidgetcomponentsWhilethemajorityofthetime,wewouldwanttoleavetheexistingJScomponentstoworktheirmagicasis,therearetimeswhenbusinessrequirementsaredrasticenoughtomakethewholecomponentunusable.ThinkingintermsofPHPclasses,wecanimaginethatclassAimplementsX,whereaswewanttohaveacompletelydifferentimplementationofX,let'scallitB,thatsharesverylittlewithA.ThisisacasewheresimplyhavingBextendsAwouldnotsuffice,soweoptfordirectlyBimplementsX.WhiletherearenointerfacesinpureJS,thisdoesnotmeanwecannotcompletelyreplaceoneconcreteclasswithanother,aslongasweensurethosefewcrucialmethodsareavailableviathenewclass.
ReplacingJSclassesiseasywithMagento.Let'simaginewewanttofullyreplacetheredirectUrlcomponent.
Westartbycreatingthe<MODULE_DIR>/view/frontend/requirejs-config.jsfile,asfollows:
varconfig={
map:{
'*':{
redirectUrl:'Magelicious_Jsco/js/redirect-url'
}
}
};
WethenimplementtheactualMagelicious_Jsco/js/redirect-urlaspartofthe<MODULE_DIR>/view/frontend/web/js/redirect-url.jsfile,asfollows.
define([
'jquery',
],function($){
'usestrict';
$.widget('magelicious.redirectUrl',{
_create:function(){
//Newimplementation
console.log('magelicious.redirectUrl');
}
});
return$.magelicious.redirectUrl;
});
magelicious.redirectUrlmatchesthenewnameofourwidget,whereasmageliciousis
ournamespaceandredirectUrlistheactualnameofthewidgetwithinournamespace.
Oncewerefreshthestaticcontentviathephpbin/magentosetup:static-content:deploycommand,weshouldnowbeabletoseemagelicious.redirectUrlshowupinthebrowserconsolewindow.Clearly,thecurrentimplementationofredirectUrlwouldbreakthefunctionalitywehadwiththeoriginalcomponent,butitgoestoshowhoweasilywecanfullyreplacethecomponentwithanewone.
ExtendingjQuerywidgetcomponentsAssumingwewishtoextendtheredirectUrlcomponentinsteadofreplacingitcompletely,wecandosoinasimilarfashion.Theentryinourrequirejs-config.jsremainsthesame,whereasthedifferenceliesinhowweeditourredirect-url.jsfile:
define([
'jquery',
'jquery/ui',
'mage/redirect-url'
],function($){
'usestrict';
$.widget('magelicious.redirectUrl',$.mage.redirectUrl,{
/*Overrideofparent_onEventmethod*/
_onEvent:function(){
//Callparent's_onEvent()methodifneeded
returnthis._super();
}
});
return$.magelicious.redirectUrl;
});
Usingthe_superor_superApplyisajQuerywidgetwayofinvokingmethodsofthesamenameintheparentwidget.Whilethisapproachworks,thereisamoreelegantsolutioncalledmixins.
TheMagentomixinsforJSaremuchlikeitspluginsforPHP.Toconverttothemixinapproach,wereplaceourrequirejs-config.jswithcontent,asfollows.
varconfig={
config:{
mixins:{
'mage/redirect-url':{
'Magelicious_Jsco/js/redirect-url-mixin':true
}
}
}
};
Note,thatthistimeweareusingthefullpath'mage/redirect-url'insteadoftheredirectUrlaliasontheleftsideofthemapping,whereastherightsideofmappingpointstoourmixin.Theconventionistousethe-mixingsuffixontopoftheoriginalJSfilename.
Wethencreate<MODULE_DIR>/view/frontend/web/js/redirect-url-mixin.jswithcontent,as
follows:
define([
'jquery'
],function($){
returnfunction(originalWidget){
$.widget(
'magelicious.redirectUrl',
originalWidget,{
/*Redefined_onEventmethod*/
_onEvent:function(){
console.log('_onEventviamixin');
//Callparent's_onEvent()methodifneeded
returnthis._super();
}
}
);
return$.magelicious.redirectUrl;
};
});
Theexampleheremightnotdojustice,asitmerelylooksmorecomplexthanthepreviousexampleofdirectlyextendingthewidget.ThisisbecausewecannotsimplydooriginalWidget._onEvent=function(){/*...*/};ororiginalWidget._proto._onEvent=function(){/*...*/};andthusoverridethewidgetmethod.Widgetmethodsneedtobeoverriddenontheprototype,which,inourcase,essentiallymeanscreatinganewwidgetfromtheoriginal.
Ifwewereaddingamixinforanon-widgettypeofJS,suchasMagento_Checkout/js/action/place-order,thentheapproachwouldbedifferent,asshowninMagento_CheckoutAgreements/js/model/place-order-mixin.
CreatingjQuerywidgetscomponentsCreatingsimplejQuerywidgetscomponentsisprettystraightforwardfromaMagentopointofview.TheactualknowledgeofbuildingrobustjQuerywidgetsdependsonourknowledgeofjQueryitself.
Let'sassumeourwidgetwillbecalledwelcome,anditspurposeistosimplyoutputWelcome%name%totheelement,providedwepassedonthenameoptionduringwidgetinitialization.
Westartbyaddingthemappingunderour<MODULE_DIR>/view/frontend/requirejs-config.jsfile,asfollows:
varconfig={
map:{
'*':{
welcome:'Magelicious_Jsco/js/welcome'
}
}
};
Wethendefinethewidgetitself,aspartofthe<MODULE_DIR>/view/frontend/web/js/welcome.jsfile,asfollows:
define([
'jquery',
'mage/translate'
],function($,$t){
'usestrict';
$.widget('magelicious.welcome',{
_create:function(){
this.element.text($t('Welcome'+this.options.name));
}
});
return$.magelicious.welcome;
});
Wecanseethatourwidgetisquitesimple.IfwenowrunMagento'ssetup:static-content:deploycommand,ourwidgetshouldalreadybereadyforuse,aswecannowinitializeitfromtemplatefiles.
Finally,let'sinitializeourwelcomewidgetbyamendingplayground.phtml,asfollows:
<?php$helper=$this->helper('Magento\Framework\Json\Helper\Data')?>
<spandata-mage-init='<?=$helper->jsonEncode(
['welcome'=>['name'=>'JohnDoe']]
)?>'></span>
Withthisinplace,weshouldnowbeabletoseetheWelcomeJohnDoemessageinourbrowser.Whilethislittlecomponentseemsquiteanoverkillforwhatitdoes,theconceptsbehinditarewhatmatters.
Seehttps://api.jqueryui.com/jquery.widget/formoreinformationoncreatingjQuerywidgets.
CreatingUI/KnockoutJScomponentsTothispoint,wehaveonlybeendealingwithjQuerywidgetsascomponents.Whileextremelypowerful,jQuerywidgetsarenotbestsuitedforrenderingrobustcomponentswithcomplexHTMLstructures.TheothertypeofJScomponentsiswhatwerefertoasUI/KnockoutJScomponents.BuiltontheshouldersoftheKnockoutJSlibrary,thesecomponentsallowpowerfultemplatingofourdata,amongotherthings.Withoutgettingtoodeepintotheinsandoutsofthesetypeofcomponents,sufficetosaythatthemainconstructwearereferringtowhenwespeakofUI/KnockoutJScomponentsisuiComponent.
Asper<MAGENTO_DIR>/module-ui/view/base/requirejs-config.js,theuiComponentmapstotheMagento_Ui/js/lib/core/collectionJSfile.Inspectingthecollection.jsfile,wecanseethatuiComponentextendsuiElement,whichmapstotheMagento_Ui/js/lib/core/element/elementJSfile.TheuiComponentanduiElementmakeuseoftheko,underscore,mageUtils,uiRegistry,uiEvents,anduiClasslibraries,amongotherthings,soit'sworthgettingourselvesfamiliarwiththose.
CreatingnewUI/KnockoutJScomponentsisaslightlymoreinvolvedprocessthancreatingajQuerywidget.
Westartbycreatingthepropermappingunderour<MODULE_DIR>/view/frontend/requirejs-config.jsfile,asfollows:
varconfig={
map:{
'*':{
popularProducts:'Magelicious_Jsco/js/popular-products'
}
}
};
ThispartisthesameaswithjQuerywidgets.Herewesimplyregister,oraliasifyouwill,ourcomponentnametoitsfilelocation.
Wethendefinethecomponentitself,underthe<MODULE_DIR>/view/frontend/web/js/popular-products.jsfile,asfollows:
define([
'jquery',
'uiComponent',
'ko',
'mage/translate'
],function($,Component,ko,$t){
'usestrict';
returnComponent.extend({
defaults:{
template:'Magelicious_Jsco/popular-products',
title:$t('PopularProducts'),
products:[],
},
getTitle:function(){
returnthis.title;
}
});
}
);
ThebasisofallUIcomponentsisuiComponent.WepassontheinstanceofuiComponentasaComponentparameter.WethenimplementthespecificsofourcomponentaspartoftheJSONobjectpassedontotheComponent.extendmethod.
WithourcomponentJSfilenowinplace,wefurthercreatethetemplatefilereferencedbythecomponent.Wedosounderthe<MODULE_DIR>/view/frontend/web/template/popular-products.htmlfile,asfollows:
<h4data-bind="text:getTitle()"></h4>
<uldata-bind="foreach:products">
<li>
<span>
<spandata-bind="text:title"></span>
(<spandata-bind="text:sku"></span>)
</span>
</li>
</ul>
WhathappensintheHTMLtemplatefilesisallaboutKnockoutJS,whichmeansacertainpartoftheKnockoutJSlibraryisrequiredinordertobuiltUI/KnockoutJScomponents.
Seehttp://knockoutjs.comformoreinformationontheKnockoutJSlibrary.
Wethenamendourjsco_playground_index.xmlbyaddingthefollowinglineunder<referenceContainername="content">:
<blockname="popular_products"
template="Magelicious_Jsco::popular-products.phtml"/>
popular-products.phtmliswherewewillinstantiateourUI/KnockoutJScomponent.
Finally,wecreate<MODULE_DIR>/view/frontend/templates/popular-products.phtmlwithcontent,asfollows:
<?php$jsonHelper=$this->helper('Magento\Framework\Json\Helper\Data');?>
<divclass="popular-products"data-bind="scope:'popular-products-scope'">
<!--kotemplate:getTemplate()--><!--/ko-->
</div>
<scripttype="text/x-magento-init">
{
".popular-products":{
"Magento_Ui/js/core/app":{
"components":{
"popular-products-scope":{
"component":"popularProducts",
"products":<?=/*@escapeNotVerified*/$jsonHelper->jsonEncode([
['sku'=>'sku1','title'=>'Title1'],
['sku'=>'sku2','title'=>'Title2']
])?>
}
}
}
}
}
</script>
Hereweareusingthedeclarativeapproachtoinitializeourcomponent.ThestructureoftheJSONobjectunderthescripttagmightseemabitconfusingatfirst.The.popular-productskeyisessentiallyaselector,targetingwhateverHTMLelementitmightfind.Magento_Ui/js/core/appisanaliasfortheapp.jsfile,whichcreatestheUIcomponentsinstancesaccordingtotheconfigurationoftheJSONusingtheuiLayoutcomponent.componentsisakeyunderwhichwenestoneormorecomponentswewishtoinitialize.popular-products-scopeissortofascopekeyassignedtoourcomponent,whichweusetodata-bindthescopevaluetotheHTMLelement.
Clearingthecacheandredeployingthestaticfiles,weshouldnowbeabletoseeournewlycreatedcomponent.
ExtendingUI/KnockoutJScomponentsExtendingUI/KnockoutJScomponentsisaprocesssimilartoextendingthejQuerywidgets.Let'sforamomentassumewehavetheMagelicious_Jsco2modulethatwantstooverrideourpopularProductscomponent.
Thewaytodoitwouldbetoaddthepropermappingunderthemapkeyofour<MODULE2_DIR>/view/frontend/requirejs-config.jsfile:
varconfig={
map:{
'*':{
popularProducts:'Magelicious_Jsco2/js/new-popular-products'
}
}
};
Wethencreatethepropernew-popular-products.jsfile,asfollows:
define([
'jquery',
'Magelicious_Jsco/js/popular-products',
'ko',
'mage/translate',
],function($,popularProductsComponent,ko,$t){
'usestrict';
returnpopularProductsComponent.extend({
getTitle:function(){
return'NEW|'+this._super();
}
});
}
);
TheexamplehereshowsthatwearenolongerpassingintheinstanceofuiComponent,rathertheinstanceoftheoriginalMagelicious_Jsco/js/popular-productsthatwewishtoextend.SimplyusingtheextendmethodonourpopularProductsComponentobjectallowsustoextenditeasily.Byredefiningthemethodsofthesamename,suchasgetTitle,weeffectivelyoverridethesamemethodonthecomponentwearerunningtheextendon.
SummaryThoughtherearelotsofotherbitsandpiecesinvolvedinstorefrontdevelopment,JScomponentsmakeforthemostchallengingpartofit.Understandinghowtowritenewcomponents,aswellashowtooverrideorbypassexistingonesisanessentialskillforanyMagentodeveloper,beitbackendorfrontend.Admittedly,thischaptertookmoreofabackend/module-developertypeofanapproachonthesubject.
Wheneverthereisaneedtochangethebehavioroftheunderlyingcomponent,whetheritispureJS,ajQuerywidget,orUI/KnockoutJS,weshouldconsiderthescopeofchangesinordertodecidewhetherweshouldapproachitbyreplacing,overriding,orusingmixin.
Movingforward,wearegoingtotakealookatsomeoftheneatthingswecandoaroundcustomizingthestorefrontcatalogbehavior,mostofwhichcomedowntopluginsandJScomponents.
CustomizingCatalogBehaviorRightoutofthebox,Magentoprovidesaprettyrobustcatalogfunctionality.Managingcategoriesandproductsonamulti-store,multi-currency,multi-languagelevelwithasupportforcustomattributes,catalogsearch,catalogrules,andalikearefeaturesthatarelikelytosufficeformostcustomers.Sometimes,however,certainintegrationsorlargerandsmallerfeaturesarerequested,thatbuildontopoftheexistingfunctionality.Whethertoimproveuserexperienceoraccommodateessentialbusinessrequirements,catalogcustomization'splayamajorroleineverydayMagentodevelopment.
Wearegoingtocustomizeourcatalogbehaviorby:
CreatingthesizeguideCreatingthesamedaydeliveryFlaggingnewproducts
Thesestandonlyasasmallfragmentofwhat'spossiblewithMagentocatalogcustomizations.
Movingforward,ourworkistobedoneaspartoftheMagelicious_Catalogmodule,whichwewilldevelopthroughoutthechapter.
TechnicalrequirementsYouwillneedtohavebasicknowledgeofPHP,OOP,JavaScript,andXML.YouwillalsoneedApache,MySQL,andAMPPSinstalledonyoursystemtoexecutethecodes.
ThecodefilesofthischaptercanbefoundonGitHub:https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
CheckoutthefollowingvideotoseetheCodeinAction:
http://bit.ly/2MFJaCN.
CreatingthesizeguideWehavebeenaskedtoaddafunctionalitythatshowsthesizeguideonaproductviewpage.ThisistoappearasanewtabnexttotheexistingDetails,MoreInformation,andReviewstabs.Thecontentofthesizeguidetabistobethesameforallproductscontainingthesizeattribute.WealsoneedittobeeditablefromMagentoadmin.
Let'stakeamomenttothinkaboutourapproachhere:
TobethesameforallproductsandeditablefromMagentoadminneedstheCMSblockTheCMSblockneedssetupscriptforcreatingthesizeguideblockToAppearasanewtabnexttotheexistingtabsrequiresacatalog_product_view.xmllayoutupdate
Assumingwehavedefinedregistration.php,composer.json,andetc/module.xmlasbasicmodulefiles,wecandealwiththemorespecificdetailsofourMagelicious_Catalogmodule.
Westartbydefining<MODULE_DIR>/Setup/InstallData.phpwithcontent,asfollows:
namespaceMagelicious\Catalog\Setup;
classInstallDataimplements\Magento\Framework\Setup\InstallDataInterface{
protected$searchCriteriaBuilder;
protected$blockRepository;
protected$blockFactory;
publicfunction__construct(
\Magento\Framework\Api\SearchCriteriaBuilder$searchCriteriaBuilder,
\Magento\Cms\Api\BlockRepositoryInterface$blockRepository,
\Magento\Cms\Api\Data\BlockInterfaceFactory$blockFactory
){
$this->searchCriteriaBuilder=$searchCriteriaBuilder;
$this->blockRepository=$blockRepository;
$this->blockFactory=$blockFactory;
}
publicfunctioninstall(
\Magento\Framework\Setup\ModuleDataSetupInterface$setup,
\Magento\Framework\Setup\ModuleContextInterface$context
){
$setup->startSetup();
$searchCriteria=$this->searchCriteriaBuilder
->addFilter('identifier','size-guide','eq')
->create();
$blocks=$this->blockRepository->getList($searchCriteria)->getItems();
if(empty($blocks)){
/*@var\Magento\Cms\Api\Data\BlockInterface$block*/
$block=$this->blockFactory->create();
$block->setIdentifier('size-guide');
$block->setTitle('SizeGuide');
$block->setContent('Sizeguide!');
$this->blockRepository->save($block);
}
$setup->endSetup();
}
}
TheInstallDatascriptensuresthatthesize-guideCMSblockiscreatedduringmoduleinstallationifitdoesnotalreadyexist.Withthisinplace,wecanalreadyrunthesetup:upgradecommand.Thisshouldinstallourmoduleandcreatethesize-guideCMSblock.
Wethendefine<MODULE_DIR>/Block/SizeGuide.phpwithcontent,asfollows:
namespaceMagelicious\Catalog\Block;
classSizeGuideextends\Magento\Cms\Block\Blockimplements\Magento\Framework\DataObject\IdentityInterface{
protected$product;
protected$coreRegistry;
publicfunction__construct(
\Magento\Framework\View\Element\Context$context,
\Magento\Cms\Model\Template\FilterProvider$filterProvider,
\Magento\Store\Model\StoreManagerInterface$storeManager,
\Magento\Cms\Model\BlockFactory$blockFactory,
\Magento\Framework\Registry$coreRegistry,
array$data=[]
){
$this->coreRegistry=$coreRegistry;
parent::__construct($context,$filterProvider,$storeManager,$blockFactory,$data);
}
publicfunction_toHtml(){/*...*/}
publicfunctiongetProduct(){
if(!$this->product){
$this->product=$this->coreRegistry->registry('product');
}
return$this->product;
}
}
Thisistheactualblockclassthatwewilloutputontheproductviewpage.Theregistry'sobjectproductkeyisalreadysetbytheparentclassupthelayouttree.Thisallowsustoeasilyfetchtheinstanceofthecurrentproduct.
The_toHtmlmethodisfurtherimplemented,asfollows:
protectedfunction_toHtml()
{
if($this->getProduct()->getTypeId()==\Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE){
$configurableAttributes=$this->getProduct()->getTypeInstance()->getConfigurableAttributesAsArray($this->getProduct());
foreach($configurableAttributesas$attribute){
if(isset($attribute['attribute_code'])&&$attribute['attribute_code']=='size'){
returnparent::_toHtml();
}
}
}
return'';
}
Thisisthegistofoursizeguidefunctionality.Theconfigurabletypeandsizeattributecodechecksensurethattheoutputof_toHtmlrendersthesize-guideblockonlyforcertaingroupsofproducts.
Wefinallydefine<MODULE_DIR>/view/frontend/layout/catalog_product_view.xmlwithcontent,asfollows:
<page>
<body>
<referenceBlockname="product.info.details">
<blockclass="Magelicious\Catalog\Block\SizeGuide"name="size-guide"after="-"group="detailed_info">
<arguments>
<argumentname="block_id"xsi:type="string">size-guide</argument>
<argumentname="css_class"xsi:type="string">description</argument>
<argumentname="at_label"xsi:type="string">none</argument>
<argumentname="title"translate="true"xsi:type="string">SizeGuide</argument>
</arguments>
</block>
</referenceBlock>
</body>
</page>
ThisisthegluethatbindsourSizeGuideblocktoaproductviewpage,and,morespecifically,theproduct.info.detailsblockthatneatlycontainstheDetails,MoreInformation,andReviewstabs.
Thefinalproductviewpageresultshouldlooklikethis:
CreatingthesamedaydeliveryWehavebeenaskedtoaddafunctionalitythatshowsanactivecountdownwithaYouhave%h%min%sectocatchoursamedaydeliveryoffermessageonaproductviewpage,whereasthecountdownisbasedonanoptionallyassigneddailycutoffAttime,setforeveryproductindividually,foreverydayofaweekindependently.
Let'stakeamomenttothinkaboutourapproachhere:
EveryproductandeverydayofaweekimplyMondaytoSunday_[Cutoff_At]productattributesProductattributesimplysetupscriptActivecountdownimpliesJScomponents
Westartbybumpingupthesetup_versionvalueofour<MODULE_DIR>/etc/module.xmlfilefrom1.0.0to1.0.1.Thisallowsustointroducethe<MODULE_DIR>/Setup/UpgradeData.phpfilewithanupgrade,asfollows:
protectedfunctionupgradeToVersionOneZeroOne(
\Magento\Framework\Setup\ModuleDataSetupInterface$setup
){
$eavSetup=$this->eavSetupFactory->create(['setup'=>$setup]);
$days=[
'monday','tuesday','wednesday','thursday',
'friday','saturday','sunday'
];
$sortOrder=100;
foreach($daysas$day){
$eavSetup->addAttribute(
\Magento\Catalog\Model\Product::ENTITY,
$day.'_cutoff_at',
[
'type'=>'varchar',
'label'=>ucfirst($day).'CutoffAt',
'input'=>'text',
'required'=>false,
'sort_order'=>$sortOrder++,
'global'=>\Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
'group'=>'Cutoff',
]
);
}
}
TheaddAttributemethodhereisrunforeachdayoftheweek,thuscreatingmonday_cutoff_attosunday_cutoff_atproductattributes.If,atthispoint,weweretoruntheMagento'ssetup:upgradecommand,ourUpgradeDatascriptwouldgetexecutedandschema_versionanddata_versionnumbersfromwithinthesetup_moduletablewouldgetbumpedtothe1.0.1version.Likewise,goingintotheMagentoadminareaandeditingorcreatinganewproduct,wouldshowthefollowingscreen.Thisiswhereweenabletheusertoenterthetimeofthedayinan<hour>:<minute>format,suchas15:30.Thistime,ifentered,willlaterbeusedbytheJScomponenttorenderthecountdownfunctionalityonthestorefrontproductviewpage:
Wethencreate<MODULE_DIR>/Block/Product/View/Cutoff.php,asfollows:
namespaceMagelicious\Catalog\Block\Product\View;
classCutoffextends\Magento\Framework\View\Element\Templateimplements\Magento\Framework\DataObject\IdentityInterface
{
private$product;
protected$coreRegistry;
protected$localeDate;
publicfunction__construct(
\Magento\Framework\View\Element\Template\Context$context,
\Magento\Framework\Registry$coreRegistry,
\Magento\Framework\Stdlib\DateTime\TimezoneInterface$localeDate,
array$data=[]
){
$this->coreRegistry=$coreRegistry;
$this->localeDate=$localeDate;
parent::__construct($context,$data);
}
publicfunctiongetProduct(){/*...*/}
publicfunctiongetCutoffAt(){/*...*/}
publicfunctiongetIdentities(){/*...*/}
}
Wewillusethisclasswhenwereachourlayoutupdate.
ThegetProductmethodisfurtherimplemented,asfollows:
publicfunctiongetProduct()
{
if(!$this->product){
$this->product=$this->coreRegistry->registry('product');
}
return$this->product;
}
Asmentionedpreviously,theregistry'sproductkeyisalreadysetbytheparentclassupthelayouttree,soweexploitthatfacttofetchthecurrentproduct.
ThegetCutoffAtmethodisfurtherimplemented,asfollows:
publicfunctiongetCutoffAt()
{
$timezone=new\DateTimeZone($this->localeDate->getConfigTimezone());
$now=new\DateTime('now',$timezone);
$day=strtolower($now->format('l'));
$cutoffAt=$this->getProduct()->getData($day.'_cutoff_at');
if($cutoffAt){
$timeForDay=\DateTime::createFromFormat(
'Y-m-dH:i',
$now->format('Y-m-d').''.$cutoffAt,
$timezone
);
if($timeForDayinstanceof\DateTime){
return$timeForDay->format(DATE_ISO8601);
}
}
return0;
}
ThisisthegistofoursamedaydeliveryfunctionalityfromthePHPsideofthings.Weensureweproperlyreturnthefulldateandtimebasedontheproduct's$day.'_cutoff_at'attributevalue;thiswilllaterbepassedontotheJScomponent.
Finally,thegetIdentitiesmethodisfurtherimplemented,asfollows:
publicfunctiongetIdentities()
{
$identities=$this->getProduct()->getIdentities();
$timezone=new\DateTimeZone($this->localeDate->getConfigTimezone());
$now=new\DateTime('now',$timezone);
$day=strtolower($now->format('l'));
returnarray_push($identities,$day);
}
ThegetIdentitiesmethodhasbeenimplementedinawaytoensurecachingofthisblockisconsideredinarelationtoproductidentityaswellasthedayoftheweek.
Wethencreatethe<MODULE_DIR>/view/frontend/requirejs-config.jsfile,asfollows:
varconfig={
map:{
'*':{
cutoffAt:'Magelicious_Catalog/js/cutoff'
}
}
};
ThisregistersthecutoffAtcomponentwithMagento,whichpointstoourmodule'scutoff.jsfile.
Wethencreatethe<MODULE_DIR>/view/frontend/web/js/cutoff.jsfile,asfollows:
define([
'jquery',
'uiComponent',
'ko',
'moment'
],function($,Component,ko,moment){
'usestrict';
returnComponent.extend({
defaults:{
template:'Magelicious_Catalog/cutoff',
expiresAt:null,
timerHide:false,
timerHours:null,
timerMinutes:null,
timerSeconds:null,
},
initialize:function(){
this._super();
this.countdown(this);
returnthis;
},
initObservable:function(){
this._super()
.observe('timerHidetimerHourstimerMinutestimerSeconds');
returnthis;
},
countdown:function(self){/*...*/}
});
}
);
OurJScomponenttemplatevaluepointsto<MODULE_DIR>/view/frontend/web/template/cutoff.html,whichwewillsoonaddress.expiresAtistheonlyrealoptionthatisexpectedtobepassedonwhenthecomponentisinitialized.Theobservabletimer*optionswillbeusedinternallytocontrolthefunctionalityofourcomponent.
Thecountdownfunctionisfurtherimplemented,asfollows:
countdown:function(self){
vartoday=moment(newDate());
setInterval(function(){
self.expiresAt=moment(self.expiresAt).subtract(1,'seconds');
varmilliseconds=moment(self.expiresAt,'DD/MM/YYYYHH:mm:ss').diff(moment(today,'DD/MM/YYYYHH:mm:ss'));
varduration=moment.duration(milliseconds);
self.timerHours(duration.hours()>=0?duration.hours():0);
self.timerMinutes(duration.minutes()>=0?duration.minutes():0);
self.timerSeconds(duration.seconds()>=0?duration.seconds():0);
if(self.timerHours()==0
&&self.timerMinutes()==0
&&self.timerSeconds()==0
){
self.timerHide(true);
}
},1000);
}
Thishereisthegistofoursamedaydeliveryfunctionality.UsingthecoreJSsetIntervalmethod,wesetupasimpleper-secondcounter.WiththefewlinesofcodewrappedwithinsetInterval,wecontrolourobservabletimer*optionsboundtoourcutoff.htmltemplate.This,inturn,resultsinthevisualcountdowneffect.
Wethencreatethe<MODULE_DIR>/view/frontend/web/template/cutoff.htmlfile,asfollows:
<spanclass="cutoff-component"data-bind="ifnot:timerHide">
<spantranslate="'Youhave'"></span>
<spanclass="timer">
<spanclass="timer-parttimer-part-hours">
<spanclass="numeric"data-bind="text:timerHours"></span>
<spanclass="label"data-bind="i18n:'hours'"></span>
</span>
<spanclass="timer-parttimer-part-minutes">
<spanclass="numeric"data-bind="text:timerMinutes"></span>
<spanclass="label"data-bind="i18n:'minutes'"></span>
</span>
<spanclass="timer-parttimer-part-seconds">
<spanclass="numeric"data-bind="text:timerSeconds"></span>
<spanclass="label"data-bind="i18n:'seconds'"></span>
</span>
</span>
<spantranslate="'tocatchoursamedaydeliveryoffer.'"></span>
</span>
ThisisthetemplatefilebehindourJScomponent.Weseeallthosetimer*optionsbeingboundedtoproperspanelements.Wrappingeverytimer*optioninitsownspanallowsforpotentialflexibilityaroundstylinglateron.
Seehttps://devdocs.magento.com/guides/v2.2/ui_comp_guide/concepts/knockout-bindings.htmlforalistofMagentocustomKnockout.jsbindings.
Wethencreatethe<MODULE_DIR>/view/frontend/templates/product/view/cutoff.phtmlfile,asfollows:
<?php/*@var\Magelicious\Catalog\Block\Product\View\Cutoff$block*/?>
<?php$jsonHelper=$this->helper('Magento\Framework\Json\Helper\Data');?>
<divclass="cutoff"data-bind="scope:'cutoff-scope'">
<!--kotemplate:getTemplate()--><!--/ko-->
</div>
<scripttype="text/x-magento-init">
{
".cutoff":{
"Magento_Ui/js/core/app":{
"components":{
"cutoff-scope":{
"component":"cutoffAt",
"expiresAt":<?=/*@escapeNotVerified*/$jsonHelper->jsonEncode($block->getCutoffAt())?>
}
}
}
}
}
</script>
ThisisthetemplatefilethatinitializesourJScomponent.Withthisfileinplace,wecanfinallygluethingstogetherbyamendingthebodyelementofthe<MODULE_DIR>/view/frontend/layout/catalog_product_view.xmlfile,asfollows:
<referenceBlockname="product.info.extrahint">
<blockname="cutoff"
class="Magelicious\Catalog\Block\Product\View\Cutoff"
template="Magelicious_Catalog::product/view/cutoff.phtml">
</block>
</referenceBlock>
Thefinalproductviewpageresultshouldlooklikethis:
Oncethetimerreaches0hours0minutes0seconds,itshoulddisappear.
FlaggingnewproductsWehavebeenaskedtoaddafunctionalitythatflagseverynewproductshownonthestorefrontcategoryviewandproductviewpageswitha[NEW]prefixinfrontofitsname.Newimpliesanythingwithinthe5daysoftheproduct'screated_atvalue.
Luckilyforus,wecaneasilycontrolaproduct'snameviaanafterpluginonaproduct'sgetNamemethod.AllittakesistodefineanafterGetNamepluginwithacategoryviewandproductviewpagesconstraint,furtherfilteredbyacreated_atconstraint.
Toregistertheplugin,westartbycreatingthe<MODULE_DIR>/etc/frontend/di.xmlfilewithcontent,asfollows:
<config>
<typename="Magento\Catalog\Api\Data\ProductInterface">
<pluginname="newProductFlag"type="Magelicious\Catalog\Plugin\NewProductFlag"/>
</type>
</config>
Wethencreatethe<MODULE_DIR>/Plugin/NewProductFlag.phpfilewithcontent,asfollows:
namespaceMagelicious\Catalog\Plugin;
classNewProductFlag
{
protected$request;
protected$localeDate;
publicfunction__construct(
\Magento\Framework\App\RequestInterface$request,
\Magento\Framework\Stdlib\DateTime\TimezoneInterface$localeDate
)
{
$this->request=$request;
$this->localeDate=$localeDate;
}
publicfunctionafterGetName(\Magento\Catalog\Api\Data\ProductInterface$subject,$result)
{
$pages=['catalog_product_view','catalog_category_view'];
if(in_array($this->request->getFullActionName(),$pages)){
$timezone=new\DateTimeZone($this->localeDate->getConfigTimezone());
$now=new\DateTime('now',$timezone);
$createdAt=\DateTime::createFromFormat('Y-m-dH:i:s',$subject->getCreatedAt(),$timezone);
if($now->diff($createdAt)->days<5){
return__('[NEW]').$result;
}
}
return$result;
}
}
TheafterGetNameisourafterplugintargetingtheproduct'sgetNamemethod.Usingtherequest'sgetFullActionNamemethod,wemakesureourpluginisconstrainedtoonlycatalog_product_viewandcatalog_category_viewpages,orelsetheoriginalproductnameisreturned.Theuseofthepropertimezoneanddiffmethodassuresthatwefurtherfilterdowntoonlythoseproductsthatweconsidernew.Clearingthecacheatthispointshouldallowourfunctionalitytokickin.
Thefinalresultshouldlooklikethis:
SummaryInthischapter,wehavebuiltthreedistinctivefunctionalities,allofwhichrelatetothecatalogpartofMagento.Thoughverylightweight,theystandtoshowhoweasilyMagentocanbeextendedwithnewfeatureswithoutreallyoverridinganyofthecorefiles.UsingpluginsandJScomponentsaremerelysomeoftheapproacheswemighttake.Quiteoften,wewillfindthatasinglerequirementmightbedeliveredwithmorethanoneapproach.Themainguidingruleforourcodeshouldalwaysbe:usetheleastintrusive.Catalogfunctionalityplaysamajorroleinthecustomerconversionprocess,soourpriorityshouldalwaysbefailsafewhenpossible.
Movingforward,wearegoingtotakealookatsomeofthethingswecandotocustomizethecheckout.
CustomizingCheckoutExperiencesWhilethedefaultMagentocheckoutprovideseverythingashopneedstocompleteatransactionsuccessfully,therearedetailsspecifictotheindividualbusinessesthatoftenneedtobeaddressed.Agreatdealofthesedetailsoftenrelatetocheckoutcustomizationsthatallowforthecapturingofadditionalinformationorengagingcustomersinagreementsandsubscriptionactivities.
Movingforward,wearegoingtotakealookatthefollowing:
PassingdatatothecheckoutAddingordernotestothecheckout
TechnicalrequirementsYouwillneedtohavebasicknowledgeofPHP,OOP,JavaScript,andXML.YouwillalsoneedApache,MySQL,andAMPPSinstalledonyoursystemtoexecutethecodes.
ThecodefilesofthischaptercanbefoundonGitHub:https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
CheckoutthefollowingvideotoseetheCodeinAction:
http://bit.ly/2PHMwqX.
PassingdatatothecheckoutUnlikethemostlystaticCMS,category,andproductpages,thecheckoutpagehasamoredynamicnature.Itisanapplicationonitsown,primarilyconstructedoutofJScomponents,whichfurtherutilizeMagento'sAPIendpointstomoveusthroughthecheckoutsteps.Magento'sMagento\Checkout\Model\CompositeConfigProvidertypeallowsustopushthenecessaryserver-sideinformationeasilytotheuiComponentofthestorefronts.
Aquicklookupforthename="configProviders"stringacrossthecontentofdi.xmlinthe<MAGENTO_DIR>directoryrevealsdozenofdefinitions.Acloserlookatthe<MAGENTO_DIR>/module-tax/etc/frontend/di.xmlrevealsthefollowing:
<typename="Magento\Checkout\Model\CompositeConfigProvider">
<arguments>
<argumentname="configProviders"xsi:type="array">
<itemname="tax_config_provider"xsi:type="object">Magento\Tax\Model\TaxConfigProvider</item>
</argument>
</arguments>
</type>
WeareessentiallyinjectingnewitemsundertheconfigProvidersargumentoftheMagento\Checkout\Model\CompositeConfigProvidertype.Theimplementationofacustomconfigprovider,suchastheMagento\Tax\Model\TaxConfigProvider,mustimplementtheMagento\Checkout\Model\ConfigProviderInterface.TheunderlyinggetConfigmethodreturnsanarrayofkey-valuemappings,suchas:
return[
'isDisplayShippingPriceExclTax'=>$this->isDisplayShippingPriceExclTax(),
'isDisplayShippingBothPrices'=>$this->isDisplayShippingBothPrices(),
'reviewShippingDisplayMode'=>$this->getDisplayShippingMode(),
/*...*/
];
These,inturn,becomeavailabletotheuiComponent,asobservedin<MAGENTO_DIR>/module-tax/view/frontend/web/js/view/checkout/shipping_method/price.js:
isDisplayShippingPriceExclTax:window.checkoutConfig.isDisplayShippingPriceExclTax,
isDisplayShippingBothPrices:window.checkoutConfig.isDisplayShippingBothPrices,
WecanseethevaluesreturnedbythegetConfigmethodnowavailableundertheJavaScriptwindow.checkoutConfigobject.Thisisasimplemechanismbywhichwe
canpushourserver-sidedatatoourstorefrontwhenapageloads.
Tounderstandcheckoutmodificationsbetter,weshouldfamiliarizeourselveswiththecontentofthewindow.checkoutConfigobject.
AddingordernotestothecheckoutNowthatweunderstandthemechanismbehindthewindow.checkoutConfigobject,let'sputittousebycreatingasmallmodulethataddsordernotesfunctionalitytothecheckout.OurworkistobedoneaspartoftheMagelicious_OrderNotesmodule,withthefinalvisualoutcome,asfollows:
Theideabehindthemoduleistoprovideacustomerwithanoptionofputtinganoteagainsttheirorder.Ontopofthat,wealsoprovideastandardrangeofpossiblenotestochoosefrom.
Assumingwehavedefinedregistration.php,composer.json,andetc/module.xmlasbasicmodulefiles,wecandealwiththemorespecificdetailsofourMagelicious_OrderNotesmodule.
Westartbydefiningthe<MODULE_DIR>/Setup/InstallSchema.phpwithcontent,asfollows:
namespaceMagelicious\OrderNotes\Setup;
classInstallSchemaimplements\Magento\Framework\Setup\InstallSchemaInterface
{
publicfunctioninstall(
\Magento\Framework\Setup\SchemaSetupInterface$setup,
\Magento\Framework\Setup\ModuleContextInterface$context
){
$connection=$setup->getConnection();
$connection->addColumn(
$setup->getTable('quote'),
'order_notes',
[
'type'=>\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
'nullable'=>true,
'comment'=>'OrderNotes'
]
);
$connection->addColumn(
$setup->getTable('sales_order'),
'order_notes',
[
'type'=>\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
'nullable'=>true,
'comment'=>'OrderNotes'
]
);
}
}
OurInstallSchemascriptcreatesthenecessaryorder_notescolumninboththequoteandsales_ordertables.Thisiswherewewillstorethevalueofthecustomer'scheckoutnote,ifthereisany.
Wethendefinethe<MODULE_DIR>/etc/frontend/routes.xmlwithcontent,asfollows:
<config>
<routerid="standard">
<routeid="ordernotes"frontName="ordernotes">
<modulename="Magelicious_OrderNotes"/>
</route>
</router>
</config>
TheroutedefinitionhereensuresthatMagentowillrecognizeHTTPrequestsstartingwithordernotes,andlookforcontrolleractionswithinourmodule.
Wethendefinethe<MODULE_DIR>/Controller/Index.phpwithcontent,asfollows:
namespaceMagelicious\OrderNotes\Controller;
abstractclassIndexextends\Magento\Framework\App\Action\Action
{
}
Thisismerelyanemptybaseclass,foroursoon-to-followcontrolleraction.
Wethendefinethe<MODULE_DIR>/Controller/Index/Process.phpwithcontent,asfollows:
namespaceMagelicious\OrderNotes\Controller\Index;
classProcessextends\Magelicious\OrderNotes\Controller\Index
{
protected$checkoutSession;
protected$logger;
publicfunction__construct(
\Magento\Framework\App\Action\Context$context,
\Magento\Checkout\Model\Session$checkoutSession,
\Psr\Log\LoggerInterface$logger
)
{
$this->checkoutSession=$checkoutSession;
$this->logger=$logger;
parent::__construct($context);
}
publicfunctionexecute()
{
//implement...
}
}
ThiscontrolleractionshouldcatchanyHTTPordernotes/index/processrequests.Wethenextendtheexecutemethod,asfollows:
publicfunctionexecute()
{
$result=[];
try{
if($notes=$this->getRequest()->getParam('order_notes',null)){
$quote=$this->checkoutSession->getQuote();
$quote->setOrderNotes($notes);
$quote->save();
$result[$quote->getId()];
}
}catch(\Exception$e){
$this->logger->critical($e);
$result=[
'error'=>__('Somethingwentwrong.'),
'errorcode'=>$e->getCode(),
];
}
$resultJson=$this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_JSON);
$resultJson->setData($result);
return$resultJson;
}
Thisiswherewearestoringtheordernotesonourquoteobject.Lateron,wewillpullthisontooursalesorderobject.Wefurtherdefinethe<MODULE_DIR>/etc/frontend/di.xmlwithcontent,asfollows:
<config>
<typename="Magento\Checkout\Model\CompositeConfigProvider">
<arguments>
<argumentname="configProviders"xsi:type="array">
<itemname="order_notes_config_provider"xsi:type="object">
Magelicious\OrderNotes\Model\ConfigProvider
</item>
</argument>
</arguments>
</type>
</config>
Weareregisteringourconfigurationproviderhere.Theorder_notes_config_providermustbeunique.Wethendefinethe<MODULE_DIR>/Model/ConfigProvider.phpwithcontent,asfollows:
namespaceMagelicious\OrderNotes\Model;
classConfigProviderimplements\Magento\Checkout\Model\ConfigProviderInterface
{
publicfunctiongetConfig()
{
return[
'orderNotes'=>[
'title'=>__('OrderNotes'),
'header'=>__('Headercontent.'),
'footer'=>__('Footercontent.'),
'options'=>[
['code'=>'ring','value'=>__('Ringlonger')],
['code'=>'backyard','value'=>__('Trybackyard')],
['code'=>'neighbour','value'=>__('Pingneighbour')],
['code'=>'other','value'=>__('Other')],
]
]
];
}
}
Thisistheimplementationofourorder_notes_config_providerconfigurationprovider.Wecanprettymuchreturnanyarraystructurewewish.Thetop-levelorderNoteswillbeaccessiblelaterviaJScomponentsaswindow.checkoutConfig.orderNotes.Wefurtherdefinethe<MODULE_DIR>/view/frontend/layout/checkout_index_index.xmlwithcontent,asfollows:
<page>
<body>
<referenceBlockname="checkout.root">
<arguments>
<argumentname="jsLayout"xsi:type="array">
<itemname="components"xsi:type="array">
<itemname="checkout"xsi:type="array">
<itemname="children"xsi:type="array">
<itemname="steps"xsi:type="array">
<itemname="children"xsi:type="array">
<itemname="order-notes"xsi:type="array">
<itemname="component"xsi:type="string">
Magelicious_OrderNotes/js/view/order-notes
</item>
<itemname="sortOrder"xsi:type="string">2</item>
<!--closingtags-->
Thereisquiteanestingstructurehere.Ourordernotescomponentisbeinginjectedunderthechildrencomponentofthecheckout'sstepscomponent.
Wethendefinethe<MODULE_DIR>/view/frontend/web/js/view/order-notes.jswithcontent,asfollows:
define([
'ko',
'uiComponent',
'underscore',
'Magento_Checkout/js/model/step-navigator',
'jquery',
'mage/translate',
'mage/url'
],function(ko,Component,_,stepNavigator,$,$t,url){
'usestrict';
letcheckoutConfigOrderNotes=window.checkoutConfig.orderNotes;
returnComponent.extend({
defaults:{
template:'Magelicious_OrderNotes/order/notes'
},
isVisible:ko.observable(true),
initialize:function(){
//TODO
},
navigate:function(){
//TODO
},
navigateToNextStep:function(){
//TODO
}
});
});
ThisisouruiComponent,poweredbyKnockout.Thetemplateconfigurationpointstothephysicallocationofthe.htmlfilethatisusedasacomponent'stemplate.ThenavigateandnavigateToNextStepareresponsiblefornavigationbetweenthecheckoutstepsduringcheckout.Let'sextendtheinitializefunctionfurther,asfollows:
initialize:function(){
this._super();
stepNavigator.registerStep(
'order_notes',
null,
$t('OrderNotes'),
this.isVisible,
_.bind(this.navigate,this),
15
);
returnthis;
}
Weusetheinitializemethodtoregisterourorder_notesstepwiththestepNavigator.
Let'sextendthenavigateToNextStepfunctionfurther,asfollows:
navigateToNextStep:function(){
if($(arguments[0]).is('form')){
$.ajax({
type:'POST',
url:url.build('ordernotes/index/process'),
data:$(arguments[0]).serialize(),
showLoader:true,
complete:function(response){
stepNavigator.next();
}
});
}
}
WeusethenavigateToNextStepmethodtopersistourdata.TheAJAXPOSTordernotes/index/processactionshouldgrabtheentireformandpassitsdataalong.
Finally,let'saddthehelpermethodsforour.htmltemplate,asfollows:
getTitle:function(){
returncheckoutConfigOrderNotes.title;
},
getHeader:function(){
returncheckoutConfigOrderNotes.header;
},
getFooter:function(){
returncheckoutConfigOrderNotes.footer;
},
getNotesOptions:function(){
returncheckoutConfigOrderNotes.options;
},
getCheckoutConfigOrderNotesTime:function(){
returncheckoutConfigOrderNotes.time;
},
setOrderNotes:function(valObj,event){
if(valObj.code=='other'){
$('[name="order_notes"]').val('');
}else{
$('[name="order_notes"]').val(valObj.value);
}
returntrue;
},
Thesearejustsomeofthehelpermethodswewillbindtowithinour.htmltemplate.Theymerelypullthedataoutfromthewindow.checkoutConfig.orderNotesobject.
Wethendefinethe<MODULE_DIR>/view/frontend/web/template/order/notes.htmlwithcontent,asfollows:
<liid="order_notes"data-bind="fadeVisible:isVisible">
<divdata-bind="text:getTitle()"data-role="title"></div>
<divid="step-content"data-role="content">
<divdata-bind="text:getHeader()"data-role="header"></div>
<!--form-->
<divdata-bind="text:getFooter()"data-role="footer"></div>
</div>
</li>
Thisisourcomponenttemplate,whichgivesitavisualstructure.Weexpanditfurtherbyreplacingthe<!--form-->withthefollowing:
<formdata-bind="submit:navigateToNextStep"novalidate="novalidate">
<divdata-bind="foreach:getNotesOptions()"class="fieldchoice">
<inputtype="radio"name="order[notes]"class="radio"
data-bind="value:code,click:$parent.setOrderNotes"/>
<labeldata-bind="attr:{'for':code}"class="label">
<spandata-bind="text:value"></span>
</label>
</div>
<textareaname="order_notes"></textarea>
<divclass="actions-toolbar">
<divclass="primary">
<buttondata-role="opc-continue"type="submit"class="buttonactioncontinueprimary">
<span><!--koi18n:'Next'--><!--/ko--></span>
</button>
</div>
</div>
</form>
Theformitselfisrelativelysimple,thoughitrequiressomeknowledgeofKnockout.Understandingthedatabindingisquiteimportant.ItallowsustobindnotjusttextandtheHTMLvaluesofHTMLelements,butotherattributesaswell,suchastheclick.
Wethendefinethe<MODULE_DIR>/etc/webapi_rest/events.xmlwithcontent,asfollows:
<config>
<eventname="sales_model_service_quote_submit_before">
<observername="orderNotesToOrder"
instance="Magelicious\OrderNotes\Observer\SaveOrderNotesToOrder"
shared="false"/>
</event>
</config>
Thesales_model_service_quote_submit_beforeeventischosenbecauseitallowsustogainaccesstobothquoteandorderobjectseasilyattherighttimeintheordercreationprocess.
Wethendefinethe<MODULE_DIR>/Observer/SaveOrderNotesToOrder.phpwithcontent,asfollows:
namespaceMagelicious\OrderNotes\Observer;
classSaveOrderNotesToOrderimplements\Magento\Framework\Event\ObserverInterface
{
publicfunctionexecute(\Magento\Framework\Event\Observer$observer)
{
$event=$observer->getEvent();
if($notes=$event->getQuote()->getOrderNotes()){
$event->getOrder()
->setOrderNotes($notes)
->addStatusHistoryComment('Customernote:'.$notes);
}
return$this;
}
}
Here,wearegrabbingtheinstanceofanorderobjectandsettingtheordernotestothevaluefetchedfromtheordernotesvalueofapreviouslystoredquote.ThismakesthecustomernoteappearundertheCommentsHistorytaboftheMagentoadminorderViewscreen,asfollows:
Withthis,wehavefinalizedourlittlemodule.Eventhoughthemodule'sfunctionalityisquitesimple,thestepsforgettingitupandrunningweresomewhatinvolved.
SummaryInthischapter,wehavebuiltasmall,butfunctional,ordernotesmodule.Thisallowedustofamiliarizeourselveswithanimportantaspectofcustomizingthecheckoutexperience.Thegistofthisliesinunderstandingthecheckout_index_indexlayouthandle,theJavaScriptwindow.checkoutConfigobject,andtheuiComponent.
Failuretodeliverconsistentandstablecheckoutexperiencesisboundtoresultinalossofconversions.Giventhenumberandcomplexityofthecomponentsinvolved,itisbesttokeepthenumberofcheckoutcustomizationstoaminimum.
Movingforward,wearegoingtotakealookatsomeofthethingswecandoregardingthecustomizationofcustomerinteractions.
CustomizingCustomerInteractionsAlongwiththecatalogandcheckout,customer-relatedfunctionalityiscentraltoMagento.Thecustomer'sMyAccountareaallowscontroloveraddresses,orders,billingagreements,productwishlists,productreviews,newslettersubscriptions,andmore.CustomizingcustomerfunctionalityoftenincludeschangestotheSignInandCreateanAccountprocesses,aswellasmodifyingexisting,oraddingnewfunctionalityundertheMyAccountarea.
Dependingonthedynamicsandintricacyofourfunctionality,JScomponentsareoftenfriendliersolutionsthanserver-sidePHTMLtemplates.Theyallowustoengagethecustomerwithoutnecessarilyreloadingentirepages,thusimprovingtheoverallcustomerexperience.Aswithanyclienttoserver-sidecommunication,thequestionofpassingandupdatingthedataremainstobeaddressed.ThisiswhereweturnourfocustoMagento'ssectionmechanism.
Movingforwardwearegoingtotakealookatthefollowing:
UnderstandingthesectionmechanismAddingcontactpreferencestocustomeraccountsAddingcontactpreferencestothecheckout
TechnicalrequirementsYouwillneedtohavebasicknowledgeofPHP,OOP,JavaScript,andXML.YouwillalsoneedApache,MySQL,andAMPPSinstalledonyoursystemtoexecutethecodes.
ThecodefilesofthischaptercanbefoundonGitHub:https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
CheckoutthefollowingvideotoseetheCodeinAction:
http://bit.ly/2NQFB1f.
UnderstandingthesectionmechanismOurpreviouschaptertoucheduponconfigprovidersandthewindow.checkoutConfigobject;amechanismbywhichwecanpushourserver-sidedatatoourstorefrontwhenapageloads.ThesectionmechanismallowsustopushdatatoabrowserpageuponanynamedHTTPPOSTrequest.
Let'stakeaquicklookatthe<MAGENTO_DIR>/module-review/etc/frontend/sections.xmlfile:
<actionname="review/product/post">
<sectionname="review"/>
</action>
Thedefinitionprovidedhereistobeinterpretedas:"anystorefrontHTTPPOSTreview/product/postrequestistotriggerareviewsectionload,"wherereviewsectionloadmeansMagentotriggeringanadditionalAJAXrequestfollowingthecompletionofanobservedHTTPPOST.Theresultofthissectionloadaction,inthiscase,istherefreshofsectiondata,retrievableviacustomerData.get('review'),aswewillsoonsee.
Nowlet'stakealookatthe<MAGENTO_DIR>/module-review/etc/frontend/di.xmlfile:
<typename="Magento\Customer\CustomerData\SectionPoolInterface">
<arguments>
<argumentname="sectionSourceMap"xsi:type="array">
<itemname="review"xsi:type="string">Magento\Review\CustomerData\Review</item>
</argument>
</arguments>
</type>
WeareessentiallyinjectingnewitemsunderthesectionSourceMapargumentoftheMagento\Customer\CustomerData\SectionPoolInterfacetype.Theimplementationofacustomsection,suchastheMagento\Review\CustomerData\Review,mustimplementtheMagento\Customer\CustomerData\SectionSourceInterface.TheunderlyinggetSectionDatamethodreturnsanarrayofkey-valuemappings,suchas:
return[
'nickname'=>'',
'title'=>'',
'detail'=>''
]
These,inturn,becomeavailabletotheuiComponent,asobservedinthepartial<MAGENTO_DIR>/module-review/view/frontend/web/js/view/review.jsfile:
define([
'uiComponent',
'Magento_Customer/js/customer-data',
'Magento_Customer/js/view/customer'
],function(Component,customerData){
'usestrict';
returnComponent.extend({
initialize:function(){
this.review=customerData.get('review')...
},
nickname:function(){
returnthis.review().nickname...
}
});
});
ThegetmethodofthecustomerDataobjectcanbeusedtofetchthesectionSourceMapdata,suchascustomerData.get('review').ThisdataisrefreshedeverytimeanHTTPPOSTismadetothereview/product/postroute.ThisisbecausefollowinganyHTTPPOSTreview/product/post,MagentowilltriggeranHTTPGETcustomer/section/load/?sections=review&update_section_id=true&_=1533836467415,whichinturnupdatescustomerDataaccordingly.
AddingcontactpreferencestocustomeraccountsNowthatweunderstandthemechanismbehindthecustomerDataobjectandthesectionload,let'sputittousebycreatingasmallmodulethataddscontactpreferencesfunctionalityunderthecustomer'sMyAccountarea,aswellasunderthecheckout.OurworkistobedoneaspartoftheMagelicious_ContactPreferencesmodule,withthefinalvisualoutcomeasfollows:
Bycontrast,thecustomer'scheckoutareawouldshowcontactpreferences,asfollows:
Theideabehindthemoduleistoprovideacustomerwithanoptionofchoosingpreferredcontactpreferences,sothatamerchantmayfollowupwiththedeliveryprocessaccordingly.
Assumingwehavedefinedregistration.php,composer.json,andetc/module.xmlasbasicmodulefiles,wecandealwiththemorespecificdetailsofourMagelicious_ContactPreferencesmodule.
Westartbydefiningthe<MODULE_DIR>/Setup/InstallData.php,asfollows:
$customerSetup=$this->customerSetupFactory->create(['setup'=>$setup]);
$customerSetup->addAttribute(
\Magento\Customer\Model\Customer::ENTITY,
'contact_preferences',
[
'type'=>'varchar',
'label'=>'ContactPreferences',
'input'=>'multiselect',
'source'=>\Magelicious\ContactPreferences\Model\Entity\Attribute\Source\Contact\Preferences::class,
'required'=>0,
'sort_order'=>99,
'position'=>99,
'system'=>0,
'visible'=>1,
'global'=>\Magento\Catalog\Model\ResourceModel\Eav\Attribute::SCOPE_GLOBAL,
]
);
$contactPreferencesAttr=$customerSetup
->getEavConfig()
->getAttribute(
\Magento\Customer\Model\Customer::ENTITY,
'contact_preferences'
);
$contactPreferencesAttr->setData('used_in_forms',['adminhtml_customer']);
$contactPreferencesAttr->save();
WeareinstructingMagentotocreateamultiselecttypeofattribute.TheattributebecomesvisibleundertheMagentoadminarea,withacustomereditingscreenasfollows:
Wethendefinethe<MODULE_DIR>/Model/Entity/Attribute/Source/Contact/Preferences.php,asfollows:
namespaceMagelicious\ContactPreferences\Model\Entity\Attribute\Source\Contact;
classPreferencesextends\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource
{
constVALUE_EMAIL='email';
constVALUE_PHONE='phone';
constVALUE_POST='post';
constVALUE_SMS='sms';
publicfunctiongetAllOptions()
{
return[
['label'=>__('Email'),'value'=>self::VALUE_EMAIL],
['label'=>__('Phone'),'value'=>self::VALUE_PHONE],
['label'=>__('Post'),'value'=>self::VALUE_POST],
['label'=>__('SMS'),'value'=>self::VALUE_SMS],
];
}
}
Thesearethecontactpreferenceoptionswewanttoprovideasourattributesource.Wewillusethisclassnotjustforinstallation,butlateronaswell.
Wethendefinethe<MODULE_DIR>/etc/frontend/routes.xml,asfollows:
<config>
<routerid="standard">
<routeid="customer"frontName="customer">
<modulename="Magelicious_ContactPreferences"before="Magento_Customer"/>
</route>
</router>
</config>
Unlikeourroutedefinitionsinpreviouschapters,hereweareusinganalreadyexistingroutenamecustomer.TheattributebeforeitallowsustoinsertourmodulebeforetheMagento_Customermodule,allowingustorespondtothesamecustomer/*routes.Weshouldbeverycarefulwiththisapproach,nottodetachsomeoftheexistingcontrolleractions.Inourcase,weareonlydoingthissothatwemightusethecustomer/contact/preferencesURLlateron.
Wethendefinethe<MODULE_DIR>/Controller/Contact/Preferences.php,asfollows:
namespaceMagelicious\ContactPreferences\Controller\Contact;
classPreferencesextends\Magento\Customer\Controller\AbstractAccount
{
publicfunctionexecute()
{
if($this->getRequest()->isPost()){
$resultJson=$this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_JSON);
if($this->getRequest()->getParam('load')){
//Merelyfortriggering"contact_preferences"section
}else{
//SAVEPREFERENCES
}
return$resultJson;
}else{
$resultPage=$this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE);
$resultPage->getConfig()->getTitle()->set(__('MyContactPreferences'));
return$resultPage;
}
}
}
Thisistheonlycontrolleractionwewillhave.Wewillusethesameactionforhandlingthreedifferentintents.Thisisnotanidealexampleofhowoneshouldwritecodeinthisscenario,butitisacompactone.Thefirstintentwewillhandleisthesectionloadtrigger,thesecondistheactualpreferencesave,and
thethirdisthepageload.Thesewillbecomeclearaswemoveforward.
WethenreplacetheSAVEPREFERENCEScommentwiththefollowing:
//\Magento\Framework\App\Action\Context$context
//\Magento\Customer\Model\Session$customerSession
//\Magento\Customer\Api\CustomerRepositoryInterface$customerRepository
//\Psr\Log\LoggerInterface$logger
try{
$preferences=implode(',',
array_keys(
array_filter($this->getRequest()->getParams(),function($_checked,$_preference){
returnfilter_var($_checked,FILTER_VALIDATE_BOOLEAN);
},ARRAY_FILTER_USE_BOTH)
)
);
$customer=$this->customerRepository->getById($this->customerSession->getCustomerId());
$customer->setCustomAttribute('contact_preferences',$preferences);
$this->customerRepository->save($customer);
$this->messageManager->addSuccessMessage(__('Successfullysavedcontactpreferences.'));
}catch(\Exception$e){
$this->logger->critical($e);
$this->messageManager->addErrorMessage(__('Errorsavingcontactpreferences.'));
}
Herewearehandlingtheactualsavingofthechosencontactpreferences.Therequestparametersareexpectedtobeinthe<preference_name>=<true|false>format.Weusetheimplodetoturntheincomingrequestandpassitontotherepository'ssetCustomAttributemethod.Thisisbecause,bydefault,Magentostoresthemultiselectattributeasacomma-separatedstringinthedatabase.TheaddSuccessMessageandaddErrorMessagecallsareinterestinghere.OnemightexpectthatwewouldreturnthesemessagesaspartofaJSONresponse.But,wedon'treallyneedaJSONresponsebodyhere.ThisisbecauseMagentohasthemessagessectiondefinedunder<MAGENTO_DIR>/module-theme/etc/frontend/sections.xmlas<actionname="*">.Whatthismeansisthatmessagesgetrefresheduponeverysectionloadand,sinceourcontrolleractionismappedinourownsections.xml,theloadofoursectionwillalsoloadmessages.
Wethendefinethe<MODULE_DIR>/view/frontend/layout/customer_account.xml,asfollows:
<page>
<body>
<referenceBlockname="customer_account_navigation">
<blockclass="Magento\Customer\Block\Account\SortLinkInterface"name="customer-account-navigation-contact-preferences-link">
<arguments>
<argumentname="path"xsi:type="string">customer/contact/preferences</argument>
<argumentname="label"xsi:type="string"translate="true">MyContactPreferences</argument>
<argumentname="sortOrder"xsi:type="number">230</argument>
</arguments>
</block>
</referenceBlock>
</body>
</page>
Thedefinitionshereinjectanewmenuitemunderthecustomer'sMyAccountscreen.Thecustomer_account_navigationblock,originallydefinedunder<MAGENTO_DIR>/module-customer/view/frontend/layout/customer_account.xml,isinchargeofrenderingthesidebarmenu.ByinjectingthenewblockofMagento\Customer\Block\Account\SortLinkInterfacetype,wecaneasilyaddnewmenuitems.
Wethendefinethe<MODULE_DIR>/view/frontend/layout/customer_contact_preferences.xml,asfollows:
<page>
<updatehandle="customer_account"/>
<body>
<referenceContainername="content">
<blockname="contact_preferences"
template="Magelicious_ContactPreferences::customer/contact/preferences.phtml"cacheable="false"/>
</referenceContainer>
</body>
</page>
Thisistheblockthatwillgetloadedintothecontentareaofapage,onceweclickonournewlyaddedMyContactPreferenceslink.Sincetheonlyroleofthecontact_preferencesblockwillbetoloadtheJScomponent,weomittheclassdefinitionthatwewouldnormallyhaveoncustomblocks.
Wethendefinethe<MODULE_DIR>/view/frontend/templates/customer/contact/preferences.phtml,asfollows:
<divclass="contact-preferences"data-bind="scope:'contact-preferences-scope'">
<!--kotemplate:getTemplate()--><!--/ko-->
</div>
<scripttype="text/x-magento-init">
{
".contact-preferences":{
"Magento_Ui/js/core/app":{
"components":{
"contact-preferences-scope":{
"component":"contactPreferences"
}
}
}
}
}
</script>
TheonlypurposeofthetemplatehereistoloadtheJScontactPreferencescomponent.Wecanseethatnodataispassedfromtheserver-side.phtmltemplatetotheJScomponent.WewillusethesectionandcustomerDatamechanismslateronforthat.
Wethendefinethe<MODULE_DIR>/view/frontend/requirejs-config.js,asfollows:
varconfig={
map:{
'*':{
contactPreferences:'Magelicious_ContactPreferences/js/view/contact-preferences'
}
}
};
Herewemapthecomponentname,contactPreferences,toitsphysicallocationinourmoduledirectory.
Wethendefinethe<MODULE_DIR>/view/frontend/web/js/view/contact-preferences.js,asfollows:
define([
'uiComponent',
'jquery',
'mage/url',
'Magento_Customer/js/customer-data'
],function(Component,$,url,customerData){
'usestrict';
letcontactPreferences=customerData.get('contact_preferences');
returnComponent.extend({
defaults:{
template:'Magelicious_ContactPreferences/contact-preferences'
},
initialize:function(){/*...*/},
isCustomerLoggedIn:function(){
returncontactPreferences().isCustomerLoggedIn;
},
getSelectOptions:function(){
returncontactPreferences().selectOptions;
},
saveContactPreferences:function(){/*...*/}
});
});
ThisisourJScomponent,thecoreofourclient-sidefunctionality.WeinjecttheMagento_Customer/js/customer-datacomponentasacustomerDataobject.ThisgivesusaccesstodatawearepushingfromtheserversideviathegetSectionDatamethodoftheMagelicious\ContactPreferences\CustomerData\Preferencesclass.Thestringvaluecontact_preferencespassedtothegetmethodofthecustomerDataobjectmustmatchtheitemnameunderthesectionSourceMapofourdi.xmldefinition.
Let'sextendtheinitializefunctionfurther,asfollows:
initialize:function(){
this._super();
$.ajax({
type:'POST',
url:url.build('customer/contact/preferences'),
data:{'load':true},
showLoader:true
});
}
TheadditionofanAJAXrequestcallwithinthecomponent'sinitializemethodismoreofatricktotriggerthecontact_preferencessectionloadinourcase.WearedoingitsimplybecausesectionsdonotloadonHTTPGETrequests,asthatmightloadthesamecustomer/contact/preferencespage.Rather,theyloadonHTTPPOSTevents.Thiswayweensurethatthecontact_preferencessectionwillloadwhenourcomponentisinitialized,thusprovidingitwiththenecessarydata.WearefarfromsayingthatthisisarecommendedapproachforgeneralJScomponentdevelopment,though.
Let'sextendthesaveContactPreferencesfunctionfurther,asfollows:
saveContactPreferences:function(){
letpreferences={};
$('.contact_preference').children(':checkbox').each(function(){
preferences[$(this).attr('name')]=$(this).attr('checked')?true:false;
});
$.ajax({
type:'POST',
url:url.build('customer/contact/preferences'),
data:preferences,
showLoader:true,
complete:function(response){
//someactions...
}
});
returntrue;
}
ThesaveContactPreferencesmethodwillbetriggeredeverytimeacustomerclicksonthecontactpreferenceonthestorefront,whetheritisanactofcheckingoruncheckingindividualcontactpreferences.
Wethendefinethe<MODULE_DIR>/view/frontend/web/template/contact-preferences.html,asfollows:
<divdata-bind="if:isCustomerLoggedIn()">
<divdata-role="title"data-bind="i18n:'ContactPreferences'"></div>
<divdata-role="content">
<divclass="contact_preference"repeat="foreach:getSelectOptions(),item:'$option'">
<inputtype="checkbox"
click="saveContactPreferences"
ko-checked="$option().checked"
attr="name:$option().value"/>
<labeltext="$option().label"attr="for:$option().value"/>
</div>
</div>
</div>
TheHTMLdefinedherevisuallysetsourcomponent.AbasicknowledgeofKnockoutJSisrequiredinordertoutilizetherepeatdirective,fedwiththearrayofdatacomingfromthegetSelectOptionsmethod,whichbynowweknoworiginatesfromtheserverside.
Wethendefinethe<MODULE_DIR>/etc/frontend/sections.xml,asfollows:
<config>
<actionname="customer/contact/preferences">
<sectionname="contact_preferences"/>
</action>
</config>
Withthis,wemakethenecessarymappingbetweenHTTPPOSTcustomer/contact/preferencesrequestsandthecontact_preferencessectionweexpecttoload.
Wethendefinethe<MODULE_DIR>/etc/frontend/di.xml,asfollows:
<config>
<typename="Magento\Customer\CustomerData\SectionPoolInterface">
<arguments>
<argumentname="sectionSourceMap"xsi:type="array">
<itemname="contact_preferences"xsi:type="string">Magelicious\ContactPreferences\CustomerData\Preferences</item>
</argument>
</arguments>
</type>
</config>
Hereweinjectourcontact_preferencessection,instructingMagentowheretoreaditsdatafrom.Withthisinplace,anyHTTPPOSTcustomer/contact/preferencesrequestisexpectedtotriggerafollow-upAJAXPOSTcustomer/section/load/?sections=contact_preferences%2Cmessages&update_section_id=true&_=1533887023603requestthat,inturn,returnsdatamuchlikethefollowing:
{
"contact_preferences":{
"selectOptions":[
{
"label":"Email",
"value":"email",
"checked":true
},
{...}
],
"isCustomerLoggedIn":true,
"data_id":1533875246
},
"messages":{
"messages":[
{
"type":"success",
"text":"Successfullysavedcontactpreferences."
}
],
"data_id":1533875246
}
}
Ifweweretoenableourmoduleatthispoint,weshouldbeabletoseeitworkingunderthecustomer'sMyAccountscreen.Thoughsimple,thestepsofgettingeverythinglinkedweresomewhatinvolved.Thebenefitofthisapproach,wheredataissentviathesectionsmechanism,isthatourcomponentplaysnicelywithfull-pagecaching.Theneededcustomer-relateddataissimplyfetchedbyadditionalAJAXcalls,insteadofcachingitonaper-customerbasis,andthusthisbypassesthepurposeoffull-pagecaching.
AddingcontactpreferencestothecheckoutWithourcomponentnowworkingonthecustomer'sMyAccountpage,let'sgoaheadandaddittothecheckout'sReview&Paymentsstepaswell.
Bytappingintothecheckout_index_indexlayouthandle,andnestingourcomponentunderthedesiredchildrenelement,wecaneasilyaddittothecheckoutpage.Wedosowiththe<MODULE_DIR>/view/frontend/layout/checkout_index_index.xmlfile,asfollows:
<page>
<body>
<referenceBlockname="checkout.root">
<arguments>
<argumentname="jsLayout"xsi:type="array">
<itemname="components"xsi:type="array">
<itemname="checkout"xsi:type="array">
<itemname="children"xsi:type="array">
<itemname="steps"xsi:type="array">
<itemname="children"xsi:type="array">
<itemname="billing-step"xsi:type="array">
<itemname="children"xsi:type="array">
<itemname="payment"xsi:type="array">
<itemname="children"xsi:type="array">
<itemname="afterMethods"xsi:type="array">
<itemname="children"xsi:type="array">
<itemname="contact-preferences"xsi:type="array">
<itemname="component"xsi:type="string">Magelicious_ContactPreferences/js/view/contact-preferences</item>
<!--closingtags-->
Thenestingstructureofcheckout_index_index.xmlisquiterobust.Thereareseveralplaceswherewecanactuallyinsertourowncomponent.Mostofthetime,thismightbetrialanderror.Inthiscase,weoptedforthechildrenareaofafterMethods.Thisshouldpositionitunderthecheckout'sReview&Paymentsstep,rightafterthepaymentsmethodlist.
SummaryInthischapter,wehavebuiltasmallmodulethatallowedustogetagreaterinsightintoMagento'scustomerDataandsectionsmechanisms.Wemanagedtobuildasinglecomponent,thatgotusedbothonthecustomer'sMyAccountpage,aswellasonthecheckout.
Withthis,wehavereachedtheendofourbook.ThetopicswehavecoveredshouldbeenoughtogetusgoingwithMagentodevelopment,butthesheersizeoftheplatformandtheintricatespecificsofitsindividualmodulesleaveplentymoretoexplorefurtheron.Itgoeswithoutsayingthatourjourneyhasmerelybegun.
OtherBooksYouMayEnjoyIfyouenjoyedthisbook,youmaybeinterestedintheseotherbooksbyPackt:
Magento2BeginnersGuideGabrielGuarino
ISBN:9781785880766
BuildyourfirstwebstoreinMagento2MigrateyourdevelopmentenvironmenttoalivestoreConfigureyourMagento2webstoretherightway,sothatyourtaxesarehandledproperlyCreatepageswitharbitrarycontentCreateandmanagecustomercontactsandaccountsProtectMagentoinstanceadminfromunexpectedintrusionsSetupnewsletterandtransactionalemailssothatcommunicationfromyourwebsitecorrespondstothewebsite'slookandfeelMakethestorelookgoodintermsofPCIcompliance
Magento2Developer'sGuide
BrankoAjzele
ISBN:9781785886584
SetupthedevelopmentandproductionenvironmentofMagento2UnderstandthenewmajorconceptsandconventionsusedinMagento2Buildaminiatureyetfully-functionalmodulefromscratchtomanageyoure-commerceplatformefficientlyWritemodelsandcollectionstomanageandsearchyourentitydataDiveintobackenddevelopmentsuchascreatingevents,observers,cronjobs,logging,profiling,andmessagingfeaturesGettothecoreoffrontenddevelopmentsuchasblocks,templates,layouts,andthethemesofMagento2Usetoken,session,andOauthtoken-basedauthenticationviavariousflavorsofAPIcalls,aswellascreatingyourownAPIsGettogripswithtestingMagentomodulesandcustomMagentothemes,whichformsanintegralpartofdevelopment
Leaveareview-letotherreadersknowwhatyouthinkPleaseshareyourthoughtsonthisbookwithothersbyleavingareviewonthesitethatyouboughtitfrom.IfyoupurchasedthebookfromAmazon,pleaseleaveusanhonestreviewonthisbook'sAmazonpage.Thisisvitalsothatotherpotentialreaderscanseeanduseyourunbiasedopiniontomakepurchasingdecisions,wecanunderstandwhatourcustomersthinkaboutourproducts,andourauthorscanseeyourfeedbackonthetitlethattheyhaveworkedwithPackttocreate.Itwillonlytakeafewminutesofyourtime,butisvaluabletootherpotentialcustomers,ourauthors,andPackt.Thankyou!