Magento 2 Developer's Guide Branko Ajzele December 2015
Magento%202%20Developer's%20Guide
Magento%202%20Developer_s%20Guide%20
Magento%202%20Developer's%20Guide
Magento-2-Developers-Guide-Branko-Ajzele-December-2015
User Manual: Pdf
Open the PDF directly: View PDF .
Page Count: 529 [warning: Documents this large are best viewed by clicking the View PDF Link!]
- Magento 2 Developer's Guide
- Credits
- About the Author
- About the Reviewer
- www.PacktPub.com
- Support files, eBooks, discount offers, and more
- Why subscribe?
- Free access for Packt account holders
- Preface
- What this book covers
- What you need for this book
- Who this book is for
- Conventions
- Reader feedback
- Customer support
- Downloading the example code
- Errata
- Piracy
- Questions
- 1. Understanding the Platform Architecture
- The technology stack
- The architectural layers
- The top-level filesystem structure
- The module filesystem structure
- Summary
- 2. Managing the Environment
- Setting up a development environment
- VirtualBox
- Vagrant
- Vagrant project
- Provisioning PHP
- Provisioning MySQL
- Provisioning Apache
- Provisioning Magento installation
- Setting up a production environment
- Introduction to Amazon Web Services
- Setting up access for S3 usage
- Creating IAM users
- Creating IAM groups
- Setting up S3 for database and media files backup
- Bash script for automated EC2 setup
- Setting up EC2
- Setting up Elastic IP and DNS
- Summary
- 3. Programming Concepts and Conventions
- Composer
- Service contracts
- Code generation
- The var directory
- Coding standards
- Summary
- 4. Models and Collections
- Creating a miniature module
- Creating a simple model
- Creating an EAV model
- Understanding the flow of schema and data scripts
- Creating an install schema script (InstallSchema.php)
- Creating an upgrade schema script (UpgradeSchema.php)
- Creating an install data script (InstallData.php)
- Creating an upgrade data script (UpgradeData.php)
- Entity CRUD actions
- Creating new entities
- Reading existing entities
- Updating existing entities
- Deleting existing entities
- Managing collections
- Collection filters
- Summary
- 5. Using the Dependency Injection
- The object manager
- Dependency injection
- Configuring class preferences
- Using virtual types
- Summary
- 6. Plugins
- Creating a plugin
- Using the before listener
- Using the after listener
- Using the around listener
- The plugin sort order
- Summary
- 7. Backend Development
- Cron jobs
- Notification messages
- Session and cookies
- Logging
- The profiler
- Events and observers
- Cache(s)
- Widgets
- Custom variables
- i18n
- Indexer(s)
- Summary
- 8. Frontend Development
- Rendering flow
- View elements
- Ui components
- Containers
- Blocks
- Block architecture and life cycle
- Templates
- Layouts
- Themes
- Creating a new theme
- JavaScript
- Creating a custom JS component
- CSS
- Summary
- 9. The Web API
- User types
- Authentication methods
- REST versus SOAP
- Hands-on with token-based authentication
- Hands-on with OAuth-based authentication
- OAuth-based Web API calls
- Hands-on with session-based authentication
- Creating custom Web APIs
- API call examples
- The getById service method call examples
- The getList service method call examples
- The save (as new) service method call examples
- The save (as update) service method call examples
- The deleteById service method call examples
- Search Criteria Interface for list filtering
- Summary
- 10. The Major Functional Areas
- CMS management
- Managing blocks manually
- Managing blocks via code
- Managing blocks via API
- Managing pages manually
- Managing pages via code
- Managing pages via API
- Catalog management
- Managing categories manually
- Managing categories via code
- Managing categories via API
- Managing products manually
- Managing products via code
- Managing products via API
- Customer management
- Managing customers manually
- Managing customers via code
- Managing customers via an API
- Managing customer address via code
- Managing customers address via an API
- Products and customers import
- The custom product types
- Custom offline shipping methods
- Custom offline payment methods
- Summary
- 11. Testing
- Types of tests
- Unit testing
- Integration testing
- Static testing
- Integrity testing
- Legacy testing
- Performance testing
- Functional testing
- Writing a simple unit test
- Summary
- 12. Building a Module from Scratch
- Module requirements
- Registering a module
- Creating a configuration file (config.xml)
- Creating e-mail templates (email_templates.xml)
- Creating a system configuration file (system.xml)
- Creating access control lists (acl.xml)
- Creating an installation script (InstallSchema.php)
- Managing entity persistence (model, resource, collection)
- Building a frontend interface
- Creating routes, controllers, and layout handles
- Creating blocks and templates
- Handling form submissions
- Building a backend interface
- Linking the access control list and menu
- Creating routes, controllers, and layout handles
- Utilizing the grid widget
- Creating a grid column renderer
- Creating grid column options
- Creating controller actions
- Creating unit tests
- Summary
- Index
Magento2Developer’sGuide
TableofContents
Magento2Developer’sGuide
Credits
AbouttheAuthor
AbouttheReviewer
www.PacktPub.com
Supportfiles,eBooks,discountoffers,andmore
Whysubscribe?
FreeaccessforPacktaccountholders
Preface
Whatthisbookcovers
Whatyouneedforthisbook
Whothisbookisfor
Conventions
Readerfeedback
Customersupport
Downloadingtheexamplecode
Errata
Piracy
Questions
1.UnderstandingthePlatformArchitecture
Thetechnologystack
Thearchitecturallayers
Thetop-levelfilesystemstructure
Themodulefilesystemstructure
Summary
2.ManagingtheEnvironment
Settingupadevelopmentenvironment
VirtualBox
Vagrant
Vagrantproject
ProvisioningPHP
ProvisioningMySQL
ProvisioningApache
ProvisioningMagentoinstallation
Settingupaproductionenvironment
IntroductiontoAmazonWebServices
SettingupaccessforS3usage
CreatingIAMusers
CreatingIAMgroups
SettingupS3fordatabaseandmediafilesbackup
BashscriptforautomatedEC2setup
SettingupEC2
SettingupElasticIPandDNS
Summary
3.ProgrammingConceptsandConventions
Composer
Servicecontracts
Codegeneration
Thevardirectory
Codingstandards
Summary
4.ModelsandCollections
Creatingaminiaturemodule
Creatingasimplemodel
CreatinganEAVmodel
Understandingtheflowofschemaanddatascripts
Creatinganinstallschemascript(InstallSchema.php)
Creatinganupgradeschemascript(UpgradeSchema.php)
Creatinganinstalldatascript(InstallData.php)
Creatinganupgradedatascript(UpgradeData.php)
EntityCRUDactions
Creatingnewentities
Readingexistingentities
Updatingexistingentities
Deletingexistingentities
Managingcollections
Collectionfilters
Summary
5.UsingtheDependencyInjection
Theobjectmanager
Dependencyinjection
Configuringclasspreferences
Usingvirtualtypes
Summary
6.Plugins
Creatingaplugin
Usingthebeforelistener
Usingtheafterlistener
Usingthearoundlistener
Thepluginsortorder
Summary
7.BackendDevelopment
Cronjobs
Notificationmessages
Sessionandcookies
Logging
Theprofiler
Eventsandobservers
Cache(s)
Widgets
Customvariables
i18n
Indexer(s)
Summary
8.FrontendDevelopment
Renderingflow
Viewelements
Uicomponents
Containers
Blocks
Blockarchitectureandlifecycle
Templates
Layouts
Themes
Creatinganewtheme
JavaScript
CreatingacustomJScomponent
CSS
Summary
9.TheWebAPI
Usertypes
Authenticationmethods
RESTversusSOAP
Hands-onwithtoken-basedauthentication
Hands-onwithOAuth-basedauthentication
OAuth-basedWebAPIcalls
Hands-onwithsession-basedauthentication
CreatingcustomWebAPIs
APIcallexamples
ThegetByIdservicemethodcallexamples
ThegetListservicemethodcallexamples
Thesave(asnew)servicemethodcallexamples
Thesave(asupdate)servicemethodcallexamples
ThedeleteByIdservicemethodcallexamples
SearchCriteriaInterfaceforlistfiltering
Summary
10.TheMajorFunctionalAreas
CMSmanagement
Managingblocksmanually
Managingblocksviacode
ManagingblocksviaAPI
Managingpagesmanually
Managingpagesviacode
ManagingpagesviaAPI
Catalogmanagement
Managingcategoriesmanually
Managingcategoriesviacode
ManagingcategoriesviaAPI
Managingproductsmanually
Managingproductsviacode
ManagingproductsviaAPI
Customermanagement
Managingcustomersmanually
Managingcustomersviacode
ManagingcustomersviaanAPI
Managingcustomeraddressviacode
ManagingcustomersaddressviaanAPI
Productsandcustomersimport
Thecustomproducttypes
Customofflineshippingmethods
Customofflinepaymentmethods
Summary
11.Testing
Typesoftests
Unittesting
Integrationtesting
Statictesting
Integritytesting
Legacytesting
Performancetesting
Functionaltesting
Writingasimpleunittest
Summary
12.BuildingaModulefromScratch
Modulerequirements
Registeringamodule
Creatingaconfigurationfile(config.xml)
Creatinge-mailtemplates(email_templates.xml)
Creatingasystemconfigurationfile(system.xml)
Creatingaccesscontrollists(acl.xml)
Creatinganinstallationscript(InstallSchema.php)
Managingentitypersistence(model,resource,collection)
Buildingafrontendinterface
Creatingroutes,controllers,andlayouthandles
Creatingblocksandtemplates
Handlingformsubmissions
Buildingabackendinterface
Linkingtheaccesscontrollistandmenu
Creatingroutes,controllers,andlayouthandles
Utilizingthegridwidget
Creatingagridcolumnrenderer
Creatinggridcolumnoptions
Creatingcontrolleractions
Creatingunittests
Magento2Developer’sGuide
Magento2Developer’sGuide
Copyright©2015PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,
ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthe
publisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyofthe
informationpresented.However,theinformationcontainedinthisbookissoldwithout
warranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,andits
dealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecaused
directlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthe
companiesandproductsmentionedinthisbookbytheappropriateuseofcapitals.
However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:December2015
Productionreference:1171215
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78588-658-4
www.packtpub.com
Credits
Author
BrankoAjzele
Reviewer
MitchellRobles,Jr
CommissioningEditor
NeilAlexander
AcquisitionEditor
VinayArgekar
ContentDevelopmentEditor
PreetiSingh
TechnicalEditor
GauravSuri
CopyEditors
VedangiNarvekar
JonathanTodd
ProjectCoordinator
ShwetaH.Birwatkar
Proofreader
SafisEditing
Indexer
PriyaSane
ProductionCoordinator
ShantanuN.Zagade
CoverWork
ShantanuN.Zagade
AbouttheAuthor
BrankoAjzeleisahusband,fatheroftwo,son,brother,author,andasoftwaredeveloper.
Hehasadegreeinelectricalengineering.Aloverofallthingsdigital,hemakesaliving
fromsoftwaredevelopment.Hehopestofindenoughqualitytimesomedaytoseriously
diveintohobbyelectronics;hehashiseyeonArduinoandRaspberryPi.
Hehasyearsofhands-onexperiencewithfull-timesoftwaredevelopmentandteam
management,andhasspecializingine-commerceplatforms.Hehasbeenworkingwith
Magentosince2008;hehasbeenknee-deepinitsinceitsveryfirstbetaversion.Brankois
regularlyintouchwitheverythingrelatedtoPHP,databases(MySQL/MongoDB),
search/analytics(Solr/Elasticsearch),Node.js,andrelatedtechnologies.
Hehasastrongtechnicalknowledgewithanabilitytocommunicatethosetechnicalities
frequentlyandclearlywithastrongdirection.Hefeelscomfortableproposingalternatives
todemandswhichhefeelscanbeimproved,evenwhenthismeanspullingalateshiftto
meetthedeadlines.
HeholdsseveralrespectedITcertifications,suchasZendCertifiedEngineer(ZCEPHP),
MagentoCertifiedDeveloper(MCD),MagentoCertifiedDeveloperPlus(MCD+),
MagentoCertifiedSolutionSpecialist(MCSS),andJavaScriptCertifiedDeveloper.
InstantE-CommercewithMagento:BuildaShop,PacktPublishing,washisfirst
Magento-relatedbookthatwasorientedtowardsMagentonewcomers.Afterwritingthis
book,hewroteGettingStartedwithMagentoExtensionDevelopmentfordevelopers.
Currently,heworksasafull-timecontractorforLabLateralLtd,anaward-winningteam
ofinnovativethinkers,artists,anddeveloperswhospecializeincustomer-centricwebsites,
digitalconsultancy,andmarketing.HeistheLeadMagentoDeveloperandHeadofLab’s
Croatiaoffice.
HewasawardedtheE-CommerceDeveloperoftheYearbyDigitalEntrepreneurAwards
inOctober2014forhisexcellentknowledgeandexpertiseine-commercedevelopment.
Hisworkissecondtonone.HeistrulydedicatedtohelpingtheLabLateralLtdteamand
hisfellowdevelopersacrosstheworld.
AbouttheReviewer
MitchellRobles,Jr,isasolutionsarchitectandapplicationsengineerwhohasworkedin
variousleadrolesforseveralaward-winningdigitalagenciesinSanDiego,CA,USA.
Throughhisownentrepreneurialspirit,hefoundedMojoCreative&TechnicalSolutions
(formoreinformation,visithttp://www.mojomage.com/),whichspecializesinday-to-day
Magentosupportanddevelopmentformerchants,agencies,freelancers,andindustry
partners.AsacertifiedMagentodeveloper,Mitchellisthebrainchildandleadin
developingseveralmust-haveMagentoextensions,includingMojoCreative&Technical
Solutions’BundledMojo,apopular,full-featuredMagentoextensionthatgives
administratorstotalcontroloverhowtheydisplayandselltheirbundledproducts.When
heisnotinthedigitalmatrix,Mitchellenjoystravelingabroad,exploring,skateboarding,
scubadiving,andtinkeringwithrandomprojects,fromwoodworkingto3Dprinting.
YoucanfollowMitchellontheMojoCreative&TechnicalSolutions’blog,whichcanbe
viewedbyvisitinghttp://b.mojomage.com/.
www.PacktPub.com
Supportfiles,eBooks,discountoffers,and
more
Forsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFand
ePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandas
aprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwith
usat<service@packtpub.com>formoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signup
forarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooks
andeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt’sonlinedigital
booklibrary.Here,youcansearch,access,andreadPackt’sentirelibraryofbooks.
Whysubscribe?
FullysearchableacrosseverybookpublishedbyPackt
Copyandpaste,print,andbookmarkcontent
Ondemandandaccessibleviaawebbrowser
Preface
BuildingMagento-poweredstorescanbeachallengingtask.Itrequiresagreatrangeof
technicalskillsthatarerelatedtothePHP/JavaScriptprograminglanguage,development
andproductionenvironments,andnumerousMagento-specificfeatures.Thisbookwill
providenecessaryinsightsintothebuildingblocksofMagento.
Bytheendofthisbook,youshouldbefamiliarwithconfigurationfiles,thedependency
injection,models,collections,blocks,controllers,events,observers,plugins,cronjobs,
shippingmethods,paymentmethods,andafewotherthings.Alloftheseshouldforma
solidfoundationforyourdevelopmentjourneylateron.
Whatthisbookcovers
Chapter1,UnderstandingthePlatformArchitecture,givesahigh-leveloverviewofthe
technologystack,architecturallayers,top-levelsystemstructure,andindividualmodule
structure.
Chapter2,ManagingtheEnvironment,givesanintroductiontoVirtualBox,Vagrant,and
AmazonAWSasplatformstosetupdevelopmentandproductionenvironments.Itfurther
provideshands-onexamplestosetup/scriptVagrantandAmazonEC2boxes.
Chapter3,ProgramingConceptsandConventions,introducesreaderstoafewseemingly
unrelatedbutimportantpartsofMagento,suchascomposer,servicecontracts,code
generation,thevardirectory,andfinally,codingstandards.
Chapter4,ModelsandCollections,takesalookintomodels,resources,collections,
schemas,anddatascripts.ItalsoshowsthepracticalCRUDactionsthatareappliedtoan
entityalongsidefilteringcollections.
Chapter5,UsingtheDependencyInjection,guidesreadersthroughthedependency
injectionmechanism.Itexplainstheroleofanobjectmanager,howtoconfigureclass
preferences,andhowtousevirtualtypes.
Chapter6,Plugins,givesadetailedinsightintothepowerfulnewconceptcalledplugins.
Itshowshoweasyitistoextend,oraddto,anexistingfunctionalityusingthe
before/after/aroundlisteners.
Chapter7,BackendDevelopment,takesreadersthroughahands-onapproachtowhatis
mostlyconsideredbackend-relateddevelopmentbits.Theseinvolvecronjobs,notification
messages,sessions,cookies,logging,profiler,events,cache,widgets,andsoon.
Chapter8,FrontendDevelopment,usesahigher-levelapproachtoguidethereader
throughwhatismostlyconsideredfrontend-relateddevelopment.Ittouchesonrendering
theflow,viewelements,blocks,templates,layouts,themes,CSS,andJavaScriptin
Magento.
Chapter9,TheWebAPI,takesupadetailedapproachtothepowerfulWebAPIprovided
byMagento.Itgiveshands-onpracticalexamplestocreateandusebothRESTandSOAP,
eitherthroughthePHPcURLlibrary,orfromtheconsole.
Chapter10,TheMajorFunctionalAreas,adoptsahigh-levelapproachtowards
introducingreaderswithsomeofthemostcommonsectionsofMagento.Theseinclude
CMS,catalogandcustomermanagement,andproductsandcustomerimport.Iteven
showshowtocreateacustomproducttypeandashippingandpaymentmethod.
Chapter11,Testing,givesanoverviewofthetypesoftestthatareavailableinMagento.It
furthershowshowtowriteandexecuteacustomtest.
Chapter12,BuildingaModulefromScratch,showstheentireprocessofdevelopinga
module,whichusesmostofthefeaturesintroducedinthepreviouschapters.Thefinal
resultisamodulethathasadminandstorefrontinterface,anadminconfigurationarea,e-
mailtemplates,installedschemascripts,tests,andsoon.
Whatyouneedforthisbook
Inordertosuccessfullyrunalltheexamplesprovidedinthisbook,youwillneedeither
yourownwebserverorathird-partywebhostingsolution.Thehigh-leveltechnology
stackincludesPHP,Apache/Nginx,andMySQL.TheMagento2CommunityEdition
platformitselfcomeswithadetailedlistofsystemrequirementsthatcanbefoundat
http://devdocs.magento.com/guides/v2.0/install-gde/system-requirements.html.Theactual
environmentsetupisexplainedinChapter2,ManagingtheEnvironment.
Whothisbookisfor
ThisbookisintendedprimarilyforintermediatetoprofessionalPHPdeveloperswhoare
interestedinMagento2development.Forbackenddevelopers,severaltopicsarecovered
thatwillenableyoutomodifyandextendyourMagentostore.Frontenddeveloperswill
alsofindsomecoverageonhowtocustomizethelookofasiteinthefrontend.
Giventhemassivecodeandstructurechanges,Magentoversion2.xcanbedescribedasa
platformthatissignificantlydifferentfromitspredecessor.Keepingthisinmind,this
bookwillneitherassumenorrequirepreviousknowledgeofMagento1.x.
Conventions
Inthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkinds
ofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheir
meaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,
pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:“The
AbstractProductPlugin1classdoesnothavetobeextendedfromanotherclassforthe
plugintowork.”
Ablockofcodeissetasfollows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:
ObjectManager/etc/config.xsd">
<typename="Magento\Catalog\Block\Product\AbstractProduct">
<pluginname="foggyPlugin1"
type="Foggyline\Plugged\Block\Catalog\Product\AbstractProductPlugin1"
disabled="false"sortOrder="100"/>
<pluginname="foggyPlugin2"
type="Foggyline\Plugged\Block\Catalog\Product\AbstractProductPlugin2"
disabled="false"sortOrder="200"/>
<pluginname="foggyPlugin3"
type="Foggyline\Plugged\Block\Catalog\Product\AbstractProductPlugin3"
disabled="false"sortOrder="300"/>
</type>
</config>
Anycommand-lineinputoroutputiswrittenasfollows:
phpbin/magentosetup:upgrade
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,
forexample,inmenusordialogboxes,appearinthetextlikethis:“IntheStoreView
drop-downfield,weselectthestoreviewwherewewanttoapplythetheme.”
Note
Warningsorimportantnotesappearinaboxlikethis.
Tip
Tipsandtricksappearlikethis.
Readerfeedback
Feedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthis
book—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsus
developtitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,simplye-mail<feedback@packtpub.com>,andmentionthe
book’stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingor
contributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
Customersupport
NowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelp
youtogetthemostfromyourpurchase.
Downloadingtheexamplecode
Youcandownloadtheexamplecodefilesfromyouraccountathttp://www.packtpub.com
forallthePacktPublishingbooksyouhavepurchased.Ifyoupurchasedthisbook
elsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-
maileddirectlytoyou.
Errata
Althoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdo
happen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthe
code—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveother
readersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufind
anyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,
selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthe
detailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedand
theerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataunderthe
Erratasectionofthattitle.
Toviewthepreviouslysubmittederrata,goto
https://www.packtpub.com/books/content/supportandenterthenameofthebookinthe
searchfield.TherequiredinformationwillappearundertheErratasection.
Piracy
PiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.At
Packt,wetaketheprotectionofourcopyrightandlicensesveryseriously.Ifyoucome
acrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswith
thelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<copyright@packtpub.com>withalinktothesuspectedpirated
material.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluable
content.
Chapter1.UnderstandingthePlatform
Architecture
Magentoisapowerful,highlyscalable,andhighlycustomizablee-commerceplatform
thatcanbeusedtobuildwebshopsand,ifneeded,somenon-e-commercesites.It
providesalargenumberofe-commercefeaturesoutofthebox.
Featuressuchasproductinventory,shoppingcart,supportfornumerouspaymentand
shipmentmethods,promotionrules,contentmanagement,multiplecurrencies,multiple
languages,multiplewebsites,andsoonmakeitagreatchoiceformerchants.Ontheother
hand,developersenjoythefullsetofmerchant-relevantfeaturesplusallthethingsrelated
toactualdevelopment.ThischapterwilltouchuponthetopicofrobustWebAPIsupport,
extensibleadministrationinterface,modules,theming,embeddedtestingframeworks,and
muchmore.
Inthischapter,ahigh-leveloverviewofMagentoisprovidedinthefollowingsections:
Thetechnologystack
Thearchitecturallayers
Thetop-levelfilesystemstructure
Themodulefilesystemstructure
Thetechnologystack
Magento’shighlymodularstructureisaresultofseveralopensourcetechnologies
embeddedintoastack.Theseopensourcetechnologiesarecomposedofthefollowing
components:
PHP:PHPisaserver-sidescriptinglanguage.Thisbookassumesthatyouhave
advancedknowledgeoftheobject-orientedaspectsofPHP,whichisoftenreferredto
asPHPOOP.
Codingstandards:Magentoputsalotofemphasisoncodingstandards.These
includePSR-0(theautoloadingstandard),PSR-1(thebasiccodingstandards),PSR-
2(thecodingstyleguide),PSR-3,andPSR-4.
Composer:ComposerisadependencymanagementpackageforPHP.Itisusedto
pullinallthevendorlibraryrequirements.
HTML:HTML5issupportedoutofthebox.
CSS:MagentosupportsCSS3viaitsin-builtLESSCSSpreprocessor.
jQuery:jQueryisamaturecross-platformJavaScriptlibrarythatwasdesignedto
simplifytheDOMmanipulation.ItisoneofthemostpopularJavaScriptframeworks
today.
RequireJS:RequireJSisaJavaScriptfileandmoduleloader.Usingamodularscript
loadersuchasRequireJShelpsimprovethespeedandqualityofcode.
Third-partylibraries:Magentocomespackedwithlotofthird-partylibraries,with
themostnotableonesbeingZendFrameworkandSymfony.Itisworthnotingthat
ZendFrameworkcomesintwodifferentmajorversions,namelyversion1.xand
version2.x.Magentousesbothoftheseversionsinternally.
ApacheorNginx:BothApacheandNginxareHTTPservers.Eachhasitsdistinct
advantagesanddisadvantages.Itwouldbeunfairtosayoneisbetterthananother,as
theirperformancewidelydependsontheentiresystem’ssetupandusage.Magento
workswithApache2.2and2.4andNginx1.7.
MySQL:MySQLisamatureandwidelyusedrelationaldatabasemanagement
system(RDBMS)thatusesstructuredquerylanguage(SQL).Therearebothfree
communityversionsandcommercialversionsofMySQL.Magentorequiresatleast
theofMySQLCommunityEditionversion5.6.
MTF:MagentoTestingFramework(MTF)deliversanautomatedtestingsuite.It
coversvarioustypesoftests,suchasperformance,functional,andunittesting.The
entireMTFisavailableonGitHub,whichcanbeviewedbyvisiting
https://github.com/magento/mtfasanisolatedproject.
Differentpiecesoftechnologycanbegluedintovariousarchitectures.Therearedifferent
waystolookattheMagentoarchitecture—fromtheperspectiveofamoduledeveloper,
systemintegrator,oramerchant,orfromsomeotherangle.
Thearchitecturallayers
Fromtoptobottom,Magentocanbedividedintofourarchitecturallayers,namely
presentation,service,domain,andpersistence.
Thepresentationlayeristheonethatwedirectlyinteractwiththroughthebrowser.It
containslayouts,blocks,templates,andevencontrollers,whichprocesscommandstoand
fromtheuserinterface.Client-sidetechnologiessuchasjQuery,RequireJS,CSS,and
LESSarealsoapartofthislayer.Usually,threetypesofusersinteractwiththislayer,
namelywebusers,systemadministrators,andthosemakingtheWebAPIcalls.Sincethe
WebAPIcallscanbemadeviaHTTPinamannerthatisthesameashowauserusesa
browser,there’sathinlinebetweenthetwo.WhilewebusersandWebAPIcallsconsume
thepresentationlayerasitis,thesystemadministratorshavethepowertochangeit.This
changemanifestsintheformofsettingtheactivethemeandchangingthecontentofthe
CMS(shortforcontentmanagementsystem)pages,blocks,andtheproducts
themselves.
Whenthecomponentsofapresentationlayerarebeinginteractedwith,theyusuallymake
callstotheunderlyingservicelayer.
Theservicelayeristhebridgebetweenthepresentationanddomainlayer.Itcontainsthe
servicecontracts,whichdefinetheimplementationbehavior.Aservicecontractis
basicallyafancynameforaPHPinterface.Thislayeriswherewecanfindthe
REST/SOAPAPIs.Mostuserinteractiononthestorefrontisroutedthroughtheservice
layer.Similarly,theexternalapplicationsthatmaketheREST/SOAPAPIcallsalso
interactwiththislayer.
Whenthecomponentsofaservicelayerarebeinginteractedwith,theyusuallymakecalls
totheunderlyingdomainlayer.
ThedomainlayerisreallythebusinesslogicofMagento.Thislayerisallaboutgeneric
dataobjectsandmodelsthatcomposethebusinesslogic.Thedomainlayermodels
themselvesdonotcontributetodatapersistence,buttheydocontainareferencetoa
resourcemodelthatisusedtoretrieveandpersistthedatatoaMySQLdatabase.A
domainlayercodefromonemodulecaninteractwithadomainmodulecodefromanother
moduleviatheuseofeventobservers,plugins,andthedi.xmldefinitions.Wewilllook
intothedetailsoftheselateroninotherchapters.Giventhepowerofpluginsanddi.xml,
itsimportanttonotethatthisinteractionisbestestablishedusingservicecontracts(the
PHPinterface).
Whenthecomponentsofthedomainlayerarebeinginteractedwith,theyusuallymake
callstotheunderlyingpersistencelayer.
Thepersistencelayeriswherethedatagetspersisted.Thislayerisinchargeofallthe
CRUD(shortforcreate,read,update,anddelete)requests.Magentousesanactive
recordpatternstrategyforthepersistencelayer.Themodelobjectcontainsaresource
modelthatmapsanobjecttooneormoredatabaserows.Here,itisimportantto
differentiatethecasesofsimpleresourcemodelandtheEntity-Attribute-Value(EAV)
resourcemodels.Asimpleresourcemodelmapstoasingletable,whiletheEAVresource
modelshavetheirattributesspreadoutoveranumberofMySQLtables.Asanexample,
theCustomerandCatalogresourcemodelsuseEAVresourcemodels,whilethe
newsletter’sSubscriberresourcemodelusesasimpleresourcemodel.
Thetop-levelfilesystemstructure
ThefollowinglistdepictstherootMagentofilesystemstructure:
.htaccess
.htaccess.sample
.php_cs
.travis.yml
CHANGELOG.md
CONTRIBUTING.md
CONTRIBUTOR_LICENSE_AGREEMENT.html
COPYING.txt
Gruntfile.js
LICENSE.txt
LICENSE_AFL.txt
app
bin
composer.json
composer.lock
dev
index.php
lib
nginx.conf.sample
package.json
php.ini.sample
phpserver
pub
setup
update
var
vendor
Theapp/etc/di.xmlfileisoneofthemostimportantfilesthatwemightoftenlookinto
duringdevelopment.Itcontainsvariousclassmappingsorpreferencesforindividual
interfaces.
Thevar/magento/language-*directoriesiswheretheregisteredlanguagesreside.
Thougheachmodulecandeclareitsowntranslationsunder
app/code/{VendorName}/{ModuleName}/i18n/,Magentowilleventuallyfallbacktoits
ownindividualmodulenamedi18nincasetranslationsarenotfoundinthecustom
moduleorwithinthethemedirectory.
Thebindirectoryiswherewecanfindthemagentofile.Themagentofileisascriptthatis
intendedtoberunfromaconsole.Oncetriggeredviathephpbin/magentocommand,it
runsaninstanceoftheMagento\Framework\Console\Cliapplication,presentinguswith
quiteanumberofconsoleoptions.Wecanusethemagentoscripttoenable/disablecache,
enable/disablemodules,runanindexer,anddomanyotherthings.
ThedevdirectoryiswherewecanfindtheMagentotestscripts.Wewillhavealookat
moreofthoseinlaterchapters.
Thelibdirectorycomprisestwomajorsubdirectories,namelytheserver-sidePHPlibrary
codeandfontsfoundunderlib/internalandtheclient-sideJavaScriptlibrariesfoundin
lib/web.
Thepubdirectoryiswherethepubliclyexposedfilesarelocated.Thisisthedirectorythat
weshouldsetasrootwhensettingupApacheorNginx.Thepub/index.phpfileiswhat
getstriggeredwhenthestorefrontisopenedinabrowser.
Thevardirectoryiswherethedynamicallygeneratedgrouptypeoffilessuchascache,
log,andafewothersgetcreatedin.Weshouldbeabletodeletethecontentofthisfolder
atanytimeandhaveMagentoautomaticallyrecreateit.
Thevendordirectoryiswheremostofthecodeislocated.Thisiswherewecanfind
variousthird-partyvendorcode,Magentomodules,themes,andlanguagepacks.Looking
furtherintothevendordirectory,youwillseethefollowingstructure:
.htaccess
autoload.php
bin
braintree
composer
doctrine
fabpot
justinrainbow
league
lusitanian
magento
monolog
oyejorge
pdepend
pelago
phpmd
phpseclib
phpunit
psr
sebastian
seld
sjparkinson
squizlabs
symfony
tedivm
tubalmartin
zendframework
Withinthevendordirectory,wecanfindcodefromvariousvendors,suchasphpunit,
phpseclib,monolog,symfony,andsoon.Magentoitselfcanbefoundhere.TheMagento
codeislocatedundervendor/magentodirectory,listed(partially)asfollows:
composer
framework
language-en_us
magento-composer-installer
magento2-base
module-authorization
module-backend
module-catalog
module-customer
module-theme
module-translation
module-ui
module-url-rewrite
module-user
module-version
module-webapi
module-widget
theme-adminhtml-backend
theme-frontend-blank
theme-frontend-luma
Youwillseethatthefurtherstructuringofdirectoriesfollowsacertainnamingschema,
whereasthetheme-*directorystoresthemes,themodule-*directorystoresmodules,and
thelanguage-*directorystoresregisteredlanguages.
Themodulefilesystemstructure
Magentoidentifiesitselfasahighlymodularplatform.Whatthismeansisthatthereis
literallyadirectorylocationwheremodulesareplaced.Let’stakeapeakattheindividual
modulestructurenow.ThefollowingstructurebelongstooneofthesimplercoreMagento
modules—theContactmodulethatcanbefoundinvendor/magento/module-contact:
Block
composer.json
Controller
etc
acl.xml
adminhtml
system.xml
config.xml
email_templates.xml
frontend
di.xml
page_types.xml
routes.xml
module.xml
Helper
i18n
LICENSE_AFL.txt
LICENSE.txt
Model
README.md
registration.php
Test
Unit
Block
Controller
Helper
Model
view
adminhtml
frontend
layout
contact_index_index.xml
default.xml
templates
form.phtml
Eventhoughtheprecedingstructureisforoneofthesimplermodules,youcanseethatit
isstillquiteextensive.
TheBlockdirectoryiswheretheview-relatedblockPHPclassesarelocated.
TheControllerdirectoryiswherethecontroller-relatedPHPclassesarestored.Thisis
thecodethatrespondstothestorefrontPOSTandGETHTTPactions.
Theetcdirectoryiswherethemoduleconfigurationfilesarepresent.Here,wecansee
filessuchasmodule.xml,di.xml,acl.xml,system.xml,config.xml,
email_templates.xml,page_types.xml,routes.xml,andsoon.Themodule.xmlfileis
anactualmoduledeclarationfile.Wewilllookintothecontentsofsomeofthesefilesin
thelaterchapters.
TheHelperdirectoryiswherevarioushelperclassesarelocated.Theseclassesareusually
usedtoabstractvariousstoreconfigurationvaluesintothegettermethods.
Thei18ndirectoryiswherethemoduletranslationpackageCSVfilesarestored.
TheModuledirectoryiswheretheentities,resourceentities,collections,andvariousother
businessclassescanbefound.
TheTestdirectorystoresthemoduleunittests.
Theviewdirectorycontainsallthemoduleadministratorandstorefronttemplatefiles
(.phtmland.html)andstaticfiles(.jsand.css).
Finally,theregistration.phpisamoduleregistrationfile.
Summary
Inthischapter,wetookaquicklookatthetechnologystackusedinMagento.We
discussedhowMagento,beinganopensourceproduct,takesextensiveuseofotheropen
sourceprojectsandlibrariessuchasMySQL,Apache,Nginx,ZendFramework,Symfony,
jQuery,andsoon.Wethenlearnedhowtheselibrariesarearrangedintodirectories.
Finally,weexploredoneoftheexistingcoremodulesandbrieflytookalookatan
exampleofamodule’sstructure.
Inthenextchapter,wearegoingtotackletheenvironmentsetupsothatwecanget
Magentoinstalledandreadyfordevelopment.
Chapter2.ManagingtheEnvironment
Throughoutthischapter,wewilllookintosettingupourdevelopmentandproduction
environments.Theideaistohaveafullyautomateddevelopmentenvironment,whichcan
beinitiatedwithasingleconsolecommand.Foraproductionenvironment,wewillturn
ourfocustooneoftheavailablecloudservices,andseehoweasyitistosetupMagento
forsimplerproductionprojects.Wewillnotbecoveringanyrobustenvironmentsetups
likeauto-scaling,cachingservers,contentdeliverynetworks,andsimilar.Thesearereally
jobsforSystemAdministratororDevOpsroles.Ourattentionhereisthebareminimum
neededtogetourMagentostoreupandrunning;amilestonewewillachievethroughout
thefollowingsectionswouldbe:
Settingupadevelopmentenvironment
VirtualBox
Vagrant
Vagrantproject
ProvisioningPHP
ProvisioningMySQL
ProvisioningApache
ProvisioningMagentoinstallation
Settingupaproductionenvironment
IntroductiontoAmazonWebServices(AWS)
SettingupaccessforS3usage
CreatingIAMusers
CreatingIAMgroups
SettingupS3fordatabaseandmediafilesbackup
BashscriptforautomatedEC2setup
SettingupEC2
SettingupElasticIPandDNS
Settingupadevelopmentenvironment
Inthissection,wewillbuildadevelopmentenvironmentusingVirtualBoxandVagrant.
Note
MagentoofficialrequirementscallforApache2.2or2.4,PHP5.6.xor5.5.x(PHP5.4is
notsupported),andMySQL5.6.x.Weneedtokeepthisinmindduringtheenvironment
setup.
VirtualBox
VirtualBoxispowerfulandfeature-richx86andAMD64/Intel64virtualizationsoftware.
Itisfree,runsonalargenumberofplatforms,andsupportsalargenumberofguest
operatingsystems.IfweareusingWindows,Linux,orOSXinourdailydevelopment,
wecanuseVirtualBoxtospinupavirtualmachinewithanisolatedguestoperating
systeminwhichwecaninstallourserversoftwareneededtorunMagento.Thismeans
usingMySQL,Apache,andafewotherthings.
Vagrant
Vagrantisahigh-levelsoftwarewrapperusedforvirtualizationsoftwaremanagement.
Wecanuseittocreateandconfiguredevelopmentenvironments.Vagrantsupportsseveral
typesofvirtualizationsoftwaresuchasVirtualBox,VMware,Kernel-basedVirtual
Machine(KVM),LinuxContainers(LXC),andothers.Itevensupportsserver
environmentslikeAmazonEC2.
Note
Beforewestart,weneedtomakesurewehaveVirtualBoxandVagrantinstalledalready.
Wecandownloadthemandinstallthefollowinginstructionsfromtheirofficialwebsites:
https://www.virtualbox.organdhttps://www.vagrantup.com.
Vagrantproject
Westartbymanuallycreatinganemptydirectorysomewherewithinourhostoperating
system,let’ssay/Users/branko/www/B05032-Magento-Box/.Thisisthedirectorywewill
pullinMagentocode.WewantMagentosourcecodetobeexternaltoVagrantbox,sowe
caneasilyworkwithitinourfavoriteIDE.
WethencreateaVagrantprojectdirectory,let’ssay/Users/branko/www/magento-box/.
Withinthemagento-boxdirectory,weruntheconsolecommandvagrantinit.This
resultsinanoutputasfollows:
A'Vagrantfile'hasbeenplacedinthisdirectory.Youarenowreadyto
'vagrantup'yourfirstvirtualenvironment!Pleasereadthecommentsin
theVagrantfileaswellasdocumentationon'vagrantup.com'formore
informationonusingVagrant.
TheVagrantfileisactuallyaRubylanguagesourcefile.Ifwestripawaythecomments,
itsoriginalcontentlookslikethefollowing:
#-*-mode:ruby-*-
#vi:setft=ruby:
Vagrant.configure(2)do|config|
config.vm.box="base"
end
Ifweweretorunvagrantupnowwithinthemagento-boxdirectory,thiswouldstartthe
VirtualBoxinheadless(noGUI)modeandrunthebaseoperatingsystem.However,let’s
holdoffrunningthatcommandjustnow.
TheideaistocreateamorerobustVagrantfilethatcoverseverythingrequiredfor
runningMagento,fromApache,MySQL,PHP,PHPUnit,composer,andfullMagento
installationwithperformancefixturedata.
ThoughVagrantdoesnothaveaseparateconfigurationfileonitsown,wewillcreateone,
aswewanttostoreconfigurationdatalikeaMySQLuserandpasswordinit.
Let’sgoaheadandcreatetheVagrantfile.config.ymlfile,alongsideaVagrantfilein
thesamedirectory,withcontentasfollows:
ip:192.168.10.10
s3:
access_key:"AKIAIPRNHSWEQNWHLCDQ"
secret_key:"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"
bucket:"foggy-project-dhj6"
synced_folder:
host_path:"/Users/branko/www/B05032-Magento-Box/"
guest_path:"/vagrant-B05032-Magento-Box/"
mysql:
host:"127.0.0.1"
username:root
password:user123
http_basic:
repo_magento_com:
username:a8adc3ac98245f519ua0d2v2c8770ec8
password:a38488dc908c6d6923754c268vc41bc4
github_oauth:
github_com:"d79fb920d4m4c2fb9d8798b6ce3a043f0b7c2af6"
magento:
db_name:"magento"
admin_firstname:"John"
admin_lastname:"Doe"
admin_password:"admin123"
admin_user:"admin"
admin_email:"email@change.me"
backend_frontname:"admin"
language:"en_US"
currency:"USD"
timezone:"Europe/London"
base_url:"http://magento.box"
fixture:"small"
ThereisnoVagrant-imposedstructurehere.ThiscanbeanyvalidYAMLfile.Thevalues
presentedaremerelyexamplesofwhatwecanputin.
Magentoenablesustogenerateapairof32-characterauthenticationtokensthatcanuseto
accesstheGitrepository.ThisisdonebyloggingintoMagentoConnectwithausername
andpassword,thengoingtoMyAccount|Developers|SecureKeys.ThePublicKey
andPrivateKeythenbecomeourusernameandpasswordforaccessingMagentoGitHub
repository.
HavingaseparateconfigurationfilemeanswecancommitVagrantfiletoversion
controlwithourproject,whileleavingtheVagrantfile.config.ymloutofversion
control.
WenowedittheVagrantfilebyreplacingitscontentwiththefollowing:
#-*-mode:ruby-*-
#vi:setft=ruby:
require'yaml'
vagrantConfig=YAML.load_file'Vagrantfile.config.yml'
Vagrant.configure(2)do|config|
config.vm.box="ubuntu/vivid64"
config.vm.network"private_network",ip:vagrantConfig['ip']
#Mountlocal"~/www/B05032-Magento-Box/"pathintobox's"/vagrant-
B05032-Magento-Box/"path
config.vm.synced_foldervagrantConfig['synced_folder']['host_path'],
vagrantConfig['synced_folder']['guest_path'],owner:"vagrant",group:"www-
data",mount_options:["dmode=775,fmode=664"]
#VirtualBoxspecificsettings
config.vm.provider"virtualbox"do|vb|
vb.gui=false
vb.memory="2048"
vb.cpus=2
end
#<provisionerhere>
end
Theprecedingcodefirstincludestheyamllibrary,andthenreadsthecontentofthe
Vagrantfile.config.ymlfileintoavagrantConfigvariable.Thenwehaveaconfig
block,withinwhichwedefinetheboxtype,fixedIPaddress,sharedfolderbetweenour
hostandguestoperatingsystem,andafewVirtualBoxspecificdetailssuchasCPUand
memoryallocated.
Weareusingtheubuntu/vivid64boxthatstandsfortheservereditionofUbuntu15.04
(VividVervet).ThereasonisthatthisUbuntuversiongivesustheMySQL5.6.xandPHP
5.6.x,whichstandasrequirementsforMagentoinstallation,amongotherthings.
WefurtherhaveaconfigurationentryassigningafixedIPtoourvirtualmachine.Let’sgo
aheadandaddanentryinthehostsfileofourhostoperatingsystemasfollows:
192.168.10.10magento.box
Note
ThereasonwhyweareassigningthefixedIPaddresstoourboxisthatwecandirectly
openaURLlikehttp://magento.boxwithinourhostoperatingsystem,andthenaccess
Apacheservedpagewithinourguestoperatingsystem.
Anotherimportantpartoftheprecedingcodeistheonewherewedefinedsynced_folder.
Besidessourceanddestinationpaths,thecrucialpartshereareowner,group,and
mount_options.Wesetthosetothevagrantuserthewww-datausergroup,and774and
664fordirectoryandfilepermissionsthatplaynicelywithMagento.
Let’scontinueeditingourVagrantfileaddingseveralprovisionerstoit,onebelowthe
other.Wedosobyreplacingthe#<provisionerhere>fromtheprecedingexample,
withcontentasfollows:
config.vm.provision"file",source:"~/.gitconfig",destination:
".gitconfig"
config.vm.provision"shell",inline:"sudoapt-getupdate"
HereweareinstructingVagranttopassour.gitconfigfilefromthehosttotheguest
operatingsystem.ThisissoweinheritthehostoperatingsystemGitsetuptotheguest
operatingsystemGit.Wethencallforapt-getupdateinordertoupdatetheguest
operatingsystem.
ProvisioningPHP
FurtheraddingtoVagrantfile,werunseveralprovisionersthatwillinstallPHP,required
PHPmodules,andPHPUnit,asfollows:
config.vm.provision"shell",inline:"sudoapt-get-yinstallphp5php5-dev
php5-curlphp5-imagickphp5-gdphp5-mcryptphp5-mhashphp5-mysqlphp5-
xdebugphp5-intlphp5-xsl"
config.vm.provision"shell",inline:"sudophp5enmodmcrypt"
config.vm.provision"shell",inline:"echo\"xdebug.max_nesting_level=200\"
>>/etc/php5/apache2/php.ini"
config.vm.provision"shell",inline:"sudoapt-get-yinstallphpunit"
Note
Thereisonethingworthpointingouthere–thelinewherewearewriting
xdebug.max_nesting_level=200intothephp.inifile.Thisisdonetoexcludethe
possibilitythatMagentowouldnotstartthrowingaMaximumFunctionsNestingLevel
of‘100’reached…error.
ProvisioningMySQL
FurtheraddingtoVagrantfile,werunprovisionersthatwillinstalltheMySQLserver,as
follows:
config.vm.provision"shell",inline:"sudodebconf-set-selections<<<
'mysql-servermysql-server/root_passwordpassword#{vagrantConfig['mysql']
['password']}'"
config.vm.provision"shell",inline:"sudodebconf-set-selections<<<
'mysql-servermysql-server/root_password_againpassword#
{vagrantConfig['mysql']['password']}'"
config.vm.provision"shell",inline:"sudoapt-get-yinstallmysql-server"
config.vm.provision"shell",inline:"sudoservicemysqlstart"
config.vm.provision"shell",inline:"sudoupdate-rc.dmysqldefaults"
WhatisinterestingwiththeMySQLinstallationisthatitrequiresapasswordanda
passwordconfirmationtobeprovidedduringinstallation.Thismakesitatroublingpartof
theprovisioningprocessthatexpectsshellcommandstosimplyexecutewithoutasking
forinput.Tobypassthis,weusedebconf-set-selectionstostoretheparametersfor
input.WereadthepasswordfromtheVagrantfile.config.ymlfileandpassitonto
debconf-set-selections.
Onceinstalled,update-rc.dmysqldefaultswilladdMySQLtotheoperatingsystem
bootprocess,thusmakingsureMySQLisrunningwhenwerebootthebox.
ProvisioningApache
FurtheraddingtoVagrantfile,weruntheApacheprovisionerasfollows:
config.vm.provision"shell",inline:"sudoapt-get-yinstallapache2"
config.vm.provision"shell",inline:"sudoupdate-rc.dapache2defaults"
config.vm.provision"shell",inline:"sudoserviceapache2start"
config.vm.provision"shell",inline:"sudoa2enmodrewrite"
config.vm.provision"shell",inline:"sudoawk'/<Directory
\\/>/,/AllowOverrideNone/{sub(\"None\",\"All\",$0)}{print}'
/etc/apache2/apache2.conf>/tmp/tmp.apache2.conf"
config.vm.provision"shell",inline:"sudomv/tmp/tmp.apache2.conf
/etc/apache2/apache2.conf"
config.vm.provision"shell",inline:"sudoawk'/<Directory
\\/var\\/www\\/>/,/AllowOverrideNone/{sub(\"None\",\"All\",$0)}{print}'
/etc/apache2/apache2.conf>/tmp/tmp.apache2.conf"
config.vm.provision"shell",inline:"sudomv/tmp/tmp.apache2.conf
/etc/apache2/apache2.conf"
config.vm.provision"shell",inline:"sudoserviceapache2stop"
TheprecedingcodeinstallsApache,addsittothebootsequence,startsit,andturnsonthe
rewritemodule.WethenhaveanupdatetotheApacheconfigurationfile,aswewantto
replaceAllowOverrideNonewithAllowOverrideAll,orelseourMagentowon’twork.
Oncethechangesaredone,westopApacheduetothelaterprocesses.
ProvisioningMagentoinstallation
FurtheraddingtoVagrantfile,wenowturnourattentiontoMagentoinstallation,which
wesplitintoseveralsteps.First,welinkourhostfolder,/vagrant-B05032-Magento-
Box/,withtheguest,/var/www/html,usingVagrant’ssyncedfolderfeature:
config.vm.provision"shell",inline:"sudorm-Rf/var/www/html"
config.vm.provision"shell",inline:"sudoln-s#
{vagrantConfig['synced_folder']['guest_path']}/var/www/html"
Wethenusethecomposercreate-projectcommandtopulltheMagento2filesfromthe
officialrepo.magento.comsourceintothe/var/www/html/director:
config.vm.provision"shell",inline:"curl-sS
https://getcomposer.org/installer|php"
config.vm.provision"shell",inline:"mvcomposer.phar
/usr/local/bin/composer"
config.vm.provision"shell",inline:"composerclearcache"
config.vm.provision"shell",inline:"echo'{\"http-basic\":
{\"repo.magento.com\":{\"username\":\"#{vagrantConfig['http_basic']
['repo_magento_com']['username']}\",\"password\":\"#
{vagrantConfig['http_basic']['repo_magento_com']['password']}\"}},
\"github-oauth\":{\"github.com\":\"#{vagrantConfig['github_oauth']
['github_com']}\"}}'>>/root/.composer/auth.json"
config.vm.provision"shell",inline:"composercreate-project—repository-
url=https://repo.magento.com/magento/project-community-edition
/var/www/html/"
WethencreateadatabaseinwhichMagentowillbeinstalledlateron:
config.vm.provision"shell",inline:"sudomysql—user=#
{vagrantConfig['mysql']['username']}—password=#{vagrantConfig['mysql']
['password']}-e\"CREATEDATABASE#{vagrantConfig['magento']
['db_name']};\""
WethenruntheMagentoinstallationfromthecommandline:
config.vm.provision"shell",inline:"sudophp/var/www/html/bin/magento
setup:install--base-url=\"#{vagrantConfig['magento']['base_url']}\"--db-
host=\"#{vagrantConfig['mysql']['host']}\"--db-user=\"#
{vagrantConfig['mysql']['username']}\"--db-password=\"#
{vagrantConfig['mysql']['password']}\"--db-name=\"#
{vagrantConfig['magento']['db_name']}\"--admin-firstname=\"#
{vagrantConfig['magento']['admin_firstname']}\"—admin-lastname=\"#
{vagrantConfig['magento']['admin_lastname']}\"--admin-email=\"#
{vagrantConfig['magento']['admin_email']}\"—admin-user=\"#
{vagrantConfig['magento']['admin_user']}\"—admin-password=\"#
{vagrantConfig['magento']['admin_password']}\"--backend-frontname=\"#
{vagrantConfig['magento']['backend_frontname']}\"--language=\"#
{vagrantConfig['magento']['language']}\"—currency=\"#
{vagrantConfig['magento']['currency']}\"—timezone=\"#
{vagrantConfig['magento']['timezone']}\""
config.vm.provision"shell",inline:"sudophp/var/www/html/bin/magento
deploy:mode:setdeveloper"
config.vm.provision"shell",inline:"sudophp/var/www/html/bin/magento
cache:disable"
config.vm.provision"shell",inline:"sudophp/var/www/html/bin/magento
cache:flush"
config.vm.provision"shell",inline:"sudophp/var/www/html/bin/magento
setup:performance:generate-fixtures/var/www/html/setup/performance-
toolkit/profiles/ce/small.xml"
Theprecedingcodeshowsweareinstallingthefixturesdataaswell.
WeneedtobecarefulduringtheVagrantfile.config.ymlfileconfiguration.Magento
installationisquitesensiblearoundprovideddata.Weneedtomakesureweprovidevalid
dataforfieldslikemailandpasswordorelsetheinstallationwillfailshowingerrors
similartothefollowing:
SQLSTATE[28000][1045]Accessdeniedforuser'root'@'localhost'(using
password:NO)
UserNameisarequiredfield.
FirstNameisarequiredfield.
LastNameisarequiredfield.
'magento.box'isnotavalidhostnameforemailaddress
'john.doe@magento.box'
'magento.box'appearstobeaDNShostnamebutcannotmatchTLDagainst
knownlist
'magento.box'appearstobealocalnetworknamebutlocalnetworknames
arenotallowed
Passwordisrequiredfield.
Yourpasswordmustbeatleast7characters.
Yourpasswordmustincludebothnumericandalphabeticcharacters.
Withthis,weconcludeourVagrantfilecontent.
RunningthevagrantupcommandnowwithinthesamedirectoryasVagrantfile
triggerstheboxcreationprocess.Duringthisprocess,allofthepreviouslylisted
commandswillgetexecuted.Theprocessalonetakesuptoanhourorso.
Oncevagrantupiscomplete,wecanissueanotherconsolecommand,vagrantssh,tolog
intothebox.
Atthesametime,ifweopenaURLlikehttp://magento.boxinourbrowser,weshould
seetheMagentostorefrontloading.
TheprecedingVagrantfilesimplypullsfromtheofficialMagentoGitrepositoryand
installsMagentofromthegroundup.VagrantfileandVagrantfile.config.ymlcanbe
furtherextendedandtailoredtosuitourindividualprojectneeds,likepullingthecode
fromtheprivateGitrepository,restoringthedatabasefromtheshareddrive,andsoon.
Thismakesforasimpleyetpowerfulscriptingprocessbywhichwecanpreparefully
readyper-projectmachinesforotherdevelopersinateamtobeabletoquicklyspinup.
Settingupaproductionenvironment
Aproductionenvironmentistheclient-facingenvironmentthatfocusesongood
performanceandavailability.Settingupproductionenvironmentsisnotreallysomething
wedeveloperstendtodo,especiallyiftherearerobustrequirementsaroundscaling,load
balancing,highavailability,andsimilar.Sometimes,however,weneedtosetupasimple
productionenvironment.Therearevariouscloudprovidersthatofferquickandsimple
solutionstothis.Forthepurposeofthissection,wewillturntoAmazonWebServices.
IntroductiontoAmazonWebServices
AmazonWebServices(AWS)isacollectionofremotecomputingservicesfrequently
referredtoaswebservices.AWSprovideson-demandcomputingresourcesandservices
inthecloud,withpay-as-you-gopricing.AmazongivesanicecomparisonofitsAWS
resources,sayingthatusingAWSresourcesinsteadofyourownislikepurchasing
electricityfromapowercompanyinsteadofrunningyourowngenerator.
Ifwestopandthinkaboutitforaminute,thismakesitinterestingtonotonlysystem
operationrolesbutalsofordeveloperslikeus.We(developers)arenowabletospin
variousdatabases,webapplicationservers,andevencomplexinfrastructuresinamatter
ofminutesandafewmouseclicks.Wecanruntheseservicesforafewminutes,hours,or
daysthenshutthemdown.Meanwhile,weonlypayfortheactualusage,notthefull
monthlyoryearlypriceaswedowithmostofthehostingservices.Althoughtheoverall
AWSpricingforcertainservicesmightnotbethecheapestoutthere,itcertainlyprovides
alevelofcommodityandusabilityunlikemanyotherservices.Commoditycomesfrom
thingslikeauto-scalingresources,afeaturethatoftenofferssignificantcostsavings
comparedtotheequivalenton-premisesinfrastructure.
QualitytrainingandacertificationprogramisanotherimportantaspectoftheAWS
ecosystem.CertificationsareavailableforSolutionsArchitect,Developer,andSysOps
Administrator,acrossassociateandprofessionallevels.Thoughthecertificationisnot
mandatory,ifwedealwithAWSonaregularbasis,weareencouragedtotakeone.
Earningthecertificationputsthesealonourexpertisetodesign,deploy,andoperate
highlyavailable,cost-effective,andsecureapplicationsontheAWSplatform.
WecanmanageourAWSthroughasimpleandintuitiveweb-baseduserinterfacecalled
AWSmanagementconsole,whichisavailableathttps://aws.amazon.com/console.Signing
intoAWS,weshouldbeabletoseeascreensimilartothefollowingone:
TheprecedingimageshowshowtheAWSmanagementconsolegroupstheAWSservices
visuallyintoseveralmajorgroups,asfollows:
Compute
DeveloperTools
MobileServices
Storage&ContentDelivery
ManagementTools
ApplicationServices
Database
Security&Identity
Networking
Analytics
EnterpriseApplications
Aspartofthischapter,wewillbetakingalookattheEC2servicefoundunderthe
ComputegroupandtheS3servicefoundundertheStorage&ContentDeliverygroup.
AmazonElasticComputeCloud(AmazonEC2)isawebservicethatprovidesare-
sizablecomputecapacityinthecloud.Wecanthinkofitasavirtualcomputermachinein
thecloudthatwecanturnonandoffatanytime,withinminutes.Wecanfurther
commissionone,hundreds,oreventhousandsofthesemachineinstancessimultaneously.
Thismakesforthere-sizablecomputecapacity.
S3providessecure,durable,andhighlyscalableobjectstorage.Itisdesignedtoprovide
durabilityof99.99%ofobjects.Theserviceprovidesawebserviceinterfacetostoreand
retrieveanyamountofdatafromanywhereontheweb.S3ischargedonlyperstoragethat
isactuallyused.S3canbeusedaloneortogetherwithotherAWSservicessuchasEC2.
SettingupaccessforS3usage
Aspartofourproductionenvironment,itisgoodtohavereliablestoragewherewecan
archivedatabaseandmediafiles.AmazonS3standsoutasapossiblesolution.
InordertoproperlysetaccesstotheS3scalablestorageservice,weneedtotakeaquick
lookintoAWSIdentityandAccessManagement(IAMforshort).IAMisawebservice
thathelpsussecurelycontrolaccesstoAWSresourcesforourusers.WecanuseIAMto
controlauthentication(whocanuseourAWSresources)andauthorization(what
resourcestheycanuseandinwhatways).Morespecifically,aswewillsoonsee,weare
interestedinUsersandGroups.
CreatingIAMusers
ThissectiondescribeshowtocreateIAMusers.AnIAMuserisanentitythatwecreatein
AWStorepresentthepersonorserviceusingitwheninteractingwithAWS.
LogintotheAWSconsole.
Undertheusermenu,clickonSecurityCredentialsasshowninthefollowingscreenshot:
Thisopensupthesecuritydashboardpage.
ClickingontheUsersmenushouldopenascreenlikethefollowingone:
OntheUsersmenu,weclickonCreateNewUser,whichopensapagelikethe
following:
Here,wefillinthedesiredusernameforoneormoreusers,somethinglike
foggy_s3_user1,andthenclickontheCreatebutton.
Weshouldnowseeascreenlikethefollowingone:
Here,wecanclickonDownloadCredentialstoinitiatetheCSVformatfiledownloador
copyandpasteourcredentialsmanually.
Note
AccessKeyIDandSecretAccessKeyarethetwopiecesofinformationwewillbeusing
toaccessS3storage.
ClickingthecloselinktakesusbacktotheUsersmenu,showingournewlycreateduser
listedasshowninthefollowingscreenshot:
CreatingIAMgroups
ThissectiondescribeshowtocreateIAMgroups.GroupsarecollectionsofIAMusers
thatwecanmanageasasingleunit.Solet’sbegin:
1. LogintotheAWSconsole.
2. Undertheusermenu,clickonSecurityCredentialsasshowninthefollowing
screenshot:
3. Thisopensupthesecuritydashboardpage.ClickingontheGroupsmenushould
openascreenlikethefollowingone:
4. OntheGroupsmenu,weclickonCreateNewGroup,whichopensapagelikethe
following:
5. Here,wefillinthedesiredgroupname,somethinglikeFoggyS3Test.
6. Weshouldnowseeascreenlikethefollowingone,whereweneedtoselectthe
groupPolicyTypeandclicktheNextStepbutton:
7. WeselecttheAmazonS3FullAccesspolicytypeandclicktheNextStepbutton.The
Reviewscreenisnowshown,askingustoreviewtheprovidedinformation:
8. Iftheprovidedinformationiscorrect,weconfirmitbyclickingtheCreateGroup
button.WeshouldnowbeabletoseeourgroupundertheGroupsmenuasshownin
thefollowingscreenshot:
9. MarkthecheckboxtotheleftofGroupName,clicktheGroupActionsdropdown,
andthenselectAddUserstoGroupasshowninthefollowingscreenshot:
10. ThisopenstheAddUserstoGrouppageasshowninthefollowingscreenshot:
11. MarkthecheckboxtotheleftofUserNameandclickontheAddUsersbutton.This
shouldaddtheselectedusertothegroupandthrowusbacktotheGroupslisting.
TheresultofthisuserandgroupcreationprocessisauserwithAccessKeyId,Secret
AccessKey,andassignedusergroupwiththeAmazonS3FullAccesspolicy.Wewilluse
thisinformationlateronwhenwedemonstratebackingupthedatabasetoS3.
SettingupS3fordatabaseandmediafilesbackup
S3consistsofbuckets.WecanthinkofabucketasthefirstleveldirectorywithinourS3
account.Wethensetthepermissionsandotheroptionsonthatdirectory(bucket).Inthis
section,wearegoingtocreateourownbucket,withtwoemptyfolderscalleddatabase
andmedia.Wewillusethesefolderslateronduringourenvironmentsetupinorderto
backupourMySQLdatabaseandourmediafiles.
WestartbyloggingintotheAWSmanagementconsole.
UndertheStorage&ContentDeliverygroup,weclickonS3.Thisopensascreen
similartothefollowing:
ClickontheCreateBucketbutton.Thisopensapopupliketheoneshowninthe
followingscreenshot:
Let’sprovideauniqueBucketName,preferablysomethingidentifyingtheprojectfor
whichwewillbebackingupthedatabaseandmediafile,andclicktheCreatebutton.
Forthepurposeofthischapter,let’simagineweselectedsomethinglikefoggy-project-
dhj6.
OurbucketshouldnowbevisibleundertheAllBucketslist.Ifweclickonit,anew
screenopensliketheoneshowninthefollowingscreenshot:
Here,weclickontheCreateFolderbuttonandaddthenecessarydatabaseandmedia
folders.
Whilestillwithintherootbucketdirectory,clickonthePropertiesbuttonandfillinthe
Permissionssectionasshowninthefollowingscreenshot:
Here,wearebasicallyassigningallpermissionstoAuthenticatedUsers.
WeshouldnowhaveanS3buckettowhichwecanpotentiallystoreourdatabaseand
mediabackupsusingthes3cmdconsoletoolthatwewillsoonreference.
BashscriptforautomatedEC2setup
SimilartotheVagrantfileshellprovisioners,let’sgoaheadandcreateasequenceof
bashshellcommandswecanuseforaproductionsetup.
Thefirstblockofcommandsgoesasfollows:
#!/bin/bash
apt-getupdate
apt-get-yinstalls3cmd
Here,startwiththe#!/bin/bashexpression.Thisspecifiesthetypeofscriptweare
executing.Thenwehaveasystemupdateands3cmdtoolinstallation.Thes3cmdisafree
command-linetoolandclientforuploading,retrieving,andmanagingdatainAmazonS3.
Wecanuseitlateronfordatabaseandmediafilebackupsandrestores.
Wetheninstallthepostfixmailserver,usingthefollowingcommands.Sincethepostfix
installationtriggersagraphicalinterfaceintheconsole,askingformailnameand
main_mailer_type,webypassthoseusingsudodebconf-set-selections.Once
installed,wereloadpostfix.
sudodebconf-set-selections<<<"postfixpostfix/mailnamestring
magentize.me"
sudodebconf-set-selections<<<"postfixpostfix/main_mailer_typestring
'InternetSite'"
sudoapt-getinstall-ypostfix
sudo/etc/init.d/postfixreload
UsingmailserverdirectlyontheEC2boxisfineforsmallerproductionsites,wherewe
donotexpecthightrafficoralargenumberofcustomers.Formoreintensiveproduction
sites,weneedtopayattentiontoAmazon,possiblyputtingathrottleonport25,thus
resultinginoutgoinge-mailtimeouts.InwhichcasewecaneitheraskAmazontoremove
thelimitationonouraccount,ormoveontomorerobustserviceslikeAmazonSimple
EmailService.
WetheninstallallthingsrelatedtoPHP.Noticehowweeveninstallxdebug,though
immediatelyturningitoff.Thismightcomeinhandyforthoseveryraremomentswhen
wereallyneedtodebugthelivesite,thenwecanturnitoffandplaywithremote
debugging.Wefurtherdownloadandsetcomposertotheuserpath:
apt-get-yinstallphp5php5-devphp5-curlphp5-imagickphp5-gdphp5-
mcryptphp5-mhashphp5-mysqlphp5-xdebugphp5-intlphp5-xsl
php5enmodmcrypt
php5dismodxdebug
servicephp5-fpmrestart
apt-get-yinstallphpunit
echo"StartingComposerstuff">>/var/tmp/box-progress.txt
curl-sShttps://getcomposer.org/installer|php
mvcomposer.phar/usr/local/bin/composer
WethenmoveontoMySQLinstallation.Here,wearealsousingdebconf-set-
selectionstoautomatetheconsolepartofprovidinginputparameterstotheinstallation.
Onceinstalled,MySQLisstartedandaddedtothebootprocess.
debconf-set-selections<<<'mysql-servermysql-server/root_password
passwordRrkSBi6VDg6C'
debconf-set-selections<<<'mysql-servermysql-server/root_password_again
passwordRrkSBi6VDg6C'
apt-get-yinstallmysql-server
servicemysqlstart
update-rc.dmysqldefaults
AlongsideMySQL,anothermajorcomponentisApache.Weinstallitusingthefollowing
commands.WithApache,weneedtopayattentiontoitsapache2.conffile.Weneedto
changeAllowOverrideNonetoAllowOverrideAllfortheMagentodirectory:
apt-get-yinstallapache2
update-rc.dapache2defaults
serviceapache2start
a2enmodrewrite
awk'/<Directory\/>/,/AllowOverrideNone/{sub("None","All",$0)}{print}'
/etc/apache2/apache2.conf>/tmp/tmp.apache2.conf
mv/tmp/tmp.apache2.conf/etc/apache2/apache2.conf
awk'/<Directory\/var\/www\/>/,/AllowOverrideNone/{sub("None","All",$0)}
{print}'/etc/apache2/apache2.conf>/tmp/tmp.apache2.conf
mv/tmp/tmp.apache2.conf/etc/apache2/apache2.conf
serviceapache2restart
NowthatwehaveMySQLandApacheinstalled,wemoveontogettingthesourcecode
filesinplace.Next,wearepullingfromtheofficialMagentoGitrepository.Thisisnotthe
sameasrepo.magento.comweusedwhensettingupthevagrant.Thoughinthiscasethe
MagentoGitrepositoryispublic,theideaistobeabletopullthecodefromtheprivate
GitHubrepository.Basedontheproductionenvironmentwetendtosetup,wecaneasily
replacethenextpartwithpullingfromanyotherprivateGitrepository.
sudorm-Rf/var/www/html/*
gitclonehttps://github.com/magento/magento2.git/var/www/html/.
sudocomposerconfig--globalgithub-oauth.github.com
7d6da6bld50dub454edc27db70db78b1f8997e6
sudocomposerinstall--working-dir="/var/www/html/"
mysql-uroot-pRrkSBi6VDg6C-e"CREATEDATABASEmagento;"
PUBLIC_HOSTNAME="'wget-q-O-http://instance-data/latest/meta-
data/public-hostname'"
Tip
Topullthecodefromaprivategitrepository,wecanuseacommandofthefollowing
form,Gitclone:https://<user>:<OAuthToken>@github.com/<user>/<repo>.git.
ThePUBLIC_HOSTNAMEvariablestorestheresponseofthewgetcommandthatcallsthe
http://instance-data/latest/meta-data/public-hostnameURL.ThisURLisa
featureofAWSthatallowsustogetthecurrentEC2instancemetadata.Wethenusethe
PUBLIC_HOSTNAMEvariableduringMagentoinstallation,passingitasthe--base-url
parameter:
php/var/www/html/bin/magentosetup:install--base-
url="http://$PUBLIC_HOSTNAME"--db-host="127.0.0.1"--db-user="root"--db-
password="RrkSBi6VDg6C"--db-name="magento"—admin-firstname="John"--admin-
lastname="Doe"--admin-email="john.doe@change.me"--admin-user="admin"--
admin-password="pass123"--backend-frontname="admin"—language="en_US"--
currency="USD"--timezone="Europe/London"
Theprecedingcommandtakesalotofperprojectspecificconfigurationvalues,sowe
needtobesuretopasteinourowninformationhereappropriatelybeforesimplycopying
andpastingit.
NowwemakesuretheMagentomodeissettoproduction,andcacheisturnedonand
flushed,soitregeneratesfresh:
php/var/www/html/bin/magentodeploy:mode:setproduction
php/var/www/html/bin/magentocache:enable
php/var/www/html/bin/magentocache:flush
Finally,weresetthepermissionsonthe/var/www/htmldirectoryinorderforourMagento
tofunctionproperly:
chown-Rubuntu:www-data/var/www/html
find/var/www/html-typef-print0|xargs-r0chmod640
find/var/www/html-typed-print0|xargs-r0chmod750
chmod-Rg+w/var/www/html/pub
chmod-Rg+w/var/www/html/var
chmod-Rg+w/var/www/html/app
chmod-Rg+w/var/www/html/vendor
WeneedtotakecautionwiththeprecedingGitandMagentoinstallationexample.The
ideaherewastoshowhowwecouldautomaticallysetGitpullfromthepublicorprivate
repository.TheMagentoinstallationpartisalittlebonusforthisspecificcase,not
somethingwewouldactuallydoonourproductionmachine.Thewholepurposeofthis
scriptwouldbetoserveasablueprintforpoweringupnewAMIimages.Soideallywhat
wewouldusuallydooncethecodeispulled,istorestorethedatabasefromsomeprivate
storagelikeS3andthenattachittoourinstallation.Thusmakingforacompleterestoreof
files,database,andmediaoncethescriptisfinished.
Puttingthatthoughtaside,let’sgetbacktoourscript,furtheraddingthedailydatabase
backupusingthesetofcommandasfollows:
CRON_CMD="mysql--user=root--password=RrkSBi6VDg6Cmagento|gzip-9>
~/database.sql.gz"CRON_JOB="302***$CRON_CMD"
(crontab-l|grep-v"$CRON_CMD";echo"$CRON_JOB")|crontab-
CRON_CMD="s3cmd--access_key="AKIAINLIM7M6WGJKMMCQ"—
secret_key="YJuPwkmkhrm4HQwoepZqUhpJPC/yQ/WFwzpzdbuO"put~/database.sql.gz
s3://foggy-project-ghj7/database/database_'date+"%Y-%m-%d_%H-%M"'.sql.gz"
CRON_JOB="303***$CRON_CMD"
(crontab-l|grep-v"$CRON_CMD";echo"$CRON_JOB")|crontab-
Here,weareaddingthe2:30AMcronjobforbackingupthedatabaseintothehome
directoryfilenameddatabase.sql.gz.Thenweareaddinganothercronjobthatexecutes
at3:30AM,whichpushesthedatabasebackuptoS3storage.
Similartothedatabasebackup,wecanaddmediabackupinstructionstoourscriptusing
thesetofcommandasfollows:
CRON_CMD="tar-cvvzf~/media.tar.gz/var/www/html/pub/media/"
CRON_JOB="302***$CRON_CMD"
(crontab-l|grep-v"$CRON_CMD";echo"$CRON_JOB")|crontab-
CRON_CMD="s3cmd--access_key="AKIAINLIM7M6WGJKMMCQ"—
secret_key="YJuPwkmkhrm4HQwoepZqUhpJPC/yQ/WFwzpzdbuO"put~/media.tar.gz
s3://foggy-project-ghj7/media/media_'date+"%Y-%m-%d_%H-%M"'.tar.gz"
CRON_JOB="303***$CRON_CMD"
(crontab-l|grep-v"$CRON_CMD";echo"$CRON_JOB")|crontab-
Theprecedingcommandshaveseveralpiecesofinformationcodedinthem.Weneedto
makesuretopasteinouraccesskey,secretkey,andS3bucketnameaccordingly.For
simplicitysake,wearenotaddressingsecurityimplicationssuchashardcodingtheaccess
tokensintothecronjobs.AmazonprovidesanextensiveAWSSecurityBestPractices
guidethatcanbedownloadedviatheofficialAWSwebsite.
NowthatwehavesomeunderstandingofwhatthebashscriptforautomatedEC2setup
couldlooklike,let’sproceedtosettinguptheEC2instance.
SettingupEC2
Followthesestepstogetthesettingdone:
1. LogintotheAWSconsole
2. UndertheComputegroup,clickonEC2,whichshouldopenascreenlikethe
following:
3. ClickontheLaunchInstancebutton,whichshouldopenascreenlikethefollowing:
4. ClickontheCommunityAMIstabtotheleft,andtypeinUbuntuVividintothe
searchfield,asshowninthefollowingscreenshot:
Tip
TheUbuntu15.x(VividVervet)serverbydefaultsupportsMySQL5.6.xandPHP
5.6.x,whichmakesitagoodcandidateforMagentoinstallation.
Weshouldnowseeascreenlikethefollowing:
5. ChooseaninstancetypeandclicktheNext:ConfigureInstanceDetailsbutton.We
shouldnowseeascreenlikethefollowing:
Note
Wewon’tbegettingintothedetailsofeachoftheseoptions.Sufficetosaythatifwe
areworkingonsmallerproductionsites,chancesarewecanleavemostofthese
optionswiththeirdefaultvalues.
6. MakesureShutdownbehaviorissettoStop.
7. WhilestillontheStep3:ConfigureInstanceDetailsscreen,scrolldowntothe
bottomAdvancedDetailsareaandexpandit.Weshouldseeascreenlikethe
following:
8. TheUserDatainputiswherewewillcopyandpastetheauto-setupbashscript
describedintheprevioussection,asshowninthefollowingscreenshot:
9. OncewecopyandpasteintheUserData,clickontheNext:AddStoragebutton.
Thisshouldbringupthescreenasshowninthefollowingscreenshot:
10. WithinStep4:AddStorage,wecanselectoneormorevolumestoattachtoour
EC2instance.Preferably,weshouldselecttheSSDtypeofstorageforfaster
performance.Oncethevolumeisset,clickonNext:TagInstance.Weshouldnow
seeascreenlikethefollowing:
11. TheTagInstancescreenallowsustoassigntags.Tagsenableustocategorizeour
AWSresourcebypurpose,owner,environment,orsomeotherway.Oncewehave
assignedoneormoretags,weclickontheNext:ConfigureSecurityGroupbutton.
Weshouldnowseeascreenlikethefollowing:
12. TheConfigureSecurityGroupscreenallowsustosetrulesforinboundand
outboundtraffic.WewanttobeabletoaccessSSH,HTTP,HTTPs,andSMTP
servicesonthebox.Onceweaddtheruleswewant,clickontheReviewand
Launchbutton.Thisopensascreenlikethefollowing:
13. TheReviewInstanceLaunchscreeniswherewecanviewthesummaryofthebox
weconfigureduptothispoint.Ifneeded,wecangobackandeditindividualsettings.
Oncewearesatisfiedwiththesummary,weclickontheLaunchbutton.Thisopens
apopuplikethefollowing:
14. Here,wegettochooseanexistingsecuritykey,orcreateanewone.Keysare
providedinPEMformat.Onceweselectthekey,weclickontheLaunchInstance
button.
WeshouldnowseetheLaunchStatusscreenlikethefollowing:
15. ClickingontheinstancenamelinkshouldthrowusbackattheEC2Dashboardlike
showninthefollowingscreenshot:
Withregardtotheprecedingimage,weshouldnowbeabletoconnecttoourEC2box
witheitheroneofthefollowingconsolecommands:
ssh-i/path/to/magento-box.pemubuntu@ec2-52-29-35-49.eu-central-
1.compute.amazonaws.com
ssh-i/path/to/magento-box.pemubuntu@52.29.35.49
ItmighttakesometimeforourEC2boxtoexecutealloftheshellcommandspassedtoit.
WecanconvenientlySSHintotheboxandthenexecutethefollowingcommandtogetan
overviewofcurrentprogress:
sudotail-f/var/tmp/box-progress.txt
Withthis,weconcludeourinstancelaunchprocess.
SettingupElasticIPandDNS
NowthatwehaveanEC2boxinplace,let’sgoaheadandcreatetheso-calledElasticIP
forit.TheElasticIPaddressisastaticIPaddressdesignedfordynamiccloudcomputing.
ItistiedtotheAWSaccount,andnotsomespecificinstance.Thismakesitconvenientto
easilyre-mapitfromoneinstancetoanother.
Let’sgoaheadandcreateanElasticIPasfollows:
1. LogintotheAWSconsole.
2. UndertheComputegroup,clickonEC2,whichshouldgetustotheEC2
Dashboard.
3. UndertheEC2Dashboard,intheleftareaunderNetworkandSecuritygrouping,
clickonElasticIPs.Thisshouldopenascreenlikethefollowing:
4. ClickontheAllocateNewAddressbutton,whichshouldopenapopuplikethe
following:
5. ClickontheYes,Allocatebutton,whichshouldopenanotherpopuplikethe
following:
6. NowthattheElasticIPaddressiscreated,right-clickingonitwithinthetablelisting
shouldbringuptheoptionsmenuasshowninthefollowingscreenshot:
7. ClickontheAssociateAddresslink.Thisshouldopenapopuplikethefollowing:
8. OntheAssociateAddresspopup,weselecttheInstancetowhichwewanttoassign
theElasticIPaddressandclickontheAssociatebutton.
Atthispoint,ourEC2boxhasastatic(ElasticIP)addressassigned.Wecannowloginto
ourdomainregistrarandpointtheA-recordofourDNStotheElasticIPwejustcreated.
UntilwewaitfortheDNSchangetokickin,thereisonemorethingweneedtoaddress.
WeneedtoSSHintoourboxandexecutethefollowingsetofcommands:
mysql-uroot-pRrkSBi6VDg6C-e"USEmagento;UPDATEcore_config_dataSET
value='http://our-domain.something/'WHEREpathLIKE
"%web/unsecure/base_url%";"
php/var/www/html/bin/magentocache:flush
ThiswillupdatetheMagentoURL,sowecanaccessitviaawebbrowseroncetheDNS
changekicksin.Withalittlebitofupfrontplanning,wecouldhaveeasilymadethisbita
partoftheuserdataforourEC2instance,simplybyprovidingtheright--base-url
parametervalueinthefirstplace.
Summary
Throughoutthischapter,wefocusedontwomainthings:settingupdevelopmentand
productionenvironments.
Aspartofthedevelopmentenvironment,weembracedfreesoftwaresuchasVirtualBox
andVagranttomanageourenvironmentsetup.Thesetupalonecamedowntoasingle
Vagrantfilescriptthatcontainedthenecessarysetofcommandstoinstalleverything
fromtheUbuntuserver,PHP,Apache,MySQL,andevenMagentoitself.Weshouldbyno
meanslookatthisscriptasfinalandonlyasavalidscripttosetupourdevelopment
environment.Investingtimeinmakingthedevelopmentenvironmentclosertotheproject-
specificrequirementspaysoffintermsofteamproductivity.
Wethenmovedontotheproductionenvironment.Here,welookedintoAmazonWeb
Services,utilizingS3andEC2alongtheway.Theproductionenvironmentalsocamewith
itsownscriptedinstallationprocessthatsetsmostofthethings.Similarly,thisscriptisby
nomeansfinalandisonlyavalidwaytosetthingsup;it’smoreofabaseexampleofhow
todoit.
Inthenextchapter,wewilltakeacloserlookatsomeofprogrammingconceptsand
conventions.
Chapter3.ProgrammingConceptsand
Conventions
Withyearsofexperience,theMagentoplatformgrewuptoimplementalotofindustry
concepts,standards,andconventions.Throughoutthischapter,wewilllookintoseveralof
theseindependentsectionsthatstandoutinday-to-dayinteractionswithMagento
development.
Wewillgothroughthefollowingsectionsinthischapter:
Composer
Servicecontracts
Codegeneration
Thevardirectory
Codingstandards
Composer
ComposerisatoolthathandlesdependencymanagementinPHP.Itisnotapackage
managerlikeYumandAptonLinuxsystemsare.Thoughitdealswithlibraries
(packages),itdoessoonaper-projectlevel.Itdoesnotinstallanythingglobally.
Composerisamultiplatformtool.Therefore,itrunsequallywellonWindows,Linux,and
OSX.
InstallingComposeronamachineisassimpleasrunningtheinstallerintheproject
directorybyusingthefollowingcommand:
curl-sShttps://getcomposer.org/installer|php
MoreinformationabouttheinstallationofComposercanbefoundonitsofficialwebsite,
whichcanbeviewedbyvisitinghttps://getcomposer.org.
ComposerisusedtofetchMagentoandthethird-partycomponentsthatituses.Asseenin
thepreviouschapter,thefollowingcomposercommandiswhatpullseverythingintothe
specifieddirectory:
composercreate-project--repository-url=https://repo.magento.com/
magento/project-enterprise-edition<installationdirectoryname>
OnceMagentoisdownloadedandinstalled,therearenumerouscomposer.jsonfilesthat
canbefoundinitsdirectory.Assuming<installationdirectoryname>ismagento2,if
weweretodoaquicksearchexecutingcommandsuchasfindmagento2/-name
'composer.json',thatwouldyieldover100composer.jsonfiles.Someofthesefilesare
(partially)listedhere:
/vendor/magento/module-catalog/composer.json
/vendor/magento/module-cms/composer.json
/vendor/magento/module-contact/composer.json
/vendor/magento/module-customer/composer.json
/vendor/magento/module-sales/composer.json
/...
/vendor/magento/theme-adminhtml-backend/composer.json
/vendor/magento/theme-frontend-blank/composer.json
/vendor/magento/theme-frontend-luma/composer.json
/vendor/magento/language-de_de/composer.json
/vendor/magento/language-en_us/composer.json
/...
/composer.json
/dev/tests/...
/vendor/magento/framework/composer.json
Themostrelevantfileisprobablythecomposer.jsonfileintherootofthemagento
directory.Itscontentappearslikethis:
{
"name":"magento/project-community-edition",
"description":"eCommercePlatformforGrowth(CommunityEdition)",
"type":"project",
"version":"2.0.0",
"license":[
"OSL-3.0",
"AFL-3.0"
],
"repositories":[
{
"type":"composer",
"url":"https://repo.magento.com/"
}
],
"require":{
"magento/product-community-edition":"2.0.0",
"composer/composer":"@alpha",
"magento/module-bundle-sample-data":"100.0.*",
"magento/module-widget-sample-data":"100.0.*",
"magento/module-theme-sample-data":"100.0.*",
"magento/module-catalog-sample-data":"100.0.*",
"magento/module-customer-sample-data":"100.0.*",
"magento/module-cms-sample-data":"100.0.*",
"magento/module-catalog-rule-sample-data":"100.0.*",
"magento/module-sales-rule-sample-data":"100.0.*",
"magento/module-review-sample-data":"100.0.*",
"magento/module-tax-sample-data":"100.0.*",
"magento/module-sales-sample-data":"100.0.*",
"magento/module-grouped-product-sample-data":"100.0.*",
"magento/module-downloadable-sample-data":"100.0.*",
"magento/module-msrp-sample-data":"100.0.*",
"magento/module-configurable-sample-data":"100.0.*",
"magento/module-product-links-sample-data":"100.0.*",
"magento/module-wishlist-sample-data":"100.0.*",
"magento/module-swatches-sample-data":"100.0.*",
"magento/sample-data-media":"100.0.*",
"magento/module-offline-shipping-sample-data":"100.0.*"
},
"require-dev":{
"phpunit/phpunit":"4.1.0",
"squizlabs/php_codesniffer":"1.5.3",
"phpmd/phpmd":"@stable",
"pdepend/pdepend":"2.0.6",
"sjparkinson/static-review":"~4.1",
"fabpot/php-cs-fixer":"~1.2",
"lusitanian/oauth":"~0.3<=0.7.0"
},
"config":{
"use-include-path":true
},
"autoload":{
"psr-4":{
"Magento\\Framework\\":"lib/internal/Magento/Framework/",
"Magento\\Setup\\":"setup/src/Magento/Setup/",
"Magento\\":"app/code/Magento/"
},
"psr-0":{
"":"app/code/"
},
"files":[
"app/etc/NonComposerComponentRegistration.php"
]
},
"autoload-dev":{
"psr-4":{
"Magento\\Sniffs\\":
"dev/tests/static/framework/Magento/Sniffs/",
"Magento\\Tools\\":"dev/tools/Magento/Tools/",
"Magento\\Tools\\Sanity\\":"dev/build/publication/sanity/
Magento/Tools/Sanity/",
"Magento\\TestFramework\\Inspection\\":
"dev/tests/static/framework/Magento/TestFramework/Inspection/",
"Magento\\TestFramework\\Utility\\":
"dev/tests/static/framework/Magento/TestFramework/Utility/"
}
},
"minimum-stability":"alpha",
"prefer-stable":true,
"extra":{
"magento-force":"override"
}
}
Composer’sJSONfilefollowsacertainschema.Youwillfindadetaileddocumentation
ofthisschemaathttps://getcomposer.org/doc/04-schema.md.Applyingtotheschema
ensuresvalidityofthecomposerfile.Wecanseethatallthelistedkeyssuchasname,
description,require,config,andsoon,aredefinedbytheschema.
Let’stakealookattheindividualmodule’scomposer.jsonfile.Oneofthesimpler
moduleswiththeleastamountofdependenciesistheContactmodulewithits
vendor/magento/module-contact/composer.jsoncontent,whichlookslikethis:
{
"name":"magento/module-contact",
"description":"N/A",
"require":{
"php":"~5.5.0|~5.6.0|~7.0.0",
"magento/module-config":"100.0.*",
"magento/module-store":"100.0.*",
"magento/module-backend":"100.0.*",
"magento/module-customer":"100.0.*",
"magento/module-cms":"100.0.*",
"magento/framework":"100.0.*"
},
"type":"magento2-module",
"version":"100.0.2",
"license":[
"OSL-3.0",
"AFL-3.0"
],
"autoload":{
"files":[
"registration.php"
],
"psr-4":{
"Magento\\Contact\\":""
}
}
}
YouwillseethatthemodulesdefinedependenciesonthePHPversionandothermodules.
Furthermore,youwillseetheuseofPSR-4forautoloadingandthedirectloadingofthe
registration.phpfile.
Next,let’stakealookatthecontentsofvendor/magento/language-
en_us/composer.jsonfromtheen_uslanguagemodule:
{
"name":"magento/language-en_us",
"description":"English(UnitedStates)language",
"version":"100.0.2",
"license":[
"OSL-3.0",
"AFL-3.0"
],
"require":{
"magento/framework":"100.0.*"
},
"type":"magento2-language",
"autoload":{
"files":[
"registration.php"
]
}
}
Finally,let’stakealookatthecontentsofvendor/magento/theme-frontend-
luma/composer.jsonfromthelumatheme:
{
"name":"magento/theme-frontend-luma",
"description":"N/A",
"require":{
"php":"~5.5.0|~5.6.0|~7.0.0",
"magento/theme-frontend-blank":"100.0.*",
"magento/framework":"100.0.*"
},
"type":"magento2-theme",
"version":"100.0.2",
"license":[
"OSL-3.0",
"AFL-3.0"
],
"autoload":{
"files":[
"registration.php"
]
}
}
Asmentionedpreviously,therearealotmorecomposerfilesscatteredaroundMagento.
Servicecontracts
AservicecontractisasetofPHPinterfacesthatisdefinedbyamodule.Thiscontract
comprisesdatainterfacesandserviceinterfaces.
Theroleofthedatainterfaceistopreservedataintegrity,whiletheroleoftheservice
interfaceistohidethebusinesslogicdetailsfromserviceconsumers.
Datainterfacesdefinevariousfunctions,suchasvalidation,entityinformation,search
relatedfunctions,andsoon.TheyaredefinedwithintheApi/Datadirectoryofan
individualmodule.Tobetterunderstandtheactualmeaningofit,let’stakealookatthe
datainterfacesfortheMagento_Cmsmodule.Inthevendor/magento/module-
cms/Api/Data/directory,therearefourinterfacesdefined,asfollows:
BlockInterface.php
BlockSearchResultsInterface.php
PageInterface.php
PageSearchResultsInterface.php
TheCMSmoduleactuallydealswithtwoentities,onebeingBlockandtheotheronebeing
Page.Lookingattheinterfacesdefinedintheprecedingcode,wecanseethatwehave
separatedatainterfacefortheentityitselfandseparatedatainterfaceforsearchresults.
Let’stakeacloserlookatthe(stripped)contentsoftheBlockInterface.phpfile,whichis
definedasfollows:
namespaceMagento\Cms\Api\Data;
interfaceBlockInterface
{
constBLOCK_ID='block_id';
constIDENTIFIER='identifier';
constTITLE='title';
constCONTENT='content';
constCREATION_TIME='creation_time';
constUPDATE_TIME='update_time';
constIS_ACTIVE='is_active';
publicfunctiongetId();
publicfunctiongetIdentifier();
publicfunctiongetTitle();
publicfunctiongetContent();
publicfunctiongetCreationTime();
publicfunctiongetUpdateTime();
publicfunctionisActive();
publicfunctionsetId($id);
publicfunctionsetIdentifier($identifier);
publicfunctionsetTitle($title);
publicfunctionsetContent($content);
publicfunctionsetCreationTime($creationTime);
publicfunctionsetUpdateTime($updateTime);
publicfunctionsetIsActive($isActive);
}
Theprecedinginterfacedefinesallthegetterandsettermethodsfortheentityathand
alongwiththeconstantvaluesthatdenoteentityfieldnames.Thesedatainterfacesdonot
includemanagementactions,suchasdelete.Theimplementationofthisspecific
interfacecanbeseeninthevendor/magento/module-cms/Model/Block.phpfile,where
theseconstantscometouse,asfollows(partially):
publicfunctiongetTitle()
{
return$this->getData(self::TITLE);
}
publicfunctionsetTitle($title)
{
return$this->setData(self::TITLE,$title);
}
Serviceinterfacesaretheonesthatincludemanagement,repository,andmetadata
interfaces.Theseinterfacesaredefineddirectlywithinthemodule’sApidirectory.
LookingbackattheMagentoCmsmodule,itsvendor/magento/module-cms/Api/
directoryhastwoserviceinterfaces,whicharedefinedasfollows:
BlockRepositoryInterface.php
PageRepositoryInterface.php
AquicklookintothecontentsofBlockRepositoryInterface.phprevealsthefollowing
(partial)content:
namespaceMagento\Cms\Api;
useMagento\Framework\Api\SearchCriteriaInterface;
interfaceBlockRepositoryInterface
{
publicfunctionsave(Data\BlockInterface$block);
publicfunctiongetById($blockId);
publicfunctiongetList(SearchCriteriaInterface$searchCriteria);
publicfunctiondelete(Data\BlockInterface$block);
publicfunctiondeleteById($blockId);
}
Here,weseemethodsthatareusedtosave,fetch,search,anddeletetheentity.
TheseinterfacesarethenimplementedviatheWebAPIdefinitions,aswewillseelaterin
Chapter9,TheWebAPI.Theresultiswell-definedanddurableAPI’sthatothermodules
andthird-partyintegratorscanconsume.
Codegeneration
OneoftheneatfeaturesoftheMagentoapplicationiscodegeneration.Codegeneration,
asimpliedbyitsname,generatesnonexistentclasses.Theseclassesaregeneratedin
Magento’svar/generationdirectory.
Thedirectorystructurewithinvar/generationissomewhatsimilartothatofthecore
vendor/magento/module-*andapp/codedirectories.Tobemoreprecise,itfollowsthe
modulestructure.ThecodeisgeneratedforsomethingthatiscalledFactory,Proxy,and
Interceptorclasses.
TheFactoryclasscreatesaninstanceofatype.Forexample,a
var/generation/Magento/Catalog/Model/ProductFactory.phpfilewitha
Magento\Catalog\Model\ProductFactoryclasshasbeencreatedbecausesomewhere
withinthevendor/magentodirectoryanditscode,thereisacalltothe
Magento\Catalog\Model\ProductFactoryclass,whichoriginallydoesnotexistin
Magento.Duringruntime,when{someClassName}Factoryiscalledinthecode,Magento
createsaFactoryclassunderthevar/generationdirectoryifitdoesnotexist.The
followingcodeisanexampleofthe(partial)ProductFactoryclass:
namespaceMagento\Catalog\Model;
/**
*Factoryclassfor@see\Magento\Catalog\Model\Product
*/
classProductFactory
{
//...
/**
*Createclassinstancewithspecifiedparameters
*
*@paramarray$data
*@return\Magento\Catalog\Model\Product
*/
publicfunctioncreate(array$data=array())
{
return$this->_objectManager->create($this->_instanceName,$data);
}
}
NotethecreatemethodthatcreatesandreturnstheProducttypeinstance.Also,note
howthegeneratedcodeistypesafeproviding@returnannotationforintegrated
developmentenvironments(IDEs)tosupporttheautocompletefunctionality.
Factoriesareusedtoisolateanobjectmanagerfromthebusinesscode.Factoriescanbe
dependentontheobjectmanager,unlikebusinessobjects.
TheProxyclassisawrapperforsomebaseclass.Proxyclassesprovidebetter
performancethanthebaseclassesbecausetheycanbeinstantiatedwithoutinstantiatinga
baseclass.Abaseclassisinstantiatedonlywhenoneofitsmethodsiscalled.Thisis
highlyconvenientforcaseswherethebaseclassisusedasadependency,butittakesalot
oftimetoinstantiate,anditsmethodsareusedonlyduringsomepathsofexecution.
LikeFactory,theProxyclassesarealsogeneratedunderthevar/generationdirectory.
Ifweweretotakealookatthe
var/generation/Magento/Catalog/Model/Session/Proxy.phpfilethatcontainsthe
Magento\Catalog\Model\Session\Proxyclass,wewouldseethatitactuallyextends
\Magento\Catalog\Model\Session.ThewrappingProxyclassimplementsseveral
magicalmethodsalongtheway,suchas__sleep,__wakeup,__clone,and__call.
InterceptorisyetanotherclasstypethatgetsautogeneratedbyMagento.Itisrelatedtothe
pluginsfeature,whichwillbediscussedindetaillaterinChapter6,Plugins.
Inordertotriggercoderegeneration,wecanusethecodecompilerthatisavailableonthe
console.Wecanruneitherthesingle-tenantcompilerorthemulti-tenantcompiler.
Thesingle-tenantimpliesonewebsiteandstore,anditisexecutedbyusingthefollowing
command:
magentosetup:di:compile
Themulti-tenantimpliesmorethanoneindependentMagentoapplication,anditis
executedbyusingfollowingcommand.
magentosetup:di:compile-multi-tenant
Codecompilationgeneratesfactories,proxies,interceptors,andseveralotherclasses,as
listedinthesetup/src/Magento/Setup/Module/Di/App/Task/Operation/directory.
Thevardirectory
Magentodoesalotofcachingandautogenerationofcertainclasstypes.Thesecachesand
generatedclassesarealllocatedinMagento’srootvardirectory.Theusualcontentsofthe
vardirectoryisasfollows:
cache
composer_home
generation
log
di
view_preprocessed
page_cache
Duringdevelopment,wewillmostlikelyneedtoperiodicallyclearthesesothatour
changescankickin.
Wecanissuetheconsolecommandasfollowstoclearindividualdirectories:
rm-rf{Magentorootdir}/var/generation/*
Alternatively,wecanusethebuilt-inbin/magentoconsoletooltotriggercommandsthat
willdeletetheproperdirectoriesforus,asfollows:
bin/magentosetup:upgrade:ThisupdatestheMagentodatabaseschemaanddata.
Whiledoingthis,ittruncatesthevar/diandvar/generationdirectories.
bin/magentosetup:di:compile:Thisclearsthevar/generationdirectory.After
doingthis,itcompilesthecodeinitagain.
bin/magentodeploy:mode:set{mode}:Thischangesthemodefromthedeveloper
modetotheproductionmodeandviceversa.Whiledoingthis,ittruncatesthe
var/di,var/generation,andvar/view_preprocesseddirectories.
bin/magentocache:clean{type}:Thiscleansthevar/cacheandvar/page_cache
directories.
Itisimportanttokeepthevardirectoryinmindatalltimesduringdevelopment.
Otherwise,thecodemightencounterexceptionsandfunctionimproperly.
Codingstandards
Codingstandardsarearesultofconventionsdesignedtoproducehigh-qualitycode.
Adoptingcertainstandardsyieldsbettercodequality,reducesthetimetakentodevelop,
andminimizesmaintenancecost.Followingcodingstandardsrequiresknowingthe
standardsinquestionandmeticulouslyapplyingittoeveryaspectofthecodethatwe
write.
ThereareseveralcodingstandardsthatMagentoabidesby,suchasthefollowingones:
Thecodedemarcationstandard
ThePHPcodingstandard
TheJavaScriptcodingstandard
ThejQuerywidgetcodingstandard
TheDocBlockstandard
JavaScriptDocBlockstandard
TheLESScodingstandard
ThecodedemarcationstandardspeaksofdecouplingHTML,CSS,andJSfromPHP
classes.Bydoingso,thebackend-relateddevelopmentstaysunaffectedbyfrontend
developmentandviceversa.Thismeansthatwecanmakebusinesslogicchangeswithout
fearingabrokenfrontend.
ThePHPcodingstandardreferstoPSR-1:BasicCodingStandardandPSR-2:Coding
StyleGuidethataredescribedathttp://www.php-fig.org.PSR-1touchesonPHP
filenames,classnames,namespaces,classconstant,properties,andmethods.PSR-2
extendsthePSR-1bytouchingupontheactualinnersofaclass,suchasspaces,braces,
methodandpropertiesvisibility,controlstructures,andsoon.
TheJavaScriptcodingstandardisbasedontheGoogleJavaScriptStyleGuidefoundat
https://google.github.io/styleguide/javascriptguide.xml.Thiscodingstandardtoucheson
theJavaScriptlanguageandcodingstylerules.ItisalotlikePSR-1andPSR-2forPHP.
ThejQuerywidgetcodingstandardisflaggedasmandatoryforMagentocoredevelopers
andrecommendedforthird-partydevelopers.Itgoeswithoutsayinghowimportant
jQueryUIwidgetsareinMagento.Thestandarddescribesseveralthings,suchaswidget
naming,instantiation,extension,DOMeventbubbling,andsoon.
TheDocBlockstandardtouchesontherequirementsandconventionsfortheadditionof
inlinecodedocumentation.TheideaistounifytheusageofcodeDocBlocksforallfiles
regardlessoftheprogramminglanguageinuse.However,aDocBlockstandardforthat
particularlanguagemayoverrideit.
TheJavaScriptDocBlockstandardrelatestotheJavaScriptcodefilesandtheirinline
documentation.ItisasubsetofGoogleJavaScriptStyleGuideandJSDoc,whichcanbe
foundathttp://usejsdoc.org.
TheLESScodingstandarddefinestheformattingandcodingstylewhenworkingwith
LESSandCSSfiles.
Summary
Inthischapter,wetookalookatComposer,whichisoneofthefirstthingsthatwewill
interactwithwheninstallingMagento.Wethenmovedontoservicecontractsasoneof
thestrongestMagentoarchitecturalparts,whichturnedouttobegoodoldPHPinterfaces
inuse.Further,wecoveredsomebitsabouttheMagentocodegenerationfeature.Thus,
wehaveabasicknowledgeoftheFactoryandProxyclasses.Wethenhadalookatthe
vardirectoryandexploreditsrole,especiallyduringdevelopment.Finally,wetouched
uponthecodingstandardsusedinMagento.
Inthenextchapter,wewilldiscussthedependencyinjection,whichisoneofthemost
importantarchitecturalpartsofMagento.
Chapter4.ModelsandCollections
Likemostmodernframeworksandplatforms,thesedaysMagentoembracesanObject
RelationalMapping(ORM)approachoverrawSQLqueries.Thoughtheunderlying
mechanismstillcomesdowntoSQL,wearenowdealingstrictlywithobjects.Thismakes
ourapplicationcodemorereadable,manageable,andisolatedfromvendor-specificSQL
differences.Model,resource,andcollectionarethreetypesofclassesworkingtogetherto
allowusfullentitydatamanagement,fromloading,saving,deleting,andlistingentities.
ThemajorityofourdataaccessandmanagementwillbedoneviaPHPclassescalled
Magentomodels.Modelsthemselvesdon’tcontainanycodeforcommunicatingwiththe
database.
ThedatabasecommunicationpartisdecoupledintoitsownPHPclasscalledresource
class.Eachmodelisthenassignedaresourceclass.Callingload,save,ordelete
methodsonmodelsgetdelegatedtoresourceclasses,astheyaretheonestoactuallyread,
write,anddeletedatafromthedatabase.Theoretically,withenoughknowledge,itis
possibletowritenewresourceclassesforvariousdatabasevendors.
Nexttothemodelandresourceclasses,wehavecollectionclasses.Wecanthinkofa
collectionasanarrayofindividualmodelinstances.Onabaselevel,collectionsextend
fromthe\Magento\Framework\Data\Collectionclass,whichimplements
\IteratorAggregateand\CountablefromStandardPHPLibrary(SPL)andafew
otherMagento-specificclasses.
Moreoftenthannot,welookatmodelandresourceasasingleunifiedthing,thussimply
callingitamodel.Magentodealswithtwotypesofmodels,whichwemightcategorizeas
simpleandEAVmodels.
Inthischapter,wewillcoverthefollowingtopics:
Creatingaminiaturemodule
Creatingasimplemodel
TheEAVmodel
Understandingtheflowofschemaanddatascripts
Creatinganinstallschemascript(InstallSchema.php)
Creatinganupgradeschemascript(UpgradeSchema.php)
Creatinganinstalldatascript(InstallData.php)
Creatinganupgradedatascript(UpgradeData.php)
EntityCRUDactions
Managingcollections
Creatingaminiaturemodule
Forthepurposeofthischapter,wewillcreateaminiaturemodulecalled
Foggyline_Office.
Themodulewillhavetwoentitiesdefinedasfollows:
Department:asimplemodelwiththefollowingfields:
entity_id:primarykey
name:nameofdepartment,stringvalue
Employee:anEAVmodelwiththefollowingfieldsandattributes:
Fields:
entity_id:primarykey
department_id:foreignkey,pointingtoDepartment.entity_id
email:uniquee-mailofanemployee,stringvalue
first_name:firstnameofanemployee,stringvalue
last_name:lastnameofanemployee,stringvalue
Attributes:
service_years:employee’syearsofservice,integervalue
dob:employee’sdateofbirth,date-timevalue
salary–monthlysalary,decimalvalue
vat_number:VATnumber,(short)stringvalue
note:possiblenoteonemployee,(long)stringvalue
Everymodulestartswiththeregistration.phpandmodule.xmlfiles.Forthepurposeof
ourchaptermodule,let’screatetheapp/code/Foggyline/Office/registration.phpfile
withcontentasfollows:
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Foggyline_Office',
__DIR__
);
Theregistration.phpfileissortofanentrypointtoourmodule.
Nowlet’screatetheapp/code/Foggyline/Office/etc/module.xmlfilewiththe
followingcontent:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/
etc/module.xsd">
<modulename="Foggyline_Office"setup_version="1.0.0">
<sequence>
<modulename="Magento_Eav"/>
</sequence>
</module>
</config>
Wewillgetintomoredetailsaboutthestructureofthemodule.xmlfileinlaterchapters.
Rightnow,wewillonlyfocusonthesetup_versionattributeandmoduleelementwithin
sequence.
Thevalueofsetup_versionisimportantbecausewemightuseitwithinourschema
installscript(InstallSchema.php)files,effectivelyturningtheinstallscriptintoan
updatescript,aswewillshowsoon.
ThesequenceelementisMagento’swayofsettingdependenciesforourmodule.Given
thatourmodulewillmakeuseofEAVentities,welistMagento_Eavasadependency.
Creatingasimplemodel
TheDepartmententity,asperrequirements,ismodeledasasimplemodel.Wepreviously
mentionedthatwheneverwetalkaboutmodels,weimplicitlythinkofmodelclass,
resourceclass,andcollectionclassformingoneunit.
Let’sstartbyfirstcreatingamodelclass,(partially)definedunderthe
app/code/Foggyline/Office/Model/Department.phpfileasfollows:
namespaceFoggyline\Office\Model;
classDepartmentextends\Magento\Framework\Model\AbstractModel
{
protectedfunction_construct()
{
$this->_init('Foggyline\Office\Model\ResourceModel\Department');
}
}
Allthatishappeninghereisthatweareextendingfromthe
\Magento\Framework\Model\AbstractModelclass,andtriggeringthe$this->_init
methodwithin_constructpassingitourresourceclass.
TheAbstractModelfurtherextends\Magento\Framework\Object.Thefactthatourmodel
classultimatelyextendsfromObjectmeansthatwedonothavetodefineapropertyname
onourmodelclass.WhatObjectdoesforusisthatitenablesustoget,set,unset,and
checkforavalueexistenceonpropertiesmagically.Togiveamorerobustexamplethan
name,imagineourentityhasapropertycalledemployee_average_salaryinthe
followingcode:
$department->getData('employee_average_salary');
$department->getEmployeeAverageSalary();
$department->setData('employee_average_salary','theValue');
$department->setEmployeeAverageSalary('theValue');
$department->unsetData('employee_average_salary');
$department->unsEmployeeAverageSalary();
$department->hasData('employee_average_salary');
$department->hasEmployeeAverageSalary();
ThereasonwhythisworksisduetoObjectimplementingthesetData,unsetData,
getData,andmagic__callmethods.Thebeautyofthemagic__callmethod
implementationisthatitunderstandsmethodcallslikegetEmployeeAverageSalary,
setEmployeeAverageSalary,unsEmployeeAverageSalary,and
hasEmployeeAverageSalaryeveniftheydonotexistontheModelclass.However,ifwe
choosetoimplementsomeofthesemethodswithinourModelclass,wearefreetodoso
andMagentowillpickitupwhenwecallit.
ThisisanimportantaspectofMagento,sometimesconfusingtonewcomers.
Oncewehaveamodelclassinplace,wecreateamodelresourceclass,(partially)defined
undertheapp/code/Foggyline/Office/Model/ResourceModel/Department.phpfileas
follows:
namespaceFoggyline\Office\Model\ResourceModel;
classDepartmentextends
\Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
protectedfunction_construct()
{
$this->_init('foggyline_office_department','entity_id');
}
}
Ourresourceclassthatextendsfrom
\Magento\Framework\Model\ResourceModel\Db\AbstractDbtriggersthe$this->_init
methodcallwithin_construct.$this->_initacceptstwoparameters.Thefirst
parameteristhetablenamefoggyline_office_department,whereourmodelwillpersist
itsdata.Thesecondparameteristheprimarycolumnnameentity_idwithinthattable.
AbstractDbfurtherextends
Magento\Framework\Model\ResourceModel\AbstractResource.
Note
Theresourceclassisthekeytocommunicatingtothedatabase.Allittakesisforusto
namethetableanditsprimarykeyandourmodelscansave,delete,andupdateentities.
Finally,wecreateourcollectionclass,(partially)definedunderthe
app/code/Foggyline/Office/Model/ResourceModel/Department/Collection.phpfile
asfollows:
namespaceFoggyline\Office\Model\ResourceModel\Department;
classCollectionextends\Magento\Framework\Model\ResourceModel
\Db\Collection\AbstractCollection
{
protectedfunction_construct()
{
$this->_init(
'Foggyline\Office\Model\Department',
'Foggyline\Office\Model\ResourceModel\Department'
);
}
}
Thecollectionclassextendsfrom
\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollectionand,
similartothemodelandresourceclasses,doesa$this->_initmethodcallwithin
_construct.Thistime,_initacceptstwoparameters.Thefirstparameteristhefull
modelclassnameFoggyline\Office\Model\Department,andthesecondparameteristhe
fullresourceclassnameFoggyline\Office\Model\ResourceModel\Department.
AbstractCollectionimplements
Magento\Framework\App\ResourceConnection\SourceProviderInterface,andextends
\Magento\Framework\Data\Collection\AbstractDb.AbstractDbfurtherextends
\Magento\Framework\Data\Collection.
Itisworthtakingsometimetostudytheinnersofthesecollectionclasses,asthisisour
go-toplaceforwheneverweneedtodealwithfetchingalistofentitiesthatmatchcertain
searchcriteria.
CreatinganEAVmodel
TheEmployeeentity,asperrequirements,ismodeledasanEAVmodel.
Let’sstartbyfirstcreatinganEAVmodelclass,(partially)definedunderthe
app/code/Foggyline/Office/Model/Employee.phpfileasfollows:
namespaceFoggyline\Office\Model;
classEmployeeextends\Magento\Framework\Model\AbstractModel
{
constENTITY='foggyline_office_employee';
publicfunction_construct()
{
$this->_init('Foggyline\Office\Model\ResourceModel\Employee');
}
}
Here,weareextendingfromthe\Magento\Framework\Model\AbstractModelclass,
whichisthesameaswiththesimplemodelpreviouslydescribed.Theonlydifferencehere
isthatwehaveanENTITYconstantdefined,butthisismerelysyntacticalsugarforlater
on;itbearsnomeaningfortheactualmodelclass.
Next,wecreateanEAVmodelresourceclass,(partially)definedunderthe
app/code/Foggyline/Office/Model/ResourceModel/Employee.phpfileasfollows:
namespaceFoggyline\Office\Model\ResourceModel;
classEmployeeextends\Magento\Eav\Model\Entity\AbstractEntity
{
protectedfunction_construct()
{
$this->_read='foggyline_office_employee_read';
$this->_write='foggyline_office_employee_write';
}
publicfunctiongetEntityType()
{
if(empty($this->_type)){
$this->setType(\Foggyline\Office\Model\Employee::ENTITY);
}
returnparent::getEntityType();
}
}
Ourresourceclassextendsfrom\Magento\Eav\Model\Entity\AbstractEntity,and
setsthe$this->_read,$this->_writeclasspropertiesthrough_construct.Theseare
freelyassignedtowhatevervaluewewant,preferablyfollowingthenamingpatternofour
module.ThereadandwriteconnectionsneedtobenamedorelseMagentoproducesan
errorwhenusingourentities.
ThegetEntityTypemethodinternallysetsthe_typevalueto
\Foggyline\Office\Model\Employee::ENTITY,whichisthestring
foggyline_office_employee.Thissamevalueiswhat’sstoredintheentity_type_code
columnwithintheeav_entity_typetable.Atthispoint,thereisnosuchentryinthe
eav_entity_typetable.Thisisbecausetheinstallschemascriptwillbecreatingone,as
wewillbedemonstratingsoon.
Finally,wecreateourcollectionclass,(partially)definedunderthe
app/code/Foggyline/Office/Model/ResourceModel/Employee/Collection.phpfileas
follows:
namespaceFoggyline\Office\Model\ResourceModel\Employee;
classCollectionextends
\Magento\Eav\Model\Entity\Collection\AbstractCollection
{
protectedfunction_construct()
{
$this->_init('Foggyline\Office\Model\Employee',
'Foggyline\Office\Model\ResourceModel\Employee');
}
}
Thecollectionclassextendsfrom
\Magento\Eav\Model\Entity\Collection\AbstractCollectionand,similartothe
modelclass,doesa$this->_initmethodcallwithin_construct._initacceptstwo
parameters:thefullmodelclassnameFoggyline\Office\Model\Employee,andthefull
resourceclassnameFoggyline\Office\Model\ResourceModel\Employee.
AbstractCollectionhasthesameparenttreeasthesimplemodelcollectionclass,buton
itsownitimplementsalotofEAVcollection-specificmethodslike
addAttributeToFilter,addAttributeToSelect,addAttributeToSort,andsoon.
Note
Aswecansee,EAVmodelslookalotlikesimplemodels.Thedifferenceliesmostlyin
theresourceclassandcollectionclassimplementationsandtheirfirstlevelparent
classes.However,weneedtokeepinmindthattheexamplegivenhereisthesimplestone
possible.Ifwelookattheeav_entity_typetableinthedatabase,wecanseethatother
entitytypesmakeuseofattribute_model,entity_attribute_collection,
increment_model,andsoon.Thesearealladvancedpropertieswecandefinealongside
ourEAVmodelmakingitclosertotheimplementationofthecatalog_productentity
type,whichisprobablythemostrobustoneinMagento.ThistypeofadvancedEAV
usageisoutofthescopeofthisbookasitisprobablyworthabookonitsown.
NowthatwehavesimpleandEAVmodelsinplace,itistimetolookintoinstallingthe
necessarydatabaseschemaandpossiblypre-fillitwithsomedata.Thisisdonethrough
schemaanddatascripts.
Understandingtheflowofschemaand
datascripts
Simplyput,theroleoftheschemascriptsistocreateadatabasestructuresupportingyour
modulelogic.Forexample,creatingatablewhereourentitieswouldpersisttheirdata.
Theroleofthedatascriptsistomanagethedatawithinexistingtables,usuallyintheform
ofaddingsomesampledataduringmoduleinstallation.
Ifwelookafewstepsback,wecannoticehowschema_versionanddata_versionfrom
thedatabasematchthesetup_versionnumberfromourmodule.xmlfile.Theyallimply
thesamething.Ifweweretonowchangethesetup_versionnumberinourmodule.xml
fileandrunthephpbin/magentosetup:upgradeconsolecommandagain,ourdatabase
schema_versionanddata_versionwouldgetupdatedtothisnewversionnumber.
Thisisdonethroughmodule’sinstallandupgradescripts.Ifwetakeaquicklookatthe
setup/src/Magento/Setup/Model/Installer.phpfile,wecanseeafunction,
getSchemaDataHandler,withcontentasfollows:
privatefunctiongetSchemaDataHandler($moduleName,$type)
{
$className=str_replace('_','\\',$moduleName).'\Setup';
switch($type){
case'schema-install':
$className.='\InstallSchema';
$interface=self::SCHEMA_INSTALL;
break;
case'schema-upgrade':
$className.='\UpgradeSchema';
$interface=self::SCHEMA_UPGRADE;
break;
case'schema-recurring':
$className.='\Recurring';
$interface=self::SCHEMA_INSTALL;
break;
case'data-install':
$className.='\InstallData';
$interface=self::DATA_INSTALL;
break;
case'data-upgrade':
$className.='\UpgradeData';
$interface=self::DATA_UPGRADE;
break;
default:
thrownew\Magento\Setup\Exception("$classNamedoesnot
exist");
}
return$this->createSchemaDataHandler($className,$interface);
}
ThisiswhattellsMagentowhichclassestopickupandrunfromtheindividualmodule
Setupdirectory.WewillignoretheRecurringcaseforthemoment,asonlythe
Magento_Indexermoduleusesit.
Forthefirsttime,werunphpbin/magentosetup:upgradeagainstourmodule;whileit
stillhasnoentriesunderthesetup_moduletable,Magentowillexecutethefileswithinthe
moduleSetupfolderinfollowingorder:
InstallSchema.php
UpgradeSchema.php
InstallData.php
UpgradeData.php
Noticethatthisisthesameorder,toptobottom,asinthegetSchemaDataHandlermethod.
Everysubsequentuppermoduleversionnumberchange,followedbytheconsolephp
bin/magentosetup:upgradecommand,wouldresultinthefollowingfilesbeingrunin
theorderaslisted:
UpgradeSchema.php
UpgradeData.php
Additionally,Magentowouldrecordtheuppedversionnumberunderthesetup_module
database.Magentowillonlytriggerinstallorupgradescriptswhentheversionnumberin
thedatabaseislessthantheversionnumberinthemodule.xmlfile.
Tip
Wearenotrequiredtoalwaysprovidetheseinstallorupgradescripts,ifever.Theyare
onlyneededwhenweneedtoaddoreditexistingtablesorentriesinadatabase.
Ifwelookcarefullyattheimplementationoftheinstallandupdatemethodswithinthe
appropriatescripts,wecanseetheybothacceptModuleContextInterface$contextasa
secondparameter.Sinceupgradescriptsaretheonestriggeringoneveryuppedversion
number,wecanuse$context->getVersion()totargetchangesspecifictothemodule
version.
Creatinganinstallschemascript
(InstallSchema.php)
Nowthatweunderstandtheflowofschemaanddatascriptsandtheirrelationtothe
moduleversionnumber,letusgoaheadandstartassemblingourInstallSchema.Westart
bydefiningtheapp/code/Foggyline/Office/Setup/InstallSchema.phpfilewith
(partial)contentasfollows:
namespaceFoggyline\Office\Setup;
useMagento\Framework\Setup\InstallSchemaInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\SchemaSetupInterface;
classInstallSchemaimplementsInstallSchemaInterface
{
publicfunctioninstall(SchemaSetupInterface$setup,
ModuleContextInterface$context)
{
$setup->startSetup();
/*#snippet1*/
$setup->endSetup();
}
}
InstallSchemaconformstoInstallSchemaInterface,whichrequiresthe
implementationoftheinstallmethodthatacceptstwoparametersoftype
SchemaSetupInterfaceandModuleContextInterface.
Theinstallmethodisallthatisrequiredhere.Withinthismethod,wewouldaddany
relevantcodewemighthavetocreatethetablesandcolumnsweneed.
Lookingthroughthecodebase,wecanseethatMagento\Setup\Module\Setupistheone
extending\Magento\Framework\Module\Setupandimplementing
SchemaSetupInterface.Thetwomethodsseenintheprecedingcode,startSetupand
endSetup,areusedtorunadditionalenvironmentsetupbeforeandafterourcode.
Goingfurther,let’sreplacethe/*#snippet1*/bitwithcodethatwillcreateour
Departmentmodelentitytableasfollows:
$table=$setup->getConnection()
->newTable($setup->getTable('foggyline_office_department'))
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity'=>true,'unsigned'=>true,'nullable'=>false,
'primary'=>true],
'EntityID'
)
->addColumn(
'name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'Name'
)
->setComment('FoggylineOfficeDepartmentTable');
$setup->getConnection()->createTable($table);
/*#snippet2*/
Here,weareinstructingMagentotocreateatablenamedfoggyline_office_department,
addentity_idandnamecolumnstoit,andsetthecommentonthetable.Assumingwe
areusingtheMySQLserver,whencodeexecutes,thefollowingSQLgetsexecutedinthe
database:
CREATETABLE'foggyline_office_department'(
'entity_id'int(10)unsignedNOTNULLAUTO_INCREMENTCOMMENT'EntityID',
'name'varchar(64)DEFAULTNULLCOMMENT'Name',
PRIMARYKEY('entity_id')
)ENGINE=InnoDBAUTO_INCREMENT=3DEFAULTCHARSET=utf8COMMENT='Foggyline
OfficeDepartmentTable';
TheaddColumnmethodisthemostinterestingonehere.Ittakesfiveparameters,from
columnname,columndatatype,columnlength,arrayofadditionaloptions,andcolumn
description.However,onlycolumnnameandcolumndatatypearemandatory!Accepted
columndatatypescanbefoundundertheMagento\Framework\DB\Ddl\Tableclass,and
goasfollows:
booleansmallintintegerbigint
floatnumericdecimaldate
timestampdatetimetextblob
varbinary
Anadditionaloptionsarraymightcontainsomeofthefollowingkeys:unsigned,
precision,scale,unsigned,default,nullable,primary,identity,auto_increment.
HavinggainedinsightintotheaddColumnmethod,let’sgoaheadandcreatethe
foggyline_office_employee_entitytablefortheEmployeeentityaswell.Wedosoby
replacingthe/*#snippet2*/bitfromtheprecedingcodewiththefollowingcode:
$employeeEntity=\Foggyline\Office\Model\Employee::ENTITY;
$table=$setup->getConnection()
->newTable($setup->getTable($employeeEntity.'_entity'))
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity'=>true,'unsigned'=>true,'nullable'=>false,
'primary'=>true],
'EntityID'
)
->addColumn(
'department_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['unsigned'=>true,'nullable'=>false],
'DepartmentId'
)
->addColumn(
'email',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'Email'
)
->addColumn(
'first_name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'FirstName'
)
->addColumn(
'last_name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'LastName'
)
->setComment('FoggylineOfficeEmployeeTable');
$setup->getConnection()->createTable($table);
/*#snippet3*/
Followinggooddatabasedesignpractices,wemightnoticeonethinghere.Ifweagree
thateveryemployeecanbeassignedasingledepartment,weshouldaddaforeignkeyto
thistable’sdepartment_idcolumn.Forthemoment,wewillpurposelyskipthisbit,aswe
wanttodemonstratethisthroughtheupdateschemascriptlateron.
EAVmodelsscattertheirdataacrossseveraltables,threeataminimum.Thetable
foggyline_office_employee_entitythatwejustcreatedisoneofthem.Theotheroneis
thecoreMagentoeav_attributetable.Thethirdtableisnotasingletable,ratheralistof
multipletables;oneforeachEAVtype.Thesetablesaretheresultofourinstallscript.
InformationstoredwithinthecoreMagentoeav_attributetableisnotthevalueofan
attributeoranythinglikeit;informationstoredthereisanattribute’smetadata.Sohow
doesMagentoknowaboutourEmployeeattributes(service_years,dob,salary,
vat_number,note)?Itdoesnot;notyet.Weneedtoaddtheattributesintothattable
ourselves.Wewilldosolateron,aswedemonstratetheInstallData.
DependingontheEAVattributedatatype,weneedtocreatethefollowingtables:
foggyline_office_employee_entity_datetime
foggyline_office_employee_entity_decimal
foggyline_office_employee_entity_int
foggyline_office_employee_entity_text
foggyline_office_employee_entity_varchar
Thenamesoftheseattributevaluetablescomefromasimpleformula,whichsays{name
oftheentitytable}+{_}+{eav_attribute.backend_typevalue}.Ifwelookatthesalary
attribute,weneedittobeadecimalvalue,thusitwillgetstoredin
foggyline_office_employee_entity_decimal.
Giventhechunkinessofcodebehinddefiningattributevaluetables,wewillfocusonlyon
asingle,decimaltypetable.Wedefineitbyreplacing/*#snippet3*/fromthe
precedingcodewiththefollowingbit:
$table=$setup->getConnection()
->newTable($setup->getTable($employeeEntity.'_entity_decimal'))
->addColumn(
'value_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity'=>true,'nullable'=>false,'primary'=>true],
'ValueID'
)
->addColumn(
'attribute_id',
\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
null,
['unsigned'=>true,'nullable'=>false,'default'=>'0'],
'AttributeID'
)
->addColumn(
'store_id',
\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
null,
['unsigned'=>true,'nullable'=>false,'default'=>'0'],
'StoreID'
)
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['unsigned'=>true,'nullable'=>false,'default'=>'0'],
'EntityID'
)
->addColumn(
'value',
\Magento\Framework\DB\Ddl\Table::TYPE_DECIMAL,
'12,4',
[],
'Value'
)
//->addIndex
//->addForeignKey
->setComment('EmployeeDecimalAttributeBackendTable');
$setup->getConnection()->createTable($table);
Noticethe//->addIndexpartwithincodeabove.Letsreplaceitwiththefollowingbit.
->addIndex(
$setup->getIdxName(
$employeeEntity.'_entity_decimal',
['entity_id','attribute_id','store_id'],
\Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE
),
['entity_id','attribute_id','store_id'],
['type'=>
\Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE]
)
->addIndex(
$setup->getIdxName($employeeEntity.'_entity_decimal',['store_id']),
['store_id']
)
->addIndex(
$setup->getIdxName($employeeEntity.'_entity_decimal',
['attribute_id']),
['attribute_id']
)
Theprecedingcodeaddsthreeindexesonthe
foggyline_office_employee_entity_decimaltable,resultinginaSQLasfollows:
UNIQUEKEY
'FOGGYLINE_OFFICE_EMPLOYEE_ENTT_DEC_ENTT_ID_ATTR_ID_STORE_ID'
('entity_id','attribute_id','store_id')
KEY'FOGGYLINE_OFFICE_EMPLOYEE_ENTITY_DECIMAL_STORE_ID'('store_id')
KEY'FOGGYLINE_OFFICE_EMPLOYEE_ENTITY_DECIMAL_ATTRIBUTE_ID'
('attribute_id')
Similarly,wereplacethe//->addForeignKeypartfromtheprecedingcodewiththe
followingbit:
->addForeignKey(
$setup->getFkName(
$employeeEntity.'_entity_decimal',
'attribute_id',
'eav_attribute',
'attribute_id'
),
'attribute_id',
$setup->getTable('eav_attribute'),
'attribute_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)
->addForeignKey(
$setup->getFkName(
$employeeEntity.'_entity_decimal',
'entity_id',
$employeeEntity.'_entity',
'entity_id'
),
'entity_id',
$setup->getTable($employeeEntity.'_entity'),
'entity_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)
->addForeignKey(
$setup->getFkName($employeeEntity.'_entity_decimal','store_id',
'store','store_id'),
'store_id',
$setup->getTable('store'),
'store_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)
Theprecedingcodeaddsforeignkeyrelationsintothe
foggyline_office_employee_entity_decimaltable,resultinginaSQLasfollows:
CONSTRAINT'FK_D17982EDA1846BAA1F40E30694993801'FOREIGNKEY
('entity_id')REFERENCES'foggyline_office_employee_entity'
('entity_id')ONDELETECASCADE,
CONSTRAINT
'FOGGYLINE_OFFICE_EMPLOYEE_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID'
FOREIGNKEY('store_id')REFERENCES'store'('store_id')ONDELETE
CASCADE,
CONSTRAINT
'FOGGYLINE_OFFICE_EMPLOYEE_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID'FOREIGN
KEY('attribute_id')REFERENCES'eav_attribute'('attribute_id')ON
DELETECASCADE
Noticehowweaddedthestore_idcolumntoourEAVattributevaluetables.Thoughour
exampleswon’tfinduseofit,itisagoodpracticetousestore_idwithyourEAVentities
toscopethedataforapossiblemulti-storesetup.Toclarifyfurther,imaginewehada
multi-storesetup,andwithEAVattributetablessetupliketheprecedingone,wewould
beabletostoreadifferentattributevalueforeachstore,sincetheuniqueentryinthetable
isdefinedasacombinationofentity_id,attribute_id,andstore_idcolumns.
Tip
Forthereasonsofperformanceanddataintegrity,itisimportanttodefineindexesand
foreignkeyaspergooddatabasedesignpractice.WecandosowithinInstallSchema
whendefiningnewtables.
Creatinganupgradeschemascript
(UpgradeSchema.php)
Duringthefirst-timemoduleinstall,anupgradeschemaiswhatgetsrunimmediatelyafter
aninstallschema.Wedefineupgradeschemawithinthe
app/code/Foggyline/Office/Setup/UpgradeSchema.phpfilewith(partial)contentas
follows:
namespaceFoggyline\Office\Setup;
useMagento\Framework\Setup\UpgradeSchemaInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\SchemaSetupInterface;
classUpgradeSchemaimplementsUpgradeSchemaInterface
{
publicfunctionupgrade(SchemaSetupInterface$setup,
ModuleContextInterface$context)
{
$setup->startSetup();
/*#snippet1*/
$setup->endSetup();
}
}
UpgradeSchemaconformstoUpgradeSchemaInterface,whichrequiresthe
implementationoftheupgrademethodthatacceptstwoparametersoftype
SchemaSetupInterfaceandModuleContextInterface.
ThisisquitesimilartoInstallSchemaInterface,exceptthemethodname.Theupdate
methodisrunwhenthisschemagetstriggered.Withinthismethod,wewouldaddany
relevantcodewemightwanttoexecute.
Goingfurther,let’sreplacethe/*#snippet1*/partfromtheprecedingcodewiththe
followingcode:
$employeeEntityTable=\Foggyline\Office\Model\Employee::ENTITY.'_entity';
$departmentEntityTable='foggyline_office_department';
$setup->getConnection()
->addForeignKey(
$setup->getFkName($employeeEntityTable,'department_id',
$departmentEntityTable,'entity_id'),
$setup->getTable($employeeEntityTable),
'department_id',
$setup->getTable($departmentEntityTable),
'entity_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
);
Here,weareinstructingMagentotocreateaforeignkeyonthe
foggyline_office_employee_entitytable,morepreciselyonitsdepartment_id
column,pointingtothefoggyline_office_departmenttableanditsentity_idcolumn.
Creatinganinstalldatascript
(InstallData.php)
Aninstalldatascriptiswhatgetsrunimmediatelyafterupgradeschema.Wedefineinstall
dataschemawithintheapp/code/Foggyline/Office/Setup/InstallData.phpfilewith
(partial)contentasfollows:
namespaceFoggyline\Office\Setup;
useMagento\Framework\Setup\InstallDataInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\ModuleDataSetupInterface;
classInstallDataimplementsInstallDataInterface
{
private$employeeSetupFactory;
publicfunction__construct(
\Foggyline\Office\Setup\EmployeeSetupFactory$employeeSetupFactory
)
{
$this->employeeSetupFactory=$employeeSetupFactory;
}
publicfunctioninstall(ModuleDataSetupInterface$setup,
ModuleContextInterface$context)
{
$setup->startSetup();
/*#snippet1*/
$setup->endSetup();
}
}
InstallDataconformstoInstallDataInterface,whichrequirestheimplementationof
theinstallmethodthatacceptstwoparametersoftypeModuleDataSetupInterfaceand
ModuleContextInterface.
Theinstallmethodisrunwhenthisscriptgetstriggered.Withinthismethod,wewould
addanyrelevantcodewemightwanttoexecute.
Goingfurther,let’sreplacethe/*#snippet1*/partfromtheprecedingcodewiththe
followingcode:
$employeeEntity=\Foggyline\Office\Model\Employee::ENTITY;
$employeeSetup=$this->employeeSetupFactory->create(['setup'=>$setup]);
$employeeSetup->installEntities();
$employeeSetup->addAttribute(
$employeeEntity,'service_years',['type'=>'int']
);
$employeeSetup->addAttribute(
$employeeEntity,'dob',['type'=>'datetime']
);
$employeeSetup->addAttribute(
$employeeEntity,'salary',['type'=>'decimal']
);
$employeeSetup->addAttribute(
$employeeEntity,'vat_number',['type'=>'varchar']
);
$employeeSetup->addAttribute(
$employeeEntity,'note',['type'=>'text']
);
UsingtheaddAttributemethodontheinstanceof
\Foggyline\Office\Setup\EmployeeSetupFactory,weareinstructingMagentotoadda
numberofattributes(service_years,dob,salary,vat_number,note)toitsentity.
WewillsoongettotheinnersofEmployeeSetupFactory,butrightnownoticethecallto
theaddAttributemethod.Withinthismethod,thereisacalltothe$this-
>attributeMapper->map($attr,$entityTypeId)method.attributeMapperconforms
toMagento\Eav\Model\Entity\Setup\PropertyMapperInterface,whichlookingat
vendor/magento/module-eav/etc/di.xmlhasapreferenceforthe
Magento\Eav\Model\Entity\Setup\PropertyMapper\Compositeclass,whichfurther
initializesthefollowingmapperclasses:
Magento\Eav\Model\Entity\Setup\PropertyMapper
Magento\Customer\Model\ResourceModel\Setup\PropertyMapper
Magento\Catalog\Model\ResourceModel\Setup\PropertyMapper
Magento\ConfigurableProduct\Model\ResourceModel\Setup\PropertyMapper
Sincewearedefiningourownentitytypes,themapperclasswearemostlyinterestedinis
Magento\Eav\Model\Entity\Setup\PropertyMapper.Aquicklookinsideofitrevealsthe
followingmappingarrayinthemapmethod:
[
'backend_model'=>'backend',
'backend_type'=>'type',
'backend_table'=>'table',
'frontend_model'=>'frontend',
'frontend_input'=>'input',
'frontend_label'=>'label',
'frontend_class'=>'frontend_class',
'source_model'=>'source',
'is_required'=>'required',
'is_user_defined'=>'user_defined',
'default_value'=>'default',
'is_unique'=>'unique',
'note'=>'note'
'is_global'=>'global'
]
Lookingattheprecedingarraykeysandvaluestringsgivesusaclueastowhatis
happening.Thekeystringsmatchthecolumnnamesintheeav_attributetable,while
thevaluestringsmatchthekeysofourarraypassedtotheaddAttributemethodwithin
InstallData.php.
Let’stakealookattheEmployeeSetupFactoryclasswithinthe
app/code/Foggyline/Office/Setup/EmployeeSetup.phpfile,(partially)definedas
follows:
namespaceFoggyline\Office\Setup;
useMagento\Eav\Setup\EavSetup;
classEmployeeSetupextendsEavSetup
{
publicfunctiongetDefaultEntities()
{
/*#snippet1*/
}
}
What’shappeninghereisthatweareextendingfromtheMagento\Eav\Setup\EavSetup
class,thuseffectivelytellingMagentoweareabouttocreateourownentity.Wedosoby
overridinggetDefaultEntities,replacing/*#snippet1*/withcontentasfollows:
$employeeEntity=\Foggyline\Office\Model\Employee::ENTITY;
$entities=[
$employeeEntity=>[
'entity_model'=>'Foggyline\Office\Model\ResourceModel\Employee',
'table'=>$employeeEntity.'_entity',
'attributes'=>[
'department_id'=>[
'type'=>'static',
],
'email'=>[
'type'=>'static',
],
'first_name'=>[
'type'=>'static',
],
'last_name'=>[
'type'=>'static',
],
],
],
];
return$entities;
ThegetDefaultEntitiesmethodreturnsanarrayofentitieswewanttoregisterwith
Magento.Withinour$entitiesarray,thekey$employeeEntitybecomesanentryinthe
eav_entity_typetable.Giventhatour$employeeEntityhasavalueof
foggyline_office_employee,runningthefollowingSQLqueryshouldyieldaresult:
SELECT*FROMeav_entity_typeWHEREentity_type_code=
"foggyline_office_employee";
Onlyahandfulofmetadatavaluesarerequiredtomakeournewentitytypefunctional.
Theentity_modelvalueshouldpointtoourEAVmodelresourceclass,notthemodel
class.ThetablevalueshouldequalthenameofourEAVentitytableinthedatabase.
Finally,theattributesarrayshouldlistanyattributewewantcreatedonthisentity.
Attributesandtheirmetadatagetcreatedintheeav_attributetable.
Ifwelookbackatallthosefoggyline_office_employee_entity_*attributevaluetables
wecreated,theyarenottheonesthatactuallycreateattributesorregisteranewentitytype
inMagento.Whatcreatesattributesandanewentitytypeisthearraywejustdefined
underthegetDefaultEntitiesmethod.OnceMagentocreatestheattributesandregisters
anewentitytype,itsimplyroutestheentitysaveprocesstoproperattributevaluetables
dependingonthetypeofattribute.
Creatinganupgradedatascript
(UpgradeData.php)
Theupgradedatascriptisthelastonetoexecute.Wewilluseittodemonstratethe
exampleofcreatingthesampleentriesforourDepartmentandEmployeeentities.
Westartbycreatingtheapp/code/Foggyline/Office/Setup/UpgradeData.phpfilewith
(partial)contentasfollows:
namespaceFoggyline\Office\Setup;
useMagento\Framework\Setup\UpgradeDataInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\ModuleDataSetupInterface;
classUpgradeDataimplementsUpgradeDataInterface
{
protected$departmentFactory;
protected$employeeFactory;
publicfunction__construct(
\Foggyline\Office\Model\DepartmentFactory$departmentFactory,
\Foggyline\Office\Model\EmployeeFactory$employeeFactory
)
{
$this->departmentFactory=$departmentFactory;
$this->employeeFactory=$employeeFactory;
}
publicfunctionupgrade(ModuleDataSetupInterface$setup,
ModuleContextInterface$context)
{
$setup->startSetup();
/*#snippet1*/
$setup->endSetup();
}
}
UpgradeDataconformstoUpgradeDataInterface,whichrequirestheimplementationof
theupgrademethodthatacceptstwoparametersoftypeModuleDataSetupInterfaceand
ModuleContextInterface.Wearefurtheraddingourown__constructmethodtowhich
wearepassingDepartmentFactoryandEmployeeFactory,aswewillbeusingthem
withintheupgrademethodasshownnext,byreplacing/*#snippet1*/withthe
followingcode:
$salesDepartment=$this->departmentFactory->create();
$salesDepartment->setName('Sales');
$salesDepartment->save();
$employee=$this->employeeFactory->create();
$employee->setDepartmentId($salesDepartment->getId());
$employee->setEmail('john@sales.loc');
$employee->setFirstName('John');
$employee->setLastName('Doe');
$employee->setServiceYears(3);
$employee->setDob('1983-03-28');
$employee->setSalary(3800.00);
$employee->setVatNumber('GB123456789');
$employee->setNote('JustsomenotesaboutJohn');
$employee->save();
Theprecedingcodecreatesaninstanceofthedepartmententityandthensavesit.An
instanceofemployeeisthencreatedandsaved,passingitthenewlycreateddepartment
IDandotherattributes.
Tip
Amoreconvenientandprofessional-lookingapproachforsavinganentitycouldbegiven
asfollows:
$employee->setDob('1983-03-28')
->setSalary(3800.00)
->setVatNumber('GB123456789')
->save();
Here,weareutilizingthefactthateachoftheentitysettermethodsreturns$this(an
instanceoftheentityobjectitself),sowecanchainthemethodcalls.
EntityCRUDactions
Uptothispoint,wehavelearnedhowtocreateasimplemodel,anEAVmodel,andinstall
andupgradetypesofschemaanddatascript.Now,letusseehowwecancreate,read,
updateanddeleteourentities,operationsthatarecommonlyreferredtoasCRUD.
Thoughthischapterisaboutmodels,collections,andrelatedthings,forthepurposeof
demonstration,let’smakeatinydetourintoroutesandcontrollers.Theideaistocreatea
simpleTestcontrollerwiththeCrudactionwecantriggerinthebrowserviaaURL.
WithinthisCrudaction,wewillthendumpourCRUD-relatedcode.
TomakeMagentorespondtotheURLwepunchintothebrowser,weneedtodefinethe
route.Wedosobycreatingtheapp/code/Foggyline/Office/etc/frontend/routes.xml
filewiththefollowingcontent:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<routerid="standard">
<routeid="foggyline_office"frontName="foggyline_office">
<modulename="Foggyline_Office"/>
</route>
</router>
</config>
RoutedefinitionrequiresauniqueIDandfrontNameattributevalues,whichinourcase
bothequalfoggyline_office.ThefrontNameattributevaluebecomesthepartofour
URLstructure.Simplyput,theURLformulaforhittingtheCrudactiongoeslike
{magento-base-url}/index.php/{routefrontName}/{controllername}/{actionname}.
Note
Forexample,ifourbaseURLwerehttp://shop.loc/,thefullURLwouldbe
http://shop.loc/index.php/foggyline_office/test/crud/.IfwehaveURLrewrites
turnedon,wecouldomittheindex.phppart.
Oncetheroutehasbeendefined,wecangoaheadandcreatetheTestcontroller,defined
intheapp/code/Foggyline/Office/Controller/Test.phpfilewith(partial)codeas
follows:
namespaceFoggyline\Office\Controller;
abstractclassTestextends\Magento\Framework\App\Action\Action
{
}
Thisreallyisthesimplestcontrollerwecouldhavedefined.Theonlythingworthnoting
hereisthatthecontrollerclassneedstobedefinedasabstractandextendthe
\Magento\Framework\App\Action\Actionclass.Controlleractionsliveoutsideofthe
controlleritselfandcanbefoundunderthesubdirectoryonthesamelevelandnamedas
controller.SinceourcontrolleriscalledTest,weplaceourCrudactionunderthe
app/code/Foggyline/Office/Controller/Test/Crud.phpfilewithcontentasfollows:
namespaceFoggyline\Office\Controller\Test;
classCrudextends\Foggyline\Office\Controller\Test
{
protected$employeeFactory;
protected$departmentFactory;
publicfunction__construct(
\Magento\Framework\App\Action\Context$context,
\Foggyline\Office\Model\EmployeeFactory$employeeFactory,
\Foggyline\Office\Model\DepartmentFactory$departmentFactory
)
{
$this->employeeFactory=$employeeFactory;
$this->departmentFactory=$departmentFactory;
returnparent::__construct($context);
}
publicfunctionexecute()
{
/*CRUDCodeHere*/
}
}
TheControlleractionclassisbasicallyjustanextensionofthecontrollerdefiningthe
executemethod.CodewithintheexecutemethodiswhatgetsrunwhenwehittheURL
inthebrowser.Additionally,wehavea__constructmethodtowhichwearepassingthe
EmployeeFactoryandDepartmentFactoryclasses,whichwewillsoonuseforourCRUD
examples.NotethatEmployeeFactoryandDepartmentFactoryarenotclassescreatedby
us.MagentowillautogeneratethemundertheDepartmentFactory.phpand
EmployeeFactory.phpfileswithinthevar/generation/Foggyline/Office/Modelfolder.
ThesearefactoryclassesforourEmployeeandDepartmentmodelclasses,generated
whenrequested.
Withthis,wefinishourlittledetourandfocusbackonourentities.
Creatingnewentities
Therearethreedifferentflavors,ifwemightcallthemthat,bywhichwecansetproperty
(fieldandattribute)valuesonourentity.Theyallleadtothesameresult.Thefollowing
fewcodesnippetscanbecopiedandpastedintoourCrudclassexecutemethodfor
testing,simplybyreplacing/*CRUDCodeHere*/withoneofthefollowingcode
snippets:
//Simplemodel,creatingnewentities,flavour#1
$department1=$this->departmentFactory->create();
$department1->setName('Finance');
$department1->save();
//Simplemodel,creatingnewentities,flavour#2
$department2=$this->departmentFactory->create();
$department2->setData('name','Research');
$department2->save();
//Simplemodel,creatingnewentities,flavour#3
$department3=$this->departmentFactory->create();
$department3->setData(['name'=>'Support']);
$department3->save();
Theflavour#1approachfromtheprecedingcodeisprobablythepreferredwayof
settingproperties,asitisusingthemagicmethodapproachwementionedpreviously.
Bothflavour#2andflavour#3usethesetDatamethod,justinaslightlydifferent
manner.Allthreeexamplesshouldyieldthesameresultoncethesavemethodiscalledon
anobjectinstance.
Nowthatweknowhowtosavethesimplemodel,let’stakeaquicklookatdoingthesame
withtheEAVmodel.Thefollowingareanalogouscodesnippets:
//EAVmodel,creatingnewentities,flavour#1
$employee1=$this->employeeFactory->create();
$employee1->setDepartment_id($department1->getId());
$employee1->setEmail('goran@mail.loc');
$employee1->setFirstName('Goran');
$employee1->setLastName('Gorvat');
$employee1->setServiceYears(3);
$employee1->setDob('1984-04-18');
$employee1->setSalary(3800.00);
$employee1->setVatNumber('GB123451234');
$employee1->setNote('Note#1');
$employee1->save();
//EAVmodel,creatingnewentities,flavour#2
$employee2=$this->employeeFactory->create();
$employee2->setData('department_id',$department2->getId());
$employee2->setData('email','marko@mail.loc');
$employee2->setData('first_name','Marko');
$employee2->setData('last_name','Tunukovic');
$employee2->setData('service_years',3);
$employee2->setData('dob','1984-04-18');
$employee2->setData('salary',3800.00);
$employee2->setData('vat_number','GB123451234');
$employee2->setData('note','Note#2');
$employee2->save();
//EAVmodel,creatingnewentities,flavour#3
$employee3=$this->employeeFactory->create();
$employee3->setData([
'department_id'=>$department3->getId(),
'email'=>'ivan@mail.loc',
'first_name'=>'Ivan',
'last_name'=>'Telebar',
'service_years'=>2,
'dob'=>'1986-08-22',
'salary'=>2400.00,
'vat_number'=>'GB123454321',
'note'=>'Note#3'
]);
$employee3->save();
Aswecansee,theEAVcodeforpersistingthedataisidenticaltothesimplemodel.There
isonethinghereworthnoting.TheEmployeeentityhasarelationdefinedtoward
department.Forgettingtospecifydepartment_idonanewemployeeentitysavewould
resultinanerrormessagesimilartothefollowing:
SQLSTATE[23000]:Integrityconstraintviolation:1452Cannotaddorupdate
achildrow:aforeignkeyconstraintfails
('magento'.'foggyline_office_employee_entity',CONSTRAINT
'FK_E2AEE8BF21518DFA8F02B4E95DC9F5AD'FOREIGNKEY('department_id')
REFERENCES'foggyline_office_department'('entity_id')ON),querywas:
INSERTINTO'foggyline_office_employee_entity'('email','first_name',
'last_name','entity_id')VALUES(?,?,?,?)
Magentosavesthesetypesoferrorsunderitsvar/reportdirectory.
Readingexistingentities
ReadinganentitybasedonaprovidedentityIDvaluecomesdowntoinstantiatingthe
entityandusingtheloadmethodtowhichwepasstheentityIDasshownnext:
//Simplemodel,readingexistingentities
$department=$this->departmentFactory->create();
$department->load(28);
/*
\Zend_Debug::dump($department->toArray());
array(2){
["entity_id"]=>string(2)"28"
["name"]=>string(8)"Research"
}
*/
ThereisnorealdifferencebetweenloadingthesimplemodelorEAVmodel,asshownin
thefollowingEAVmodelexample:
//EAVmodel,readingexistingentities
$employee=$this->employeeFactory->create();
$employee->load(25);
/*
\Zend_Debug::dump($employee->toArray());
array(10){
["entity_id"]=>string(2)"25"
["department_id"]=>string(2)"28"
["email"]=>string(14)"marko@mail.loc"
["first_name"]=>string(5)"Marko"
["last_name"]=>string(9)"Tunukovic"
["dob"]=>string(19)"1984-04-1800:00:00"
["note"]=>string(7)"Note#2"
["salary"]=>string(9)"3800.0000"
["service_years"]=>string(1)"3"
["vat_number"]=>string(11)"GB123451234"
}
*/
NoticehowtheEAVentityloadsallofitsfieldandattributevalues,whichisnotalways
thecasewhenweobtaintheentitythroughEAVcollection,aswewillshowlateron.
Updatingexistingentities
Updatingentitiescomesdowntousingtheloadmethodtoreadanexistingentity,resetits
value,andcallingthesavemethodintheend,likeshowninthefollowingexample:
$department=$this->departmentFactory->create();
$department->load(28);
$department->setName('Finance#2');
$department->save();
RegardlessoftheentitybeingthesimplemodeloranEAV,thecodeisthesame.
Deletingexistingentities
Callingthedeletemethodonaloadedentitywilldeletetheentityfromthedatabaseor
throwExceptionifitfails.Codetodeletetheentitylooksasfollows:
$employee=$this->employeeFactory->create();
$employee->load(25);
$employee->delete();
ThereisnodifferenceindeletingthesimpleandEAVentities.Weshouldalwaysuse
try/catchblockswhendeletingorsavingourentities.
Managingcollections
Let’sstartwithEAVmodelcollections.Wecaninstantiatethecollectioneitherthroughthe
entityfactoryclasslikefollows:
$collection=$this->employeeFactory->create()
->getCollection();
Orwecanuseobjectmanagertoinstantiatethecollectionasshownnext:
$collection=$this->_objectManager->create(
'Foggyline\Office\Model\ResourceModel\Employee\Collection's
);
Thereisalsoathirdway,whichmightbethepreferredone,butitrequiresustodefine
APIssowewillskipthatoneforthemoment.
Onceweinstantiatethecollectionobject,wecanloopthroughitanddosomevariable
dumpstoseethecontentonindividual$employeeentities,likeshownnext:
foreach($collectionas$employee){
\Zend_Debug::dump($employee->toArray(),'$employee');
}
Theprecedingwouldyieldresultslikethefollowing:
$employeearray(5){
["entity_id"]=>string(2)"24"
["department_id"]=>string(2)"27"
["email"]=>string(14)"goran@mail.loc"
["first_name"]=>string(5)"Goran"
["last_name"]=>string(6)"Gorvat"
}
Noticehowtheindividual$employeeonlyhasfieldsonit,nottheattributes.Let’ssee
whathappenswhenwewanttoextendourcollectionbyusingaddAttributeToSelectto
specifytheindividualattributestoaddtoit,likeshownnext:
$collection->addAttributeToSelect('salary')
->addAttributeToSelect('vat_number');
Theprecedingwouldyieldresultslikethefollowing:
$employeearray(7){
["entity_id"]=>string(2)"24"
["department_id"]=>string(2)"27"
["email"]=>string(14)"goran@mail.loc"
["first_name"]=>string(5)"Goran"
["last_name"]=>string(6)"Gorvat"
["salary"]=>string(9)"3800.0000"
["vat_number"]=>string(11)"GB123451234"
}
Thoughwearemakingprogress,imagineifwehadtensofattributes,andwewanteach
andeveryonetobeincludedintocollection.UsingaddAttributeToSelectnumerous
timeswouldmakeforclutteredcode.Whatwecandoispass'*'asaparameterto
addAttributeToSelectandhavecollectionpickupeveryattribute,asshownnext:
$collection->addAttributeToSelect('*');
Thiswouldyieldresultslikethefollowing:
$employeearray(10){
["entity_id"]=>string(2)"24"
["department_id"]=>string(2)"27"
["email"]=>string(14)"goran@mail.loc"
["first_name"]=>string(5)"Goran"
["last_name"]=>string(6)"Gorvat"
["dob"]=>string(19)"1984-04-1800:00:00"
["note"]=>string(7)"Note#1"
["salary"]=>string(9)"3800.0000"
["service_years"]=>string(1)"3"
["vat_number"]=>string(11)"GB123451234"
}
ThoughthePHPpartofthecodelooksseeminglysimple,what’shappeninginthe
backgroundontheSQLlayerisrelativelycomplex.ThoughMagentoexecutesseveral
SQLqueriespriortofetchingthefinalcollectionresult,let’sfocusonthelastthreequeries
asshownnext:
SELECTCOUNT(*)FROM'foggyline_office_employee_entity'AS'e'
SELECT'e'.*FROM'foggyline_office_employee_entity'AS'e'
SELECT
'foggyline_office_employee_entity_datetime'.'entity_id',
'foggyline_office_employee_entity_datetime'.'attribute_id',
'foggyline_office_employee_entity_datetime'.'value'
FROM'foggyline_office_employee_entity_datetime'
WHERE(entity_idIN(24,25,26))AND(attribute_idIN('349'))
UNIONALLSELECT
'foggyline_office_employee_entity_text'.'entity_id',
'foggyline_office_employee_entity_text'.'attribute_id',
'foggyline_office_employee_entity_text'.'value'
FROM'foggyline_office_employee_entity_text'
WHERE(entity_idIN(24,25,26))AND(attribute_idIN('352'))
UNIONALLSELECT
'foggyline_office_employee_entity_decimal'.'entity_id',
'foggyline_office_employee_entity_decimal'.'attribute_id',
'foggyline_office_employee_entity_decimal'.'value'
FROM'foggyline_office_employee_entity_decimal'
WHERE(entity_idIN(24,25,26))AND(attribute_idIN('350'))
UNIONALLSELECT
'foggyline_office_employee_entity_int'.'entity_id',
'foggyline_office_employee_entity_int'.'attribute_id',
'foggyline_office_employee_entity_int'.'value'
FROM'foggyline_office_employee_entity_int'
WHERE(entity_idIN(24,25,26))AND(attribute_idIN('348'))
UNIONALLSELECT
'foggyline_office_employee_entity_varchar'.'entity_id',
'foggyline_office_employee_entity_varchar'.'attribute_id',
'foggyline_office_employee_entity_varchar'.'value'
FROM'foggyline_office_employee_entity_varchar'
WHERE(entity_idIN(24,25,26))AND(attribute_idIN('351'))
Note
Beforeweproceedanyfurther,itisimportanttoknowthatthesequeriesarenotcopyand
pasteapplicable.Thereasonisthattheattribute_idvalueswillforsuredifferfrom
installationtoinstallation.Queriesgivenhereareforustogainahigh-levelunderstanding
ofwhatishappeninginthebackendontheSQLlayerwhenweuseMagentocollections
onthePHPapplicationlevel.
Thefirstqueryselectsimplycountsthenumberofentriesintheentitytable,andthen
passesthatinfototheapplicationlayer.Thesecondselectfetchesallentriesfrom
foggyline_office_employee_entity,thenpassesthatinfototheapplicationlayertouse
ittopassentityIDsinthethirdqueryaspartofentity_idIN(24,25,26).Secondand
thirdqueriesherecanbeprettyresourceintenseifwehavealargeamountofentriesinour
entityandEAVtables.Topreventpossibleperformancebottlenecks,weshouldalwaysuse
thesetPageSizeandsetCurPagemethodsoncollection,likeshownnext:
$collection->addAttributeToSelect('*')
->setPageSize(25)
->setCurPage(5);
ThiswouldresultinthefirstCOUNTquerystillbeingthesame,butthesecondquerywould
nowlooklikethefollowing:
SELECT'e'.*FROM'foggyline_office_employee_entity'AS'e'LIMIT25OFFSET
4
Thismakesforamuchsmaller,thusperformance-lighterdatasetifwehavethousandsor
tensofthousandsofentries.ThepointhereistoalwaysusesetPageSizeandsetCurPage.
Ifweneedtoworkwithareallylargeset,thenweneedtopagethroughit,orwalk
throughit.
Nowweknowhowtolimitthesizeoftheresultsetandfetchtheproperpage,let’ssee
howwecanfurtherfilterthesettoavoidoverusingPHPloopsforthesamepurpose.Thus
effectivelypassingthefilteringtothedatabaseandnottheapplicationlayer.Tofilterthe
EAVcollection,weuseitsaddAttributeToFiltermethod.
Let’sinstantiateacleannewcollectionlikeshownnext:
$collection=$this->_objectManager->create(
'Foggyline\Office\Model\ResourceModel\Employee\Collection'
);
$collection->addAttributeToSelect('*')
->setPageSize(25)
->setCurPage(1);
$collection->addAttributeToFilter('email',array('like'=>'%mail.loc%'))
->addAttributeToFilter('vat_number',array('like'=>'GB%'))
->addAttributeToFilter('salary',array('gt'=>2400))
->addAttributeToFilter('service_years',array('lt'=>10));
NoticethatwearenowusingtheaddAttributeToSelectandaddAttributeToFilter
methodsoncollection.Wehavealreadyseenthedatabaseimpactof
addAttributeToSelectonaSQLquery.WhataddAttributeToFilterdoesissomething
completelydifferent.
WiththeaddAttributeToFiltermethod,thecountquerynowgetstransformedintothe
followingSQLquery:
SELECTCOUNT(*)
FROM'foggyline_office_employee_entity'AS'e'
INNERJOIN'foggyline_office_employee_entity_varchar'AS'at_vat_number'
ON('at_vat_number'.'entity_id'='e'.'entity_id')AND
('at_vat_number'.'attribute_id'='351')
INNERJOIN'foggyline_office_employee_entity_decimal'AS'at_salary'
ON('at_salary'.'entity_id'='e'.'entity_id')AND
('at_salary'.'attribute_id'='350')
INNERJOIN'foggyline_office_employee_entity_int'AS'at_service_years'
ON('at_service_years'.'entity_id'='e'.'entity_id')AND
('at_service_years'.'attribute_id'='348')
WHERE('e'.'email'LIKE'%mail.loc%')AND(at_vat_number.valueLIKE'GB%')
AND(at_salary.value>2400)AND
(at_service_years.value<10)
Wecanseethatthisismuchmorecomplexthanthepreviouscountquery,nowwehave
INNERJOINsteppingin.NoticehowwehavefouraddAttributeToFiltermethodcalls
butonlythreeINNERJOIN.Thisisbecauseoneofthosefourcallsisfore-mail,whichis
notanattributebutafieldwithinthefoggyline_office_employee_entitytable.Thatis
whythereisnoneedforINNERJOINasthefieldisalreadythere.ThethreeINNERJOIN
thensimplymergetherequiredinfointothequeryinordertogettheselect.
Thesecondqueryalsobecomesmorerobust,asshownnext:
SELECT
'e'.*,
'at_vat_number'.'value'AS'vat_number',
'at_salary'.'value'AS'salary',
'at_service_years'.'value'AS'service_years'
FROM'foggyline_office_employee_entity'AS'e'
INNERJOIN'foggyline_office_employee_entity_varchar'AS'at_vat_number'
ON('at_vat_number'.'entity_id'='e'.'entity_id')AND
('at_vat_number'.'attribute_id'='351')
INNERJOIN'foggyline_office_employee_entity_decimal'AS'at_salary'
ON('at_salary'.'entity_id'='e'.'entity_id')AND
('at_salary'.'attribute_id'='350')
INNERJOIN'foggyline_office_employee_entity_int'AS'at_service_years'
ON('at_service_years'.'entity_id'='e'.'entity_id')AND
('at_service_years'.'attribute_id'='348')
WHERE('e'.'email'LIKE'%mail.loc%')AND(at_vat_number.valueLIKE'GB%')
AND(at_salary.value>2400)AND
(at_service_years.value<10)
LIMIT25
Here,wealsoseetheusageofINNERJOIN.WealsohavethreeandnotfourINNERJOIN,
becauseoneoftheconditionsisdoneagainstemail,whichisafield.Theresultofthe
queryisaflattenedpieceofrowswheretheattributesvat_number,salary,and
service_yearsarepresent.Wecanimaginetheperformanceimpactifwehaven’tused
setPageSizetolimittheresultset.
Finally,thethirdqueryisalsoaffectedandnowlookssimilartothefollowing:
SELECT
'foggyline_office_employee_entity_datetime'.'entity_id',
'foggyline_office_employee_entity_datetime'.'attribute_id',
'foggyline_office_employee_entity_datetime'.'value'
FROM'foggyline_office_employee_entity_datetime'
WHERE(entity_idIN(24,25))AND(attribute_idIN('349'))
UNIONALLSELECT
'foggyline_office_employee_entity_text'.'entity_id',
'foggyline_office_employee_entity_text'.'attribute_id',
'foggyline_office_employee_entity_text'.'value'
FROM'foggyline_office_employee_entity_text'
WHERE(entity_idIN(24,25))AND(attribute_idIN('352'))
NoticeherehowUNIONALLhasbeenreducedtoasingleoccurrencenow,thuseffectively
makingfortwoselects.Thisisbecausewehaveatotaloffiveattributes(service_years,
dob,salary,vat_number,note),andthreeofthemhavebeenpulledinthroughsecond
query.Outoftheprecedingthreequeriesdemonstrated,Magentobasicallypullsthe
collectiondatafromsecondandthirdquery.Thisseemslikeaprettyoptimizedand
scalablesolution,thoughweshouldreallygiveitsomethoughtontheproperuseof
setPageSize,addAttributeToSelect,andaddAttributeToFiltermethodswhen
creatingcollection.
Duringdevelopment,ifworkingwithcollectionsthathavelotofattributes,filters,and
possiblyafuturelargedataset,wemightwanttouseSQLloggingtorecordactualSQL
querieshittingthedatabaseserver.Thismighthelpusspotpossibleperformance
bottlenecksandreactontime,eitherbyaddingmorelimitingvaluestosetPageSizeor
addAttributeToSelect,orboth.
Intheprecedingexamples,theuseofaddAttributeToSelectresultsinANDconditionson
theSQLlayer.WhatifwewanttofiltercollectionusingORconditions?
addAttributeToSelectcanalsoresultinSQLORconditionsifthe$attributeparameter
isusedinthefollowingway:
$collection->addAttributeToFilter([
['attribute'=>'salary','gt'=>2400],
['attribute'=>'vat_number','like'=>'GB%']
]);
WithoutgoingintothedetailsofactualSQLqueriesthistime,itissufficetosaythatthey
arenearidenticaltothepreviousexamplewiththeANDconditionuseof
addAttributeToFilter.
UsingcollectionmethodslikeaddExpressionAttributeToSelect,groupByAttribute,
andaddAttributeToSort,collectionsofferfurthergradientfilteringandevenshiftsome
calculationsfromthePHPapplicationlayertotheSQLlayer.Gettingintotheinsandouts
ofthoseandothercollectionmethodsisbeyondthescopeofthischapter,andwould
probablyrequireabookonitsown.
Collectionfilters
LookingbackattheprecedingaddAttributeToFiltermethodcallexamples,questions
popoutastowherecanweseethelistofallavailablecollectionfilters.Ifwetakeaquick
lookinsidethevendor/magento/framework/DB/Adapter/Pdo/Mysql.phpfile,wecansee
themethodcalledprepareSqlCondition(partially)definedasfollows:
publicfunctionprepareSqlCondition($fieldName,$condition)
{
$conditionKeyMap=[
'eq'=>"{{fieldName}}=?",
'neq'=>"{{fieldName}}!=?",
'like'=>"{{fieldName}}LIKE?",
'nlike'=>"{{fieldName}}NOTLIKE?",
'in'=>"{{fieldName}}IN(?)",
'nin'=>"{{fieldName}}NOTIN(?)",
'is'=>"{{fieldName}}IS?",
'notnull'=>"{{fieldName}}ISNOTNULL",
'null'=>"{{fieldName}}ISNULL",
'gt'=>"{{fieldName}}>?",
'lt'=>"{{fieldName}}/*AJZELE*/<?",
'gteq'=>"{{fieldName}}>=?",
'lteq'=>"{{fieldName}}<=?",
'finset'=>"FIND_IN_SET(?,{{fieldName}})",
'regexp'=>"{{fieldName}}REGEXP?",
'from'=>"{{fieldName}}>=?",
'to'=>"{{fieldName}}<=?",
'seq'=>null,
'sneq'=>null,
'ntoa'=>"INET_NTOA({{fieldName}})LIKE?",
];
$query='';
if(is_array($condition)){
$key=key(array_intersect_key($condition,$conditionKeyMap));
...
}
ThismethodiswhateventuallygetscalledatsomepointduringSQLqueryconstruction.
The$conditionparameterisexpectedtohaveoneofthefollowing(partiallylisted)
forms:
array("from"=>$fromValue,"to"=>$toValue)
array("eq"=>$equalValue)
array("neq"=>$notEqualValue)
array("like"=>$likeValue)
array("in"=>array($inValues))
array("nin"=>array($notInValues))
array("notnull"=>$valueIsNotNull)
array("null"=>$valueIsNull)
array("gt"=>$greaterValue)
array("lt"=>$lessValue)
array("gteq"=>$greaterOrEqualValue)
array("lteq"=>$lessOrEqualValue)
array("finset"=>$valueInSet)
array("regexp"=>$regularExpression)
array("seq"=>$stringValue)
array("sneq"=>$stringValue)
If$conditionispassedasanintegerorstring,thentheexactvaluewillbefiltered('eq'
condition).Ifnoneoftheconditionsismatched,thenasequentialarrayisexpectedasa
parameterandORconditionswillbebuiltusingtheprecedingstructure.
TheprecedingexamplescoveredEAVmodelcollections,astheyareslightlymore
complex.Thoughtheapproachtofilteringmoreorlessappliestosimplemodel
collectionsaswell,themostnotabledifferenceisthatthereareno
addAttributeToFilter,addAttributeToSelect,andaddExpressionAttributeToSelect
methods.ThesimplemodelcollectionsmakeuseofaddFieldToFilter,
addFieldToSelect,andaddExpressionFieldToSelect,amongothersubtledifferences.
Summary
Inthischapter,wefirstlearnedhowtocreatesimplemodel,itsresource,andcollection
class.ThenwedidthesameforanEAVmodel.Oncewehadtherequiredmodel,
resource,andcollectionclassesinplace,wetookadetailedlookatthetypeandflowof
schemaanddatascripts.Goinghands-on,wecoveredInstallSchema,UpgradeSchema,
InstallData,andUpgradeDatascripts.Oncethescriptswererun,thedatabaseendedup
havingtherequiredtablesandsampledatauponwhichwebasedourentityCRUD
examples.Finally,wetookaquickbutfocusedlookatcollectionmanagement,mostly
comprisingfilteringcollectiontogetthedesiredresultset.
Thefullmodulecodecanbedownloadedfromhttps://github.com/ajzele/B05032-
Foggyline_Office.
Chapter5.UsingtheDependency
Injection
Dependencyinjectionisasoftwaredesignpatternviawhichoneormoredependencies
areinjectedorpassedbyreferenceintoanobject.Whatthisexactlymeansonapractical
levelisshowninthefollowingtwosimpleexamples:
publicfunctiongetTotalCustomers()
{
$database=new\PDO(…);
$statement=$database->query('SELECT…');
return$statement->fetchColumn();
}
Here,youwillseeasimplifiedPHPexample,wherethe$databaseobjectiscreatedinthe
getTotalCustomersmethod.Thismeansthatthedependencyonthedatabaseobjectis
beinglockedinanobjectinstancemethod.Thismakesfortightcoupling,whichhas
severaldisadvantagessuchasreducedreusabilityandapossiblesystem-wideeffect
causedbychangesmadetosomepartsofthecode.
Asolutiontothisproblemistoavoidmethodswiththesesortsofdependenciesby
injectingadependencyintoamethod,asfollows:
publicfunctiongetTotalCustomers($database)
{
$statement=$database->query('SELECT…');
return$statement->fetchColumn();
}
Here,a$databaseobjectispassed(injected)intoamethod.That’sallthatdependency
injectionis—asimpleconceptthatmakescodelooselycoupled.Whiletheconceptis
simple,itmaynotbeeasytoimplementitacrosslargeplatformssuchasMagento.
Magentohasitsownobjectmanageranddependencyinjectionmechanismthatwewill
soonlookatindetailinthefollowingsections:
Theobjectmanager
Dependencyinjection
Configuringclasspreferences
Usingvirtualtypes
Note
Tofollowandtestthecodeexamplesgiveninthefollowingsections,wecanusethecode
availableathttps://github.com/ajzele/B05032-Foggyline_Di.Toinstallit,wesimplyneed
todownloaditandputitintheapp/code/Foggyline/Didirectory.Then,runthe
followingsetofcommandsontheconsolewithinMagento’srootdirectory:
phpbin/magentomodule:enableFoggyline_Di
phpbin/magentosetup:upgrade
phpbin/magentofoggy:di
Thelastcommandcanbeusedrepeatedlywhentestingthesnippetspresentedinthe
followingsection.Whenphpbin/magentofoggy:diisrun,itwillrunthecodewithin
theexecutemethodintheDiTestCommandclass.Therefore,wecanusethe__construct
andexecutemethodsfromwithintheDiTestCommandclassandthedi.xmlfileitselfasa
playgroundforDI.
Theobjectmanager
TheinitializingofobjectsinMagentoisdoneviawhatiscalledtheobjectmanager.The
objectmanageritselfisaninstanceofthe
Magento\Framework\ObjectManager\ObjectManagerclassthatimplementsthe
Magento\Framework\ObjectManagerInterfaceclass.TheObjectManagerclassdefines
thefollowingthreemethods:
create($type,array$arguments=[]):Thiscreatesanewobjectinstance
get($type):Thisretrievesacachedobjectinstance
configure(array$configuration):Thisconfiguresthediinstance
TheobjectmanagercaninstantiateaPHPclass,whichcanbeamodel,helper,orblock
object.Unlesstheclassthatweareworkingwithhasalreadyreceivedaninstanceofthe
objectmanager,wecanreceiveitbypassingObjectManagerInterfaceintotheclass
constructor,asfollows:
publicfunction__construct(
\Magento\Framework\ObjectManagerInterface$objectManager
)
{
$this->_objectManager=$objectManager;
}
Usually,wedon’thavetotakecareoftheconstructorparameter’sorderinMagento.The
followingexamplewillalsoenableustofetchaninstanceoftheobjectmanager:
publicfunction__construct(
$var1,
\Magento\Framework\ObjectManagerInterface$objectManager,
$var2=[]
)
{
$this->_objectManager=$objectManager;
}
ThoughwecanstilluseplainoldPHPtoinstantiateanobjectsuchas$object=new
\Foggyline\Di\Model\Object(),byusingtheobjectmanager,wecantakeadvantageof
Magento’sadvancedobjectfeaturessuchasautomaticconstructordependencyinjection
andobjectproxying.
Hereareafewexamplesofusingobjectmanager’screatemethodtocreatenewobjects:
$this->_objectManager->create('Magento\Sales\Model\Order')
$this->_objectManager->create('Magento\Catalog\Model\Product\Image')
$this->_objectManager->create('Magento\Framework\UrlInterface')
$this->_objectManager->create('SoapServer',['wsdl'=>$url,'options'=>
$options])
Thefollowingareafewexamplesofusingobjectmanager’sgetmethodtocreatenew
objects:
$this->_objectManager->get('Magento\Checkout\Model\Session')
$this->_objectManager->get('Psr\Log\LoggerInterface')->critical($e)
$this->_objectManager->get('Magento\Framework\Escaper')
$this->_objectManager->get('Magento\Sitemap\Helper\Data')
Theobjectmanager’screatemethodalwaysreturnsanewobjectinstance,whiletheget
methodreturnsasingleton.
Notehowsomeofthestringparameterspassedtocreateandgetareactuallyinterface
namesandnotstrictlyclassnames.Wewillsoonseewhythisworkswithbothclass
namesandinterfacenames.Fornow,itsufficestosaythatitworksbecauseofMagento’s
dependencyinjectionimplementation.
Dependencyinjection
Untilnow,wehaveseenhowtheobjectmanagerhascontrolovertheinstantiationof
dependencies.However,byconvention,theobjectmanagerisn’tsupposedtobeused
directlyinMagento.Rather,itshouldbeusedforsystem-levelthingsthatbootstrap
Magento.Weareencouragedtousethemodule’setc/di.xmlfiletoinstantiateobjects.
Let’sdissectoneoftheexistingdi.xmlentries,suchastheonefoundunderthe
vendor/magento/module-admin-notification/etc/adminhtml/di.xmlfileforthe
Magento\Framework\Notification\MessageListtype:
<typename="Magento\Framework\Notification\MessageList">
<arguments>
<argumentname="messages"xsi:type="array">
<itemname="baseurl"xsi:type="string">
Magento\AdminNotification\Model\System\Message\Baseurl</item>
<itemname="security"xsi:type="string">
Magento\AdminNotification\Model\System\Message\Security</item>
<itemname="cacheOutdated"xsi:type="string">
Magento\AdminNotification\Model\System\Message\CacheOutdated</item>
<itemname="media_synchronization_error"
xsi:type="string">Magento\AdminNotification\Model\
System\Message\Media\Synchronization\Error</item>
<itemname="media_synchronization_success"
xsi:type="string">Magento\AdminNotification\Model\
System\Message\Media\Synchronization\Success</item>
</argument>
</arguments>
</type>
Basically,whatthismeansisthatwheneveraninstanceof
Magento\Framework\Notification\MessageListisbeingcreated,themessages
parameterispassedontotheconstructor.Themessagesparameterisbeingdefinedasan
array,whichfurtherconsistsofotherstringtypeitems.Inthiscase,valuesofthesestring
typeattributesareclassnames,asfollows:
Magento\Framework\ObjectManager\ObjectManager
Magento\AdminNotification\Model\System\Message\Baseurl
Magento\AdminNotification\Model\System\Message\Security
Magento\AdminNotification\Model\System\Message\CacheOutdated
Magento\AdminNotification\Model\System\Message\Media\Synchronization\Error
Magento\AdminNotification\Model\System\Message\Media\Synchronization\Success
IfyounowtakealookattheconstructorofMessageList,youwillseethatitisdefinedin
thefollowingway:
publicfunction__construct(
\Magento\Framework\ObjectManagerInterface$objectManager,
$messages=[]
)
{
//Methodbodyhere…
}
IfwemodifytheMessageListconstructorasfollows,thecodewillwork:
publicfunction__construct(
\Magento\Framework\ObjectManagerInterface$objectManager,
$someVarX='someDefaultValueX',
$messages=[]
)
{
//Methodbodyhere…
}
Aftermodification:
publicfunction__construct(
\Magento\Framework\ObjectManagerInterface$objectManager,
$someVarX='someDefaultValueX',
$messages=[],
$someVarY='someDefaultValueY'
)
{
//Methodbodyhere…
}
However,ifwechangetheMessageListconstructortooneofthefollowingvariations,the
codewillfailtowork:
publicfunction__construct(
\Magento\Framework\ObjectManagerInterface$objectManager,
$Messages=[]
)
{
//Methodbodyhere…
}
Anothervariationisasfollows:
publicfunction__construct(
\Magento\Framework\ObjectManagerInterface$objectManager,
$_messages=[]
)
{
//Methodbodyhere…
}
Thenameofthe$messagesparameterintheconstructorofthePHPclasshastoexactly
matchthenameoftheargumentwithinthearguments’listofdi.xml.Theorderof
parametersintheconstructordoesnotreallymatterasmuchastheirnaming.
LookingfurtherintheMessageListconstructor,ifweexecutefunc_get_argssomewhere
withinit,thelistofitemswithinthe$messagesparameterwillmatchandexceedtheone
showninvendor/magento/module-admin-notification/etc/adminhtml/di.xml.Thisis
sobecausethelistisnotfinal,asMagentocollectstheDIdefinitionsfromacrossentire
theplatformandmergesthem.So,ifanothermoduleismodifyingtheMessageListtype,
themodificationswillbereflected.
Ifweperformastringsearchwithinallthedi.xmlfilesacrosstheentireMagentocode
basefor<typename="Magento\Framework\Notification\MessageList">,thiswillyield
someadditionaldi.xmlfilesthathavetheirownadditionstotheMessageListtype,as
follows:
//vendor/magento/module-indexer/etc/adminhtml/di.xml
<typename="Magento\Framework\Notification\MessageList">
<arguments>
<argumentname="messages"xsi:type="array">
<itemname="indexer_invalid_message"
xsi:type="string">Magento\Indexer\Model\Message\Invalid</item>
</argument>
</arguments>
</type>
//vendor/magento/module-tax/etc/adminhtml/di.xml
<typename="Magento\Framework\Notification\MessageList">
<arguments>
<argumentname="messages"xsi:type="array">
<itemname="tax"xsi:type="string">Magento
\Tax\Model\System\Message\Notifications</item>
</argument>
</arguments>
</type>
WhatthismeansisthattheMagento\Indexer\Model\Message\Invalidand
Magento\Tax\Model\System\Message\Notificationsstringitemsarebeingaddedtothe
messagesargumentandarebeingmadeavailablewithintheMessageListconstructor.
IntheprecedingDIexample,weonlyhadthe$messagesparameterdefinedasone
argumentofthearraytype,andtherestwereitsarrayitems.
Let’stakealookataDIexampleforanothertypedefinition.Thistime,itistheonefound
underthevendor/magento/module-backend/etc/di.xmlfileandwhichisdefinedas
follows:
<typename="Magento\Backend\Model\Url">
<arguments>
<argumentname="scopeResolver"xsi:type="object">
Magento\Backend\Model\Url\ScopeResolver</argument>
<argumentname="authSession"xsi:type="object">
Magento\Backend\Model\Auth\Session\Proxy</argument>
<argumentname="formKey"xsi:type="object">
Magento\Framework\Data\Form\FormKey\Proxy</argument>
<argumentname="scopeType"xsi:type="const">
Magento\Store\Model\ScopeInterface::SCOPE_STORE</argument>
<argumentname="backendHelper"xsi:type="object">
Magento\Backend\Helper\Data\Proxy</argument>
</arguments>
</type>
Here,youwillseeatypewithseveraldifferentargumentspassedtotheconstructorofthe
Magento\Backend\Model\Urlclass.IfyounowtakealookattheconstructoroftheUrl
class,youwillseethatitisdefinedinthefollowingway:
publicfunction__construct(
\Magento\Framework\App\Route\ConfigInterface$routeConfig,
\Magento\Framework\App\RequestInterface$request,
\Magento\Framework\Url\SecurityInfoInterface$urlSecurityInfo,
\Magento\Framework\Url\ScopeResolverInterface$scopeResolver,
\Magento\Framework\Session\Generic$session,
\Magento\Framework\Session\SidResolverInterface$sidResolver,
\Magento\Framework\Url\RouteParamsResolverFactory
$routeParamsResolverFactory,
\Magento\Framework\Url\QueryParamsResolverInterface
$queryParamsResolver,
\Magento\Framework\App\Config\ScopeConfigInterface$scopeConfig,
$scopeType,
\Magento\Backend\Helper\Data$backendHelper,
\Magento\Backend\Model\Menu\Config$menuConfig,
\Magento\Framework\App\CacheInterface$cache,
\Magento\Backend\Model\Auth\Session$authSession,
\Magento\Framework\Encryption\EncryptorInterface$encryptor,
\Magento\Store\Model\StoreFactory$storeFactory,
\Magento\Framework\Data\Form\FormKey$formKey,
array$data=[]
){
//Methodbodyhere…
}
The__constructmethodhereclearlyhasmoreparametersthanwhat’sdefinedinthe
di.xmlfile.Whatthismeansisthatthetypeargumententriesindi.xmldonotnecessarily
coveralltheclass__constructparameters.Theargumentsthataredefinedindi.xml
simplyimposethetypesofindividualparametersdefinedinthePHPclassitself.This
worksaslongasthedi.xmlparametersareofthesametypeordescendantsofthesame
type.
Ideally,wewouldnotpasstheclasstypebutinterfaceintothePHPconstructorandthen
setthetypeindi.xml.Thisiswherethetype,preference,andvirtualTypeplayamajor
roleindi.xml.Wehaveseentheroleoftype.Now,let’sgoaheadandseewhat
preferencedoes.
Configuringclasspreferences
AgreatnumberofMagento’scoreclassespassinterfacesaroundconstructors.Thebenefit
ofthisisthattheobjectmanager,withthehelpofdi.xml,candecidewhichclassto
actuallyinstantiateforagiveninterface.
Let’simaginetheFoggyline\Di\Console\Command\DiTestCommandclasswitha
constructor,asfollows:
publicfunction__construct(
\Foggyline\Di\Model\TestInterface$myArg1,
$myArg2,
$name=null
)
{
//Methodbodyhere…
}
Notehow$myArg1istypehintedasthe\Foggyline\Di\Model\TestInterfaceinterface.
Theobjectmanagerknowsthatitneedstolookintotheentiredi.xmlforpossible
preferencedefinitions.
Wecandefinepreferencewithinthemodule’sdi.xmlfile,asfollows:
<preference
for="Foggyline\Di\Model\TestInterface"
type="Foggyline\Di\Model\Cart"/>
Here,wearebasicallysayingthatwhensomeoneasksforaninstanceof
Foggyline\Di\Model\TestInterface,giveitaninstanceofthe
Foggyline\Di\Model\Cartobject.Forthistowork,theCartclasshastoimplement
TestInterfaceitself.Oncethepreferencedefinitionisinplace,$myArg1showninthe
precedingexamplebecomesanobjectoftheCartclass.
Additionally,thepreferenceelementisnotreservedonlytopointoutthepreferred
classesforsomeinterfaces.Wecanuseittosetthepreferredclassforsomeotherclass.
Now,let’shavealookattheFoggyline\Di\Console\Command\DiTestCommandclasswith
aconstructor:
publicfunction__construct(
\Foggyline\Di\Model\User$myArg1,
$myArg2,
$name=null
)
{
//Methodbodyhere…
}
Notehow$myArg1isnowtypehintedasthe\Foggyline\Di\Model\Userclass.Likein
thepreviousexample,theobjectmanagerwilllookintodi.xmlforpossiblepreference
definitions.
Let’sdefinethepreferenceelementwithinthemodule’sdi.xmlfile,asfollows:
<preference
for="\Foggyline\Di\Model\User"
type="Foggyline\Di\Model\Cart"/>
WhatthispreferencedefinitionissayingisthatwheneveraninstanceoftheUserclassis
requested,passaninstanceoftheCartobject.ThiswillworkonlyiftheCartclass
extendsfromUser.Thisisaconvenientwayofrewritingaclass,wheretheclassisbeing
passeddirectlyintoanotherclassconstructorinplaceoftheinterface.
Sincetheclass__constructparameterscanbetypehintedaseitherclassesorinterfaces
andfurthermanipulatedviathedi.xmlpreferencedefinition,aquestionrisesastowhatis
better.Isitbettertouseinterfacesorspecificclasses?Whiletheanswermightnotbefully
clear,itisalwayspreferabletouseinterfacestospecifythedependenciesweareinjecting
intothesystem.
Usingvirtualtypes
Alongwithtypeandpreference,thereisanotherpowerfulfeatureofdi.xmlthatwecan
use.ThevirtualTypeelementenablesustodefinevirtualtypes.Creatingavirtualtypeis
likecreatingasubclassofanexistingclassexceptforthefactthatit’sdoneindi.xmland
notincode.
Virtualtypesareawayofinjectingdependenciesintosomeoftheexistingclasses
withoutaffectingotherclasses.Toexplainthisviaapracticalexample,let’stakealookat
thefollowingvirtualtypedefinedintheapp/etc/di.xmlfile:
<virtualTypename="Magento\Framework\Message\Session\Storage"
type="Magento\Framework\Session\Storage">
<arguments>
<argumentname="namespace"xsi:type="string">message</argument>
</arguments>
</virtualType>
<typename="Magento\Framework\Message\Session">
<arguments>
<argumentname="storage"xsi:type="object">
Magento\Framework\Message\Session\Storage</argument>
</arguments>
</type>
ThevirtualTypedefinitionintheprecedingexampleis
Magento\Framework\Message\Session\Storage,whichextendsfrom
Magento\Framework\Session\Storageandoverwritesthenamespaceparametertothe
messagestringvalue.InvirtualType,thenameattributedefinesthegloballyunique
nameofthevirtualtype,whilethetypeattributematchestherealPHPclassthatthe
virtualtypeisbasedon.
Now,ifyoulookatthetypedefinition,youwillseethatitsstorageargumentissettothe
objectofMagento\Framework\Message\Session\Storage.TheSession\Storagefileis
actuallyavirtualtype.ThisallowsMessage\Sessiontobecustomizedwithoutaffecting
otherclassesthatalsodeclareadependencyonSession\Storage.
Virtualtypesallowustoeffectivelychangethebehaviorofadependencywhenitisused
inaspecificclass.
Summary
Inthischapter,wehadalookattheobjectmanageranddependencyinjection,whichare
thefoundationsofMagentoobjectmanagement.Welearnedthemeaningofthetypeand
preferenceelementsofdependencyinjectionandhowtousethemtomanipulateclass
constructparameters.Thoughthereismuchmoretobesaidaboutdependencyinjectionin
Magento,thepresentedinformationshouldsufficeandhelpuswithotheraspectsof
Magento.
Inthenextchapter,wewillextendourjourneyintodi.xmlviatheconceptofplugins.
Chapter6.Plugins
Inthischapter,wewilltakealookatafeatureofMagentocalledplugins.Beforewestart
withplugins,wefirstneedtounderstandtheterminterceptionbecausethetwotermsare
usedsomewhatinterchangeablywhendealingwithMagento.
Interceptionisasoftwaredesignpatternthatisusedwhenwewanttoinsertcode
dynamicallywithoutnecessarilychangingtheoriginalclassbehavior.Thisworksby
dynamicallyinsertingcodebetweenthecallingcodeandthetargetobject.
TheinterceptionpatterninMagentoisimplementedviaplugins.Theyprovidethebefore,
after,andaroundlisteners,whichhelpusextendtheobservedmethodbehavior.
Inthischapter,wewillcoverthefollowingtopics:
Creatingaplugin
Usingthebeforelistener
Usingtheafterlistener
Usingthearoundlistener
Thepluginsortorder
Beforewestartcreatingaplugin,itisworthnotingtheirlimitations.Pluginscannotbe
createdforjustanyclassormethod,astheydonotworkforthefollowing:
Finalclasses
Finalmethods
Theclassesthatarecreatedwithoutadependencyinjection
Let’sgoaheadandcreateapluginusingasimplemodulecalledFoggyline_Plugged.
Creatingaplugin
Startbycreatingtheapp/code/Foggyline/Plugged/registration.phpfilewithpartial
content,asfollows:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Foggyline_Plugged',
__DIR__
);
Then,createtheapp/code/Foggyline/Plugged/etc/module.xmlfilewithpartialcontent,
asfollows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/
etc/module.xsd">
<modulename="Foggyline_Plugged"setup_version="1.0.0">
<sequence>
<modulename="Magento_Catalog"/>
</sequence>
</module>
</config>
Theprecedingfileissimplyanewmoduledeclarationwiththedependencysetagainstthe
Magento_Catalogmodule,aswewillbeobservingitsclass.Wewillnotgointothedetails
ofmoduledeclarationrightnow,asthatwillbecoveredlaterinthefollowingchapters.
Now,createtheapp/code/Foggyline/Plugged/etc/di.xmlfilewithpartialcontent,as
follows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:
ObjectManager/etc/config.xsd">
<typename="Magento\Catalog\Block\Product\AbstractProduct">
<pluginname="foggyPlugin1"
type="Foggyline\Plugged\Block\Catalog\Product\AbstractProductPlugin1"
disabled="false"sortOrder="100"/>
<pluginname="foggyPlugin2"
type="Foggyline\Plugged\Block\Catalog\Product\AbstractProductPlugin2"
disabled="false"sortOrder="200"/>
<pluginname="foggyPlugin3"
type="Foggyline\Plugged\Block\Catalog\Product\AbstractProductPlugin3"
disabled="false"sortOrder="300"/>
</type>
</config>
Pluginsaredefinedwithinthemoduledi.xmlfile.Todefineaplugin,byusingthetype
elementanditsnameattribute,wefirstmaptheclassthatwewanttoobserve.Inthiscase,
weareobservingtheMagento\Catalog\Block\Product\AbstractProductclass.Note
thateventhoughthefileandclassnameimplyanabstracttypeofclass,the
AbstractProductclassisnotabstract.
Inthetypeelement,wethendefineoneormorepluginsusingthepluginelement.
Thepluginelementhasthefollowingfourattributesassignedtoit:
name:Usingthisattribute,youcanprovideauniqueandrecognizablenamevaluethat
isspecifictotheplugin
sortOrder:Thisattributedeterminestheorderofexecutionwhenmultipleplugins
areobservingthesamemethod
disabled:Thedefaultvalueofthisattributeissettofalse,butifitissettotrue,it
willdisabletheplugin
type:Thisattributepointstotheclassthatwewillbeusingtoimplementthebefore,
after,oraroundlistener
Afterdoingthis,createthe
app/code/Foggyline/Plugged/Block/Catalog/Product/AbstractProductPlugin1.php
filewithpartialcontent,asfollows:
namespaceFoggyline\Plugged\Block\Catalog\Product;
classAbstractProductPlugin1
{
publicfunctionbeforeGetAddToCartUrl(
$subject,
$product,$additional=[]
)
{
var_dump('Plugin1-beforeGetAddToCartUrl');
}
publicfunctionafterGetAddToCartUrl($subject)
{
var_dump('Plugin1-afterGetAddToCartUrl');
}
publicfunctionaroundGetAddToCartUrl(
$subject,
\Closure$proceed,
$product,
$additional=[]
)
{
var_dump('Plugin1-aroundGetAddToCartUrl');
return$proceed($product,$additional);
}
}
Asperthetypedefinitioninthedi.xmlfile,thepluginobservesthe
Magento\Catalog\Block\Product\AbstractProductclass,andthisclasshasamethod
calledgetAddToCartUrl,whichisdefinedasfollows:
publicfunctiongetAddToCartUrl($product,$additional=[])
{
//methodbodyhere…
}
TheAbstractProductPlugin1classdoesnothavetobeextendedfromanotherclassfor
theplugintowork.Wedefinethebefore,afterandaroundlistenersforthe
getAddToCartUrlmethodbyusingthenamingconvention,asfollows:
<before>+<getAddToCartUrl>=>beforeGetAddToCartUrl
<after>+<getAddToCartUrl>=>afterGetAddToCartUrl
<around>+<getAddToCartUrl>=>aroundGetAddToCartUrl
Wewillgointothedetailsofeachlistenerlater.Rightnowweneedtofinishthemodule
bycreatingtheAbstractProductPlugin2.phpandAbstractProductPlugin3.phpfilesas
acopyofAbstractProductPlugin1.phpandalongwiththat,simplychangingallthe
numbervalueswithintheircodefrom1to2or3.
It’sagoodpracticetoorganizethelistenersintofoldersmatchingthestructureofthe
observedclasslocation.Forexample,ifamoduleiscalledFoggyline_Pluggedandwe
areobservingthemethodintheMagento\Catalog\Block\Product\AbstractProduct
class,weshouldconsiderputtingthepluginclassintothe
Foggyline/Plugged/Block/Catalog/Product/AbstractProductPlugin.phpfile.Thisis
anotarequirement.Rather,itisaniceconventionforotherdeveloperstoeasilymanage
thecode.
Oncethemoduleisinplace,weneedtoexecutethefollowingcommandsontheconsole:
phpbin/magentomodule:enableFoggyline_Plugged
phpbin/magentosetup:upgrade
ThiswillmakethemodulevisibletoMagento.
Ifwenowopenthestorefrontinabrowserforacategorypage,wewillseetheresultsof
allthevar_dumpfunctioncalls.
Let’sgoaheadandtakealookateachandeverylistenermethodindetail.
Usingthebeforelistener
Thebeforelistenersareusedwhenwewanttochangetheargumentsofanoriginal
methodoraddsomebehaviorbeforeanoriginalmethodiscalled.
LookingbackatthebeforeGetAddToCartUrllistenermethoddefinition,youwillseethat
ithasthreepropertiesassignedinsequence—$subject,$product,and$additional.
Withthebeforemethodlistener,thefirstpropertyisalwaysthe$subjectproperty,which
containstheinstanceoftheobjecttypebeingobserved.Propertiesfollowingthe$subject
propertymatchthepropertiesoftheobservedgetAddToCartUrlmethodinasequential
order.
Thissimpleruleusedfortransformationisasfollows:
getAddToCartUrl($product,$additional=[])
beforeGetAddToCartUrl($subject,$product,$additional=[])
Thebeforelistenermethodsdonotneedtohaveareturnvalue.
Ifwerunget_class($subject)inthebeforeGetAddToCartUrllistenermethodthatwe
previouslysaw,wewillhavethefollowingresult:
\Magento\Catalog\Block\Product\ListProduct\Interceptor
extends\Magento\Catalog\Block\Product\ListProduct
extends\Magento\Catalog\Block\Product\AbstractProduct
WhatthisshowsisthateventhoughweareobservingtheAbstractProductclass,the
$subjectpropertyisnotdirectlyofthattype.Rather,itisofthe
ListProduct\Interceptortype.Thisissomethingthatyoushouldkeepinmindduring
development.
Usingtheafterlistener
Theafterlistenersareusedwhenwewanttochangethevaluesreturnedbyanoriginal
methodoraddsomebehaviorafteranoriginalmethodiscalled.
LookingbackattheafterGetAddToCartUrllistenermethoddefinition,youwillseethatit
hasonlyone$subjectpropertyassigned.
Withtheaftermethodlistener,thefirstandonlypropertyisalwaysthe$subject
property,whichcontainstheinstanceoftheobjecttypebeingobservedandnotthereturn
valueoftheobservedmethod.
Thissimpleruleusedfortransformationisasfollows:
getAddToCartUrl($product,$additional=[])
afterGetAddToCartUrl($subject)
Theafterlistenermethodsdonotneedtohaveareturnvalue.
Likethebeforeinterceptormethod,the$subjectpropertyinthiscaseisnotdirectlyof
theAbstractProducttype.Rather,itisoftheparentListProduct\Interceptortype.
Usingthearoundlistener
Thearoundlistenersareusedwhenwewanttochangeboththeargumentsandthe
returnedvaluesofanoriginalmethodoraddsomebehaviorbeforeandafteranoriginal
methodiscalled.
LookingbackatthearoundGetAddToCartUrllistenermethoddefinition,youwillseethat
ithasfourpropertiesassignedinsequence—$subject,$proceed,$product,and
$additional.
Withtheaftermethodlistener,thefirstpropertyisalwaysthe$subjectproperty,which
containstheinstanceoftheobjecttypebeingobservedandnotthereturnvalueofthe
observedmethod.Thesecondpropertyisalwaysthe$proceedpropertyof\Closure.The
propertiesfollowingthe$subjectand$proceedmatchthepropertiesoftheobserved
getAddToCartUrlmethodinthesequentialordertoo.
Thissimpleruleusedfortransformationisasfollows:
getAddToCartUrl($product,$additional=[])
aroundGetAddToCartUrl(
$subject,
\Closure$proceed,
$product,
$additional=[]
)
Thearoundlistenermethodsmusthaveareturnvalue.Thereturnvalueisformedinsuch
waythattheparametersfollowingthe$closureparameterinthearoundlistenermethod
definitionarepassedtothe$closurefunctioncallinasequentialorder,asfollows:
return$proceed($product,$additional);
//or
$result=$proceed($product,$additional);
return$result;
Thepluginsortorder
Lookingback,whenwedefinedaplugininthedi.xmlfile,oneoftheattributesthatwe
setforeveryplugindefinitionwassortOrder.Itwassetto100,200to300for
foggyPlugin1,foggyPlugin2andfoggyPlugin3respectively.
Theflowofthecodeexecutionfortheprecedingpluginsisasfollows:
Plugin1-beforeGetAddToCartUrl
Plugin1-aroundGetAddToCartUrl
Plugin2-beforeGetAddToCartUrl
Plugin2-aroundGetAddToCartUrl
Plugin3-beforeGetAddToCartUrl
Plugin3-aroundGetAddToCartUrl
Plugin3-afterGetAddToCartUrl
Plugin2-afterGetAddToCartUrl
Plugin1-afterGetAddToCartUrl
Inotherwords,ifmultiplepluginsarelisteningtothesamemethod,thefollowing
executionorderisused:
ThebeforepluginfunctionswiththelowestsortOrdervalue
ThearoundpluginfunctionswiththelowestsortOrdervalue
ThebeforepluginfunctionsfollowingthesortOrdervaluefromthelowesttothe
highest
ThearoundpluginfunctionsfollowingthesortOrdervaluefromthelowesttothe
highest
TheafterpluginfunctionswiththehighestsortOrdervalue
TheafterpluginfunctionsfollowingthesortOrdervaluefromthehighesttothe
lowest
Note
Specialcareneedstobetakenwhenitcomestothearoundlistener,asitistheonly
listenerthatneedstoreturnavalue.Ifweomitthereturnvalue,weriskbreakingthe
executionflowinsuchawaythattheotheraroundpluginsforthesamemethodwon’tbe
executed.
Summary
Inthischapter,wehadalookatapowerfulfeatureofMagentocalledplugins.Wecreated
asmallmodulewiththreeplugins;eachpluginhadadifferentsortorder.Thisenabledus
totracetheexecutionflowofmultiplepluginsthatobservethesamemethod.Weexplored
indetailthebefore,after,andaroundlistenermethods,whilehavingastrongemphasis
ontheparameterorder.Thefinalizedmoduleusedinthischaptercanbefoundat
https://github.com/ajzele/B05032-Foggyline_Plugged.
Inthenextchapter,wearegoingtodivedeepintobackenddevelopment.
Chapter7.BackendDevelopment
Backenddevelopmentisatermthatismostcommonlyusedtodescribeworkclosely
relatedtotheserverside.Thisusuallyimpliestheactualserver,applicationcode,andthe
database.Forexample,ifweopenastorefrontofawebshop,addafewproductstothe
cart,andthencheckout,theapplicationwillstoretheinformationprovided.This
informationismanagedonaserverwithaserver-sidelanguage,suchasPHP,andthen
savedinadatabase.InChapter4,ModelsandCollections,wetookalookatthebackbone
ofbackenddevelopment.Inthischapter,wewillexploreotherbackend-relatedaspects.
WewillusetheFoggyline_Officemodulethatwasdefinedinoneoftheprevious
chaptersaswegothroughthefollowingtopics:
Cronjobs
Notificationmessages
Sessionsandcookies
Logging
Theprofiler
Eventsandobservers
Caches
Widgets
Customvariables
i18n(internationalization)
Indexers
Theseindividualisolatedunitsoffunctionalityaremostlyusedineverydaybackend-
relateddevelopment.
Cronjobs
Speakingofcronjobs,itisworthnotingoneimportantthing.AMagentocronjobisnot
thesameasanoperatingsystemcronjob.Anoperatingsystemcronisdrivenbya
crontab(shortforcrontable)file.Thecrontabfile,isaconfigurationfilethatspecifies
shellcommandsthatneedtoberunperiodicallyonagivenschedule.
AMagentocronjobisdrivenbyaperiodicexecutionofPHPcodethathandlesentriesin
thecron_scheduletable.Thecron_scheduletableiswhereMagentocronjobsare
queuedoncetheyarepickedupfromtheindividualcrontab.xmlfile.
TheMagentocronjobscannotbeexecutedwithouttheoperatingsystemcronjobbeing
settoexecutethephpbin/magentocron:runcommand.Ideally,anoperatingsystem
cronjobshouldbesettotriggerMagento'scron:runeveryminute.Magentowillthen
internallyexecuteitscronjobsaccordingtothewayanindividualcronjobisdefinedin
thecrontab.xmlfile.
TodefineanewcronjobinMagentocron,wefirstneedtodefineacrontab.xmlfilein
themodule.Let’screateaapp/code/Foggyline/Office/etc/crontab.xmlfilewiththe
followingcontent:
<?xmlversion="1.0"?>
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation=
"urn:magento:module:Magento_Cron:etc/crontab.xsd">
<groupid="default">
<jobname="foggyline_office_logHello"instance=
"Foggyline\Office\Model\Cron"method="logHello">
<schedule>*/2****</schedule>
</job>
</group>
</config>
NotethattheXSDschemalocationpointstocrontab.xsdfromwithintheMagento_Cron
module.
Theidattributeofagroupelementissettothedefaultvalue.Initsmodules,Magento
definestwodifferentgroups,namelydefaultandindex.Weusedthedefaultvalue,asthis
istheonethatgetsexecutedwhenthestandardphpbin/magentocron:runcommandis
triggeredontheconsole.
Withinthegroupelement,wehaveindividualjobsdefinedunderthejobelement.The
jobelementrequiresustospecifythename,instance,andmethodattributes.Thename
attributehastobeuniquewithinthegroupelement.Thevalueoftheinstanceand
methodattributesshouldpointtotheclassthatwillbeinstantiatedandthemethodwithin
theclassthatneedstobeexecuted.
Thescheduleelementnestedwithinthecronjobspecifiesthedesiredtimeofjob
execution.Itusesthesametimeexpressionasthatoftheentriesinanoperatingsystem
crontabfile.Thespecificexamplethatwewilllookatdefinesanexpression(*/2***
*)thatisexecutedeverytwominutes.
Oncewehavedefinedthecrontab.xmlfile,weneedtodefinethe
Foggyline\Office\Model\Cronclassfile,asfollows:
namespaceFoggyline\Office\Model;
classCron
{
protected$logger;
publicfunction__construct(
\Psr\Log\LoggerInterface$logger
)
{
$this->logger=$logger;
}
publicfunctionlogHello()
{
$this->logger->info('HellofromCronjob!');
return$this;
}
}
TheprecedingcodesimplydefinesalogHellomethodusedbythecronjob.Inthe
logHellomethod,weusedtheloggermethodthatwasinstantiatedviatheconstructor.
Theloggermethodwillmakealogentryinthevar/log/system.logfileonceitis
executed.
Oncethecommandisexecuted,youwillseetheRanjobsbyschedulemessageinthe
console.Additionally,thecron_scheduletableshouldgetfilledwithalltheMagento
cronjobsthatweredefined.
Atthispoint,weshouldtriggerthephpbin/magentocron:runcommandintheconsole.
Thecron_scheduletablecontainsthefollowingcolumns:
schedule_id:Theauto-incrementprimaryfield.
job_code:Thevalueofthejobnameattribute,asdefinedincrontab.xmlfile,which
equalstofoggyline_office_logHellotableinourexample.
status:Defaultstothependingvalueforthenewlycreatedentriesinthetableand
allowsforapending,running,success,missedorerrorvalue.Itsvaluechangesas
thecronjobtraversesthroughitslifecycle.
messages:Storesthepossibleexceptionerrormessageiftheexceptionhasoccurred
duringajob’sexecution.
created_at:Thetimestampvaluethatdenoteswhenajobwascreated.
scheduled_at:Thetimestampvaluethatdenoteswhenajobwasscheduledfor
execution.
executed_at:Thetimestampvaluethatdenoteswhenajob’sexecutionstarted.
finished_at:Thetimestampvaluethatdenoteswhenajobhasfinishedexecuting.
Unlesswehavealreadysettheoperatingsystemcrontotriggerthephpbin/magento
cron:runcommand,weneedtotriggeritonourownafewtimeseverytwominutesin
ordertoactuallyexecutethejob.Thefirsttimeacommandisrun,ifthejobdoesnotexist
inthecron_scheduletable,Magentowillmerelyqueueit,butitwon’texecuteit.The
subsequentcronrunswillexecutethecommand.Oncewearesurethatthecronjobentry
inthecron_scheduletablehasthefinished_atcolumnvaluefilled,wewillseeanentry
thatlookslike[2015-11-2109:42:18]main.INFO:HellofromCronjob![][]in
thevar/log/system.logfile.
Tip
WhiledevelopingandtestingcronjobsinMagento,wemightneedtotruncatethe
cron_scheduletable,deleteMagento'svar/cachevalue,andexecutethephp
bin/magentocron:runcommandrepetitivelyuntilwegetittestedandworking.
Notificationmessages
MagentoimplementsthenotificationmessagemechanismviatheMessagesmodule.The
Messagesmoduleconformsto\Magento\Framework\Message\ManagerInterface.
Thoughtheinterfaceitselfdoesnotimposeanysessionrelation,animplementationadds
interface-definedtypesofmessagestoasessionandallowsaccesstothosemessageslater.
Intheapp/etc/di.xmlfile,thereisapreferencedefinedfor
\Magento\Framework\Message\ManagerInterfacetowardsthe
Magento\Framework\Message\Managerclass.
Message\ManagerInterfacespecifiesfourtypesofmessages,namelyerror,warning,
notice,andsuccess.Thetypesofmessagesarefollowedbyseveralkeymethodsinthe
Message\Managerclass,suchasaddSuccess,addNotice,addWarning,addError,and
addException.TheaddExceptionmethodisbasicallyawrapperforaddErrorthat
acceptsanexceptionobjectasaparameter.
Let’strytorunthefollowingcodeintheexecutemethodof
app/code/Foggyline/Office/Controller/Test/Crud.php:
$resultPage=$this->resultPageFactory->create();
$this->messageManager->addSuccess('Success-1');
$this->messageManager->addSuccess('Success-2');
$this->messageManager->addNotice('Notice-1');
$this->messageManager->addNotice('Notice-2');
$this->messageManager->addWarning('Warning-1');
$this->messageManager->addWarning('Warning-2');
$this->messageManager->addError('Error-1');
$this->messageManager->addError('Error-2');
return$resultPage;
Oncethiscodeexecuted,theresult,asshowninthefollowingscreenshot,willappearon
thepageinthebrowser:
Notificationmessagesappearbothinthefrontendandadminarea.
Thefrontendlayoutvendor/magento/module-
theme/view/frontend/layout/default.xmlfiledefinesitasfollows:
<pagelayout="3columns"xmlns:xsi="http://www.w3.org/2001/XMLSchema-
instance"xsi:noNamespaceSchemaLocation=
"../../../../../../../lib/internal/Magento/Framework
/View/Layout/etc/page_configuration.xsd">
<updatehandle="default_head_blocks"/>
<body>
<!--...-->
<referenceContainername="columns.top">
<containername="page.messages"htmlTag="div"htmlClass="page
messages">
<blockclass="Magento\Framework\View\Element\Messages"
name="messages"as="messages"template="Magento_Theme::messages.phtml"/>
</container>
</referenceContainer>
<!--...-->
</body>
</page>
Thetemplatefilethatrendersthemessagesis
view/frontend/templates/messages.phtmlintheMagento_Thememodule.Bylooking
attheMagento\Framework\View\Element\Messagesclass,youwillseethatthe_toHtml
methodbranchesintoif-elsestatements,dependingonwhethertemplateissetornot.In
casethetemplateisnotset,_toHtmlinternallycallsthe_renderMessagesByTypemethod,
whichrendersmessagesintheHTMLformatthataregroupedbytype.
Theview/adminhtml/layout/default.xmladminlayoutfileinthe
Magento_AdminNotificationmoduledefinesitasfollows:
<pagexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:
framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainername="notifications">
<blockclass="Magento\AdminNotification\Block\System\Messages"
name="system_messages"as="system_messages"before="-"template=
"Magento_AdminNotification::system/messages.phtml"/>
</referenceContainer>
</body>
</page>
Thetemplatefilethatrendersthemessagesis
view/adminhtml/templates/system/messages.phtmlinthe
Magento_AdminNotificationmodule.Whenyoulookatthe
Magento\AdminNotification\Block\System\Messagesclass,youwillseethatits
_toHtmliscallingthe_toHtmlparentmethod,wheretheparentbelongstothe
\Magento\Framework\View\Element\Templateclass.Thismeansthattheoutputis
relyingontheview/adminhtml/templates/system/messages.phtmlfileinthe
Magento_AdminNotificationmodule.
Sessionandcookies
SessionsinMagentoconformto
Magento\Framework\Session\SessionManagerInterface.Intheapp/etc/di.xmlfile,
thereisadefinitionpreferencefortheSessionManagerInterfaceclasswhichpointsto
theMagento\Framework\Session\Genericclasstype.TheSession\Genericclassisjust
anemptyclassthatextendstheMagento\Framework\Session\SessionManagerclass,
whichinturnimplementstheSessionManagerInterfaceclass.
ThereisoneimportantobjectthatgetsinstantiatedintheSessionManagerinstancethat
conformsto\Magento\Framework\Session\Config\ConfigInterface.Onlookingat
app/etc/di.xmlfile,wecanseeapreferenceforConfigInterfacepointingtoa
Magento\Framework\Session\Configclasstype.
Tip
TofullyunderstandthesessionbehaviorinMagento,weshouldstudytheinnerworkings
ofboththeSessionManagerandSession\Configclasses.
Magentousescookiestokeeptrackofasession.Thesecookieshaveadefaultlifetimeof
3,600seconds.Whenasessionisestablished,acookiewiththenameofPHPSESSIDis
createdinthebrowser.Thevalueofthecookieequalsthesessionname.Bydefault,
sessionsarestoredinfilesinthevar/sessiondirectoryofMagento‘srootinstallation.
Ifyouhavealookatthesesessionfiles,youwillseethatsessioninformationisbeing
storedinserializedstringsthataredividedintogroupingssuchas
_session_validator_data,_session_hosts,default,customer_website_1,and
checkout,asshowninthefollowingscreenshot:
Thisisnotthefinitelistofgrouping.Modulesthatimplementtheirownsessionhandling
bitscanaddtheirowngroups.
Wecanstoreandretrieveinformationinasessionbysimplyusingexpressionslikethe
followingones:
$this->sessionManager->setFoggylineOfficeVar1('Office1');
$this->sessionManager->getFoggylineOfficeVar1();
Theprecedingexpressionswillcreateandgetanentryfromthesessionunderthedefault
group.
Wecangettheentirecontentofthedefaultsessiongroupsimplybyusingthe$this-
>sessionManager->getData()expression,whichwillreturnanarrayofdatathatis
similartothefollowingone:
array(3){
["_form_key"]=>string(16)"u3sNaa26Ii21nveV"
["visitor_data"]=>array(14){
["last_visit_at"]=>string(19)"2015-08-1907:40:03"
["session_id"]=>string(26)"8p82je0dkqq1o00lanlr6bj6m2"
["visitor_id"]=>string(2)"35"
["server_addr"]=>int(2130706433)
["remote_addr"]=>int(2130706433)
["http_secure"]=>bool(false)
["http_host"]=>string(12)"magento2.loc"
["http_user_agent"]=>string(121)"Mozilla/5.0…"
["http_accept_language"]=>string(41)"en-US,en;"
["http_accept_charset"]=>string(0)""
["request_uri"]=>string(38)"/index.php/foggyline_office/test/crud/"
["http_referer"]=>string(0)""
["first_visit_at"]=>string(19)"2015-08-1907:40:03"
["is_new_visitor"]=>bool(false)
}
["foggyline_office_var_1"]=>string(7)"Office1"
}
Asyoucansee,thefoggyline_office_var_1valueisrightthereamongothersession
values.
ThereareseveralusefulmethodsofConfigInterfacethatwecanusetofetchsession
configurationinformation;afewofthesemethodsareasfollows:
getCookieSecure
getCookieDomain
getCookieHttpOnly
getCookieLifetime
getName
getSavePath
getUseCookies
getOptions
Here’saresultexampleofthegetOptionsmethodcallontheSession\Configinstance:
array(9){
["session.save_handler"]=>string(5)"files"
["session.save_path"]=>string(39)
"/Users/branko/www/magento2/var/session/"
["session.cookie_lifetime"]=>int(3600)
["session.cookie_path"]=>string(1)"/"
["session.cookie_domain"]=>string(12)"magento2.loc"
["session.cookie_httponly"]=>bool(true)
["session.cookie_secure"]=>string(0)""
["session.name"]=>string(9)"PHPSESSID"
["session.use_cookies"]=>bool(true)
}
Cookiesoftengohandinhandwithsessions.Besidesbeingusedtolinktoacertain
session,cookiesareoftenusedtostoresomeinformationontheclientside,thustracking
oridentifyingthereturnusersandcustomers.
BesidesthepurePHPapproachwiththesetcookiefunction,wecanmanagecookiesin
MagentothroughaninstanceofMagento\Framework\Stdlib\CookieManagerInterface.
Whenyoulookatapp/etc/di.xmlfile,youwillseethatthepreferencefor
CookieManagerInterfacepointstoaclassofthe
Magento\Framework\Stdlib\Cookie\PhpCookieManagertype.
ThefollowingrestrictionsareworthnotingwhenitcomestoMagentocookies:
Wecansetmaximumof50cookiesinthesystem.Otherwise,Magentowillthrowan
Unabletosendthecookie.Maximumnumberofcookieswouldbeexceeded
exception.
Wecanstoreacookiewithamaximumsizeof4096bytes.Otherwise,Magentowill
throwanUnabletosendthecookie.Sizeof\'%name\'is%sizebytes
exception.
Byimposingtheserestrictions,Magentoensuresthatwearecompatiblewithmost
browsers.
TheCookieManagerInterfaceclass,amongotherthings,specifiesthe
setSensitiveCookiemethodrequirement.Thismethodsetsavalueinaprivatecookie
withthegiven$name$valuepairing.SensitivecookieshaveHttpOnlysettotrueand
thuscannotbeaccessedbyJavaScript.
Aswewillsoondemonstrateinthefollowingexamples,tosetapublicorprivatecookie,
wecanhelpourselvesbyusinginstancesofthefollowing:
\Magento\Framework\Stdlib\Cookie\CookieMetadataFactory
\Magento\Framework\Stdlib\CookieManagerInterface
\Magento\Framework\Session\Config\ConfigInterface
Wecansetpubliccookiesinthefollowingway:
$cookieValue='Justsomevalue';
$cookieMetadata=$this->cookieMetadataFactory
->createPublicCookieMetadata()
->setDuration(3600)
->setPath($this->sessionConfig->getCookiePath())
->setDomain($this->sessionConfig->getCookieDomain())
->setSecure($this->sessionConfig->getCookieSecure())
->setHttpOnly($this->sessionConfig->getCookieHttpOnly());
$this->cookieManager
->setPublicCookie('cookie_name_1',$cookieValue,$cookieMetadata);
Theprecedingcodewillresultinacookie,asshowninthefollowingscreenshot:
Wecansetprivatecookiesinthefollowingway:
$cookieValue='Justsomevalue';
$cookieMetadata=$this->cookieMetadataFactory
->createSensitiveCookieMetadata()
->setPath($this->sessionConfig->getCookiePath())
->setDomain($this->sessionConfig->getCookieDomain());
$this->cookieManager
->setSensitiveCookie('cookie_name_2',$cookieValue,$cookieMetadata);
Theprecedingcodewillresultinacookie,asshowninthefollowingscreenshot:
Interestingly,boththepublicandprivatecookiesintheprecedingexampleshowthat
HttpOnlyischeckedoffbecausebydefault,aMagentoadminhasStores|Settings|
Configuration|General|Web|DefaultCookieSettings|UseHTTPOnlysettoYes.
SinceweareusingthesetHttpOnlymethodinthepubliccookieexample,wesimply
pickeduptheconfigvaluevia$this->sessionConfig->getCookieHttpOnly()and
passediton.Ifwecommentoutthatline,wewillseethatthepubliccookiedoesnotreally
setHttpOnlybydefault.
Logging
Magentosupportsthemessagesloggingmechanismviaits\Psr\Log\LoggerInterface
class.TheLoggerInterfaceclasshasapreferencedefinedwithinapp/etc/di.xmlfile
fortheMagento\Framework\Logger\Monologclasstype.Theactualcruxof
implementationisactuallyintheMonologparentclassnamedMonolog\Logger,which
comesfromtheMonologvendor.
TheLoggerInterfaceclassusesthefollowingeightmethodstowritelogstotheeight
RFC5424levels:
debug
info
notice
warning
error
critical
alert
emergency
Tousealogger,weneedtopasstheLoggerInterfaceclasstoaconstructorofaclass
fromwithinwewanttouseitandthensimplymakeoneofthefollowingmethodcalls:
$this->logger->log(\Monolog\Logger::DEBUG,'debugmsg');
$this->logger->log(\Monolog\Logger::INFO,'infomsg');
$this->logger->log(\Monolog\Logger::NOTICE,'noticemsg');
$this->logger->log(\Monolog\Logger::WARNING,'warningmsg');
$this->logger->log(\Monolog\Logger::ERROR,'errormsg');
$this->logger->log(\Monolog\Logger::CRITICAL,'criticalmsg');
$this->logger->log(\Monolog\Logger::ALERT,'alertmsg');
$this->logger->log(\Monolog\Logger::EMERGENCY,'emergencymsg');
Alternatively,thepreferredshorterversionthroughindividuallogleveltypemethodsisas
follows:
$this->logger->debug('debugmsg');
$this->logger->info('infomsg');
$this->logger->notice('noticemsg');
$this->logger->warning('warningmsg');
$this->logger->error('errormsg');
$this->logger->critical('criticalmsg');
$this->logger->alert('alertmsg');
$this->logger->emergency('emergencymsg');
BothapproachesresultinthesametwologfilesbeingcreatedinMagento,whichareas
follows:
var/log/debug.log
var/log/system.log
Thedebug.logfilecontainsonlythedebugleveltypeofthelog,whiletherestaresaved
undersystem.log.
Entrieswithintheselogswillthenlooklikethis:
[2015-11-2109:42:18]main.DEBUG:debugmsg{"is_exception":false}[]
[2015-11-2109:42:18]main.INFO:infomsg[][]
[2015-11-2109:42:18]main.NOTICE:noticemsg[][]
[2015-11-2109:42:18]main.WARNING:warningmsg[][]
[2015-11-2109:42:18]main.ERROR:errormsg[][]
[2015-11-2109:42:18]main.CRITICAL:criticalmsg[][]
[2015-11-2109:42:18]main.ALERT:alertmsg[][]
[2015-11-2109:42:18]main.EMERGENCY:emergencymsg[][]
Eachoftheseloggermethodscanacceptanentirearrayofarbitrarydatacalledcontext,
asfollows:
$this->logger->info('Userloggedin.',['user'=>'Branko','age'=>32]);
Theprecedingexpressionwillproducethefollowingentryinsystem.log:
[2015-11-2109:42:18]main.INFO:Userloggedin.{"user":"Branko","age":32}
[]
Tip
Wecanmanuallydeleteanyofthe.logfilesfromthevar/logdirectory,andMagento
willautomaticallycreateitagainwhenneeded.
Magentoalsohasanotherloggingmechanisminplace,whereitlogsthefollowingactions
inthelog_*tablesinadatabase:
log_customer
log_quote
log_summary
log_summary_type
log_url
log_url_info
log_visitorz
log_visitor_info
log_visitor_online
ItisworthnotingthatthisdatabaseloggingisnotrelatedinanywaytoPsrloggerthat
wasdescribedpreviously.WhilePsrloggerservesdeveloperswithinthecodetogroup
andlogcertainmessagesaccordingtothePsrstandard,thedatabaselogginglogsthelive
datathatisaresultofuser/customerinteractioninthebrowser.
Bydefault,Magentokeepsdatabaselogsforaround180days.Thisisaconfigurable
optionthatcanbecontrolledintheMagentoadminareaundertheStores|Settings|
Configuration|Advanced|System|LogCleaningtabwithotherlogrelatedoptions,as
showninthefollowingscreenshot:
Configurationoptionsthatareshownintheprecedingscreenshotonlybaremeaning
operatingsystemcronistriggeringMagentocron.
Tip
Wecanexecutetwocommandsonterminal:phpbin/magentolog:statustogetthe
currentstateinformationaboutlogtablesandphpbin/magentolog:cleantoforcethe
clearingoftables.
Theprofiler
Magentohasanin-builtprofilerthatcanbeusedtoidentifyperformanceproblemsonthe
serverside.Inanutshell,theprofilercantellustheexecutiontimeofcertainchunksof
code.Thereisnothingthatgreatwithitsbehavior.Wecanonlygettheexecutiontimeof
codeblocksorindividualexpressionsthathavebeenwrappedbytheprofiler’sstartand
stopmethods.Onitsown,Magentocallsfortheprofilerextensivelyacrossitscode.
However,wecan’tseeitineffectastheprofileroutputisdisabledbydefault.
Magentosupportsthreeprofileroutputs,namelyhtml,csvfile,andfirebug.
Toenabletheprofiler,wecanedit.htaccessandaddoneofthefollowingexpressions:
SetEnvMAGE_PROFILER"html"
SetEnvMAGE_PROFILER"csvfile"
SetEnvMAGE_PROFILER"firebug"
TheHTMLtypeofprofilerwillshowitsoutputintothefooterareaofapagethatweopen
inthebrowser,asshowninthefollowingscreenshot:
Thecsvfiletypeofprofilerwilloutputintovar/log/profiler.csv,asshowninthe
followingscreenshot:
Thefirebugtypeofprofilerwilloutputintovar/log/profiler.csv,asshowninthe
followingscreenshot:
Theprofileroutputsthefollowingpiecesofinformation:
TimeprofilershowsthetimespentfromProfiler::starttoProfiler::stop.
AvgprofilershowstheaveragetimespentfromProfiler::starttoProfiler::stop
forcaseswhereCntisgreaterthanone.
Cntprofilershowstheintegervalueofhowmanytimeswehavestartedtheprofiler
withthesametimername.Forexample,ifwehavecalled
\Magento\Framework\Profiler::start('foggyline:office');twicesomewhere
inthecode,thenCntwillshowthevalueof2.
EmallocprofilerstandsfortheamountofmemoryallocatedtoPHP.Itisamixofthe
corePHPmemory_get_usagefunctionwithoutthetrueparameterpassedtoitandthe
timervalues.
RealMemprofileralsostandsfortheamountofmemoryallocatedtoPHPwhosefinal
valueisalsoobtainedviathememory_get_usagefunctionminusthetimervalues,but
thistimewiththetrueparameterpassedtoit.
WecaneasilyaddourownProfiler::startcallsanywhereinthecode.Every
Profiler::startshouldbefollowedbysomecodeexpressionsandthenfinalizedwitha
Profiler::stopcall,asfollows:
\Magento\Framework\Profiler::start('foggyline:office');
sleep(2);/*codeblockorsingleexpressionhere*/
\Magento\Framework\Profiler::stop('foggyline:office');
Dependingonwherewecalltheprofilerinthecode,theresultingoutputshouldbesimilar
totheoneshowninthefollowingscreenshot:
Eventsandobservers
Magentoimplementstheobserverpatternthrough
\Magento\Framework\Event\ManagerInterface.Inapp/etc/di.xml,thereisa
preferenceforManagerInterfacethatpointstothe
Magento\Framework\Event\Manager\Proxyclasstype.TheProxyclassfurtherextends
the\Magento\Framework\Event\Managerclassthatimplementstheactualeventdispatch
method.
EventsaredispatchedbycallingadispatchmethodontheinstanceoftheEvent\Manager
classandpassingthenameandsomedata,whichisoptional,toit.Here’sanexampleofa
Magentocoreevent:
$this->eventManager->dispatch(
'customer_customer_authenticated',
['model'=>$this->getFullCustomerObject($customer),'password'=>
$password]
);
The$this->eventManagerisaninstanceofthepreviouslymentionedEvent\Manager
class.Inthiscase,theeventnameequalstocustomer_customer_authenticated,while
thedatapassedtotheeventisthearraywithtwoelements.Theprecedingeventisfired
whentheauthenticatemethodiscalledon
\Magento\Customer\Model\AccountManagement,thatis,whenacustomerlogsin.
Dispatchinganeventonlymakessenseifweexpectsomeonetoobserveitandexecute
theircodewhentheeventisdispatched.Dependingontheareafromwhichwewantto
observeevents,wecandefineobserversinoneofthefollowingXMLfiles:
app/code/{vendorName}/{moduleName}/etc/events.xml
app/code/{vendorName}/{moduleName}/etc/frontend/events.xml
app/code/{vendorName}/{moduleName}/etc/adminhtml/events.xml
Let’sdefineanobserverthatwilllogane-mailaddressofanauthenticateduserintoa
var/log/system.logfile.WecanusetheFoggyline_Officemoduleandaddsomecode
toit.Asweareinterestedinthestorefront,itmakessensetoputtheobserverinthe
etc/frontend/events.xmlmodule.
Let’sdefinetheapp/code/Foggyline/Office/etc/frontend/events.xmlfilewith
content,asfollows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:
Event/etc/events.xsd">
<eventname="customer_customer_authenticated">
<observername="foggyline_office_customer_authenticated"
instance="Foggyline\Office\Observer\LogCustomerEmail"/>
</event>
</config>
Here,wearespecifyingafoggyline_office_customer_authenticatedobserverforthe
customer_customer_authenticatedevent.Theobserverisdefinedinthe
LogCustomerEmailclassthatisplacedintheObservermoduledirectory.TheObserver
classhastoimplementtheMagento\Framework\Event\ObserverInterfaceclass.The
Observerinterfacedefinesasingleexecutemethod.Theexecutemethodhoststhe
observercodeandisexecutedwhenthecustomer_customer_authenticatedeventis
dispatched.
Let’sgoaheadanddefinetheFoggyline\Office\Observer\LogCustomerEmailclassin
theapp/code/Foggyline/Office/Observer/LogCustomerEmail.phpfile,asfollows:
namespaceFoggyline\Office\Observer;
useMagento\Framework\Event\ObserverInterface;
classLogCustomerEmailimplementsObserverInterface
{
protected$logger;
publicfunction__construct(
\Psr\Log\LoggerInterface$logger
)
{
$this->logger=$logger;
}
/**
*@param\Magento\Framework\Event\Observer$observer
*@returnself
*/
publicfunctionexecute(\Magento\Framework\Event\Observer$observer)
{
//$password=$observer->getEvent()->getPassword();
$customer=$observer->getEvent()->getModel();
$this->logger->info('Foggyline\Office:'.$customer->getEmail());
return$this;
}
}
Theexecutemethodtakesasingleparametercalled$observerofthe
\Magento\Framework\Event\Observertype.Theeventthatweareobservingispassing
twopiecesofdatawithinthearray,namelythemodelandpassword.Wecanaccessthisby
usingthe$observer->getEvent()->get{arrayKeyName}expression.The$customer
objectisaninstanceoftheMagento\Customer\Model\Data\CustomerSecureclass,which
containspropertiessuchasemail,firstname,lastname,andsoon.Thus,wecanextract
thee-mailaddressfromitandpassittologger’sinfomethod.
Nowthatweknowhowtoobserveexistingevents,let’sseehowwecandispatchourown
events.Wecandispatcheventsfromalmostanywhereinthecode,withorwithoutdata,as
showninthefollowingexample:
$this->eventManager->dispatch('foggyline_office_foo');
//or
$this->eventManager->dispatch(
'foggyline_office_bar',
['var1'=>'val1','var2'=>'val2']
);
Itisworthnotingthattherearetwotypesofevents;wecangrouptheminthefollowing
wayaccordingtothewaytheirnameisassigned:
Static:$this->eventManager->dispatch('event_name',...)
Dynamic:$this->eventManager->dispatch({expression}.'_event_name',...)
Thestaticeventshaveafixedstringforaname,whilethedynamiconeshaveanamethat
isdeterminedduringtheruntime.Here’saniceexampleofthecoreMagentofunctionality
fromtheafterLoadmethodthatisdefinedunder
lib/internal/Magento/Framework/Data/AbstractSearchResult.php,whichshowcases
howtousebothtypesofevents:
protectedfunctionafterLoad()
{
$this->eventManager->dispatch('abstract_search_result_load_after',
['collection'=>$this]);
if($this->eventPrefix&&$this->eventObject){
$this->eventManager->dispatch($this->eventPrefix.'_load_after',
[$this->eventObject=>$this]);
}
}
Wecanseeastaticevent(abstract_search_result_load_after)andadynamicevent
($this->eventPrefix.'_load_after').The$this->eventPrefixisanexpressionthat
getsevaluatedduringtheruntime.Weshouldbecarefulwhenusingdynamiceventsas
theyaretriggeredundermultiplesituations.Someinterestingdynamiceventsaretheone
definedonclasseslikethefollowingones:
Magento\Framework\Model\AbstractModel
$this->_eventPrefix.'_load_before'
$this->_eventPrefix.'_load_after'
$this->_eventPrefix.'_save_commit_after'
$this->_eventPrefix.'_save_before'
$this->_eventPrefix.'_save_after'
$this->_eventPrefix.'_delete_before'
$this->_eventPrefix.'_delete_after'
$this->_eventPrefix.'_delete_commit_after'
$this->_eventPrefix.'_clear'
\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
$this->_eventPrefix.'_load_before'
$this->_eventPrefix.'_load_after'
\Magento\Framework\App\Action\Action
'controller_action_predispatch_'.$request->getRouteName()
'controller_action_predispatch_'.$request->getFullActionName()
'controller_action_postdispatch_'.$request->getFullActionName()
'controller_action_postdispatch_'.$request->getRouteName()
Magento\Framework\View\Result\Layout
'layout_render_before_'.$this->request->getFullActionName()
Theseeventsarefiredonthemodel,collection,controller,andlayoutclasses,which
areprobablyamongthemostusedbackendelementsthatoftenrequireobservingand
interacting.Eventhoughwecansaythatthefulleventnameisknownduringtheruntime
alongwiththedynamicevent,thiscanbeassumedevenbeforetheruntime.
Forexample,assumingthatwewanttoobserve'controller_action_predispatch_'.
$request->getFullActionName()fortheFoggyline_Officemodule’sCrudcontroller
action,theactualfulleventnamewillbe
'controller_action_predispatch_foggyline_office_test_crud',giventhat
$request->getFullActionName()willresolvetofoggyline_office_test_crudduring
theruntime.
Cache(s)
Magentohaselevenout-of-the-boxcachetypes,accordingtothefollowinglist.Theseare
usedacrossmanylevelswithinthesystem:
Configuration:VariousXMLconfigurationsthatwerecollectedacrossmodulesand
merged
Layouts:Layoutbuildinginstructions
BlocksHTMLoutput:PageblocksHTML
Collectionsdata:Collectiondatafiles
Reflectiondata:APIinterfacesreflectiondata
DatabaseDDLoperations:ResultsofDDLqueries,suchasdescribingtablesor
indexes
EAVtypesandattributes:Entitytypesdeclarationcache
Pagecache:Fullpagecaching
Translations:Translationfiles
Integrationsconfiguration:Integrationconfigurationfile
IntegrationsAPIconfiguration:IntegrationsAPIconfigurationfile
Webservicesconfiguration:RESTandSOAPconfigurations,generatedWSDLfile
ThereisalsoAdditionalCacheManagementthatmanagesthecacheforthefollowing
files:
Previouslygeneratedproductimagefiles
ThemesJavaScriptandCSSfilescombinedtoonefile
Preprocessedviewfilesandstaticfiles
Eachofthesecachescanbeclearedseparately.
Wecaneasilydefineourowncachetype.Wecandosobyfirstcreatingan
app/code/Foggyline/Office/etc/cache.xmlfilewithcontent,asfollows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Cache/etc/cache.xsd">
<typename="foggyline_office"
instance="Foggyline\Office\Model\Cache">
<label>FoggylineOfficeExample</label>
<description>ExamplecachefromFoggylineOfficemodule.
</description>
</type>
</config>
Whendefininganewcachetype,weneedtospecifyitsnameandinstanceattributes.The
nameattributeofthetypeelementshouldbesettofoggyline_officeandshouldbe
uniqueacrossMagento.ThisvalueshouldmatchtheTYPE_IDENTIFIERconstantvalueon
theFoggyline\Office\Model\Cacheclass,whichwillbecreatedsoon.Theinstance
attributeholdstheclassnamethatwewilluseforcaching.
Then,wewilldefinetheFoggyline\Office\Model\Cacheclassinthe
app/code/Foggyline/Office/Model/Cache.phpfilewiththefollowingcontent:
namespaceFoggyline\Office\Model;
classCacheextends\Magento\Framework\Cache\Frontend\Decorator\TagScope
{
constTYPE_IDENTIFIER='foggyline_office';
constCACHE_TAG='OFFICE';
publicfunction__construct(
\Magento\Framework\App\Cache\Type\FrontendPool$cacheFrontendPool
)
{
parent::__construct(
$cacheFrontendPool->get(self::TYPE_IDENTIFIER),self::CACHE_TAG
);
}
}
TheCacheclassextendsfromTagScopeandspecifiesitsownvaluesforTYPE_IDENTIFIER
andCACHE_TAG,passingthemalongtotheparentconstructorinthe__constructmethod.
Withthesetwofiles(cache.xmlandCache),wehavebasicallydefinedanewcachetype.
Oncewehavespecifiedthecache.xmlfileandthereferencedcacheclass,weshouldbe
abletoseeourcachetypeintheMagentoadminundertheSystem|Tools|Cache
Managementmenu,asshowninthefollowingscreenshot:
Onitsown,simplydefininganewcachedoesnotmeanthatitwillgetfilledandusedby
Magento.
Ifyouwouldliketousethecacheanywherewithinyourcode,youcandosobyfirst
passingtheinstanceofthecacheclasstotheconstructor,asfollows:
protected$cache;
publicfunction__construct(
\Foggyline\Office\Model\Cache$cache
)
{
$this->cache=$cache;
}
Then,youcanexecuteachunkofcode,asfollows:
$cacheId='some-specific-id';
$objInfo=null;
$_objInfo=$this->cache->load($cacheId);
if($_objInfo){
$objInfo=unserialize($_objInfo);
}else{
$objInfo=[
'var1'=>'val1',
'var2'=>'val2',
'var3'=>'val3'
];
$this->cache->save(serialize($objInfo),$cacheId);
}
Theprecedingcodeshowshowwefirsttrytoloadthevaluefromtheexistingcacheentry,
andifthereisnone,wesaveit.IfthecachetypeissettodisabledundertheCache
Managementmenu,thentheprecedingcodewillneversaveandpullthedatafromthe
cache,asitisnotineffect.
Ifyoutakealookatthevar/cachefolderofMagentoatthispoint,youwillseesomething
similartowhat’sshowninthefollowingscreenshot:
Magentocreatedtwocacheentriesforus,namelyvar/cache/mage-tags/mage---
a8a_OFFICEandvar/cache/mage--f/mage---a8a_SOME_SPECIFIC_ID.Themage---
a8a_OFFICEfilehasonlyasinglelineofentryinthisspecificcase,andtheentryisthe
a8a_SOME_SPECIFIC_IDstring,whichobviouslypointstotheotherfile.Themage---
a8a_SOME_SPECIFIC_IDfilecontainstheactualserialized$objInfoarray.
Thea8a_prefixandotherprefixesinthecachefilenamesarenotreallyrelevanttous;
thisissomethingthatMagentoaddsonitsown.Whatisrelevanttousisthepassingof
properindividualcachetagstothechunksorvariablesthatwewanttocache,likeinthe
precedingexample,andtheTYPE_IDENTIFIERandCACHE_TAGtagsthatwesetforthe
Cacheclass.
Widgets
Magentoprovidessupportforwidgets.Thoughtheword“widget”mightimplyfrontend
developmentskillsandactivities,wewilllookatthemasapartofthebackend
developmentflowbecausecreatingusefulandrobustwidgetsrequiresasignificant
amountofbackendknowledge.
Magentoprovidesseveralout-of-the-boxwidgets;someofthemareasfollows:
CMSpagelink
CMSstaticblock
Catalogcategorylink
Catalognewproductslist
Catalogproductlink
Catalogproductslist
Ordersandreturns
Recentlycomparedproducts
Recentlyviewedproducts
Tocreateafullycustomwidget,westartbydefining
app/code/Foggyline/Office/etc/widget.xmlwithcontent,asfollows:
<widgetsxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:
Magento_Widget:etc/widget.xsd">
<widgetid="foggyline_office"
class="Foggyline\Office\Block\Widget\Example"
placeholder_image="Magento_Cms::images/widget_block.png">
<labeltranslate="true">FoggylineOffice</label>
<descriptiontranslate="true">ExampleWidget</description>
<parameters>
<parametername="var1"xsi:type="select"visible="true"
source_model="Magento\Config\Model\Config\Source\Yesno">
<labeltranslate="true">Yes/Novar1</label>
</parameter>
<parametername="var2"xsi:type="text"required="true"
visible="true">
<labeltranslate="true">Numbervar2</label>
<depends>
<parametername="var1"value="1"/>
</depends>
<value>5</value>
</parameter>
</parameters>
</widget>
</widgets>
Theidwidgethasbeensettofoggyline_office,whiletheclasspoweringwidgethas
beensettoFoggyline\Office\Block\Widget\Example.thewidgetclassisbasicallya
blockclassthatextendsfrom\Magento\Framework\View\Element\AbstractBlockand
implements\Magento\Widget\Block\BlockInterface.Thelabelanddescription
elementsetvaluesappearundertheMagentoadminwhenweselectthewidgetforuse.
TheparametersofawidgetareitsconfigurableoptionsthattranslateintoHTMLform
elements,dependingonthetypeandsource_modeloptionsthatwehaveselected.Inthe
followingexample,wewilldemonstratetheusageoftheselectandtextelementsto
retrieveinputfromauser,asshowninthefollowingscreenshot:
Let’sproceedbycreatingtheactualWidget\Exampleclassinthe
app/code/Foggyline/Office/Block/Widget/Example.phpfilewithcontent,asfollows:
namespaceFoggyline\Office\Block\Widget;
classExampleextends\Magento\Framework\View\Element\Textimplements
\Magento\Widget\Block\BlockInterface
{
protectedfunction_beforeToHtml()
{
$this->setText(sprintf(
'examplewidget:var1=%s,var2=%s',
$this->getData('var1'),
$this->getData('var2')
));
returnparent::_beforeToHtml();
}
}
WhatishappeninghereisthatweareusingElement\Textasablocktypeandnot
Element\Templatebecausewewanttosimplifytheexample,asElement\Templatewill
requirethephtmltemplatetobedefinedaswell.ByusingElement\Text,wecansimply
define_beforeToHtmlandcallthesetTextmethodtosetthetextstringoftheblock’s
output.Wewillbuildtheoutputstringbypickingupthevar1andvar2variables,which
werepassedasparameterstotheblock.
Now,ifweopentheMagentoadminarea,gotoContent|Elements|Pages,andselect
HomePagetoedit,weshouldbeabletoclickontheInsertFrontendAppbuttonand
addourwidgettothepage.Alternatively,ifwearenoteditingthepagecontentinthe
WYSIWYGmode,wecanalsoaddthewidgetmanuallytothepagebyusingthefollowing
expression:
{{widgettype="Foggyline\\Office\\Block\\Widget\\Example"var1="1"
var2="5"}}
Finally,weshouldseetheexamplewidget:var1=1,var2=5stringinthebrowserwhile
visitingthehomepageofthestorefront.
Wecanusefrontendappstocreatehighlyconfigurableandembeddablewidgetsthatusers
caneasilyassigntoaCMSpageorblock.
Customvariables
VariablesareahandylittlefeatureofacoreMagento_Variablemodule.Magentoallows
youtocreatecustomvariablesandthenusethemine-mailtemplates,theWYSIWYGeditor,
orevencodeexpressions.
Thefollowingstepsoutlinehowwecancreateanewvariablemanually:
1. IntheMagentoadminarea,navigatetoSystem|OtherSettings|CustomVariables.
2. ClickontheAddNewVariablebutton.
3. WhilekeepinginmindtheStoreViewswitcher,fillintherequiredVariableCode
andVariableNameoptions,andpreferablyoneoftheoptionaloptions,either
VariableHTMLValueorVariablePlainValue.
4. ClickontheSavebutton.
Nowthatwehavecreatedthecustomvariable,wecanuseitinane-mailtemplateorthe
WYSIWYGeditorbycallingitusingthefollowingexpression:
{{customVarcode=foggyline_hello}}
Theprecedingexpressionwillcallforthevalueofthecustomvariablewithcode
foggyline_hello.
Variablescanbeusedwithinvariouscodeexpressions,thoughitisnotrecommendedto
relyontheexistenceofanindividualvariable,asanadminusercandeleteitatanypoint.
Thefollowingexampledemonstrateshowwecanuseanexistingvariableinthecode:
$storeId=0;
$variable=$this->_variableFactory->create()->setStoreId(
$storeId
)->loadByCode(
'foggyline_hello'
);
$value=$variable->getValue(
\Magento\Variable\Model\Variable::TYPE_HTML
);
The$this->_variableFactoryisaninstanceof
\Magento\Variable\Model\VariableFactory.
Ifusedintherightway,variablescanbeuseful.Storinginformationsuchasphone
numbersorspecializedlabelsthatareusedinCMSpages,blogs,ande-mailtemplatesisa
niceexampleofusingcustomvariables.
i18n
i18nistheabbreviationforinternationalization.Magentoaddsi18nsupportoutofthe
box,thusadaptingtovariouslanguagesandregionswithoutapplicationchanges.Within
app/functions.php,thereisa__()translationfunction,whichisdefinedasfollows:
function__()
{
$argc=func_get_args();
$text=array_shift($argc);
if(!empty($argc)&&is_array($argc[0])){
$argc=$argc[0];
}
returnnew\Magento\Framework\Phrase($text,$argc);
}
Thistranslationfunctionacceptsavariablenumberofargumentsandpassesthemtoa
constructorofthe\Magento\Framework\Phraseclassandreturnsitsinstance.ThePhrase
classhasthe__toStringmethod,whichthenreturnsthetranslatedstring.
Hereareafewexamplesofhowwecanusethe__()function:
__('Translateme')
__('Var1%1,Var2%2,Var%3',time(),date('Y'),32)
__('Copyright%1<ahref="%2">Magento</a>',date('Y'),
'http://magento.com')
Stringspassedthroughthetranslationfunctionareexpectedtobefoundunderthelocal
CSVfiles,suchasapp/code/{vendorName}/{moduleName}/i18n/{localeCode}.csv.
Let’simagineforamomentthatwehavetwodifferentstoreviewsdefinedintheMagento
adminareaunderStores|Settings|AllStores.OnestorehasStore|Settings|
Configuration|General|LocaleOptions|LocalesettoEnglish(UnitedKingdom)
andtheotheronetoGerman(Germany).ThelocalcodeforEnglish(UnitedKingdom)
isen_GB,andforGerman(Germany),itisde_DE.
Forthede_DElocale,wewilladdtranslationentriesinthe
app/code/Foggyline/Office/i18n/de_DE.csvfile,asfollows:
"Translateme","de_DETranslateme"
"Var1%1,Var2%2,Var%3","de_DEVar1%1,Var2%2,Var%3"
"Copyright%1<ahref=""%2"">Magento</a>","de_DECopyright%1<a
href=""%2"">Magento</a>"
Fortheen_GBlocale,wewilladdtranslationentriesinthe
app/code/Foggyline/Office/i18n/en_GB.csvfile,asfollows:
"Translateme","en_GBTranslateme"
"Var1%1,Var2%2,Var%3","en_GBVar1%1,Var2%2,Var%3"
"Copyright%1<ahref=""%2"">Magento</a>","en_GBCopyright%1<a
href=""%2"">Magento</a>"
LookingatthetwoCSVfiles,apatternemerges.WecanseethattheCSVfilesfunctionin
thefollowingway:
IndividualtranslationstringsareprovidedaccordingtoeverylineofCSV
Eachlinefurthercomprisestwoindividualstringsthatareseparatedbyacomma
Bothindividualstringsaresurroundedbyquotes
Ifastringcontainsquotes,itisescapedbyadoublequotesothatitdoesnotbreak
translation
The%1,%2,%3…%npatternisusedtomarkvariableplaceholdersthatweprovided
duringapplicationruntimethroughthecode
Magentosupportsseveralcommandsrelatedtoitsbin/magentoconsoletool:
i18n
i18n:collect-phrasesDiscoversphrasesinthecodebase
i18n:packSaveslanguagepackage
i18n:uninstallUninstallslanguagepackages
Ifweexecuteaconsolecommandasfollows,Magentowillrecursivelylookfor
translatableexpressionswithinPHP,PHTML,orXMLfilesthathavephrasestotranslate:
phpbin/magentoi18n:collect-phrases-o
"/Users/branko/www/magento2/app/code/Foggyline/Office/i18n/en_GB.csv"
/Users/branko/www/magento2/app/code/Foggyline/Office
Theoutputoftheprecedingcommandwillbasicallyoverwritethe
app/code/Foggyline/Office/i18n/en_GB.csvfile,whichhasalltheFoggyline/Office
moduletranslatablephrases.Thisisanicewayofaggregatingallthetranslatablephrases
intoappropriatelocalefiles,suchasen_GB.csvinthiscase.
ThetranslationCSVfilescanalsobeplacedundertheindividualtheme.Forexample,
let’simagineasituationwhereweaddcontentto
app/design/frontend/Magento/blank/i18n/en_GB.csv,asfollows:
"Translateme","Theme_en_GBTranslateme"
"Var1%1,Var2%2,Var%3","Theme_en_GBVar1%1,Var2%2,Var%3"
"Copyright%1<ahref=""%2"">Magento</a>","Theme_en_GBCopyright%1<a
href=""%2"">Magento</a>"
Now,aTranslatemestringoutputofthestorefrontfortheen_GBlocalewouldresolveto
Theme_en_GBTranslatemeandnottotheen_GBTranslatemestring.
Tip
ThemeCSVtranslationstakehigherprecedencethanmoduleCSVtranslations,thus
enablingdeveloperstooverrideindividualmoduletranslations.
AlongwithCSVtranslationfiles,Magentoalsosupportsafeaturecalledinline
translation.WecanactivatetheinlinetranslationintheMagentoadminareaby
navigatingtoStore|Settings|Configuration|Advanced|Developer|TranslateInline.
Thisfeaturecanbeturnedonseparatelyforadminandstorefront,asshowninthe
followingscreenshot:
Asshownintheprecedingscreenshot,whenafeatureisactivated,reddottedborders
appeararoundtheHTMLelements.Hoveringoveranindividualelementshowsalittle
bookiconneartheindividualelementatthebottomleftcorner.Clickingonthebookicon
opensapopup,asshowninthefollowingscreenshot:
Itisimportanttonotethatthesereddottedbordersandthebookiconwillonlyappearfor
stringsthatwepassedthroughthe__()translatefunction.
Here,wecanseevariouspiecesofinformationaboutthestring,suchastheShown,
Translated,andOriginalstring.ThereisalsoaninputfieldcalledCustom,wherewecan
addanewtranslation.Inlinetranslationstringsarestoredinthetranslationtableinthe
database.
Tip
InlinetranslationtakeshigherprecedencethanthemeCSVtranslationfiles.
Indexer(s)
Indexingistheprocessoftransformingdatabyreducingittoflatteneddatawithless
databasetables.Thisprocessisrunforproducts,categories,andsooninordertoimprove
theperformanceofawebstore.Sincedataconstantlychanges,thisisnotaone-time
process.Rather,itisaperiodicone.TheMagento_Indexermoduleisabaseofthe
MagentoIndexingfunctionality.
TheMagentoconsoletoolsupportsthefollowingindexercommands.
indexer
indexer:infoShowsallowedIndexers
indexer:reindexReindexesData
indexer:set-modeSetsindexmodetype
indexer:show-modeShowsIndexMode
indexer:statusShowsstatusofIndexer
Onrunningphpbin/magentoindexer:info,youwillgetalistofalltheMagento
indexers;thedefaultonesareasfollows:
catalog_category_productCategoryProducts
catalog_product_categoryProductCategories
catalog_product_priceProductPrice
catalog_product_attributeProductEAV
foggyline_office_employeeEmployeeFlatData
cataloginventory_stockStock
catalogrule_ruleCatalogRuleProduct
catalogrule_productCatalogProductRule
catalogsearch_fulltextCatalogSearch
YouwillseealltheindexerslistedintheMagentoadminintheSystem|Tools|Index
Managementmenu.
Fromwithintheadminarea,wecanonlychangetheindexermode.Therearetwomodes
ofindexers:
UpdateonSave:Indextablesareupdatedrightafterthedictionarydataischanged
UpdatebySchedule:Indextablesareupdatedbycronjobsaccordingtothe
configuredschedule
Sinceindexerscannotberunmanuallyfromadmin,wehavetorelyeitherontheirmanual
executionorthecronexecution.
Manualexecutionisdoneviathefollowingconsolecommand:
phpbin/magentoindexer:reindex
Theprecedingcommandwillrunalltheindexersatonce.Wecanfine-tuneitfurtherto
executeindividualindexesbyrunningaconsolecommandthatissimilartothefollowing
lineofcode:
phpbin/magentoindexer:reindexcatalogsearch_fulltext
Cron-executedindexersaredefinedviatheMagento_Indexermodule,asfollows:
indexer_reindex_all_invalid:Thiswillexecuteeveryminuteofeveryhourevery
day.ItrunsthereindexAllInvalidmethodonaninstanceofthe
Magento\Indexer\Model\Processorclass.
indexer_update_all_views:Thiswillexecuteeveryminuteofeveryhourevery
day.ItrunstheupdateMviewmethodonaninstanceofthe
Magento\Indexer\Model\Processorclass.
indexer_clean_all_changelogs:Thiswillexecutethe0thminuteofeveryhour
everyday.ItrunstheclearChangelogmethodonaninstanceofthe
Magento\Indexer\Model\Processorclass.
ThesecronjobsuseanoperatingsystemcronjobsetupinsuchawaythattheMagento
cronjobistriggeredeveryminute.
Thefollowingthreestatusesiswhatanindexercanhave:
valid:Thedataissynchronizedandnore-indexingisrequired
invalid:Theoriginaldatawaschangedandtheindexshouldbeupdated
working:Theindexprocessisrunning
Whilewewon’tgointothedetailsofactuallycreatingacustomindexerwithinthis
chapter,itisworthnotingthatMagentodefinesitsindexersinthe
vendor/magento/module-*/etc/indexer.xmlfile.Thismightcomeinhandyforcases
wherewewantadeeperunderstandingoftheinnerworkingsofanindividualindexer.For
example,thecatalog_product_flatindexerisimplementedviathe
Magento\Catalog\Model\Indexer\Product\Flatclass,asdefinedwithinthe
vendor/magento/module-catalog/etc/indexer.xmlfile.BystudyingtheFlatclass
implementationindepth,youcanlearnhowdataistakenfromEAVtablesandflattened
intoasimplifiedstructure.
Summary
Inthischapter,wecoveredsomeofthemostrelevantaspectsofMagento,whichwas
beyondmodelsandclasses,regardingbackenddevelopment.Wehadalookat
crontab.xml,whichhelpsusschedulejobs(commands)sothattheycanberun
periodically.Then,wetacklednotificationmessages,whichenableustopushstyled
messagestousersviaabrowser.TheSessionandcookiessectiongaveusan
understandingofhowMagentotracksuserinformationfromabrowsertoasession.
Loggingandprofilingshowedusasimpleyeteffectivemechanismtokeeptrackof
performanceandpossibleissuesacrosscode.TheEventsandobserverssectionintroduced
ustoapowerfulpatternthatMagentoimplementsacrossthecode,wherewecantrigger
customcodeexecutionwhenacertaineventisfired.Thesectiononcachingguidedus
throughtheavailablecachetypes,andwestudiedhowtocreateanduseourowncache
type.Throughthesectiononfrontendapps(widgets),welearnedhowtocreateourown
miniatureappsthatcanbecalledintoCMSpagesandblocks.Customvariablesgaveusan
insightintoasimpleyetinterestingfeature,wherewecandefineavariableviatheadmin
interfaceandthenuseitwithinCMSpage,block,ore-mailtemplate.Thesectiononi18n
showedushowtousetheMagentotranslationfeaturetotranslateanystringonthree
differentlevels,namelythemoduleCSVfile,thethemeCSVfile,andinlinetranslation.
Finally,wehadalookatindexersandtheirmodeandstatus;welearnedhowtocontrol
theirexecution.
Thenextchapterwilltacklefrontenddevelopment.Wewilllearnhowcreateourown
themeanduseblocksandlayoutstoaffecttheoutput.
Chapter8.FrontendDevelopment
FrontenddevelopmentisatermmostcommonlytiedtoproducingHTML,CSS,and
JavaScriptforawebsiteorwebapplication.Interchangeably,itaddressesaccessibility,
usability,andperformancetowardreachingasatisfyinguserexperience.Variouslevelsof
customizationwewanttoapplytoourwebstorerequiredifferentdevelopmentskill
levels.WecanmakerelativelysimplechangestoourstoreusingjustCSS.Thesewould
bethechangeswhereweacceptthestructureofthestoreandfocusonlyonvisualslike
changingcolorsandimages.Thismightbeagoodstartingpointforlessexperienced
developersandthosenewtotheMagentoplatform.Amoreinvolvedapproachwouldbe
tomakechangestotheoutputgeneratedbyMagentomodules.Thisusuallymeanstiny
bitsofPHPknowledge,mostlycopy-paste-modifyofexistingcodefragments.Askilllevel
abovethisonewouldimplyknowledgeofmakingstructuralchangestoourstore.This
usuallymeansmasteringMagento’smoderatelysophisticatedlayoutengine,wherewe
makechangesthroughXMLdefinitions.ThefinalandhighestskilllevelforMagento
frontenddevelopmentimpliesthemodificationofexistingornewcustomfunctionality
development.
Throughoutthischapter,wewilltakeadeepdivethroughthefollowingsections:
Renderingflow
Viewelements
Blockarchitectureandlifecycle
Templates
XMLlayouts
Themes
JavaScript
CSS
Renderingflow
TheMagentoapplicationentrypointisitsindex.phpfile.AlloftheHTTPrequestsgo
throughit.
Let’sanalyzethe(trimmed)versionoftheindex.phpfileasfollows:
//PART-1-1
require__DIR__.'/app/bootstrap.php';
//PART-1-2
$bootstrap=\Magento\Framework\App\Bootstrap::create(BP,$_SERVER);
//PART-1-3
$app=$bootstrap->createApplication('Magento\Framework\App\Http');
//PART-1-4
$bootstrap->run($app);
PART-1-1oftheprecedingcodesimplyincludes/app/bootstrap.phpintothecode.What
happensinsidethebootstrapistheinclusionofapp/autoload.phpand
app/functions.php.Thefunctionsfilecontainsasingle__()function,usedfor
translationpurposes,returninganinstanceofthe\Magento\Framework\Phraseobject.
Withoutgoingintothedetailsoftheauto-loadfile,itissufficetosayithandlestheauto-
loadingofallourclassfilesacrossMagento.
PART-1-2issimplyastaticcreatemethodcalltoobtaintheinstanceofthe
\Magento\Framework\App\Bootstrapobject,storingitintothe$bootstrapvariable.
PART-1-3iscallingthecreateApplicationmethodonthe$bootstrapobject.Whatis
happeningwithincreateApplicationisnothingmorethanusingobjectmanagertocreate
andreturntheobjectinstanceoftheclasswearepassingtoit.Sincewearepassingthe
\Magento\Framework\App\HttpclassnametothecreateApplicationmethod,our$app
variablebecomestheinstanceofthatclass.Whatthismeans,effectively,isthatourweb
storeappisaninstanceofMagento\Framework\App\Http.
PART-1-4iscallingtherunmethodonthe$bootstrapobject,passingittheinstanceofthe
Magento\Framework\App\Httpclass.Althoughitlookslikeasimplelineofcode,thisis
wherethingsgetcomplicated,aswewillsoonsee.
Let’sanalyzethe(trimmed)versionofthe\Magento\Framework\App\Bootstrap->run
methodasfollows:
publicfunctionrun(\Magento\Framework\AppInterface$application)
{
//PART-2-1
$this->initErrorHandler();
$this->initObjectManager();
$this->assertMaintenance();
$this->assertInstalled();
//PART-2-2
$response=$application->launch();
//PART-2-3
$response->sendResponse();
}
Intheprecedingcode,PART-2-1handlesthesortofhousekeepingbits.Itinitializesthe
customerrorhandler,initializestheobjectmanager,checksifourapplicationisin
maintenancemode,andchecksthatitisinstalled.
PART-2-2lookslikeasimplelineofcode.Here,wearecallingthelaunchmethodon
$application,whichistheMagento\Framework\App\Httpinstance.Withoutgoinginto
theinnerworkingsofthelaunchmethodforthemoment,let’sjustsayitreturnsthe
instanceoftheMagento\Framework\App\Response\Http\Interceptorclassdefined
undervar/generation/Magento/Framework/App/Response/Http/Interceptor.php.
Notethatthisisanautomaticallygeneratedwrapperclass,extendingthe
\Magento\Framework\App\Response\Httpclass.Effectively,ignoringInterceptor,we
cansaythat$responseisaninstancethe\Magento\Framework\App\Response\Http
class.
Finally,PART-2-3callsthesendResponsemethodon$response.Though$responseisan
instanceofthe\Magento\Framework\App\Response\Httpclass,theactualsendResponse
methodisfoundfurtherdowntheparenttreeonthe
\Magento\Framework\HTTP\PhpEnvironment\Responseclass.ThesendResponsemethod
callsanotherparentclassmethodcalledsend.Thesendmethodcanbefoundunderthe
Zend\Http\PhpEnvironment\Responseclass.IttriggersthesendHeadersand
sendContentmethods.Thisiswheretheactualoutputgetssenttothebrowser,asthe
sendHeadersmethodisusingPHP’sheaderfunctionandechoconstructtopushthe
output.
Toreiterateonthepreceding,theflowofexecutionasweunderstanditcomesdowntothe
following:
index.php
\Magento\Framework\App\Bootstrap->run
\Magento\Framework\App\Http->launch
\Magento\Framework\App\Response\Http->sendResponse
Thoughwehavejustmadeittotheendofthebootstrap’srunmethod,itwouldbeunfair
tosaywecoveredtherenderingflow,aswebarelytouchedit.
WeneedtotakeastepbackandtakeadetailedlookatPART-2-2,theinnerworkingsof
thelaunchmethod.Let’stakealookatthe(trimmed)versionofthe
\Magento\Framework\App\Http->launchmethodasfollows:
publicfunctionlaunch()
{
//PART-3-1
$frontController=$this->_objectManager->get
('Magento\Framework\App\FrontControllerInterface');
//PART-3-2
$result=$frontController->dispatch($this->_request);
if($resultinstanceof\Magento\Framework\Controller\ResultInterface)
{
//PART-3-3
$result->renderResult($this->_response);
}elseif($resultinstanceof\Magento\Framework\App
\Response\HttpInterface){
$this->_response=$result;
}else{
thrownew\InvalidArgumentException('Invalidreturntype');
}
//PART-3-4
return$this->_response;
}
PART-3-1createstheinstanceoftheobjectwhoseclassconformsto
\Magento\Framework\App\FrontControllerInterface.Ifwelookunder
app/etc/di.xml,wecanseethereisapreferenceforFrontControllerInterfacein
favorofthe\Magento\Framework\App\FrontControllerclass.However,ifwewereto
debugthecodeandcheckfortheactualinstanceclass,itwouldshow
Magento\Framework\App\FrontController\Interceptor.ThisisMagentoaddingan
interceptorwrapperthatthenextends\Magento\Framework\App\FrontController,which
weexpectedfromthedi.xmlpreferenceentry.
Nowthatweknowtherealclassbehindthe$frontControllerinstance,weknowwhere
tolookforthedispatchmethod.Thedispatchmethodisanotherimportantstepin
understandingtherenderingflowprocess.Wewilllookintoitsinnerworkingsinabit
moredetaillateron.Fornow,let’sfocusbackonthe$resultvariableofPART-3-2.Ifwe
weretodebugthevariable,thedirectclassbehinditwouldshowas
Magento\Framework\View\Result\Page\Interceptor,definedunderthedynamically
createdvar/generation/Magento/Framework/View/Result/Page/Interceptor.phpfile.
Interceptoristhewrapperforthe\Magento\Framework\View\Result\Pageclass.Thus,
itissafetosaythatour$resultvariableisaninstanceofthePageclass.
ThePageclassextends\Magento\Framework\View\Result\Layout,whichfurther
extends\Magento\Framework\Controller\AbstractResultandimplements
\Magento\Framework\Controller\ResultInterface.Quiteachainwehavehere,butit
isimportanttounderstandit.
NoticePART-3-3.Sinceour$resultisaninstanceof
\Magento\Framework\Controller\ResultInterface,wefallintothefirstifcondition
thatcallstherenderResultmethod.TherenderResultmethoditselfisdeclaredwithin
the\Magento\Framework\View\Result\Layoutclass.Withoutgoingintothedetailsof
renderResult,sufficetosaythatitaddsHTTPheaders,andcontenttothe$this-
>_responseobjectpassedtoit.Thatsameresponseobjectiswhatthelaunchmethod
returns,aswedescribedbeforeinPART-2-2.
ThoughPART-3-3doesnotdepictanyreturnvalue,theexpression$result-
>renderResult($this->_response)doesnotdoanyoutputonitsown.Itmodifies
$this->_responsethatwefinallyreturnfromthelaunchmethodasshowninPART-3-4.
Toreiterateonthepreceding,theflowofexecutionasweunderstanditcomesdowntothe
following:
index.php
\Magento\Framework\App\Bootstrap->run
\Magento\Framework\App\Http->launch
\Magento\Framework\App\FrontController->dispatch
\Magento\Framework\View\Result\Page->renderResult
\Magento\Framework\App\Response\Http->sendResponse
AswementionedwhileexplainingPART-3-2,thedispatchmethodisanotherimportant
stepintherenderingflowprocess.Let’stakealookatthe(trimmed)versionofthe
\Magento\Framework\App\FrontController->dispatchmethodasfollows:
publicfunctiondispatch(\Magento\Framework\App\RequestInterface$request)
{
//PART-4-1
while(!$request->isDispatched()&&$routingCycleCounter++<100){
//PART-4-2
foreach($this->_routerListas$router){
try{
//PART-4-3
$actionInstance=$router->match($request);
if($actionInstance){
$request->setDispatched(true);
//PART-4-4
$result=$actionInstance->dispatch($request);
break;
}
}catch(\Magento\Framework\Exception\NotFoundException$e){}
}
}
//PART-4-4
return$result;
}
PART-4-1andPART-4-2intheprecedingcodeshows(almost)theentiredispatchmethod
bodycontainedwithinaloop.Theloopdoes100iterations,furtherloopingthroughall
availableroutertypes,thusgivingeachrouter100timestofindaroutematch.
Therouterlistloopincludesroutersofthefollowingclasstypes:
Magento\Framework\App\Router\Base
Magento\UrlRewrite\Controller\Router
Magento\Cms\Controller\Router
Magento\Framework\App\Router\DefaultRouter
Allofthelistedroutersimplement\Magento\Framework\App\RouterInterface,making
themallhavetheimplementationofthematchmethod.
Amodulecanfurtherdefinenewroutersiftheychooseso.Asanexample,imagineifwe
aredevelopingaBlogmodule.WewouldwantourmodulecatchingallrequestsonaURL
thatstartswitha/blog/part.Thiscanbedonebyspecifyingthecustomrouter,which
wouldthenshowupontheprecedinglist.
PART-4-3showsthe$actionInstancevariablestoringtheresultoftheroutermatch
methodcall.AsperRouterInterfacerequirements,thematchmethodisrequiredto
returnaninstancewhoseclassimplements\Magento\Framework\App\ActionInterface.
Let’simaginewearenowhittingtheURL/foggyline_office/test/crud/fromthe
modulewewroteinChapter4,ModelsandCollections.Inthiscase,our$routerclass
wouldbe\Magento\Framework\App\Router\Baseandour$actionInstancewouldbeof
theclass\Foggyline\Office\Controller\Test\Crud\Interceptor.Magento
automaticallyaddsInterceptor,throughthedynamicallygenerated
var/generation/Foggyline/Office/Controller/Test/Crud/Interceptor.phpfile.
ThisInterceptorclassfurtherextendsourmodule
\Foggyline\Office\Controller\Test\Crudclassfile.TheCrudclassextends
\Foggyline\Office\Controller\Test,whichfurtherextends
\Magento\Framework\App\Action\Action,whichimplements
\Magento\Framework\App\ActionInterface.Afteralengthyparent-childtree,wefinally
gottoActionInterface,whichiswhatourmatchmethodisrequiredtoreturn.
PART-4-4showsthedispatchmethodbeingcalledon$actionInstance.Thismethodis
implementedwithin\Magento\Framework\App\Action\Action,andisexpectedtoreturn
anobjectthatimplements\Magento\Framework\App\ResponseInterface.Internalto
dispatch,theexecutemethodiscalled,thusrunningthecodewithinourCrudcontroller
actionexecutemethod.
AssumingourCrudcontrolleractionexecutemethoddoesnotreturnnothing,the$result
objectbecomesaninstanceofMagento\Framework\App\Response\Http\Interceptor,
whichiswrappedaround\Magento\Framework\App\Response\Http.
Let’simagineourCrudclasshasbeendefinedasfollows:
/**
*@var\Magento\Framework\View\Result\PageFactory
*/
protected$resultPageFactory;
publicfunction__construct(
\Magento\Framework\App\Action\Context$context,
\Magento\Framework\View\Result\PageFactory$resultPageFactory
)
{
$this->resultPageFactory=$resultPageFactory;
returnparent::__construct($context);
}
publicfunctionexecute()
{
$resultPage=$this->resultPageFactory->create();
//...
return$resultPage;
}
Debuggingthe$resultvariablenowshowsit’saninstanceof
\Magento\Framework\View\Result\Page\Interceptor.ThisInterceptorgets
dynamicallygeneratedbyMagentounder
var/generation/Magento/Framework/View/Result/Page/Interceptor.phpandis
merelyawrapperfor\Magento\Framework\View\Result\Page.ThisPageclassfurther
extendsthe\Magento\Framework\View\Result\Layoutclass,andimplements
\Magento\Framework\App\ResponseInterface.
Finally,PART-4-4showsthe$resultobjectoftype
\Magento\Framework\View\Result\PagebeingreturnedfromtheFrontController
dispatchmethod.
Toreiterateonthepreceding,theflowofexecutionasweunderstanditcomesdowntothe
following:
index.php
\Magento\Framework\App\Bootstrap->run
\Magento\Framework\App\Http->launch
\Magento\Framework\App\FrontController->dispatch
\Magento\Framework\App\Router\Base->match
\Magento\Framework\App\Action\Action->dispatch
\Magento\Framework\View\Result\Page->renderResult
\Magento\Framework\App\Response\Http->sendResponse
Inanutshell,whatweasfrontenddevelopersshouldknowisthatreturningthePagetype
objectfromourcontrolleractionwillautomaticallycalltherenderResultmethodonthat
object.PageandLayoutiswhereallthethemetranslations,layout,andtemplateloading
aretriggering.
Viewelements
Magento’sprimaryviewelementsareitsUIComponents,containers,andblocks.The
followingisabriefoverviewofeachofthem.
Uicomponents
Underthevendor/magento/framework/View/Element/folder,wecanfind
UiComponentInterfaceandUiComponentFactory.ThefullsetofUicomponentsis
locatedunderthevendor/magento/framework/View/Element/directory.Magento
implementsUiComponentthroughaseparatemodulecalledMagento_Ui.Thus,the
componentsthemselvesarelocatedunderthevendor/magento/module-ui/Component/
directory.
ComponentsimplementUiComponentInterface,whichisdefinedunderthe
vendor/magento/framework/View/Element/UiComponentInterface.phpfileasfollows:
namespaceMagento\Framework\View\Element;
useMagento\Framework\View\Element\UiComponent\ContextInterface;
interfaceUiComponentInterfaceextendsBlockInterface
{
publicfunctiongetName();
publicfunctiongetComponentName();
publicfunctiongetConfiguration();
publicfunctionrender();
publicfunctionaddComponent($name,UiComponentInterface$component);
publicfunctiongetComponent($name);
publicfunctiongetChildComponents();
publicfunctiongetTemplate();
publicfunctiongetContext();
publicfunctionrenderChildComponent($name);
publicfunctionsetData($key,$value=null);
publicfunctiongetData($key='',$index=null);
publicfunctionprepare();
publicfunctionprepareDataSource(array&$dataSource);
publicfunctiongetDataSourceData();
}
NoticehowBlockInterfaceextendsBlockInterface,whereasBlockInterfacedefines
onlyonemethodrequirementasfollows:
namespaceMagento\Framework\View\Element;
interfaceBlockInterface
{
publicfunctiontoHtml();
}
SinceBlockisanelementoftheinterface,UiComponentcanbelookedatasanadvanced
block.Let’stakeaquicklookatthe_renderUiComponentmethodofthe
\Magento\Framework\View\Layoutclass,(partially)definedasfollows:
protectedfunction_renderUiComponent($name)
{
$uiComponent=$this->getUiComponent($name);
return$uiComponent?$uiComponent->toHtml():'';
}
ThisshowsthatUiComponentisrenderedinthesamewayasblock,bycallingthetoHtml
methodonthecomponent.Thevendor/magento/module-
ui/view/base/ui_component/etc/definition.xmlfilecontainsanextensivelistof
severalUiComponentsasfollows:
dataSource:Magento\Ui\Component\DataSource
listing:Magento\Ui\Component\Listing
paging:Magento\Ui\Component\Paging
filters:Magento\Ui\Component\Filters
container:Magento\Ui\Component\Container
form:Magento\Ui\Component\Form
price:Magento\Ui\Component\Form\Element\DataType\Price
image:Magento\Ui\Component\Form\Element\DataType\Media
nav:Magento\Ui\Component\Layout\Tabs\Nav
…andmanymore
Thesecomponentsaremostlyusedtoconstructalistingandfiltersintheadminarea.If
wedoastringsearchforuiComponentacrosstheentireMagento,wewouldmostlyfind
entriesliketheoneinvendor/magento/module-
cms/view/adminhtml/layout/cms_block_index.xmlwithcontentasfollows:
<pagexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout
/etc/page_configuration.xsd">
<body>
<referenceContainername="content">
<uiComponentname="cms_block_listing"/>
</referenceContainer>
</body>
</page>
Thevaluecms_block_listingofuiComponent‘snameattributereferstothenameofthe
vendor/magento/module-cms
/view/adminhtml/ui_component/cms_block_listing.xmlfile.Withinthe
cms_block_listing.xmlfile,wehavealistingcomponentdefinedacrossmorethanafew
hundredslinesofXML.ListingcomponentthendataSource,container,bookmark,
filterSearch,filters,andsoon.Wewillnotgointothedetailsofthesedeclarations,as
ourfocushereisonmoregeneralfrontendbits.
Containers
Containershavenoblockclassesrelatedtothem.Containerrendersallofitschildren
automatically.Theyallowtheconfigurationofsomeattributes.Simplyattachanyelement
toacontaineranditwillrenderitautomatically.Withacontainer,wecandefinewrapping
tags,CSSclasses,andmore.
Wecannotcreateinstancesofcontainersbecausetheyareanabstractconcept,whereaswe
cancreateinstancesofblocks.
Containersarerenderedviathe_renderContainermethodofthe
Magento\Framework\View\Layoutclass,definedasfollows:
protectedfunction_renderContainer($name)
{
$html='';
$children=$this->getChildNames($name);
foreach($childrenas$child){
$html.=$this->renderElement($child);
}
if($html==''||!$this->structure->getAttribute($name,
Element::CONTAINER_OPT_HTML_TAG)){
return$html;
}
$htmlId=$this->structure->getAttribute($name,
Element::CONTAINER_OPT_HTML_ID);
if($htmlId){
$htmlId='id="'.$htmlId.'"';
}
$htmlClass=$this->structure->getAttribute($name,
Element::CONTAINER_OPT_HTML_CLASS);
if($htmlClass){
$htmlClass='class="'.$htmlClass.'"';
}
$htmlTag=$this->structure->getAttribute($name,
Element::CONTAINER_OPT_HTML_TAG);
$html=sprintf('<%1$s%2$s%3$s>%4$s</%1$s>',$htmlTag,$htmlId,
$htmlClass,$html);
return$html;
}
Containerssupportthefollowingextraattributes:htmlTag,htmlClass,htmlId,andlabel.
Tomakealittledemonstrationofacontainerinaction,letusmakesurewehaveamodule
fromChapter4,ModelsandCollectionsinplace,andthencreatethe
view/frontend/layout/foggyline_office_test_crud.xmlfilewithinthemoduleroot
folderapp/code/Foggyline/Office/withcontentasfollows:
<pagexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
layout="1column"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View
/Layout/etc/page_configuration.xsd">
<head>
<title>OfficeCRUD#layout</title>
</head>
<body>
<containername="foobar"htmlTag="div"htmlClass="foo-bar">
<blockclass="Magento\Framework\View\Element\Text"name="foo">
<actionmethod="setText">
<argumentname="text"xsi:type="string"><!
[CDATA[<p>TheFoo</p>]]></argument>
</action>
</block>
<blockclass="Magento\Framework\View\Element\Text"name="bar">
<actionmethod="setText">
<argumentname="text"xsi:type="string"><!
[CDATA[<p>TheBar</p>]]></argument>
</action>
</block>
</container>
</body>
</page>
TheprecedingXMLdefinesasinglecontainernamedfoobar,andwithinthecontainer
therearetwoblockelementsnamedfooandbar.Itshouldkickinwhenweopen
http://{our-shop-url}/index.php/foggyline_office/test/crud/inthebrowser.
Noticehowthecontaineritselfisnotnestedwithinanyotherelement,ratherdirectlyinto
thebody.Wecouldhaveeasilynestedintosomeothercontainerasshown:
<body>
<referenceContainername="content">
<containername="foobar"htmlTag="div"htmlClass="foo-bar">
Eitherway,weshouldseethestringsTheFooandTheBarshowninthebrowser,witha
full-pagelayoutloaded,asshowninthefollowingscreenshot:
Blocks
Althoughcontainersdeterminethelayoutofthepage,theydonotcontainactualcontent
directly.Piecesthatcontainthecontentandarenestedwithincontainersarecalledblocks.
Eachblockcancontainanynumberofchildcontentblocksorchildcontainers.Thus,
mostlyeverywebpageinMagentoisformedasamixofblocksandcontainers.Layout
definesasequenceofblocksonthepage,nottheirlocation.Thelookandfeelofthe
blocksisdeterminedbyCSSandhowthepageisrendered.Whenwespeakofblocks,we
almostalwaysimplicitlyrefertotemplatesaswell.Templatesarethethingthatactually
drawelementswithinapage;blocksarethethingthatcontainthedata.Inotherwords,
templatesarePHTMLorHTMLfilespullingdatathroughvariablesormethodssentona
linkedPHPblockclass.
MagentodefinestheMagento\Framework\View\Result\Pagetypeunderapp/etc/di.xml
asfollows:
<typename="Magento\Framework\View\Result\Page">
<arguments>
<argumentname="layoutReaderPool"
xsi:type="object">pageConfigRenderPool</argument>
<argumentname="generatorPool"
xsi:type="object">pageLayoutGeneratorPool</argument>
<argumentname="template"
xsi:type="string">Magento_Theme::root.phtml</argument>
</arguments>
</type>
NoticethetemplateargumentissettoMagento_Theme::root.phtml.WhenPagegets
initialized,itpicksupthevendor/magento/module-
theme/view/base/templates/root.phtmlfile.root.phtmlisdefinedasfollows:
<!doctypehtml>
<html<?phpecho$htmlAttributes?>>
<head<?phpecho$headAttributes?>>
<?phpecho$requireJs?>
<?phpecho$headContent?>
<?phpecho$headAdditional?>
</head>
<bodydata-container="body"data-mage-init='{"loaderAjax":{},
"loader":{"icon":"<?phpecho$loaderIcon;?>"}}'<?phpecho
$bodyAttributes?>>
<?phpecho$layoutContent?>
</body>
</html>
Variableswithinroot.phtmlareassignedduringthe
Magento\Framework\View\Result\Pagerendermethodcallas(partially)asshown:
protectedfunctionrender(ResponseInterface$response)
{
$this->pageConfig->publicBuild();
if($this->getPageLayout()){
$config=$this->getConfig();
$this->addDefaultBodyClasses();
$addBlock=$this->getLayout()->getBlock('head.additional');
$requireJs=$this->getLayout()->getBlock('require.js');
$this->assign([
'requireJs'=>$requireJs?$requireJs->toHtml():null,
'headContent'=>$this->pageConfigRenderer->
renderHeadContent(),
'headAdditional'=>$addBlock?$addBlock->toHtml():null,
'htmlAttributes'=>$this->pageConfigRenderer->
renderElementAttributes($config::ELEMENT_TYPE_HTML),
'headAttributes'=>$this->pageConfigRenderer->
renderElementAttributes($config::ELEMENT_TYPE_HEAD),
'bodyAttributes'=>$this->pageConfigRenderer->
renderElementAttributes($config::ELEMENT_TYPE_BODY),
'loaderIcon'=>$this->getViewFileUrl('images/loader-2.gif'),
]);
$output=$this->getLayout()->getOutput();
$this->assign('layoutContent',$output);
$output=$this->renderPage();
$this->translateInline->processResponseBody($output);
$response->appendBody($output);
}else{
parent::render($response);
}
return$this;
}
Theexpression$this->assigniswhatassignsvariableslikelayoutContenttothe
root.phtmltemplate.layoutContentisgeneratedbasedonbaselayouts,togetherwithall
layoutupdatesforthecurrentpage.
WhereasbaselayoutsincludethefollowingXMLswithinvendor/magento/module-
theme/view/:
base/page_layout/empty.xml
frontend/page_layout/1column.xml
frontend/page_layout/2columns-left.xml
frontend/page_layout/2columns-right.xml
frontend/page_layout/3columns.xml
Theexpression$this->getLayout()->getOutput()iswhatgetsallblocksmarkedfor
output.Itbasicallyfindselementsinalayout,rendersthem,andreturnsthestringwithits
output.Alongtheway,theeventcore_layout_render_elementgetsfired,givingusone
possiblewayofaffectingtheoutputresult.Atthispoint,mostoftheelementsonthepage
arerendered.Thisisimportantbecauseblocksplayabigrolehere.Therenderingsystem
willtakeempty.xmlintoaccount,asittooconsistsofalistofcontainers,andevery
containerhassomeblocksattachedtoitbyotherlayoutupdates.
Note
Inanutshell,eachcontainerhasblocksassignedtoit.Eachblockusually(butnotalways)
rendersatemplate.Thetemplateitselfmayormaynotcallotherblocks,andsoon.Blocks
arerenderedwhentheyarecalledfromthetemplate.
Blockarchitectureandlifecycle
BlocksareanotheroneoftheprimaryviewelementsinMagento.Attherootoftheparent
treestructure,blocksextendfromtheMagento\Framework\View\Element\AbstractBlock
classandimplementMagento\Framework\View\Element\BlockInterface.
BlockInterfacesetsonlyonerequirement,theimplementationofthetoHtmlmethod.
ThismethodshouldreturnblocksHTMLoutput.
LookinginsideAbstractBlock,wecanseeithasanumberofmethodsdeclared.Among
themostimportantonesarethefollowingmethods:
_prepareLayout:Preparesagloballayout.Wecanredefinethismethodinchild
classesforchangingthelayout.
addChild:Createsanewblock,setsitasachildofthecurrentblock,andreturnsthe
newlycreatedblock.
_toHtml:Returnsanemptystring.Weneedtooverridethismethodindescendantsto
produceHTML.
_beforeToHtml:Returns$this.ExecutesbeforerenderingHTML,butaftertryingto
loadacache.
_afterToHtml:ProcessingblockHTMLafterrendering.ReturnsaHTMLstring.
toHtml:Producesandreturnsablock’sHTMLoutput.Thismethodshouldnotbe
overridden.Wecanoverridethe_toHtmlmethodindescendantsifneeded.
TheAbstractBlockexecutionflowcanbedescribedasfollows:
_prepareLayout
toHtml
_beforeToHtml
_toHtml
_afterToHtml
Itstartswith_prepareLayoutandflowsthroughasetofmethodsuntilitreaches
_afterToHtml.Thisis,inessence,whatweneedtoknowaboutblockexecutionflow.
Themostimportantblocktypesare:
Magento\Framework\View\Element\Text
Magento\Framework\View\Element\Text\ListText
Magento\Framework\View\Element\Messages
Magento\Framework\View\Element\Template
Alloftheseblocksarebasicallyanimplementationofanabstractblock.Sincethe
_toHtmlmethodinAbstractBlockreturnsonlyanemptystring,allofthesedescendants
areimplementingtheirownversionofthe_toHtmlmethod.
Todemonstratetheusageoftheseblocks,wecanuseourpreviouslycreated
app/code/Foggyline/Office/view/frontend/layout/foggyline_office_test_crud.xml
file.
TheTextblockhasasetTextmethodwecanusetosetitscontent.Thewaywe
instantiatetheTextblockandsetitstextvaluethroughthelayoutfileisshownasfollows:
<blockclass="Magento\Framework\View\Element\Text"name="example_1">
<actionmethod="setText">
<argumentname="text"xsi:type="string"><![CDATA[<p>Text_1</p>]]>
</argument>
</action>
</block>
TheListTextblockextendsfromText.However,itdoesnotreallysupporttheuseof
setTexttosetitscontent.Thisisobviousjustbylookingatitscode,wherethe$this-
>setText('')expressionisimmediatelycalledwithinits_toHtmlmethod
implementation.Instead,whathappensisthatthe_toHtmlmethodloopsthroughanychild
blocksitmighthaveandcallsthelayout’srenderElementmethodonit.Basically,we
mightcomparetheListTextblocktocontainer,asithasnearlythesamepurpose.
However,unlikecontainer,blockisaclasssowecanmanipulateitfromPHP.The
followingisanexampleofusingListText,containingafewchildTextblocks:
<blockclass="Magento\Framework\View\Element\Text\ListText"
name="example_2">
<blockclass="Magento\Framework\View\Element\Text"name="example_2a">
<actionmethod="setText">
<argumentname="text"xsi:type="string"><!
[CDATA[<p>Text_2A</p>]]></argument>
</action>
</block>
<blockclass="Magento\Framework\View\Element\Text"name="example_2b">
<actionmethod="setText">
<argumentname="text"xsi:type="string"><!
[CDATA[<p>Text_2B</p>]]></argument>
</action>
</block>
</block>
TheMessagesblocksupportsfourmethodsthatwecanusetoaddcontenttooutput:
addSuccess,addNotice,addWarning,andaddError.Thefollowingisanexample
instantiatingtheMessagesblockthroughthelayoutupdatefile:
<blockclass="Magento\Framework\View\Element\Messages"name="example_3">
<actionmethod="addSuccess">
<argumentname="text"xsi:type="string"><![CDATA[<p>Text_3A:
Success</p>]]></argument>
</action>
<actionmethod="addNotice">
<argumentname="text"xsi:type="string"><![CDATA[<p>Text_3B:
Notice</p>]]></argument>
</action>
<actionmethod="addWarning">
<argumentname="text"xsi:type="string"><![CDATA[<p>Text_3C:
Warning</p>]]></argument>
</action>
<actionmethod="addError">
<argumentname="text"xsi:type="string"><![CDATA[<p>Text_3D:
Error</p>]]></argument>
</action>
</block>
Theprecedingexampleshouldbetakenwithcaution,sincecallingthesesettermethodsin
layoutisnottheproperwaytodoit.ThedefaultMagento_Thememodulealreadydefines
theMessagesblockthatusesvendor/magento/module-
theme/view/frontend/templates/messages.phtmlformessagerendering.Thus,for
mostofthepartthereisnoneedtodefineourownmessagesblock.
Finally,let’slookattheexampleoftheTemplateblockasfollows:
<blockclass="Magento\Framework\View\Element\Template"
name="example_4"template="Foggyline_Office::office
/no4/template.phtml"/>
TheprecedingXMLwillinstantiatetheTemplatetypeofblockandrenderthecontentof
theview/frontend/templates/office/no4/template.phtmlfilewithinthe
app/code/Foggyline/Office/directory.
OnthePHPlevel,instantiatinganewblockcanbeaccomplishedusingthelayoutobject,
ordirectlythroughtheobjectmanager.Thelayoutapproachisthepreferredway.With
regardtothepreviousexamplesinXML,let’sseetheiralternativesinPHP(assuming
$resultPageisaninstanceof\Magento\Framework\View\Result\PageFactory).
ThefollowingisanexampleofinstantiatingtheTexttypeofblockandaddingitasa
childofthecontentcontainer:
$block=$resultPage->getLayout()->createBlock(
'Magento\Framework\View\Element\Text',
'example_1'
)->setText(
'<p>Text_1</p>'
);
$resultPage->getLayout()->setChild(
'content',
$block->getNameInLayout(),
'example_1_alias'
);
TheListTextversionisdoneinPHPasfollows:
$blockLT=$resultPage->getLayout()->createBlock(
'Magento\Framework\View\Element\Text\ListText',
'example_2'
);
$resultPage->getLayout()->setChild(
'content',
$blockLT->getNameInLayout(),
'example_2_alias'
);
$block2A=$resultPage->getLayout()->createBlock(
'Magento\Framework\View\Element\Text',
'example_2a'
)->setText(
'<p>Text_2A</p>'
);
$resultPage->getLayout()->setChild(
'example_2',
$block2A->getNameInLayout(),
'example_2a_alias'
);
$block2B=$resultPage->getLayout()->createBlock(
'Magento\Framework\View\Element\Text',
'example_2b'
)->setText(
'<p>Text_2B</p>'
);
$resultPage->getLayout()->setChild(
'example_2',
$block2B->getNameInLayout(),
'example_2b_alias'
);
NoticehowwefirstmadeaninstanceoftheListTextblockandassigneditasachildof
anelementnamedcontent.ThenwecreatedtwoindividualTextblocksandassignedthem
asachildofanelementnamedexample_2,whichisourListText.
Next,let’sdefinetheMessagesblockasfollows:
$messagesBlock=$resultPage->getLayout()->createBlock(
'Magento\Framework\View\Element\Messages',
'example_3'
);
$messagesBlock->addSuccess('Text_3A:Success');
$messagesBlock->addNotice('Text_3B:Notice');
$messagesBlock->addWarning('Text_3C:Warning');
$messagesBlock->addError('Text_3D:Error');
$resultPage->getLayout()->setChild(
'content',
$messagesBlock->getNameInLayout(),
'example_3_alias'
);
Finally,let’slookattheTemplateblocktype,whichweinitiateasfollows:
$templateBlock=$resultPage->getLayout()->createBlock(
'Magento\Framework\View\Element\Template',
'example_3'
)->setTemplate(
'Foggyline_Office::office/no4/template.phtml'
);
$resultPage->getLayout()->setChild(
'content',
$templateBlock->getNameInLayout(),
'example_4_alias'
);
Wheneverpossible,weshouldsetourblocksusingXMLlayouts.
NowthatweknowhowtoutilizethemostcommontypesofMagentoblocks,let’ssee
howwecancreateourownblocktype.
Definingourownblockclassisassimpleascreatingacustomclassfilethatextends
Template.ThisblockclassshouldbeplacedunderourmoduleBlockdirectory.Usingour
Foggyline_Officemodule,let’screateafile,Block/Hello.php,withcontentasfollows:
namespaceFoggyline\Office\Block;
classHelloextends\Magento\Framework\View\Element\Template
{
publicfunctionhelloPublic()
{
return'Hello#1';
}
protectedfunctionhelloProtected()
{
return'Hello#2';
}
privatefunctionhelloPrivate()
{
return'Hello#3';
}
}
Theprecedingcodesimplycreatesanewcustomblockclass.Wecanthencallthisblock
classthroughourlayoutfileasfollows:
<blockclass="Foggyline\Office\Block\Hello"
name="office.hello"template="office/hello.phtml"/>
Finally,withinourmoduleapp/code/Foggyline/Office/directory,wecreateatemplate
file,view/frontend/templates/office/hello.phtml,withcontentasfollows:
<?php/*@var$blockFoggyline\Office\Block\Hello*/?>
<h1>Hello</h1>
<p><?phpecho$block->helloPublic()?></p>
<p><?php//echo$block->helloProtected()?></p>
<p><?php//echo$block->helloPrivate()?></p>
Tofurtherunderstandwhatishappeningherewithinthetemplatefile,let’stakeadeeper
lookattemplatesthemselves.
Templates
TemplatesaresnippetsofHTMLmixedwithPHP.ThePHPpartincludeselementssuch
asvariables,expressions,andclassmethodcalls.MagentousesthePHTMLfile
extensionfortemplatefiles.Templatesarelocatedunderanindividualmodule’s
view/{_area_}/templates/directory.
Inourpreviousexample,wereferredtoourmoduletemplatefilewithanexpressionlike
Foggyline_Office::office/hello.phtml.Sincetemplatescanbelongtodifferent
modules,weshouldprependthetemplatewiththemodulenameasabestpractice.This
willhelpuslocatetemplatefilesandavoidfileconflicts.
Asimplenamingformulagoeslikethis:wetypethenameofthemodule,doublesingle
colon,andthenthename.Thusmakingatemplatepathlikeoffice/hello.phtml
equalingtoFoggyline_Office::office/hello.phtml.
WithinthePHTMLtemplatefileweoftenhavevariousPHPexpressionslike$block-
>helloPublic().NoticetheblockclassFoggyline\Office\Block\Hellointhe
precedingXML.Aninstanceofthisblockclassbecomesavailabletousinhello.phtml
throughthe$blockvariable.Thus,anexpressionlike$block->helloPublic()is
effectivelycallingthehelloPublicmethodfromaninstanceoftheHelloclass.The
HelloclassisnotoneoftheMagentocoreclasses,butitdoesextend
\Magento\Framework\View\Element\Template.
Ourhello.phtmltemplatealsohastwomoreexpressions:$block->helloProtected()
and$block->helloPrivate().However,thesearenotexecutedastemplatefilescanonly
seepublicmethodsfromtheir$blockinstances.
The$thisvariableisalsoavailablewithinthePHTMLtemplateasaninstanceofthe
Magento\Framework\View\TemplateEngine\Phpclass.
Intheprecedingtemplatecodeexample,wecouldhaveeasilyreplaced$block-
>helloPublic()withthe$this->helloPublic()expression.Thereasonwhythiswould
workliesinthetemplateenginePhpclass,(partially)definedasfollows:
publicfunction__call($method,$args)
{
returncall_user_func_array([$this->_currentBlock,$method],$args);
}
publicfunction__isset($name)
{
returnisset($this->_currentBlock->{$name});
}
publicfunction__get($name)
{
return$this->_currentBlock->{$name};
}
Giventhattemplatesareincludedinthecontextoftheengineratherthaninthecontextof
theblock,__callredirectsmethodscallstothecurrentblock.Similarly,__issetredirects
issetcallstothecurrentblockand__getallowsreadaccesstopropertiesofthecurrent
block.
Thoughwecanuseboth$blockand$thisforthesamepurposewithinthetemplatefile,
weshouldreallyoptforusing$block.
Anotherimportantaspectoftemplatesistheirfallbackmechanism.Fallbackistheprocess
ofdefiningafulltemplatepathgivenonlyitsrelativepath.Forexample,
office/hello.phtmlfallsbacktothe
app/code/Foggyline/Office/view/frontend/templates/office/hello.phtmlfile.
Pathresolutionstartsfromthe_toHtmlmethoddefinedonthe
Magento\Framework\View\Element\Templateclass.The_toHtmlmethodthencalls
getTemplateFilewithinthesameclass,whichinturncallsgetTemplateFileNameon
resolver,whichisaninstanceof
\Magento\Framework\View\Element\Template\File\Resolver.Lookingfurther,
resolver’sgetTemplateFileNamefurthercallsgetTemplateFileNameon
_viewFileSystem,whichisaninstanceof\Magento\Framework\View\FileSystem.The
methodgetFileisfurthercalledonaninstanceof
\Magento\Framework\View\Design\FileResolution\Fallback\TemplateFile.getFile
furthertriggerstheresolvemethodonthe
Magento\Framework\View\Design\FileResolution\Fallback\Resolver\Simple
instance,whichfurthercallsthegetRulemethodonthe
Magento\Framework\View\Design\Fallback\RulePoolinstance.TheRulePollclassis
thefinalclassinthechainhere.getRulefinallycallsthecreateTemplateFileRule
method,whichcreatestherulethatdetectswherethefileislocated.
WhilerunningthegetRulemethod,Magentochecksagainstthefollowingtypesof
fallbackrules:
file
locale
template
static
email
ItisworthspendingsometimetostudytheinnerworkingsoftheRulePoolclass,asit
showcasesdetailedfallbacksforthelistedrules.
Layouts
Uptothispoint,webrieflytouchedonlayoutXMLs.LayoutXMLisatooltobuildthe
pagesoftheMagentoapplicationinamodularandflexiblemanner.Itenablesusto
describethepagelayoutandcontentplacement.LookingatXMLrootnodes,we
differentiatetwotypesoflayouts:
layout:XMLwrappedin<layout>
page:XMLwrappedin<page>
PagelayoutsrepresentafullpageinHTML,whereaslayoutlayoutsrepresentapartofa
page.Thelayouttypeisasubsetofthepagelayouttype.BothtypesoflayoutXMLfiles
arevalidatedbytheXSDschemafoundunderthe
vendor/magento/framework/View/Layout/etc/directory:
layout–layout_generic.xsd
page–page_configuration.xsd
Basedontheapplicationcomponentsthatprovide<layout>and<page>elements,we
canfurthersectionthemasbaseandthemelayouts.
Thebaselayoutsareprovidedbythemodules,usuallyatthefollowinglocations:
<module_dir>/view/frontend/layout:pageconfigurationandgenericlayoutfiles
<module_dir>/view/frontend/page_layout:pagelayoutfiles
Thethemelayoutsareprovidedbythethemes,usuallyatthefollowinglocations:
<theme_dir>/<Namespace>_<Module>/layout:pageconfigurationandgeneric
layoutfiles
<theme_dir>/<Namespace>_<Module>/page_layout:pagelayoutfiles
MagentowillloadandmergeallmoduleandthemeXMLfilesontheappropriatepage.
OncefilesaremergedandXMLinstructionsareprocessed,theresultisrenderedandsent
tothebrowserfordisplay.HavingtwodifferentlayoutXMLfiles,wherebothreference
thesameblock,meansthatthesecondonewiththesamenameinthesequencewill
replacethefirstone.
WhentheXMLfilesareloaded,Magentoappliesaninheritancethemeatthesametime.
Wecanapplyathemeanditwilllookfortheparentuntilathemewithoutaparentis
reached.
Inadditiontothemergingoffilesfromeachmodule,layoutfilesfromwithinmodule
directoriescanalsobeextendedoroverriddenbythemes.OverridinglayoutXMLisnota
goodpractice,butitmightbenecessarysometimes.
Tooverridethebaselayoutfilesprovidedbythemodulewithinthe
<module_dir>/view/frontend/layout/directory.
WeneedtocreateanXMLfilewiththesamenameinthe
app/design/frontend/<vendor>/<theme>/<Namespace_Module>/layout/override/base/
Tooverridethethemelayoutfilesprovidedbytheparentthemewithinthe
<parent_theme_dir>/<Namespace>_<Module>/layout/directory.
WeneedtocreateanXMLfilewiththesamenameinthe
app/design/frontend/<vendor>/<theme
>/<Namespace_Module>/layout/override/theme/<Parent_Vendor>/<parent_theme>/directory.
Layoutscanbebothoverriddenandextended.
Therecommendedwaytocustomizelayoutistoextenditthroughacustomtheme.We
candosobysimplyaddingacustomXMLlayoutfilewiththesamenameinthe
app/design/frontend/{vendorName}/{theme}/{vendorName}_{moduleName}/layout/
directory.
Layouts,aswesawinpreviousexamples,supportalargenumberofdirectives:pagepage,
head,block,andsoon.Thepracticaluseofthesedirectivesandhowtheymixtogetheris
achallengeonitsown.Givingfulldetailsoneachandeverydirectiveisbeyondthescope
ofthisbook.However,whatwecandoistoshowhowtofigureouttheuseofan
individualdirective,whichwemightneedatagiventime.Forthatpurpose,itishighly
recommendedtouseanIDEenvironmentlikeNetBeansPHPorPhpStormthatprovide
autocompleteonXMLsthatincludeXSD.
ThefollowingisanexampleofdefininganexternalschematoPhpStorm,whereweare
simplysayingthatthe
urn:magento:framework:View/Layout/etc/page_configuration.xsdaliasbelongsto
thevendor/magento/framework/View/Layout/etc/page_configuration.xsdfile:
Thisway,PhpStormwillknowhowtoprovideautocompletewhilewetypearoundXML
files.
Asanexample,let’stakealookathowwecouldusethecssdirectivetoaddanexternal
CSSfiletoourpage.WithanIDEthatsupportsautocompleteassoonaswetypethecss
directivewithinthepage|headelement,autocompletemightthrowoutsomethinglikethe
following:
Alistofavailableattributesisshown,suchassrc,sizes,ie_condtion,src_type,andso
on.IDEslikePhpStormwillallowustoright-clickanelementoritsattributeandgotothe
definition.Lookingintothedefinitionforthesrcattributegetsusintothe
vendor/magento/framework/View/Layout/etc/head.xsdfilethatdefinesthecss
elementasfollows:
<xs:complexTypename="linkType">
<xs:attributename="src"type="xs:string"use="required"/>
<xs:attributename="defer"type="xs:string"/>
<xs:attributename="ie_condition"type="xs:string"/>
<xs:attributename="charset"type="xs:string"/>
<xs:attributename="hreflang"type="xs:string"/>
<xs:attributename="media"type="xs:string"/>
<xs:attributename="rel"type="xs:string"/>
<xs:attributename="rev"type="xs:string"/>
<xs:attributename="sizes"type="xs:string"/>
<xs:attributename="target"type="xs:string"/>
<xs:attributename="type"type="xs:string"/>
<xs:attributename="src_type"type="xs:string"/>
</xs:complexType>
Alloftheseareattributeswecansetonthecsselement,andassuchgettheir
autocompleteasshown:
AlthoughitisnotrequiredtousearobustIDEwithMagento,itcertainlyhelpstohave
onethatunderstandsXMLandXSDfilestothelevelofprovidingautocompleteand
validation.
Themes
Bydefault,Magentocomeswithtwothemes,namedBlankandLuma.Ifwelogintothe
Magentoadminarea,wecanseealistofavailablethemesundertheContent|Design|
Themesmenu,asshowninthefollowingscreenshot:
Magentothemessupportaparent-childrelationship,somethingwenotedpreviously,that
isvisibleontheprecedingimagewithintheParentThemecolumn.
Creatinganewtheme
Thefollowingstepsoutlinetheprocessofcreatingourowntheme:
1. Under{Magentorootdirectory}/app/design/frontend,createanewdirectory
bearingourvendorname,Foggyline.
2. Withinthevendordirectory,createanewdirectorybearingthethemename,
jupiter.
3. Withinthejupiterdirectory,createtheregistration.phpfilewithcontentas
follows:
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::THEME,
'frontend/Foggyline/jupiter',
__DIR__
);
4. Copyvendor/magento/theme-frontend-blank/theme.xmlintoourtheme,
app/design/frontend/Foggyline/jupiter/theme.xml,changingthecontentas
follows:
<themexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:
framework:Config/etc/theme.xsd">
<title>FoggylineJupiter</title>
<parent>Magento/blank</parent>
<media>
<preview_image>media/preview.jpg</preview_image>
</media>
</theme>
5. Createtheapp/design/frontend/Foggyline/jupiter/media/preview.jpgimage
filetoserveasthethemepreviewimage(theoneusedintheadminarea).
6. Optionally,createseparatedirectoriesforstaticfilessuchasstyles,fonts,JavaScript,
andimages.Thesearestoredwithinthewebsubdirectoryofourtheme
app/design/frontend/Foggyline/jupiter/folderlikefollows:
web/css/
web/css/source/
web/css/source/components/
web/images/
web/js/
Withinthethemewebdirectory,westoregeneralthemestaticfiles.Ifourtheme
containsmodule-specificstaticfiles,thesearestoredunderthecorrespondingvendor
modulesubdirectories,like
app/design/frontend/Foggyline/jupiter/{vendorName_moduleName}/web/.
7. Optionally,wecancreatethethemelogo.svgimageunderourthemeweb/images/
folder.
Oncewearedonewiththeprecedingsteps,lookingbackintotheadminareaunderthe
Content|Design|Themesmenu,weshouldnowseeourthemelistedasshowninthe
followingscreenshot:
Whereasclickingontherowinthetablenexttoourthemenamewouldopenascreenlike
thefollowing:
Noticehowtheprevioustwoscreensdonotshowanyoptionstoapplythetheme.They
areonlylistingoutavailablethemesandsomebasicinformationnexttoeachtheme.Our
customthemeshowsaninterestingrelationship,whereaparentandachildthemecan
belongtodifferentvendors.
Applyingthethemerequiresthefollowingextrasteps:
1. Makesureourthemeappearsinthethemelist,undertheContent|Design|Themes
menu.
2. GotoStores|Settings|Configuration|General|Design.
3. IntheStoreViewdrop-downfield,weselectthestoreviewwherewewanttoapply
thetheme,asshownintheupper-leftcornerofthefollowingimage:
4. OntheDesignThemetab,weselectournewlycreatedthemeintheDesignTheme
drop-down,asshownontheright-handsideoftheprecedingimage.ClickSave
Config.
5. UnderSystem|Tools|CacheManagement,selectandrefreshtheinvalidcache
typesandclickontheFlushCatalogImagesCache,FlushJavaScript/CSSCache,
andFlushStaticFilesCachebuttons.
6. Finally,toseeourchangesapplied,reloadthestorefrontpagesinthebrowser.
Thereisalotmoretobesaidaboutthemesthatcanfitinabookofitsown.However,we
willmoveontotheotherimportantbits.
JavaScript
MagentomakesuseofquitealargenumberofJavaScriptlibraries,suchas:
Knockout:http://knockoutjs.com
ExtJS:https://www.sencha.com/products/extjs/
jQuery:https://jquery.com/
jQueryUI:https://jqueryui.com/
modernizr:http://www.modernizr.com/
Prototype:http://www.prototypejs.org/
RequireJS:http://requirejs.org/
script.aculo.us:http://script.aculo.us/
moment.js:http://momentjs.com/
Underscore.js:http://underscorejs.org/
gruntjs:http://gruntjs.com/
AngularJS:https://angularjs.org/
jasmine:http://jasmine.github.io/
…andafewothers
Thoughafrontenddeveloperisnotrequiredtoknowtheinsandoutsofeverylibrary,itis
recommendedtoatleasthaveabasicinsightintomostofthem.
Tip
Itisworthrunningfind{MAGENTO-DIR}/-name\*.js>js-list.txtontheconsoleto
getafulllistofeachandeveryJavaScriptfileinMagento.Spendingafewminutes
glossingoverthelistmightserveasanicefuturememowhenworkingwithJavaScript
bitsinMagento.
TheRequireJSandjQuerylibrariesareprobablythemostinterestingones,astheyoften
stepintothespotlightduringfrontenddevelopment.RequireJSplaysabigrolein
Magento,asitloadsotherJavaScriptfiles.UsingamodularscriptloaderlikeRequireJS
improvesthespeedofcode.SpeedimprovementcomesfromremovingJavaScriptfrom
theheaderandasynchronouslyorlazyloadingJavaScriptresourcesinthebackground.
JavaScriptresourcescanbespecifiedasfollows:
LibrarylevelforalllibrariesintheMagentocodebase(lib/web).
Modulelevelforalllibrariesinamodule
(app/code/{vendorName}/{moduleName}/view/{area}/web).
Themeforalllibrariesinatheme
(app/design/{area}/{vendorName}/{theme}/{vendorName}_{moduleName}/web).
Alllibrariesinatheme(app/design/{area}/{vendorName}/{theme}/web).Though
possible,itisnotrecommendedusingthisleveltospecifyJavaScriptresources.
ItisrecommendedtospecifyJavaScriptresourcesinthetemplatesratherthaninthe
layoutupdates.Thisway,weensureprocessingoftheresourcesthroughRequireJS.
ToworkwiththeRequireJSlibrary,specifythemappingofJavaScriptresources;thatis,
assignthealiasestoresources.Userequires-config.jstocreatethemapping.
Tomakeourconfigurationsmorepreciseandspecificfordifferentmodules/themes,we
canidentifymappingintherequires-config.jsfileatseverallevelsdependingonour
needs.Configurationsarecollectedandexecutedinthefollowingorder:
Libraryconfigurations
Configurationsatthemodulelevel
Configurationsatthethememodulelevelfortheancestorthemes
Configurationsatthethememodulelevelforacurrenttheme
Configurationsatthethemelevelfortheancestorthemes
Configurationsatthethemelevelforthecurrenttheme
WhenwespeakofJavaScriptinMagento,wecanhearvarioustermslikecomponentand
widget.WecaneasilydividethosetermsbydescribingthetypeofJavaScriptinMagento
asperthefollowinglist:
JavaScriptcomponent(JScomponent):ThiscanbeanysingleJavaScriptfile
decoratedasanAMD(shortforAsynchronousModuleDefinition)module
Uicomponent:AJavaScriptcomponentlocatedintheMagento_Uimodule
jQueryUIwidget:AJavaScriptcomponent/widgetprovidedbythejQueryUI
libraryusedinMagento
jQuerywidget:AcustomwidgetcreatedusingjQueryUIWidgetFactoryand
decoratedasanAMDmodule
TherearetwowayswecaninitializeaJavaScriptcomponentintemplatefiles:
Usingthedata-mage-initattribute
Usingthe<script>tag
Thedata-mage-initattributeisparsedonaDOMreadyevent.Sinceitisinitializedona
certainelement,thescriptiscalledonlyforthatparticularelement,andisnot
automaticallyinitializedforotherelementsofthesametypeonthepage.Anexampleof
data-mage-initusagewouldbesomethinglikethefollowing:
<divdata-mage-init='{"<componentName>":{...}}'></div>
The<script>taginitializationisdonewithoutrelationtoanyspecificelement,orin
relationtoaspecificelementbutnodirectaccesstotheelement.Thescripttaghastohave
anattribute,type="text/x-magento-init".Anexampleof<script>taginitialization
wouldbesomethinglikethefollowing:
<scripttype="text/x-magento-init">
//specificelementbutnodirectaccesstotheelement
"<element_selector>":{
"<jsComponent1>":...,
"<jsComponent2>":...
},
//withoutrelationtoanyspecificelement
"*":{
"<jsComponent3">:...
}
</script>
Dependingonthesituationanddesiredlevelofexpressiveness,wecaneitheroptfor
usageofdata-mage-initorattributeor<script>tag.
CreatingacustomJScomponent
Let’sgothroughapracticalexampleofcreatingaJScomponentwithinour
Foggyline_OfficemoduleinaformofthejQuerywidgetasfollows:
First,weaddourentrytoapp/code/Foggyline/Office/view/frontend/requirejs-
config.js,asshown:
varconfig={
map:{
'*':{
foggylineHello:'Foggyline_Office/js/foggyline-hello'
}
}
};
ThenweaddtheactualJavaScript
app/code/Foggyline/Office/view/frontend/web/js/foggyline-hello.jswith
contentasfollows:
define([
"jquery",
"jquery/ui"
],function($){
"usestrict";
$.widget('mage.foggylineHello',{
options:{
},
_create:function(){
alert(this.options);
//mycodehere
}
});
return$.mage.foggylineHello;
});
Finally,wecallourJavaScriptcomponentwithinsomePHTMLtemplate,let’ssay
app/code/Foggyline/Office/view/frontend/templates/office/hello.phtml,as
show:
<divdata-mage-init='{"foggylineHello":{"myVar1":"myValue1","myVar2":
"myValue2"}}'>Foggyline</div>
Oncewerefreshthefrontend,weshouldseetheresultofalert(this.options)inthe
browsershowingmyVar1andmyVar2.
Thedata-mage-initpartbasicallytriggersassoonasthepageloads.Itisnottriggered
viasomeclickorsimilareventontopofthedivelement;itistriggeredonpageload.
Ifwedon’tseethedesiredresultinthebrowser,wemightneedtofullyclearthecachein
theadminarea.
CSS
MagentousesaPHPportoftheofficialLESSprocessortoparsethe.lessfilesinto.css
files.LESSisaCSSpreprocessorthatextendstheCSSlanguagebyaddingvarious
featurestoit,likevariables,mixins,andfunctions.AllofthismakesCSSmore
maintainable,extendable,andeasiertotheme.Frontenddevelopersarethusexpectedto
writeLESSfilesthatMagentothenconvertstoappropriateCSSvariants.
Tip
Itisworthrunningfind{MAGENTO-DIR}/-name\*.less>less-list.txtontheconsole
togetafulllistofeachandeveryLESSfileinMagento.Spendingafewminutesglossing
overthelistmightserveasanicefuturememowhenworkingwithstylesheetbitsin
Magento.
Wecancustomizethestorefrontlookandfeelthroughoneofthefollowingapproaches:
OverridethedefaultLESSfiles–onlyifourthemeinheritsfromthedefaultorany
othertheme,inwhichcasewecanoverridetheactualLESSfiles
CreateourownLESSfilesusingthebuilt-inLESSpreprocessor
CreateourownCSSfiles,optionallyhavingcompiledthemusingathird-partyCSS
preprocessor
Withintheindividualfrontendthemedirectory,wecanfindstylesheetsatthefollowing
locations:
{vendorName}_{moduleName}/web/css/source/
{vendorName}_{moduleName}/web/css/source/module/
web/css/
web/css/source/
CSSfilescanbeincludedinapagethroughtemplatesandlayoutfiles.Arecommended
wayistoincludethemthroughlayoutfiles.Ifwewantourstylesheetstobeavailable
throughallpagesonthefrontend,wecanaddusingthedefault_head_blocks.xmlfile.If
welookattheblanktheme,itusesvendor/magento/theme-frontend-
blank/Magento_Theme/layout/default_head_blocks.xmldefinedasfollows:
<pagexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout
/etc/page_configuration.xsd">
<head>
<csssrc="css/styles-m.css"/>
<csssrc="css/styles-l.css"media="screenand(min-width:768px)"/>
<csssrc="css/print.css"media="print"/>
</head>
</page>
Allittakesisforustocopythisfileinthesamelocationunderourcustomtheme;
assumingit’sthejupiterthemefromtheprecedingexamples,thatwouldbe
app/design/frontend/Foggyline/jupiter/Magento_Theme/layout/default_head_blocks.xml
ThenwesimplymodifythefiletoincludeourCSS.
Whenrun,MagentowilltrytofindtheincludedCSSfiles.IfaCSSfileisnotfound,it
thensearchesforthesamefilenameswitha.lessextension.Thisispartofthebuilt-in
preprocessingmechanism.
Summary
Inthischapter,westartedoffbylookingintothethreeaspectsoftherenderingflow
process:theview,resultobject,andpages.Thenwetookadetailedlookatthreeprimary
viewelements:ui-components,containers,andblocks.Wefurtherstudiedblocksin
depth,lookingintotheirarchitectureandlifecycle.Wemovedontotemplates,looking
intotheirlocations,rendering,andfallback.ThencameXMLlayouts,asthegluebetween
blocksandtemplates.Allofthisgaveusafoundationforfurtherlookingintotheme
structure,JavaScriptcomponents,andCSS.Alongtheway,wedidalittlebitofhands-on
withacustomthemeandJavaScriptcomponentscreation.CSSandJavaScriptismerelya
fragmentofwhattheMagentofrontendisallabout.Technology-wise,havingasolid
understandingofXMLandevensomePHPismoreofarequirementthananexceptionfor
frontend-relateddevelopment.
ThefollowingchapterwillintroduceustoMagento’swebAPIwherewewilllearnhowto
authenticate,makeAPIcalls,andevenbuildourownAPIs.
Chapter9.TheWebAPI
Throughoutpreviouschapters,welearnedhowtousesomeofthebackendcomponentsso
thatstoreownerscanmanageandmanipulatethedatasuchascustomers,products,
categories,orders,andsoon.Sometimesthisisnotenough,likewhenwearepullingdata
inoroutfromthird-partysystems.Incaseslikethese,theMagentoWebAPIframework
makesiteasytocallMagentoservicesthroughRESTorSOAP.
Inthischapter,wewillcoverthefollowingtopics:
Usertypes
Authenticationmethods
RESTversusSOAP
Hands-onwithtoken-basedauthentication
Hands-onwithOAuth-basedauthentication
OAuth-basedWebAPIcalls
Hands-onwithsession-basedauthentication
CreatingcustomWebAPIs
SearchCriteriaInterfaceforlistfiltering
BeforewecanstartmakingWebAPIcalls,wemustauthenticateouridentityandhavethe
necessarypermissions(authorization)toaccesstheAPIresource.Authenticationallows
Magentotoidentifythecaller’susertype.Basedontheuser’s(administrator,integration,
customer,orguest)accessrights,theAPIcalls’resourceaccessibilityisdetermined.
Usertypes
Thelistofresourcesthatwecanaccessdependsonourusertypeandisdefinedwithinour
modulewebapi.xmlconfigurationfile.
TherearethreetypesofusersknowntoAPI,listedasfollows:
Administratororintegration:Resourcesforwhichadministratorsorintegratorsare
authorized.Forexample,ifadministratorsareauthorizedfortheMagento_Cms::page
resource,theycanmakeaPOST/V1/cmsPagecall.
Customer:Resourcesforwhichcustomersareauthorized.Thesearetheresources
withanonymousorselfpermission.
Guestuser:Resourcesforwhichguestsareauthorized.Thesearetheresourceswith
anonymouspermission.
TwofilesplayacrucialroletowarddefininganAPI:ourmoduleacl.xmlandwebapi.xml
files.
acl.xmliswherewedefineourmoduleaccesscontrollist(ACL).Itdefinesanavailable
setofpermissionstoaccesstheresources.Theacl.xmlfilesacrossallMagentomodules
areconsolidatedtobuildanACLtreethatisusedtoselectallowedadminroleresources
orthird-partyintegration’saccess(System|Extensions|Integrations|AddNew
Integration|AvailableAPIs).
webapi.xmliswherewedefineWebAPIresourcesandtheirpermissions.Whenwecreate
webapi.xml,thepermissionsdefinedinacl.xmlarereferencedtocreateaccessrightsfor
eachAPIresource.
Let’stakealookatthefollowing(truncated)webapi.xmlfromthecoreMagento_Cms
module:
<routesxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation=
"urn:magento:module:Magento_Webapi:etc/webapi.xsd">
...
<routeurl="/V1/cmsPage"method="POST">
<serviceclass="Magento\Cms\Api\PageRepositoryInterface"
method="save"/>
<resources>
<resourceref="Magento_Cms::page"/>
</resources>
</route>
...
<routeurl="/V1/cmsBlock/search"method="GET">
<serviceclass="Magento\Cms\Api\BlockRepositoryInterface"
method="getList"/>
<resources>
<resourceref="Magento_Cms::block"/>
</resources>
</route>
...
</routes>
Intheprecedingwebapi.xmlfilefortheCMSpageAPI,onlyauserwith
Magento_Cms::pageauthorizationcanaccessPOST/V1/cmsPageorGET
/V1/cmsBlock/search.Wewillgetbacktoamoredetailedexplanationofroutelateron
inourexamples;forthemoment,ourfocusisonresource.Wecanassignmultiplechild
resourceelementsunderresources.Incaseslikethese,itwouldbesufficientforauserto
haveanyoneofthoseACLsassignedtobeabletomakeanAPIcall.
Theactualauthorizationisthengrantedtoeitheranadministratororintegration,defined
intheMagentoadmin,withfullgrouporaspecificresourceselectedintheACLtreeas
showninthefollowingscreenshot:
Giventhatwebapi.xmlandacl.xmlgohandinhand,let’stakealookatthe(truncated)
acl.xmlfilefromthecoreMagento_Cmsmodule:
<resources>
<resourceid="Magento_Backend::admin">
<resourceid="Magento_Backend::content">
<resourceid="Magento_Backend::content_elements">
<resourceid="Magento_Cms::page"...>
...
</resource>
</resource>
</resource>
</resource>
</resources>
NoticehowthepositionoftheMagento_Cms::pageresourceisnestedunder
Magento_Backend::content_elements,whichinturnisnestedunder
Magento_Backend::content,whichisfurthernestedunderMagento_Backend::admin.
ThistellsMagentowheretorendertheACLunderMagentoadminwhenshowingthe
RolesResourcestreeasshowninthepreviousscreenshot.Thisdoesnotmeanthatthe
userauthorizedagainsttheMagento_Cms::pageresourcewon’tbeabletoaccesstheAPI
ifallthoseparentMagento_Backendresourcesaregrantedtohimaswell.
Authorizingagainstaresourceissortofaflatthing.Thereisnotreecheckwhen
authorizing.Thus,eachresourceisrequiredtohaveauniqueidattributevalueona
resourceelementwhendefinedunderacl.xml.
Theresourcesjustdefinedarewhatwelistedbeforeasresourcesforwhichadministrators
orintegratorsareauthorized.
Thecustomer,ontheotherhand,isassignedaresourcenamedanonymousorself.Ifwe
weretodoafull<resourceref="anonymous"/>stringsearchacrossallMagentocore
modules,severaloccurrenceswouldshowup.
Let’stakealookatthe(truncated)coremodulevendor/magento/module-
catalog/etc/webapi.xmlfile:
<routeurl="/V1/products"method="GET">
<serviceclass="Magento\Catalog\Api\ProductRepositoryInterface"
method="getList"/>
<resources>
<resourceref="anonymous"/>
</resources>
</route>
TheprecedingXMLdefinesanAPIendpointpathwithavalueof/V1/products,
availableviatheHTTPGETmethod.Itfurtherdefinesaresourcecalledanonymous,
whichmeanseitherthecurrentlylogged-incustomerorguestusercancallthisAPI
endpoint.
anonymousisaspecialpermissionthatdoesn’tneedtobedefinedinacl.xml.Assuch,it
willnotshowupinthepermissionstreeunderMagentoadmin.Thissimplymeansthatthe
currentresourceinwebapi.xmlcanbeaccessedwithouttheneedforauthentication.
Finally,wetakealookattheselfresource,whoseexamplewecanfindunderthe
(truncated)vendor/magento/module-customer/etc/webapi.xmlfileasfollows:
<routeurl="/V1/customers/me"method="PUT">
<serviceclass="Magento\Customer\Api\CustomerRepositoryInterface"
method="save"/>
<resources>
<resourceref="self"/>
</resources>
<data>
<parametername="customer.id"force="true">%customer_id%
</parameter>
</data>
</route>
selfisaspecialkindofaccessthatenablesausertoaccessresourcestheyown,assuming
wealreadyhaveanauthenticatedsessionwiththesystem.Forexample,GET
/V1/customers/mefetchesthelogged-incustomer’sdetails.Thisissomethingthatis
typicallyusefulforJavaScript-basedcomponents/widgets.
Authenticationmethods
Mobileapplications,third-partyapplications,andJavaScriptcomponents/widgets
(storefrontoradmin)arethethreemaintypesofclientsasseenbyMagento.Thougha
clientisbasicallyeverythingcommunicatingwithourAPIs,eachtypeofclienthasa
preferredauthenticationmethod.
Magentosupportsthreetypesofauthenticationmethods,listedasfollows:
Token-basedauthentication
OAuth-basedauthentication
Session-basedauthentication
Token-basedauthenticationismostsuitableformobileapplications,whereatokenacts
likeanelectronickeyprovidingaccesstotheWebAPI’s.Thegeneralconceptbehinda
token-basedauthenticationsystemisrelativelysimple.Theuserprovidesausernameand
passwordduringinitialauthenticationinordertoobtainatime-limitedtokenfromthe
system.Ifatokenissuccessfullyobtained,allsubsequentAPIcallsarethenmadewith
thattoken.
OAuth-basedauthenticationissuitableforthird-partyapplicationsthatintegratewith
Magento.OnceanapplicationisauthorizedthroughtheOAuth1.0ahandshakeprocess,
itgainsaccesstoMagentoWebAPIs.Therearethreekeyterminologieswemust
understandhere:user(resourceowner),client(consumer),andserver(serviceprovider).
Theuserorresourceowneristheonewhoisbeingaskedtoallowaccesstoitsprotected
resource.Imagineacustomerasauser(resourceowner)allowingaccesstoitsordersto
somethird-partyapplications.Insuchacase,thisthird-partyapplicationwouldbethe
client(consumer),whereasMagentoanditsWebAPIwouldbetheserver(service
provider).
Session-basedauthenticationisprobablythesimplestonetograsp.Asacustomer,you
logintotheMagentostorefrontwithyourcustomercredentials.Asanadmin,youlogin
totheMagentoadminwithyouradmincredentials.TheMagentoWebAPIframework
usesyourlogged-insessioninformationtoverifyyouridentityandauthorizeaccesstothe
requestedresource.
RESTversusSOAP
MagentosupportsbothSOAP(shortforSimpleObjectAccessProtocol)andREST
(shortforRepresentationalStateTransfer)typesofcommunicationwiththeWebAPI.
Authenticationmethodsthemselvesarenotreallyboundtoanyofthem.Wecanusethe
sameauthenticationmethodandWebAPImethodcallswithbothSOAPandREST.
SomeoftheRESTspecificswemightoutlineasfollows:
WerunRESTWebAPIcallsthroughcURLcommandsoraRESTclient.
RequestssupportHTTPverbs:GET,POST,PUT,orDELETE.
AHTTPheaderrequiresanauthorizationparameter,specifyingtheauthentication
tokenwiththeBearerHTTPauthorizationscheme,Authorization:Bearer
<TOKEN>.<TOKEN>istheauthenticationtokenreturnedbytheMagentotokenservice.
WecanusetheHTTPheaderAccept:application/<FORMAT>,where<FORMAT>is
eitherJSONorXML.
SomeoftheSOAPspecificswemightoutlineasfollows:
WerunSOAPWebAPIcallsthroughcURLcommandsoraSOAPclient.
AWebServiceDefinitionLanguage(WSDL)fileisgeneratedonlyforservices
thatwerequest.ThereisnoonebigmergedWSDLfileforallservices.
TheMagentoWebAPIusesWSDL1.2,compliantwithWS-I2.0BasicProfile.
EachMagentoserviceinterfacethatispartofaservicecontractisrepresentedasa
separateserviceintheWSDL.
ConsumingseveralservicesimpliesspecifyingthemintheWSDLendpointURLina
comma-separatedmanner,forexample
http://<magento.host>/soap/<optional_store_code>?wsdl&services=
<service_name_1>,<service_name_2>.
WecangetalistofallavailableservicesbyhittingaURLlikehttp://<SHOP-
URL>/soap/default?wsdl_listinthebrowser.
ThefollowingRESTandSOAPexampleswillmakeextensiveuseofcURL,whichis
essentiallyaprogramthatallowsyoutomakeHTTPrequestsfromthecommandlineor
differentlanguageimplementations(likePHP).WecanfurtherdescribecURLasthe
consolebrowser,orourviewsourcetoolfortheweb.Anythingwecandowithvarious
fancyRESTandSOAPlibraries,wecandowithcURLaswell;itisjustconsideredtobe
amorelow-levelapproach.
DoingSOAPrequestswithcURLoranythingelsethatdoesnothaveWSDL/XMLparsing
implementedinternallyiscumbersome.Thus,usingPHPSoapClientorsomethingmore
robustisamust.SoapClientisanintegrated,activelymaintainedpartofPHP,andisthus
generallyavailable.
Withnegativepointsbeingpointed,wewillstillpresentallofourAPIcallswithconsole
cURL,PHPcURL,andPHPSoapClientexamples.Giventhatlibrariesabstractsomuch
functionality,itisabsolutelyessentialthatadeveloperhasasolidunderstandingofcURL,
evenformakingSOAPcalls.
Hands-onwithtoken-basedauthentication
Thecruxoftoken-basedauthenticationisasfollows:
Clientrequestsaccesswithausernameandpassword
Applicationvalidatescredentials
Applicationprovidesasignedtokentotheclient
ThefollowingcodeexampledemonstratestheconsolecURLREST-likerequestforthe
customeruser:
curl-XPOST"http://magento2.ce/rest/V1/integration/customer/token"\
-H"Content-Type:application/json"\
-d'{"username":"john@change.me","password":"abc123"}'
ThefollowingcodeexampledemonstratesthePHPcURLREST-likerequestforthe
customeruser:
$data=array('username'=>'john@change.me','password'=>'abc123');
$data_string=json_encode($data);
$ch=curl_init('http://magento2.ce/rest/V1/integration/customer/token');
curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'POST');
curl_setopt($ch,CURLOPT_POSTFIELDS,$data_string);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HTTPHEADER,array(
'Content-Type:application/json',
'Content-Length:'.strlen($data_string))
);
$result=curl_exec($ch);
ThefollowingcodeexampledemonstratestheconsolecURLSOAP-likerequestforthe
customeruser:
curl-XPOST-H'Content-Type:application/soap+xml;
charset=utf-8;action=
"integrationCustomerTokenServiceV1CreateCustomerAccessToken"'
-d@request.xmlhttp://magento2.ce/index.php/soap/default?services=
integrationCustomerTokenServiceV1
Noticethe-d@request.xmlpart.Here,wearesayingtothecurlcommandtotakethe
contentoftherequest.xmlfileandpassitonasPOSTbodydatawherethecontentofthe
request.xmlfilefortheprecedingcurlcommandisdefinedasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=integrationCustomerTokenServiceV1">
<env:Body>
<ns1:integrationCustomerTokenServiceV1CreateCustomer
AccessTokenRequest>
<username>john@change.me</username>
<password>abc123</password>
</ns1:integrationCustomerTokenServiceV1CreateCustomer
AccessTokenRequest>
</env:Body>
</env:Envelope>
ThefollowingcodeexampledemonstratesthePHPcURLSOAP-likerequestforthe
customeruser:
$data_string=file_get_contents('request.xml');
$ch=curl_init('http://magento2.ce/index.php/soap/default?services=
integrationCustomerTokenServiceV1');
curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'POST');
curl_setopt($ch,CURLOPT_POSTFIELDS,$data_string);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HTTPHEADER,array(
'Content-Type:application/soap+xml;charset=utf-8;
action="integrationCustomerTokenServiceV1CreateCustomerAccessToken"',
'Content-Length:'.strlen($data_string))
);
$result=curl_exec($ch);
ThefollowingcodeexampledemonstratestheusageofPHPSoapClienttomakeaWeb
APIcall:
$request=newSoapClient(
'http://magento2.ce/index.php/soap/default?wsdl&services=
integrationCustomerTokenServiceV1',
array('soap_version'=>SOAP_1_2,'trace'=>1)
);
$token=$request->integrationCustomerTokenServiceV1Create
CustomerAccessToken(array('username'=>'john@change.me','password'=>
'abc123'));
TheAPIcallforadminuserauthenticationisnearlyidentical,anddependsonwhichone
ofthreeapproacheswetake.Thedifferenceismerelyinusing
https://magento2.ce/rest/V1/integration/admin/tokenastheendpointURLinthe
caseofREST,andusinghttp://magento2.ce/index.php/soap/default?
services=integrationCustomerTokenServiceV1.Additionally,foraSOAPcall,weare
callingintegrationAdminTokenServiceV1CreateAdminAccessTokenonthe$request
object.
Inthecaseofsuccessfulauthentication,forboththecustomerandadminAPIcall,the
responsewouldbearandom-looking32-characters-longstringthatwecalltoken.This
tokenisfurthersavedtotheoauth_tokentableinthedatabase,underthetokencolumn.
Thismightbeabitconfusingwithregardtowhattheoauth_tokentablehastodowith
tokenauthentication.
Note
Ifwethinkaboutit,token-basedauthenticationcanbelookedatasasimplifiedversionof
OAuth,wheretheuserwouldauthenticateusingausernameandpasswordandthengive
theobtainedtime-expiringtokentosomethird-partyapplicationtouseit.
Inthecaseoffailedauthentication,theserverreturnsHTTP401Unauthorized,witha
bodycontainingaJSONmessage:
{"message":"Invalidloginorpassword."}
NoticehowweareabletocalltheAPImethod,thoughwearenotalreadyauthenticated?
ThismeanswemustbecallinganAPIdefinedbytheanonymoustypeofresource.A
quicklookattheAPIendpointgivesusahintastothelocationofitsdefinition.Looking
underthevendor/magento/module-integration/etc/webapi.xmlfile,wecanseethe
following(truncated)XML:
<routeurl="/V1/integration/admin/token"method="POST">
<serviceclass="Magento\Integration\Api\AdminTokenServiceInterface"
method="createAdminAccessToken"/>
<resources>
<resourceref="anonymous"/>
</resources>
</route>
<routeurl="/V1/integration/customer/token"method="POST">
<serviceclass="Magento\Integration\Api\CustomerTokenServiceInterface"
method="createCustomerAccessToken"/>
<resources>
<resourceref="anonymous"/>
</resources>
</route>
Wecanclearlyseehoweventoken-basedauthenticationitselfisdefinedasAPI,usingthe
anonymousresourcesothateveryonecanaccessit.Inanutshell,token-based
authenticationisafeatureoftheMagento\Integrationmodule.
Nowthatwehaveourauthenticationtoken,wecanstartmakingotherAPIcalls.
Remember,tokensimplymeanswehavebeenauthenticatedagainstagivenusernameand
password.ItdoesnotmeanwegetfullaccesstoallWebAPImethods.Thisfurther
dependsonwhetherourcustomeroruserhastheproperaccessrole.
Hands-onwithOAuth-based
authentication
OAuth-basedauthenticationisthemostcomplex,yetmostflexibleonesupportedby
Magento.Beforewegettouseit,themerchantmustregisterourexternalapplicationas
integrationwiththeMagentoinstance.Placingourselvesintheroleofmerchant,wedoso
intheMagentoadminareaunderSystem|Extensions|Integrations.Clickingonthe
AddNewIntegrationbuttonopensthescreenasshowninthefollowingscreenshot:
ThevalueExternalBookAppisthefreelygivennameofourexternalapplication.Ifwe
wereconnectingitwithTwitter,wecouldhaveeasilyputitsnamehere.NexttoName,we
havetheEmail,CallbackURL,andIdentityLinkURLfields.Thevalueofe-mailis
notreallythatimportant.ThecallbackURLandidentitylinkURLdefinetheexternal
applicationendpointthatreceivesOAuthcredentials.Thevaluesoftheselinkspointto
externalappthatstandsastheOAuthclient.Wewillcomebacktoitinamoment.
IntheAPItabundertheAvailableAPIspane,wesetResourceAccesstothevalueofAll
orCustom.IfsettoCustom,wecanfurtherfine-tunetheresourcesintheResources
optionwewanttoallowaccesstothisintegrationasshowninthefollowingscreenshot:
Weshouldalwaysgivetheminimumrequiredresourcestotheexternalapplicationweare
using.Thisway,weminimizepossiblesecurityrisks.Theprecedingscreenshotshowsus
definingonlySales,Products,Customer,andMarketingresourcestotheintegration.
ThismeansthattheAPIuserwouldnotbeabletousecontentresources,suchassaveor
deletepages.
IfweclicktheSavebuttonnow,weshouldberedirectedbacktotheSystem|Extensions
|Integrationsscreenasshowninthefollowingscreenshot:
Therearethreethingstofocusourattentionhere.First,weareseeinganIntegrationnot
securemessage.ThisisbecausewhenwedefinedourcallbackURLandidentitylink
URL,weusedHTTPandnotHTTPSprotocol.Whendoingreal-worldconnections,for
securityreasons,weneedtobesuretouseHTTPS.Further,wenoticehowtheStatus
columnstillsaysInactive.
TheActivatelink,totherightoftheStatuscolumn,istheprecedingstepbeforethetwo-
leggedOAuthhandshakestarts.Onlyanadministratorwithaccesstointegrationlistingin
thebackendcaninitiatethis.
Atthispoint,weneedtopulltheentirePHPcodebehindtheExternalBookAppOAuth
clientfromhere,https://github.com/ajzele/B05032-BookAppOauthClient,andplaceitinto
therootofourMagentoinstallationunderthepub/external-book-app/folderasshown
inthefollowingscreenshot:
Thefunctionofthesefilesistosimulateourownmini-OAuthclient.Wewillnotgointo
muchdetailaboutthecontentofthesefiles,Itismoreimportanttolookatitasanexternal
OAuthclientapp.Thecallback-url.phpandidentity-link-url.phpfileswillexecute
whenMagentotriggersthecallbackandidentitylinkURL’sasconfiguredunderthe
outputimageonthepreviouspage.
OncetheOAuthclientfilesareinplace,wegobacktoourintegrationslisting.Here,we
clickontheActivatelink.Thisopensamodalbox,askingustoapproveaccesstotheAPI
resourcesasshowninthefollowingscreenshot:
NoticehowAPIresourceslistedherematchthosefewwesetundertheAPItabwhen
creatingintegration.Thereareonlytwoactionswecandoherereally:eitherclickCancel
orAllowtostartthetwo-leggedOAuthhandshake.ClickingtheAllowbuttondoestwo
thingsinparallel.
First,itinstantlypoststhecredentialstotheendpoint(callbackURL)specifiedwhen
creatingtheExternalBookAppintegration.TheHTTPPOSTfromMagentotothe
callbackURLcontainsparameterswithvaluessimilartothefollowing:
Array
(
[oauth_consumer_key]=>cn5anfyvkg7sgm2lrv8cxvq0dxcrj7xm
[oauth_consumer_secret]=>wvmgy0dmlkos2vok04k3h94r40jvi5ye
[store_base_url]=>http://magento2-merchant.loc/index.php/
[oauth_verifier]=>hlnsftola6c7b6wjbtb6wwfx4tow2x6x
)
Basically,aHTTPPOSTrequestishittingthecallback-url.phpfilewhosecontent
(partial)isasfollows:
session_id('BookAppOAuth');
session_start();
$_SESSION['oauth_consumer_key']=$_POST['oauth_consumer_key'];
$_SESSION['oauth_consumer_secret']=$_POST['oauth_consumer_secret'];
$_SESSION['store_base_url']=$_POST['store_base_url'];
$_SESSION['oauth_verifier']=$_POST['oauth_verifier'];
session_write_close();
header('HTTP/1.0200OK');
echo'Response';
WecanseethatparameterspassedbyMagentoarestoredintoanexternalappsession
namedBookAppOAuth.Lateron,withinthecheck-login.phpfile,theseparameterswillbe
usedtoinstantiatetheBookAppOauthClient,whichwillfurtherbeusedtogetarequest
token,whichisapre-authorizedtoken.
ParalleltoCallbackURLHTTPPOST,wehaveapopupwindowopeningasshownin
thefollowingscreenshot:
Theloginformweseeinthepopupisjustsomedummycontentweplacedunderthe
identity-link-url.phpfile.MagentopassestwovaluestothisfileviaHTTPGET.
Theseareconsumer_idandsuccess_call_back.Theconsumer_idvalueistheIDofour
integrationwecreatedintheadminarea.ItisuptotheOAuthclientapptodecideifit
wantstodoanythingwiththisvalueornot.Thesuccess_call_backURLpointstoour
Magentoadminintegration/loginSuccessCallbackpath.Ifwetakealookatthecode
oftheidentity-link-url.phpfile,wecanseetheformissettodothePOSTactiononthe
URLlikecheck-login.php?consumer_id={$consumerId}&callback_url=
{$callbackUrl}.
IfwenowclicktheLoginbutton,theformwillPOSTdatatothecheck-login.phpfile
passingitconsumer_idandcallback_urlwithintheURLasGETparameters.
Thecontentofcheck-login.phpisdefined(partially)asfollows:
require'../../vendor/autoload.php';
$consumer=$_REQUEST['consumer_id'];
$callback=$_REQUEST['callback_url'];
session_id('BookAppOAuth');
session_start();
$consumerKey=$_SESSION['oauth_consumer_key'];
$consumerSecret=$_SESSION['oauth_consumer_secret'];
$magentoBaseUrl=rtrim($_SESSION['store_base_url'],'/');
$oauthVerifier=$_SESSION['oauth_verifier'];
define('MAGENTO_BASE_URL',$magentoBaseUrl);
$credentials=new\OAuth\Common\Consumer\Credentials($consumerKey,
$consumerSecret,$magentoBaseUrl);
$oAuthClient=newBookAppOauthClient($credentials);
$requestToken=$oAuthClient->requestRequestToken();
$accessToken=$oAuthClient->requestAccessToken(
$requestToken->getRequestToken(),
$oauthVerifier,
$requestToken->getRequestTokenSecret()
);
header('Location:'.$callback);
Tokeepthingsimple,wehavenorealuserlogincheckhere.Wemighthaveaddedone
abovetheOAuth-relatedcalls,andthenauthenticatetheuseragainstsomeusernameand
passwordbeforeallowingittouseOAuth.However,forsimplicityreasonsweomitted
thispartfromoursampleOAuthclientapp.
Withinthecheck-login.phpfile,wecanseethatbasedonthepreviouslystoredsession
parametersweperformthefollowing:
Instantiatethe\OAuth\Common\Consumer\Credentialsobjectpassingitthe
oauth_consumer_key,oauth_consumer_secret,store_base_urlstoredinthe
session
InstantiatetheBookAppOauthClientobjectpassingitsconstructortheentire
credentialsobject
UsetheOauthClientobjecttogettherequesttoken
Usetherequesttokentogetalong-livedaccesstoken
Ifeverythingexecutessuccessfully,thepopupwindowclosesandwegetredirectedback
totheintegrationslisting.Thedifferencenowisthatlookingatthegrid,wehavean
ActivestatusandnexttoitwehaveaReauthorizelink,asshowninthefollowing
screenshot:
WhatwearereallyafteratthispointareAccessTokenandAccessTokenSecret.Wecan
seethoseifweedittheExternalBookAppintegration.Thesevaluesshouldnowbe
presentontheIntegrationDetailstabasshowninthefollowingscreenshot:
AccessTokenisthekeytoallofourfurtherAPIcalls,andwithitwesuccessfullyfinish
ourauthenticationbitofOAuth-basedauthentication.
OAuth-basedWebAPIcalls
OncewehaveobtainedOAuthaccesstoken,fromtheprecedingsteps,wecanstart
makingWebAPIcallstoothermethods.EventhoughtheWebAPIcoverageisthesame
forbothRESTandSOAP,thereisasignificantdifferencewhenmakingmethodcalls.
Forthepurposeofgivingamorerobustexample,wewillbetargetingthecustomergroup
savemethod,(partially)definedinthevendor/magento/module-
customer/etc/webapi.xmlfileasfollows:
<routeurl="/V1/customerGroups"method="POST">
<serviceclass="Magento\Customer\Api\GroupRepositoryInterface"
method="save"/>
<resources>
<resourceref="Magento_Customer::group"/>
</resources>
</route>
TousetheaccesstokentomakeWebAPIcalls,likePOST/V1/customerGroups,weneed
toincludetheserequestparametersintheauthorizationrequestheaderinthecall:
oauth_consumer_key,availablefromtheMagentoadminarea,undertheintegration
editscreen.
oauth_nonce,randomvalue,uniquelygeneratedbytheapplicationforeachrequest.
oauth_signature_method,nameofthesignaturemethodusedtosigntherequest.
Validvaluesare:HMAC-SHA1,RSA-SHA1,andPLAINTEXT.
EventhoughtheOuthprotocolsupportsPLAINTEXT,Magentodoesnot.Wewillbe
usingHMAC-SHA1.
oauth_timestamp,integervalue,Unix-liketimestamp.
oauth_token,availablefromtheMagentoadminarea,undertheintegrationedit
screen.
oauth_version,MagentosupportsOauth1.0a,thusweuse1.0.
oauth_signature,generatedsignaturevalue,omittedfromthesignaturegeneration
process.
TogenerateanOAuth1.0aHMAC-SHA1signatureforaHTTPrequesttakesfocused
effort,ifdonemanually.
WeneedtodeterminetheHTTPmethodandURLoftherequest,whichequalstoPOST
http://magento2-merchant.loc/rest/V1/customerGroups.Itisimportanttousethe
correctprotocolhere,somakesurethatthehttps://orhttp://portionoftheURL
matchestheactualrequestsenttotheAPI.
Wethengatheralloftheparametersincludedintherequest.Therearetwosuchlocations
fortheseadditionalparameters:theURL(aspartofthequerystring)andtherequestbody.
IntheHTTPrequest,theparametersareURLencoded,butweneedtocollecttheraw
values.Inadditiontotherequestparameters,everyoauth_*parameterneedstobe
includedinthesignature,excepttheoauth_signatureitself.
Theparametersarenormalizedintoasinglestringasfollows:
Parametersaresortedbyname,usinglexicographicalbytevalueordering.Iftwoor
moreparameterssharethesamename,theyaresortedbytheirvalue.
Parametersareconcatenatedintheirsortedorderintoasinglestring.Foreach
parameter,thenameisseparatedfromthecorrespondingvaluebyan=character
(ASCIIcode61),evenifthevalueisempty.Eachname-valuepairisseparatedbyan
&character(ASCIIcode38).
Further,wedefinethesigningkeyasavalueof{ConsumerKey}+{&}+{AccessToken
Secret}.
Onceweapplythestringnormalizationrulestoparametersanddeterminethesigningkey,
wecallhash_hmac('sha1',$data,{SigningKey},true)togetthefinal
oauth_signaturevalue.
Thisshouldgetustheoauth_signatureasarandom28-characters-longstring,similarto
thisone–Pi/mGfA0SOlIxO9W30sEch6bjGE=.
Understandinghowtogeneratethesignaturestringisimportant,butgettingitrightevery
timeistediousandtimeconsuming.Wecanhelpourselvesbyinstantiatingtheobjectsof
thebuilt-in\OAuth\Common\Consumer\Credentialsand
\OAuth\OAuth1\Signature\Signatureclasses,like(partially)shownasfollows:
$credentials=new\OAuth\Common\Consumer\Credentials($consumerKey,
$consumerSecret,$magentoBaseUrl);
$signature=new\OAuth\OAuth1\Signature\Signature($credentials);
$signature->setTokenSecret($accessTokenSecret);
$signature->setHashingAlgorithm('HMAC-SHA1');
echo$signature->getSignature($uri,array(
'oauth_consumer_key'=>$consumerKey,
'oauth_nonce'=>'per-request-unique-token',
'oauth_signature_method'=>'HMAC-SHA1',
'oauth_timestamp'=>'1437319569',
'oauth_token'=>$accessToken,
'oauth_version'=>'1.0',
),'POST');
Nowthatwehavetheoauth_signaturevalue,wearereadytodoourconsolecurlREST
example.Itcomesdowntorunningthefollowingonaconsole:
curl-XPOSThttp://magento2.ce/rest/V1/customerGroups
-H'Content-Type:application/json'
-H'Authorization:OAuth
oauth_consumer_key="vw2xi6kaq0o3f7ay60owdpg2f8nt66g6",
oauth_nonce="per-request-token-by-app-1",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1437319569",
oauth_token="cney3fmk9p5282bm1khb83q846l7dner",
oauth_version="1.0",
oauth_signature="Pi/mGfA0SOlIxO9W30sEch6bjGE="'
-d'{"group":{"code":"TheBookWriter","tax_class_id":"3"}}'
Notethattheprecedingcommandismerelyvisuallybrokenintonewlines.Itshouldallbe
singlelineonaconsole.Onceexecuted,theAPIcallwillcreateanewcustomergroup
calledTheBookWriter.Alogicalquestiononemightasklookingatthecurlcommand
ishowcomewedidnotnormalizethePOSTdatapassedasJSONviathe–dflagswitch.
ThisisbecauseparametersintheHTTPPOSTrequestbodyareonlytakeninto
considerationforsignaturegenerationifcontent-typeisapplication/x-www-form-
urlencoded.
TheconsolecURLSOAPrequestsdonotrequireusageoftheOAuthsignature.Wecan
executeaSOAPrequestpassingAuthorization:Bearer{AccessTokenvalue}into
therequestheader,likeshowninthefollowingexample:
curl-XPOSThttp://magento2.ce/index.php/soap/default?services=
customerGroupRepositoryV1-H'Content-Type:application/soap+xml;
charset=utf-8;action="customerGroupRepositoryV1Save"'-H'Authorization:
Bearercney3fmk9p5282bm1khb83q846l7dner'-d@request.xml
Whererequest.xmlcontainscontentasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=customerGroupRepositoryV1">
<env:Body>
<ns1:customerGroupRepositoryV1SaveRequest>
<group>
<code>TheBookWriter</code>
<taxClassId>3</taxClassId>
</group>
</ns1:customerGroupRepositoryV1SaveRequest>
</env:Body>
</env:Envelope>
ThefollowingcodeexampledemonstratesthePHPcURLSOAP-likerequestforthe
customergroupsavemethodcall:
$request=newSoapClient(
'http://magento2.ce/index.php/soap/?wsdl&services=
customerGroupRepositoryV1',
array(
'soap_version'=>SOAP_1_2,
'stream_context'=>stream_context_create(array(
'http'=>array(
'header'=>'Authorization:Bearer
cney3fmk9p5282bm1khb83q846l7dner')
)
)
)
);
$response=$request->customerGroupRepositoryV1Save(array(
'group'=>array(
'code'=>'TheBookWriter',
'taxClassId'=>3
)
));
NoticehowthemethodnamecustomerGroupRepositoryV1Saveactuallycomprises
servicenamecustomerGroupRepositoryV1,plustheSavenameoftheactualmethod
withintheservice.
WecangetalistofallservicesdefinedbyopeningaURLlike
http://magento2.ce/soap/default?wsdl_listinthebrowser(dependingonour
Magentoinstallation).
Hands-onwithsession-based
authentication
Session-basedauthenticationisthethirdandmostsimpletypeofauthenticationin
Magento.Wedonothaveanycomplexitiesoftoken-passinghere.Asthecustomer,welog
intotheMagentostorefrontwithourcustomercredentials.Asanadmin,welogintothe
Magentoadminwithouradmincredentials.MagentousesacookienamedPHPSESSIDto
trackthesessionwhereourloginstateisstored.TheWebAPIframeworkusesourlogged-
insessioninformationtoverifyouridentityandauthorizeaccesstotherequested
resource.
Customerscanaccessresourcesthatareconfiguredwithanonymousorself-permissionin
thewebapi.xmlconfigurationfile,likeGET/rest/V1/customers/me.
Ifwetrytoopenthehttp://magento2.ce/rest/V1/customers/meURLwhileinthe
browser,butnotloggedinasthecustomer,wewouldgetaresponseasfollows:
<response>
<message>Consumerisnotauthorizedtoaccess%resources</message>
<parameters>
<resources>self</resources>
</parameters>
</response>
IfweloginasthecustomerandthentrytoopenthatsameURL,wewouldgetaresponse
asfollows:
<response>
<id>2</id>
<group_id>1</group_id>
<created_at>2015-11-2214:15:33</created_at>
<created_in>DefaultStoreView</created_in>
<email>john@change.me</email>
<firstname>John</firstname>
<lastname>Doe</lastname>
<store_id>1</store_id>
<website_id>1</website_id>
<addresses/>
<disable_auto_group_change>0</disable_auto_group_change>
</response>
AdminuserscanaccessresourcesthatareassignedtotheirMagentoadminprofile.
CreatingcustomWebAPIs
MagentocomeswithasolidnumberofAPImethodsthatwecancall.However,
sometimesthisisnotenough,asourbusinessneedsdictateadditionallogic,andweneed
tobeabletoaddourownmethodstotheWebAPI.
ThebestpartofcreatingourownAPI’sisthatwedonothavetobeconcernedabout
makingthemRESTorSOAP.MagentoabstractsthissothatourAPImethodsare
automaticallyavailableforRESTandforSOAPcalls.
AddingnewAPI’sconceptuallyevolvesaroundtwothings:definingbusinesslogic
throughvariousclasses,andexposingitviathewebapi.xmlfile.However,aswewillsoon
see,thereisalotofboilerplatetoit.
Let’screateaminiaturemodulecalledFoggyline_Slider,onwhichwewilldemonstrate
create(POST),update(PUT),delete(DELETE),andlist(GET)methodcalls.
Createamoduleregistrationfile,app/code/Foggyline/Slider/registration.php,with
content(partial)asfollows:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Foggyline_Slider',
__DIR__
);
Createamoduleconfigurationfile,app/code/Foggyline/Slider/etc/module.xml,with
contentasfollows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module
/etc/module.xsd">
<modulename="Foggyline_Slider"setup_version="1.0.0"/>
</config>
Createaninstallscriptwhereourfuturemodelswillpersistmoduledata.Wedosoby
creatingtheapp/code/Foggyline/Slider/Setup/InstallSchema.phpfilewithcontent
(partial)asfollows:
namespaceFoggyline\Slider\Setup;
useMagento\Framework\Setup\InstallSchemaInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\SchemaSetupInterface;
classInstallSchemaimplementsInstallSchemaInterface
{
publicfunctioninstall(SchemaSetupInterface$setup,
ModuleContextInterface$context)
{
$installer=$setup;
$installer->startSetup();
/**
*Createtable'foggyline_slider_slide'
*/
$table=$installer->getConnection()
->newTable($installer->getTable('foggyline_slider_slide'))
->addColumn(
'slide_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity'=>true,'unsigned'=>true,'nullable'=>
false,'primary'=>true],
'SlideId'
)
->addColumn(
'title',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
200,
[],
'Title'
)
->setComment('FoggylineSliderSlide');
$installer->getConnection()->createTable($table);
...
$installer->endSetup();
}
}
NowwespecifytheACLforourresources.OurresourcesaregoingtobeCRUDactions
wedoonourmoduleentities.Wewillstructureourmoduleinawaythatslideandimage
areseparateentities,whereoneslidecanhavemultipleimageentitieslinkedtoit.Thus,
wewouldliketobeabletocontrolaccesstosaveanddeleteactionsseparatelyforeach
entity.Wedosobydefiningtheapp/code/Foggyline/Slider/etc/acl.xmlfileas
follows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<acl>
<resources>
<resourceid="Magento_Backend::admin">
<resourceid="Magento_Backend::content">
<resourceid="Magento_Backend::content_elements">
<resourceid="Foggyline_Slider::slider"
title="Slider"sortOrder="10">
<resourceid="Foggyline_Slider::slide"
title="SliderSlide"sortOrder="10">
<resourceid=
"Foggyline_Slider::slide_save"title="SaveSlide"sortOrder="10"/>
<resourceid="Foggyline_Slider::
slide_delete"title="DeleteSlide"sortOrder="20"/>
</resource>
<resourceid="Foggyline_Slider::image"
title="SliderImage"sortOrder="10">
<resourceid=
"Foggyline_Slider::image_save"title="SaveImage"sortOrder="10"/>
<resourceid=
"Foggyline_Slider::image_delete"title="DeleteImage"sortOrder="20"/>
</resource>
</resource>
</resource>
</resource>
</resource>
</resources>
</acl>
</config>
NowthattheACLhasbeenset,wedefineourWebAPIresourceswithinthe
app/code/Foggyline/Slider/etc/webapi.xmlfile(partial)asfollows:
<routesxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation=
"urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<routeurl="/V1/foggylineSliderSlide/:slideId"method="GET">
<serviceclass="Foggyline\Slider\Api\SlideRepositoryInterface"
method="getById"/>
<resources>
<resourceref="Foggyline_Slider::slide"/>
</resources>
</route>
<routeurl="/V1/foggylineSliderSlide/search"method="GET">
<serviceclass="Foggyline\Slider\Api\SlideRepositoryInterface"
method="getList"/>
<resources>
<resourceref="anonymous"/>
</resources>
</route>
<routeurl="/V1/foggylineSliderSlide"method="POST">
<serviceclass="Foggyline\Slider\Api\SlideRepositoryInterface"
method="save"/>
<resources>
<resourceref="Foggyline_Slider::slide_save"/>
</resources>
</route>
<routeurl="/V1/foggylineSliderSlide/:id"method="PUT">
<serviceclass="Foggyline\Slider\Api\SlideRepositoryInterface"
method="save"/>
<resources>
<resourceref="Foggyline_Slider::slide_save"/>
</resources>
</route>
<routeurl="/V1/foggylineSliderSlide/:slideId"method="DELETE">
<serviceclass="Foggyline\Slider\Api\SlideRepositoryInterface"
method="deleteById"/>
<resources>
<resourceref="Foggyline_Slider::slide_delete"/>
</resources>
</route>
<routeurl="/V1/foggylineSliderImage/:imageId"method="GET">
<serviceclass="Foggyline\Slider\Api\ImageRepositoryInterface"
method="getById"/>
<resources>
<resourceref="Foggyline_Slider::image"/>
</resources>
</route>
<routeurl="/V1/foggylineSliderImage/search"method="GET">
<serviceclass="Foggyline\Slider\Api\ImageRepositoryInterface"
method="getList"/>
<resources>
<resourceref="Foggyline_Slider::image"/>
</resources>
</route>
<routeurl="/V1/foggylineSliderImage"method="POST">
<serviceclass="Foggyline\Slider\Api\ImageRepositoryInterface"
method="save"/>
<resources>
<resourceref="Foggyline_Slider::image_save"/>
</resources>
</route>
<routeurl="/V1/foggylineSliderImage/:id"method="PUT">
<serviceclass="Foggyline\Slider\Api\ImageRepositoryInterface"
method="save"/>
<resources>
<resourceref="Foggyline_Slider::image_save"/>
</resources>
</route>
<routeurl="/V1/foggylineSliderImage/:imageId"method="DELETE">
<serviceclass="Foggyline\Slider\Api\ImageRepositoryInterface"
method="deleteById"/>
<resources>
<resourceref="Foggyline_Slider::image_delete"/>
</resources>
</route>
</routes>
Noticehoweachofthoseserviceclassattributespointtotheinterface,nottheclass.This
isthewayweshouldbuildourexposableservices,alwayshavinganinterfacedefinition
behindthem.Aswewillsoonsee,usingdi.xml,thisdoesnotmeanMagentowilltryto
createobjectsfromtheseinterfacesdirectly.
Wenowcreatetheapp/code/Foggyline/Slider/etc/di.xmlfilewithcontent(partial)as
follows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation=
"urn:magento:framework:ObjectManager/etc/config.xsd">
<preferencefor="Foggyline\Slider\Api\Data\SlideInterface"
type="Foggyline\Slider\Model\Slide"/>
<preferencefor="Foggyline\Slider\Api\SlideRepositoryInterface"type=
"Foggyline\Slider\Model\SlideRepository"/>
...
</config>
WhatishappeninghereisthatwearetellingMagentosomethinglike,“hey,wheneveryou
needtopassaroundaninstancethatconformstothe
Foggyline\Slider\Api\Data\SlideInterfaceinterface,preferablyusethe
Foggyline\Slider\Model\Slideclassforit.”
Atthispoint,westilldonothaveanyofthoseinterfacesormodelclassesactuallycreated.
WhencreatingAPIs,weshouldfirststartbydefininginterfaces,andthenourmodels
shouldextendfromthoseinterfaces.
InterfaceFoggyline\Slider\Api\Data\SlideInterfaceisdefinedwithinthe
app/code/Foggyline/Slider/Api/Data/SlideInterface.phpfile(partial)asfollows:
namespaceFoggyline\Slider\Api\Data;
/**
*@api
*/
interfaceSlideInterface
{
constPROPERTY_ID='slide_id';
constPROPERTY_SLIDE_ID='slide_id';
constPROPERTY_TITLE='title';
/**
*GetSlideentity'slide_id'propertyvalue
*@returnint|null
*/
publicfunctiongetId();
/**
*SetSlideentity'slide_id'propertyvalue
*@paramint$id
*@return$this
*/
publicfunctionsetId($id);
/**
*GetSlideentity'slide_id'propertyvalue
*@returnint|null
*/
publicfunctiongetSlideId();
/**
*SetSlideentity'slide_id'propertyvalue
*@paramint$slideId
*@return$this
*/
publicfunctionsetSlideId($slideId);
/**
*GetSlideentity'title'propertyvalue
*@returnstring|null
*/
publicfunctiongetTitle();
/**
*SetSlideentity'title'propertyvalue
*@paramstring$title
*@return$this
*/
publicfunctionsetTitle($title);
}
Wearegoingforultimatesimplificationhere.OurSlideentityonlyreallyhasIDandtitle
values.Theidandslide_idpointtothesamefieldinthedatabaseandthe
implementationoftheirgettersandsettersshouldyieldthesameresults.
AlthoughAPI/Data/*.phpinterfacesbecomeblueprintrequirementsforourdatamodels,
wealsohaveApi/*RepositoryInterface.phpfiles.Theideahereistoextractcreate,
update,delete,search,andsimilardata-handlinglogicawayfromthedatamodelclassinto
itsownclass.Thisway,ourmodelclassesbecomemorepuredataandbusinesslogic
classeswhiletherestofpersistenceandsearch-relatedlogicmovesintotheserepository
classes.
OurSlideRepositoryInterfaceisdefinedwithinthe
app/code/Foggyline/Slider/Api/SlideRepositoryInterface.phpfileasfollows:
namespaceFoggyline\Slider\Api;
/**
*@api
*/
interfaceSlideRepositoryInterface
{
/**
*Retrieveslideentity.
*@paramint$slideId
*@return\Foggyline\Slider\Api\Data\SlideInterface
*@throws\Magento\Framework\Exception\NoSuchEntityExceptionIfslide
withthespecifiedIDdoesnotexist.
*@throws\Magento\Framework\Exception\LocalizedException
*/
publicfunctiongetById($slideId);
/**
*Saveslide.
*@param\Foggyline\Slider\Api\Data\SlideInterface$slide
*@return\Foggyline\Slider\Api\Data\SlideInterface
*@throws\Magento\Framework\Exception\LocalizedException
*/
publicfunctionsave(\Foggyline\Slider\Api\Data\SlideInterface$slide);
/**
*Retrieveslidesmatchingthespecifiedcriteria.
*@param\Magento\Framework\Api\SearchCriteriaInterface$searchCriteria
*@return\Magento\Framework\Api\SearchResultsInterface
*@throws\Magento\Framework\Exception\LocalizedException
*/
publicfunctiongetList(\Magento\Framework\Api\SearchCriteriaInterface
$searchCriteria);
/**
*DeleteslidebyID.
*@paramint$slideId
*@returnbooltrueonsuccess
*@throws\Magento\Framework\Exception\NoSuchEntityException
*@throws\Magento\Framework\Exception\LocalizedException
*/
publicfunctiondeleteById($slideId);
}
Withinterfacesinplace,wecanmoveontomodelclass.Inordertopersistandfetchdata
inadatabase,ourSlideentityreallyneedsthreefilesundertheModeldirectory.Theseare
calleddatamodel,resourceclass,andcollectionclass.
Thedatamodelclassisdefinedunderthe
app/code/Foggyline/Slider/Model/Slide.phpfile(partial)asfollows:
namespaceFoggyline\Slider\Model;
classSlideextends\Magento\Framework\Model\AbstractModel
implements\Foggyline\Slider\Api\Data\SlideInterface{/**
*InitializeFoggylineSlideModel
*
*@returnvoid
*/
protectedfunction_construct()
{
/*_init($resourceModel)*/
$this->_init('Foggyline\Slider\Model\ResourceModel\Slide');
}
/**
*GetSlideentity'slide_id'propertyvalue
*
*@api
*@returnint|null
*/
publicfunctiongetId()
{
return$this->getData(self::PROPERTY_ID);
}
/**
*SetSlideentity'slide_id'propertyvalue
*
*@api
*@paramint$id
*@return$this
*/
publicfunctionsetId($id)
{
$this->setData(self::PROPERTY_ID,$id);
return$this;
}
/**
*GetSlideentity'slide_id'propertyvalue
*
*@api
*@returnint|null
*/
publicfunctiongetSlideId()
{
return$this->getData(self::PROPERTY_SLIDE_ID);
}
/**
*SetSlideentity'slide_id'propertyvalue
*
*@api
*@paramint$slideId
*@return$this
*/
publicfunctionsetSlideId($slideId)
{
$this->setData(self::PROPERTY_SLIDE_ID,$slideId);
return$this;
}
/**
*GetSlideentity'title'propertyvalue
*
*@api
*@returnstring|null
*/
publicfunctiongetTitle()
{
return$this->getData(self::PROPERTY_TITLE);
}
/**
*SetSlideentity'title'propertyvalue
*
*@api
*@paramstring$title
*@return$this
*/
publicfunctionsetTitle($title)
{
$this->setData(self::PROPERTY_TITLE,$title);
}
}
Followingthemodeldataclassisthemodelresourceclass,definedinthe
app/code/Foggyline/Slider/Model/ResourceModel/Slide.phpfile(partial)asfollows:
namespaceFoggyline\Slider\Model\ResourceModel;
/**
*FoggylineSlideresource
*/
classSlideextends\Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
/**
*Definemaintable
*
*@returnvoid
*/
protectedfunction_construct()
{
/*_init($mainTable,$idFieldName)*/
$this->_init('foggyline_slider_slide','slide_id');
}
}
Finally,thethirdbitisthemodelcollectionclass,definedinthe
app/code/Foggyline/Slider/Model/ResourceModel/Slide/Collection.phpfileas
follows:
namespaceFoggyline\Slider\Model\ResourceModel\Slide;
/**
*Foggylineslidescollection
*/
classCollectionextends
\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
{
/**
*Defineresourcemodelandmodel
*
*@returnvoid
*/
protectedfunction_construct()
{
/*_init($model,$resourceModel)*/
$this->_init('Foggyline\Slider\Model\Slide',
'Foggyline\Slider\Model\ResourceModel\Slide');
}
}
Ifweweretomanuallyinstantiatethemodeldataclassnow,wewouldbeabletopersist
thedatainthedatabase.Tocompletethedi.xmlrequirements,westilllackonemorefinal
ingredient–theModel/SlideRepositoryclassfile.
Letusgoandcreatetheapp/code/Foggyline/Slider/Model/SlideRepository.phpfile
withcontent(partial)asfollows:
namespaceFoggyline\Slider\Model;
useMagento\Framework\Api\DataObjectHelper;
useMagento\Framework\Api\SearchCriteriaInterface;
useMagento\Framework\Exception\CouldNotDeleteException;
useMagento\Framework\Exception\CouldNotSaveException;
useMagento\Framework\Exception\NoSuchEntityException;
useMagento\Framework\Reflection\DataObjectProcessor;
classSlideRepositoryimplements
\Foggyline\Slider\Api\SlideRepositoryInterface
{
/**
*@var\Foggyline\Slider\Model\ResourceModel\Slide
*/
protected$resource;
/**
*@var\Foggyline\Slider\Model\SlideFactory
*/
protected$slideFactory;
/**
*@var\Foggyline\Slider\Model\ResourceModel\Slide\CollectionFactory
*/
protected$slideCollectionFactory;
/**
*@var\Magento\Framework\Api\SearchResultsInterface
*/
protected$searchResultsFactory;
/**
*@var\Magento\Framework\Api\DataObjectHelper
*/
protected$dataObjectHelper;
/**
*@var\Magento\Framework\Reflection\DataObjectProcessor
*/
protected$dataObjectProcessor;
/**
*@var\Foggyline\Slider\Api\Data\SlideInterfaceFactory
*/
protected$dataSlideFactory;
/**
*@paramResourceModel\Slide$resource
*@paramSlideFactory$slideFactory
*@paramResourceModel\Slide\CollectionFactory$slideCollectionFactory
*@param\Magento\Framework\Api\SearchResultsInterface
$searchResultsFactory
*@paramDataObjectHelper$dataObjectHelper
*@paramDataObjectProcessor$dataObjectProcessor
*@param\Foggyline\Slider\Api\Data\SlideInterfaceFactory
$dataSlideFactory
*/
publicfunction__construct(
\Foggyline\Slider\Model\ResourceModel\Slide$resource,
\Foggyline\Slider\Model\SlideFactory$slideFactory,
\Foggyline\Slider\Model\ResourceModel\Slide\CollectionFactory
$slideCollectionFactory,
\Magento\Framework\Api\SearchResultsInterface
$searchResultsFactory,
\Magento\Framework\Api\DataObjectHelper$dataObjectHelper,
\Magento\Framework\Reflection\DataObjectProcessor
$dataObjectProcessor,
\Foggyline\Slider\Api\Data\SlideInterfaceFactory$dataSlideFactory
)
{
$this->resource=$resource;
$this->slideFactory=$slideFactory;
$this->slideCollectionFactory=$slideCollectionFactory;
$this->searchResultsFactory=$searchResultsFactory;
$this->dataObjectHelper=$dataObjectHelper;
$this->dataObjectProcessor=$dataObjectProcessor;
$this->dataSlideFactory=$dataSlideFactory;
}
...
}
Itmightappearthatthereisalotgoingonhere,butreallywearejustpassingonsome
classandinterfacenamestotheconstructorinordertoinstantiatetheobjectswewilluse
acrossindividualservicemethodsdefinedinthewebapi.xmlfile.
ThefirstservicemethodonourlistisgetById,definedwithinSlideRepository.phpas
follows:
/**
*Retrieveslideentity.
*
*@api
*@paramint$slideId
*@return\Foggyline\Slider\Api\Data\SlideInterface
*@throws\Magento\Framework\Exception\NoSuchEntityExceptionIfslidewith
thespecifiedIDdoesnotexist.
*@throws\Magento\Framework\Exception\LocalizedException
*/
publicfunctiongetById($slideId)
{
$slide=$this->slideFactory->create();
$this->resource->load($slide,$slideId);
if(!$slide->getId()){
thrownewNoSuchEntityException(__('Slidewithid%1doesnot
exist.',$slideId));
}
return$slide;
}
Thenwehavethesavemethod,definedwithinSlideRepository.phpasfollows:
/**
*Saveslide.
*
*@param\Foggyline\Slider\Api\Data\SlideInterface$slide
*@return\Foggyline\Slider\Api\Data\SlideInterface
*@throws\Magento\Framework\Exception\LocalizedException
*/
publicfunctionsave(\Foggyline\Slider\Api\Data\SlideInterface$slide)
{
try{
$this->resource->save($slide);
}catch(\Exception$exception){
thrownewCouldNotSaveException(__($exception->getMessage()));
}
return$slide;
}
ThesavemethodaddressesbothPOSTandPUTrequestsdefinedinwebapi.xml,thus
effectivelyhandlingthecreationofnewslidesoranupdateofexistingones.
Goingfurther,wehavethegetListmethod,definedwithinSlideRepository.phpas
follows:
/**
*Retrieveslidesmatchingthespecifiedcriteria.
*
*@param\Magento\Framework\Api\SearchCriteriaInterface$searchCriteria
*@return\Magento\Framework\Api\SearchResultsInterface
*@throws\Magento\Framework\Exception\LocalizedException
*/
publicfunctiongetList(\Magento\Framework\Api\SearchCriteriaInterface
$searchCriteria)
{
$this->searchResultsFactory->setSearchCriteria($searchCriteria);
$collection=$this->slideCollectionFactory->create();
foreach($searchCriteria->getFilterGroups()as$filterGroup){
foreach($filterGroup->getFilters()as$filter){
$condition=$filter->getConditionType()?:'eq';
$collection->addFieldToFilter($filter->getField(),[$condition
=>$filter->getValue()]);
}
}
$this->searchResultsFactory->setTotalCount($collection->getSize());
$sortOrders=$searchCriteria->getSortOrders();
if($sortOrders){
foreach($sortOrdersas$sortOrder){
$collection->addOrder(
$sortOrder->getField(),
(strtoupper($sortOrder->getDirection())==='ASC')?'ASC'
:'DESC'
);
}
}
$collection->setCurPage($searchCriteria->getCurrentPage());
$collection->setPageSize($searchCriteria->getPageSize());
$slides=[];
/**@var\Foggyline\Slider\Model\Slide$slideModel*/
foreach($collectionas$slideModel){
$slideData=$this->dataSlideFactory->create();
$this->dataObjectHelper->populateWithArray(
$slideData,
$slideModel->getData(),
'\Foggyline\Slider\Api\Data\SlideInterface'
);
$slides[]=$this->dataObjectProcessor->buildOutputDataArray(
$slideData,
'\Foggyline\Slider\Api\Data\SlideInterface'
);
}
$this->searchResultsFactory->setItems($slides);
return$this->searchResultsFactory;
}
Finally,wehavethedeleteByIdmethod,definedwithinSlideRepository.phpas
follows:
/**
*DeleteSlide
*
*@param\Foggyline\Slider\Api\Data\SlideInterface$slide
*@returnbool
*@throwsCouldNotDeleteException
*/
publicfunctiondelete(\Foggyline\Slider\Api\Data\SlideInterface$slide)
{
try{
$this->resource->delete($slide);
}catch(\Exception$exception){
thrownewCouldNotDeleteException(__($exception->getMessage()));
}
returntrue;
}
/**
*DeleteslidebyID.
*
*@paramint$slideId
*@returnbooltrueonsuccess
*@throws\Magento\Framework\Exception\NoSuchEntityException
*@throws\Magento\Framework\Exception\LocalizedException
*/
publicfunctiondeleteById($slideId)
{
return$this->delete($this->getById($slideId));
}
KeepinmindthatweonlycoveredtheSlideentityintheprecedingpartialcode
examples,whichisenoughtoprogressfurtherwithAPIcallexamples.
APIcallexamples
SinceallofourdefinedAPI’sareresourceprotected,wefirstneedtoauthenticateasthe
adminuser,assumingtheadminuserhasaccesstoallourcustomresourcesthat
encompasstheoneswedefined.Forsimplicitysake,wewillusethetoken-based
authenticationmethod,examplesofwhicharegivenpreviouslyinthischapter.Once
authenticated,weshouldhavea32randomcharacterslongtokenlike
pk8h93nq9cevaw55bohkjbp0o7kpl4d3,forexample.
Oncethetokenkeyhasbeenobtained,wewilltestthefollowingAPIcallsusingconsole
cURL,PHPcURL,PHPSoapClient,andconsoleSOAPstylecURLexamples:
GET/V1/foggylineSliderSlide/:slideId,callsthegetByIdservicemethod,
requirestheFoggyline_Slider::slideresource
GET/V1/foggylineSliderSlide/search,callsthegetListservicemethod,requires
theFoggyline_Slider::slideresource
POST/V1/foggylineSliderSlide,callsthesaveservicemethod,requiresthe
Foggyline_Slider::slide_saveresource
PUT/V1/foggylineSliderSlide/:id,callsthesaveservicemethod,requiresthe
Foggyline_Slider::slide_saveresource
DELETE/V1/foggylineSliderSlide/:slideId,callsthedeleteByIdservice
method,requirestheFoggyline_Slider::slide_deleteresource
ThegetByIdservicemethodcallexamples
TheconsolecURLstyleforexecutingGET/V1/foggylineSliderSlide/:slideIdisdone
asfollows:
curl-XGET-H'Content-type:application/json'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
http://magento2.ce/rest/V1/foggylineSliderSlide/1
ThePHPcURLstyleforexecutingGET/V1/foggylineSliderSlide/:slideIdisdoneas
follows:
$ch=curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide/1');
curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'GET');
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HTTPHEADER,array(
'Content-Type:application/json',
'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'
));
$result=curl_exec($ch);
TheresponseforconsoleandPHPcURLstyleshouldbeaJSONstringsimilartothe
followingone:
{"slide_id":1,"title":"Awesomestuff#1"}
ThePHPSoapClientstyleforexecutingGET/V1/foggylineSliderSlide/:slideIdis
doneasfollows:
$request=newSoapClient(
'http://magento2.ce/index.php/soap/?
wsdl&services=foggylineSliderSlideRepositoryV1',
array(
'soap_version'=>SOAP_1_2,
'stream_context'=>stream_context_create(array(
'http'=>array(
'header'=>'Authorization:Bearer
pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
)
)
)
);
$response=$request->
foggylineSliderSlideRepositoryV1GetById(array('slideId'=>1));
TheresponseforPHPSoapClientstyleshouldbethestdClassPHPobjectasfollows:
object(stdClass)#2(1){
["result"]=>
object(stdClass)#3(2){
["slideId"]=>
int(1)
["title"]=>
string(16)"Awesomestuff#1"
}
}
TheconsoleSOAPstylecURLforexecutingGET/V1/foggylineSliderSlide/:slideId
isdoneasfollows:
curl-XPOST\
-H'Content-Type:application/soap+xml;charset=utf-8;
action="foggylineSliderSlideRepositoryV1GetById"'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
-d@request.xml\
http://magento2.ce/index.php/soap/default?services=foggyline
SliderSlideRepositoryV1
Whererequest.xmlhascontentasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1GetByIdRequest>
<slideId>1</slideId>
</ns1:foggylineSliderSlideRepositoryV1GetByIdRequest>
</env:Body>
</env:Envelope>
NoticehowwedidnotreallydoGET,ratheraPOSTtypeofrequest.Also,theURLto
whichwearepointingourPOSTisnotreallythesameaswithpreviousrequests.Thisis
becauseMagentoSOAPrequestsarealwaysPOST(orPUT)type,asthedataissubmittedin
XMLformat.XMLformatinreturnspecifiestheservice,andtherequestheaderaction
specifiesthemethodtobecalledontheservice.
TheresponseforconsoleSOAPstylecURLshouldbeanXMLasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1GetByIdResponse>
<result>
<slideId>1</slideId>
<title>Awesomestuff#1</title>
</result>
</ns1:foggylineSliderSlideRepositoryV1GetByIdResponse>
</env:Body>
</env:Envelope>
ThegetListservicemethodcallexamples
TheconsolecURLstyleforexecutingGET/V1/foggylineSliderSlide/searchisdone
asfollows:
curl-XGET-H'Content-type:application/json'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
"http://magento2.ce/rest/V1/foggylineSliderSlide/search?
search_criteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bfield%5D=ti
tle&search_criteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bvalue%5
D=%25some%25&search_criteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%
5Bcondition_type%5D=like&search_criteria%5Bcurrent_page%5D=1&search_criteri
a%5Bpage_size%5D=10&search_criteria%5Bsort_orders%5D%5B0%5D%5Bfield%5D=slid
e_id&search_criteria%5Bsort_orders%5D%5B0%5D%5Bdirection%5D=ASC"
ThePHPcURLstyleforexecutingGET/V1/foggylineSliderSlide/searchisdoneas
follows:
$searchCriteriaJSON='{
"search_criteria":{
"filter_groups":[
{
"filters":[
{
"field":"title",
"value":"%some%",
"condition_type":"like"
}
]
}
],
"current_page":1,
"page_size":10,
"sort_orders":[
{
"field":"slide_id",
"direction":"ASC"
}
]
}
}';
$searchCriteriaQueryString=
http_build_query(json_decode($searchCriteriaJSON));
$ch=curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide/search?'
.$searchCriteriaQueryString);
curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'GET');
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HTTPHEADER,array(
'Content-Type:application/json',
'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'
));
$result=curl_exec($ch);
TheresponseforconsoleandPHPcURLstyleshouldbeaJSONstringsimilartothe
followingone:
{"items":[{"slide_id":2,"title":"Justsomeotherslider"},
{"slide_id":1,"title":"Awesomestuff#1"}],"search_criteria":
{"filter_groups":[{"filters":
[{"field":"title","value":"%some%","condition_type":"like"}]}],
"sort_orders":[{"field":"slide_id","direction":"-
1"}],"page_size":10,"current_page":1},"total_count":2}
ThePHPSoapClientstyleforexecutingGET/V1/foggylineSliderSlide/searchisdone
asfollows:
$searchCriteria=[
'searchCriteria'=>
[
'filterGroups'=>
[
[
'filters'=>
[
[
'field'=>'title',
'value'=>'%some%',
'condition_type'=>'like',
],
],
],
],
'currentPage'=>1,
'pageSize'=>10,
'sort_orders'=>
[
[
'field'=>'slide_id',
'direction'=>'ASC',
],
],
],
];
$request=newSoapClient(
'http://magento2.ce/index.php/soap/?wsdl&services=
foggylineSliderSlideRepositoryV1',
array(
'soap_version'=>SOAP_1_2,
'trace'=>1,
'stream_context'=>stream_context_create(array(
'http'=>array(
'header'=>'Authorization:Bearer
pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
)
)
)
);
$response=$request->
foggylineSliderSlideRepositoryV1GetList($searchCriteria);
TheresponseforPHPSoapClientstyleshouldbethestdClassPHPobjectasfollows:
object(stdClass)#2(1){
["result"]=>
object(stdClass)#3(3){
["items"]=>
object(stdClass)#4(0){
}
["searchCriteria"]=>
object(stdClass)#5(3){
["filterGroups"]=>
object(stdClass)#6(1){
["item"]=>
object(stdClass)#7(1){
["filters"]=>
object(stdClass)#8(1){
["item"]=>
object(stdClass)#9(2){
["field"]=>
string(5)"title"
["value"]=>
string(6)"%some%"
}
}
}
}
["pageSize"]=>
int(10)
["currentPage"]=>
int(1)
}
["totalCount"]=>
int(0)
}
}
TheconsoleSOAPstylecURLforexecutingGET/V1/foggylineSliderSlide/searchis
doneasfollows:
curl-XPOST\
-H'Content-Type:application/soap+xml;charset=utf-8;
action="foggylineSliderSlideRepositoryV1GetList"'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
-d@request.xml\
http://magento2.ce/index.php/soap/default?services=foggyline
SliderSlideRepositoryV1
Whererequest.xmlhascontentasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1GetListRequest>
<searchCriteria>
<filterGroups>
<item>
<filters>
<item>
<field>title</field>
<value>%some%</value>
</item>
</filters>
</item>
</filterGroups>
<pageSize>10</pageSize>
<currentPage>1</currentPage>
</searchCriteria>
</ns1:foggylineSliderSlideRepositoryV1GetListRequest>
</env:Body>
</env:Envelope>
NoticewedidnotreallydoGET,ratherPOST.Also,theURLtowhichwearepointingour
POSTisnotreallythesameaswithpreviousrequests.ThisisbecauseMagentoSOAP
requestsarealwaysPOSTtype,asthedataissubmittedinXMLformat.XMLformatin
returnspecifiestheservice,andtherequestheaderactionspecifiesthemethodtobecalled
ontheservice.
TheresponseforconsoleSOAPstylecURLshouldbeanXMLasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1GetListResponse>
<result>
<items/>
<searchCriteria>
<filterGroups>
<item>
<filters>
<item>
<field>title</field>
<value>%some%</value>
</item>
</filters>
</item>
</filterGroups>
<pageSize>10</pageSize>
<currentPage>1</currentPage>
</searchCriteria>
<totalCount>0</totalCount>
</result>
</ns1:foggylineSliderSlideRepositoryV1GetListResponse>
</env:Body>
</env:Envelope>
Thesave(asnew)servicemethodcallexamples
TheconsolecURLstyleforexecutingPOST/V1/foggylineSliderSlideisdoneas
follows:
curl-XPOST-H'Content-type:application/json'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
-d'{"slide":{"title":"APItest"}}'\
http://magento2.ce/rest/V1/foggylineSliderSlide/
ThePHPcURLstyleforexecutingPOST/V1/foggylineSliderSlideisdoneasfollows:
$slide=json_encode(['slide'=>['title'=>'APItest']]);
$ch=curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide');
curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'POST');
curl_setopt($ch,CURLOPT_POSTFIELDS,$slide);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HTTPHEADER,array(
'Content-Type:application/json',
'Content-Length:'.strlen($slide),
'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'
));
$result=curl_exec($ch);
TheresponseforconsoleandPHPcURLstyleshouldbeaJSONstringsimilartothe
followingone:
{"slide_id":4,"title":"APItest"}
ThePHPSoapClientstyleforexecutingPOST/V1/foggylineSliderSlideisdoneas
follows:
$slide=['slide'=>['title'=>'APItest']];
$request=newSoapClient(
'http://magento2.ce/index.php/soap/?wsdl&services=
foggylineSliderSlideRepositoryV1',
array(
'soap_version'=>SOAP_1_2,
'trace'=>1,
'stream_context'=>stream_context_create(array(
'http'=>array(
'header'=>'Authorization:Bearer
pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
)
)
)
);
$response=$request->foggylineSliderSlideRepositoryV1Save($slide);
TheresponseforPHPSoapClientstyleshouldbethestdClassPHPobjectasfollows:
object(stdClass)#2(1){
["result"]=>
object(stdClass)#3(2){
["slideId"]=>
int(6)
["title"]=>
string(8)"APItest"
}
}
TheconsoleSOAPstylecURLforexecutingPOST/V1/foggylineSliderSlideisdoneas
follows:
curl-XPOST\
-H'Content-Type:application/soap+xml;charset=utf-8;
action="foggylineSliderSlideRepositoryV1Save"'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
-d@request.xml\
http://magento2.ce/index.php/soap/default?services=foggyline
SliderSlideRepositoryV1
Whererequest.xmlhascontentasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1SaveRequest>
<slide>
<title>APItest</title>
</slide>
</ns1:foggylineSliderSlideRepositoryV1SaveRequest>
</env:Body>
</env:Envelope>
TheresponseforconsoleSOAPstylecURLshouldbeanXMLasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1SaveResponse>
<result>
<slideId>8</slideId>
<title>APItest</title>
</result>
</ns1:foggylineSliderSlideRepositoryV1SaveResponse>
</env:Body>
</env:Envelope>
Thesave(asupdate)servicemethodcallexamples
TheconsolecURLstyleforexecutingPUT/V1/foggylineSliderSlide/:idisdoneas
follows:
curl-XPUT-H'Content-type:application/json'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
-d'{"slide":{"slide_id":2,"title":"APIupdatetest"}}'\
http://magento2.ce/rest/V1/foggylineSliderSlide/2
ThePHPcURLstyleforexecutingPUT/V1/foggylineSliderSlide/:idisdoneas
follows:
$slideId=2;
$slide=json_encode(['slide'=>['slide_id'=>$slideId,'title'=>'API
updatetest']]);
$ch=curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide/'.
$slideId);
curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'PUT');
curl_setopt($ch,CURLOPT_POSTFIELDS,$slide);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HTTPHEADER,array(
'Content-Type:application/json',
'Content-Length:'.strlen($slide),
'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'
));
$result=curl_exec($ch);
TheresponseforconsoleandPHPcURLstyleshouldbeaJSONstringsimilartothe
followingone:
{"id":2,"slide_id":2,"title":"APIupdatetest"}
ThePHPSoapClientstyleforexecutingPUT/V1/foggylineSliderSlide/:idisdoneas
follows:
$slideId=2;
$slide=['slide'=>['slideId'=>$slideId,'title'=>'APIupdatetest']];
$request=newSoapClient(
'http://magento2.ce/index.php/soap/?wsdl&services=
foggylineSliderSlideRepositoryV1',
array(
'soap_version'=>SOAP_1_2,
'trace'=>1,
'stream_context'=>stream_context_create(array(
'http'=>array(
'header'=>'Authorization:Bearer
pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
)
)
)
);
$response=$request->foggylineSliderSlideRepositoryV1Save($slide);
TheresponseforPHPSoapClientstyleshouldbethestdClassPHPobjectasfollows:
object(stdClass)#2(1){
["result"]=>
object(stdClass)#3(2){
["slideId"]=>
int(2)
["title"]=>
string(15)"APIupdatetest"
}
}
TheconsoleSOAPstylecURLforexecutingPUT/V1/foggylineSliderSlide/:idis
doneasfollows:
curl-XPUT\
-H'Content-Type:application/soap+xml;charset=utf-8;
action="foggylineSliderSlideRepositoryV1Save"'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
-d@request.xml\
http://magento2.ce/index.php/soap/default?services=
foggylineSliderSlideRepositoryV1
Whererequest.xmlhascontentasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1SaveRequest>
<slide>
<slideId>2</slideId>
<title>APIupdatetest</title>
</slide>
</ns1:foggylineSliderSlideRepositoryV1SaveRequest>
</env:Body>
</env:Envelope>
TheresponseforconsoleSOAPstylecURLshouldbeanXMLasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1SaveResponse>
<result>
<slideId>2</slideId>
<title>APIupdatetest</title>
</result>
</ns1:foggylineSliderSlideRepositoryV1SaveResponse>
</env:Body>
</env:Envelope>
ThedeleteByIdservicemethodcallexamples
TheconsolecURLstyleforexecutingDELETE/V1/foggylineSliderSlide/:slideIdis
doneasfollows:
curl-XDELETE-H'Content-type:application/json'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
http://magento2.ce/rest/V1/foggylineSliderSlide/3
ThePHPcURLstyleforexecutingDELETE/V1/foggylineSliderSlide/:slideIdis
doneasfollows:
$slideId=4;
$ch=curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide/'.
$slideId);
curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'DELETE');
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HTTPHEADER,array(
'Content-Type:application/json',
'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'
));
$result=curl_exec($ch);
TheresponseforconsoleandPHPcURLstyleshouldbeaJSONstringsimilartothe
followingone:
true
ThePHPSoapClientstyleforexecutingDELETE/V1/foggylineSliderSlide/:slideIdis
doneasfollows:
$slideId=2;
$request=newSoapClient(
'http://magento2.ce/index.php/soap/?wsdl&services=
foggylineSliderSlideRepositoryV1',
array(
'soap_version'=>SOAP_1_2,
'trace'=>1,
'stream_context'=>stream_context_create(array(
'http'=>array(
'header'=>'Authorization:Bearer
pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
)
)
)
);
$response=$request->
foggylineSliderSlideRepositoryV1DeleteById(array('slideId'=>$slideId));
TheresponseforPHPSoapClientstyleshouldbethestdClassPHPobjectasfollows:
object(stdClass)#2(1){
["result"]=>
bool(true)
}
TheconsoleSOAPstylecURLforexecutingDELETE
/V1/foggylineSliderSlide/:slideIdisdoneasfollows:
curl-XPOST\
-H'Content-Type:application/soap+xml;charset=utf-8;
action="foggylineSliderSlideRepositoryV1DeleteById"'\
-H'Authorization:Bearerpk8h93nq9cevaw55bohkjbp0o7kpl4d3'\
-d@request.xml\
http://magento2.ce/index.php/soap/default?services=
foggylineSliderSlideRepositoryV1
Whererequest.xmlhascontentasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1DeleteByIdRequest>
<slideId>5</slideId>
</ns1:foggylineSliderSlideRepositoryV1DeleteByIdRequest>
</env:Body>
</env:Envelope>
TheresponseforconsoleSOAPstylecURLshouldbeanXMLasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<env:Envelopexmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns1="http://magento2.ce/index.php/soap/default?
services=foggylineSliderSlideRepositoryV1">
<env:Body>
<ns1:foggylineSliderSlideRepositoryV1DeleteByIdResponse>
<result>true</result>
</ns1:foggylineSliderSlideRepositoryV1DeleteByIdResponse>
</env:Body>
</env:Envelope>
TheprecedingAPIcallexamplescoverallofourcustom-definedAPIsfortheSlide
entity.
Lookingbackatthe$searchCriteriavariable,weusedtheGETtypeofHTTPmethod,
passingtheentirevariableasaquerystring.Ifwethinkaboutit,wecouldhavespecified
POSTduringtheWebAPIresourcedefinitionandpackedthecontentofthe
$searchCriteriavariableintotherequestbody.AlthoughtheGETmethodapproach
mightlookabitdirtier,imagineifweassignedtheanonymousorselfroletotheresource:
wewouldbeabletosimplyopenalengthyURLinthebrowserandhavethesearch
results.Thinkofapossiblewidgetuse,whereawidgetwouldsimplydoanAJAXrequest
totheURLandfetchtheresultsforguestsorthecustomer.
Thefullmodulesourcecodecanbefoundhere:https://github.com/ajzele/B05032-
Foggyline_Slider.AsidefromtheSlideentity,thefullmodulecodeincludestheImage
entityaswell.Sinceeachslidecancontainmultipleimages,wecanfurthertesttheImage
APIcallsanalogoustotheprecedingcalls.
SearchCriteriaInterfaceforlistfiltering
Knowinghowtodoaproperlistfilteringtofetchtheentitiesthatmatchacertainlookup
isessentialfortheeffectiveuseofgetListservicesacrosscoreMagentoandpossibly
custom-codedAPI’s.Anexampleisfetchingthelistofcustomersregisteredwithinthe
last24hoursforthelatestaddedproduct.
Let’stakealookbackatourapp/code/Foggyline/Slider/etc/webapi.xmlfile,thebit
wherewedefinedtheservicemethod="getList".Theserviceclassisdefinedas
Foggyline\Slider\Api\SlideRepositoryInterface,whichisdefinedasapreference
fortheFoggyline\Slider\Model\SlideRepositoryclass.Finally,withinthe
SlideRepositoryclass,wehavetheactualgetList.MethodgetListisdefinedas
follows:
getList(\Magento\Framework\Api\SearchCriteriaInterface$searchCriteria);
WecanseethatthegetListmethodtakesonlyoneparameter,objectinstance,that
complieswithSearchCriteriaInterfacecalled$searchCriteria.
Whatthismeansiswealreadyhavethe(incomplete)JSONobjectofthefollowingtypeto
passtothegetListmethod:
{
"search_criteria":{
}
}
Inordertofurtherunderstandtheinnerworkingsofsearch_criteria,weneedto
understandSearchCriteriaInterface,whichis(partially)definedasfollows:
interfaceSearchCriteriaInterface
{
/*@param\Magento\Framework\Api\Search\FilterGroup[]$filterGroups*/
publicfunctionsetFilterGroups(array$filterGroups=null);
/*@param\Magento\Framework\Api\SortOrder[]$sortOrders*/
publicfunctionsetSortOrders(array$sortOrders=null);
/*@paramint$pageSize*/
publicfunctionsetPageSize($pageSize);
/*@paramint$currentPage*/
publicfunctionsetCurrentPage($currentPage);
}
EveryinterfacegetterandsettermethodexpectsthevaluestobefoundinpassedAPI
parameters.WhatthismeansisthatthegetPageSize()andsetPageSize()methods
wouldexpectsearch_criteriatohaveanintegertypepage_sizepropertyonit.
Similarly,thegetFilterGroups()andsetFilterGroups()methodswouldexpect
search_criteriatohaveanarrayof\Magento\Framework\Api\Search\FilterGroup
passedtoit.Theseinsightsbringustoan(incomplete)JSONobjectofthefollowingtype
topasstothegetListmethod:
{
"search_criteria":{
"filter_groups":[
],
"current_page":1,
"page_size":10,
"sort_orders":[
]
}
}
Nowwehavegottothepointwhereweneedtodeterminewhatgoesintofilter_groups
andsort_orders,sincethesearenotsimpletypesbutcompoundvalues.
Lookingfurtherinto\Magento\Framework\Api\Search\FilterGroup,weseethe
definitionofthegetFilters()andsetFilters()methodsthatworkwithanarrayof
\Magento\Framework\Api\Filterobjects.Whatthismeansisthatfilter_groupshasa
propertyfilterthatisanarrayofindividualfilterobjectsdefinedas
\Magento\Framework\Api\Filter.Withthisinmind,wearenowdowntothefollowing
formofthesearch_criteriaJSONobject:
{
"search_criteria":{
"filter_groups":[
{
"filters":[
]
}
],
"current_page":1,
"page_size":10,
"sort_orders":[
]
}
}
Lookingfurtherintoindividual\Magento\Framework\Api\Filter,throughitsgettersand
settersitdefineswecanconcludepropertieslikefield,value,andcondition_type.This
bringsusonestepfurthertofinalizingoursearch_criteriaJSONobject,whichisnow
structuredasfollows:
{
"search_criteria":{
"filter_groups":[
{
"filters":[
{
"field":"title",
"value":"%some%",
"condition_type":"like"
}
]
}
],
"current_page":1,
"page_size":10,
"sort_orders":[
]
}
}
Letustakealookatsort_ordersasthelastoutstandingbit.sort_ordersisoftype
\Magento\Framework\Api\SortOrder,whichhasgettersandsettersforthefieldand
directionproperties.Knowingthis,weareabletofullyconstructoursearch_criteria
JSONobject(orarray)thatwewouldbepassingtothegetList()servicemethodcall,as
follows:
{
"search_criteria":{
"filter_groups":[
{
"filters":[
{
"field":"title",
"value":"%some%",
"condition_type":"like"
}
]
}
],
"current_page":1,
"page_size":10,
"sort_orders":[
{
"field":"slide_id",
"direction":-1
}
]
}
}
Whathappenswhenwedefinemultipleentriesunderfilter_groups,filters,or
sort_orders?ThelogicalexpectationwouldbethatthesebreakintoANDandORoperators
inSQLwhentheyhitthedatabase.Surprisingly,thisisnotalwaysthecase,atleastnot
withourprecedingexample.SincetheactualimplementationofthegetListmethodis
leftforustohandle,wecandecidehowwewanttohandlethefiltergroupsandfilters.
LookingbackatourgetListmethod,as(partially)shownnext,wearenotdoinganything
toimplyanORoperator,soeverythingendsupwithanANDconditiononthedatabase:
foreach($searchCriteria->getFilterGroups()as$filterGroup){
foreach($filterGroup->getFilters()as$filter){
$condition=$filter->getConditionType()?:'eq';
$collection->addFieldToFilter($filter->getField(),[$condition=>
$filter->getValue()]);
}
}
Theprecedingcodesimplyloopsthroughallfiltergroups,pullinginallfilterswithinthe
groupandcallingthesameaddFieldToFiltermethodforeverything.Similarbehavioris
implementedacrosscoreMagentomodules.Althoughthefilteringitselffollowsthe
\Magento\Framework\Api\SearchCriteriaInterfaceinterface,thereisnounified
Magento-wideapproachtoforceANDandORoperatorsinfiltering.
However,MagentocoreAPI’slikeGETproductsdoimplementbothANDandOR
conditions.Incaseslikethese,filtergroupsresultinORandfilterswithinthegroupresult
inANDconditions.
Tip
Followingbestpractices,weshouldmakesureourmodulesthatimplementsearchcriteria
dosorespectingthefilter_groups/filtersandOR/ANDrelationship.
Summary
Inthischapter,wecoveredalotofgroundrelatingtoMagentoAPI’s.Thereismuchmore
lefttobesaid,butthestepsoutlinedhereshouldbeenoughtogetusstartedevenwith
moreadvancedAPIusage.Westartedthechapterwithlearningabouttypesofusersand
theauthenticationmethodssupported.Strongemphasiswasplacedonmakingseveral
typesofAPIcalls,likeconsolecURL,PHPcURL,PHPSoapClient,andconsolecURL
SOAP.ThiswastoencouragedeveloperstounderstandtheinnerworkingsofAPIcalls
moredeeplythanjustusinghigh-levellibraries.
Throughoutthenextchapter,wewilllookintosomeofthemajorsectionsofMagento.
Chapter10.TheMajorFunctionalAreas
TheMagentoplatformcomprisesvariousmodulesthatdelivervariousbitsof
functionality.Developersareoftenmoreintouchwithonegroupoffunctionalitythan
others.Examplesofsomeofthemostcommonlyusedfunctionalitiesincludethoserelated
toCMSblocksandpages,categories,products,customers,imports,customproducttypes,
custompayment,andshippingmodules.Thisisnottosaythatotherfunctionalitiesare
lessimportant.Inthischapter,wewilltakeaquicklookatthefunctionalitiesinthe
Magentoadminarea,PHPcode,andAPIcalls.Thechapterisdividedintothefollowing
sections:
CMSmanagement
Catalogmanagement
Customermanagement
Productsandcustomerimport
Customproducttypes
Customofflineshippingmethods
Customofflinepaymentmethods
Theintentionisnottogointothedetailsofeachfunctionalarea.Rather,theaimisto
showtheadmininterfaceandthecorrespondingprogrammaticandAPIapproachtowards
basicmanagement.
CMSmanagement
Contentiswhathelpsdifferentiateonestorefromanother.Qualitycontentcanboosta
store’svisibilityonsearchengines,provideinformativeinsighttothecustomerswhobuy
products,andprovidecredibilityandtrust.Magentoprovidesasolidcontentmanagement
system,whichcanbeusedtocreaterichcontentforastore.Wecanuseittomanage
blocksandpagestoo.
Managingblocksmanually
ACMSblockisasmallmodularunitofcontentthatcanbepositionedalmostanywhere
onapage.Theycanevenbecalledintoanotherblocks.BlockssupportHTMLand
JavaScriptasitscontent.Therefore,theyareabletodisplaystaticinformationsuchastext,
images,andembeddedvideoaswellasdynamicinformation.
Blockscanbecreatedviaanadmininterface,APIs,orcode.
Thefollowingstepsoutlinetheblockcreationprocessfromwithinanadmininterface:
1. LogintotheMagentoadminarea.
2. IntheContent|Elements|Blocksmenu,clickonAddNewBlock.Thisopensa
screenthatissimilartotheoneshowninthefollowingscreenshot:
3. Fillinsomevaluesfortherequiredfields(BlockTitle,Identifier,StoreView,
Status,andContent)andclickontheSaveBlockbutton.
Oncetheblockissaved,youwillseetheYousavedtheblock.successmessageinthe
browser.CMSblocksarestoredinthecms_blockandcms_block_storetablesina
database.
TheIdentifiervalueisprobablythemostinterestingaspecthere.WecanuseitinaCMS
page,anotherCMSblock,orsomecodetofetchtheblockthatwehavejustcreated.
AssumingthatwehavecreatedablockwiththeIdentifiervalueoffoggyline_hello,we
cancallitintheCMSpageoranotherblockbyusingthefollowingexpression:
{{widgettype="Magento\\Cms\\Block\\Widget\\Block"
template="widget/static_block/default.phtml"block_id="foggyline_hello"}}
WecanalsopasstheactualintegerIDvalueofablocktotheprecedingexpression,as
follows:
{{widgettype="Magento\\Cms\\Block\\Widget\\Block"
template="widget/static_block/default.phtml"block_id="2"}}
However,thisapproachrequiresustoknowtheactualintegerIDofablock.
Theprecedingexpressionsshowthatblocksareincludedinapageoranotherblockviaa
widget,whichisalsoknownasafrontendapp.Awidgetofthe
Magento\Cms\Block\Widget\Blockclasstypeisusingthe
widget/static_block/default.phtmltemplatefiletorendertheactualCMSblock.
Managingblocksviacode
Besidesthemanualcreationofblocksviatheadmininterface,wecancreateCMSblocks
byusingcode,asshowninthefollowingcodesnippet:
$model=$this->_objectManager->create('Magento\Cms\Model\Block');
$model->setTitle('Testblock');
$model->setIdentifier('test_block');
$model->setContent('Testblock!');
$model->setIsActive(true);
$model->save();
Here,weusedtheinstancemanagertocreateanewmodelinstanceofthe
Magento\Cms\Model\Blockclass.Then,wesetsomepropertiesthroughdefinedmethods
andfinallycalledthesavemethod.
Wecanloadandupdatetheexistingblocksusingacodesnippetthatissimilartothe
followingcode:
$model=$this->_objectManager->create('Magento\Cms\Model\Block');
//$model->load(3);
$model->load('test_block');
$model->setTitle('UpdatedTestblock');
$model->setStores([0]);
$model->save();
Theblock’sloadmethodacceptseitheranintegervalueofablockIDorastringvalueof
ablockidentifier.
Finally,wecanmanagethecreationandupdatingofblocksthroughtheavailableAPIs
method.ThefollowingcodesnippetshowshowaCMSblockiscreatedviaaconsole
cURLRESTAPIcall:
curl-XPOST"http://magento2.ce/index.php/rest/V1/cmsBlock"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"\
-d'{"block":{"identifier":"test_api_block","title":"TestAPI
Block","content":"APIBlockContent"}}'
Thebearerstringisjustalogintokenthatweobtainbyfirstrunningtheauthentication
APIcall,asdescribedinthepreviouschapter.Oncewehavetheauthenticationtoken,we
canmakeaV1/cmsBlockPOSTrequest,passingaJSONobjectasdata.
ManagingblocksviaAPI
WecangetthenewlycreatedCMSblockthroughanAPIbyexecutingasnippetofcode
thatlookslikethis:
curl-XGET"http://magento2.ce/index.php/rest/V1/cmsBlock/4"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"
WecanupdatetheexistingCMSblockbyusinganAPIandexecutingasnippetofcode
thatissimilartothis:
curl-XPUT"http://magento2.ce/index.php/rest/V1/cmsBlock/4"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"\
-d'{"block":{"title":"UpdatedTestAPIBlock"}}'
Here,weusedtheHTTPPUTmethodandpassedtheinteger4asapartofthe
V1/cmsBlock/4URL.Thenumber4representstheIDvalueoftheblockinthedatabase.
Managingpagesmanually
CMSpagesarerobustcontentunitsunlikeCMSblocks,whicharesimplyembeddedinto
certainpages.TheCMSpagecanhaveitsownURL.ExamplesofCMSpagesarepages
suchas404NotFound,Homepage,EnableCookies,andPrivacyandCookiePolicy.
Theidea,whenitcomestodealingwithCMSpages,isthatwecancontrolthecontent
areaofapagewithoutaffectingsite-wideelementssuchastheheader,footer,orsidebars.
Magentodoesnotreallycomewithmanyout-of-the-boxCMSpagesotherthantheones
thatwerelistedpreviously.
Likeblocks,pagescanalsobecreatedviatheadmininterface,APIs,orcode.
Thefollowingstepsoutlinethepagecreationprocessfromwithintheadmininterface:
1. LogintoMagentoadminarea.
2. IntheContent|Elements|Pagesmenu,clickonAddNewPage.Thisopensa
screenthatissimilartheoneshowninthefollowingscreenshot:
3. Fillinsomevaluesfortherequiredfields(PageTitle,StoreView,Status,and
Content)andclickontheSaveBlockbutton.
Oncethepageissaved,youwillseetheYousavedthispage.successmessageinthe
browser.CMSpagesarestoredinthecms_pageandcms_page_storetablesinthe
database.
AssumingthatwehavecreatedapagewithPageTitlevalueInfo,wecanaccessthispage
inabrowserviaaURLsuchashttp://magento2.ce/info.Thoughwecouldhaveto
specifytheURLKeyvalueintheNewPageeditscreen,Magentoautomaticallyassigns
URLKeythatmatchesPageTitle.
Managingpagesviacode
Besidesthemanualcreationthroughtheadmininterface,wecancreateCMSpagesvia
code,asshowninthefollowingcodesnippet:
$model=$this->_objectManager->create('Magento\Cms\Model\Page');
$model->setTitle('Testpage');
$model->setIdentifier('test-page');
$model->setPageLayout('1column');
$model->setContent('Testpage!');
$model->setIsActive(true);
$model->setStores([0]);
$model->save();
Here,weusedtheinstancemanagertocreateanewmodelinstanceofthe
Magento\Cms\Model\Pageclass.Then,wesetsomepropertiesthroughthedefined
methodsandfinallycalledthesavemethod.TheURLKeythatwesetthroughtheadmin
interfaceisactuallyanidentifierthatwesetviathesetIdentifiermethodcall.
ManagingpagesviaAPI
Wecanloadandupdatetheexistingpagesbyusingacodesnippetthatissimilartothe
followingone:
$model=$this->_objectManager->create('Magento\Cms\Model\Page');
//$model->load(6);
$model->load('test-page');
$model->setContent('UpdatedTestpage!');
$model->save();
ThepagemodelloadmethodacceptseitheranintegerIDvalueofapageidentifier(URL
Key).
Finally,wecanmanagethecreationandupdatingofpagesthroughtheavailableAPIs
method.ThefollowingcodesnippetshowshowaCMSpageiscreatedviaaconsole
cURLRESTAPIcall:
curl-XPOST"http://magento2.ce/index.php/rest/V1/cmsPage"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"\
-d'{"page":{"identifier":"test-api-page","title":"TestAPIPage",
"content":"APIBlockContent"}}'
Oncewehavetheauthenticationtoken,wecanmakeaV1/cmsPagePOSTrequest,passing
ontheJSONobjectasdata.
WecangetthenewlycreatedCMSpagethroughanAPIbyexecutingasnippetofcode
thatissimilartothefollowingone:
curl-XGET"http://magento2.ce/index.php/rest/V1/cmsPage/7"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"
WecanupdatetheexistingCMSpagethroughanAPIbyexecutingasnippetofcodethat
issimilartothefollowingone:
curl-XPUT"http://magento2.ce/index.php/rest/V1/cmsPage/7"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"\
-d'{"page":{"content":"UpdatedTestAPIPage",
"identifier":"updated-page"}}'
Here,weusedtheHTTPPUTmethod,passingtheinteger7asapartoftheV1/cmsPage/7
URL.Thenumber7representstheIDvalueofthepageinthedatabase.
Catalogmanagement
TheMagento_CatalogmoduleisoneofthebackbonesoftheentireMagentoplatform.It
providesrobustsupportfortheinventorymanagementofvariousproducttypes.This
moduleiswhatmanagesproducts,categoriesandtheirattributes,thedisplayonthe
frontend,andmanymorethings.
Managingcategoriesmanually
WecanaccessthecatalogfunctionalitywithintheMagentoadminareabynavigatingto
Products|Inventory|CatalogorProducts|Inventory|Category.
IfwestartwithablankMagentoinstallation,wewillprobablystartwithcategoriesasone
ofthefirstentitiestobecreated.Wecanmanuallycreatecategoriesbyperformingthe
followingsteps:
1. LogintotheMagentoadminarea.
2. GototheProducts|Inventory|Categorymenu.Thisopensascreenthatissimilar
totheoneshowninthefollowingscreenshot:
3. Ontheleft-handsideofthescreen,clickonDefaultCategory.Then,whenthepage
reloads,clickontheAddSubcategorybutton.
4. Thoughitmayseemthatnothinghashappened,asthescreencontentdoesnot
change,weshouldnowfillintherequiredoptionsintheGeneralInformationtab,
settingNametosomestringvalueandIsActivetoYes.
5. Finally,clickontheSaveCategorybutton.
Thenewcategoryshouldnowbecreated.Totheleftscreenarea,ifyouclickonthename
ofthenewlycreatedcategory,youwillseeitsIDvalueabovetheGeneralInformation
tab,asshowninthefollowingscreenshot:
Note
KnowingthecategoryIDenablesyoutodirectlytestitonastorefrontsimplybyopening
aURLsuchashttp://magento2.ce/index.php/catalog/category/view/id/3inthe
browser,wherethenumber3istheIDofthecategory.Youwillseealoadedcategory
pagethatprobablyshowstheWecan’tfindproductsmatchingtheselection.message,
whichisgood,aswehaven’tassignedproductstoacategory.
Thoughwewillnotgointoitsdetails,itisworthnotingthatwehavesimplyscratchedthe
surfacehere,ascategoriesenableustoprovidemanyadditionaloptionsusingtheDisplay
Settings,CustomDesigntabs.
GiventhatcategoriesareEAVentities,theirdataisstoredacrossseveraltablesinthe
database,asfollows:
catalog_category_entity
catalog_category_entity_datetime
catalog_category_entity_decimal
catalog_category_entity_int
catalog_category_entity_text
catalog_category_entity_varchar
Thereareafewadditionaltablesthatlinkcategoriestoproducts:
catalog_category_product
catalog_category_product_index
catalog_category_product_index_tmp
catalog_url_rewrite_product_category
Managingcategoriesviacode
Besidesthemanualcreationthroughtheadmininterface,wecancreatecategoriesvia
code,asshowninthefollowingcodesnippet:
$parentId=\Magento\Catalog\Model\Category::TREE_ROOT_ID;
$parentCategory=$this->_objectManager
->create('Magento\Catalog\Model\Category')
->load($parentId);
$category=$this->_objectManager
->create('Magento\Catalog\Model\Category');
$category->setPath($parentCategory->getPath());
$category->setParentId($parentId);
$category->setName('Test');
$category->setIsActive(true);
$category->save();
Whatisspecifichereisthatwhencreatinganewcategory,wefirstcreateda
$parentCategoryinstance,whichrepresentstherootcategoryobject.Weusedthe
CategorymodelTREE_ROOT_IDconstantastheIDvalueofaparentcategoryID.Then,we
createdaninstanceofthecategory,setitspath,parent_id,name,andis_activevalue.
ManagingcategoriesviaAPI
WecanfurthermanagecategorycreationthroughtheavailableAPIsmethod.The
followingcodesnippetshowscategorycreationviatheconsolecURLRESTAPIcall:
curl-XPOST"http://magento2.ce/index.php/rest/V1/categories"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"\
-d'{"category":{"parent_id":"1","name":"TestAPICategory",
"is_active":true}}'
Thebearerstringisjustalogintokenthatweobtainbyfirstrunningtheauthentication
APIcall,asdescribedinthepreviouschapter.Oncewehavetheauthenticationtoken,we
canmakea/V1/categoriesPOSTrequest,passingaJSONobjectasdata.
WecangetthenewlycreatedcategoryasaJSONobjectthroughanAPIbyexecutinga
snippetofcodethatlookslikethefollowingone:
curl-XGET"http://magento2.ce/index.php/rest/V1/categories/9"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"
Managingproductsmanually
Now,let’stakealookathowtocreateanewproduct.Wecanmanuallycreateproductsby
performingthefollowingsteps:
1. LogintotheMagentoadminarea.
2. IntheProducts|Inventory|Catalogmenu,clickontheAddProductbutton.This
opensascreensimilartotheoneshowninthefollowingscreenshot:
3. Now,fillintherequiredoptionsontheProductDetailstab.
4. Finally,clickontheSavebutton.
Ifitissuccessfullysaved,thepagereloadsandshowstheYousavedtheproduct.
message.
Likecategories,wehavebarelyscratchedthesurfaceofproductshere.Lookingatthe
otheravailabletabs,therearealargenumberofadditionaloptionsthatcanbeassignedto
aproduct.Simplyassigningtherequiredoptionsshouldbeenoughforustoseethe
productonthestore’sfrontendonaURLsuchas
http://magento2.ce/index.php/catalog/product/view/id/4,wherethenumber4is
theIDvalueofaproduct.
ProductsarealsoEAVentities,whosedataisstoredacrossseveraltablesinadatabase,as
follows:
catalog_product_entity
catalog_product_entity_datetime
catalog_product_entity_decimal
catalog_product_entity_gallery
catalog_product_entity_group_price
catalog_product_entity_int
catalog_product_entity_media_gallery
catalog_product_entity_media_gallery_value
catalog_product_entity_text
catalog_product_entity_tier_price
catalog_product_entity_varchar
Therearealsoalargenumberofothertablereferencingproducts,suchas
catalog_product_bundle_selection,butthesearemostlyusedtolinkbitsof
functionalities.
Managingproductsviacode
Besidesthemanualcreationthroughtheadmininterface,wecancreateproductsviacode,
asshowninthefollowingcodesnippet:
$catalogConfig=$this->_objectManager
->create('Magento\Catalog\Model\Config');
$attributeSetId=$catalogConfig->getAttributeSetId(4,'Default');
$product=$this->_objectManager
->create('Magento\Catalog\Model\Product');
$product
->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)
->setAttributeSetId($attributeSetId)
->setWebsiteIds([$this->storeManager->getWebsite()->getId()])
->setStatus(\Magento\Catalog\Model\Product\Attribute
\Source\Status::STATUS_ENABLED)
->setStockData(['is_in_stock'=>1,'manage_stock'=>0])
->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID)
->setVisibility(\Magento\Catalog\Model\Product
\Visibility::VISIBILITY_BOTH);
$product
->setName('TestAPI')
->setSku('tets-api')
->setPrice(19.99);
$product->save();
ManagingproductsviaAPI
ThefollowingexampleusestheRESTAPItocreateanewsimpleproduct:
curl-XPOST"http://magento2.ce/index.php/rest/V1/products"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"\
-d'{"product":{"sku":"test_api_1","name":"TestAPI
#1","attribute_set_id":4,"price":19.99,"status":1,
"visibility":4,"type_id":"simple","weight":1}}'
TheBearertokenshouldhavebeenpreviouslyobtainedbyusinganauthentication
request.TheresponseshouldbeaJSONobjectthatcontainsalltheexposedproductdata.
WecangettheexistingproductasinformationthroughanAPIthatexecutesasnippetof
code,asfollows:
curl-XGET"http://magento2.ce/index.php/rest/V1/products
/product_dynamic_125"\
-H"Content-Type:application/json"
Theproduct_dynamic_125partintheprecedingURLstandsforthisspecificproduct
SKUvalue.TheresponseisaJSONobjectthatcontainsalltheexposedproductdata.
TheentirelistoftheavailablecatalogAPIscanbeseeninthevendor/magento/module-
catalog/etc/webapi.xmlfile.
Customermanagement
ManagingcustomersisanotherimportantaspectoftheMagentoplatform.Mostofthe
time,customercreationissomethingthatisleftforanewcustomertodo.Anewcustomer
whovisitsastoreinitiatestheregistrationprocessandfinishesupwithacustomeraccount
beingcreated.Onceregistered,customerscanthenfurtheredittheiraccountdetailsonthe
storefrontundertheMyAccountpage,whichisusuallyavailableonalinksuchas
http://magento2.ce/index.php/customer/account/index/.
Asapartofthissection,weareinterestedinthepossibilityofmanagingcustomer
accountsbyusingtheadminarea,code,andAPI.
Managingcustomersmanually
Thefollowingstepsoutlinethecustomeraccountcreationprocessfromwithintheadmin
interface:
1. LogintoMagentoadminarea.
2. IntheCustomers|AllCustomersmenu,clickontheAddNewCustomerbutton.
Thisopensascreenthatlookssimilartotheoneshowninthefollowingscreenshot:
3. Fillinsomevaluesfortherequiredfields(AssociatetoWebsite,Group,First
Name,LastName,andEmail)andclickontheSaveCustomerbutton.
Oncethecustomerissaved,youwillseetheYousavedthecustomer.successmessagein
thebrowser.
TheAssociatetoWebsitevalueisprobablythemostimportantvalueforcaseslikethis
one,wherecustomeraccountsarebeingindirectlycreatedbyauserwho’snotacustomer.
Note
SinceMagentosupportsthesettingupofmultiplewebsites,customeraccountscanbeset
toeithertheGlobalorPerWebsitevalue,dependingontheStores|Settings|
Configuration|Customers|CustomerConfiguration|AccountSharingOption|
ShareCustomerAccountsoption.Thus,iftheShareCustomerAccountsoptionhas
beensettoPerWebsite,itisoftheutmostimportanttopointtheAssociatetoWebsite
valuetotheproperwebsite.Otherwise,acustomeraccountwillbecreatedbutthe
customerwon’tbeabletologintoitonthestorefront.
TheMagento_CustomermoduleusestheEAVstructuretostorecustomerdata.Thus,there
isnosingletablethatstorescustomerinformation.Rather,multipletablesexist,depending
onthecustomerpropertyanditsdatatype.
Thefollowinglistcomprisestablesthatstorecustomerentity:
customer_entity
customer_entity_datetime
customer_entity_decimal
customer_entity_int
customer_entity_text
customer_entity_varchar
Customeraccountswillnotreallybecompletewithoutacustomeraddress.Theaddress
canbeaddedviatheAddressestabunderthecustomereditscreenintheadminarea,as
showninthefollowingscreenshot:
NotethatMagentoenablesustosetoneoftheaddressesasDefaultShippingAddress
andDefaultBillingAddress.
Likethecustomerentity,thecustomeraddressentityalsousestheEAVstructuretostore
itsdata.
Thefollowinglistcomprisestablesthatstorethecustomeraddressentity:
customer_address_entity
customer_address_entity_datetime
customer_address_entity_decimal
customer_address_entity_int
customer_address_entity_text
customer_address_entity_varchar
Managingcustomersviacode
Besidesthemanualcreationviatheadmininterface,wecancreatecustomersviacode,as
showninthefollowingcodesnippet:
$model=$this->_objectManager->create('Magento\Customer\Model\Customer');
$model->setWebsiteId(1);
$model->setGroupId(1);
$model->setFirstname('John');
$model->setLastname('Doe');
$model->setEmail('john.doe@mail.com');
$model->save();
Here,weareusingtheinstancemanagertocreateanewmodelinstanceofthe
Magento\Customer\Model\Customerclass.Wecanthensetsomepropertiesthroughthe
definedmethodsandfinallycallthesavemethod.
Wecanloadandupdateanexistingcustomerbyusingacodesnippetthatissimilartothe
followingone:
$model=$this->_objectManager->create('Magento\Customer\Model\Customer');
$model->setWebsiteId(1);
//$model->loadByEmail('john.doe@mail.com');
$model->load(1);
$model->setFirstname('UpdatedJohn');
$model->save();
WecanuseeithertheloadorloadByEmailmethodcall.Theloadmethodacceptsthe
integerIDvalueoftheexistingcustomerentity,whileloadByEmailacceptsastringe-mail
address.ItisworthnotingthatsetWebsiteIdhastobecalledpriortoanyoftheload
methods.Otherwise,wewillgetanerrormessagethatsaysAcustomerwebsiteIDmust
bespecifiedwhenusingthewebsitescope.
ManagingcustomersviaanAPI
Finally,wecanmanagethecreationandupdatingofcustomerinformationusingthe
availableAPImethod.Thefollowingcodesnippetshowshowtocreateacustomerviaa
consolecURLRESTAPIcall:
curl-XPOST"http://magento2.ce/index.php/rest/V1/customers"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerr9ok12c3wsusrxqomyxiwo0v7etujw9h"\
-d'{"customer":{"website_id":1,"group_id":1,"firstname":"John",
"lastname":"Doe","email":"john.doe@mail.com"},"password":"abc123"}'
Oncewehavetheauthenticationtoken,wecanmakeaV1/customersPOSTrequest,
passingaJSONobjectasdata.
WecangetthenewlycreatedcustomerviaanAPIbyexecutingasnippetofcodethatis
similartothefollowingone:
curl-XGET"http://magento2.ce/index.php/rest/V1/customers/24"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"
WecanupdateanexistingcustomerthroughanAPIbyexecutingasnippetofcodethatis
similartothefollowingone:
curl-XPUT"http://magento2.ce/index.php/rest/V1/customers/24"\
-H"Content-Type:application/json"\
-H"Authorization:Bearerr9ok12c3wsusrxqomyxiwo0v7etujw9h"\
-d'{"customer":{"id":24,"website_id":1,"firstname":"John
Updated","lastname":"Doe","email":"john2@mail.com"},
"password_hash":"cda57c7995e5f03fe07ad52d99686ba130e0d3e
fe0d84dd5ee9fe7f6ea632650:cEf8i1f1ZXT1L2NwawTRNEqDWGyru6h3:1"}'
Here,weusedtheHTTPPUTmethod,passingtheinteger24asapartofthe
V1/customers/24andaspartofthebodyURL.Thenumber24representstheID
valueofacustomerinthedatabase.Also,notethepassword_hashvalue;withoutit,the
updatewillfail.
Managingcustomeraddressviacode
Similartocustomers,wecancreateacustomeraddressusingcode,asshowninthe
followingcodesnippet:
$model=$this->_objectManager->create('Magento\Customer\Model\Address');
//$model->setCustomer($customer);
$model->setCustomerId(24);
$model->setFirstname('John');
$model->setLastname('Doe');
$model->setCompany('Foggyline');
$model->setStreet('Teststreet');
$model->setCity('London');
$model->setCountryId('GB');
$model->setPostcode('GU227PY');
$model->setTelephone('112233445566');
$model->setIsDefaultBilling(true);
$model->setIsDefaultShipping(true);
$model->save();
Here,weusedtheinstancemanagertocreateanewmodelinstanceofthe
Magento\Customer\Model\Addressclass.Wethensetsomepropertiesthroughthe
definedmethodsandfinallycalledthesavemethod.
Wecanloadandupdatetheexistingcustomeraddressbyusingacodesnippetthatis
similartothefollowingone:
$model=$this->_objectManager->create('Magento\Customer\Model\Address');
$model->load(22);
$model->setCity('UpdateLondon');
$model->save();
Here,weusedtheloadmethodtoloadanexistingaddressbyitsIDvalue.Then,we
calledthesetCitymethodpassingittheupdatedstring.Afterthesavemethodis
executed,theaddressshouldreflectthechange.
ManagingcustomersaddressviaanAPI
Surprisingly,acustomeraddresscannotbecreatedorupdateddirectlyviaanAPIcall,as
thereisnoPOSTorPUTRESTAPIdefined.However,wecanstillgettheexistingcustomer
addressinformationbyusinganAPI,asfollows:
curl-XGET"http://magento2.ce/index.php/rest/V1/customers/addresses/22"
\
-H"Content-Type:application/json"\
-H"Authorization:Bearerlcpnsrk4t6al83lymhfs86jabbi9mmt8"
TheentirelistofavailablecustomerAPIscanbeseeninthevendor/magento/module-
customer/etc/webapi.xmlfile.
Productsandcustomersimport
Magentoprovidesanout-of-the-boxmassimportandexportfunctionalityviathe
followingmodules:
AdvancedPricingImportExport
BundleImportExport
CatalogImportExport
ConfigurableImportExport
CustomerImportExport
GroupedImportExport
ImportExport
TaxImportExport
TheheartoftheimportfunctionalityactuallyliesintheImportExportmodule,while
othermodulesprovideindividualimportandexportentitiesthroughthe
vendor/magento/module-{partialModuleName}-import-export/etc/import.xmland
vendor/magento/module-{partialModuleName}-import-export/etc/export.xmlfiles.
ThesefunctionalitiescanbeaccessedfromtheMagentoadminareafromtheSystem|
DataTransfermenu.Theyenableustoexportandimportseveralentitytypes,suchas
AdvancedPricing,Products,CustomersMainFile,andCustomerAddresses.
ThefollowingscreenshotshowstheEntityTypeoptionsfortheImportSettingsscreen:
NexttoImportSettings,whenweselectEntityTypeforimport,theImportBehavior
sectionappears,asshowninthefollowingscreenshot:
MostentitytypeshavesimilaroptionsforImportBehavior.Mostofthetime,wewillbe
interestedintheAdd/Updatebehavior.
Sinceimportingisabitmorecomplicatedprocessthanexporting,wewillfocuson
importingandtheCSVfileformat.Morespecifically,ourfocusisonProducts,
CustomersMainFile,andCustomerAddressesimports.
WhenworkingwithacleanMagentoinstallation,thefollowingcolumnsarerequired
duringtheproductimportinordertomaketheproductvisibleonthestorefront
afterwards:
sku(forexample,“test-sku”):Thiscanhavealmostanyvalueaslongasitisunique
acrossMagento.
attribute_set_code(forexample,“Default”):Thiscanhaveanyofthevalues
foundinadatabasewhentheSELECTDISTINCTattribute_set_nameFROM
eav_attribute_set;queryisexecuted.
product_type(forexample,“simple”):Thiscanhavethevaluesofsimple,
configurable,grouped,virtual,bundle,ordownloadable.Additionally,ifwe
createorinstallathird-partymodulethataddsanewproducttype,wecanusethat
oneaswell.
categories(forexample,“Root/Shoes”):Createafullcategorypathusingthe
“Rootcategoryname/Childcategoryname/Childchildcategoryname”syntax.If
therearemultiplecategories,thenapipe(“|”)isusedtoseparatethem.Anexample
ofthisis“Rootcategoryname/Childcategoryname/Childchildcategoryname|Root
categoryname/Child_2categoryname”.
product_websites(forexample,“base”):Thiscanhavethevaluesfoundina
databasewhentheSELECTDISTINCTcodeFROMstore_website;queryisexecuted.
name(forexample,“Test”):Thiscanhavealmostanyvalue.
product_online(forexample,“1”):Thiscanbeeither1forvisibleor0fornot
visible
visibility(forexample,“Catalog,Search”):Thiscanhavethevaluesof“Not
VisibleIndividually”,“Catalog”,“Search”,or“Catalog,Search”.
price(forexample,“9.99”):Thiscanbeanintegeroradecimalvalue.
qty(forexample,“100”):Thiscanbeanintegeroradecimalvalue.
Thoughtheproductswillgetimportedjustwiththeprecedinglistthatcomprisesasetof
columns,weusuallywouldliketoassignadditionalinformationtothem,suchas
descriptionsandimages.Wecandosowiththehelpofthefollowingcolumns:
description(forexample,“Thedescription”):Thiscanhaveanystringvalue.
HTMLandJavaScriptaresupported.
short_description(forexample,“Theshortdescription”):Thiscanhaveanystring
value.HTMLandJavaScriptaresupported.
base_image(forexample,butterfly.jpg):Thisisthefinalimportimagename.
small_image(forexample,galaxy.jpg)
thumbnail_image(forexample,serenity.jpg)
Regardingtheimportingofimages,weonlyneedtoprovidethefinalimagenameaslong
astheImagesFileDirectorypathissetduringtheimport.Wecanusearelativepathfor
theMagentoinstallation,suchasvar/export,var/import,var/export/some/dir.
Oncetheimportisfinished,itissuggestedtorunthephpbin/magentoindexer:reindex
commandviatheconsole.Otherwise,theproductswon’tbevisibleonthestorefrontuntil
theindexerisrun.
Oncethereindexingisdone,wecantryopeningthestorefrontURL,whichlookslike
http://magento2.ce/index.php/catalog/product/view/id/1.Thenumber1inthis
caseisanewlyimportedproductID.
WhenworkingwithacleanMagentoinstallation,thefollowingcolumnsarerequired
duringacustomer’smainfileimportinorderforourcustomertobeabletosuccessfully
logintothestorefrontafterwards:
email(forexample,<john.doe@fake.mail>):ane-mailaddressasastringvalue
_website(forexample,base):Thiscanhaveanyofthevaluesfoundinthedatabase
whentheSELECTDISTINCTcodeFROMstore_website;queryisexecuted
firstname(forexample,John):astringvalue
lastname(forexample,Doe):astringvalue
group_id(forexample,1):Thiscanhaveanyofthevaluesfoundinthedatabase
whentheSELECTcustomer_group_idcodeFROMcustomer_groupWHERE
customer_group_id!=0;queryisexecuted
Thoughacustomerwillbeabletologintothestorefrontwithjustthepreviouslylistedset
ofcolumns,weusuallywouldliketoassignotherrelevantpiecesofinformation.Wecan
dosowiththehelpofthefollowingcolumns:
gender(forexample,Male):ThiscanbeeitherMaleorFemale
taxvat(forexample,HR33311122299):anyvalidVATnumber,thoughanimport
willaccepteventheinvalidones
dob(forexample,1983-01-16):dateofbirth
prefix(forexample,Mr):anystringvalue
middlename(forexample,thedevguy):anystringvalue
suffix(forexample,engineer):anystringvalue
password(forexample,123abc):anystringvaluethathasaminimumlengthof6
characters,asdefinedvia
\Magento\CustomerImportExport\Model\Import\Customer::MIN_PASSWORD_LENGTH
Weneedtopayspecialattentiontothepasswordcolumn.Thisisacleartextpassword.
Therefore,weneedtobecarefulnottodistributeaCSVfileinanonsecuremanner.
Ideally,wecanprovidethepassword_hashcolumninsteadofpassword.However,entries
underthepassword_hashcolumnwillneedtobehashedviathesamealgorithmastheone
thatwascalledwithinthehashPasswordmethodofthe
Magento\Customer\Model\Customerclass.ThisfurthercallsthegetHashmethodonan
instanceoftheMagento\Framework\Encryption\Encryptorclass,whichfinallyresolves
tothemd5orsha256algorithm.
WhenworkingwithacleanMagentoinstallation,thefollowingcolumnsarerequired
duringthecustomeraddressimportinorderforourcustomerstobeabletosuccessfully
usetheaddressesonthestorefrontafterwards:
_website(forexample,base):Thiscanhaveanyofthevaluesfoundinthedatabase
whentheSELECTDISTINCTcodeFROMstore_website;queryisexecuted
_email(forexample,<john@change.me>):ane-mailaddressasastringvalue
_entity_id
firstname(forexample,John):anystringvalue
lastname(forexample,Doe):anystringvalue
street(forexample,AshtonLane):anystringvalue
city(forexample,Austin):anystringvalue
telephone(forexample,0038591111000):anystringvalue
country_id(forexample,GB):thecountrycodeintheISO-2format
postcode(forexample,TX78753):anystringvalue
Thoughacustomerwillbeabletousetheaddressesonthestorefrontwithjustalistedset
ofcolumns,weusuallywouldliketoassignotherrelevantpiecesofinformation.Wecan
dosowiththehelpofthefollowingcolumns:
region(forexample,California):Thiscanbeblank,afreeformstring,oraspecific
stringthatmatchesanyofthevaluesfoundinthedatabasewhentheSELECT
DISTINCTdefault_nameFROMdirectory_country_region;queryisexecuted.On
runningSELECTDISTINCTcountry_idFROMdirectory_country_region;,13
differentcountrycodesthathaveentrieswithinthedirectory_country_regiontable
areshown—AT,BR,CA,CH,DE,EE,ES,FI,FR,LT,LV,RO,US.Thismeansthatcountries
withthatcodeneedtohaveaproperregionnameassigned.
company(forexample,Foggyline):Thiscanbeanystringvalue.
fax(forexample,0038591111000):Thiscanbeanystringvalue.
middlename(forexample,thedeveloper):Thiscanbeanystringvalue.
prefix(forexample,Mr):Thiscanbeanystringvalue.
suffix(forexample,engineer):Thiscanbeanystringvalue.
vat_id(forexample,HR33311122299):ThiscanbeanyvalidVATnumber,though
importwillaccepteventhenon-validones.
_address_default_billing_(forexample,“1”):Thiscanbeeither“1”asyesor“0”
asno,toflagtheaddressasbeingthedefaultbillingaddress.
_address_default_shipping_(forexample,“1”):Thiscanbeeither“1”asyesor
“0”asno,toflagtheaddressasbeingdefaultshippingaddress.
WhileCSVimportsareagreatandrelativelyfastwaytomassimportproducts,customers,
andtheiraddresses,therearesomelimitationstoit.CSVissimplyflatdata.Wecannot
applyanylogictoit.Dependingonhowcleanandvalidthedatais,theCSVimportmight
dojustfine.Otherwise,wemightwanttooptforAPIs.Weneedtokeepinmindthata
CSVimportismuchfasterthantheAPIcreationofproductsandcustomersbecauseCSV
importsworkdirectlybybulkinsertingonthedatabase,whileAPIsinstantiatefull
models,respecttheeventobservers,andsoon.
Thecustomproducttypes
Magentoprovidesthefollowingsixout-of-the-boxproducttypes:
Simpleproducts
Configurableproducts
Groupedproducts
Virtualproducts
Bundleproducts
Downloadableproducts
Eachproducthasitsspecifics.Forexample,thevirtualanddownloadableproductsdonot
havetheweightattribute.Therefore,theyareexcludedfromthestandardshipping
calculations.Withcustomcodingaroundbuilt-inproducttypes,byusingobserversand
pluginswecanachievealmostanyfunctionality.However,thisisnotenoughsometimes
orthereisnosolutiontotherequirement.Incasessuchasthese,wemightneedtocreate
ourownproducttypethatwillmatchtheprojectrequirementsinamorestreamlinedway.
Let’screateaminiaturemodulecalledFoggyline_DailyDealthatwilladdanewproduct
typetoMagento.
Startbycreatingamoduleregistrationfilenamed
app/code/Foggyline/DailyDeal/registration.phpthathasthefollowingpartial
content:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Foggyline_DailyDeal',
__DIR__
);
Then,createanapp/code/Foggyline/DailyDeal/etc/module.xmlwiththefollowing
content:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module
/etc/module.xsd">
<modulename="Foggyline_DailyDeal"setup_version="1.0.0">
<sequence>
<modulename="Magento_Catalog"/>
</sequence>
</module>
</config>
Now,createanapp/code/Foggyline/DailyDeal/etc/product_types.xmlfilethathas
thefollowingcontent:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:
Magento_Catalog:etc/product_types.xsd">
<typename="foggylinedailydeal"
label="DailyDeal"
modelInstance="Foggyline\DailyDeal\Model\Product\Type\DailyDeal"
composite="false"
isQty="true"
canUseQtyDecimals="false">
<priceModelinstance="Foggyline\DailyDeal\Model\Product\Price"/>
<indexerModelinstance="Foggyline\DailyDeal\Model
\ResourceModel\Indexer\Price"/>
<stockIndexerModelinstance="Foggyline\DailyDeal\Model
\ResourceModel\Indexer\Stock"/>
<!--customAttributesparsedby
Magento\Catalog\Model\ProductTypes\Config-->
<customAttributes>
<attributename="is_real_product"value="true"/>
<attributename="refundable"value="false"/>
<attributename="taxable"value="true"/>
</customAttributes>
</type>
</config>
ThecustomAttributeselementisparsedbyvendor/magento/module-
catalog/Model/ProductTypes/Config.php.
Createanapp/code/Foggyline/DailyDeal/Model/Product/Type/DailyDeal.phpfile
withpartialcontent,asfollows:
namespaceFoggyline\DailyDeal\Model\Product\Type;
classDailyDealextends\Magento\Catalog\Model\Product\Type\AbstractType
{
constTYPE_DAILY_DEAL='foggylinedailydeal';
publicfunctiondeleteTypeSpecificData(\Magento\Catalog\Model\Product
$product)
{
//TODO:ImplementdeleteTypeSpecificData()method.
}
}
Now,createanapp/code/Foggyline/DailyDeal/Model/Product/Price.phpfilewith
partialcontent,asfollows:
namespaceFoggyline\DailyDeal\Model\Product;
classPriceextends\Magento\Catalog\Model\Product\Type\Price
{
}
Afterthisisdone,createan
app/code/Foggyline/DailyDeal/Model/ResourceModel/Indexer/Price.phpfilewith
partialcontent,asfollows:
namespaceFoggyline\DailyDeal\Model\ResourceModel\Indexer;
classPriceextends\Magento\Catalog\Model\ResourceModel\Product
\Indexer\Price\DefaultPrice
{
}
Then,createan
app/code/Foggyline/DailyDeal/Model/ResourceModel/Indexer/Stock.phpfilewith
partialcontent,asfollows:
namespaceFoggyline\DailyDeal\Model\ResourceModel\Indexer;
classStockextends\Magento\CatalogInventory\Model\ResourceModel
\Indexer\Stock\DefaultStock
{
}
Finally,createanapp/code/Foggyline/DailyDeal/Setup/InstallData.phpfilewith
partialcontent,asfollows:
namespaceFoggyline\DailyDeal\Setup;
classInstallDataimplements\Magento\Framework\Setup\InstallDataInterface
{
private$eavSetupFactory;
publicfunction__construct(\Magento\Eav\Setup\EavSetupFactory
$eavSetupFactory)
{
$this->eavSetupFactory=$eavSetupFactory;
}
publicfunctioninstall(
\Magento\Framework\Setup\ModuleDataSetupInterface$setup,
\Magento\Framework\Setup\ModuleContextInterface$context
)
{
//the"foggylinedailydeal"typespecifics
}
}
ExtendtheinstallmethodfromwithintheInstallDataclassbyaddingthefollowing
foggylinedailydealtypespecificstoit:
$eavSetup=$this->eavSetupFactory->create(['setup'=>$setup]);
$type=\Foggyline\DailyDeal\Model\Product\Type\
DailyDeal::TYPE_DAILY_DEAL;
$fieldList=[
'price',
'special_price',
'special_from_date',
'special_to_date',
'minimal_price',
'cost',
'tier_price',
'weight',
];
//maketheseattributesapplicabletofoggylinedailydealproducts
foreach($fieldListas$field){
$applyTo=explode(
',',
$eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY,
$field,'apply_to')
);
if(!in_array($type,$applyTo)){
$applyTo[]=$type;
$eavSetup->updateAttribute(
\Magento\Catalog\Model\Product::ENTITY,
$field,
'apply_to',
implode(',',$applyTo)
);
}
}
Now,runphpbin/magentosetup:upgradefromtheconsole.
IfyounowopentheProducts|Inventory|Catalogmenuintheadminareaandclickon
thedropdowniconnexttotheAddProductbutton,youwillseetheDailyDealproduct
typeonthelist,asshowninthefollowingscreenshot:
ClickingontheDailyDealproducttypeinthedropdownlistshouldopentheproductedit
page,asshowninthefollowingscreenshot:
Thereisnonoticeabledifferencebetweenthecustomproducttypeeditscreenandoneof
thebuilt-inproducttypes.
AssumingthatwehavenamedtheproductDailyDealTestProductandsavedit,we
shouldbeabletoseeitonthestorefront,asshowninthefollowingscreenshot:
Ifweaddtheproducttothecartandperformacheckout,anordershouldbecreatedjustas
withanyotherproducttype.Withintheadminarea,ontheorderviewpage,underItems
Ordered,weshouldbeabletoseetheproductonthelist,asshowninthefollowing
screenshot:
Again,thereisnonoticeabledifferencebetweenthecustomproducttypeandthebuilt-in
producttypethatisrenderingundertheItemsOrderedsection.
Finally,weshouldrunthephpbin/magentoindexer:reindexcommandontheconsole.
Eventhoughwehaven’treallyimplementedanycodewithintheindexers,thisisjustto
ensurethatnoneoftheexistingindexersbroke.
Theentiremodulecodecanbedownloadedfromhttps://github.com/ajzele/B05032-
Foggyline_DailyDeal.
Customofflineshippingmethods
Magentoprovidesseveralout-of-the-boxofflineshippingmethods,suchasFlatrate,
Freeshipping,Pickup,andTablerate.Wecanseethoseinthevendor/magento/module-
offline-shipping/Model/Carrierdirectory.
However,projectrequirementsquiteoftenaresuchthatweneedacustomcodedshipping
methodwhereaspecialbusinesslogicisapplied.Thus,theshippingpricecalculationcan
becontrolledbyus.Insuchcases,knowinghowtocodeourownofflineshippingmethod
mightcomeinhandy.
Let’sgoaheadandcreateasmallmodulecalledFoggyline_Shipboxthatprovides
Magentoanextraofflineshippingmethod.
Startbycreatingamoduleregistrationfilenamed
app/code/Foggyline/Shipbox/registration.phpwithpartialcontent,asfollows:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Foggyline_Shipbox',
__DIR__
);
Then,createanapp/code/Foggyline/Shipbox/etc/module.xmlfilewiththefollowing
content:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module
/etc/module.xsd">
<modulename="Foggyline_Shipbox"setup_version="1.0.0">
<sequence>
<modulename="Magento_OfflineShipping"/>
</sequence>
</module>
</config>
Now,createanapp/code/Foggyline/Shipbox/etc/config.xmlfilewithcontent,as
follows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store
:etc/config.xsd">
<default>
<carriers>
<shipbox>
<active>0</active>
<sallowspecific>0</sallowspecific>
<model>Foggyline\Shipbox\Model\Carrier\Shipbox</model>
<name>Shipbox</name>
<price>4.99</price>
<title>FoggylineShipbox</title>
<specificerrmsg>Thisshippingmethodisnotavailable.To
usethisshippingmethod,pleasecontactus.</specificerrmsg>
</shipbox>
</carriers>
</default>
</config>
Afterthisisdone,createanapp/code/Foggyline/Shipbox/etc/adminhtml/system.xml
filewithcontent,asfollows:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:
Magento_Config:etc/system_file.xsd">
<system>
<sectionid="carriers">
<groupid="shipbox"translate="label"type="text"
sortOrder="99"showInDefault="1"showInWebsite="1"showInStore="1">
<label>FoggylineShipbox</label>
<fieldid="active"translate="label"type="select"
sortOrder="1"showInDefault="1"showInWebsite="1"showInStore="0">
<label>Enabled</label>
<source_model>Magento\Config\Model\Config\Source\Yesno
</source_model>
</field>
<fieldid="name"translate="label"type="text"
sortOrder="3"showInDefault="1"showInWebsite="1"showInStore="1">
<label>MethodName</label>
</field>
<fieldid="price"translate="label"type="text"
sortOrder="5"showInDefault="1"showInWebsite="1"showInStore="0">
<label>Price</label>
<validate>validate-numbervalidate-zero-or-
greater</validate>
</field>
<fieldid="title"translate="label"type="text"
sortOrder="2"showInDefault="1"showInWebsite="1"showInStore="1">
<label>Title</label>
</field>
<fieldid="sallowspecific"translate="label"type="select"
sortOrder="90"showInDefault="1"showInWebsite="1"showInStore="0">
<label>ShiptoApplicableCountries</label>
<frontend_class>shipping-applicable-country
</frontend_class>
<source_model>Magento\Shipping\Model\Config\Source
\Allspecificcountries</source_model>
</field>
<fieldid="specificcountry"translate="label"
type="multiselect"sortOrder="91"showInDefault="1"showInWebsite="1"
showInStore="0">
<label>ShiptoSpecificCountries</label>
<source_model>Magento\Directory\Model
\Config\Source\Country</source_model>
<can_be_empty>1</can_be_empty>
</field>
</group>
</section>
</system>
</config>
Now,createanapp/code/Foggyline/Shipbox/Model/Carrier/Shipbox.phpfilewith
partialcontent,asfollows:
namespaceFoggyline\Shipbox\Model\Carrier;
useMagento\Quote\Model\Quote\Address\RateRequest;
classShipboxextends\Magento\Shipping\Model\Carrier\AbstractCarrier
implements\Magento\Shipping\Model\Carrier\CarrierInterface
{
protected$_code='shipbox';
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(RateRequest$request)
{
//implementbusinesslogic
}
publicfunctiongetAllowedMethods()
{
return['shipbox'=>$this->getConfigData('name')];
}
}
ExtendthecollectRatesmethodintheCarrier\Shipboxclass,asfollows:
publicfunctioncollectRates(RateRequest$request)
{
if(!$this->getConfigFlag('active')){
returnfalse;
}
//Dosomefilteringofitemsincart
if($request->getAllItems()){
foreach($request->getAllItems()as$item){
//$item->getQty();
//$item->getFreeShipping()
//$item->isShipSeparately()
//$item->getHasChildren()
//$item->getProduct()->isVirtual()
//...
}
}
//Afterfiltering,startformingfinalprice
//Finalpricedoesnothavetobefixedlikebelow
$shippingPrice=$this->getConfigData('price');
$result=$this->_rateResultFactory->create();
$method=$this->_rateMethodFactory->create();
$method->setCarrier('shipbox');
$method->setCarrierTitle($this->getConfigData('title'));
$method->setMethod('shipbox');
$method->setMethodTitle($this->getConfigData('name'));
$method->setPrice($shippingPrice);
$method->setCost($shippingPrice);
$result->append($method);
return$result;
}
IntheMagentoadminarea,ifyounowlookunderStores|Settings|Configuration|
Sales|ShippingMethods,youwillseeFoggylineShipboxonthelist,asshowninthe
followingscreenshot:
SettheEnabledoptiontoYesandclicktheSaveConfigbutton.
IfyounowruntheSELECT*FROMcore_config_dataWHEREpathLIKE"%shipbox%";
queryontheMySQLserver,youwillseeresultsthataresimilartotheonesshowninthe
followingscreenshot:
Notehowthereisnodirectcodewithinthecodesnippetsintheprecedingscreenshotthat
isrelatedtotheShiptoApplicableCountriesandShiptoSpecificCountriesoptions,
becausethehandlingoftheseoptionsisbuiltintotheparentAbstractCarrierclass.
Thus,simplybyaddingthesallowspecificoptioninconfig.xmlandsystem.xml,we
enabledafeaturewheretheshippingmethodcanbeshownorhiddenfromcertain
countries.
ThecruxoftheimplementationcomesdowntothecollectRatesmethod.Thisiswhere
weimplementourownbusinesslogicthatshouldcalculatetheshippingpricebasedonthe
itemsinthecart.Wecanusethe$request->getAllItems()inthecollectRatesmethod
tofetchthecollectionofallthecartitems,traversethroughthem,formafinalshipping
pricebasedonvariousconditions,andsoon.
Now,let’sgoaheadandjumptothestorefrontinordertotestthecheckout.Weshouldbe
abletoseeourmethodonthecheckout,asshowninthefollowingscreenshot:
Ifwecompleteoneorder,weshouldfurtherseetheshippingmethoddetailsontheorder
itself.Withintheadminarea,underSales|Operations|Orders,ifweViewourorderin
thePayment&ShippingMethodsection,weshouldseetheshippingmethod,asshown
inthefollowingscreenshot:
Similarly,intheOrderTotalssection,weshouldseetheshippingamountinShipping&
Handling,asshowninthefollowingscreenshot:
Customofflinepaymentmethods
Magentoprovidesseveralout-of-the-boxofflinepaymentmethods,suchasBanktransfer,
Cashondelivery,Checkmo,andPurchaseorder.Youcanseetheminthe
vendor/magento/module-offline-payments/Modeldirectory.
Whenitcomestopaymentmethods,itismorecommontouseanonlinepaymentprovider
(gateway),suchasPayPalorBraintree.Sometimes,projectrequirementsmaybesuchthat
wemayneedacustomcodedpaymentmethod.Youwillneedtothinkofprogrammatic
productimportandordercreationscriptthatmightspecializeinsomespecificallylabeled
paymentmethod.Thus,thepaymentprocesswillbecontrolledbyus.
Insuchcases,knowinghowtocodeourownofflinepaymentmethodmightcomein
handy.Itisworthnotingthatwhilewecanmakeanofflinepaymentthatwillgrabauser’s
creditcardinformation,itisnotreallyadvisabletodosounlessourinfrastructureisPCI-
compliant.
Let’sgoaheadandcreateasmallmodulecalledFoggyline_PayboxthatprovidesMagento
anextraofflinepaymentmethod.
Startbycreatingamoduleregistrationfilenamed
app/code/Foggyline/Paybox/registration.phpwithpartialcontent,asfollows:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Foggyline_Paybox',
__DIR__
);
Then,createanapp/code/Foggyline/Paybox/etc/module.xmlfilewiththefollowing
content:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module
/etc/module.xsd">
<modulename="Foggyline_Paybox"setup_version="1.0.0">
<sequence>
<modulename="Magento_OfflinePayments"/>
</sequence>
</module>
</config>
Afterthisisdone,createanapp/code/Foggyline/Paybox/etc/config.xmlfilewiththe
followingcontent:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:
Magento_Store:etc/config.xsd">
<default>
<payment>
<paybox>
<active>0</active>
<model>Foggyline\Paybox\Model\Paybox</model>
<order_status>pending</order_status>
<title>FoggylinePaybox</title>
<allowspecific>0</allowspecific>
<group>offline</group>
</paybox>
</payment>
</default>
</config>
Then,createtheapp/code/Foggyline/Paybox/etc/payment.xmlfilewiththefollowing
content:
<paymentxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:
Magento_Payment:etc/payment.xsd">
<methods>
<methodname="paybox">
<allow_multiple_address>1</allow_multiple_address>
</method>
</methods>
</payment>
Now,createanapp/code/Foggyline/Paybox/etc/adminhtml/system.xmlfilewiththe
followingcontent:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:
Magento_Config:etc/system_file.xsd">
<system>
<sectionid="payment">
<groupid="paybox"translate="label"type="text"sortOrder="30"
showInDefault="1"showInWebsite="1"showInStore="1">
<label>Paybox</label>
<fieldid="active"translate="label"type="select"
sortOrder="1"showInDefault="1"showInWebsite="1"showInStore="0">
<label>Enabled</label>
<source_model>Magento\Config\Model\Config\Source\Yesno
</source_model>
</field>
<fieldid="order_status"translate="label"type="select"
sortOrder="20"showInDefault="1"showInWebsite="1"showInStore="0">
<label>NewOrderStatus</label>
<source_model>Magento\Sales\Model\Config
\Source\Order\Status\NewStatus</source_model>
</field>
<fieldid="sort_order"translate="label"type="text"
sortOrder="100"showInDefault="1"showInWebsite="1"showInStore="0">
<label>SortOrder</label>
<frontend_class>validate-number</frontend_class>
</field>
<fieldid="title"translate="label"type="text"
sortOrder="10"showInDefault="1"showInWebsite="1"showInStore="1">
<label>Title</label>
</field>
<fieldid="allowspecific"translate="label"
type="allowspecific"sortOrder="50"showInDefault="1"showInWebsite="1"
showInStore="0">
<label>PaymentfromApplicableCountries</label>
<source_model>Magento\Payment\Model\
Config\Source\Allspecificcountries</source_model>
</field>
<fieldid="specificcountry"translate="label"
type="multiselect"sortOrder="51"showInDefault="1"showInWebsite="1"
showInStore="0">
<label>PaymentfromSpecificCountries</label>
<source_model>Magento\Directory\Model
\Config\Source\Country</source_model>
<can_be_empty>1</can_be_empty>
</field>
<fieldid="payable_to"translate="label"sortOrder="61"
showInDefault="1"showInWebsite="1"showInStore="1">
<label>MakeCheckPayableto</label>
</field>
<fieldid="mailing_address"translate="label"
type="textarea"sortOrder="62"showInDefault="1"showInWebsite="1"
showInStore="1">
<label>SendCheckto</label>
</field>
<fieldid="min_order_total"translate="label"type="text"
sortOrder="98"showInDefault="1"showInWebsite="1"showInStore="0">
<label>MinimumOrderTotal</label>
</field>
<fieldid="max_order_total"translate="label"type="text"
sortOrder="99"showInDefault="1"showInWebsite="1"showInStore="0">
<label>MaximumOrderTotal</label>
</field>
<fieldid="model"></field>
</group>
</section>
</system>
</config>
Createanapp/code/Foggyline/Paybox/etc/frontend/di.xmlfilewiththefollowing
content:
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:
ObjectManager/etc/config.xsd">
<typename="Magento\Checkout\Model\CompositeConfigProvider">
<arguments>
<argumentname="configProviders"xsi:type="array">
<itemname="offline_payment_paybox_config_provider"
xsi:type="object">
Foggyline\Paybox\Model\PayboxConfigProvider
</item>
</argument>
</arguments>
</type>
</config>
Afterthisisdone,createanapp/code/Foggyline/Paybox/Model/Paybox.phpfilewith
thefollowingcontent:
namespaceFoggyline\Paybox\Model;
classPayboxextends\Magento\Payment\Model\Method\AbstractMethod
{
constPAYMENT_METHOD_PAYBOX_CODE='paybox';
protected$_code=self::PAYMENT_METHOD_PAYBOX_CODE;
protected$_isOffline=true;
publicfunctiongetPayableTo()
{
return$this->getConfigData('payable_to');
}
publicfunctiongetMailingAddress()
{
return$this->getConfigData('mailing_address');
}
}
Now,createanapp/code/Foggyline/Paybox/Model/PayboxConfigProvider.phpfile
withthefollowingcontent:
namespaceFoggyline\Paybox\Model;
classPayboxConfigProviderimplements
\Magento\Checkout\Model\ConfigProviderInterface
{
protected$methodCode=
\Foggyline\Paybox\Model\Paybox::PAYMENT_METHOD_PAYBOX_CODE;
protected$method;
protected$escaper;
publicfunction__construct(
\Magento\Payment\Helper\Data$paymentHelper
)
{
$this->method=$paymentHelper->getMethodInstance($this->
methodCode);
}
publicfunctiongetConfig()
{
return$this->method->isAvailable()?[
'payment'=>[
'paybox'=>[
'mailingAddress'=>$this->getMailingAddress(),
'payableTo'=>$this->getPayableTo(),
],
],
]:[];
}
protectedfunctiongetMailingAddress()
{
$this->method->getMailingAddress();
}
protectedfunctiongetPayableTo()
{
return$this->method->getPayableTo();
}
}
Copytheentirevendor/magento/module-offline-
payments/view/frontend/layout/checkout_index_index.xmlMagentocorefileinto
theapp/code/Foggyline/Paybox/view/frontend/layout/checkout_index_index.xml
module.Then,editthemodule’scheckout_index_index.xmlfilebyreplacingtheentire
<itemname="offline-payments"xsi:type="array">elementanditschildrenwiththe
followingcode:
<itemname="foggline-offline-payments"xsi:type="array">
<itemname="component"xsi:type="string">
Foggyline_Paybox/js/view/payment/foggline-offline-payments</item>
<itemname="methods"xsi:type="array">
<itemname="paybox"xsi:type="array">
<itemname="isBillingAddressRequired"
xsi:type="boolean">true</item>
</item>
</item>
</item>
Then,createan
app/code/Foggyline/Paybox/view/frontend/web/js/view/payment/offline-
payments.jsfilewiththefollowingcontent:
/*browser:true*/
/*globaldefine*/
define(
[
'uiComponent',
'Magento_Checkout/js/model/payment/renderer-list'
],
function(
Component,
rendererList
){
'usestrict';
rendererList.push(
{
type:'paybox',
component:'Foggyline_Paybox/js/view/payment/method-
renderer/paybox'
}
);
returnComponent.extend({});
}
);
Afterthisisdone,createan
app/code/Foggyline/Paybox/view/frontend/web/js/view/payment/method-
renderer/paybox.jsfilewiththefollowingcontent:
/*browser:true*/
/*globaldefine*/
define(
[
'Magento_Checkout/js/view/payment/default'
],
function(Component){
'usestrict';
returnComponent.extend({
defaults:{
template:'Foggyline_Paybox/payment/paybox'
},
getMailingAddress:function(){
returnwindow.checkoutConfig.payment.
paybox.mailingAddress;
},
getPayableTo:function(){
returnwindow.checkoutConfig.payment.paybox.payableTo;
}
});
}
);
Now,createan
app/code/Foggyline/Paybox/view/frontend/web/template/payment/paybox.htmlfile
withthefollowingcontent:
<divclass="payment-method"data-bind="css:{'_active':(getCode()==
isChecked())}">
<divclass="payment-method-titlefieldchoice">
<inputtype="radio"
name="payment[method]"
class="radio"
data-bind="attr:{'id':getCode()},value:getCode(),
checked:isChecked,click:selectPaymentMethod,visible:
isRadioButtonVisible()"/>
<labeldata-bind="attr:{'for':getCode()}"class="label"><span
data-bind="text:getTitle()"></span></label>
</div>
<divclass="payment-method-content">
<divclass="payment-method-billing-address">
<!--koforeach:$parent.getRegion(getBillingAddressFormName())
-->
<!--kotemplate:getTemplate()--><!--/ko-->
<!--/ko-->
</div>
<!--koif:getMailingAddress()||getPayableTo()-->
<dlclass="itemscheckpayable">
<!--koif:getPayableTo()-->
<dtclass="title"><!--koi18n:'MakeCheckpayabletoooooo:'-
-><!--/ko--></dt>
<ddclass="content"><!--koi18n:getPayableTo()--><!--/ko-
-></dd>
<!--/ko-->
<!--koif:getMailingAddress()-->
<dtclass="title"><!--koi18n:'SendChecktoxyz:'—><!--/ko-
-></dt>
<ddclass="content">
<addressclass="payboxmailingaddress"data-bind="html:
$t(getMailingAddress())"></address>
</dd>
<!--/ko-->
</dl>
<!--/ko-->
<divclass="checkout-agreements-block">
<!--koforeach:$parent.getRegion('before-place-order')-->
<!--kotemplate:getTemplate()--><!--/ko-->
<!--/ko-->
</div>
<divclass="actions-toolbar">
<divclass="primary">
<buttonclass="actionprimarycheckout"
type="submit"
data-bind="
click:placeOrder,
attr:{title:$t('PlaceOrder')},
css:{disabled:!isPlaceOrderActionAllowed()},
enable:(getCode()==isChecked())
"
disabled>
<spandata-bind="i18n:'PlaceOrder'"></span>
</button>
</div>
</div>
</div>
</div>
Withthis,weconcludeourcustomofflinepaymentmethodmodule.Theentiremodule
canbefoundathttps://github.com/ajzele/B05032-Foggyline_Paybox.
Summary
Inthischapter,wetoucheduponsomeofthemostcommonbitsoffunctionalitythat
developerscomeincontactwith.Welearnedwheretolookintheadminareaandhowto
programmaticallymanagetheentitiesbehindthesefunctionalities.Thus,wewere
effectivelyabletomanuallyandprogrammaticallycreateandfetchCMSpages,blocks,
categories,andproducts.Wealsolearnedhowtocreateproductandcustomerimport
scripts.Finally,westudiedhowtocreateourowncustomproducttype,simplepayment,
andshipmentmodule.
ThefollowingchapterwillguideusthroughMagento’sin-builttestsandhowwecanuse
themtoeffectivelyQAanapplicationtokeepithealthy.
Chapter11.Testing
Softwaretestingcanbedefinedasacriticalstepinthedevelopmentlifecycle.Thisstepis
oftensilentlyoverlookedbyanumberofdevelopersbecauseacertainamountoftime
needtobeinvestedintowritingadecenttestsuiteforacodebase.Ratherthanbeinga
singleone-timeactivity,writingtestsisaprocessthatfollowsourcodeasitgrowsand
changes.Testresultsshould,atanygiventime,validateandverifythatoursoftwareworks
asexpected,thusmeetingthebusinessandtechnicalrequirements.Writingtestsshould
followwritingtheactualapplicationcodeearlyoninthelifecycle.Thishelpsprevent
defectsfrombeingintroducedinthecode.
Onahighlevel,wecandividetestsintothefollowingcategories:
Static:Applicationcodeisnotexecutedduringtesting.Possibleerrorsarefoundby
inspectingtheapplicationcodefilesandnotontheirexecution.
Dynamic:Applicationcodeisexecutedduringtesting.Possibleerrorsarefound
whilecheckingforfunctionalbehaviorofanapplication.
Inthischapter,wewilltakealookatthetestingoptionsthatMagentooffers.Alongthe
way,wewillbuildabasicmodulewithsometestingfeaturesinit.
Typesoftests
Magentoprovidesseveraltypesoftestsoutofthebox.Wecanseealistofthesetestson
runningthefollowingcommandontheconsoleintheMagentorootfolder:
phpbin/magentodev:tests:run–help
Theresultofthecommandisanoutputthatlookslikethis:
Usage:
dev:tests:run[type]
Arguments:
typeTypeoftesttorun.Availabletypes:all,unit,integration,
integration-all,static,static-all,integrity,legacy,default(default:
"default")
ThisoutputoriginatesfromtheConsole/Command/DevTestsRunCommand.phpfileinthe
coreMagento_Developermodule.Lookingattheoutput,wemightsaythatthereare
actuallyninetypesoftests,whichareasfollows:
all
unit
integration
integration-all
static
static-all
integrity
legacy
default
However,thesearenotuniquetypesoftests;thesearecombinations,aswewillsoonsee.
Let’stakeacloserlookatthecodeintheDevTestsRunCommandclassandits
setupTestInfomethod.
ThesetupTestInfomethoddefinestheinternalcommandsproperty,asfollows:
$this->commands=[
'unit'=>['../tests/unit',''],
'unit-performance'=>['../tests/performance/
framework/tests/unit',''],
'unit-static'=>['../tests/static/framework/tests/unit',
''],
'unit-integration'=>['../tests/integration/
framework/tests/unit',''],
'integration'=>['../tests/integration',''],
'integration-integrity'=>['../tests/integration','
testsuite/Magento/Test/Integrity'],
'static-default'=>['../tests/static',''],
'static-legacy'=>['../tests/static','
testsuite/Magento/Test/Legacy'],
'static-integration-js'=>['../tests/static','
testsuite/Magento/Test/Js/Exemplar'],
];
Furthermore,wecanseethetypespropertyinthesetupTestInfomethoddefinedinthe
followingway:
$this->types=[
'all'=>array_keys($this->commands),
'unit'=>['unit','unit-performance','unit-static',
'unit-integration'],
'integration'=>['integration'],
'integration-all'=>['integration','integration-integrity'],
'static'=>['static-default'],
'static-all'=>['static-default','static-legacy','static-
integration-js'],
'integrity'=>['static-default','static-legacy','integration-
integrity'],
'legacy'=>['static-legacy'],
'default'=>[
'unit',
'unit-performance',
'unit-static',
'unit-integration',
'integration',
'static-default',
],
];
Thetypespropertylogicallygroupsoneormoretestsintoasinglenamethatisfound
underthecommandsproperty.Wecanseehowlikeunitsingletypeencompassestheunit,
unit-performance,unit-static,andunit-integrationtestsinit.Thecommands
propertypointstothedisklocationoftheactualtestlibrary.RelativetotheMagentoroot
installationfolder,testscanbefoundinthedev/tests/directory.
Unittesting
Unittestsaredesignedtotestindividualclassmethodsinisolation,assertingallpossible
combinationsandtakingcareofthesmallesttestablepartofanapplication.Magentouses
thePHPUnittestingframeworkforitsunittests.Beinghighlyfocused,unittestsmakeit
easytoidentifytherootcauseofissuesifacertaintestfails.
WecanspecificallytriggertheunittestsfromtherootoftheMagentoinstallationbyusing
thefollowingcommand:
phpbin/magentodev:tests:rununit
Oncetriggered,Magentowillruntheexecutecommandinthevendor/magento/module-
developer/Console/Command/DevTestsRunCommand.phpfile.Sincetheunittypeis
mappedtoseveralcommands,whatwillhappeninternallyisthatMagentowillchangethe
directoriesfromonedirectorytoanother,asfollows:
dev/tests/unit
dev/tests/performance/framework/tests/unit
dev/tests/static/framework/tests/unit
dev/tests/integration/framework/tests/unit
Wecansaythatallofthesedirectoriesareconsideredunittestdirectories.
Withineachofthosedirectories,Magentointernallyrunsthepassthru($command,
$returnVal)method,wherethe$commandparametergetsresolvedtoastringsimilarto
thefollowingone:
php/www/magento2/./vendor/phpunit/phpunit/phpunit
ThePHPUnitwillthenlookforthephpunit.xmlconfigurationfileaccordinglyineachof
thesedirectories.Ifphpunit.xmldoesnotexist,weneedtocopythecontentsof
phpunit.xml.distintophpunit.xml.
Let’stakeacloserlookatthedev/tests/unit/phpunit.xmlfilefortestsuite,filter,
whitelist,andotherconfigurationelements.
Thefollowingdefaulttestsuitedirectorylistisfoundinthe
dev/tests/unit/phpunit.xmlfile,whichliststhedirectoriesinwhichyouneedtolook
fortestsfilesprefixedwithTest.php:
../../../app/code/*/*/Test/Unit
../../../dev/tools/*/*/Test/Unit
../../../dev/tools/*/*/*/Test/Unit
../../../lib/internal/*/*/Test/Unit
../../../lib/internal/*/*/*/Test/Unit
../../../setup/src/*/*/Test/Unit
../../../update/app/code/*/*/Test/Unit
../../../vendor/*/module-*/Test/Unit
../../../vendor/*/framework/Test/Unit
../../../vendor/*/framework/*/Test/Unit
Thelistisrelativetothedev/tests/unit/directory.Forexample,ifwetakealookatthe
firstlineintheprecedingcodeandthenlookattheMagento_Catalogmodule,itisclear
thattheTestfilesarefoundundertheapp/code/<vendorName>/<moduleName>/Test/
directoryanditssubdirectories.EverythingsuffixedwithTest.phpinthesefolderswill
getexecutedasapartofaunittest.
Tip
Ifwewerebuildingourownmodule,wecouldeasilymakeacopyof
dev/tests/unit/phpunit.xml.dist,properlyedittestsuiteandfilter>whitelist
toquicklyexecuteonlyourmodule’sunittests,thussavingsometimeonavoiding
frequentexecutionofentireMagentounittests.
Integrationtesting
Integrationteststesttheinteractionbetweenindividualcomponents,layers,andan
environment.Theycanbefoundinthedev/tests/integrationdirectory.Likeunittests,
MagentoalsousesPHPUnitforintegrationtests.Thus,thedifferencebetweenaunitand
anintegrationtestisnotthatmuchofatechnicalnature;rather,it’sofalogicalnature.
Tospecificallytriggerintegrationtestsonly,wecanexecutethefollowingcommandon
theconsole:
phpbin/magentodev:tests:runintegration
Whenexecuted,Magentointernallychangesthedirectorytodev/tests/integrationand
executesacommandthatissimilartothefollowingone:
php/Users/branko/www/magento2/./vendor/phpunit/phpunit/phpunit
Theintegrationdirectoryhasitsownphpunit.xml.distfile.Lookingatitstestsuite
definition,wecanseethatitispointingtoalltheTest.phpsuffixedfilesthatarefoundin
thedev/tests/integration/testsuitedirectory.
Statictesting
Statictestsdonotreallyrunthecode;theyanalyzeit.Theyareusedtoverifythatthe
codeconformstocertaincodingstandards,suchasPSR-1.Wecanfindthemunderthe
dev/tests/staticdirectory.
Tospecificallytriggerstatictestsonly,wecanexecutethefollowingcommandonthe
console:
phpbin/magentodev:tests:runstatic
Whenexecuted,Magentointernallychangesthedirectorytodev/tests/staticand
executesacommandthatissimilartothefollowingone:
php/Users/branko/www/magento2/./vendor/phpunit/phpunit/phpunit
Thestaticdirectoryhasitsownphpunit.xml.distfile.Lookingatitstestsuite
definition,youwillseethefollowingfourtestsuitesdefined:
JavaScriptstaticcodeanalysis
PHPcodingstandardverification
Codeintegritytests
XSSunsafeoutputtest
JSHint,aJavaScriptcodequalitytool,isusedforJavaScriptstaticcodeanalysis.ForPHP
codestandardverification,theelementsofPHP_CodeSnifferlibrariesareused.
PHP_CodeSniffertokenizesPHP,JavaScript,andCSSfilesanddetectsviolationsofa
definedsetofcodingstandards.
Integritytesting
Integritytestscheckhowanapplicationislinked.Theycheckforthingssuchasmerged
configurationvalidation.Basically,theytellusifyourapplicationshouldbeabletorun.
WecanspecificallytriggertheintegritytestsfromtherootoftheMagentoinstallationby
usingthefollowingcommand:
phpbin/magentodev:tests:runintegrity
Whenthisisexecuted,Magentofirstinternallychangesthedirectoryto
dev/tests/staticandthenexecutestwocommandsthataresimilartothefollowing
ones:
php/Users/branko/www/magento2/./vendor/phpunit/phpunit/phpunit
php/Users/branko/www/magento2/./vendor/phpunit/phpunit/phpunit
testsuite/Magento/Test/Legacy
Then,Magentointernallychangesthedirectorytodev/tests/integrationandexecutes
acommandthatissimilartothefollowingone:
php/Users/branko/www/magento2/./vendor/phpunit/phpunit/phpunit
testsuite/Magento/Test/Integrity
IntegrationtestsalsoutilizethePHPUnittowritetheactualtests.
Legacytesting
Legacytestscomprisefragmentsoflibrariesthathelpdevelopersporttheirmodulestoa
newversionofMagento.
WecantriggerlegacytestsspecificallyfromtherootoftheMagentoinstallationbyusing
thefollowingcommand:
phpbin/magentodev:tests:runlegacy
Whenthisisexecuted,Magentofirstinternallychangesthedirectoryto
/dev/tests/staticandthenexecutesacommand,whichissimilartothefollowingone:
php/Users/branko/www/magento2/./vendor/phpunit/phpunit/phpunit
testsuite/Magento/Test/Legacy
Oncethisistriggered,thecoderunsacheckforobsoleteaccesslists,connections,menus,
responses,systemconfiguration,andafewotherthings.
Performancetesting
Performancetestscanbefoundunderthesetup/performance-toolkit/directory.These
testsrequireApacheJMetertobeinstalledandareavailableontheconsoleviathejmeter
command.ApacheJMetercanbedownloadedandinstalledbyfollowingtheinstructions
athttp://jmeter.apache.org.
Thecruxoftheperformancetestisdefinedinthebenchmark.jmxfile,whichcanbe
openedintheJMeterGUItool,asshowninthefollowingscreenshot:
Asshownintheprecedingscreenshot,thedefaultbenchmark.jmxtestsaresectionedinto
threethreadgroupsthatarenamedsetUpThreadGroup,CustomerCheckout,and
tearDownThreadGroup.Wemightwanttoadditionallyclickoneachgroupand
configureitwithsomeextraparameters,thuspossiblychangingNumberofThreads
(users),asshowninthefollowingscreenshot.Wecanthensimplysavethechangesas
modificationstothebenchmark.jmxfileorafilewithnewname:
WecanmanuallytriggeraperformancetestfromtheconsolewithoutusingaGUI
interfacebyrunningthefollowingcommand:
jmeter-n\
-t/Users/branko/www/magento2/setup/performance-toolkit/benchmark.jmx\
-l/Users/branko/Desktop/jmeter-tmp/results.jtl\
-Jhost="magento2.ce"\
-Jbase_path="/"\
-Jreport_save_path="/Users/branko/report"\
-Jloops=2\
-Jurl_suffix=".html"\
-Jcustomer_email="john.doe@email.loc"\
-Jcustomer_password="abc123"\
-Jadmin_path="/admin_nwb0bx"\
-Jadmin-user="john"\
-Jadmin-password="abc123"\
-Jresponse_time_file_name="/Users/branko/report/AggregateGraph.csv"\
-Jsimple_product_url_key="simple-product-1"\
-Jsimple_product_name="SimpleProduct1"\
-Jconfigurable_product_url_key="configurable-product-1"\
-Jconfigurable_product_name="ConfigurableProduct1"\
-Jcategory_url_key="category-1"\
-Jcategory_name="Category1"\
-Jsleep_between_steps=50
Theconsoleparametersthatarelistedhereandwhichstartwith-Jalsomatchthenames
oftheUsedDefinedVariablestesttoolkit,asshownintheprecedingscreenshot.Weneed
tobecarefulandsetthemaccordingtotheMagentoinstallation.The-nparameter
instructsjmetertorunintherunnonguimode.The-tparameteriswherewesetthepath
ofthetest(.jmx)filetorun.The-lparametersetsthefilewhereweneedtologsamples
to.
Functionaltesting
Functionaltestsmimictheuserinteractionwithourapplication.Theyliterallymean
testingintheformofbrowserinteraction,whichinvolvesclickingonthepage,adding
productstothecart,andsoon.Forthispurpose,MagentousesMagentoTesting
Framework(MTF).It’saPHPwrapperaroundSelenium,whichisaportablesoftware
testingframeworkforwebapplications.MTFisnotavailableoutoftheboxviathe
console.Itcanbedownloadedathttps://github.com/magento/mtf.
ThefollowingrequirementsneedtobemetbeforeinstallingMTF:
Gitmustbeinstalled.
TheFirefoxbrowsermustbeinstalled.
ThePHPopensslextensionmustbeinstalledandenabled.
Javaversion1.6orlaterisrequiredandit’sJARexecutablemustbeinthesystem
PATH.
TheSeleniumstandaloneserver,whichisavailableathttp://www.seleniumhq.org/,
needstobedownloaded.ThedownloadshouldprovideaJARfilethatwewilllater
needtoreferto.
MagentomustbeinstalledandconfiguredtonotusethesecretURLkey.Wecanset
thesecretURLkeyoptionbynavigatingtoStores|Configuration|Advanced|
Admin|Security|AddSecretKeytoURLs[Yes/No]andsettingittoNo.
Oncetheminimalrequirementsaremet,wecaninstallMTF,asfollows:
1. Runthecomposerinstallcommandfromthedev/tests/functional/directory.
Thiscreatesanewdirectorynamedvendor;MTFispulledfromtheGitrepositoryat
https://github.com/magento/mtf.Weshouldseeanewdirectorynamedvendorthatis
createdwiththecheckedoffMTF.Thevendordirectorycontainsthecontentthatis
showninthefollowingscreenshot:
2. Runthegenerate.phpfilefromthedev/tests/functional/utils/directory.This
shouldgiveusaconsoleoutputthatissimilartothefollowingone:
||Item||Count||Time||
||PageClasses||152||0||
||FixtureClasses||46||0||
||RepositoryClasses||67||0||
||Block||475||0||
||Fixture||100||0||
||Handler||3||0||
||Page||165||0||
||Repository||67||0||
Note
Thegeneratortoolcreatesfactoriesforfixtures,handlers,repositories,pageobjects,
andblockobjects.WhenMTFisinitialized,thefactoriesarepregeneratedto
facilitatethecreationandrunningoftests.
Beforewecanactuallyrunthetests,thereareafewmorethingsthatweneedtoconfigure,
asfollows:
1. Editthedev/tests/functional/phpunit.xmlfile.Underthephpelement,for
name="app_frontend_url",setthevalueoftheactualURLfortheMagento
storefrontundertest.Forname="app_backend_url",setthevalueoftheactualURL
fortheMagentoadminURLundertest.Forname="credentials_file_path",setthe
valueof./credentials.xml.
Tip
Ifphpunit.xmldoesnotexist,weneedtocreateitandcopythecontentsof
dev/tests/functional/phpunit.xml.distintoitandthenedititafterwards.
2. Editthedev/tests/functional/etc/config.xmlfile.Undertheapplication
element,findandedittheinformationaboutbackendLogin,backendPassword,and
appBackendUrlsothatitmatchesthatofourstore.
Tip
Ifconfig.xmldoesnotexist,weneedtocreateitandcopythecontentsof
dev/tests/functional/etc/config.xml.distintoitandthenedititafterwards.
3. Editthedev/tests/functional/credentials.xmlfile.Chancesarethatwewillnot
needthisonablankMagentoinstallation,aswecanseebydefaulttheentriesforthe
fedex,ups,dhlUS,anddhlEUcarriers,whichhaven’tbeensetonthefreshly
installedMagento.
Tip
Ifcredentials.xmldoesnotexist,weneedtocreateitandcopythecontentsof
dev/tests/functional/credentials.xml.distintoitandthenedititafterwards.
4. Runthejava-jar{selenium_directory}/selenium-server.jarcommandvia
theconsole.ThisistoensurethattheSeleniumserverisrunning.
5. Openanewconsoleoraconsoletabandexecutethephpunitcommandinthe
dev/tests/functional/directory.ThiscommandshouldopentheFirefoxbrowser
andstartrunningtestcasesinit,simulatingauserclickingonthebrowserwindow
andfillingintheforminputs.
Whileatestisrunning,Magentowilllogallthefailedtestsunderthe
dev/tests/functional/var/logdirectoryinastructurethatissimilartotheoneshown
inthefollowingscreenshot:
Thelogpathcanbeconfiguredinthedev/tests/functional/phpunit.xmlfileunderthe
phpelementwithname="basedir".
Ifwewanttotargetaspecifictestwithintheentiretestsuite,wecansimplytriggera
commandlikethefollowingoneinthedev/tests/functional/directory:
phpunittests/app/Magento/Customer/Test/TestCase
/RegisterCustomerFrontendEntityTest.php
Theprecedingcommandwillrunasingletestcalled
RegisterCustomerFrontendEntityTest.php.Wecanalsouseashorterformexpression
forthesamething,asfollows:
phpunit--filterRegisterCustomerFrontendEntityTest
Oncethisisexecuted,thebrowsershouldopenandsimulatethecustomerregistration
processonthestorefront.
Writingasimpleunittest
NowthatwetookaquicklookatallthetypeofteststhatMagentooffers,let’stakeastep
backandlookatunittestsagain.Inpractice,unittestsareprobablytheonesthatwewill
bewritingmostofthetime.Withthisinmind,let’sgrabtheFoggyline_Unitlymodule
fromhttps://github.com/ajzele/B05032-Foggyline_Unitlyandstartwritingunittestsforit.
IfyoudonotalreadyhavetheFoggyline_Unitlymoduleinthecodebasethatwasapart
ofthepreviouschapters,thenyouneedtoplaceitscontentunder
app/code/Foggyline/Unitlyandexecutethefollowingcommandsontheconsolefrom
therootoftheMagentodirectory:
phpbin/magentomodule:enableFoggyline_Unitly
phpbin/magentosetup:upgrade
Theteststhatwewillwriteresideinthemodule’sTest/Unitdirectory.Thismakesthe
entirepathofthetestdirectorylooklikeapp/code/Foggyline/Unitly/Test/Unit/.
Magentoknowsthatitneedstolookinsidethisfoldersimplybecauseofthetestsuite
directorydefinitionsfoundinthedev/tests/unit/phpunit.xmlfile,asshowninthe
followingpieceofcode:
<directorysuffix="Test.php">
../../../app/code/*/*/Test/Unit
</directory>
ThestructureoffilesandthefolderwithintheindividualmoduleTest/Unitdirectoryalso
followsthestructureofthatmodule’sfilesandfolders.Thefollowingscreenshotshowsa
structureoftheTest/UnitdirectoryfortheMagento_Catalogmodule:
ThisshowsthatalmostanyPHPclasscanbeunittestedirrespectiveofthefactthatitisa
controller,block,helper,module,observer,orsomethingelse.Tokeepthingssimple,we
willfocusonthecontrollerandblockunittestsinrelationtotheFoggyline_Unitly
module,whichisstructuredasfollows:
Let’sstartbyfirstwritingatestfortheFoggyline\Unitly\Controller\Hello\Shout
controllerclass.TheShoutclass,ignoringthe__construct,hasonlyonemethodcalled
execute.
Wewillwriteatestforitunderthesamedirectorystructure,relativetothemodule’s
Test\Unitdirectory,placingthetestunderthe
app/code/Foggyline/Unitly/Test/Unit/Controller/Hello/ShoutTest.phpfilewith
(partial),asfollows:
namespaceFoggyline\Unitly\Test\Unit\Controller\Hello;
classShoutTestextends\PHPUnit_Framework_TestCase
{
protected$resultPageFactory;
protected$controller;
publicfunctionsetUp()
{
/*setUp()codehere*/
}
publicfunctiontestExecute()
{
/*testExecute()codehere*/
}
}
EveryunittestintheMagentomoduledirectoryextendsfromthe
\PHPUnit_Framework_TestCaseclass.ThesetUpmethodiscalledbeforethetestis
executed;wecanthinkofitasPHP’s__construct.Here,wewouldusuallysetupthe
fixtures,openanetworkconnection,orperformsimilaractions.
ThetestExecutemethodnameisactuallyformedfromtest+themethodnamefromthe
classthatwearetesting.SincetheShoutclasshasanexecutemethod,thetestmethod
formedbecomestest+execute.Bycapitalizingthefirstletteroftheclassmethodname,
thefinalnameistestExecute.
Now,let’sgoaheadandreplace/*setUp()codehere*/withcontent.asfollows:
$request=$this->getMock(
'Magento\Framework\App\Request\Http',
[],
[],
'',
false
);
$context=$this->getMock(
'\Magento\Framework\App\Action\Context',
['getRequest'],
[],
'',
false
);
$context->expects($this->once())
->method('getRequest')
->willReturn($request);
$this->resultPageFactory=$this->getMockBuilder
('Magento\Framework\View\Result\PageFactory')
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
$this->controller=new\Foggyline\Unitly\Controller\Hello\Shout(
$context,
$this->resultPageFactory
);
Thewholeconceptoftestsisbasedonmockingtheobjectsthatweneedtoworkwith.We
usethegetMockmethodthatreturnsamockobjectforaspecifiedclass.Besidestheclass
name,thegetMockmethodacceptsquiteabitofotherarguments.Thesecond$methods
parametermarksthenamesofthemethodsthatarereplacedwithatestdouble.Providing
nullforthe$methodsparametermeansthatnomethodswillbereplaced.Thethird
parameterforthegetMockmethodstandsfor$arguments,whichareparametersthatare
passedtotheoriginalclassconstructor.
Wecanseefromtheprecedingcodethatthe$requestmockobjectdoesnotprovideany
$methodsor$argumentsparameterstoitsgetMockmethod.Ontheotherhand,the
$contextobjectpassesonthearraywithasinglegetRequestelementinit.Oncethe
$contextobjectisinitialized,itthencallstheexpectsmethod,whichregistersanew
expectationinthemockobjectandreturnsInvocationMockeronwhichwecallmethod
andwillReturn.Inthiscase,theinstanceonthepreviouslyinitiated$requestobjectis
passedtowillReturn.WeusedgetMockBuildertocreateaResult\PageFactorymock
objectandinstantiatedtheShoutcontrolleractionclass,passingthecontextandresult
pagemockstoit.
AllthecodeinthissetUpmethodservedapurposeingettingoutthecontrollerinstance,
whichwillbeusedinthetestExecutemethod.
Tip
Thefinal,private,andstaticmethodscannotbemocked.Theyareignoredby
PHPUnit’stestfunctionalitybecausetheyretaintheiroriginalbehavior.
Let’sgoaheadandreplacethe/*testExecute()codehere*/withcontent,asfollows:
$title=$this->getMockBuilder('Magento\Framework\View\Page\Title')
->disableOriginalConstructor()
->getMock();
$title->expects($this->once())
->method('set')
->with('Unitly');
$config=$this->getMockBuilder('Magento\Framework\View\Page\Config')
->disableOriginalConstructor()
->getMock();
$config->expects($this->once())
->method('getTitle')
->willReturn($title);
$page=$this->getMockBuilder('Magento\Framework\View\Result\Page')
->disableOriginalConstructor()
->getMock();
$page->expects($this->once())
->method('getConfig')
->willReturn($config);
$this->resultPageFactory->expects($this->once())
->method('create')
->willReturn($page);
$result=$this->controller->execute();
$this->assertInstanceOf('Magento\Framework\View\Result\Page',$result);
Intheprecedingcode,wecheckedintothepagetitle,page,andresultpageobject.Toget
tothepagetitlefromwithinthecontrollercode,wewouldnormallyuseanexpression
suchas$resultPage->getConfig()->getTitle().Thisexpressioninvolvesthree
objects.The$resultPageobjectcallsthegetConfig()method,whichreturnsthe
instanceofthePage\Configobject.ThisobjectcallsforthegetTitlemethod,which
returnstheinstanceofthePage\Titleobject.Thus,wearemockingandtestingallthe
threeobjects.
Nowthatwetookalookatthecontrollertestcase,let’sseehowwecanmakeoneforthe
blockclass.Createan
app/code/Foggyline/Unitly/Test/Unit/Block/Hello/ShoutTest.phpfilewithpartial
content,asfollows:
namespaceFoggyline\Unitly\Test\Unit\Block\Hello;
classShoutTestextends\PHPUnit_Framework_TestCase
{
/**
*@var\Foggyline\Unitly\Block\Hello\Shout
*/
protected$block;
protectedfunctionsetUp()
{
$objectManager=new\Magento\Framework\TestFramework\Unit
\Helper\ObjectManager($this);
$this->block=$objectManager->
getObject('Foggyline\Unitly\Block\Hello\Shout');
}
publicfunctiontestGreeting()
{
$name='Foggyline';
$this->assertEquals(
'Hello'.$this->block->escapeHtml($name),
$this->block->greeting($name)
);
}
}
Here,wehavealsodefinedthesetUpmethodandtestGreeting.ThetestGreeting
methodisusedasatestforthegreetingmethodontheShoutblockclass.
Conceptually,thereisnodifferencebetweenunittestingacontroller,block,ormodel
class.Therefore,wewillomitthemodelunittestinthisexample.What’simportantfor
youtorealizeisthatthetestiswhatwemakeofit.Technicallyspeaking,wecantesta
singlemethodforvariouscasesorjustthemostobviousone.However,toservethe
purposeofthetestsinabetterway,weshouldtestitforanypossiblenumberofresult
combinations.
Let’sgoaheadandcreateadev/tests/unit/foggyline-unitly-phpunit.xmlfilewith
content,asfollows:
<phpunitxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
colors="true"
bootstrap="./framework/bootstrap.php"
>
<testsuitename="Foggyline_Unitly-UnitTests">
<directorysuffix="Test.php">
../../../app/code/Foggyline/Unitly/Test/Unit
</directory>
</testsuite>
<php>
<ininame="date.timezone"value="Europe/Zagreb"/>
<ininame="xdebug.max_nesting_level"value="200"/>
</php>
<filter>
<whitelistaddUncoveredFilesFromWhiteList="true">
<directorysuffix=".php">
../../../app/code/Foggyline/Unitly/*
</directory>
</whitelist>
</filter>
<logging>
<logtype="coverage-html"
target="coverage_dir/Foggyline_Unitly/test-reports/coverage"charset="UTF-
8"yui="true"highlight="true"/>
</logging>
</phpunit>
Finally,wecanexecuteonlyourownmoduleunittestsbyrunningacommandsuchas
phpunit-cfoggyline-unitly-phpunit.xml.
Oncetestsareexecuted,weshouldbeabletoseetheentirecodecoveragereportinthe
dev/tests/unit/coverage_dir/Foggyline_Unitly/test-
reports/coverage/index.htmlfile,asshowninthefollowingscreenshot:
Theprecedingscreenshotdemonstrateshowdetailedthecodecoverageis,whichshows
eventhepercentagesandlinesofcodecoveredwithtest.
Summary
Inthischapter,wetookalookatthetestingfacilityembeddedinMagentothroughthe
librariesintherootdev/tests/directoryandtheMagento_Developermodule.We
learnedhowtorunallofitstesttypesandstudiedasimpleexampleofwritingourown
unittests.TheexamplesthataregivenheredonotdojusticetoPHPUnit,givenits
robustness.MoreinformationonPHPUnitcanbefoundathttps://phpunit.de/.
Wewillnowmoveontothefinalchapterofthisbook,wherewewillreiteratethethings
thatwelearnedsofaranddevelopafunctionalminiaturemodulethatinvolvessomebasic
testing.
Chapter12.BuildingaModulefrom
Scratch
Basedontheknowledgeacquiredfrompreviouschapters,wewillnowbuildaminiature
Helpdeskmodule.Thoughminiature,themodulewillshowcasetheusageofseveral
importantMagentoplatformfeaturesaswegothroughthefollowingsections:
Registeringamodule(registration.phpandmodule.xml)
Creatingaconfigurationfile(config.xml)
Creatinge-mailtemplates(email_templates.xml)
Creatingasystemconfigurationfile(system.xml)
Creatingaccesscontrollists(acl.xml)
Creatinganinstallationscript(InstallSchema.php)
Managingentitypersistence(model,resource,collection)
Buildingafrontendinterface
Buildingabackendinterface
Creatingunittests
Modulerequirements
Modulerequirementsaredefinedasfollows:
Nameused,Foggyline/Helpdesk
Datatobestoredintableiscalledfoggyline_helpdesk_ticket
Ticketsentitywillcontainticket_id,customer_id,title,severity,created_at,
andstatusproperties
Thecustomer_idpropertyistobeforeignkeyonthecustomer_entitytable
Therewillbethreeavailableticketseverityvalues:low,medium,andhigh
Ifnotspecified,thedefaultseverityvaluefornewticketsislow
Therewillbetwoavailableticketstatuses:openedandclosed
Ifnotspecified,thedefaultstatusvaluefornewticketsisopened
Twoe-mailstemplates:store_owner_to_customer_email_templateand
customer_to_store_owner_email_templatearetobedefinedforpushinge-mail
updatesuponticketcreationandstatuschange
CustomerswillbeabletosubmitaticketthroughtheirMyAccountsection
CustomerswillbeabletoseealloftheirpreviouslysubmittedticketsundertheirMy
Accountsection
Customerswillnotbeabletoeditanyexistingtickets
Onceacustomersubmitsanewticket,transactionale-mail(let’scallitFoggyline–
Helpdesk–Customer|StoreOwner)issenttothestoreowner
ConfigurableoptionisrequiredforpossiblyoverridingFoggyline–Helpdesk–
Customer|StoreOwnere-mail
AdminuserswillbeabletoaccessalistofallticketsunderCustomers|Helpdesk
Tickets
AdminuserswillbeabletochangeticketstatusfromOpenedtoClosedandother
wayround
Onceanadminuserchangestheticketstatus,transactionale-mail(let’scallit
Foggyline–Helpdesk–StoreOwner|Customer)issenttothecustomer
ConfigurableoptionisrequiredforpossiblyoverridingFoggyline–Helpdesk–
StoreOwner|Customere-mail
Withtherequirementsoutlined,wearereadytobeginourmoduledevelopment.
Registeringamodule
Wefirststartbydefiningtheapp/code/Foggyline/Helpdesk/registration.phpfile
withthefollowingcontent:
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Foggyline_Helpdesk',
__DIR__
);
Wethendefinetheapp/code/Foggyline/Helpdesk/etc/module.xmlfilewiththe
followingcontent:
<?xmlversion="1.0"?>
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module
/etc/module.xsd">
<modulename="Foggyline_Helpdesk"setup_version="1.0.0">
<sequence>
<modulename="Magento_Store"/>
<modulename="Magento_Customer"/>
</sequence>
</module>
</config>
Lookingattheprecedingfile,ifwestripawaytheboilerplatethatrepeatsitselfacrossall
modules,weareleftwiththreeimportantthingshere:
Themodulenameattribute,definedasFoggyline_Helpdesk.Weneedtobesureto
followacertainpatternwhennamingourmodules,likeVendor+_+Modulename.
Themodulenameattributecancontainonlylettersandnumbers[A-Z,a-z,0-9,_].
Theschemasetup_versionattributethatdefinesourmoduleversion.Itsvaluecan
containonlynumbers[0-9].Ourexamplesetsthevalueof1.0.0forthe
setup_versionattribute.
Thesequencemodulenameattribute,whichdefinesmoduledependencies.Our
modulebasicallysaysitrequiresMagento_StoreandMagento_Customermodulesto
beenabled.
Oncethisfileisinplace,weneedtogotothecommandline,changethedirectorytothat
ofMagentoinstallation,andsimplyexecutethefollowingcommand:
phpbin/magentomodule:enableFoggyline_Helpdesk
However,ifwenowopeneithertheadminofthefrontendareainourbrowser,wemight
getanerrorpage,whichgeneratesthefollowingerrorunderthevar/reports/folder:
Pleaseupgradeyourdatabase:Run"bin/magentosetup:upgrade"fromthe
Magentorootdirectory.
Luckily,theerrorisprettyself-descriptivesowesimplymovebacktotheconsole,change
thedirectorytotheMagentorootfolder,andexecutethefollowingcommand:
phpbin/magentosetup:upgrade
Executedcommandswillactivateourmodule.
Wecanconfirmthatbylookingundertheapp/etc/config.phpfile,asshowninthe
followingscreenshot(online33):
Furtherifwelogintotheadminarea,andgotoStores|Configuration|Advanced|
Advanced,weshouldseeourmodulelistedthere,asshowninthefollowingscreenshot:
Creatingaconfigurationfile(config.xml)
Nowwewillcreateanapp/code/Foggyline/Helpdesk/etc/config.xmlfilewiththe
content,asfollows:
<?xmlversion="1.0"?>
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:
etc/config.xsd">
<default>
<foggyline_helpdesk>
<email_template>
<customer>
foggyline_helpdesk_email_template_customer
</customer>
<store_owner>
foggyline_helpdesk_email_template_store_owner
</store_owner>
</email_template>
</foggyline_helpdesk>
</default>
</config>
Thismightlookconfusingatfirstastowherethedefault|foggyline_helpdesk|
email_templatestructurecomesfrom.Thestructureitselfdenotesthepositionofour
configurationvaluesthatwewillmaptotheadministrativeinterfacevisibleinourbrowser
undertheStores|Configurationsection.GiventhatallthingsvisualregardingtheStores
|Configurationsectionoriginatefromsystem.xmlfiles,thisstructurewehavenowin
config.xmlwillthenmaptoanothersystem.xmlfilewewilldefinesoon.
Rightnow,justrememberthestructureandthevaluescontainedwithinthecustomerand
store_ownerattributes.Thesevalueswillfurthermaptoanotheremail_templates.xml
file,whichwewillsooncreate.
Thereisonemoreimportantthingregardingtheconfig.xmlfile.Weneedtobevery
carefulofthexsi:noNamespaceSchemaLocationattributevalue.Thisvalueneedstobeset
tourn:magento:module:Magento_Store:etc/config.xsd.It’sanaliasthatactually
pointstothevendor/magento/module-store/etc/config.xsdfile.
Creatinge-mailtemplates
(email_templates.xml)
Ourmodulerequirementsspecifythattwoe-mailtemplatesneedtobedefined.Hintsto
thishavealreadybeengivenintheapp/code/Foggyline/Helpdesk/etc/config.xmlfile
previouslydefined.Theactualdefinitionofe-mailtemplatesavailabletoourmodulesis
donethroughtheapp/code/Foggyline/Helpdesk/etc/email_templates.xmlfile,with
thecontentasfollows:
<?xmlversion="1.0"?>
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:
Magento_Email:etc/email_templates.xsd">
<templateid="foggyline_helpdesk_email_template_customer"
label="FoggylineHelpdesk-CustomerEmail"
file="store_owner_to_customer.html"type="html"
module="Foggyline_Helpdesk"area="frontend"/>
<templateid="foggyline_helpdesk_email_template_store_owner"
label="FoggylineHelpdesk-StoreOwnerEmail"
file="customer_to_store_owner.html"type="html"
module="Foggyline_Helpdesk"area="frontend"/>
</config>
Lookingintoemail_templates.xsd,wecanconcludethatthevaluesforid,label,file,
type,andmoduleareallrequired.idshouldbedefineduniquetoourmodule,giving
somesensibleandreasonablecodenametooure-mailtemplates,asthiscodenameis
goingtobeusedfurtherinotherXMLfilesorincode.
WhatwedefinedasIDvalueshere,canbefoundunder
app/code/Foggyline/Helpdesk/etc/config.xml,asthevalueofdefault|
foggyline_helpdesk|email_template|customeranddefault|foggyline_helpdesk|
email_template|store_ownerelements.
Ifitisnotyetfullyclearwhattheconnectionbetweenthetwois;wewillgettoitwhen
westartbuildingoursystem.xmlfilesoon.
Thevalueofthelabelattributeissomethingthatisvisiblelateron,withintheMagento
adminareaunderMarketing|Communications|EmailTemplates,sobesuretoput
somethinguserfriendlyandeasilyrecognizablehere.
Further,thevaluesofthefileattributepointtothelocationofthefollowingfiles:
app/code/Foggyline/Helpdesk/view/frontend/email/customer_to_store_owner.html
app/code/Foggyline/Helpdesk/view/frontend/email/store_owner_to_customer.html
Thecontentofthefileswillbesetsuchthatlateron,inthecode,wewillneedtopassiton
certainvariablesinordertofillinthevariableplaceholders.
Thecustomer_to_store_owner.htmle-mailtemplate,withcontentasfollows,willbe
triggeredlateroninthecodewhenacustomercreatesanewticket:
<!--@subjectNewTicketCreated@-->
<h1>Ticket#{{varticket.ticket_id}}created</h1>
<ul>
<li>Id:{{varticket.ticket_id}}</li>
<li>Title:{{varticket.title}}</li>
<li>Created_at:{{varticket.created_at}}</li>
<li>Severity:{{varticket.severity}}</li>
</ul>
Lateron,wewillseehowtopasstheticketobjectasavariableintothetemplate,in
ordertoenablecallslike{{varticket.title}}withintheHTMLtemplate.
Thestore_owner_to_customer.htmle-mailtemplate,withcontentasfollows,willbe
triggeredlateroninthecodewhenthestoreownerchangesthestatusofaticket:
<!--@subjectTicketUpdated@-->
<h1>Ticket#{{varticket.ticket_id}}updated</h1>
<p>Hi{{varcustomer_name}}.</p>
<p>Statusofyourticket#{{varticket.ticket_id}}hasbeenupdated</p>
<ul>
<li>Title:{{varticket.title}}</li>
<li>Created_at:{{varticket.created_at}}</li>
<li>Severity:{{varticket.severity}}</li>
</ul>
IfwenowlogintotheMagentoadminarea,gounderMarketing|Communications|
EmailTemplates,clickontheAddNewTemplatebutton,andweshouldbeabletosee
ourtwoe-mailtemplatesundertheTemplatedrop-down,asshowninthefollowing
screenshot:
Ifwelookbackatourconfig.xmlandemail_templates.xml,thereisstillnoclear
connectionastowhatdefault|foggyline_helpdesk|email_template|customerand
default|foggyline_helpdesk|email_template|store_ownerunderconfig.xml
actuallydo.Thatisbecausewestilllacktwomoreingredientsthatwilllinkthemtogether:
theapp/code/Foggyline/Helpdesk/etc/adminhtml/system.xmland
app/code/Foggyline/Helpdesk/etc/acl.xmlfiles.
Creatingasystemconfigurationfile
(system.xml)
Thesystem.xmlfileisessentiallytheStores|Configurationinterfacebuilder.Entrieswe
defineinourmodule’ssystem.xmlfilewillrendercertainpartsoftheStores|
ConfigurationinterfaceundertheMagentoadminarea.
UnliketheprevioustwoXMLfiles,thisconfigurationfileislocatedunderanadditional
subfolder,soitsfullpathgoeslike
app/code/Foggyline/Helpdesk/etc/adminhtml/system.xml,withcontentasfollows:
<?xmlversion="1.0"?>
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:
Magento_Config:etc/system_file.xsd">
<system>
<tabid="foggyline"translate="label"sortOrder="200">
<label>Foggyline</label>
</tab>
<sectionid="foggyline_helpdesk"translate="label"type="text"
sortOrder="110"showInDefault="1"
showInWebsite="1"showInStore="1">
<label>Helpdesk</label>
<tab>foggyline</tab>
<resource>Foggyline_Helpdesk::helpdesk</resource>
<groupid="email_template"translate="label"type="text"
sortOrder="1"showInDefault="1"showInWebsite="1"showInStore="1">
<label>EmailTemplateOptions</label>
<fieldid="customer"translate="label"type="select"
sortOrder="1"showInDefault="1"showInWebsite="1"showInStore="1">
<label>
StoreOwnertoCustomerEmailTemplate
</label>
<source_model>
Magento\Config\Model\Config\Source\Email\Template
</source_model>
</field>
<fieldid="store_owner"translate="label"type="select"
sortOrder="1"showInDefault="1"showInWebsite="1"showInStore="1">
<label>
CustomertoStoreOwnerEmailTemplate
</label>
<source_model>
Magento\Config\Model\Config\Source\Email\Template
</source_model>
</field>
</group>
</section>
</system>
</config>
Eventhoughwehavealotgoingoninthisfile,itcanallbesummedupinafewimportant
bits.
Note
Determiningwherewewanttoshowourmoduleconfigurationoptionsisamatterof
choice.Eitherwedefineanduseourowntaborweuseanexistingtabfromoneofthe
coremodules.Itreallycomesdowntowherewedecidetoputourconfigurationoptions.
system.xmldefinesonetab,asnotedbythetabelementassignedidattributevalueof
foggyline.Wecanhavemultipletabsdefinedunderasinglesystem.xmlfile.Thetab
elementattributeidneedstobeuniqueunderalltabs,notjustthosedefinedwithinour
module.Withinthetabelement,wehavealabelelementwiththevalueofFoggyline.
ThisvalueiswhatshowsupundertheMagentoadminStores|Configurationarea.
Thefinalresultsshouldbeasshowninthefollowingimage:
Tip
Magentohassixpre-existingtabsdefined(General,Service,Advanced,Catalog,
Customer,Sales)acrossitscoremodules.Wecaneasilygetalistofalldefinedtabsin
Magentojustbydoingasearchforthetabstring,filteringonlyonfilesnamed
system.xml.
Nexttothetabelement,wehavetheconfig|system|sectionelement.Thisisthe
elementwithinwhichwefurtherdefinewhataretobecomeHTMLinputfieldsfor
acceptingconfigurationoptions,asvisibleonthepreviousimage.
Wecanhavemultiplesectionsdefinedwithinasinglesystem.xmlfile.Theactual
sectionelementattributesrequireustospecifytheidattributevalue,whichinour
exampleissettofoggyline_helpdesk.Otherimportantsectionelementattributesare
showInWebsiteandshowInStore.Thesecanhaveeither0or1asavalue.Dependingon
ourmodulebusinesslogic,wemightfindagoodreasonforchoosingonevalueoverthe
other.
Lookingfurther,theelementscontainedwithinoursectionelementare:
label:ThisspecifiesthelabelwewillseeundertheMagentoadminStore|
Configurationarea.
tab:ThisspecifiestheIDvalueofatabunderwhichwewantthissectiontoappear,
whichinourcaseequalstofoggyline.
resource:ThisspecifiestheACLresourceIDvalue.
group:Thisspecifiesthegroupoffields.Similartothesectionelement,italsohas
id,sortOrder,showInWebsite,andshowInStoreattributes.Further,thegroup
elementhaschildfieldelements,whichtranslatetoHTMLinputfieldsunderthe
MagentoadminStore|Configurationarea.
Wedefinedtwofields,customerandstore_owner.Similartosectionandgroup,field
elementsalsohaveid,sortOrder,showInWebsite,andshowInStoreattributes.
Noticehowfieldfurthercontainschildelementsthatdefineitsoptions.Giventhatour
fieldelementtypeattributewassettoselectwithbothfields,weneededtodefinethe
source_modelelementwithineachfield.Bothfieldshavethesamesource_modelvalue
whichpointstotheMagentocoreclass,
Magento\Config\Model\Config\Source\Email\Template.Lookingintothatclass,we
canseeitimplements\Magento\Framework\Option\ArrayInterfaceanddefinesthe
toOptionArraymethod.DuringrenderingtheadminStores|Configurationarea,
MagentowillcallthismethodtofillinthevaluesfortheselectHTMLelement.
Tip
Understandingwhatwecandowithsystem.xmlcomesdowntounderstandingwhatis
definedundervendor/magento/module-config/etc/system_file.xsdandstudying
existingMagentocoremodulesystem.xmlfilestogetsomeexamples.
Asnotedpreviously,oursystem.xmlhasaresourceelementthatpointstothe
app/code/Foggyline/Helpdesk/etc/acl.xmlfile,whichwewillnowlookinto.
Creatingaccesscontrollists(acl.xml)
Theapp/code/Foggyline/Helpdesk/etc/acl.xmlfileiswherewedefineourmodule
accesscontrollistresources.AccesscontrollistresourcesarevisibleundertheMagento
adminSystem|Permissions|UserRolesarea,whenweclickontheAddNewRole
button,asshowninthefollowingscreenshot:
Lookingattheprecedingscreenshot,wecanseeourHelpdeskSectionunderStores|
Settings|Configuration.Howdidweputitthere?Wehavedefineditinour
app/code/Foggyline/Helpdesk/etc/acl.xmlfilewithcontentasfollows:
<?xmlversion="1.0"?>
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<acl>
<resources>
<resourceid="Magento_Backend::admin">
<resourceid="Magento_Customer::customer">
<resourceid="Foggyline_Helpdesk::ticket_manage"
title="ManageHelpdeskTickets"/>
</resource>
<resourceid="Magento_Backend::stores">
<resourceid="Magento_Backend::stores_settings">
<resourceid="Magento_Config::config">
<resourceid="Foggyline_Helpdesk::helpdesk"
title="HelpdeskSection"/>
</resource>
</resource>
</resource>
</resource>
</resources>
</acl>
</config>
Lookingattheprovidedcode,theimmediateconclusionisthatresourcescanbenested
intoeachother.Itisunclearhowweshouldknowwheretonestourcustom-defined
resourcewithanIDvalueofFoggyline_Helpdesk::helpdesk.Thesimpleansweriswe
followedtheMagentostructure.BylookingintoafewoftheMagentocoremodules
system.xmlfilesandtheiracl.xmlfiles,apatternemergedwheremodulesnesttheir
resourceunderMagento_Backend::admin|Magento_Backend::stores|
Magento_Backend::stores_settings|Magento_Config::config.Theseareallexisting
resourcesdefinedincoreMagento,sowearemerelyreferencingthem,notdefiningthem.
Theonlyresourcewearedefininginouracl.xmlfileisourown,whichwearethen
referencingfromoursystem.xmlfile.Wecandefineotherresourceswithinacl.xmland
notallwouldbenestedintothesamestructureasFoggyline_Helpdesk::helpdesk.
Thevalueofthetitleattributeweassigntoaresourceelementisshownintheadmin
area,asinthepreviousscreenshot.
Tip
Besuretouseadescriptivelabelsothatourmoduleresourceiseasilyrecognizable.
Creatinganinstallationscript
(InstallSchema.php)
InstallSchema,orinstallscript,isawayforustosetuptablesinthedatabasethatwillbe
usedtopersistourmodelslateron.
Ifwelookbackatthemodulerequirements,thefollowingfieldsneedtobecreatedinthe
foggyline_helpdesk_tickettable:
ticket_id
customer_id
title
severity
created_at
status
OurInstallSchemaisdefinedunderthe
app/code/Foggyline/Helpdesk/Setup/InstallSchema.phpfilewith(partial)contentas
follows:
<?php
namespaceFoggyline\Helpdesk\Setup;
useMagento\Framework\Setup\InstallSchemaInterface;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\SchemaSetupInterface;
/**
*@codeCoverageIgnore
*/
classInstallSchemaimplementsInstallSchemaInterface
{
publicfunctioninstall(SchemaSetupInterface$setup,
ModuleContextInterface$context)
{
$installer=$setup;
$installer->startSetup();
$table=$installer->getConnection()
->newTable($installer->getTable('foggyline_helpdesk_ticket'))
/*->addColumn…*/
/*->addIndex…*/
/*->addForeignKey…*/
->setComment('FoggylineHelpdeskTicket');
$installer->getConnection()->createTable($table);
$installer->endSetup();
}
}
TheInstallSchemaclassconformstoInstallSchemaInterfacebyimplementinga
singleinstallmethod.Withinthismethod,westarttheinstaller,createnewtables,create
newfields,addindexesandforeignkeystothetable,andfinallyendtheinstaller,as
showninthefollowing(partial)code:
->addColumn(
'ticket_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity'=>true,'unsigned'=>true,'nullable'=>false,'primary'
=>true],
'TicketId'
)
->addColumn(
'customer_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['unsigned'=>true],
'CustomerId'
)
->addColumn(
'title',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
null,
['nullable'=>false],
'Title'
)
->addColumn(
'severity',
\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
null,
['nullable'=>false],
'Severity'
)
->addColumn(
'created_at',
\Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
null,
['nullable'=>false],
'CreatedAt'
)
->addColumn(
'status',
\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
null,
['nullable'=>false],
'Status'
)
->addIndex(
$installer->getIdxName('foggyline_helpdesk_ticket',['customer_id']),
['customer_id']
)
->addForeignKey(
$installer->getFkName('foggyline_helpdesk_ticket','customer_id',
'customer_entity','entity_id'),
'customer_id',
$installer->getTable('customer_entity'),
'entity_id',
\Magento\Framework\DB\Ddl\Table::ACTION_SET_NULL
)
Theprovidedcodeshowseachofthefieldsfromthemodulerequirementbeingaddedto
thedatabaseusingtheaddColumnmethodcallandpassingitcertainparameterssuchasthe
fieldtypeandnullablestate.ItisworthgettingfamiliarwiththeaddColumn,
addIndex,andaddForeignKeymethodsasthesearemostcommonlyusedwhen
specifyingnewtablesforourmodules.
Tip
Wecouldfurtherdeepenourunderstandingoftheinstallationscriptbystudyinghowother
coremoduleshandletheInstallSchema.phpfile.Followingagooddatabasedesign
practice,weshouldalwayscreateindexesandforeignkeysonourtablewhenreferencing
datafromothertables.
Managingentitypersistence(model,
resource,collection)
WithInstallSchemainplace,wenowhaveconditionsforentitypersistence.Ournext
stepistodefinemodel,resource,andcollectionclassesfortheTicketentity.
TheTicketentitymodelclassisdefinedunderthe
app/code/Foggyline/Helpdesk/Model/Ticket.phpfilewithcontentasfollows:
<?php
namespaceFoggyline\Helpdesk\Model;
classTicketextends\Magento\Framework\Model\AbstractModel
{
constSTATUS_OPENED=1;
constSTATUS_CLOSED=2;
constSEVERITY_LOW=1;
constSEVERITY_MEDIUM=2;
constSEVERITY_HIGH=3;
protectedstatic$statusesOptions=[
self::STATUS_OPENED=>'Opened',
self::STATUS_CLOSED=>'Closed',
];
protectedstatic$severitiesOptions=[
self::SEVERITY_LOW=>'Low',
self::SEVERITY_MEDIUM=>'Medium',
self::SEVERITY_HIGH=>'High',
];
/**
*Initializeresourcemodel
*@returnvoid
*/
protectedfunction_construct()
{
$this->_init('Foggyline\Helpdesk\Model\ResourceModel\Ticket');
}
publicstaticfunctiongetSeveritiesOptionArray()
{
returnself::$severitiesOptions;
}
publicfunctiongetStatusAsLabel()
{
returnself::$statusesOptions[$this->getStatus()];
}
publicfunctiongetSeverityAsLabel()
{
returnself::$severitiesOptions[$this->getSeverity()];
}
}
Readingtheprecedingcode,weseeitextendsthe
\Magento\Framework\Model\AbstractModelclass,whichfurtherextendsthe
\Magento\Framework\Objectclass.ThisbringsalotofextramethodsintoourTicket
modelclass,suchasload,delete,save,toArray,toJson,toString,toXml,andsoon.
Theonlyactualrequirementforusistodefinethe_constructmethodthat,throughthe
_initfunctioncall,specifiestheresourceclassthemodelwillbeusingwhenpersisting
data.WehavesetthisvaluetoFoggyline\Helpdesk\Model\ResourceModel\Ticket,
whichwillbethenextclasswewilldefine,theso-calledresourceclass.
Wehavefurtherdefinedseveralconstants,STATUS_*andSEVERITY_*,asasignofgood
programmingpracticeandnottohardcodevaluesthatwewilluseacrossthecode,which
wecancentralizeintoaclassconstant.Theseconstants,inaway,maptoourmodule
requirements.
Additionally,wehavethreeadditionalmethods(getSeveritiesOptionArray,
getStatusAsLabel,andgetSeverityAsLabel)thatwewilluselateroninourblockclass
andtemplatefile.
TheTicketentityresourceclassisdefinedunder
app/code/Foggyline/Helpdesk/Model/ResourceModel/Ticket.phpwithcontentas
follows:
<?php
namespaceFoggyline\Helpdesk\Model\ResourceModel;
classTicketextends\Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
/**
*Initializeresourcemodel
*Gettablenamefromconfig
*
*@returnvoid
*/
protectedfunction_construct()
{
$this->_init('foggyline_helpdesk_ticket','ticket_id');
}
}
Wecanseethecodeextendsthe
\Magento\Framework\Model\ResourceModel\Db\AbstractDbclass,whichfurther
extendsthe\Magento\Framework\Model\ResourceModel\AbstractResourceclass.This
bringsalotofextramethodsintoourTicketresourceclass,suchasload,delete,save,
commit,rollback,andsoon.
Theonlyactualrequirementforusistodefinethe_constructmethod,throughwhichwe
callthe_initfunctionthatacceptstwoparameters.Thefirstparameterofthe_init
functionspecifiesthetablenamefoggyline_helpdesk_ticketandthesecondparameter
specifiesidentifyingtheticket_idcolumnwithinthattablewherewewillbepersisting
data.
Finally,wedefinetheTicketentitycollectionclassunder
app/code/Foggyline/Helpdesk/Model/ResourceModel/Ticket/Collection.phpwith
contentasfollows:
<?php
namespaceFoggyline\Helpdesk\Model\ResourceModel\Ticket;
classCollectionextends\Magento\Framework\Model\
ResourceModel\Db\Collection\AbstractCollection
{
/**
*Constructor
*Configurescollection
*
*@returnvoid
*/
protectedfunction_construct()
{
$this->_init('Foggyline\Helpdesk\Model\Ticket',
'Foggyline\Helpdesk\Model\ResourceModel\Ticket');
}
}
Thecollectionclasscodeextendsthe
\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
class,whichfurtherextendsthe\Magento\Framework\Data\Collection\AbstractDb
class,whichfurtherextends\Magento\Framework\Data\Collection.Thefinalparent
collectionclassthenimplementsthefollowinginterfaces:\IteratorAggregate,
\Countable,Magento\Framework\Option\ArrayInterface,and
Magento\Framework\Data\CollectionDataSourceInterface.Throughthisdeep
inheritance,alargenumberofmethodsbecomeavailabletoourcollectionclass,suchas
count,getAllIds,getColumnValues,getFirstItem,getLastItem,andsoon.
Withregardtoournewlydefinedcollectionclass,theonlyactualrequirementforusisto
definethe_constructmethod.Withinthe_constructmethod,wecallthe_initfunction
towhichwepasstwoparameters.ThefirstparameterspecifiestheTicketmodelclass
Foggyline\Helpdesk\Model\TicketandthesecondparameterspecifiestheTicket
resourceclassFoggyline\Helpdesk\Model\ResourceModel\Ticket.
Thethreeclasseswejustdefined(model,resource,collection)actasanoverallsingle
entitypersistencemechanism.Withthecurrentlydefinedcode,weareabletosave,delete,
update,lookupwithfiltering,andlistourTicketentities,whichwedemonstrateinthe
upcomingsections.
Buildingafrontendinterface
Nowthatwehavedefinedthenecessaryminimumfordatapersistencefunctionality,we
canmoveforwardtobuildingafrontendinterface.Themodulerequirementsaysthat
customersshouldbeabletosubmitaticketthroughtheirMyAccountsection.Wewill
thereforeaddalinkcalledHelpdeskTicketsunderthecustomer’sMyAccountsection.
Thefollowingareneededforafullyfunctionalfrontend:
Aroutethatwillmaptoourcontroller
Acontrollerthatwillcatchrequestsfromamappedroute
Acontrolleractionthatwillloadthelayout
LayoutXMLsthatwillupdatetheviewmakingitlookasifweareontheMy
Accountsectionwhileprovidingcontentofourown
Ablockclasstopowerourtemplatefile
Atemplatefilethatwewillrenderintothecontentareaofapage
AcontrolleractionthatwillsavetheNewTicketformonceitisposted
Creatingroutes,controllers,andlayouthandles
Westartbydefiningaroutewithinthe
app/code/Foggyline/Helpdesk/etc/frontend/routes.xmlfilewithcontentasfollows:
<?xmlversion="1.0"?>
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<routerid="standard">
<routeid="foggyline_helpdesk"frontName="foggyline_helpdesk">
<modulename="Foggyline_Helpdesk"/>
</route>
</router>
</config>
NotethattherouteelementidandfrontNameattributeshavethesamevalue,buttheydo
notservethesamepurpose,aswewillseesoon.
Nowwedefineourcontrollerapp/code/Foggyline/Helpdesk/Controller/Ticket.php
filewithcontentasfollows:
<?php
namespaceFoggyline\Helpdesk\Controller;
abstractclassTicketextends\Magento\Framework\App\Action\Action
{
protected$customerSession;
publicfunction__construct(
\Magento\Framework\App\Action\Context$context,
\Magento\Customer\Model\Session$customerSession
)
{
$this->customerSession=$customerSession;
parent::__construct($context);
}
publicfunctiondispatch(\Magento\Framework\App\RequestInterface
$request)
{
if(!$this->customerSession->authenticate()){
$this->_actionFlag->set('','no-dispatch',true);
if(!$this->customerSession->getBeforeUrl()){
$this->customerSession->setBeforeUrl($this->_redirect-
>getRefererUrl());
}
}
returnparent::dispatch($request);
}
}
Ourcontrollerloadsthecustomersessionobjectthroughitsconstructor.Thecustomer
sessionobjectisthenusedwithinthedispatchmethodtocheckifthecustomeris
authenticatedornot.Ifthecustomerisnotauthenticated,allfrontendactionsinthe
Internetbrowserthatleadtothiscontrollerwillresultinthecustomerbeingredirectedto
theloginscreen.
Oncethecontrollerisinplace,wecanthendefinetheactionsthatextendfromit.Each
actionisaclassfileonitsown,extendingfromtheparentclass.Wewillnowdefineour
indexaction,theonethatwillrendertheviewunderMyAccount|HelpdeskTickets,
withintheapp/code/Foggyline/Helpdesk/Controller/Ticket/Index.phpfilewith
contentasfollows:
<?php
namespaceFoggyline\Helpdesk\Controller\Ticket;
classIndexextends\Foggyline\Helpdesk\Controller\Ticket
{
publicfunctionexecute()
{
$resultPage=$this->resultFactory->create(\Magento
\Framework\Controller\ResultFactory::TYPE_PAGE);
return$resultPage;
}
}
Controlleractioncodeliveswithintheexecutemethodofitsclass.Wesimplyextend
fromthe\Foggyline\Helpdesk\Controller\Ticketcontrollerclassanddefinethe
necessarylogicwithintheexecutemethod.SimplycallingloadLayoutandrenderLayout
isenoughtorenderthepageonthefrontend.
ThefrontendXMLlayouthandlesresideunderthe
app/code/Foggyline/Helpdesk/view/frontend/layoutfolder.HavingtherouteID,
controller,andcontrolleractionisenoughforustodeterminethehandlename,which
goesbyformula{routeid}_{controllername}_{controlleractionname}.xml.Thus,we
defineanindexactionlayoutwithinthe
app/code/Foggyline/Helpdesk/view/frontend/layout/foggyline_helpdesk_ticket_index.xml
filewithcontentasfollows:
<?xmlversion="1.0"?>
<pagexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout
/etc/page_configuration.xsd">
<updatehandle="customer_account"/>
<body>
<referenceContainername="content">
<blockclass="Foggyline\Helpdesk\Block\Ticket\Index"
name="foggyline.helpdesk.ticket.index"template=
"Foggyline_Helpdesk::ticket/index.phtml"cacheable="false"/>
</referenceContainer>
</body>
</page>
Noticehowweimmediatelycalltheupdatedirective,passingitthecustomer_account
handleattributevalue.Thisislikesaying,“Includeeverythingfromthe
customer_accounthandleintoourhandlehere.”Wearefurtherreferencingthecontent
block,withinwhichwedefineourowncustomblocktype
Foggyline\Helpdesk\Block\Ticket\Index.Thoughablockclasscanspecifyitsown
template,weareusingatemplateattributewithamodule-specificpath,
Foggyline_Helpdesk::ticket/index.phtml,toassignatemplatetoablock.
Simplyincludingthecustomer_acounthandleisnotenough;weneedsomethingextrato
defineourlinkundertheMyAccountsection.Wedefinethisextrasomethingunderthe
app/code/Foggyline/Helpdesk/view/frontend/layout/customer_account.xmlfile
withcontentasfollows:
<?xmlversion="1.0"?>
<pagexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/
Layout/etc/page_configuration.xsd">
<head>
<title>HelpdeskTickets</title>
</head>
<body>
<referenceBlockname="customer_account_navigation">
<blockclass="Magento\Framework\View\Element\Html
\Link\Current"name="foggyline-helpdesk-ticket">
<arguments>
<argumentname="path"xsi:type="string">
foggyline_helpdesk/ticket/index</argument>
<argumentname="label"xsi:type="string">Helpdesk
Tickets</argument>
</arguments>
</block>
</referenceBlock>
</body>
</page>
Whatishappeninghereisthatwearereferencinganexistingblockcalled
customer_account_navigationanddefininganewblockwithinitofclass
Magento\Framework\View\Element\Html\Link\Current.Thisblockacceptstwo
parameters:thepaththatissettoourcontrolleractionandthelabelthatissettoHelpdesk
Tickets.
Creatingblocksandtemplates
TheFoggyline\Helpdesk\Block\Ticket\Indexblockclasswepointedtofrom
foggyline_helpdesk_ticket_index.xmlisdefinedunderthe
app/code/Foggyline/Helpdesk/Block/Ticket/Index.phpfilewithcontentasfollows:
<?php
namespaceFoggyline\Helpdesk\Block\Ticket;
classIndexextends\Magento\Framework\View\Element\Template
{
/**
*@var\Magento\Framework\Stdlib\DateTime
*/
protected$dateTime;
/**
*@var\Magento\Customer\Model\Session
*/
protected$customerSession;
/**
*@var\Foggyline\Helpdesk\Model\TicketFactory
*/
protected$ticketFactory;
/**
*@param\Magento\Framework\View\Element\Template\Context$context
*@paramarray$data
*/
publicfunction__construct(
\Magento\Framework\View\Element\Template\Context$context,
\Magento\Framework\Stdlib\DateTime$dateTime,
\Magento\Customer\Model\Session$customerSession,
\Foggyline\Helpdesk\Model\TicketFactory$ticketFactory,
array$data=[]
)
{
$this->dateTime=$dateTime;
$this->customerSession=$customerSession;
$this->ticketFactory=$ticketFactory;
parent::__construct($context,$data);
}
/**
*@return\Foggyline\Helpdesk\Model\ResourceModel\Ticket\Collection
*/
publicfunctiongetTickets()
{
return$this->ticketFactory
->create()
->getCollection()
->addFieldToFilter('customer_id',$this->customerSession-
>getCustomerId());
}
publicfunctiongetSeverities()
{
return\Foggyline\Helpdesk\Model\
Ticket::getSeveritiesOptionArray();
}
}
ThereasonwhywedefinedtheFoggyline\Helpdesk\Block\Ticketblockclassinstead
ofusingjust\Magento\Framework\View\Element\Templateisbecausewewantedto
definesomehelpermethodswecouldthenuseinourindex.phtmltemplate.These
methodsaregetTickets(whichwewilluseforlistingallcustomertickets)and
getSeverities(whichwewilluseforcreatingadropdownofpossibleseveritiesto
choosefromwhencreatinganewticket).
Thetemplateisfurtherdefinedunderthe
app/code/Foggyline/Helpdesk/view/frontend/templates/ticket/index.phtmlfile
withcontentasfollows:
<?php$tickets=$block->getTickets()?>
<form
id="form-validate"
action="<?phpecho$block->getUrl('foggyline_helpdesk/ticket/save')?
>"
method="post">
<?phpecho$block->getBlockHtml('formkey')?>
<divclass="fieldtitlerequired">
<labelclass="label"for="title"><span><?phpecho__('Title')?>
</span></label>
<divclass="control">
<input
id="title"
type="text"
name="title"
data-validate="{required:true}"
value=""
placeholder="<?phpecho__('Somethingdescriptive')?>"/>
</div>
</div>
<divclass="fieldseverity">
<labelclass="label"for="severity"><span><?phpecho__('Severity')
?></span></label>
<divclass="control">
<selectname="severity">
<?phpforeach($block->getSeverities()as$value=>$name):
?>
<optionvalue="<?phpecho$value?>"><?phpecho$this-
>escapeHtml($name)?></option>
<?phpendforeach;?>
</select>
</div>
</div>
<buttontype="submit"class="actionsaveprimary">
<span><?phpecho__('SubmitTicket')?></span>
</button>
</form>
<script>
require([
'jquery',
'mage/mage'
],function($){
vardataForm=$('#form-validate');
dataForm.mage('validation',{});
});
</script>
<?phpif($tickets->count()):?>
<tableclass="data-grid">
<?phpforeach($ticketsas$ticket):?>
<tr>
<td><?phpecho$ticket->getId()?></td>
<td><?phpecho$block->escapeHtml($ticket->getTitle())?>
</td>
<td><?phpecho$ticket->getCreatedAt()?></td>
<td><?phpecho$ticket->getSeverityAsLabel()?></td>
<td><?phpecho$ticket->getStatusAsLabel()?></td>
</tr>
<?phpendforeach;?>
</table>
<?phpendif;?>
Thoughthisisabigchunkofcode,itiseasilyreadableasitisdividedintoafewvery
differentrole-playingchunks.
The$blockvariableisactuallythesameasifwewrote$this,whichisareferencetothe
instanceoftheFoggyline\Helpdesk\Block\Ticketclasswherewedefinedtheactual
getTicketsmethod.Thus,the$ticketsvariableisfirstdefinedasacollectionoftickets
thatbelongtothecurrentlylogged-incustomer.
WethenspecifiedaformwithaPOSTmethodtypeandanactionURLthatpointstoour
Savecontrolleraction.Withintheform,wehavea$block->getBlockHtml('formkey')
call,whichbasicallyreturnsahiddeninputfieldnamedform_keywhosevalueisa
randomstring.FormkeysinMagentoareameansofpreventingagainstCross-Site
RequestForgery(CSRF),soweneedtobesuretousethemonanyformwedefine.As
partoftheform,wehavealsodefinedatitleinputfield,severityselectfield,andsubmit
button.NoticetheCSSclassestossedaround,whichguaranteethatourform’slookwill
matchthoseofotherMagentoforms.
Rightaftertheclosingformtag,wehaveaRequireJStypeofJavaScriptinclusionfor
validation.GiventhatourformIDvalueissettoform-validate,theJavaScriptdataForm
variablebindstoitandtriggersavalidationcheckwhenwepresstheSubmitbutton.
Wethenhaveacountcheckandaforeachloopthatrendersallpossiblyexistingcustomer
tickets.
Thefinalresultofthetemplatecodecanbeseeninthefollowingimage:
Handlingformsubmissions
Thereisonemorepiecewearemissinginordertocompleteourfrontendfunctionality–a
controlleractionthatwillsavetheNewTicketformonceitisposted.Wedefinethis
actionwithintheapp/code/Foggyline/Helpdesk/Controller/Ticket/Save.phpfile
withcontentasfollows:
<?php
namespaceFoggyline\Helpdesk\Controller\Ticket;
classSaveextends\Foggyline\Helpdesk\Controller\Ticket
{
protected$transportBuilder;
protected$inlineTranslation;
protected$scopeConfig;
protected$storeManager;
protected$formKeyValidator;
protected$dateTime;
protected$ticketFactory;
publicfunction__construct(
\Magento\Framework\App\Action\Context$context,
\Magento\Customer\Model\Session$customerSession,
\Magento\Framework\Mail\Template\TransportBuilder
$transportBuilder,
\Magento\Framework\Translate\Inline\StateInterface
$inlineTranslation,
\Magento\Framework\App\Config\ScopeConfigInterface$scopeConfig,
\Magento\Store\Model\StoreManagerInterface$storeManager,
\Magento\Framework\Data\Form\FormKey\Validator$formKeyValidator,
\Magento\Framework\Stdlib\DateTime$dateTime,
\Foggyline\Helpdesk\Model\TicketFactory$ticketFactory
)
{
$this->transportBuilder=$transportBuilder;
$this->inlineTranslation=$inlineTranslation;
$this->scopeConfig=$scopeConfig;
$this->storeManager=$storeManager;
$this->formKeyValidator=$formKeyValidator;
$this->dateTime=$dateTime;
$this->ticketFactory=$ticketFactory;
$this->messageManager=$context->getMessageManager();
parent::__construct($context,$customerSession);
}
publicfunctionexecute()
{
$resultRedirect=$this->resultRedirectFactory->create();
if(!$this->formKeyValidator->validate($this->getRequest())){
return$resultRedirect->setRefererUrl();
}
$title=$this->getRequest()->getParam('title');
$severity=$this->getRequest()->getParam('severity');
try{
/*Saveticket*/
$ticket=$this->ticketFactory->create();
$ticket->setCustomerId($this->customerSession->
getCustomerId());
$ticket->setTitle($title);
$ticket->setSeverity($severity);
$ticket->setCreatedAt($this->dateTime->formatDate(true));
$ticket->setStatus(\Foggyline\Helpdesk\Model\
Ticket::STATUS_OPENED);
$ticket->save();
$customer=$this->customerSession->getCustomerData();
/*Sendemailtostoreowner*/
$storeScope=\Magento\Store\Model\ScopeInterface::SCOPE_STORE;
$transport=$this->transportBuilder
->setTemplateIdentifier($this->scopeConfig->
getValue('foggyline_helpdesk/email_template/store_owner',$storeScope))
->setTemplateOptions(
[
'area'=>\Magento\Framework\App\
Area::AREA_FRONTEND,
'store'=>$this->storeManager->getStore()-
>getId(),
]
)
->setTemplateVars(['ticket'=>$ticket])
->setFrom([
'name'=>$customer->getFirstname().''.$customer-
>getLastname(),
'email'=>$customer->getEmail()
])
->addTo($this->scopeConfig->getValue(
'trans_email/ident_general/email',$storeScope))
->getTransport();
$transport->sendMessage();
$this->inlineTranslation->resume();
$this->messageManager->addSuccess(__('Ticketsuccessfully
created.'));
}catch(Exception$e){
$this->messageManager->addError(__('Erroroccurredduring
ticketcreation.'));
}
return$resultRedirect->setRefererUrl();
}
}
First,welookat__constructtoseewhatparametersarepassedtoit.Giventhatthecode
werunintheexecutemethodneedstocheckiftheformkeyisvalid,createaticketinthe
database,passontheticketandsomecustomerinfotothee-mailthatisbeingsenttothe
storeowner;then,wegetanideaofwhatkindofobjectsarebeingpassedaround.
Theexecutemethodstartsbycheckingthevalidityoftheformkey.Iftheformkeyis
invalid,wereturnwitharedirectiontothereferringURL.
Passingtheformkeycheck,wegrabthetitleandseverityvariablesaspassedbytheform.
Wetheninstantiatetheticketentitybytheticketfactorycreatemethodandsimplysetthe
ticketentityvaluesonebyone.NotethattheTicketentitymodel
Foggyline\Helpdesk\Model\TicketdoesnotreallyhavemethodslikesetSeverityon
itsown.Thisistheinheritedpropertyofits\Magento\Framework\Objectparentclass.
Oncetheticketentityissaved,weinitiatethetransportbuilderobject,passingalong
alloftherequiredparametersforsuccessfule-mailsending.Noticehow
setTemplateIdentifierusesoursystem.xmlconfigurationoption
foggyline_helpdesk/email_template/store_owner.This,ifnotspecificallysetunder
theadminStore|Configuration|Foggyline|Helpdeskarea,hasadefaultvaluedefined
underconfig.xmlthatpointstothee-mailtemplateIDintheemail_templates.xmlfile.
setTemplateVarsexpectsthearrayorinstanceof\Magento\Framework\Objecttobe
passedtoit.Wepasstheentire$ticketobjecttoit,justnestingitundertheticketkey,
thusmakingthepropertiesofaTicketentity,likeatitle,becomeavailableinthee-mail
HTMLtemplateas{{varticket.title}}.
WhenacustomernowsubmitstheNewTicketformfromMyAccount|Helpdesk
Tickets,theHTTPPOSTrequestwillhitthesavecontrolleractionclass.Ifthepreceding
codeissuccessfullyexecuted,theticketissavedtothedatabaseandredirectionbackto
MyAccount|HelpdeskTicketswilloccurshowingaTicketsuccessfullycreated
messageinthebrowser.
Buildingabackendinterface
Untilnow,wehavebeendealingwithsettingupgeneralmoduleconfiguration,e-mail
templates,frontendroute,frontendlayout,block,andtemplate.Whatremainstocomplete
themodulerequirementsistheadmininterface,wherethestoreownercanseesubmitted
ticketsandchangestatusesfromopentoclosed.
Thefollowingareneededforafullyfunctionaladmininterfaceaspertherequirements:
ACLresourceusedtoallowordisallowaccesstotheticketlisting
Menuitemlinkingtoticketslistingthecontrolleraction
Routethatmapstoouradmincontroller
LayoutXMLsthatmaptotheticketlistingthecontrolleraction
Controlleractionforlistingtickets
FullXMLlayoutgriddefinitionwithinlayoutXMLsdefininggrid,customcolumn
renderers,andcustomdropdownfiltervalues
Controlleractionforclosingticketsandsendinge-mailstocustomers
Linkingtheaccesscontrollistandmenu
WestartbyaddinganewACLresourceentrytothepreviouslydefined
app/code/Foggyline/Helpdesk/etc/acl.xmlfile,asachildofthe
Magento_Backend::adminresourceasfollows:
<resourceid="Magento_Customer::customer">
<resourceid="Foggyline_Helpdesk::ticket_manage"title="ManageHelpdesk
Tickets"/>
</resource>
Onitsown,thedefinedresourceentrydoesnotdoanything.Thisresourcewilllaterbe
usedwithinthemenuandcontroller.
Themenuitemlinkingtotheticketslistingthecontrolleractionisdefinedunderthe
app/code/Foggyline/Helpdesk/etc/adminhtml/menu.xmlfileasfollows:
<?xmlversion="1.0"?>
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:
Magento_Backend:etc/menu.xsd">
<menu>
<addid="Foggyline_Helpdesk::ticket_manage"title="Helpdesk
Tickets"module="Foggyline_Helpdesk"
parent="Magento_Customer::customer"
action="foggyline_helpdesk/ticket/index"
resource="Foggyline_Helpdesk::ticket_manage"/>
</menu>
</config>
Weareusingthemenu|addelementtoaddanewmenuitemundertheMagentoadmin
area.Thepositionofanitemwithintheadminareaisdefinedbytheattributeparent,
whichinourcasemeansundertheexistingCustomermenu.Iftheparentisomitted,our
itemwouldappearasanewitemonamenu.Thetitleattributevalueisthelabelwewill
seeinthemenu.Theidattributehastouniquelydifferentiateourmenuitemfromothers.
TheresourceattributereferencestheACLresourcedefinedinthe
app/code/Foggyline/Helpdesk/etc/acl.xmlfile.Ifaroleofalogged-inuserdoesnot
allowhimtousetheFoggyline_Helpdesk::ticket_manageresource,theuserwouldnot
beabletoseethemenuitem.
Creatingroutes,controllers,andlayouthandles
Nowweaddaroutethatmapstoouradmincontroller,bydefiningthe
app/code/Foggyline/Helpdesk/etc/adminhtml/routes.xmlfileasfollows:
<?xmlversion="1.0"?>
<configxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<routerid="admin">
<routeid="foggyline_helpdesk"frontName="foggyline_helpdesk">
<modulename="Foggyline_Helpdesk"/>
</route>
</router>
</config>
Theadminroutedefinitionisalmostidenticaltothefrontendrouterdefinition,wherethe
differenceprimarilyliesintherouterIDvalue,whichequalstotheadminhere.
Withtherouterdefinitioninplace,wecannowdefineourthreelayoutXMLs,underthe
app/code/Foggyline/Helpdesk/view/adminhtml/layoutdirectory,whichmaptothe
ticketlistingthecontrolleraction:
foggyline_helpdesk_ticket_grid.xml
foggyline_helpdesk_ticket_grid_block.xml
foggyline_helpdesk_ticket_index.xml
Thereasonwedefinethreelayoutfilesforasingleactioncontrollerandnotoneis
becauseofthewayweusethelistingincontrolintheMagentoadminarea.
Thecontentofthefoggyline_helpdesk_ticket_index.xmlfileisdefinedasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<pagexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout
/etc/page_configuration.xsd">
<updatehandle="formkey"/>
<updatehandle="foggyline_helpdesk_ticket_grid_block"/>
<body>
<referenceContainername="content">
<blockclass="Foggyline\Helpdesk\Block\Adminhtml\Ticket"
name="admin.block.helpdesk.ticket.grid.container">
</block>
</referenceContainer>
</body>
</page>
Twoupdatehandlesarespecified,onepullinginformkeyandtheotherpullingin
foggyline_helpdesk_ticket_grid_block.Wethenreferencethecontentcontainerand
defineanewblockoftheFoggyline\Helpdesk\Block\Adminhtml\Ticketclasswithit.
Utilizingthegridwidget
WecouldhaveusedMagento\Backend\Block\Widget\Grid\Containerasablockclass
name.However,giventhatweneededsomeextralogic,likeremovingtheAddNew
button,weoptedforacustomclassthatthenextends
\Magento\Backend\Block\Widget\Grid\Containerandaddstherequiredlogic.
TheFoggyline\Helpdesk\Block\Adminhtml\Ticketclassisdefinedunderthe
app/code/Foggyline/Helpdesk/Block/Adminhtml/Ticket.phpfileasfollows:
<?php
namespaceFoggyline\Helpdesk\Block\Adminhtml;
classTicketextends\Magento\Backend\Block\Widget\Grid\Container
{
protectedfunction_construct()
{
$this->_controller='adminhtml';
$this->_blockGroup='Foggyline_Helpdesk';
$this->_headerText=__('Tickets');
parent::_construct();
$this->removeButton('add');
}
}
NotmuchishappeningintheTicketblockclasshere.Mostimportantly,weextendfrom
\Magento\Backend\Block\Widget\Grid\Containeranddefine_controllerand
_blockGroup,astheseserveasasortofgluefortellingourgridwheretofindother
possibleblockclasses.Sincewewon’thaveanAddNewticketfeatureinadmin,weare
callingtheremoveButtonmethodtoremovethedefaultAddNewbuttonfromthegrid
container.
BacktooursecondXMLlayoutfile,thefoggyline_helpdesk_ticket_grid.xmlfile,
whichwedefineasfollows:
<?xmlversion="1.0"?>
<layoutxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout
/etc/layout_generic.xsd">
<updatehandle="formkey"/>
<updatehandle="foggyline_helpdesk_ticket_grid_block"/>
<containername="root">
<blockclass="Magento\Backend\Block\Widget\Grid\Container"
name="admin.block.helpdesk.ticket.grid.container"
template="Magento_Backend::widget/grid/container/empty.phtml"/>
</container>
</layout>
Noticehowthecontentoffoggyline_helpdesk_ticket_grid.xmlisnearlyidenticalto
thatoffoggyline_helpdesk_ticket_index.xml.Theonlydifferencebetweenthetwois
thevalueoftheblockclassandthetemplateattribute.Theblockclassisdefinedas
Magento\Backend\Block\Widget\Grid\Container,wherewepreviouslydefineditas
Foggyline\Helpdesk\Block\Adminhtml\Ticket.
Ifwelookatthecontentofthe\Magento\Backend\Block\Widget\Grid\Containerclass,
wecanseethefollowingpropertydefined:
protected$_template='Magento_Backend::widget/grid/container.phtml';
Ifwelookatthecontentofthevendor/magento/module-
backend/view/adminhtml/templates/widget/grid/container.phtmland
vendor/magento/module-
backend/view/adminhtml/templates/widget/grid/container/empty.phtmlfiles,the
differencecanbeeasilyspotted.container/empty.phtmlonlyreturnsgridHTML,
whereascontainer.phtmlreturnsbuttonsandgridHTML.
Giventhatfoggyline_helpdesk_ticket_grid.xmlwillbeahandlefortheAJAXloading
gridlistingduringsortingandfiltering,weneedittoreturnonlygridHTMLuponreload.
WenowmoveontothethirdandlargestofXML’slayoutfiles,the
app/code/Foggyline/Helpdesk/view/adminhtml/layout/foggyline_helpdesk_ticket_grid_block.xml
file.Giventhesizeofit,wewillsplititintotwocodechunksasweexplainthemoneby
one.
Thefirstpart,orinitialcontentofthefoggyline_helpdesk_ticket_grid_block.xmlfile,
isdefinedasfollows:
<?xmlversion="1.0"encoding="UTF-8"?>
<pagexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout
/etc/page_configuration.xsd">
<body>
<referenceBlockname="admin.block.helpdesk.ticket.grid.container">
<blockclass="Magento\Backend\Block\Widget\Grid"
name="admin.block.helpdesk.ticket.grid"as="grid">
<arguments>
<argumentname="id"xsi:type="string">
ticketGrid</argument>
<argumentname="dataSource"xsi:type="object">
Foggyline\Helpdesk\Model\ResourceModel\Ticket\Collection
</argument>
<argumentname="default_sort"
xsi:type="string">ticket_id</argument>
<argumentname="default_dir"
xsi:type="string">desc</argument>
<argumentname="save_parameters_in_session"
xsi:type="boolean">true</argument>
<argumentname="use_ajax"
xsi:type="boolean">true</argument>
</arguments>
<blockclass="Magento\Backend\Block\Widget\Grid\ColumnSet"
name="admin.block.helpdesk.ticket.grid.columnSet"as="grid.columnSet">
<!--Columndefinitionshere-->
</block>
</block>
</referenceBlock>
</body>
</page>
Notice<!--Columndefinitionshere-->;wewillcomebacktothatsoon.Fornow,
let’sanalyzewhatishappeninghere.Rightafterabodyelement,wehaveareferenceto
admin.block.helpdesk.ticket.grid.container,whichisacontentblockchilddefined
underthefoggyline_helpdesk_ticket_grid.xmland
foggyline_helpdesk_ticket_index.xmlfiles.Withinthisreference,wearedefining
anotherblockofclassMagento\Backend\Block\Widget\Grid,passingitanameofour
choosingandanalias.Further,thisblockhasanargumentslistandanotherblockofclass
Magento\Backend\Block\Widget\Grid\ColumnSetaschildelements.
Throughtheargumentslistwespecifythe:
id:SettothevalueofticketGrid,wecansetanyvaluewewanthere,ideally
stickingtoformula{entityname}.
dataSource:Settothevalueof
Foggyline\Helpdesk\Model\ResourceModel\Ticket\Collection,whichisthe
nameofourTicketentityresourceclass.
default_sort:Settothevalueofticket_id,whichisthepropertyoftheTicket
entitybywhichwewanttosort.
default_dir:Settothevalueofdesc,todenoteadescendingorderofsorting.This
valuefunctionstogetherwithdefault_sortasasingleunit.
save_parameters_in_session:Settotrue,thisiseasiesttoexplainusingthe
followingexample:ifwedosomesortingandfilteringontheTicketgridandthen
moveontoanotherpartoftheadminarea,thencomebacktoTicketgrid,ifthis
valueissettoyes,thegridweseewillhavethosefiltersandsortingset.
use_ajax:Settotrue,whengridfilteringandsortingistriggered,anAJAXloader
kicksinandreloadsonlythegridareaandnotthewholepage.
Rightafterthegridblocksargumentlist,wehavethegridcolumnset.Thisbringsusto
thesecondpartoffoggyline_helpdesk_ticket_grid_block.xmlcontent.Wesimply
replacethe<!--Columnshere-->commentwiththefollowing:
<blockclass="Magento\Backend\Block\Widget\Grid\Column"as="ticket_id">
<arguments>
<argumentname="header"xsi:type="string"
translate="true">ID</argument>
<argumentname="type"xsi:type="string">number</argument>
<argumentname="id"xsi:type="string">ticket_id</argument>
<argumentname="index"xsi:type="string">ticket_id</argument>
</arguments>
</block>
<blockclass="Magento\Backend\Block\Widget\Grid\Column"as="title">
<arguments>
<argumentname="header"xsi:type="string"
translate="true">Title</argument>
<argumentname="type"xsi:type="string">string</argument>
<argumentname="id"xsi:type="string">title</argument>
<argumentname="index"xsi:type="string">title</argument>
</arguments>
</block>
<blockclass="Magento\Backend\Block\Widget\Grid\Column"|as="severity">
<arguments>
<argumentname="header"xsi:type="string"
translate="true">Severity</argument>
<argumentname="index"xsi:type="string">severity</argument>
<argumentname="type"xsi:type="string">options</argument>
<argumentname="options"xsi:type="options"
model="Foggyline\Helpdesk\Model\Ticket\Grid\Severity"/>
<argumentname="renderer"xsi:type="string">
Foggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer\Severity
</argument>
<argumentname="header_css_class"xsi:type="string">col-
form_id</argument>
<argumentname="column_css_class"xsi:type="string">col-
form_id</argument>
</arguments>
</block>
<blockclass="Magento\Backend\Block\Widget\Grid\Column"as="status">
<arguments>
<argumentname="header"xsi:type="string"
translate="true">Status</argument>
<argumentname="index"xsi:type="string">status</argument>
<argumentname="type"xsi:type="string">options</argument>
<argumentname="options"xsi:type="options"
model="Foggyline\Helpdesk\Model\Ticket\Grid\Status"/>
<argumentname="renderer"xsi:type="string">
Foggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer\Status
</argument>
<argumentname="header_css_class"xsi:type="string">col-
form_id</argument>
<argumentname="column_css_class"xsi:type="string">col-
form_id</argument>
</arguments>
</block>
<blockclass="Magento\Backend\Block\Widget\Grid\Column"as="action">
<arguments>
<argumentname="id"xsi:type="string">action</argument>
<argumentname="header"xsi:type="string"
translate="true">Action</argument>
<argumentname="type"xsi:type="string">action</argument>
<argumentname="getter"xsi:type="string">getId</argument>
<argumentname="filter"xsi:type="boolean">false</argument>
<argumentname="sortable"xsi:type="boolean">false</argument>
<argumentname="actions"xsi:type="array">
<itemname="view_action"xsi:type="array">
<itemname="caption"xsi:type="string"
translate="true">Close</item>
<itemname="url"xsi:type="array">
<itemname="base"xsi:type="string">*/*/close</item>
</item>
<itemname="field"xsi:type="string">id</item>
</item>
</argument>
<argumentname="header_css_class"xsi:type="string">col-
actions</argument>
<argumentname="column_css_class"xsi:type="string">col-
actions</argument>
</arguments>
</block>
Similartogrid,columndefinitionsalsohaveargumentsthatdefineitslookandbehavior:
header:Mandatory,thevaluewewanttoseeasalabelontopofthecolumn.
type:Mandatory,canbeanythingfrom:date,datetime,text,longtext,options,
store,number,currency,skip-list,wrapline,andcountry.
id:Mandatory,auniquevaluethatidentifiesourcolumn,preferablymatchingthe
nameoftheentityproperty.
index:Mandatory,thedatabasecolumnname.
options:Optional,ifweareusingatypelikeoptions,thenfortheoptionsargument
weneedtospecifytheclasslike
Foggyline\Helpdesk\Model\Ticket\Grid\Severitythatimplements
\Magento\Framework\Option\ArrayInterface,meaningitprovidesthe
toOptionArraymethodthatthenfillsthevaluesofoptionsduringgridrendering.
renderer:Optional,asourTicketentitiesstoreseverityandstatusasintegervalues
inthedatabase,columnswouldrenderthoseintegervaluesintocolumns,whichis
notreallyuseful.Wewanttoturnthoseintegervaluesintolabels.Inordertodoso,
weneedtorewritetherenderingbitofasingletablecell,whichwedowiththehelp
oftherendererargument.Thevaluewepasstoit,
Foggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer\Severity,needs
tobeaclassthatextends
\Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRendererand
doesitsownimplementationoftherendermethod.
header_css_class:Optional,ifweprefertospecifyacustomheaderclass.
column_css_class:Optional,ifweprefertospecifyacustomcolumnclass.
Creatingagridcolumnrenderer
TheFoggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer\Severityclass,
definedinthe
app/code/Foggyline/Helpdesk/Block/Adminhtml/Ticket/Grid/Renderer/Severity.php
file,isasfollows:
<?php
namespaceFoggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer;
classSeverityextends\Magento\Backend\Block\Widget\Grid
\Column\Renderer\AbstractRenderer
{
protected$ticketFactory;
publicfunction__construct(
\Magento\Backend\Block\Context$context,
\Foggyline\Helpdesk\Model\TicketFactory$ticketFactory,
array$data=[]
)
{
parent::__construct($context,$data);
$this->ticketFactory=$ticketFactory;
}
publicfunctionrender(\Magento\Framework\DataObject$row)
{
$ticket=$this->ticketFactory->create()->load($row->getId());
if($ticket&&$ticket->getId()){
return$ticket->getSeverityAsLabel();
}
return'';
}
}
Here,wearepassingtheinstanceoftheticketfactorytotheconstructorandthenusing
thatinstancewithintherendermethodtoloadaticketbasedontheIDvaluefetchedfrom
thecurrentrow.Giventhat$row->getId()returnstheIDoftheticket,thisisaniceway
toreloadtheentireticketentityandthenfetchthefulllabelfromtheticketmodelbyusing
$ticket->getSeverityAsLabel().Whateverstringwereturnfromthismethodiswhat
willbeshownunderthegridrow.
Anotherrendererclassthatisreferencedwithinthe
foggyline_helpdesk_ticket_grid_block.xmlfileis
Foggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer\Status,andwedefine
itscontentunderthe
app/code/Foggyline/Helpdesk/Block/Adminhtml/Ticket/Grid/Renderer/Status.php
fileasfollows:
<?php
namespaceFoggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer;
classStatusextends\Magento\Backend\Block\Widget\Grid\Column
\Renderer\AbstractRenderer
{
protected$ticketFactory;
publicfunction__construct(
\Magento\Backend\Block\Context$context,
\Foggyline\Helpdesk\Model\TicketFactory$ticketFactory,
array$data=[]
)
{
parent::__construct($context,$data);
$this->ticketFactory=$ticketFactory;
}
publicfunctionrender(\Magento\Framework\DataObject$row)
{
$ticket=$this->ticketFactory->create()->load($row->getId());
if($ticket&&$ticket->getId()){
return$ticket->getStatusAsLabel();
}
return'';
}
}
Giventhatittooisusedforarenderer,thecontentoftheStatusclassisnearlyidenticalto
thecontentoftheSeverityclass.Wepassontheticketfactoryobjectviatheconstructor,
sowehaveitinternallyforusagewithintherendermethod.ThenweloadtheTicket
entityusingtheticketfactoryandIDvaluefetchedfroma$rowobject.Asaresult,the
columnwillcontainthelabelvalueofastatusandnotitsintegervalue.
Creatinggridcolumnoptions
Besidesreferencingrendererclasses,ourfoggyline_helpdesk_ticket_grid_block.xml
filealsoreferencestheoptionsclassfortheSeverityfield.
WedefinetheFoggyline\Helpdesk\Model\Ticket\Grid\Severityoptionsclassunder
theapp/code/Foggyline/Helpdesk/Model/Ticket/Grid/Severity.phpfileasfollows:
<?php
namespaceFoggyline\Helpdesk\Model\Ticket\Grid;
classSeverityimplements\Magento\Framework\Option\ArrayInterface
{
publicfunctiontoOptionArray()
{
return\Foggyline\Helpdesk\Model
\Ticket::getSeveritiesOptionArray();
}
}
TheoptionsvaluefromXMLlayoutsreferstoaclassthathastoimplementthe
toOptionArraymethod,whichreturnsanarrayofarrays,suchasthefollowingexample:
return[
['value'=>'theValue1','theLabel1'],
['value'=>'theValue2','theLabel2'],
];
OurSeverityclasssimplycallsthestaticmethodwehavedefinedontheTicketclass,
thegetSeveritiesOptionArray,andpassesalongthosevalues.
Creatingcontrolleractions
Uptothispoint,wehavedefinedthemenuitem,ACLresource,XMLlayouts,block,
optionsclass,andrendererclasses.Whatremainstoconnectitallarecontrollers.We
willneedthreecontrolleractions(Index,Grid,andClose),allextendingfromthesame
adminTicketcontroller.
WedefinetheadminTicketcontrollerunderthe
app/code/Foggyline/Helpdesk/Controller/Adminhtml/Ticket.phpfileasfollows:
<?php
namespaceFoggyline\Helpdesk\Controller\Adminhtml;
classTicketextends\Magento\Backend\App\Action
{
protected$resultPageFactory;
protected$resultForwardFactory;
protected$resultRedirectFactory;
publicfunction__construct(
\Magento\Backend\App\Action\Context$context,
\Magento\Framework\View\Result\PageFactory$resultPageFactory,
\Magento\Backend\Model\View\Result\ForwardFactory
$resultForwardFactory
)
{
$this->resultPageFactory=$resultPageFactory;
$this->resultForwardFactory=$resultForwardFactory;
$this->resultRedirectFactory=$context->
getResultRedirectFactory();
parent::__construct($context);
}
protectedfunction_isAllowed()
{
return$this->_authorization->
isAllowed('Foggyline_Helpdesk::ticket_manage');
}
protectedfunction_initAction()
{
$this->_view->loadLayout();
$this->_setActiveMenu(
'Foggyline_Helpdesk::ticket_manage'
)->_addBreadcrumb(
__('Helpdesk'),
__('Tickets')
);
return$this;
}
}
Thereareafewthingstonotehere.$this->resultPageFactory,$this-
>resultForwardFactoryand$this->resultRedirectFactoryareobjectstobeusedon
thechild(Index,Grid,andClose),sowedonothavetoinitiatethemineachchildclass
separately.
The_isAllowed()methodisextremelyimportanteverytimewehaveacustom-defined
controllerorcontrolleractionthatwewanttocheckagainstourcustomACLresource.
Here,wearetheisAllowedmethodcallonthe
\Magento\Framework\AuthorizationInterfacetypeofobject($this-
>_authorization).TheparameterpassedtotheisAllowedmethodcallshouldbetheID
valueofourcustomACLresource.
Wethenhavethe_initActionmethod,whichisusedforsettinguplogicsharedacross
childclasses,usuallythingslikeloadingtheentirelayout,settinguptheactivemenuflag,
andaddingbreadcrumbs.
Movingforward,wedefinetheIndexcontrolleractionwithinthe
app/code/Foggyline/Helpdesk/Controller/Adminhtml/Ticket/Index.phpfileas
follows:
<?php
namespaceFoggyline\Helpdesk\Controller\Adminhtml\Ticket;
classIndexextends\Foggyline\Helpdesk\Controller\Adminhtml\Ticket
{
publicfunctionexecute()
{
if($this->getRequest()->getQuery('ajax')){
$resultForward=$this->resultForwardFactory->create();
$resultForward->forward('grid');
return$resultForward;
}
$resultPage=$this->resultPageFactory->create();
$resultPage->setActiveMenu('Foggyline_Helpdesk::ticket_manage');
$resultPage->getConfig()->getTitle()->prepend(__('Tickets'));
$resultPage->addBreadcrumb(__('Tickets'),__('Tickets'));
$resultPage->addBreadcrumb(__('ManageTickets'),__('Manage
Tickets'));
return$resultPage;
}
}
Controlleractionsexecutewithintheirownclass,withintheexecutemethod.Our
executemethodfirstchecksifthecomingrequestistheAJAXparameterwithinit.If
thereisanAJAXparameter,therequestisforwardedtotheGridactionofthesame
controller.
IfthereisnoAJAXcontroller,wesimplycreatetheinstanceofthe
\Magento\Framework\View\Result\PageFactoryobject,andsettitle,activemenuitem,
andbreadcrumbsinit.
Alogicalquestionatthispointwouldbehowdoesallofthisworkandwherecanwesee
it.IfwelogintotheMagentoadminarea,undertheCustomersmenuweshouldbeable
toseetheHelpdeskTicketsmenuitem.Thisitem,definedpreviouslywithin
app/code/Foggyline/Helpdesk/etc/adminhtml/menu.xml,saysthemenuaction
attributeequalstofoggyline_helpdesk/ticket/index,whichbasicallytranslatestothe
IndexactionofourTicketcontroller.
OnceweclickontheHelpdeskTicketslink,MagentowillhittheIndexactionwithinits
TicketcontrollerandtrytofindtheXMLfilethathasthematchingroute{id}+
{controllername}+{controlleractionname}+{xmlfileextension},whichinourcase
translatesto{foggyline_helpdesk}+{ticket}+{index}+{.xml}.
Atthispoint,weshouldbeabletoseethescreen,asshowninthefollowingscreenshot:
However,ifwenowtrytousesortingorfiltering,wewouldgetabrokenlayout.Thisis
becausebasedonargumentsdefinedunderthe
foggyline_helpdesk_ticket_grid_block.xmlfile,wearemissingthecontrollerGrid
action.Whenweusesortingorfiltering,theAJAXrequesthitstheIndexcontrollerand
askstobeforwardedtotheGridaction,whichwehaven’tdefinedyet.
WenowdefinetheGridactionwithinthe
app/code/Foggyline/Helpdesk/Controller/Adminhtml/Ticket/Grid.phpfileas
follows:
<?php
namespaceFoggyline\Helpdesk\Controller\Adminhtml\Ticket;
classGridextends\Foggyline\Helpdesk\Controller\Adminhtml\Ticket
{
publicfunctionexecute()
{
$this->_view->loadLayout(false);
$this->_view->renderLayout();
}
}
ThereisonlyoneexecutemethodwiththeGridcontrolleractionclass,whichis
expected.ThecodewithintheexecutemethodsimplycallstheloadLayout(false)
methodtopreventtheentirelayoutloading,makingitloadonlythebitsdefinedunderthe
foggyline_helpdesk_ticket_grid.xmlfile.ThiseffectivelyreturnsthegridHTMLto
theAJAX,whichrefreshesthegridonthepage.
Finally,weneedtohandletheCloseactionlinkweseeonthegrid.Thislinkwasdefined
aspartofthecolumndefinitionwithinthefoggyline_helpdesk_ticket_grid_block.xml
fileandpointsto*/*/close,whichtranslatesto“routerasrelativefromcurrentURL/
controllerasrelativefromcurrentURL/closeaction”,whichfurtherequalstoourTicket
controllerCloseaction.
WedefinetheClosecontrolleractionunderthe
app/code/Foggyline/Helpdesk/Controller/Adminhtml/Ticket/Close.phpfileas
follows:
<?php
namespaceFoggyline\Helpdesk\Controller\Adminhtml\Ticket;
classCloseextends\Foggyline\Helpdesk\Controller\Adminhtml\Ticket
{
protected$ticketFactory;
protected$customerRepository;
protected$transportBuilder;
protected$inlineTranslation;
protected$scopeConfig;
protected$storeManager;
publicfunction__construct(
\Magento\Backend\App\Action\Context$context,
\Magento\Framework\View\Result\PageFactory$resultPageFactory,
\Magento\Backend\Model\View\Result\ForwardFactory
$resultForwardFactory,
\Foggyline\Helpdesk\Model\TicketFactory$ticketFactory,
\Magento\Customer\Api\CustomerRepositoryInterface
$customerRepository,
\Magento\Framework\Mail\Template\TransportBuilder
$transportBuilder,
\Magento\Framework\Translate\Inline\StateInterface
$inlineTranslation,
\Magento\Framework\App\Config\ScopeConfigInterface$scopeConfig,
\Magento\Store\Model\StoreManagerInterface$storeManager
)
{
$this->ticketFactory=$ticketFactory;
$this->customerRepository=$customerRepository;
$this->transportBuilder=$transportBuilder;
$this->inlineTranslation=$inlineTranslation;
$this->scopeConfig=$scopeConfig;
$this->storeManager=$storeManager;
parent::__construct($context,$resultPageFactory,
$resultForwardFactory);
}
publicfunctionexecute()
{
$ticketId=$this->getRequest()->getParam('id');
$ticket=$this->ticketFactory->create()->load($ticketId);
if($ticket&&$ticket->getId()){
try{
$ticket->setStatus(\Foggyline
\Helpdesk\Model\Ticket::STATUS_CLOSED);
$ticket->save();
$this->messageManager->addSuccess(__('Ticketsuccessfully
closed.'));
/*Sendemailtocustomer*/
$customer=$this->customerRepository->getById($ticket-
>getCustomerId());
$storeScope=\Magento\Store\Model\
ScopeInterface::SCOPE_STORE;
$transport=$this->transportBuilder
->setTemplateIdentifier($this->scopeConfig->
getValue('foggyline_helpdesk/email_template/customer',$storeScope))
->setTemplateOptions(
[
'area'=>
\Magento\Framework\App\Area::AREA_ADMINHTML,
'store'=>$this->storeManager->getStore()-
>getId(),
]
)
->setTemplateVars([
'ticket'=>$ticket,
'customer_name'=>$customer->getFirstname()
])
->setFrom([
'name'=>$this->scopeConfig->
getValue('trans_email/ident_general/name',$storeScope),
'email'=>$this->scopeConfig->
getValue('trans_email/ident_general/email',$storeScope)
])
->addTo($customer->getEmail())
->getTransport();
$transport->sendMessage();
$this->inlineTranslation->resume();
$this->messageManager->addSuccess(__('Customernotifiedvia
email.'));
}catch(Exception$e){
$this->messageManager->addError(__('Errorwithclosing
ticketaction.'));
}
}
$resultRedirect=$this->resultRedirectFactory->create();
$resultRedirect->setPath('*/*/index');
return$resultRedirect;
}
}
TheCloseactioncontrollerhastwoseparaterolestofulfill.Oneistochangetheticket
status;theotheristosendane-mailtothecustomerusingthepropere-mailtemplate.The
classconstructorisbeingpassedalotofparametersthatallinstantiatetheobjectswewill
bejugglingaround.
Withintheexecuteaction,wefirstcheckfortheexistenceoftheidparameterandthentry
toloadaTicketentitythroughtheticketfactory,basedontheprovidedIDvalue.Ifthe
ticketexists,wesetitsstatuslabelto
\Foggyline\Helpdesk\Model\Ticket::STATUS_CLOSEDandsaveit.
Followingtheticketsaveisthee-mail-sendingcode,whichisverysimilartotheonethat
wealreadysawinthecustomerNewTicketsaveaction.Thedifferenceisthatthee-mail
goesoutfromtheadminusertothecustomerthistime.WearesettingthetemplateIDto
theconfigurationvalueatpathfoggyline_helpdesk/email_template/customer.The
setTemplateVarsmethodispassedtothememberarraythistime,bothticketand
customer_name,astheyarebothusedinthee-mailtemplate.ThesetFrommethodis
passedthegeneralstoreusernameande-mail,andthesendMessagemethodiscalledon
thetransportobject.
Finally,usingtheresultRedirectFactoryobject,theuserisredirectedbacktothetickets
grid.
Withthis,wefinalizeourmodulefunctionalrequirement.
Thoughwearedonewiththefunctionalrequirementofamodule,whatremainsforusas
developersistowritetests.Thereareseveraltypesoftests,suchasunit,functional,
integration,andsoon.Tokeepthingssimple,withinthischapterwewillcoveronlyunit
testsacrossasinglemodelclass.
Creatingunittests
ThischapterassumesthatwehavePHPUnitconfiguredandavailableonthecommand
line.Ifthisisnotthecase,PHPUnitcanbeinstalledusinginstructionsfromthe
https://phpunit.de/website.
TobuildandruntestsusingthePHPUnittestingframework,weneedtodefinetest
locationsandotherconfigurationoptionsviaanXMLfile.MagentodefinesthisXML
configurationfileunderdev/tests/unit/phpunit.xml.dist.Let’smakeacopyofthat
fileunderdev/tests/unit/phpunit-foggyline-helpdesk.xml,withadjustmentsas
follows:
<?xmlversion="1.0"encoding="UTF-8"?>
<phpunitxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
colors="true"
bootstrap="./framework/bootstrap.php"
>
<testsuitename="Foggyline_Helpdesk-UnitTests">
<directorysuffix="Test.php">
../../../app/code/Foggyline/Helpdesk/Test/Unit</directory>
</testsuite>
<php>
<ininame="date.timezone"value="Europe/Zagreb"/>
<ininame="xdebug.max_nesting_level"value="200"/>
</php>
<filter>
<whitelistaddUncoveredFilesFromWhiteList="true">
<directorysuffix=".php">
../../../app/code/Foggyline/Helpdesk/*</directory>
<exclude>
<directory>../../../app/code/Foggyline/Form/Helpdesk
</directory>
</exclude>
</whitelist>
</filter>
<logging>
<logtype="coverage-html"
target="coverage_dir/Foggyline_Helpdesk/test-reports/coverage"
charset="UTF-8"yui="true"highlight="true"/>
</logging>
</phpunit>
Tip
WearemakingaspecialXMLconfigurationfileforourmodulealonebecausewewantto
quicklyrunafewofthetestscontainedwithinourmodulealoneandnottheentire
Magentoapp/codefolder.
Giventhattheactualartofwritingunittestsisbeyondthescopeofthisbookandwriting
thefullunittestwith100percentcodecoverageforthissimplemodulewouldrequireat
leastadozenmorepages,wewillonlywriteasingletest,onethatcoverstheTicket
entitymodelclass.
WedefineourTicketentitymodelclasstestunderthe
app/code/Foggyline/Helpdesk/Test/Unit/Model/TicketTest.phpfileasfollows:
<?php
namespaceFoggyline\Helpdesk\Test\Unit\Model;
classTicketTestextends\PHPUnit_Framework_TestCase
{
protected$objectManager;
protected$ticket;
publicfunctionsetUp()
{
$this->objectManager=new
\Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
$this->ticket=$this->objectManager->
getObject('Foggyline\Helpdesk\Model\Ticket');
}
publicfunctiontestGetSeveritiesOptionArray()
{
$this->assertNotEmpty(\Foggyline
\Helpdesk\Model\Ticket::getSeveritiesOptionArray());
}
publicfunctiontestGetStatusesOptionArray()
{
$this->assertNotEmpty(\Foggyline
\Helpdesk\Model\Ticket::getStatusesOptionArray());
}
publicfunctiontestGetStatusAsLabel()
{
$this->ticket->setStatus(\Foggyline\Helpdesk
\Model\Ticket::STATUS_CLOSED);
$this->assertEquals(
\Foggyline\Helpdesk\Model\Ticket::$statusesOptions
[\Foggyline\Helpdesk\Model\Ticket::STATUS_CLOSED],
$this->ticket->getStatusAsLabel()
);
}
publicfunctiontestGetSeverityAsLabel()
{
$this->ticket->setSeverity(\Foggyline
\Helpdesk\Model\Ticket::SEVERITY_MEDIUM);
$this->assertEquals(
\Foggyline\Helpdesk\Model\Ticket::$severitiesOptions
[\Foggyline\Helpdesk\Model\Ticket::SEVERITY_MEDIUM],
$this->ticket->getSeverityAsLabel()
);
}
}
Thelocationoftestfilesshouldmapthoseofthefilesbeingtested.Thenamingofthetest
fileshouldalsofollowthenamingofthefilebeingtestedwiththesuffixTestattachedto
it.ThismeansthatifourTicketmodelislocatedunderthemodulesModel/Ticket.php
file,thenourtestshouldbelocatedunderTest/Unit/TicketTest.php.
OurFoggyline\Helpdesk\Test\Unit\Model\TicketTestextendsthe
\PHPUnit_Framework_TestCaseclass.ThereisasetUpmethodweneedtodefine,which
actslikeaconstructor,wherewesetupthevariablesandeverythingthatrequires
initializing.
UsingMagentoObjectManager,weinstantiatetheTicketmodel,whichisthenused
withinthetestmethods.Theactualtestmethodsfollowasimplenamingpattern,where
thenameofthemethodfromtheTicketmodelmatchesthe{test}+{methodname}from
theTicketTestclass.
Wedefinedfourtestmethods:testGetSeveritiesOptionArray,
testGetStatusesOptionArray,testGetStatusAsLabel,andtestGetSeverityAsLabel.
Withinthetestmethods,weareusingonlyassertEqualsandassertNotEmptymethods
fromthePHPUnittestingframeworklibrarytodobasicchecks.
Wecannowopenaconsole,changethedirectorytoourMagentoinstallationdirectory,
andexecutethefollowingcommand:
phpunit-cdev/tests/unit/phpunit-foggyline-helpdesk.xml
Afterthecommandexecutes,theconsoleshouldshowanoutputasshown:
PHPUnit4.7.6bySebastianBergmannandcontributors…..
Time:528ms,Memory:11.50Mb
OK(4tests,4assertions)
GeneratingcodecoveragereportinHTMLformat…done
Lookingbackatourdev/tests/unit/phpunit-foggyline-helpdesk.xmlfile,underthe
targetattributeofthephpunit>logging>logelement,wecanseethatthetestreportis
dumpedintothecoverage_dir/Foggyline_Helpdesk/test-reports/coveragefolder
relativetotheXMLfile.
Ifweopenthedev/tests/unit/coverage_dir/Foggyline_Helpdesk/test-
reports/coverage/folder,weshouldseeawholelotoffilesgeneratedthere,asshownin
thefollowingscreenshot:
Openingtheindex.htmlfileinthebrowsershouldgiveusapageasshowninthe
followingscreenshot:
Wecanseethecodecoveragereportshowing60%onlinesandmethodsforourModel
folderand0%fortherest.ThisisbecauseweonlywrotethetestfortheTicketentity
modelclass,whereastherestremainuntested.
Summary
Thischaptergaveafullstep-by-stepguidetowritingasimpleyetfunctionalMagento
module.Seeminglysimpleintermsoffunctionality,wecanseethatthemodulecodeis
significantlyscatteredacrossmultiplePHP,XML,andPHMTLfiles.
Withthissimplemodule,wecoveredquitealotofvariousMagentoplatformparts,from
routes,ACLs,controllers,blocks,XMLlayouts,grids,controlleractions,models,
resources,collections,installscripts,interactionswithsession,e-mailtemplates,e-mail
transport,andlayoutobjects.
Attheend,wewroteafewsimpleunittestsforourmodels.Althoughthepracticeisto
writeunittestsforallofourPHPcode,weoptedforashorterversionorelsewewould
needmorepagestocovereverything.
Thefullmodulecodeisavailablehere:https://github.com/ajzele/B05032-
Foggyline_Helpdesk.
Withthisbeingthelastchapter,letuslookatashortoverviewofthethingswelearned
throughoutthewholebook.OurjourneystartedbygraspingtheMagentoplatform
architecture,wherewegainedsignificantinsightintothetechnologystackbehindit.We
thenprogressedtoenvironmentmanagement.Althoughitmightseemlikeawrongorder
ofthings,weoptedforthisnextstepinordertoquicklygetussetfordevelopment.We
thenlookedintoprogrammingconceptsandconventions,whichservedasaprecursorto
actualhands-ondevelopmentbits.Detailsofentitypersistencewereshownthrough
model,resource,collectionclasses,andindexers.Wefurthercoveredtheimportanceand
practicaldetailsofdependencyinjectionandinterception.Backendandfrontend-related
developmentwascoveredintheirowntwochapters,outliningthemostcommonbitsand
piecesformakingcustomizationstoourMagentoplatform.Wethendugintodetailsofthe
webAPI,showinghowtomakeauthenticatedAPIcallsandevendefineourownAPIs.
Alongtheway,wecoveredafewmajorfunctionalareasaswell,suchascustomers,
reports,importexport,cart,andsoon.ThetestingandQAtookupasignificantchunkas
webrieflycoveredallformsofavailabletests.Finally,weusedwhatwelearnedtobuilda
fullyfunctionalmodule.
Althoughwehavecoveredasignificantpathonourjourney,thisismerelyafirststep.
Givenitsmassivecodebase,diversetechnologystacks,andfeaturelist,Magentoisnotan
easyplatformtomaster.Hopefully,thisbookwillgiveenoughincentivetotakefurther
stepsintoprofilingourselvesastrueMagentoexperts.
Index
A
accesscontrollist(ACL)
about/Usertypes
accesscontrollists(acl.xml)
creating/Creatingaccesscontrollists(acl.xml)
afterlistener
using/Usingtheafterlistener
AmazonElasticComputeCloud(AmazonEC2)
about/IntroductiontoAmazonWebServices
AmazonWebServices(AWS)/IntroductiontoAmazonWebServices
AngularJS
URL/JavaScript
ApacheJMeter
URL/Performancetesting
APIcallexamples
getByIdservicemethodcallexamples/ThegetByIdservicemethodcall
examples
getListservicemethodcallexamples/ThegetListservicemethodcallexamples
save(asnew)servicemethodcallexamples/Thesave(asnew)servicemethod
callexamples
save(asupdate)servicemethodcallexamples/Thesave(asupdate)service
methodcallexamples
deleteByIdservicemethodcallexamples/ThedeleteByIdservicemethodcall
examples
references/ThedeleteByIdservicemethodcallexamples
Apt
about/Composer
aroundlistener
using/Usingthearoundlistener
authenticationmethods
defining/Authenticationmethods
token-basedauthentication/Authenticationmethods
OAuth-basedauthentication/Authenticationmethods
session-basedauthentication/Authenticationmethods
AWSmanagementconsole
URL/IntroductiontoAmazonWebServices
B
backendinterface
building/Buildingabackendinterface
accesscontrollistandmenu,linking/Linkingtheaccesscontrollistandmenu
routes,creating/Creatingroutes,controllers,andlayouthandles
controllers,creating/Creatingroutes,controllers,andlayouthandles
layouthandles,creating/Creatingroutes,controllers,andlayouthandles
gridwidget,utilizing/Utilizingthegridwidget
gridcolumnrenderer,creating/Creatingagridcolumnrenderer
gridcolumnoptions,creating/Creatinggridcolumnoptions
controlleractions,creating/Creatingcontrolleractions
BearerHTTPauthorizationscheme
about/RESTversusSOAP
beforelistener
using/Usingthebeforelistener
blocks
about/Blocks
C
cache(s)
defining/Cache(s)
catalogmanagement
about/Catalogmanagement
categories,managingmanually/Managingcategoriesmanually
categories,managingviacode/Managingcategoriesviacode
categories,managingviaAPI/ManagingcategoriesviaAPI
products,managingmanually/Managingproductsmanually
products,managingviacode/Managingproductsviacode
products,managingviaAPI/ManagingproductsviaAPI
classpreferences
configuring/Configuringclasspreferences
CMS(contentmanagementsystem)
about/Thearchitecturallayers
CMSmanagement
defining/CMSmanagement
blocks,managingmanually/Managingblocksmanually
blocks,managingviacode/Managingblocksviacode
blocks,managingviaAPI/ManagingblocksviaAPI
pages,managingmanually/Managingpagesmanually
pages,managingviacode/Managingpagesviacode
pages,managingviaAPI/ManagingpagesviaAPI
codedemarcation
about/Codingstandards
codegeneration
about/Codegeneration
codingstandards
about/Codingstandards
collectionfilters
about/Collectionfilters
collections
managing/Managingcollections
components,opensourcetechnologies
PHP/Thetechnologystack
Codingstandards/Thetechnologystack
Composer/Thetechnologystack
HTML/Thetechnologystack
CSS/Thetechnologystack
jQuery/Thetechnologystack
RequireJS/Thetechnologystack
third-partylibraries/Thetechnologystack
ApacheorNginx/Thetechnologystack
MySQL/Thetechnologystack
MTF/Thetechnologystack
Composer
defining/Composer
URL/Composer
references/Composer
configurationfile(config.xml)
creating/Creatingaconfigurationfile(config.xml)
cookies
about/Sessionandcookies
Cronjobs
defining/Cronjobs
Cross-SiteRequestForgery(CSRF)
about/Creatingblocksandtemplates
CRUD(create,read,update,anddelete)
about/Thearchitecturallayers
CSVfiles
defining/i18n
customermanagement
about/Customermanagement
customers,managingmanually/Managingcustomersmanually
customers,managingviacode/Managingcustomersviacode
customers,managingviaAPI/ManagingcustomersviaanAPI
customeraddress,managingviacode/Managingcustomeraddressviacode
customeraddress,managingviaAPI/ManagingcustomersaddressviaanAPI
customofflinepaymentmethods
defining/Customofflinepaymentmethods
URL/Customofflinepaymentmethods
customofflineshippingmethods
defining/Customofflineshippingmethods
URL/Customofflineshippingmethods
customproducttypes
defining/Thecustomproducttypes
URL/Thecustomproducttypes
customvariables
defining/Customvariables
customWebAPIs
creating/CreatingcustomWebAPIs
APIcallexamples/APIcallexamples
D
datainterfaces
about/Servicecontracts
datascripts
defining/Understandingtheflowofschemaanddatascripts
dependencyinjection
about/Dependencyinjection
developmentenvironment
settingup/Settingupadevelopmentenvironment
VirtualBox/VirtualBox
Vagrant/Vagrant
Vagrantproject/Vagrantproject
DNS
settingup/SettingupElasticIPandDNS
DocBlockstandard
about/Codingstandards
E
e-mailtemplates(email_templates.xml)
creating/Creatinge-mailtemplates(email_templates.xml)
EC2instance
settingup/SettingupEC2
ElasticIP
settingup/SettingupElasticIPandDNS
ElasticIPaddress
about/SettingupElasticIPandDNS
Entity-Attribute-Value(EAV)
about/Thearchitecturallayers
entityCRUDactions
defining/EntityCRUDactions
references/EntityCRUDactions
entitypersistence(Model,Resource,Collection)
managing/Managingentitypersistence(model,resource,collection)
events
defining/Eventsandobservers
static/Eventsandobservers
dynamic/Eventsandobservers
existingentities
reading/Readingexistingentities
updating/Updatingexistingentities
deleting/Deletingexistingentities
ExtJS
URL/JavaScript
F
factories
about/Codegeneration
flow
rendering/Renderingflow
Foggyline_Unitlymodule
URL/Writingasimpleunittest
frontendinterface
building/Buildingafrontendinterface
routes,creating/Creatingroutes,controllers,andlayouthandles
controllers,creating/Creatingroutes,controllers,andlayouthandles
layouthandles,creating/Creatingroutes,controllers,andlayouthandles
blocks,creating/Creatingblocksandtemplates
templates,creating/Creatingblocksandtemplates
formsubmissions,handling/Handlingformsubmissions
functionaltesting
defining/Functionaltesting
I
i18n(internationalization)
defining/i18n
IAMgroups
creating/CreatingIAMgroups
IAMusers
creating/CreatingIAMusers
IdentityandAccessManagement(IAM)
about/SettingupaccessforS3usage
indexing
about/Indexer(s)
inlinetranslation
about/i18n
installationscript(InstallSchema.php)
creating/Creatinganinstallationscript(InstallSchema.php)
installdatascript(InstallData.php)
creating/Creatinganinstalldatascript(InstallData.php)
installschemascript(InstallSchema.php)
creating/Creatinganinstallschemascript(InstallSchema.php)
integrateddevelopmentenvironments(IDEs)
about/Codegeneration
integrationtesting
defining/Integrationtesting
integritytesting
defining/Integritytesting
J
jasmine
URL/JavaScript
JavaScript
about/JavaScript
customJScomponent,creating/CreatingacustomJScomponent
JavaScriptcodingstandard
URL/Codingstandards
JavaScriptcomponent(JScomponent)
about/JavaScript
JavaScriptDocBlockstandard
about/Codingstandards
URL/Codingstandards
jQuery
URL/JavaScript
jQueryUI
URL/JavaScript
jQueryUIwidget
about/JavaScript
jQuerywidget
about/JavaScript
jQuerywidgetcodingstandard
about/Codingstandards
M
Magento
architecturallayers/Thearchitecturallayers
presentationlayer/Thearchitecturallayers
servicelayer/Thearchitecturallayers
domainlayer/Thearchitecturallayers
persistencelayer/Thearchitecturallayers
top-levelfilesystemstructure/Thetop-levelfilesystemstructure
modulefilesystemstructure/Themodulefilesystemstructure
MagentoTestingFramework(MTF)
about/Thetechnologystack,Functionaltesting
URL/Thetechnologystack,Functionaltesting
requirements/Functionaltesting
miniaturemodule
creating/Creatingaminiaturemodule
simplemodel,creating/Creatingasimplemodel
EAVmodel,creating/CreatinganEAVmodel
modernizr
URL/JavaScript
module
registering/Registeringamodule
modulerequirements
defining/Modulerequirements
moment.js
URL/JavaScript
O
OAuth-basedauthentication
defining/Hands-onwithOAuth-basedauthentication
OAuth-basedWebAPIcalls
defining/OAuth-basedWebAPIcalls
OAuth1.0ahandshakeprocess
about/Authenticationmethods
OAuthclient
URL/Hands-onwithOAuth-basedauthentication
objectmanager
about/Theobjectmanager
observers
defining/Eventsandobservers
P
performancetesting
defining/Performancetesting
PHP
URL/Codingstandards
PHPcodingstandard
about/Codingstandards
PHPOOP
about/Thetechnologystack
PhpStorm
about/Layouts
PHPUnit
URL/Creatingunittests
PHPUnittestingframework
about/Unittesting
plugin
creating/Creatingaplugin
pluginsortorder
about/Thepluginsortorder
productionenvironment
settingup/Settingupaproductionenvironment
AmazonWebServices(AWS)/IntroductiontoAmazonWebServices
access,settingupforS3usage/SettingupaccessforS3usage
S3,settingupfordatabaseandmediafilesbackup/SettingupS3fordatabase
andmediafilesbackup
bashscript,forautomatedEC2setup/BashscriptforautomatedEC2setup
productsandcustomersImport
defining/Productsandcustomersimport
profiler
about/Theprofiler
enabling/Theprofiler
defining/Theprofiler
Prototype
URL/JavaScript
S
schemaflow
defining/Understandingtheflowofschemaanddatascripts
script.aculo.us
URL/JavaScript
SearchCriteriaInterface
used,forlistfiltering/SearchCriteriaInterfaceforlistfiltering
Selenium
about/Functionaltesting
Seleniumstandaloneserver
URL/Functionaltesting
servicecontract
about/Thearchitecturallayers,Servicecontracts
session
about/Sessionandcookies
session-basedauthentication
defining/Hands-onwithsession-basedauthentication
SlideRepositoryInterface
about/CreatingcustomWebAPIs
SOAP
versusREST/RESTversusSOAP
SoapClient
about/RESTversusSOAP
standards
URL/Codingstandards
statictesting
defining/Statictesting
Symfony
about/Thetechnologystack
systemconfigurationfile(system.xml)
creating/Creatingasystemconfigurationfile(system.xml)
U
Uicomponent
about/JavaScript
Underscore.js
URL/JavaScript
unittest
writing/Writingasimpleunittest
unittesting
defining/Unittesting
unittests
creating/Creatingunittests
upgradedatascript(UpgradeData.php)
creating/Creatinganupgradedatascript(UpgradeData.php)
upgradeschemascript(UpgradeSchema.php)
creating/Creatinganupgradeschemascript(UpgradeSchema.php)
usertypes
about/Usertypes
administratororintegration/Usertypes
customer/Usertypes
guestuser/Usertypes
V
Vagrant
about/Vagrant
URL/Vagrant
Vagrantproject
about/Vagrantproject
PHP,provisioning/ProvisioningPHP
MySQL,provisioning/ProvisioningMySQL
Apache,provisioning/ProvisioningApache
Magentoinstallation,provisioning/ProvisioningMagentoinstallation
vardirectory
about/Thevardirectory
viewelements
about/Viewelements
Uicomponents/Uicomponents
containers/Containers
blocks/Blocks
blockarchitecture/Blockarchitectureandlifecycle
lifecycle/Blockarchitectureandlifecycle
templates/Templates
layouts/Layouts
themes/Themes
JavaScript/JavaScript
CSS/CSS
VirtualBox
about/VirtualBox
URL/Vagrant
virtualtypes
using/Usingvirtualtypes
about/Usingvirtualtypes
VMware
about/Vagrant