magento 2 development quick start guide · magento certified solution specialist, magento 2...

Post on 31-Jul-2020

35 Views

Category:

Documents

7 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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&amp;searchCriteria[filter_groups][0][filters][0][value]=%Block%&amp;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!

top related