Post on 26-Mar-2023
TableofContents
LearningPHP7
Credits
AbouttheAuthor
AbouttheReviewer
www.PacktPub.com
eBooks,discountoffers,andmore
Whysubscribe?
Preface
Whatthisbookcovers
Whatyouneedforthisbook
Whothisbookisfor
Conventions
Readerfeedback
Customersupport
Downloadingtheexamplecode
Errata
Piracy
Questions
1.SettingUptheEnvironment
SettinguptheenvironmentwithVagrant
IntroducingVagrant
InstallingVagrant
UsingVagrant
SettinguptheenvironmentonOSX
InstallingPHP
InstallingMySQL
InstallingNginx
InstallingComposer
SettinguptheenvironmentonWindows
InstallingPHP
InstallingMySQL
InstallingNginx
InstallingComposer
SettinguptheenvironmentonUbuntu
InstallingPHP
InstallingMySQL
InstallingNginx
Summary
2.WebApplicationswithPHP
TheHTTPprotocol
Asimpleexample
Partsofthemessage
URL
TheHTTPmethod
Body
Headers
Thestatuscode
Amorecomplexexample
Webapplications
HTML,CSS,andJavaScript
Webservers
Howtheywork
ThePHPbuilt-inserver
Puttingthingstogether
Summary
3.UnderstandingPHPBasics
PHPfiles
Variables
Datatypes
Operators
Arithmeticoperators
Assignmentoperators
Comparisonoperators
Logicaloperators
Incrementinganddecrementingoperators
Operatorprecedence
Workingwithstrings
Arrays
Initializingarrays
Populatingarrays
Accessingarrays
Theemptyandissetfunctions
Searchingforelementsinanarray
Orderingarrays
Otherarrayfunctions
PHPinwebapplications
Gettinginformationfromtheuser
HTMLforms
Persistingdatawithcookies
Othersuperglobals
Controlstructures
Conditionals
Switch…case
Loops
While
Do…while
For
Foreach
Functions
Functiondeclaration
Functionarguments
Thereturnstatement
Typehintingandreturntypes
Thefilesystem
Readingfiles
Writingfiles
Otherfilesystemfunctions
Summary
4.CreatingCleanCodewithOOP
Classesandobjects
Classproperties
Classmethods
Classconstructors
Magicmethods
Propertiesandmethodsvisibility
Encapsulation
Staticpropertiesandmethods
Namespaces
Autoloadingclasses
Usingthe__autoloadfunction
Usingthespl_autoload_registerfunction
Inheritance
Introducinginheritance
Overridingmethods
Abstractclasses
Interfaces
Polymorphism
Traits
Handlingexceptions
Thetry…catchblock
Thefinallyblock
Catchingdifferenttypesofexceptions
Designpatterns
Factory
Singleton
Anonymousfunctions
Summary
5.UsingDatabases
Introducingdatabases
MySQL
Schemasandtables
Understandingschemas
Databasedatatypes
Numericdatatypes
Stringdatatypes
Listofvalues
Dateandtimedatatypes
Managingtables
Keysandconstraints
Primarykeys
Foreignkeys
Uniquekeys
Indexes
Insertingdata
Queryingdata
UsingPDO
Connectingtothedatabase
Performingqueries
Preparedstatements
Joiningtables
Groupingqueries
Updatinganddeletingdata
Updatingdata
Foreignkeybehaviors
Deletingdata
Workingwithtransactions
Summary
6.AdaptingtoMVC
TheMVCpattern
UsingComposer
Managingdependencies
AutoloaderwithPSR-4
Addingmetadata
Theindex.phpfile
Workingwithrequests
Therequestobject
Filteringparametersfromrequests
Mappingroutestocontrollers
Therouter
URLsmatchingwithregularexpressions
ExtractingtheargumentsoftheURL
Executingthecontroller
Mformodel
Thecustomermodel
Thebookmodel
Thesalesmodel
Vforview
IntroductiontoTwig
Thebookview
Layoutsandblocks
Paginatedbooklist
Thesalesview
Theerrortemplate
Thelogintemplate
Cforcontroller
Theerrorcontroller
Thelogincontroller
Thebookcontroller
Borrowingbooks
Thesalescontroller
Dependencyinjection
Whyisdependencyinjectionnecessary?
Implementingourowndependencyinjector
Summary
7.TestingWebApplications
Thenecessityfortests
Typesoftests
Unittestsandcodecoverage
IntegratingPHPUnit
Thephpunit.xmlfile
Yourfirsttest
Runningtests
Writingunittests
Thestartandendofatest
Assertions
Expectingexceptions
Dataproviders
Testingwithdoubles
InjectingmodelswithDI
CustomizingTestCase
Usingmocks
Databasetesting
Test-drivendevelopment
Theoryversuspractice
Summary
8.UsingExistingPHPFrameworks
Reviewingframeworks
Thepurposeofframeworks
Themainpartsofaframework
Otherfeaturesofframeworks
Authenticationandroles
ORM
Cache
Internationalization
Typesofframeworks
Completeandrobustframeworks
Lightweightandflexibleframeworks
Anoverviewoffamousframeworks
Symfony2
ZendFramework2
Otherframeworks
TheLaravelframework
Installation
Projectsetup
Addingthefirstendpoint
Managingusers
Userregistration
Userlogin
Protectedroutes
Settinguprelationshipsinmodels
Creatingcomplexcontrollers
Addingtests
TheSilexmicroframework
Installation
Projectsetup
Managingconfiguration
Settingthetemplateengine
Addingalogger
Addingthefirstendpoint
Accessingthedatabase
SilexversusLaravel
Summary
9.BuildingRESTAPIs
IntroducingAPIs
IntroducingRESTAPIs
ThefoundationsofRESTAPIs
HTTPrequestmethods
GET
POSTandPUT
DELETE
Statuscodesinresponses
2xx–success
3xx–redirection
4xx–clienterror
5xx–servererror
RESTAPIsecurity
Basicaccessauthentication
OAuth2.0
Usingthird-partyAPIs
Gettingtheapplication’scredentials
Settinguptheapplication
Requestinganaccesstoken
Fetchingtweets
ThetoolkitoftheRESTAPIdeveloper
TestingAPIswithbrowsers
TestingAPIsusingthecommandline
BestpracticeswithRESTAPIs
Consistencyinyourendpoints
Documentasmuchasyoucan
Filtersandpagination
APIversioning
UsingHTTPcache
CreatingaRESTAPIwithLaravel
SettingOAuth2authentication
InstallingOAuth2Server
Settingupthedatabase
Enablingclient-credentialsauthentication
Requestinganaccesstoken
Preparingthedatabase
Settingupthemodels
Designingendpoints
Addingthecontrollers
TestingyourRESTAPIs
Summary
10.BehavioralTesting
Behavior-drivendevelopment
Introducingcontinuousintegration
Unittestsversusacceptancetests
TDDversusBDD
Businesswritingtests
BDDwithBehat
IntroducingtheGherkinlanguage
Definingscenarios
WritingGiven-When-Thentestcases
Reusingpartsofscenarios
Writingstepdefinitions
Theparameterizationofsteps
Runningfeaturetests
TestingwithabrowserusingMink
Typesofwebdrivers
InstallingMinkwithGoutte
Interactionwiththebrowser
Summary
Index
LearningPHP7Copyright©2016PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:March2016
Productionreference:1210316
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78588-054-4
www.packtpub.com
CreditsAuthor
AntonioLopez
Reviewer
BradBonkoski
CommissioningEditor
KunalParikh
AcquisitionEditors
NikhilKarkal
DivyaPoojari
ContentDevelopmentEditor
RohitKumarSingh
TechnicalEditor
TaabishKhan
CopyEditors
ShrutiIyer
SoniaMathur
ProjectCoordinator
IzzatContractor
Proofreader
SafisEditing
Indexer
TejalDaruwaleSoni
ProductionCoordinator
MelwynD’sa
CoverWork
MelwynD’sa
AbouttheAuthorAntonioLopezisasoftwareengineerwithmorethan7yearsofexperience.HehasworkedwithPHPsinceuniversity,whichwas10yearsago,buildingsmallpersonalprojects.Later,AntoniostartedhisjourneyaroundEurope,workinginBarcelona,London,Dublin,andbackinBarcelona.Hehasworkedinanumberofdifferentareas,fromwebapplicationstoRESTAPIsandinternaltools.Antoniolikestospendhissparetimeonpersonalprojectsandstart-upsandhasastrongvocationineducationandteaching.
Iwouldliketogivethankstomywife,Neri,forsupportingmethroughthewholeprocessofwritingthisbookwithoutgoingcrazy.
AbouttheReviewerBradBonkoskihasbeendevelopingsoftwareforover15years,specializingininternaloperations,systems,tools,andautomation.Sometimes,thisroleislooselyreferredtoasDevOps.HeleansmoretowardtheDevsideofthismisunderstoodbuzzword.AfterbuildinganincidentmanagementsystemandmanagingchangemanagementforYahoo,Bradbecamemotivatedbymetricsandnowlivesbythemantrathatwhatdoesn’tgetmeasureddoesn’tgetfixed.Today,hegreasesthewheelsofproductivityforShazam.
eBooks,discountoffers,andmoreDidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<customercare@packtpub.com>formoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt’sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt’sentirelibraryofbooks.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
PrefaceThereisnoneedtostatehowmuchweightwebapplicationshaveinourlives.Weusewebapplicationstoknowwhatourfriendsaredoing,togetthelatestnewsaboutpolitics,tochecktheresultsofourfavoritefootballteaminagame,orgraduatefromanonlineuniversity.Andasyouareholdingthisbook,youalreadyknowthatbuildingtheseapplicationsisnotajobthatonlyaselectedgroupofgeniusescanperform,andthatit’srathertheopposite.
Thereisn’tonlyonewaytobuildwebapplications;thereareactuallyquitealotoflanguagesandtechnologieswiththesolepurposeofdoingthis.However,ifthereisonelanguagethatstandsoutfromtherest,eitherhistoricallyorbecauseitisextremelyeasytouse,itisPHPandallthetoolsofitsecosystem.
TheInternetisfullofresourcesthatdetailhowtousePHP,sowhybotherreadingthisbook?That’seasy.WewillnotgiveyouthefulldocumentationofPHPastheofficialwebsitedoes.OurgoalisnotthatyougetaPHPcertification,butrathertoteachyouwhatyoureallyneedinordertobuildwebapplicationsbyyourself.Fromtheverybeginning,wewillusealltheinformationprovidedinordertobuildapplications,soyoucannotewhyeachpieceofinformationisuseful.
However,wewillnotstophere.Notonlywillweshowyouwhatthelanguageoffersyou,butalsowewilldiscussthebestapproachestowritingcode.Youwilllearnallthetechniquesthatanywebdeveloperhastomaster,fromOOPanddesignpatternssuchasMVC,totesting.YouwillevenworkwiththeexistingPHPframeworksthatbigandsmallcompaniesusefortheirownprojects.
Inshort,youwillstartajourneyinwhichyouwilllearnhowtomasterwebdevelopmentratherthanhowtomasteraprogramminglanguage.Wehopeyouenjoyit.
WhatthisbookcoversChapter1,SettingUptheEnvironment,willguideyouthroughtheinstallationofthedifferentsoftwareneeded.
Chapter2,WebApplicationswithPHP,willbeanintroductiontowhatwebapplicationsareandhowtheyworkinternally.
Chapter3,UnderstandingPHPBasics,willgothroughthebasicelementsofthePHPlanguage—fromvariablestocontrolstructures.
Chapter4,CreatingCleanCodewithOOP,willdescribehowtodevelopwebapplicationsfollowingtheobject-orientedprogrammingparadigm.
Chapter5,UsingDatabases,willexplainhowyoucanuseMySQLdatabasesinyourapplications.
Chapter6,AdaptingtoMVC,willshowhowtoapplythemostfamouswebdesignpattern,MVC,toyourapplications.
Chapter7,TestingWebApplications,willbeanextensiveintroductiontounittestingwithPHPUnit.
Chapter8,UsingExistingPHPFrameworks,willintroduceyoutoexistingPHPframeworksusedbyseveralcompaniesanddevelopers,suchasLaravelandSilex.
Chapter9,BuildingRESTAPIs,willexplainwhatRESTAPIsare,howtousethird-partyones,andhowtobuildyourown.
Chapter10,BehavioralTesting,willintroducetheconceptsofcontinuousintegrationandbehavioraltestingwithPHPandBehat.
WhatyouneedforthisbookInChapter1,SettingUptheEnvironment,wewillgothroughthedetailsofhowtoinstallPHPandtherestoftoolsthatyouneedinordertogothoughtheexamplesofthisbook.TheonlythingthatyouneedtostartreadingisacomputerwithWindows,OSX,orLinux,andanInternetconnection.
WhothisbookisforThisbookisforanyonewhowishestowritewebapplicationswithPHP.Youdonotneedtobeacomputersciencegraduateinordertounderstandit.Infact,wewillassumethatyouhavenoknowledgeatallofsoftwaredevelopment,neitherwithPHPnorwithanyotherlanguage.Wewillstartfromtheverybeginningsothateverybodycanfollowthebook.
Experiencedreaderscanstilltakeadvantageofthebook.YoucanquicklyreviewthefirstchapterinordertodiscoverthenewfeaturesPHP7comeswith,andthenfocusonthechaptersthatmightinterestyou.Youdonotneedtoreadthebookfromstarttoend,butinsteadkeepitasaguide,inordertorefreshspecifictopicswhenevertheyareneeded.
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:“Now,createamyactions.jsfilewiththefollowingcontent.”
Ablockofcodeissetasfollows:
#special{
font-size:30px;
}
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
<head>
<metacharset="UTF-8">
<title>Yourfirstapp</title>
<linkrel="stylesheet"type="text/css"href="mystyle.css">
</head>
Anycommand-lineinputoroutputiswrittenasfollows:
$sudoapt-getupdate
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:“ClickonNextuntiltheendoftheinstallationwizard.”
NoteWarningsorimportantnotesappearinaboxlikethis.
TipTipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,simplye-mail<feedback@packtpub.com>,andmentionthebook’stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORTtabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou’relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<copyright@packtpub.com>withalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusat<questions@packtpub.com>,andwewilldoourbesttoaddresstheproblem.
Chapter1.SettingUptheEnvironmentYouareabouttostartajourney—alongone,inwhichyouwilllearnhowtowritewebapplicationswithPHP.However,first,youneedtosetupyourenvironment,somethingthathasproventobetrickyattimes.ThistaskincludesinstallingPHP7,thelanguageofchoiceforthisbook;MySQL,thedatabasethatwewilluseinsomechapters;Nginx,thewebserverthatwillallowustovisualizeourapplicationswithabrowser;andComposer,thefavoritePHPdependenciesmanagementtool.WewilldoallofthiswithVagrantandalsoonthreedifferentplatforms:Windows,OSX,andUbuntu.
Inthischapter,youwilllearnabout:
UsingVagranttosetupadevelopmentenvironmentSettingupyourenvironmentmanuallyonthemainplatforms
SettinguptheenvironmentwithVagrantNotsolongago,everytimeyoustartedworkingforanewcompany,youwouldspendanimportantpartofyourfirstfewdayssettingupyournewenvironment—thatis,installingallthenecessarytoolsonyournewcomputerinordertobeabletocode.Thiswasincrediblyfrustratingbecauseeventhoughthesoftwaretoinstallwasthesame,therewasalwayssomethingthatfailedorwasmissing,andyouwouldspendlesstimebeingproductive.
IntroducingVagrantLuckilyforus,peopletriedtofixthisbigproblem.First,wehavevirtualmachines,whichareemulationsofcomputersinsideyourowncomputer.Withthis,wecanhaveLinuxinsideourMacBook,whichallowsdeveloperstoshareenvironments.Itwasagoodstep,butitstillhadsomeproblems;forexample,VMswerequitebigtomovebetweendifferentenvironments,andifdeveloperswantedtomakeachange,theyhadtoapplythesamechangetoalltheexistingvirtualmachinesintheorganization.
Aftersomedeliberation,agroupofengineerscameupwithasolutiontotheseissuesandwegotVagrant.Thisamazingsoftwareallowsyoutomanagevirtualmachineswithsimpleconfigurationfiles.Theideaissimple:aconfigurationfilespecifieswhichbasevirtualmachineweneedtousefromasetofavailableonesonlineandhowyouwouldliketocustomizeit—thatis,whichcommandsyouwillwanttorunthefirsttimeyoustartthemachine—thisiscalled“provisioning”.YouwillprobablygettheVagrantconfigurationfromapublicrepository,andifthisconfigurationeverchanges,youcangetthechangesandreprovisionyourmachine.It’seasy,right?
InstallingVagrantIfyoustilldonothaveVagrant,installingitisquiteeasy.YouwillneedtovisittheVagrantdownloadpageathttps://www.vagrantup.com/downloads.htmlandselecttheoperatingsystemthatyouareworkingwith.Executetheinstaller,whichdoesnotrequireanyextraconfiguration,andyouaregoodtogo.
UsingVagrantUsingVagrantisquiteeasy.ThemostimportantpieceistheVagrantfilefile.Thisfilecontainsthenameofthebaseimagewewanttouseandtherestoftheconfigurationthatwewanttoapply.ThefollowingcontentistheconfigurationneededinordertogetanUbuntuVMwithPHP7,MySQL,Nginx,andComposer.SaveitasVagrantfileattherootofthedirectoryfortheexamplesofthisbook.
VAGRANTFILE_API_VERSION="2"
Vagrant.configure(VAGRANTFILE_API_VERSION)do|config|
config.vm.box="ubuntu/trusty32"
config.vm.network"forwarded_port",guest:80,host:8080
config.vm.provision"shell",path:"provisioner.sh"
end
Asyoucansee,thefileisquitesmall.Thebaseimage’snameisubuntu/trusty32,messagestoourport8080willberedirectedtotheport80ofthevirtualmachine,andtheprovisionwillbebasedontheprovisioner.shscript.Youwillneedtocreatethisfile,whichwillbetheonethatcontainsallthesetupofthedifferentcomponentsthatweneed.Thisiswhatyouneedtoaddtothisfile:
#!/bin/bash
sudoapt-getinstallpython-software-properties-y
sudoLC_ALL=en_US.UTF-8add-apt-repositoryppa:ondrej/php-y
sudoapt-getupdate
sudoapt-getinstallphp7.0php7.0-fpmphp7.0-mysql-y
sudoapt-get--purgeautoremove-y
sudoservicephp7.0-fpmrestart
sudodebconf-set-selections<<<'mysql-servermysql-server/root_password
passwordroot'
sudodebconf-set-selections<<<'mysql-servermysql-
server/root_password_againpasswordroot'
sudoapt-get-yinstallmysql-servermysql-client
sudoservicemysqlstart
sudoapt-getinstallnginx-y
sudocat>/etc/nginx/sites-available/default<<-EOM
server{
listen80default_server;
listen[::]:80default_serveripv6only=on;
root/vagrant;
indexindex.phpindex.htmlindex.htm;
server_nameserver_domain_or_IP;
location/{
try_files\$uri\$uri//index.php?\$query_string;
}
location~\.php\${
try_files\$uri/index.php=404;
fastcgi_split_path_info^(.+\.php)(/.+)\$;
fastcgi_passunix:/var/run/php/php7.0-fpm.sock;
fastcgi_indexindex.php;
fastcgi_paramSCRIPT_FILENAME\$document_root\$fastcgi_script_name;
includefastcgi_params;
}
}
EOM
sudoservicenginxrestart
Thefilelooksquitelong,butwewilldoquitealotofstuffwithit.Withthefirstpartofthefile,wewilladdthenecessaryrepositoriestobeabletofetchPHP7,asitdoesnotcomewiththeofficialones,andtheninstallit.Then,wewilltrytoinstallMySQL,serverandclient.WewillsettherootpasswordonthisprovisioningbecausewecannotintroduceitmanuallywithVagrant.Asthisisadevelopmentmachine,itisnotreallyaproblem,butyoucanalwayschangethepasswordonceyouaredone.Finally,wewillinstallandconfigureNginxtolistentotheport8080.
Tostartthevirtualmachine,youneedtoexecutethefollowingcommandinthesamedirectorywhereVagrantfileis:
$vagrantup
Thefirsttimeyouexecuteit,itwilltakesometimeasitwillhavetodownloadtheimagefromtherepository,andthenitwillexecutetheprovisioner.shfile.Theoutputshouldbesomethingsimilartothisonefollowedbysomemoreoutputmessages:
InordertoaccessyournewVM,runthefollowingcommandonthesamedirectorywhere
youhaveyourVagrantfilefile:
$vagrantssh
VagrantwillstartanSSHsessiontotheVM,whichmeansthatyouareinsidetheVM.YoucandoanythingyouwoulddowiththecommandlineofanUbuntusystem.Toexit,justpressCtrl+D.
SharingfilesfromyourlaptoptotheVMiseasy;justmoveorcopythemtothesamedirectorywhereyourVagrantfilefileis,andtheywill“magically”appearonthe/vagrantdirectoryofyourVM.Theywillbesynchronized,soanychangesthatyoumakewhileinyourVMwillbereflectedonthefilesofyourlaptop.
Onceyouhaveawebapplicationandyouwanttotestitthroughawebbrowser,rememberthatwewillforwardtheports.Thismeansthatinordertoaccesstheport80ofyourVM,thecommononeforwebapplications,youwillhavetopointtotheport8080onyourbrowsers;here’sanexample:http://localhost:8080.
SettinguptheenvironmentonOSXIfyouarenotconvincedwithVagrantandprefertouseaMactodevelopPHPapplications,thisisyoursection.InstallingallthenecessarytoolsonaMacmightbeabittricky,dependingontheversionofyourOSX.Atthetimeofwritingthisbook,OraclehasnotreleasedaMySQLclientthatyoucanuseviathecommandlinethatworkswithElCapitan,sowewilldescribehowtoinstallanothertoolthatcandoasimilarjob.
InstallingPHPIfitisthefirsttimeyouareusingaMactodevelopapplicationsofanykind,youwillhavetostartbyinstallingXcode.YoucanfindthisapplicationforfreeontheAppStore:
AnotherindispensabletoolforMacusersisBrew.ThisisthepackagemanagerforOSXandwillhelpusinstallPHPwithalmostnopain.Toinstallit,runthefollowingcommandonyourcommandline:
$ruby-e"$(curl-fsSL
https://raw.githubusercontent.com/Homebrew/install/master/install)"
IfyoualreadyhaveBrewinstalled,youcanmakesurethateverythingworksfinebyrunningthesetwocommands:
$brewdoctor
$brewupdate
ItistimetoinstallPHP7usingBrew.Todoso,youwilljustneedtorunonecommand,asfollows:
$brewinstallhomebrew/php/php70
Theresultshouldbeasshowninthefollowingscreenshot:
MakesuretoaddthebinarytoyourPATHenvironmentvariablebyexecutingthiscommand:
$exportPATH="$(brew--prefixhomebrew/php/php70)/bin:$PATH"
YoucancheckwhetheryourinstallationwassuccessfulbyaskingwhichversionofPHPyoursystemisusingwiththe$php–vcommand.
InstallingMySQLAspointedoutatthebeginningofthissection,MySQLisatrickyoneforMacusers.YouneedtodownloadtheMySQLserverinstallerandMySQLWorkbenchastheclient.TheMySQLserverinstallercanbefoundathttps://dev.mysql.com/downloads/mysql/.Youshouldfindalistofdifferentoptions,asshownhere:
TheeasiestwaytogoistodownloadDMGArchive.YouwillbeaskedtologinwithyourOracleaccount;youcancreateoneifyoudonothaveany.Afterthis,thedownloadwillstart.AswithanyDMGpackage,justdouble-clickonitandgothroughtheoptions—inthiscase,justclickonNextallthetime.Becarefulbecauseattheendoftheprocess,youwillbepromptedwithamessagesimilartothis:
Makeanoteofit;otherwise,youwillhavetoresettherootpassword.ThenextoneisMySQLWorkbench,whichyoucanfindathttp://www.mysql.com/products/workbench/.Theprocessisthesame;youwillbeaskedtologin,andthenyouwillgetaDMGfile.ClickonNextuntiltheendoftheinstallationwizard.Oncedone,youcanlaunchtheapplication;itshouldlooksimilartothis:
InstallingNginxInordertoinstallNginx,wewilluseBrew,aswedidwithPHP.Thecommandisthefollowing:
$brewinstallnginx
IfyouwanttomakeNginxstarteverytimeyoustartyourlaptop,runthefollowingcommand:
$ln-sfv/usr/local/opt/nginx/*.plist~/Library/LaunchAgents
IfyouhavetochangetheconfigurationofNginx,youwillfindthefilein/usr/local/etc/nginx/nginx.conf.Youcanchangethings,suchastheportthatNginxislisteningtoortherootdirectorywhereyourcodeis(thedefaultdirectoryis/usr/local/Cellar/nginx/1.8.1/html/).RemembertorestartNginxtoapplythechangeswiththesudonginxcommand.
InstallingComposerInstallingComposerisaseasyasdownloadingitwiththecurlcommand;movethebinaryto/usr/local/bin/withthefollowingtwocommands:
$curl-sShttps://getcomposer.org/installer|php
$mvcomposer.phar/usr/local/bin/composer
SettinguptheenvironmentonWindowsEventhoughitisnotveryprofessionaltopicksidesbasedonpersonalopinions,itiswellknownamongdevelopershowharditcanbetouseWindowsasadevelopermachine.TheyprovetobeextremelytrickywhenitcomestoinstallingallthesoftwaresincetheinstallationmodeisalwaysverydifferentfromOSXandLinuxsystems,andquiteoften,therearedependencyorconfigurationproblems.Inaddition,thecommandlinehasdifferentinterpretersthanUnixsystems,whichmakesthingsabitmoreconfusing.ThisiswhymostdeveloperswouldrecommendyouuseavirtualmachinewithLinuxifyouonlyhaveaWindowsmachineatyourdisposal.
However,tobefair,PHP7istheexceptiontotherule.Itissurprisinglysimpletoinstallit,soifyouarereallycomfortablewithyourWindowsandwouldprefernottouseVagrant,hereyouhaveashortexplanationonhowtosetupyourenvironment.
InstallingPHPInordertoinstallPHP7,youwillfirstdownloadtheinstallerfromtheofficialwebsite.Forthis,gotohttp://windows.php.net/download.Theoptionsshouldbesimilartothefollowingscreenshot:
Choosex86ThreadSafeforWindows32-bitorx64ThreadSafeforthe64-bitone.Oncedownloaded,uncompressitinC:\php7.Yes,thatisit!
InstallingMySQLInstallingMySQLisalittlemorecomplex.Downloadtheinstallerfromhttp://dev.mysql.com/downloads/installer/andexecuteit.Afteracceptingthelicenseagreement,youwillgetawindowsimilartothefollowingone:
Forthepurposesofthebook—andactuallyforanydevelopmentenvironment—youshouldgoforthefirstoption:DeveloperDefault.Keepgoingforward,leavingallthedefaultoptions,untilyougetawindowsimilartothis:
Dependingonyourpreferences,youcaneitherjustsetapasswordfortherootuser,whichisenoughasitisonlyadevelopmentmachine,oryoucanaddanextrauserbyclickingonAddUser.Makesuretosetthecorrectname,password,andpermissions.Ausernamedtestwithadministrationpermissionsshouldlooksimilartothefollowingscreenshot:
InstallingNginxTheinstallationforNginxisalmostidenticaltothePHP7one.First,downloadtheZIPfilefromhttp://nginx.org/en/download.html.Atthetimeofwriting,theversionsavailableareasfollows:
Youcansafelydownloadthemainlineversion1.9.10oralateroneifitisstable.Oncethefileisdownloaded,uncompressitinC:\nginxandrunthefollowingcommandstostartthewebserver:
$cdnginx
$startnginx
InstallingComposerTofinishwiththesetup,weneedtoinstallComposer.Togofortheautomaticinstallation,justdownloadtheinstallerfromhttps://getcomposer.org/Composer-Setup.exe.Oncedownloaded,executeitinordertoinstallComposeronyoursystemandtoupdateyourPATHenvironmentvariable.
SettinguptheenvironmentonUbuntuSettingupyourenvironmentonUbuntuistheeasiestofthethreeplatforms.Infact,youcouldtaketheprovisioner.shscriptfromtheSettinguptheenvironmentwithVagrantsectionandexecuteitonyourlaptop.Thatshoulddothetrick.However,justincaseyoualreadyhavesomeofthetoolsinstalledoryouwanttohaveasenseofcontrolonwhatisgoingon,wewilldetaileachstep.
InstallingPHPTheonlythingtoconsiderinthissectionistoremoveanypreviousPHPversionsonyoursystem.Todoso,youcanrunthefollowingcommand:
$sudoapt-get-ypurgephp.*
ThenextstepistoaddthenecessaryrepositoriesinordertofetchthecorrectPHPversion.Thecommandstoaddandupdatethemare:
$sudoapt-getinstallpython-software-properties
$sudoLC_ALL=en_US.UTF-8add-apt-repositoryppa:ondrej/php-y
$sudoapt-getupdate
Finally,weneedtoinstallPHP7togetherwiththedriverforMySQL.Forthis,justexecutethefollowingthreecommands:
$sudoapt-getinstallphp7.0php7.0-fpmphp7.0-mysql-y
$sudoapt-get--purgeautoremove-y
$sudoservicephp7.0-fpmstart
InstallingMySQLInstallingMySQLmanuallycanbeslightlydifferentthanwiththeVagrantscript.Aswecaninteractwiththeconsole,wedonothavetospecifytherootpasswordpreviously;instead,wecanforceMySQLtopromptforit.Runthefollowingcommandandkeepinmindthattheinstallerwillaskyouforthepassword:
$sudoapt-get-yinstallmysql-servermysql-client
Oncedone,ifyouneedtostarttheMySQLserver,youcandoitwiththefollowingcommand:
$sudoservicemysqlstart
InstallingNginxThefirstthingthatyouneedtoknowisthatyoucanonlyhaveonewebserverlisteningonthesameport.Asport80isthedefaultoneforwebapplications,ifyouarerunningApacheonyourUbuntumachine,youwillnotbeabletostartanNginxwebserverlisteningonthesameport80.Tofixthis,youcaneitherchangetheportsforNginxorApache,stopApache,oruninstallit.Eitherway,theinstallationcommandforNginxisasfollows:
$sudoapt-getinstallnginx–y
Now,youwillneedtoenableasitewithNginx.Thesitesarefilesunder/etc/nginx/sites-available.Thereisalreadyonefilethere,default,whichyoucansafelyreplacewiththefollowingcontent:
server{
listen80default_server;
listen[::]:80default_serveripv6only=on;
root/var/www/html;
indexindex.phpindex.htmlindex.htm;
server_nameserver_domain_or_IP;
location/{
try_files$uri$uri//index.php?$query_string;
}
location~\.php${
try_files$uri/index.php=404;
fastcgi_split_path_info^(.+\.php)(/.+)$;
fastcgi_passunix:/var/run/php/php7.0-fpm.sock;
fastcgi_indexindex.php;
fastcgi_paramSCRIPT_FILENAME$document_root$fastcgi_script_name;
includefastcgi_params;
}
}
Thisconfigurationbasicallypointstherootdirectoryofyourwebapplicationtothe/var/www/htmldirectory.Youcanchoosetheonethatyouprefer,butmakesurethatithastherightpermissions.Italsolistensontheport80,whichyoucanchangewiththeoneyouprefer;justkeepthisinmindthatwhenyoutrytoaccessyourapplicationviaabrowser.Finally,toapplyallthechanges,runthefollowingcommand:
$sudoservicenginxrestart
TipDownloadingtheexamplecode
Youcandownloadtheexamplecodefilesforthisbookfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.HoverthemousepointerontheSUPPORTtabatthetop.ClickonCodeDownloads&Errata.EnterthenameofthebookintheSearchbox.Selectthebookforwhichyou’relookingtodownloadthecodefiles.Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.ClickonCodeDownload.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
SummaryInthischapter,youlearnedhoweasyitistosetupadevelopmentenvironmentusingVagrant.Ifthisdidnotconvinceyou,youstillgotthechancetosetupallthetoolsmanually.Eitherway,nowyouareabletoworkonthenextchapters.
Inthenextchapter,wewilltakealookattheideaofwebapplicationswithPHP,goingfromtheprotocolsusedtohowthewebserverservesrequests,thussettingthefoundationforthefollowingchapters.
Chapter2.WebApplicationswithPHPWebapplicationsareacommonthinginourlives,andtheyareusuallyveryuserfriendly;usersdonotneedtounderstandhowtheyworkbehindthescenes.Asadeveloper,though,youneedtounderstandhowyourapplicationworksinternally.
Inthischapter,youwilllearnabout:
HTTPandhowwebapplicationsmakeuseofitWebapplicationsandhowtobuildasimpleoneWebserversandhowtolaunchyourPHPbuilt-inwebserver
TheHTTPprotocolIfyouchecktheRFC2068standardathttps://tools.ietf.org/html/rfc2068,youwillseethatitsdescriptionisalmostendless.Luckily,whatyouneedtoknowaboutthisprotocol,atleastforstarters,iswayshorter.
HTTPstandsforHyperTextTransferProtocol.Asanyotherprotocol,thegoalistoallowtwoentitiesornodestocommunicatewitheachother.Inordertoachievethis,themessagesneedtobeformattedinawaythattheybothunderstand,andtheentitiesmustfollowsomepre-establishedrules.
AsimpleexampleThefollowingdiagramshowsaverybasicinterchangeofmessages:
AsimpleGETrequest
Donotworryifyoudonotunderstandalltheelementsinthisdiagram;wewilldescribethemshortly.Inthisrepresentation,therearetwoentities:senderandreceiver.Thesendersendsamessagetothereceiver.Thismessage,whichstartsthecommunication,iscalledtherequest.Inthiscase,themessageisaGETrequest.Thereceiverreceivesthemessage,processesit,andgeneratesasecondmessage:theresponse.Inthiscase,theresponseshowsa200statuscode,meaningthattherequestwasprocessedsuccessfully.
HTTPisstateless;thatis,ittreatseachrequestindependently,unrelatedtoanypreviousone.Thismeansthatwiththisrequestandresponsesequence,thecommunicationisfinished.Anynewrequestswillnotbeawareofthisspecificinterchangeofmessages.
PartsofthemessageAnHTTPmessagecontainsseveralparts.Wewilldefineonlythemostimportantofthem.
URLTheURLofthemessageisthedestinationofthemessage.Therequestwillcontainthereceiver’sURL,andtheresponsewillcontainthesender’s.
Asyoumightknow,theURLcancontainextraparameters,knownasaquerystring.Thisisusedwhenthesenderwantstoaddextradata.Forexample,considerthisURL:http://myserver.com/greeting?name=Alex.ThisURLcontainsoneparameter:namewiththevalueAlex.ItcouldnotberepresentedaspartoftheURLhttp://myserver.com/greeting,sothesenderchosetoadditattheendofit.Youwillseelaterthatthisisnottheonlywaythatwecanaddextrainformationintoamessage.
TheHTTPmethodTheHTTPmethodistheverbofthemessage.Itidentifieswhatkindofactionthesenderwantstoperformwiththismessage.ThemostcommononesareGETandPOST.
GET:Thisasksthereceiveraboutsomething,andthereceiverusuallysendsthisinformationback.Themostcommonexampleisaskingforawebpage,wherethereceiverwillrespondwiththeHTMLcodeoftherequestedpage.POST:Thismeansthatthesenderwantstoperformanactionthatwillupdatethedatathatthereceiverisholding.Forexample,thesendercanaskthereceivertoupdatehisprofilename.
Thereareothermethods,suchasPUT,DELETE,orOPTION,buttheyarelessusedinwebdevelopment,althoughtheyplayacrucialroleinRESTAPIs,whichwillbeexplainedinChapter9,BuildingRESTAPIs.
BodyThebodypartisusuallypresentinresponsemessageseventhougharequestmessagecancontainittoo.Thebodyofthemessagecontainsthecontentofthemessageitself;forexample,iftheuserrequestedawebpage,thebodyoftheresponsewouldconsistoftheHTMLcodethatrepresentsthispage.
Soon,wewilldiscusshowtherequestcanalsocontainabody,whichisusedtosendextrainformationaspartoftherequest,suchasformparameters.
Thebodycancontaintextinanyformat;itcanbeanHTMLtextthatrepresentsawebpage,plaintext,thecontentofanimage,JSON,andsoon.
HeadersTheheadersonanHTTPmessagearethemetadatathatthereceiverneedsinordertounderstandthecontentofthemessage.Therearealotofheaders,andyouwillseesomeoftheminthisbook.
Headersconsistofamapofkey-valuepairs.Thefollowingcouldbetheheadersofa
request:
Accept:text/html
Cookie:name=Richard
Thisrequesttellsthereceiver,whichisaserver,thatitwillaccepttextasHTML,whichisthecommonwayofrepresentingawebpage;andthatithasacookienamedRichard.
ThestatuscodeThestatuscodeispresentinresponses.Itidentifiesthestatusoftherequestwithanumericcodesothatbrowsersandothertoolsknowhowtoreact.Forexample,ifwetrytoaccessaURLthatdoesnotexist,theservershouldreplywithastatuscode404.Inthisway,thebrowserknowswhathappenedwithoutevenlookingatthecontentoftheresponse.
Commonstatuscodesare:
200:Therequestwassuccessful401:Unauthorized;theuserdoesnothavepermissiontoseethisresource404:Pagenotfound500:Internalservererror;somethingwronghappenedontheserversideanditcouldnotberecovered
AmorecomplexexampleThefollowingdiagramshowsaPOSTrequestanditsresponse:
AmorecomplexPOSTrequest
Inthisexchangeofmessages,wecanseetheotherimportantmethod,POST,inaction.Inthiscase,thesendertriestosendarequestinordertoupdatesomeentity’sdata.ThemessagecontainsacookieIDwiththevalue84,whichmayidentifytheentitytoupdate.Italsocontainstwoparametersinthebody:nameandage.Thisisthedatathatthereceiverhastoupdate.
TipSubmittingwebforms
Representingtheparametersaspartofthebodyisacommonwaytosendinformationwhensubmittingaform,butnottheonlyone.YoucanaddaquerystringtotheURL,addJSONtothebodyofthemessage,andsoon.
Theresponsehasastatuscodeof200,meaningthattherequestwasprocessedsuccessfully.Inaddition,theresponsealsocontainsabody,thistimeformattedasJSON,whichrepresentsthenewstatusoftheupdatedentity.
WebapplicationsMaybeyouhavenoticedthatintheprevioussections,Iusedthenotveryintuitivetermsofsenderandreceiverastheydonotrepresentanyspecificscenariothatyoumightknowbutratheralloftheminagenericway.ThemainreasonforthischoiceofterminologyistotrytoseparateHTTPfromwebapplications.YouwillseeattheendofthebookthatHTTPisusedformorethanjustwebsites.
Ifyouarereadingthisbook,youalreadyknowwhatawebapplicationis.Alternatively,maybeyouknowitbyotherterms,suchaswebsiteorwebpage.Let’strytogivesomedefinitions.
Awebpageisasingledocumentwithcontent.Itcontainslinksthatopenotherwebpageswithdifferentcontent.
Awebsiteisthesetofwebpagesthatusuallyliveinthesameserverandarerelatedtoeachother.
Awebapplicationisjustapieceofsoftwarethatrunsonaclient,whichisusuallyabrowser,andcommunicateswithaserver.Aserverisaremotemachinethatreceivesrequestsfromaclient,processesthem,andgeneratesaresponse.Thisresponsewillgobacktotheclient,generallyrenderedbythebrowserinordertodisplayittotheuser.
Eventhoughthisisoutofthescopeofthisbook,youmaybeinterestedtoknowthatnotonlybrowserscanactasclients,generatingrequestsandsendingthemtotheservers;evenserverscanbetheonestakingtheinitiativeofsendingmessagestothebrowsers.
So,whatisthedifferencebetweenawebsiteandawebapplication?Well,thewebapplicationcanbeasmallpartofabiggerwebsitewithaspecificfunctionality.Also,notallwebsitesarewebapplicationsasawebapplicationalwaysdoessomethingbutawebsitecanjustdisplayinformation.
HTML,CSS,andJavaScriptWebapplicationsarerenderedbythebrowsersothattheusercanseeitscontent.Todothis,theserverneedstosendthecontentofthepageordocument.ThedocumentusesHTMLtodescribeitselementsandhowtheyareorganized.Elementscanbelinks,buttons,inputfields,andsoon.Asimpleexampleofawebpagelookslikethis:
<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Yourfirstapp</title>
</head>
<body>
<aid="special"class="link"href="http://yourpage.com">Yourpage</a>
<aclass="link"href="http://theirpage.com">Theirpage</a>
</body>
</html>
Let’sfocusonthehighlightedcode.Asyoucansee,wearedescribingtwo<a>linkswithsomeproperties.Bothlinkshaveaclass,adestination,andatext.ThefirstonealsocontainsanID.Savethiscodeintoafilenamedindex.htmlandexecuteit.Youwillseehowyourdefaultbrowseropensaverysimplepagewithtwolinks.
Ifwewanttoaddsomestyles,orchangethecolor,size,andpositionofthelinks,weneedtoaddCSS.CSSdescribeshowelementsfromtheHTMLaredisplayed.ThereareseveralwaystoincludeCSS,butthebestapproachistohaveitinaseparatedfileandthenreferenceitfromtheHTML.Let’supdateour<head>sectionasshowninthefollowingcode:
<head>
<metacharset="UTF-8">
<title>Yourfirstapp</title>
<linkrel="stylesheet"type="text/css"href="mystyle.css">
</head>
Now,let’screateanewmystyle.cssfileinthesamefolderwiththefollowingcontent:
.link{
color:green;
font-weight:bold;
}
#special{
font-size:30px;
}
ThisCSSfilecontainstwostyledefinitions:oneforthelinkclassandoneforthespecialID.Theclassstylewillbeappliedtoboththelinksastheybothdefinethisclass,anditsetsthemasgreenandbold.TheIDstylethatincreasesthefontofthelinkisonlyappliedtothefirstlink.
Finally,inordertoaddbehaviortoourwebpage,weneedtoaddJSorJavaScript.JSisa
programminglanguagethatwouldneedanentirebookforitself,andinfact,therearequitealotofthem.Ifyouwanttogiveitachance,werecommendthefreeonlinebookEloquentJavaScript,MarijnHaverbeke,whichyoucanfindathttp://eloquentjavascript.net/.AswithCSS,thebestapproachwouldbetoaddaseparatefileandthenreferenceitfromourHTML.Updatethe<body>sectionwiththefollowinghighlightedcode:
<body>
<aid="special"class="link"href="http://yourpage.com">Yourpage</a>
<aclass="link"href="http://theirpage.com">Theirpage</a>
<scriptsrc="myactions.js"></script>
</body>
Now,createamyactions.jsfilewiththefollowingcontent:
document.getElementById("special").onclick=function(){
alert("Youclickedme?");
}
TheJSfileaddsafunctionthatwillbecalledwhenthespeciallinkisclickedon.Thisfunctionjustpopsupanalert.Youcansaveallyourchangesandrefreshthebrowsertoseehowitlooksnowandhowthelinksbehave.
NoteDifferentwaysofincludingJS
YoumightnoticethatweincludedtheCSSfilereferenceattheendofthe<head>sectionandJSattheendof<body>.YoucanactuallyincludeJSinboththe<head>andthe<body>;justbearinmindthatthescriptwillbeexecutedassoonasitisincluded.IfyourscriptreferencesfieldsthatarenotyetdefinedorotherJSfilesthatwillbeincludedlater,JSwillfail.
Congratulations!Youjustwroteyourveryfirstwebpage.Notimpressed?Well,thenyouarereadingthecorrectbook!YouwillhavethechancetoworkwithmoreHTML,CSS,andJSduringthebook,eventhoughthebookfocusesespeciallyonPHP.
WebserversSo,itisabouttimethatyoulearnwhatthosefamouswebserversare.Awebserverisnomorethanapieceofsoftwarerunningonamachineandlisteningtorequestsfromaspecificport.Usually,thisportis80,butitcanbeanyotherthatisavailable.
HowtheyworkThefollowingdiagramrepresentstheflowofrequest-responseontheserverside:
Request-responseflowontheserverside
Thejobofawebserveristorouteexternalrequeststothecorrectapplicationsothattheycanbeprocessed.Oncetheapplicationreturnsaresponse,thewebserverwillsendthisresponsetotheclient.Let’stakeacloselookatallthesteps:
1. Theclient,whichisabrowser,sendsarequest.Thiscanbeofanytype—GETorPOST—andcontainanythingaslongasitisvalid.
2. Theserverreceivestherequest,whichpointstoaport.Ifthereisawebserverlisteningonthisport,thewebserverwillthentakecontrolofthesituation.
3. Thewebserverdecideswhichwebapplication—usuallyafileinthefilesystem—
needstoprocesstherequest.Inordertodecide,thewebserverusuallyconsidersthepathoftheURL;forexample,http://myserver.com/app1/hiwouldtrytopasstherequesttotheapp1application,whereveritisinthefilesystem.However,anotherscenariowouldbehttp://app1.myserver.com/hi,whichwouldalsogotothesameapplication.Therulesareveryflexible,anditisuptoboththewebserverandtheuserastohowtosetthem.
4. Thewebapplication,afterreceivingarequestfromthewebserver,generatesaresponseandsendsittothewebserver.
5. Thewebserversendstheresponsetotheindicatedport.6. Theresponsefinallyarrivestotheclient.
ThePHPbuilt-inserverTherearepowerfulwebserversthatsupporthighloadsoftraffic,suchasApacheorNginx,whicharefairlysimpletoinstallandmanage.Forthepurposeofthisbook,though,wewillusesomethingevensimpler:aPHPbuilt-inserver.Thereasontousethisisthatyouwillnotneedextrapackageinstallations,configurations,andheadachesasitcomeswithPHP.Withjustonecommand,youwillhaveawebserverrunningonyourmachine.
NoteProductionwebservers
NotethatthePHPbuilt-inwebserverisgoodfortestingpurposes,butitishighlyrecommendednottouseitinproductionenvironments.IfyouhavetosetupaserverthatneedstobepublicandyourapplicationiswritteninPHP,Ihighlyrecommendyoutochooseeitheroftheclassics:Apache(http://httpd.apache.org)orNginx(https://www.nginx.com).Bothcanrunalmostonanyserver,arefreeandeasytoinstallandconfigure,and,moreimportantly,haveahugecommunitythatwillsupportyouonvirtuallyanyproblemyoumightencounter.
Finally,handson!Let’strytocreateourveryfirstwebpageusingthebuilt-inserver.Forthis,createanindex.phpfileinsideyourworkspacedirectory—forexample,Documents/workspace/index.php.Thecontentofthisfileshouldbe:
<?php
echo'helloworld';
Now,openyourcommandline,gotoyourworkspacedirectory,probablybyrunningthecdDocuments/workspacecommand,andrunthefollowingcommand:
$php-Slocalhost:8000
Thecommandlinewillpromptyouwithsomeinformation,themostimportantonebeingwhatislistening,whichshouldbelocalhost:8000asspecified,andhowtostopit,usuallybypressingCtrl+C.Donotclosethecommandlineasitwillstopthewebservertoo.
Now,let’sopenabrowserandgotohttp://localhost:8000.Youshouldseeahelloworldmessageonawhitepage.Yay,success!Ifyouareinterested,youcancheckyourcommandline,andyouwillseelogentriesofeachrequestyouaresendingviayourbrowser.
So,howdoesitreallywork?Well,ifyoucheckagaininthepreviousdiagram,thephp-Scommandstartedawebserver—inourcase,listeningtoport8000insteadof80.Also,PHPknowsthatthewebapplicationcodewillbeonthesamedirectorythatyoustartedthewebserver:yourworkspace.Therearemorespecificoptions,butbydefault,PHPwilltrytoexecutetheindex.phpfileinyourworkspace.
PuttingthingstogetherLet’strytoincludeourfirstproject(index.htmlwithitsCSSandJSfiles)aspartofthebuilt-inserver.Todothis,youjustneedtoopenthecommandlineandgotothedirectoryinwhichthesefilesareandstartthewebserverwithphp-Slocalhost:8000.Ifyouchecklocalhost:8000inyourbrowser,youwillseeourtwo-linkpage,asisexpected.
Let’snowmoveournewindex.phpfiletothesamedirectory.Youdonotneedtorestartyourwebserver;PHPwillknowaboutthechangesautomatically.Gotoyourbrowserandrefreshthepage.Youshouldnowseethehelloworldmessageinsteadofthelinks.Whathappenedhere?
Ifyoudonotchangethedefaultoptions,PHPwillalwaystrytofindanindex.phpfileinthedirectoryinwhichyoustartedthewebserver.Ifthisisnotfound,PHPwilltrytofindanindex.htmlfile.Previously,weonlyhadtheindex.htmlfile,soPHPfailedtofindindex.php.Nowthatitcanfinditsfirstoption,index.php,itwillloadit.
Ifwewanttoseeourindex.htmlfilefromthebrowser,wecanalwaysspecifyitintheURLlikehttp://localhost:8000/index.html.Ifthewebservernoticesthatyouaretryingtoaccessaspecificfile,itwilltrytoloaditinsteadofthedefaultoptions.
Finally,ifwetrytoaccessafilethatisnotonourfilesystem,thewebserverwillreturnaresponsewithstatuscode404—thatis,notfound.WecanseethiscodeifweopentheDevelopertoolssectionofourbrowserandgototheNetworksection.
TipDevelopertoolsareyourfriends
Asawebdeveloper,youwillfindveryfewtoolsmoreusefulthanthedevelopertoolsofyourbrowser.Itchangesfrombrowsertobrowser,butallofthebignames,suchasChromeorFirefox,haveit.Itisveryimportantthatyougetfamiliarwithhowtouseitasitallowsyoutodebugyourapplicationsfromtheclientside.
Iwillintroduceyoutosomeofthesetoolsduringthecourseofthisbook.
SummaryInthischapter,youlearnedwhatHTTPisandhowwebapplicationsuseitinordertointeractwiththeserver.Youalsonowknowhowwebserversworkandhowtolaunchalightbuilt-inserverwithPHP.Finally,youtookthefirststepstowardbuildingyourfirstwebapplication.Congratulations!
Inthenextchapter,wewilltakealookatthebasicsofPHPsothatyoucanstartbuildingsimpleapplications.
Chapter3.UnderstandingPHPBasicsLearninganewlanguageisnoteasy.Youneedtounderstandnotonlythesyntaxofthelanguage,butalsoitsgrammaticalrules,thatis,whenandwhytouseeachelementofthelanguage.Luckilyforyou,somelanguagescomefromthesameroot.Forexample,SpanishandFrenchareRomancelanguages,astheybothevolvedfromspokenLatin;thatmeansthatthesetwolanguagessharealotofrules,andifyoualreadyknowFrench,learningSpanishbecomesmucheasier.
Programminglanguagesarequitethesame.Ifyoualreadyknowanotherprogramminglanguage,itwillbeveryeasyforyoutogothroughthischapter.Ifthisisyourfirsttimethough,youwillneedtounderstandallthosegrammaticalrulesfromscratch,andso,itmighttakesomemoretime.Butfearnot!Weareheretohelpyouinthisendeavor.
Inthischapter,youwilllearnaboutthefollowing:
PHPfilesVariables,strings,arrays,andoperatorsinPHPPHPinwebapplicationsControlstructuresinPHPFunctionsinPHPThePHPfilesystem
PHPfilesFromnowon,wewillworkonyourindex.phpfile,soyoucanjuststartthewebserver,andgotohttp://localhost:8080toseetheresults.
YoumighthavealreadynoticedthatinordertowritePHPcode,youhavetostartthefilewith<?php.Thereareotheroptions,andyoucanalsofinishthefilewith?>,butnoneofthemareneeded.WhatisimportanttoknowisthatyoucanmixPHPcodewithothercontent,likeHTML,CSS,orJavaScript,inyourPHPfileassoonasyouenclosethePHPbitswiththe<?php?>tags.
<?php
echo'helloworld';
?>
byeworld
Ifyouchecktheresultoftheprecedingcodesnippetinyourbrowser,youwillseethatitprintsbothmessages,helloworldandbyeworld.Thereasonwhythishappensissimple:youalreadyknowthatthePHPcodethereprintsthehelloworldmessage.WhathappensnextisthatanythingoutsidethePHPtagswillbeinterpretedasis.IfthereisanHTMLcodeforinstance,itwouldnotbeprintedasis,butwillbeinterpretedbythebrowser.
YouwilllearninChapter6,AdaptingtoMVC,whyitisusuallyabadideatomixPHPandHTML.Fornow,assumingthatitisbad,let’strytoavoidit.Forthat,youcanincludeonefilefromanotherPHPfileusinganyoneofthesefourfunctions:
include:Thiswilltrytofindandincludethespecifiedfileeachtimeitisinvoked.Ifthefileisnotfound,PHPwillthrowawarning,butwillcontinuewiththeexecution.require:Thiswilldothesameasinclude,butPHPwillthrowanerrorinsteadofawarningifthefileisnotfound.include_once:Thisfunctionwilldowhatincludedoes,butitwillincludethefileonlythefirsttimethatitisinvoked.Subsequentcallswillbeignored.require_once:Thisworksthesameasrequire,butitwillincludethefileonlythefirsttimethatitisinvoked.Subsequentcallswillbeignored.
Eachfunctionhasitsownusage,soitisnotrighttosaythatoneisbetterthantheother.Justthinkcarefullywhatyourscenariois,andthendecide.Forexample,let’strytoincludeourindex.htmlfilefromourindex.phpfilesuchthatwedonotmixPHPwithHTML,buthavethebestofbothworlds:
<?php
echo'helloworld';
require'index.html';
Wechoserequireasweknowthefileisthere—andifitisnot,wearenotinterestedincontinuingtheexecution.Moreover,asitissomeHTMLcode,wemightwanttoincludeitmultipletimes,sowedidnotchoosetherequire_onceoption.Youcantrytorequireafilethatdoesnotexist,andseewhatthebrowsersays.
PHPdoesnotconsideremptylines;youcanaddasmanyasyouwanttomakeyourcode
easiertoread,anditwillnothaveanyrepercussiononyourapplication.Anotherelementthathelpsinwritingunderstandablecode,andwhichisignoredbyPHP,iscomments.Let’sseebothinaction:
<?php
/*
*Thisisthefirstfileloadedbythewebserver.
*Itprintssomemessagesandhtmlfromotherfiles.
*/
//let'sprintamessagefromphp
echo'helloworld';
//andthenincludetherestofhtml
require'index.html';
Thecodedoesthesamejobasthepreviousone,butnoweveryonewilleasilyunderstandwhatwearetryingtodo.Wecanseetwotypesofcomments:single-linecommentsandmultiple-linecomments.Thefirsttypeconsistsofasinglelinestartingwith//,andthesecondtypeenclosesmultiplelineswithin/*and*/.Westarteachcommentedlinewithanasterisk,butthatiscompletelyoptional.
VariablesVariableskeepavalueforfuturereference.Thisvaluecanchangeifwewantitto;thatiswhytheyarecalledvariables.Let’stakealookattheminanexample.Savethiscodeinyourindex.phpfile:
<?php
$a=1;
$b=2;
$c=$a+$b;
echo$c;//3
Inthisprecedingpieceofcode,wehavethreevariables:$ahasvalue1,$bhas2,and$ccontainsthesumof$aand$b,hence,$cequals3.Yourbrowsershouldprintthevalueofthevariable$c,whichis3.
Assigningavaluetoavariablemeanstogiveitavalue,anditisdonewiththeequalssignasshowninthepreviousexample.Ifyoudidnotassignavaluetoavariable,wewillgetanoticefromPHPwhenitchecksitscontents.Anoticeisjustamessagetellingusthatsomethingisnotexactlyright,butitisaminorproblemandyoucancontinuewiththeexecution.Thevalueofanunassignedvariablewillbenull,thatis,nothing.
PHPvariablesstartwiththe$signfollowedbythevariablename.Avalidvariablenamestartswithaletteroranunderscorefollowedbyanycombinationofletters,numbers,and/orunderscores.Itiscasesensitive.Let’sseesomeexamples:
<?php
$_some_value='abc';//valid
$1number=12.3;//notvalid!
$some$signs%='&^%';//notvalid!
$go_2_home="ok";//valid
$go_2_Home='no';//thisisadifferentvariable
$isThisCamelCase=true;//camelcase
Rememberthateverythingafter//isacomment,andisthusignoredbyPHP.
Inthispieceofcode,wecanseethatvariablenameslike$_some_valueand$go_2_homearevalid.$1numberand$some$signs%arenotvalidastheystartwithanumber,ortheycontaininvalidsymbols.Asnamesarecasesensitive,$go_2_homeand$go_2_Homearetwodifferentvariables.Finally,weshowtheCamelCaseconvention,whichisthepreferredoptionamongmostdevelopers.
DatatypesWecanassignmorethanjustnumberstovariables.PHPhaseightprimitivetypes,butfornow,wewillfocusonitsfourscalartypes:
Booleans:ThesetakejusttrueorfalsevaluesIntegers:Thesearenumericvalueswithoutadecimalpoint,forexample,2or5Floatingpointnumbersorfloats:Thesearenumberswithadecimalpoint,forexample,2.3Strings:Theseareconcatenationsofcharacterswhicharesurroundedbyeithersingleordoublequotes,like‘this’or“that”
EventhoughPHPdefinesthesetypes,itallowstheusertoassigndifferenttypesofdatatothesamevariable.Checkthefollowingcodetoseehowitworks:
<?php
$number=123;
var_dump($number);
$number='abc';
var_dump($number);
Ifyouchecktheresultonyourbrowser,youwillseethefollowing:
int(123)string(3)"abc"
Thecodefirstassignsthevalue123tothevariable$number.As123isaninteger,thetypeofthevariablewillbeintegerint.Thatiswhatweseewhenprintingthecontentofthevariablewithvar_dump.Afterthat,weassignanothervaluetothesamevariable,thistimeastring.Whenprintingthenewcontent,weseethatthetypeofthevariablechangedfromintegertostring,yetPHPdidnotcomplainatanytime.Thisiscalledtypejuggling.
Let’scheckanotherpieceofcode:
<?php
$a="1";
$b=2;
var_dump($a+$b);//3
var_dump($a.$b);//12
Youalreadyknowthatthe+operatorreturnsthesumoftwonumericvalues.Youwillseelaterthatthe.operatorconcatenatestwostrings.Thus,theprecedingcodeassignsastringandanintegertotwovariables,andthentriestoaddandconcatenatethem.
Whentryingtoaddthem,PHPknowsthatitneedstwonumericvalues,andsoittriestoadaptthestringtoaninteger.Inthiscase,itiseasyasthestringrepresentsavalidnumber.Thatisthereasonwhyweseethefirstresultasaninteger3(1+2).
Inthelastline,weareperformingastringconcatenation.Wehaveanintegerin$b,soPHPwillfirsttrytoconvertittoastring—whichis“2”—andthenconcatenateitwiththeotherstring,“1”.Theresultisthestring“12”.
Note
Typejuggling
PHPtriestoconvertthedatatypeofavariableonlywhenthereisacontextwherethetypeofvariableneededisdifferent.ButPHPdoesnotchangethevalueandtypeofthevariableitself.Instead,itwilltakethevalueandtrytotransformit,leavingthevariableintact.
OperatorsUsingvariablesisnice,butifwecannotmaketheminteractwitheachother,thereisnothingmuchwecando.Operatorsareelementsthattakesomeexpressions—operands—andperformactionsonthemtogetaresult.Themostcommonexamplesofoperatorsarearithmeticoperators,whichyoualreadysawpreviously.
Anexpressionisalmostanythingthathasavalue.Variables,numbers,ortextareexamplesofexpressions,butyouwillseethattheycangetwaymorecomplicated.Operatorsexpectexpressionsofaspecifictype,forexample,arithmeticoperatorsexpecteitherintegersorfloats.Butasyoualreadyknow,PHPtakescareoftransformingthetypesoftheexpressionsgivenwheneverpossible.
Let’stakealookatthemostimportantgroupsofoperators.
ArithmeticoperatorsArithmeticoperatorsareveryintuitive,asyoualreadyknow.Addition,subtraction,multiplication,anddivision(+,-,*,and/)doastheirnamessay.Modulus(%)givestheremainderofthedivisionoftwooperands.Exponentiation(**)raisesthefirstoperandtothepowerofthesecond.Finally,negation(-)negatestheoperand.Thislastoneistheonlyarithmeticoperatorthattakesjustoneoperand.
Let’sseesomeexamples:
<?php
$a=10;
$b=3;
var_dump($a+$b);//13
var_dump($a-$b);//7
var_dump($a*$b);//30
var_dump($a/$b);//3.333333…
var_dump($a%$b);//1
var_dump($a**$b);//1000
var_dump(-$a);//-10
Asyoucansee,theyarequiteeasytounderstand!
AssignmentoperatorsYoualreadyknowthisonetoo,aswehavebeenusingitinourexamples.Theassignmentoperatorassignstheresultofanexpressiontoavariable.Nowyouknowthatanexpressioncanbeassimpleasanumber,or,forexample,theresultofaseriesofarithmeticoperations.Thefollowingexampleassignstheresultofanexpressiontoavariable:
<?php
$a=3+4+5-2;
var_dump($a);//10
Thereareaseriesofassignmentoperatorsthatworkasshortcuts.Youcanbuildthemcombininganarithmeticoperatorandtheassignmentoperator.Let’sseesomeexamples:
$a=13;
$a+=14;//sameas$a=$a+14;
var_dump($a);
$a-=2;//sameas$a=$a-2;
var_dump($a);
$a*=4;//sameas$a=$a*4;
var_dump($a);
ComparisonoperatorsComparisonoperatorsareoneofthemostusedgroupsofoperators.Theytaketwooperandsandcomparethem,returningtheresultofthecomparisonusuallyasaBoolean,thatis,trueorfalse.
Therearefourcomparisonsthatareveryintuitive:<(lessthan),<=(lessorequalto),>(greaterthan),and>=(greaterthanorequalto).Thereisalsothespecialoperator<=>(spaceship)thatcomparesboththeoperandsandreturnsanintegerinsteadofaBoolean.Whencomparingawithb,theresultwillbelessthan0ifaislessthanb,0ifaequalsb,andgreaterthan0ifaisgreaterthanb.Let’sseesomeexamples:
<?php
var_dump(2<3);//true
var_dump(3<3);//false
var_dump(3<=3);//true
var_dump(4<=3);//false
var_dump(2>3);//false
var_dump(3>=3);//true
var_dump(3>3);//false
var_dump(1<=>2);//intlessthan0
var_dump(1<=>1);//0
var_dump(3<=>2);//intgreaterthan0
Therearecomparisonoperatorstoevaluateiftwoexpressionsareequalornot,butyouneedtobecarefulwithtypejuggling.The==(equals)operatorevaluatestwoexpressionsaftertypejuggling,thatis,itwilltrytotransformbothexpressionstothesametype,andthencomparethem.Instead,the===(identical)operatorevaluatestwoexpressionswithouttypejuggling,soeveniftheylookthesame,iftheyarenotofthesametype,thecomparisonwillreturnfalse.Thesameappliesto!=or<>(notequalto)and!==(notidentical):
<?php
$a=3;
$b='3';
$c=5;
var_dump($a==$b);//true
var_dump($a===$b);//false
var_dump($a!=$b);//false
var_dump($a!==$b);//true
var_dump($a==$c);//false
var_dump($a<>$c);//true
Youcanseethatwhenaskingifastringandanintegerthatrepresentthesamenumberareequal,itrepliesaffirmatively;PHPfirsttransformsbothtothesametype.Ontheotherhand,whenaskediftheyareidentical,itrepliestheyarenotastheyareofdifferenttypes.
LogicaloperatorsLogicaloperatorsapplyalogicoperation—alsoknownasabinaryoperation—toitsoperands,returningaBooleanresponse.Themostusedonesare!(not),&&(and),and||(or).&&willreturntrueonlyifbothoperandsevaluatetotrue.||willreturntrueifanyorbothoftheoperandsaretrue.!willreturnthenegatedvalueoftheoperand,thatis,trueiftheoperandisfalseorfalseiftheoperandistrue.Let’sseesomeexamples:
<?php
var_dump(true&&true);//true
var_dump(true&&false);//false
var_dump(true||false);//true
var_dump(false||false);//false
var_dump(!false);//true
IncrementinganddecrementingoperatorsIncrementing/decrementingoperatorsarealsoshortcutslike+=or-=,andtheyonlyworkonvariables.Therearefourofthem,andtheyneedspecialattention.We’vealreadyseenthefirsttwo:
++:Thisoperatorontheleftofthevariablewillincreasethevariableby1,andthenreturntheresult.Ontheright,itwillreturnthecontentofthevariable,andafterthatincreaseitby1.--:Thisoperatorworksthesameas++butdecreasesthevalueby1insteadofincreasingby1.
Let’sseeanexample:
<?php
$a=3;
$b=$a++;//$bis3,$ais4
var_dump($a,$b);
$b=++$a;//$aand$bare5
var_dump($a,$b);
Intheprecedingcode,onthefirstassignmentto$b,weuse$a++.Theoperatorontherightwillreturnfirstthevalueof$a,whichis3,assignitto$b,andonlythenincrease$aby1.Inthesecondassignment,theoperatorontheleftfirstincreases$aby1,changesthevalueof$ato5,andthenassignsthatvalueto$b.
OperatorprecedenceYoucanaddmultipleoperatorstoanexpressiontomakeitaslongasitneedstobe,butyouneedtobecarefulassomeoperatorshavehigherprecedencethanothers,andthus,theorderofexecutionmightnotbetheoneyouexpect.Thefollowingtableshowstheorderofprecedenceoftheoperatorsthatwe’vestudieduntilnow:
Operator Type
** Arithmetic
++,-- Increasing/decreasing
! Logical
*,/,% Arithmetic
+,- Arithmetic
<,<=,>,>= Comparison
==,!=,===,!== Comparison
&& Logical
|| Logical
=,+=,-=,*=,/=,%=,**= Assignment
Theprecedingtableshowsusthattheexpression3+2*3willfirstevaluatetheproduct2*3andthenthesum,sotheresultis9ratherthan15.Ifyouwanttoperformoperationsinaspecificorder,differentfromthenaturalorderofprecedence,youcanforceitbyenclosingtheoperationwithinparentheses.Hence,(3+2)*3willfirstperformthesumandthentheproduct,givingtheresult15thistime.
Let’sseesomeexamplestoclarifythisquitetrickysubject:
<?php
$a=1;
$b=3;
$c=true;
$d=false;
$e=$a+$b>5||$c;//true
var_dump($e);
$f=$e==true&&!$d;//true
var_dump($f);
$g=($a+$b)*2+3*4;//20
var_dump($g);
Thisprecedingexamplecouldbeendless,andstillnotbeabletocoverallthescenariosyoucanimagine,solet’skeepitsimple.Inthefirsthighlightedline,wehaveacombinationofarithmetic,comparison,andlogicaloperators,plustheassignment
operator.Astherearenoparentheses,theorderistheonedetailedintheprevioustable.Theoperatorwiththehighestpreferenceisthesum,soweperformitfirst:$a+$bequals4.Thenextoneisthecomparisonoperator,so4>5,whichisfalse.Finally,thelogicaloperator,false||$c($cistrue)resultsintrue.
Thesecondexamplemightneedabitmoreexplanation.Thefirstoperatorweseeinthetableisthenegation,soweresolveit.!$dis!false,soitistrue.Theexpressionisnow,$e==true&&true.Firstweneedtosolvethecomparison$e==true.Knowingthat$eistrue,thecomparisonresultsintrue.Thefinaloperationthenisthelogicalend,anditresultsintrue.
Trytoworkoutthelastexamplebyyourselftogetsomepractice.Donotbeafraidifyouthinkwearenotcoveringoperatorsenough.Duringthenextfewsections,wewillseealotofexamples.
WorkingwithstringsWorkingwithstringsinreallifeisreallyeasy.ActionslikeCheckifthisstringcontainsthisorTellmehowmanytimesthischaracterappearsareveryeasytoperform.Butwhenprogramming,stringsareconcatenationsofcharactersthatyoucannotseeatoncewhensearchingforsomething.Instead,youhavetolookonebyoneandkeeptrackofwhatthecontentis.Inthisscenario,thosereallyeasyactionsarenotthateasyanymore.
Luckilyforyou,PHPbringsawholesetofpredefinedfunctionsthathelpyouininteractingwithstrings.Youcanfindtheentirelistoffunctionsathttp://php.net/manual/en/ref.strings.php,butwewillonlycovertheonesthatareusedthemost.Let’slookatsomeexamples:
<?php
$text='Howcanaclamcraminacleancreamcan?';
echostrlen($text);//45
$text=trim($text);
echo$text;//Howcanaclamcraminacleancreamcan?
echostrtoupper($text);//HOWCANACLAMCRAMINACLEANCREAMCAN?
echostrtolower($text);//howcanaclamcraminacleancreamcan?
$text=str_replace('can','could',$text);
echo$text;//Howcouldaclamcraminacleancreamcould?
echosubstr($text,2,6);//wcoul
var_dump(strpos($text,'can'));//false
var_dump(strpos($text,'could'));//4
Intheprecedinglongpieceofcode,weareplayingwithastringwithdifferentfunctions:
strlen:Thisfunctionreturnsthenumberofcharactersthatthestringcontains.trim:Thisfunctionreturnsthestring,removingalltheblankspacestotheleftandtotheright.strtoupperandstrtolower:Thesefunctionsreturnthestringwithallthecharactersinupperorlowercaserespectively.str_replace:Thisfunctionreplacesalloccurrencesofagivenstringbythereplacementstring.substr:Thisfunctionextractsthestringcontainedbetweenthepositionsspecifiedbyparameters,withthefirstcharacterbeingatposition0.strpos:Thisfunctionshowsthepositionofthefirstoccurrenceofthegivenstring.Itreturnsfalseifthestringcannotbefound.
Additionally,thereisanoperatorforstrings(.)whichconcatenatestwostrings(ortwovariablestransformedtoastringwhenpossible).Usingitisreallysimple:inthefollowingexample,thelaststatementwillconcatenateallthestringsandvariablesformingthesentence,IamHiroNakamura!.
<?php
$firstname='Hiro';
$surname='Nakamura';
echo'Iam'.$firstname.''.$surname.'!';
Anotherthingtonoteaboutstringsisthewaytheyarerepresented.Sofar,wehavebeenenclosingthestringswithinsinglequotes,butyoucanalsoenclosethemwithindoublequotes.Thedifferenceisthatwithinsinglequotes,astringisexactlyasitisrepresented,butwithindoublequotes,somerulesareappliedbeforeshowingthefinalresult.Therearetwoelementsthatdoublequotestreatdifferentlythansinglequotes:escapecharactersandvariableexpansions.
Escapecharacters:Thesearespecialcharactersthancannotberepresentedeasily.Examplesofescapecharactersarenewlinesortabs.Torepresentthem,weuseescapesequences,whicharetheconcatenationofabackslash(\)followedbysomeothercharacter.Forexample,\nrepresentsanewline,and\trepresentsatabulation.Variableexpanding:Thisallowsyoutoincludevariablereferencesinsidethestring,andPHPreplacesthembytheircurrentvalue.Youhavetoincludethe$signtoo.
Havealookatthefollowingexample:
<?php
$firstname='Hiro';
$surname='Nakamura';
echo"Mynameis$firstname$surname.\nIamamasteroftimeandspace.
\"Yatta!\"";
Theprecedingpieceofcodewillprintthefollowinginthebrowser:
MynameisHiroNakamura.
Iamamasteroftimeandspace."Yatta!"
Here,\ninsertedanewline.\"addedthedoublequotes(youneedtoescapethemtoo,asPHPwouldunderstandthatyouwanttoendyourstring),andthevariables$firstnameand$surnamewerereplacedbytheirvalues.
ArraysIfyouhavesomeexperiencewithotherprogramminglanguagesordatastructuresingeneral,youmightbeawareoftwodatastructuresthatareverycommonanduseful:listsandmaps.Alistisanorderedsetofelements,whereasamapisasetofelementsidentifiedbykeys.Let’sseeanexample:
List:["Harry","Ron","Hermione"]
Map:{
"name":"JamesPotter",
"status":"dead"
}
Thefirstelementisalistofnamesthatcontainsthreevalues:Harry,Ron,andHermione.Thesecondoneisamap,anditdefinestwovalues:JamesPotteranddead.Eachofthesetwovaluesisidentifiedwithakey:nameandstatusrespectively.
InPHP,wedonothavelistsandmaps;wehavearrays.Anarrayisadatastructurethatimplementsboth,alistandamap.
InitializingarraysYouhavedifferentoptionsforinitializinganarray.Youcaninitializeanemptyarray,oryoucaninitializeanarraywithdata.Therearedifferentwaysofwritingthesamedatawitharraystoo.Let’sseesomeexamples:
<?php
$empty1=[];
$empty2=array();
$names1=['Harry','Ron','Hermione'];
$names2=array('Harry','Ron','Hermione');
$status1=[
'name'=>'JamesPotter',
'status'=>'dead'
];
$status2=array(
'name'=>'JamesPotter',
'status'=>'dead'
);
Intheprecedingexample,wedefinethelistandmapfromtheprevioussection.$names1and$names2areexactlythesamearray,justusingadifferentnotation.Thesamehappenswith$status1and$status2.Finally,$empty1and$empty2aretwowaysofcreatinganemptyarray.
Lateryouwillseethatlistsarehandledlikemaps.Internally,thearray$names1isamap,anditskeysareorderednumbers.Inthiscase,anotherinitializationfor$names1thatleadstothesamearraycouldbeasfollows:
$names1=[
0=>'Harry',
1=>'Ron',
2=>'Hermione'
];
Keysofanarraycanbeanyalphanumericvalue,likestringsornumbers.Valuesofanarraycanbeanything:strings,numbers,Booleans,otherarrays,andsoon.Youcouldhavesomethinglikethefollowing:
<?php
$books=[
'1984'=>[
'author'=>'GeorgeOrwell',
'finished'=>true,
'rate'=>9.5
],
'RomeoandJuliet'=>[
'author'=>'WilliamShakespeare',
'finished'=>false
]
];
Thisarrayisalistthatcontainstwoarrays—maps.Eachmapcontainsdifferentvalueslikestrings,doubles,andBooleans.
PopulatingarraysArraysarenotimmutable,thatis,theycanchangeafterbeinginitialized.Youcanchangethecontentofanarrayeitherbytreatingitasamaporasalist.Treatingitasamapmeansthatyouspecifythekeythatyouwanttooverride,whereastreatingitasalistmeansappendinganotherelementtotheendofthearray:
<?php
$names=['Harry','Ron','Hermione'];
$status=[
'name'=>'JamesPotter',
'status'=>'dead'
];
$names[]='Neville';
$status['age']=32;
print_r($names,$status);
Intheprecedingexample,thefirsthighlightedlineappendsthenameNevilletothelistofnames,hencethelistwilllooklike[‘Harry’,‘Ron’,‘Hermione’,‘Neville’].Thesecondchangeactuallyaddsanewkey-valuetothearray.Youcanchecktheresultfromyourbrowserbyusingthefunctionprint_r.Itdoessomethingsimilartovar_dump,justwithoutthetypeandsizeofeachvalue.
Noteprint_randvar_dumpinabrowser
Whenprintingthecontentofanarray,itisusefultoseeonekey-valueperline,butifyoucheckyourbrowser,youwillseethatitdisplaysthewholearrayinoneline.ThathappensbecausewhatthebrowsertriestodisplayisHTML,anditignoresnewlinesorwhitespaces.TocheckthecontentofthearrayasPHPwantsyoutoseeit,checkthesourcecodeofthepage—youwillseetheoptionbyright-clickingonthepage.
Ifyouneedtoremoveanelementfromthearray,insteadofaddingorupdatingone,youcanusetheunsetfunction:
<?php
$status=[
'name'=>'JamesPotter',
'status'=>'dead'
];
unset($status['status']);
print_r($status);
Thenew$statusarraycontainsthekeynameonly.
AccessingarraysAccessinganarrayisaseasyasspecifyingthekeyaswhenyouwereupdatingit.Forthat,youneedtounderstandhowlistswork.Youalreadyknowthatlistsaretreatedinternallyasamapwithnumerickeysinorder.Thefirstkeyisalways0;so,anarraywithnelementswillhavekeysfrom0ton-1.
Youcanaddanykeytoagivenarray,evenifitpreviouslyconsistedofnumericentries.Theproblemariseswhenaddingnumerickeys,andlater,youtrytoappendanelementtothearray.Whatdoyouthinkwillhappen?
<?php
$names=['Harry','Ron','Hermione'];
$names['badguy']='Voldemort';
$names[8]='Snape';
$names[]='McGonagall';
print_r($names);
Theresultofthatlastpieceofcodeisasfollows:
Array
(
[0]=>Harry
[1]=>Ron
[2]=>Hermione
[badguy]=>Voldemort
[8]=>Snape
[9]=>McGonagall
)
Whentryingtoappendavalue,PHPinsertsitafterthelastnumerickey,inthiscase8.
Youmight’vealreadyfigureditoutbyyourself,butyoucanalwaysprintanypartofthearraybyspecifyingitskey:
<?php
$names=['Harry','Ron','Hermione'];
print_r($names[1]);//prints'Ron'
Finally,tryingtoaccessakeythatdoesnotexistinanarraywillreturnyouanullandthrowanotice,asPHPidentifiesthatyouaredoingsomethingwronginyourcode.
<?php
$names=['Harry','Ron','Hermione'];
var_dump($names[4]);//nullandaPHPnotice
TheemptyandissetfunctionsTherearetwousefulfunctionsforenquiringaboutthecontentofanarray.Ifyouwanttoknowifanarraycontainsanyelementatall,youcanaskifitisemptywiththeemptyfunction.Thatfunctionactuallyworkswithstringstoo,anemptystringbeingastringwithnocharacters(‘‘).Theissetfunctiontakesanarrayposition,andreturnstrueorfalsedependingonwhetherthatpositionexistsornot:
<?php
$string='';
$array=[];
$names=['Harry','Ron','Hermione'];
var_dump(empty($string));//true
var_dump(empty($array));//true
var_dump(empty($names));//false
var_dump(isset($names[2]));//true
var_dump(isset($names[3]));//false
Intheprecedingexample,wecanseethatanarraywithnoelementsorastringwithnocharacterswillreturntruewhenaskedifitisempty,andfalseotherwise.Whenweuseisset($names[2])tocheckiftheposition2ofthearrayexists,wegettrue,asthereisavalueforthatkey:Hermione.Finally,isset($names[3])evaluatestofalseasthekey3doesnotexistinthatarray.
SearchingforelementsinanarrayProbably,oneofthemostusedfunctionswitharraysisin_array.Thisfunctiontakestwovalues,thevaluethatyouwanttosearchforandthearray.Thefunctionreturnstrueifthevalueisinthearrayandfalseotherwise.Thisisveryuseful,becausealotoftimeswhatyouwanttoknowfromalistoramapisifitcontainsanelement,ratherthanknowingthatitdoesoritslocation.
Evenmoreusefulsometimesisarray_search.ThisfunctionworksinthesamewayexceptthatinsteadofreturningaBoolean,itreturnsthekeywherethevalueisfound,orfalseotherwise.Let’sseebothfunctions:
<?php
$names=['Harry','Ron','Hermione'];
$containsHermione=in_array('Hermione',$names);
var_dump($containsHermione);//true
$containsSnape=in_array('Snape',$names);
var_dump($containsSnape);//false
$wheresRon=array_search('Ron',$names);
var_dump($wheresRon);//1
$wheresVoldemort=array_search('Voldemort',$names);
var_dump($wheresVoldemort);//false
OrderingarraysAnarraycanbesortedindifferentways,sotherearealotofchancesthattheorderthatyouneedisdifferentfromthecurrentone.Bydefault,thearrayissortedbytheorderinwhichtheelementswereaddedtoit,butyoucansortanarraybyitskeyorbyitsvalue,bothascendinganddescending.Furthermore,whensortinganarraybyitsvalues,youcanchoosetopreservetheirkeysortogeneratenewonesasalist.
Thereisacompletelistofthesefunctionsontheofficialdocumentationwebsiteathttp://php.net/manual/en/array.sorting.php,butherewewilldisplaythemostimportantones:
Name Sortsby Maintainskeyassociation Orderofsort
sort Value No Lowtohigh
rsort Value No Hightolow
asort Value Yes Lowtohigh
arsort Value Yes Hightolow
ksort Key Yes Lowtohigh
krsort Key Yes Hightolow
Thesefunctionsalwaystakeoneargument,thearray,andtheydonotreturnanything.Instead,theydirectlysortthearraywepasstothem.Let’sseesomeofthem:
<?php
$properties=[
'firstname'=>'Tom',
'surname'=>'Riddle',
'house'=>'Slytherin'
];
$properties1=$properties2=$properties3=$properties;
sort($properties1);
var_dump($properties1);
asort($properties3);
var_dump($properties3);
ksort($properties2);
var_dump($properties2);
Okay,thereisalotgoingoninthelastexample.Firstofall,weinitializeanarraywithsomekeyvaluesandassignitto$properties.Thenwecreatethreevariablesthatarecopiesoftheoriginalarray—thesyntaxshouldbeintuitive.Whydowedothat?Becauseifwesorttheoriginalarray,wewillnothavetheoriginalcontentanymore.Thisisnotwhatwewantinthisspecificexample,aswewanttoseehowthedifferentsortfunctionsaffectthesamearray.Finally,weperformthreedifferentsorts,andprinteachoftheresults.Thebrowsershouldshowyousomethinglikethefollowing:
array(3){
[0]=>
string(6)"Riddle"
[1]=>
string(9)"Slytherin"
[2]=>
string(3)"Tom"
}
array(3){
["surname"]=>
string(6)"Riddle"
["house"]=>
string(9)"Slytherin"
["firstname"]=>
string(3)"Tom"
}
array(3){
["firstname"]=>
string(3)"Tom"
["house"]=>
string(9)"Slytherin"
["surname"]=>
string(6)"Riddle"
}
Thefirstfunction,sort,ordersthevaluesalphabetically.Also,ifyoucheckthekeys,nowtheyarenumericasinalist,insteadoftheoriginalkeys.Instead,asortordersthevaluesinthesameway,butkeepstheassociationofkey-values.Finally,ksortorderstheelementsbytheirkeys,alphabetically.
TipHowtoremembersomanyfunctionnames
PHPhasalotoffunctionhelpersthatwillsaveyoufromwritingcustomizedfunctionsbyyourself,forexample,itprovidesyouwithupto13differentsortingfunctions.Andyoucanalwaysrelyontheofficialdocumentation.But,ofcourse,youwouldliketowritecodewithoutgoingbackandforthfromthedocs.So,herearesometipstorememberwhateachsortingfunctiondoes:
Anainthenamemeansassociative,andthus,willpreservethekey-valueassociation.Anrinthenamemeansreverse,sotheorderwillbefromhightolow.Akmeanskey,sothesortingwillbebasedonthekeysinsteadofthevalues.
OtherarrayfunctionsTherearearound80differentfunctionsrelatedtoarrays.Asyoucanimagine,youwillneverevenhearaboutsomeofthem,astheyhaveveryspecificpurposes.Thecompletelistcanbefoundathttp://php.net/manual/en/book.array.php.
Wecangetalistofthekeysofthearraywitharray_keys,andalistofitsvalueswitharray_values:
<?php
$properties=[
'firstname'=>'Tom',
'surname'=>'Riddle',
'house'=>'Slytherin'
];
$keys=array_keys($properties);
var_dump($keys);
$values=array_values($properties);
var_dump($values);
Wecangetthenumberofelementsinanarraywiththecountfunction:
<?php
$names=['Harry','Ron','Hermione'];
$size=count($names);
var_dump($size);//3
Andwecanmergetwoormorearraysintoonewitharray_merge:
<?php
$good=['Harry','Ron','Hermione'];
$bad=['Dudley','Vernon','Petunia'];
$all=array_merge($good,$bad);
var_dump($all);
Thelastexamplewillprintthefollowingarray:
array(6){
[0]=>
string(5)"Harry"
[1]=>
string(3)"Ron"
[2]=>
string(8)"Hermione"
[3]=>
string(6)"Dudley"
[4]=>
string(6)"Vernon"
[5]=>
string(7)"Petunia"
}
Asyoucansee,thekeysofthesecondarrayarenowdifferent,asoriginally,boththearrayshadthesamenumerickeys,andanarraycannothavetwovaluesforthesamekey.
PHPinwebapplicationsEventhoughthemainpurposeofthischapteristoshowyouthebasicsofPHP,doingitinareference-manualkindofawayisnotinterestingenough,andifweweretocopy-pastewhattheofficialdocumentationsays,youmightaswellgothereandreaditbyyourself.KeepinginmindthemainpurposeofthisbookandyourmaingoalistowritewebapplicationswithPHP,letusshowyouhowtoapplyeverythingyouarelearningassoonaspossible,beforeyougettoobored.
Inordertodothat,wewillnowstartonajourneytowardsbuildinganonlinebookstore.Attheverybeginning,youmightnotseetheusefulnessofit,butthatisjustbecausewe’vestillnotshownallthatPHPcando.
GettinginformationfromtheuserLet’sstartbybuildingahomepage.Inthispage,wearegoingtofigureoutiftheuserislookingforabookorjustwalkingby.Howdowefindthatout?TheeasiestwayrightnowistoinspecttheURLthattheuserusedtoaccessourapplication,andextractsomeinformationfromthere.
Savethiscontentasyourindex.php:
<?php
$looking=isset($_GET['title'])||isset($_GET['author']);
?>
<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Bookstore</title>
</head>
<body>
<p>Youlookin'?<?phpecho(int)$looking;?></p>
<p>Thebookyouarelookingforis</p>
<ul>
<li><b>Title</b>:<?phpecho$_GET['title'];?></li>
<li><b>Author</b>:<?phpecho$_GET['author'];?></li>
</ul>
</body>
</html>
Nowaccessthelink,http://localhost:8000/?author=HarperLee&title=ToKillaMockingbird.YouwillseethatthepageprintssomeoftheinformationthatyoupassedontotheURL.
Foreachrequest,PHPstoresalltheparametersthatcomefromthequerystringinanarraycalled$_GET.Eachkeyofthearrayisthenameoftheparameter,anditsassociatedvalueisthevalueoftheparameter.So$_GETcontainstwoentries:$_GET['author']containsHarperLeeand$_GET['title']hasthevalueToKillaMockingbird.
Inthefirsthighlightedline,weassignaBooleanvaluetothevariable$looking.Ifeither$_GET['title']or$_GET['author']exists,thatvariablewillbetrue,otherwisefalse.Justafterthat,weclosethePHPtagandthenweprintsomeHTML,butasyoucansee,weareactuallymixingtheHTMLwithsomePHPcode.
Anotherinterestinglinehereisthesecondhighlightedone.Beforeprintingthecontentof$looking,wecastthevalue.CastingmeansforcingPHPtotransformatypeofvaluetoanotherone.CastingaBooleantoanintegermeansthattheresultantvaluewillbe1iftheBooleanistrueor0iftheBooleanisfalse.As$lookingistruesince$_GETcontainsvalidkeys,thepageshowsa“1”.
Ifwetrytoaccessthesamepagewithoutsendinganyinformation,asinhttp://localhost:8000,thebrowserwillsayAreyoulookingforabook?0.DependingonthesettingsofyourPHPconfiguration,youwillseetwonoticemessagescomplainingthatyouaretryingtoaccesskeysofthearraythatdonotexist.
NoteCastingversustypejuggling
WealreadyknowthatwhenPHPneedsaspecifictypeofvariable,itwilltrytotransformit,whichiscalledtypejuggling.ButPHPisquiteflexible,sosometimes,youhavetobetheonespecifyingthetypethatyouneed.Whenprintingsomethingwithecho,PHPtriestotransformeverythingitgetsintostrings.SincethestringversionoftheBooleanfalseisanemptystring,thatwouldnotbeusefulforourapplication.CastingtheBooleantoanintegerfirstassuresthatwewillseeavalue,evenifitisjusta0.
HTMLformsHTMLformsareoneofthemostpopularwaysofcollectinginformationfromtheuser.Theyconsistofaseriesoffields—calledinputintheHTMLworld—andafinalsubmitbutton.InHTML,theformtagcontainstwoattributes:actionpointswheretheformwillbesubmitted,andmethod,whichspecifiestheHTTPmethodthattheformwilluse(GETorPOST).Let’sseehowitworks.Savethefollowingcontentaslogin.htmlandgotohttp://localhost:8000/login.html.
<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Bookstore-Login</title>
</head>
<body>
<p>Enteryourdetailstologin:</p>
<formaction="authenticate.php"method="post">
<label>Username</label>
<inputtype="text"name="username"/>
<label>Password</label>
<inputtype="password"name="password"/>
<inputtype="submit"value="Login"/>
</form>
</body>
</html>
Theformdefinedintheprecedingcodecontainstwofields,onefortheusernameandoneforthepassword.Youcanseethattheyareidentifiedbytheattributename.Ifyoutrytosubmitthisform,thebrowserwillshowyouaPageNotFoundmessage,asitistryingtoaccesshttp://localhost:8000/authenticate.phpandthewebservercannotfindit.Let’screateitthen:
<?php
$submitted=!empty($_POST);
?>
<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Bookstore</title>
</head>
<body>
<p>Formsubmitted?<?phpecho(int)$submitted;?></p>
<p>Yourlogininfois</p>
<ul>
<li><b>username</b>:<?phpecho$_POST['username'];?></li>
<li><b>password</b>:<?phpecho$_POST['password'];?></li>
</ul>
</body>
</html>
Aswith$_GET,$_POSTisanarraythatcontainstheparametersreceivedbyPOST.Inthis
precedingpieceofcode,wefirstaskifthatarrayisnotempty—notethe!operator.Afterwards,wejustdisplaytheinformationreceived,justasinindex.php.Noticethatthekeysofthe$_POSTarrayarethevaluesfortheargumentnameofeachinputfield.
PersistingdatawithcookiesWhenwewantthebrowsertoremembersomedatalikewhetheryouareloggedinornotonyourwebapplication,yourbasicinfo,andsoon,weusecookies.Cookiesarestoredontheclientsideandaresenttotheserverwhenmakingarequestasheaders.AsPHPisorientedtowardswebapplications,itallowsyoutomanagecookiesinaveryeasyway.
TherearefewthingsyouneedtoknowaboutcookiesandPHP.Youcanwritecookieswiththesetcookiefunctionthatacceptsseveralarguments:
Avalidnameforthecookieasastring.Thevalueofthecookie—onlystringsorvaluesthatcanbecastedtoastring.Thisparameterisoptional,andifnotset,PHPwillactuallyremovethecookie.Expirationtimeasatimestamp.Ifnotset,thecookiewillberemovedoncethebrowserisclosed.
NoteTimestamps
Computersusedifferentwaysfordescribingdatesandtimes,andaverycommonone,especiallyonUnixsystems,istheuseoftimestamps.TheyrepresentthenumberofsecondspassedsinceJanuary1,1970.Forexample,thetimestampthatrepresentsOctober4,2015at6:30p.m.wouldbe1,443,954,637,whichisthenumberofsecondssincethatdate.
YoucangetthecurrenttimestampwithPHPusingthetimefunction.
Thereareotherargumentsrelatedtosecurity,buttheyareoutofthescopeofthissection.Alsonotethatyoucanonlysetcookiesifthereisnopreviousoutputfromyourapplication,thatis,beforeHTML,echocalls,andanyothersimilarfunctionsthatsendsomeoutput.
Toreadthecookiesthattheclientsendstous,wejustneedtoaccessthearray,$_COOKIE.Itworksastheothertwoarrays,sothekeysofthearraywillbethenameofthecookiesandthevalueofthearraywillbetheirvalues.
Averycommonusageforcookiesisauthenticatingtheuser.Thereareseveraldifferentwaysofdoingso,dependingonthelevelofsecurityyouneedforyourapplication.Let’strytoimplementoneverysimple—albeitinsecureone(donotuseitforlivewebapplications).LeavingtheHTMLintact,updatethePHPpartofyourauthenticate.phpfilewiththefollowingcontent:
<?php
setcookie('username',$_POST['username']);
$submitted=!empty($_POST);
?>
Dothesamewiththebodytaginyourindex.php:
<body>
<p>Youare<?phpecho$_COOKIE['username'];?></p>
<p>Areyoulookingforabook?<?phpecho(int)$lookingForBook;?></p>
<p>Thebookyouarelookingforis</p>
<ul>
<li><b>Title</b>:<?phpecho$_GET['title'];?></li>
<li><b>Author</b>:<?phpecho$_GET['author'];?></li>
</ul>
</body>
Ifyouaccesshttp://localhost:8000/login.htmlagain,trytologin,openanewtab(inthesamebrowser),andgotothehomepageathttp://localhost:8000,youwillseehowthebrowserstillremembersyourusername.
Othersuperglobals$_GET,$_POST,and$_COOKIEarespecialvariablescalledsuperglobals.Thereareothersuperglobalstoo,like$_SERVERor$_ENV,whichwillgiveyouextrainformation.Thefirstoneshowsyouinformationaboutheaders,pathsaccessed,andotherinformationrelatedtotherequest.Thesecondonecontainstheenvironmentvariablesofthemachinewhereyourapplicationisrunning.Youcanseethefulllistofthesearraysandtheirelementsathttp://php.net/manual/es/language.variables.superglobals.php.
Ingeneral,usingsuperglobalsisuseful,sinceitallowsyoutogetinformationfromtheuser,thebrowser,therequest,andsoon.Thisisofimmeasurablevaluewhenwritingwebapplicationsthatneedtointeractwiththeuser.Butwithgreatpowercomesgreatresponsibility,andyoushouldbeverycarefulwhenusingthesearrays.Mostofthosevaluescomefromtheusersthemselves,whichcouldleadtosecurityissues.
ControlstructuresSofar,ourfileshavebeenexecutedlinebyline.Duetothat,wehavebeengettingnoticesonsomescenarios,suchaswhenthearraydoesnotcontainwhatwearelookingfor.Woulditnotbeniceifwecouldchoosewhichlinestoexecute?Controlstructurestotherescue!
Acontrolstructureislikeatrafficdiversionsign.Itdirectstheexecutionflowdependingonsomepredefinedconditions.Therearedifferentcontrolstructures,butwecancategorizetheminconditionalsandloops.Aconditionalallowsustochoosewhethertoexecuteastatementornot.Aloopexecutesastatementasmanytimesasyouneed.Let’stakealookateachoneofthem.
ConditionalsAconditionalevaluatesaBooleanexpression,thatis,somethingthatreturnsavalue.Iftheexpressionistrue,itwillexecuteeverythinginsideitsblockofcode.Ablockofcodeisagroupofstatementsenclosedby{}.Let’sseehowitworks:
<?php
echo"Beforetheconditional.";
if(4>3){
echo"Insidetheconditional.";
}
if(3>4){
echo"Thiswillnotbeprinted.";
}
echo"Aftertheconditional.";
Intheprecedingpieceofcode,weusetwoconditionals.AconditionalisdefinedbythekeywordiffollowedbyaBooleanexpressioninparenthesesandbyablockofcode.Iftheexpressionistrue,itwillexecutetheblock,otherwiseitwillskipit.
Youcanincreasethepowerofconditionalsbyaddingthekeywordelse.ThistellsPHPtoexecutesomeblockofcodeifthepreviousconditionswerenotsatisfied.Let’sseeanexample:
if(2>3){
echo"Insidetheconditional.";
}else{
echo"Insidetheelse.";
}
Theprecedingexamplewillexecutethecodeinsidetheelseastheconditionoftheifwasnotsatisfied.
Finally,youcanalsoaddanelseifkeywordfollowedbyanotherconditionandablockofcodetocontinueaskingPHPformoreconditions.Youcanaddasmanyelseifasyouneedafteranif.Ifyouaddanelse,ithastobethelastoneofthechainofconditions.AlsokeepinmindthatassoonasPHPfindsaconditionthatresolvestotrue,itwillstopevaluatingtherestofconditions.
<?php
if(4>5){
echo"Notprinted";
}elseif(4>4){
echo"Notprinted";
}elseif(4==4){
echo"Printed.";
}elseif(4>2){
echo"Notevaluated.";
}else{
echo"Notevaluated.";
}
if(4==4){
echo"Printed";
}
Inthelastexample,thefirstconditionthatevaluatestotrueisthehighlightedone.Afterthat,PHPdoesnotevaluateanymoreconditionsuntilanewifstarts.
Withthisknowledge,let’strytocleanupourapplicationabit,executingstatementsonlywhenneeded.Copythiscodetoyourindex.phpfile:
<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Bookstore</title>
</head>
<body>
<p>
<?php
if(isset($_COOKIE[username'])){
echo"Youare".$_COOKIE['username'];
}else{
echo"Youarenotauthenticated.";
}
?>
</p>
<?php
if(isset($_GET['title'])&&isset($_GET['author'])){
?>
<p>Thebookyouarelookingforis</p>
<ul>
<li><b>Title</b>:<?phpecho$_GET['title'];?></li>
<li><b>Author</b>:<?phpecho$_GET['author'];?></li>
</ul>
<?php
}else{
?>
<p>Youarenotlookingforabook?</p>
<?php
}
?>
</body>
</html>
Inthisnewcode,wehavemixedconditionalsandHTMLcodeintwodifferentways.ThefirstoneopensaPHPtag,andaddsanif…elseclausethatwillprintwhetherweareauthenticatedornotwithanecho.NoHTMLismergedwithintheconditionals,whichmakesitclear.
Thesecondoption—thesecondhighlightedblock—showsanugliersolution,butsometimesnecessary.WhenyouhavetoprintalotofHTMLcode,echoisnotthathandy,anditisbettertoclosethePHPtag,printallHTMLyouneed,andthenopenthetagagain.Youcandothateveninsidethecodeblockofanifclauseasyoucanseeinthecode.
NoteMixingPHPandHTML
Ifyoufeelthatthelastfileweeditedlooksratherugly,youareright.MixingPHPandHTMLisconfusing,andyoushouldavoidit.InChapter6,AdaptingtoMVC,wewillseehowtodothingsproperly.
Let’seditourauthenticate.phpfiletoo,asitistryingtoaccessthe$_POSTentriesthatmightnotbethere.Thenewcontentofthefilewouldbeasfollows:
<?php
$submitted=isset($_POST['username'])&&isset($_POST['password']);
if($submitted){
setcookie('username',$_POST['username']);
}
?>
<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Bookstore</title>
</head>
<body>
<?phpif($submitted):?>
<p>Yourlogininfois</p>
<ul>
<li><b>username</b>:<?phpecho$_POST['username'];?></li>
<li><b>password</b>:<?phpecho$_POST['password'];?></li>
</ul>
<?phpelse:?>
<p>Youdidnotsubmitanything.</p>
<?phpendif;?>
</body>
</html>
Thiscodealsocontainsconditionals,whichwealreadyknow.Wearesettingavariabletoknowifwesubmittedaloginornot,andsetthecookiesifso.ButthehighlightedlinesshowyouanewwayofincludingconditionalswithHTML.ThismakesthecodemorereadablewhenworkingwithHTMLcode,avoidingtheuseof{},andinsteadusing:andendif.Bothsyntaxesarecorrect,andyoushouldusetheonethatyouconsidermorereadableineachcase.
Switch…caseAnothercontrolstructuresimilartoif…elseisswitch…case.Thisstructureevaluatesonlyoneexpression,andexecutestheblockdependingonitsvalue.Let’sseeanexample:
<?php
switch($title){
case'HarryPotter':
echo"Nicestory,abittoolong.";
break;
case'LordoftheRings':
echo"Aclassic!";
break;
default:
echo"Dunnothatone.";
break;
}
Theswitchclausetakesanexpression,inthiscaseavariable,andthendefinesaseriesofcases.Whenthecasematchesthecurrentvalueoftheexpression,PHPexecutesthecodeinsideit.AssoonasPHPfindsabreakstatement,itexitstheswitch…case.Incasenoneofthecasesaresuitablefortheexpression,PHPexecutesthedefault,ifthereisone,butthatisoptional.
Youalsoneedtoknowthatbreaksaremandatoryifyouwanttoexittheswitch…case.Ifyoudonotspecifyany,PHPwillkeeponexecutingstatements,evenifitencountersanewcase.Let’sseeasimilarexample,butwithoutthebreaks:
<?php
$title='Twilight';
switch($title){
case'HarryPotter':
echo"Nicestory,abittoolong.";
case'Twilight':
echo'Uh…';
case'LordoftheRings':
echo"Aclassic!";
default:
echo"Dunnothatone.";
}
Ifyoutestthiscodeinyourbrowser,youwillseethatitprintsUh…Aclassic!Dunnothatone.PHPfoundthatthesecondcaseisvalid,soitexecutesitscontent.Butastherearenobreaks,itkeepsonexecutinguntiltheend.Thismightbethedesiredbehaviorsometimesbutnotusually,sobecarefulwhenusingit!
LoopsLoopsarecontrolstructuresthatallowyoutoexecutecertainstatementsseveraltimes,asmanytimesasyouneed.Youmightusetheminseveraldifferentscenarios,butthemostcommononeiswheninteractingwitharrays.Forexample,imagineyouhaveanarraywithelements,butyoudonotknowwhatisinit.Youwanttoprintallitselements,soyouloopthroughallofthem.
Therearefourtypesofloops.Eachofthemhasitsownusecases,butingeneral,youcantransformonetypeofloopintoanother.Let’slookatthemclosely.
WhileThewhileloopisthesimplestoftheloops.Itexecutesablockofcodeuntiltheexpressiontoevaluatereturnsfalse.Let’sseeoneexample:
<?php
$i=1;
while($i<4){
echo$i."";
$i++;
}
Intheprecedingexample,wedefineavariablewithvalue1.Thenwehaveawhileclauseinwhichtheexpressiontoevaluateis$i<4.Thisloopexecutesthecontentoftheblockofcodeuntilthatexpressionisfalse.Asyoucansee,insidetheloopweareincrementingthevalueof$iby1eachtime,sotheloopendsafter4iterations.Checktheoutputofthatscriptandyouwillsee“0123”.Thelastvalueprintedis3,soatthattimethevalueof$iwas3.Afterthat,weincreaseditsvalueto4,sowhenthewhileclauseevaluatesif$i<4,theresultisfalse.
NoteWhilesandinfiniteloops
Oneofthemostcommonproblemswiththewhileloopsiscreatinganinfiniteloop.Ifyoudonotaddanycodeinsidethewhileloopthatupdatesanyofthevariablesconsideredinthewhileexpressionsuchthatitcanbefalseatsomepoint,PHPwillneverexittheloop!
Do…whileThedo…whileloopisverysimilartowhileinthesensethatitevaluatesanexpressioneachtime,andwillexecutetheblockofcodeuntilthatexpressionisfalse.Theonlydifferenceisthatwhenthisexpressionisevaluated,thewhileclauseevaluatestheexpressionbeforeexecutingthecode,sosometimes,wemightnotevenentertheloopiftheexpressionevaluatestofalsetheveryfirsttime.Ontheotherhand,do…whileevaluatestheexpressionafteritexecutesitsblockofcode,soeveniftheexpressionisfalsefromtheverybeginning,theloopwillbeexecutedatleastonce.
<?php
echo"withwhile:";
$i=1;
while($i<0){
echo$i."";
$i++;
}
echo"withdo-while:";
$i=1;
do{
echo$i."";
$i++;
}while($i<0);
Theprecedingpieceofcodedefinestwoloopswiththesameexpressionandblockofcode,butifyouexecutethem,youwillseethatonlythecodeinsidethedo…whileisexecuted.Inbothcases,theexpressionisfalsesincethebeginning,sowhiledoesnotevenentertheloop,whereasthedo…whileentersthelooponce.
ForTheforloopisthemostcomplexofthefourloops.Itdefinesaninitializationexpression,anexitcondition,andtheendofaniterationexpression.WhenPHPfirstencounterstheloop,itexecuteswhatisdefinedastheinitializationexpression.Then,itevaluatestheexitconditionandifitresolvestotrue,itenterstheloop.Afterexecutingeverythinginsidetheloop,itexecutestheendoftheiterationexpression.Oncedone,itevaluatestheendconditionagain,goingthroughtheloopcodeandtheendoftheiterationexpression,untilitevaluatestofalse.Asalways,anexamplewillclarifyit:
<?php
for($i=1;$i<10;$i++){
echo$i."";
}
Theinitializationexpressionis$i=1,andisexecutedonlythefirsttime.Theexitconditionis$i<10,anditisevaluatedatthebeginningofeachiteration.Theendoftheiterationexpressionis$i++,whichisexecutedattheendofeachiteration.Thisexampleprintsthenumbersfrom1to9.Anothermorecommonusageoftheforloopiswitharrays:
<?php
$names=['Harry','Ron','Hermione'];
for($i=0;$i<count($names);$i++){
echo$names[$i]."";
}
Inthisexample,wehaveanarrayofnames.Sinceitisdefinedasalist,itskeyswillbe0,1,and2.Theloopinitializesthevariable$ito0,andititeratesuntilthevalueof$iisnotlessthanthenumberofelementsinthearray,thatis,3.Inthefirstiteration,$iis0,inthesecond,itis1,andinthethirdoneitisequalto2.When$iis3,itwillnotentertheloop,astheexitconditionevaluatestofalse.
Oneachiteration,weprintthecontentoftheposition$iofthearray,hencetheresultofthiscodewillbeallthreenamesinthearray.
TipBecarefulwithexitconditions
Itisverycommontosetanexitconditionthatisnotexactlywhatweneed,especiallywitharrays.Rememberthatarraysstartwith0iftheyarealist,soanarrayofthreeelementswillhaveentriesof0,1,and2.Definingtheexitconditionas$i<=count($array)willcauseanerrorinyourcode,aswhen$iis3,italsosatisfiestheexitconditionandwilltrytoaccessthekey3,whichdoesnotexist.
ForeachThelast,butnotleast,typeofloopisforeach.Thisloopisexclusiveforarrays,anditallowsyoutoiterateanarrayentirely,evenifyoudonotknowitskeys.Therearetwooptionsforthesyntax,asyoucanseeinthefollowingexamples:
<?php
$names=['Harry','Ron','Hermione'];
foreach($namesas$name){
echo$name."";
}
foreach($namesas$key=>$name){
echo$key."->".$name."";
}
Theforeachloopacceptsanarray—inthiscase$names—anditspecifiesavariablewhichwillcontainthevalueoftheentryofthearray.Youcanseethatwedonotneedtospecifyanyendcondition,asPHPwillknowwhenthearrayhasbeeniterated.Optionally,youcanspecifyavariablethatcontainsthekeyofeachiteration,asinthesecondloop.
Theforeachloopsarealsousefulwithmaps,wherethekeysarenotnecessarilynumeric.TheorderinwhichPHPiteratesthearraywillbethesameorderthatyouusedtoinsertthecontentsinthearray.
Let’susesomeloopsinourapplication.Wewanttoshowtheavailablebooksinourhomepage.Wehavethelistofbooksinanarray,sowewillhavetoiterateallofthemwithaforeachloop,printingsomeinformationfromeachone.Appendthefollowingcodetothebodytaginindex.php:
<?phpendif;
$books=[
[
'title'=>'ToKillAMockingbird',
'author'=>'HarperLee',
'available'=>true,
'pages'=>336,
'isbn'=>9780061120084
],
[
'title'=>'1984',
'author'=>'GeorgeOrwell',
'available'=>true,
'pages'=>267,
'isbn'=>9780547249643
],
[
'title'=>'OneHundredYearsOfSolitude',
'author'=>'GabrielGarciaMarquez',
'available'=>false,
'pages'=>457,
'isbn'=>9785267006323
],
];
?>
<ul>
<?phpforeach($booksas$book):?>
<li>
<i><?phpecho$book['title'];?></i>
-<?phpecho$book['author'];?>
<?phpif(!$book['available']):?>
<b>Notavailable</b>
<?phpendif;?>
</li>
<?phpendforeach;?>
</ul>
Thehighlightedcodeshowsaforeachloopusingthe:notationaswell,whichisbetterwhenmixingitwithHTML.Ititeratesallofthe$booksarray,andforeachbook,itprintssomeinformationasanHTMLlist.Noticealsothatwehaveaconditionalinsidealoop,whichisperfectlyfine.Ofcourse,thisconditionalwillbeexecutedforeachentryinthearray,soyoushouldkeeptheblockofcodeofyourloopsassimpleaspossible.
FunctionsAfunctionisareusableblockofcodethat,givenaninput,performssomeactionsand,optionally,returnssomeresult.Youalreadyknowseveralpredefinedfunctionslikeempty,in_array,orvar_dump.ThosefunctionscomewithPHPsoyoudonothavetoreinventthewheel,butyoucancreateyourownveryeasily.Youcandefinefunctionswhenyouidentifyportionsofyourapplicationthathavetobeexecutedseveraltimes,orjusttoencapsulatesomefunctionality.
FunctiondeclarationDeclaringafunctionmeanswritingitdownsoitcanbeusedlater.Afunctionhasaname,takessomearguments,andhasablockofcode.Optionally,itcandefinewhatkindofvalueistobereturned.Thenameofthefunctionhastofollowthesamerulesasvariablenames,thatis,ithastostartwithaletteroranunderscore,andcancontainanyletters,numbers,orunderscore.Itcannotbeareservedword.
Let’sseeasimpleexample:
functionaddNumbers($a,$b){
$sum=$a+$b;
return$sum;
}
$result=addNumbers(2,3);
Theprecedingfunction’snameisaddNumbers,andittakestwoarguments:$aand$b.Theblockofcodedefinesanewvariable$sum,whichisthesumofbotharguments,andthenreturnsitscontentwithreturn.Inordertousethisfunction,youjustneedtocallitbyitsnamewhilesendingalltherequiredarguments,asshowninthehighlightedline.
PHPdoesnotsupportoverloadedfunctions.Overloadingreferstotheabilityofdeclaringtwoormorefunctionswiththesamenamebutdifferentarguments.Asyoucansee,youcandeclaretheargumentswithoutknowingwhattheirtypesare,soPHPwouldnotbeabletodecidewhichfunctiontouse.
Anotherimportantthingtonoteisthevariablescope.Wearedeclaringavariable$suminsidetheblockofcode,sooncethefunctionends,thevariablewillnotbeaccessibleanymore.Thatmeansthatthescopeofvariablesdeclaredinsidethefunctionisjustthefunctionitself.Furthermore,ifyouhadavariable$sumdeclaredoutsidethefunction,itwouldnotbeaffectedatallsincethefunctioncannotaccessthatvariableunlesswesenditasanargument.
FunctionargumentsAfunctiongetsinformationfromoutsideviaarguments.Youcandefineanynumberofarguments—including0(none).Theseargumentsneedatleastanamesotheycanbeusedinsidethefunction;therecannotbetwoargumentswiththesamename.Wheninvokingthefunction,youneedtosendtheargumentsinthesameorderasdeclared.
Afunctionmaycontainoptionalarguments,thatis,youarenotforcedtoprovideavalueforthosearguments.Whendeclaringthefunction,youneedtoprovideadefaultvalueforthosearguments.So,incasetheuserdoesnotprovideavalue,thefunctionwillusethedefaultone.
functionaddNumbers($a,$b,$printResult=false){
$sum=$a+$b;
if($printResult){
echo'Theresultis'.$sum;
}
return$sum;
}
$sum1=addNumbers(1,2);
$sum1=addNumbers(3,4,false);
$sum1=addNumbers(5,6,true);//itwillprinttheresult
Thisnewfunctioninthelastexampletakestwomandatoryargumentsandanoptionalone.Thedefaultvalueoftheoptionalargumentisfalse,anditisthenusednormallyinsidethefunction.Thefunctionwillprinttheresultofthesumiftheuserprovidestrueasthethirdargument,whichhappensonlythethirdtimethatthefunctionisinvoked.Forthefirsttwo,$printResultissettofalse.
Theargumentsthatthefunctionreceivesarejustcopiesofthevaluesthattheuserprovided.Thatmeansthatifyoumodifytheseargumentsinsidethefunction,itwillnotaffecttheoriginalvalues.Thisfeatureisknownassendingargumentsbyvalue.Let’sseeanexample:
functionmodify($a){
$a=3;
}
$a=2;
modify($a);
var_dump($a);//prints2
Wearedeclaringavariable$awithvalue2,andthencallingthemodifymethodsendingthat$a.Themodifymethodmodifiestheargument$a,settingitsvalueto3,butthisdoesnotaffecttheoriginalvalueof$a,whichremains2asyoucanseefromvar_dump.
Ifwhatyouwantistoactuallychangethevalueoftheoriginalvariableusedintheinvocation,youneedtopasstheargumentbyreference.Todothat,youaddanampersand(&)beforetheargumentwhendeclaringthefunction:
functionmodify(&$a){
$a=3;
}
Now,oninvokingthefunctionmodify,$awillalwaysbe3.
NoteArgumentsbyvalueversusbyreference
PHPallowsyoutodoit,andinfact,somenativefunctionsofPHPuseargumentsbyreference.Rememberthearraysortingfunctions?Theydidnotreturnthesortedarray,butsortedthearrayprovidedinstead.Butusingargumentsbyreferenceisawayofconfusingdevelopers.Usually,whensomeoneusesafunction,theyexpectaresult,andtheydonotwanttheargumentsprovidedbythemtobemodified.Sotrytoavoidit;peoplewillbegrateful!
ThereturnstatementYoucanhaveasmanyreturnstatementsasyouwantinsideyourfunction,butPHPwillexitthefunctionassoonasitfindsone.Thatmeansthatifyouhavetwoconsecutivereturnstatements,thesecondonewillneverbeexecuted.Still,havingmultiplereturnstatementscanbeusefuliftheyareinsideconditionals.Addthisfunctioninsideyourfunctions.phpfile:
functionloginMessage(){
if(isset($_COOKIE['username'])){
return"Youare".$_COOKIE['username'];
}else{
return"Youarenotauthenticated.";
}
}
Andlet’susethelastexampleinyourindex.phpfilebyreplacingthehighlightedcontent(notethattosavesometrees,Ireplacedmostofthecodethatwasnotchangedatallwith//…):
//...
<body>
<p><?phpechologinMessage();?></p>
<?phpif(isset($_GET['title'])&&isset($_GET['author'])):?>
//...
Additionally,youcanomitthereturnstatementifyoudonotwantthefunctiontoreturnanything.Inthiscase,thefunctionwillendonceitreachestheendoftheblockofcode.
TypehintingandreturntypesWiththereleaseofPHP7,thelanguageallowsthedevelopertobemorespecificaboutwhatfunctionsaregettingandreturning.Youcan—alwaysoptionally—specifythetypeofargumentthatthefunctionneeds(typehinting),andthetypeofresultthefunctionwillreturn(returntype).Let’sfirstseeanexample:
<?php
declare(strict_types=1);
functionaddNumbers(int$a,int$b,bool$printSum):int{
$sum=$a+$b;
if($printSum){
echo'Thesumis'.$sum;
}
return$sum;
}
addNumbers(1,2,true);
addNumbers(1,'2',true);//itfailswhenstrict_typesis1
addNumbers(1,'something',true);//italwaysfails
Thisprecedingfunctionstatesthattheargumentsneedtobeinteger,integer,andBoolean,andthattheresultwillbeaninteger.Now,youknowthatPHPhastypejuggling,soitcanusuallytransformavalueofonetypetoitsequivalentvalueofanothertype,forexample,thestring“2”canbeusedasinteger2.TostopPHPfromusingtypejugglingwiththeargumentsandresultsoffunctions,youcandeclarethedirectivestrict_typesasshowninthefirsthighlightedline.Thisdirectivehastobedeclaredatthetopofeachfilewhereyouwanttoenforcethisbehavior.
Thethreeinvocationsworkasfollows:
ThefirstinvocationsendstwointegersandaBoolean,whichiswhatthefunctionexpects,soregardlessofthevalueofstrict_types,itwillalwayswork.Thesecondinvocationsendsaninteger,astring,andaBoolean.Thestringhasavalidintegervalue,soifPHPwasallowedtousetypejuggling,theinvocationwouldresolvejustnormally.Butinthisexample,itwillfailbecauseofthedeclarationatthetopofthefile.Thethirdinvocationwillalwaysfailasthestring“something”cannotbetransformedintoavalidinteger.
Let’strytouseafunctionwithinourproject.Inourindex.php,wehaveaforeachloopthatiteratesthebooksandprintsthem.ThecodeinsidetheloopiskindofhardtounderstandasitisamixofHTMLwithPHP,andthereisaconditionaltoo.Let’strytoabstractthelogicinsidetheloopintoafunction.First,createthenewfunctions.phpfilewiththefollowingcontent:
<?php
functionprintableTitle(array$book):string{
$result='<i>'.$book['title'].'</i>-'.$book['author'];
if(!$book['available']){
$result.='<b>Notavailable</b>';
}
return$result;
}
Thisfilewillcontainourfunctions.Thefirstone,printableTitle,takesanarrayrepresentingabook,andbuildsastringwithanicerepresentationofthebookinHTML.Thecodeisthesameasbefore,justencapsulatedinafunction.
Nowindex.phpwillhavetoincludethefunctions.phpfile,andthenusethefunctioninsidetheloop.Let’sseehow:
<?phprequire_once'functions.php'?>
<!DOCTYPEhtml>
<htmllang="en">
//...
?>
<ul>
<?phpforeach($booksas$book):?>
<li><?phpechoprintableTitle($book);?></li>
<?phpendforeach;?>
</ul>
//...
Well,nowourlooplookswaycleaner,right?Also,ifweneedtoprintthetitleofthebooksomewhereelse,wecanreusethefunctioninsteadofduplicatingcode!
ThefilesystemAsyoumighthavealreadynoticed,PHPcomeswithalotofnativefunctionsthathelpyoutomanagearraysandstringsinaneasierwayascomparedtootherlanguages.ThefilesystemisanotherofthoseareaswherePHPtriedtomakeitaseasyaspossible.Thelistoffunctionsextendstoover80differentones,sowewillcoverherejusttheonesthatyouaremorelikelytouse.
ReadingfilesInourcode,wedefinealistofbooks.Sofar,wehaveonlythreebooks,butyoucanguessthatifwewanttomakethisapplicationuseful,thelistwillgrowwaymore.Storingtheinformationinsideyourcodeisnotpracticalatall,sowehavetostartthinkingaboutexternalizingit.
Ifwethinkintermsofseparatingthecodefromthedata,thereisnoneedtokeepusingPHParraystodefinethebooks.Usingalesslanguage-restrictivesystemwillallowpeoplewhodonotknowPHPtoeditthecontentofthefile.Therearemanysolutionsforthis,likeCSVorXMLfiles,butnowadays,oneofthemostusedsystemstorepresentdatainwebapplicationsisJSON.PHPallowsyoutoconvertarraystoJSONandviceversausingjustacoupleoffunctions:json_encodeandjson_decode.Easy,right?
Savethefollowingintobooks.json:
[
{
"title":"ToKillAMockingbird",
"author":"HarperLee",
"available":true,
"pages":336,
"isbn":9780061120084
},
{
"title":"1984",
"author":"GeorgeOrwell",
"available":true,
"pages":267,
"isbn":9780547249643
},
{
"title":"OneHundredYearsOfSolitude",
"author":"GabrielGarciaMarquez",
"available":false,
"pages":457,
"isbn":9785267006323
}
]
TheprecedingcodesnippetisaJSONrepresentationofourarrayinPHP.Now,let’sreadthisinformationwiththefunctionfile_get_contents,andtransformittoaPHParraywithjson_decode.Replacethearraywiththesetwolines:
$booksJson=file_get_contents('books.json');
$books=json_decode($booksJson,true);
Withjustonefunction,weareabletostoreallthecontentfromtheJSONfileinavariableasastring.Withthefunction,wetransformthisJSONstringintoanarray.Thesecondargumentinjson_decodetellsPHPtotransformittoanarray,otherwiseitwoulduseobjects,whichwehavenotcoveredasyet.
WhenreferencingfileswithinPHPfunctions,youneedtoknowwhethertouseabsolute
orrelativepaths.Whenusingrelativepaths,PHPwilltrytofindthefileinsidethesamedirectorywherethePHPscriptis.Ifnotfound,PHPwilltrytofinditinotherdirectoriesdefinedintheinclude_pathdirective,butthatissomethingyouwouldliketoavoid.Instead,youcoulduseabsolutepaths,whichisawaytomakesurethereferencewillnotbemisunderstood.Let’sseetwoexamples:
$booksJson=file_get_contents('/home/user/bookstore/books.json');
$booksJson=file_get_contents(__DIR__,'/books.json');
Theconstant__DIR__containsthedirectorynameofthecurrentPHPfile,andifweprefixittothenameofourfile,wewillhaveanabsolutepath.Infact,eventhoughyoumightthinkthatwritingdownthewholepathbyyourselfisbetter,using__DIR__allowsyoutomoveyourapplicationanywhereelsewithoutneedingtochangeanythinginthecode,asitscontentwillalwaysmatchthedirectoryofthescript,whereasthehardcodedpathfromthefirstexamplewillnotbevalidanymore.
WritingfilesLet’saddsomefunctionalitytoourapplication.Imaginethatwewanttoallowtheusertotakethebookthatheorsheislookingfor,butonlyifitisavailable.Ifyouremember,weidentifythebookbythequerystring.Thatisnotverypractical,solet’shelptheuserbyaddinglinkstothelistofbooks,sowhenyouclickonalink,thequerystringwillcontainthatbook’sinformation.
<?phprequire_once'functions.php'?>
<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Bookstore</title>
</head>
<body>
<p><?phpechologinMessage();?></p>
<?php
$booksJson=file_get_contents('books.json');
$books=json_decode($booksJson,true);
if(isset($_GET['title'])){
echo'<p>Lookingfor<b>'.$_GET['title'].'</b></p>';
}else{
echo'<p>Youarenotlookingforabook?</p>';
}
?>
<ul>
<?phpforeach($booksas$book):?>
<li>
<ahref="?title=<?phpecho$book['title'];?>">
<?phpechoprintableTitle($book);?>
</a>
</li>
<?phpendforeach;?>
</ul>
</body>
</html>
Ifyoutrytheprecedingcodeinyourbrowser,youwillseethatthelistcontainslinks,andbyclickingonthem,thepagerefresheswiththenewtitleaspartofthequerystring.Let’snowcheckifthebookisavailableornot,andifitis,let’supdateitsavailablefieldtofalse.Addthefollowingfunctioninyourfunctions.php:
functionbookingBook(array&$books,string$title):bool{
foreach($booksas$key=>$book){
if($book['title']==$title){
if($book['available']){
$books[$key]['available']=false;
returntrue;
}else{
returnfalse;
}
}
}
returnfalse;
}
Wehavetopayattentionasthecodestartsgettingcomplex.Thisfunctiontakesanarrayofbooksandatitle,andreturnsaBoolean,beingtrueifitcouldbookitorfalseifnot.Moreover,thearrayofbooksispassedbyreference,whichmeansthatallchangestothatarraywillaffecttheoriginalarraytoo.Eventhoughwediscouragedthispreviously,inthiscase,itisareasonableapproach.
Weiteratethewholearrayofbooks,askingeachtimeifthetitleofthecurrentbookmatchestheonewearelookingfor.Onlyifthatistrue,wewillcheckifthebookisavailableornot.Ifitis,wewillupdatetheavailabilitytofalseandreturntrue,meaningthatwebookedthebook.Ifthebookisnotavailable,wewilljustreturnfalse.
Finally,notethatforeachdefines$keyand$book.Wedosobecausethe$bookvariableisacopyofthe$booksarray,andifweeditit,theoriginalonewillnotbeaffected.Instead,weaskforthekeyofthatbooktoo,sowheneditingthearray,weuse$books[$key]insteadof$book.
Wecanusethisfunctionfromtheindex.phpfile:
//...
echo'<p>Lookingfor<b>'.$_GET['title'].'</b></p>';
if(bookingBook($books,$_GET['title'])){
echo'Booked!';
}else{
echo'Thebookisnotavailable…';
}
}else{
//...
Tryitoutinyourbrowser.Byclickingonanavailablebook,youwillgettheBooked!message.Wearealmostdone!Wearejustmissingthelastpart:persistthisinformationbacktothefilesystem.Inordertodothat,wehavetoconstructthenewJSONcontentandthentowriteitbacktothebooks.jsonfile.Ofcourse,let’sdothatonlyifthebookwasavailable.
functionupdateBooks(array$books){
$booksJson=json_encode($books);
file_put_contents(__DIR__.'/books.json',$booksJson);
}
Thejson_encodefunctiondoestheoppositeofjson_decode:ittakesanarray—oranyothervariable—andtransformsittoJSON.Thefile_put_contentsfunctionisusedtowritetothefilereferencedasthefirstargument,thecontentsentasthesecondargument.Wouldyouknowhowtousethisfunction?
//...
if(bookingBook($books,$_GET['title'])){
echo'Booked!';
updateBooks($books);
}else{
echo'Thebookisnotavailable…';
}
//...
NoteFilesversusdatabases
StoringinformationinJSONfilesisbetterthanhavingitinyourcode,butitisstillnotthebestoption.InChapter5,UsingDatabases,youwilllearnhowtostoredataoftheapplicationinadatabase,whichisawaybettersolution.
OtherfilesystemfunctionsIfyouwanttomakeyourapplicationmorerobust,youcouldcheckthatthebooks.jsonfileexists,thatyouhavereadandwritepermission,and/orthatthepreviouscontentwasavalidJSON.YoucanusesomePHPfunctionsforthat:
file_exists:Thisfunctiontakesthepathofthefile,andreturnsaBoolean:truewhenthefileexistsandfalseotherwise.is_writable:Thisfunctionworksthesameasfile_exists,butcheckswhetherthefileiswritableornot.
Youcanfindthefulllistoffunctionsathttp://uk1.php.net/manual/en/book.filesystem.php.Youcanfindfunctionstomove,copy,orremovefiles,createdirectories,givepermissionsandownership,andsoon.
SummaryInthischapter,wewentthroughallthebasicsofproceduralPHPwhilewritingsimpleexamplesinordertopracticethem.Younowknowhowtousevariablesandarrayswithcontrolstructuresandfunctions,howtogetinformationfromHTTPrequests,andhowtointeractwiththefilesystemamongotherthings.
Inthenextchapter,wewillstudytheotherandmostusedparadigm:OOP.Thatisonestepclosertowritingcleanandwell-structuredapplications.
Chapter4.CreatingCleanCodewithOOPWhenapplicationsstartgrowing,representingmorecomplexdatastructuresbecomesnecessary.Primitivetypeslikeintegers,strings,orarraysarenotenoughwhenyouwanttoassociatespecificbehaviortodata.Morethanhalfacenturyago,computerscientistsstartedusingtheconceptofobjectstorefertotheencapsulationofpropertiesandfunctionalitythatrepresentedanobjectinreallife.
Nowadays,OOPisoneofthemostusedprogrammingparadigms,andyouwillbegladtoknowthatPHPsupportsit.KnowingOOPisnotjustamatterofknowingthesyntaxofthelanguage,butknowingwhenandhowtouseit.Butdonotworry,afterthischapterandabitofpractice,youwillbecomeaconfidentOOPdeveloper.
Inthischapter,youwilllearnaboutthefollowing:
ClassesandobjectsVisibility,staticproperties,andmethodsNamespacesAutoloadingclassesInheritance,interfaces,andtraitsHandlingexceptionsDesignpatternsAnonymousfunctions
ClassesandobjectsObjectsarerepresentationsofreal-lifeelements.Eachobjecthasasetofattributesthatdifferentiatesitfromtherestoftheobjectsofthesameclass,andiscapableofasetofactions.Aclassisthedefinitionofwhatanobjectlookslikeandwhatitcando,likeapatternforobjects.
Let’stakeourbookstoreexample,andthinkofthekindofreal-lifeobjectsitcontains.Westorebooks,andletpeopletakethemiftheyareavailable.Wecouldthinkoftwotypesofobjects:booksandcustomers.Wecandefinethesetwoclassesasfollows:
<?php
classBook{
}
classCustomer{
}
Aclassisdefinedbythekeywordclassfollowedbyavalidclassname—thatfollowsthesamerulesasanyotherPHPlabel,likevariablenames—andablockofcode.Butifwewanttohaveaspecificbook,thatis,anobjectBook—orinstanceoftheclassBook—wehavetoinstantiateit.Toinstantiateanobject,weusethekeywordnewfollowedbythenameoftheclass.Weassigntheinstancetoavariable,asifitwasaprimitivetype:
$book=newBook();
$customer=newCustomer();
Youcancreateasmanyinstancesasyouneed,aslongasyouassignthemtodifferentvariables:
$book1=newBook();
$book2=newBook();
ClasspropertiesLet’sthinkaboutthepropertiesofbooksfirst:theyhaveatitle,anauthor,andanISBN.Theycanalsobeavailableorunavailable.WritethefollowingcodeinsideBook.php:
<?php
classBook{
public$isbn;
public$title;
public$author;
public$available;
}
Thisprecedingsnippetdefinesaclassthatrepresentsthepropertiesthatabookhas.Donotbotheraboutthewordpublic;wewillexplainwhatitmeanswhentalkingaboutvisibilityinthenextsection.Fornow,justthinkofpropertiesasvariablesinsidetheclass.Wecanusethesevariablesinobjects.TryaddingthiscodeattheendoftheBook.phpfile:
$book=newBook();
$book->title="1984";
$book->author="GeorgeOrwell";
$book->available=true;
var_dump($book);
Printingtheobjectshowsthevalueofeachofitsproperties,inawaysimilartothewayarraysdowiththeirkeys.Youcanseethatpropertieshaveatypeatthemomentofprinting,butwedidnotdefinethistypeexplicitly;instead,thevariabletookthetypeofthevalueassigned.Thisworksexactlythesamewaythatnormalvariablesdo.
Whencreatingmultipleinstancesofanobjectandassigningvaluestotheirproperties,eachobjectwillhavetheirownvalues,soyouwillnotoverridethem.Thenextbitofcodeshowsyouhowthisworks:
$book1=newBook();
$book1->title="1984";
$book2=newBook();
$book2->title="ToKillaMockingbird";
var_dump($book1,$book2);
ClassmethodsMethodsarefunctionsdefinedinsideaclass.Likefunctions,methodsgetsomeargumentsandperformsomeactions,optionallyreturningavalue.Theadvantageofmethodsisthattheycanusethepropertiesoftheobjectthatinvokedthem.Thus,callingthesamemethodintwodifferentobjectsmighthavetwodifferentresults.
EventhoughitisusuallyabadideatomixHTMLwithPHP,forthesakeoflearning,let’saddamethodinourclassBookthatreturnsthebookasinouralreadyexistingfunctionprintableTitle:
<?php
classBook{
public$isbn;
public$title;
public$author;
public$available;
publicfunctiongetPrintableTitle():string{
$result='<i>'.$this->title
.'</i>-'.$this->author;
if(!$this->available){
$result.='<b>Notavailable</b>';
}
return$result;
}
}
Aswithproperties,weaddthekeywordpublicatthebeginningofthefunction,butotherthanthat,therestlooksjustasanormalfunction.Theotherspecialbitistheuseof$this:itrepresentstheobjectitself,andallowsyoutoaccessthepropertiesandmethodsofthatsameobject.Notehowwerefertothetitle,author,andavailableproperties.
Youcanalsoupdatethevaluesofthecurrentobjectfromoneofitsfunctions.Let’susetheavailablepropertyasanintegerthatshowsthenumberofunitsavailableinsteadofjustaBoolean.Withthat,wecanallowmultiplecustomerstoborrowdifferentcopiesofthesamebook.Let’saddamethodtogiveonecopyofabooktoacustomer,updatingthenumberofunitsavailable:
publicfunctiongetCopy():bool{
if($this->available<1){
returnfalse;
}else{
$this->available--;
returntrue;
}
}
Inthisprecedingmethod,wefirstcheckifwehaveatleastoneavailableunit.Ifwedonot,wereturnfalsetoletthemknowthattheoperationwasnotsuccessful.Ifwedohaveaunitforthecustomer,wedecreasethenumberofavailableunits,andthenreturntrue,
lettingthemknowthattheoperationwassuccessful.Let’sseehowyoucanusethisclass:
<?php
$book=newBook();
$book->title="1984";
$book->author="GeorgeOrwell";
$book->isbn=9785267006323;
$book->available=12;
if($book->getCopy()){
echo'Here,yourcopy.';
}else{
echo'Iamafraidthatbookisnotavailable.';
}
Whatwouldthislastpieceofcodeprint?Exactly,Here,yourcopy.Butwhatwouldbethevalueofthepropertyavailable?Itwouldbe11,whichistheresultoftheinvocationofgetCopy.
ClassconstructorsYoumighthavenoticedthatitlookslikeapaintoinstantiatetheBookclass,andsetallitsvalueseachtime.Whatifourclasshas30propertiesinsteadoffour?Well,hopefully,youwillneverdothat,asitisverybadpractice.Still,thereisawaytomitigatethatpain:constructors.
Constructorsarefunctionsthatareinvokedwhensomeonecreatesanewinstanceoftheclass.Theylooklikenormalmethods,withtheexceptionthattheirnameisalways__construct,andthattheydonothaveareturnstatement,astheyalwayshavetoreturnthenewinstance.Let’sseeanexample:
publicfunction__construct(int$isbn,string$title,string$author,int
$available){
$this->isbn=$isbn;
$this->title=$title;
$this->author=$author;
$this->available=$available;
}
Theconstructortakesfourarguments,andthenassignsthevalueofoneoftheargumentstoeachofthepropertiesoftheinstance.ToinstantiatetheBookclass,weusethefollowing:
$book=newBook("1984","GeorgeOrwell",9785267006323,12);
Thisobjectisexactlythesameastheobjectwhenwesetthevaluetoeachofitspropertiesmanually.Butthisonelookscleaner,right?Thisdoesnotmeanyoucannotsetnewvaluestothisobjectmanually,itjusthelpsyouinconstructingnewobjects.
Asaconstructorisstillafunction,itcanusedefaultarguments.Imaginethatthenumberofunitswillusuallybe0whencreatingtheobject,andlater,thelibrarianwilladdunitswhenavailable.Wecouldsetadefaultvaluetothe$availableargumentoftheconstructor,soifwedonotsendthenumberofunitswhencreatingtheobject,theobjectwillbeinstantiatedwithitsdefaultvalue:
publicfunction__construct(
int$isbn,
string$title,
string$author,
int$available=0
){
$this->isbn=$isbn;
$this->title=$title;
$this->author=$author;
$this->available=$available;
}
Wecouldusetheprecedingconstructorintwodifferentways:
$book1=newBook("1984","GeorgeOrwell",9785267006323,12);
$book2=newBook("1984","GeorgeOrwell",9785267006323);
$book1willsetthenumberofunitsavailableto12,whereas$book2willsetittothedefaultvalueof0.Butdonottrustme;tryitbyyourself!
MagicmethodsThereisaspecialgroupofmethodsthathaveadifferentbehaviorthanthenormalones.Thosemethodsarecalledmagicmethods,andtheyusuallyaretriggeredbytheinteractionoftheclassorobject,andnotbyinvocations.Youhavealreadyseenoneofthem,theconstructoroftheclass,__construct.Thismethodisnotinvokeddirectly,butratherusedwhencreatinganewinstancewithnew.Youcaneasilyidentifymagicmethods,becausetheystartwith__.Thefollowingaresomeofthemostusedmagicmethods:
__toString:Thismethodisinvokedwhenwetrytocastanobjecttoastring.Ittakesnoparameters,anditisexpectedtoreturnastring.__call:ThisisthemethodthatPHPcallswhenyoutrytoinvokeamethodonaclassthatdoesnotexist.Itgetsthenameofthemethodasastringandthelistofparametersusedintheinvocationasanarray,throughtheargument.__get:Thisisaversionof__callforproperties.Itgetsthenameofthepropertythattheuserwastryingtoaccessthroughparameters,anditcanreturnanything.
Youcouldusethe__toStringmethodtoreplacethecurrentgetPrintableTitlemethodinourBookclass.Todothat,justchangethenameofthemethodasfollows:
publicfunction__toString(){
$result='<i>'.$this->title.'</i>-'.$this->author;
if(!$this->available){
$result.='<b>Notavailable</b>';
}
return$result;
}
Totrytheprecedingcode,youcanjustaddthefollowingsnippetthatcreatesanobjectbookandthencastsittoastring,invokingthe__toStringmethod:
$book=newBook(1234,'title','author');
$string=(string)$book;//title-authorNotavailable
Asthenamesuggests,thosearemagicmethods,somostofthetimetheirfeatureswilllooklikemagic.Forobviousreasons,wepersonallyencouragedeveloperstouseconstructorsandmaybe__toString,butbecarefulaboutwhentousetherest,asyoumightmakeyourcodequiteunpredictableforpeoplenotfamiliarwithit.
PropertiesandmethodsvisibilitySofar,allthepropertiesandmethodsdefinedinourBookclassweretaggedaspublic.Thatmeansthattheyareaccessibletoanyone,ormoreprecisely,fromanywhere.Thisiscalledthevisibilityofthepropertyormethod,andtherearethreetypesofvisibility.Intheorderofbeingmorerestrictivetoless,theyareasfollows:
private:Thistypeallowsaccessonlytomembersofthesameclass.IfAandBareinstancesoftheclassC,AcanaccessthepropertiesandmethodsofB.protected:Thistypeallowsaccesstomembersofthesameclassandinstancesfromclassesthatinheritfromthatoneonly.Youwillseeinheritanceinthenextsection.public:Thistypereferstoapropertyormethodthatisaccessiblefromanywhere.Anyclassesorcodeingeneralfromoutsidetheclasscanaccessit.
Inordertoshowsomeexamples,let’sfirstcreateasecondclassinourapplication.SavethisintoaCustomer.phpfile:
<?php
classCustomer{
private$id;
private$firstname;
private$surname;
private$email;
publicfunction__construct(
int$id,
string$firstname,
string$surname,
string$email
){
$this->id=$id;
$this->firstname=$firstname;
$this->surname=$surname;
$this->email=$email;
}
}
Thisclassrepresentsacustomer,anditspropertiesconsistofthegeneralinformationthatthebookstoresusuallyknowabouttheircustomers.Butforsecurityreasons,wecannotleteverybodyknowaboutthepersonaldataofourcustomers,soweseteverypropertyasprivate.
Sofar,wehavebeenaddingthecodetocreateobjectsinthesameBook.phpfile,butsincenowwehavetwoclasses,itseemsnaturaltoleavetheclassesintheirrespectivefiles,andcreateandplaywithobjectsinaseparatefile.Let’snamethisthirdfileinit.php.Inordertoinstantiateobjectsofagivenclass,PHPneedstoknowwheretheclassis.Forthat,justincludethefilewithrequire_once.
<?php
require_once__DIR__.'/Book.php';
require_once__DIR__.'/Customer.php';
$book1=newBook("1984","GeorgeOrwell",9785267006323,12);
$book2=newBook("ToKillaMockingbird","HarperLee",9780061120084,2);
$customer1=newCustomer(1,'John','Doe','johndoe@mail.com');
$customer2=newCustomer(2,'Mary','Poppins','mp@mail.com');
Youdonotneedtoincludethefileseverysingletime.Onceyouincludethem,PHPwillknowwheretofindtheclasses,eventhoughyourcodeisinadifferentfile.
NoteConventionsforclasses
Whenworkingwithclasses,youshouldknowthattherearesomeconventionsthateveryonetriestofollowinordertoensurecleancodewhichiseasytomaintain.Themostimportantonesareasfollows:
Eachclassshouldbeinafilenamedthesameastheclassalongwiththe.phpextensionClassnamesshouldbeinCamelCase,thatis,eachwordshouldstartwithanuppercaseletter,followedbytherestofthewordinlowercaseAfileshouldcontainonlythecodeofoneclassInsideaclass,youshouldfirstplacetheproperties,thentheconstructor,andfinally,therestofthemethods
Toshowhowvisibilityworks,let’strythefollowingcode:
$book1->available=2;//OK
$customer1->id=3;//Error!
WealreadyknowthatthepropertiesoftheBookclass’objectsarepublic,andtherefore,editablefromoutside.ButwhentryingtochangeavaluefromCustomer,PHPcomplains,asitspropertiesareprivate.
EncapsulationWhenworkingwithobjects,oneofthemostimportantconceptsyouhavetoknowandapplyisencapsulation.Encapsulationtriestogroupthedataoftheobjectwithitsmethodsinanattempttohidetheinternalstructureoftheobjectfromtherestoftheworld.Insimplewords,youcouldsaythatyouuseencapsulationifthepropertiesofanobjectareprivate,andtheonlywaytoupdatethemisthroughpublicmethods.
Thereasonforusingencapsulationistomakeiteasierforadevelopertomakechangestotheinternalstructureoftheclasswithoutdirectlyaffectingtheexternalcodethatusesthatclass.Forexample,imaginethatourCustomerclass,thatnowhastwopropertiestodefineitsname—firstnameandsurname—hastochange.Fromnowon,weonlyhaveonepropertynamethatcontainsboth.Ifwewereaccessingitspropertiesstraightaway,weshouldchangeallofthoseaccesses!
Instead,ifwesetthepropertiesasprivateandenabletwopublicmethods,getFirstnameandgetSurname,evenifwehavetochangetheinternalstructureoftheclass,wecouldjustchangetheimplementationofthosetwomethods—whichisatoneplaceonly—andtherestofthecodethatusesourclasswillnotbeaffectedatall.Thisconceptisalsoknownasinformationhiding.
Theeasiestwaytoimplementthisideaisbysettingallthepropertiesoftheclassasprivateandenablingtwomethodsforeachoftheproperties:onewillgetthecurrentvalue(alsoknownasgetter),andtheotherwillallowyoutosetanewvalue(knownassetter).That’satleastthemostcommonandeasywaytoencapsulatedata.
Butlet’sgoonestepfurther:whendefiningaclass,thinkofthedatathatyouwanttheusertobeabletochangeandtoretrieve,andonlyaddsettersandgettersforthem.Forexample,customersmightchangetheire-mailaddress,buttheirname,surname,andIDremainsthesameoncewecreatethem.Thenewdefinitionoftheclasswouldlooklikethefollowing:
<?php
classCustomer{
private$id;
private$name;
private$surname;
private$email;
publicfunction__construct(
int$id,
string$firstname,
string$surname,
string$email
){
$this->id=$id;
$this->firstname=$firstname;
$this->surname=$surname;
$this->email=$email;
}
publicfunctiongetId():id{
return$this->id;
}
publicfunctiongetFirstname():string{
return$this->firstname;
}
publicfunctiongetSurname():string{
return$this->surname;
}
publicfunctiongetEmail():string{
return$this->email;
}
publicfunctionsetEmail(string$email){
$this->email=$email;
}
}
Ontheotherhand,ourbooksalsoremainalmostthesame.Theonlychangepossibleisthenumberofavailableunits.Butweusuallytakeoraddonebookatatimeinsteadofsettingthespecificnumberofunitsavailable,soasetterhereisnotreallyuseful.WealreadyhavethegetCopymethodthattakesonecopywhenpossible;let’saddanaddCopymethod,plustherestofthegetters:
<?php
classBook{
private$isbn;
private$title;
private$author;
private$available;
publicfunction__construct(
int$isbn,
string$title,
string$author,
int$available=0
){
$this->isbn=$isbn;
$this->title=$title;
$this->author=$author;
$this->available=$available;
}
publicfunctiongetIsbn():int{
return$this->isbn;
}
publicfunctiongetTitle():string{
return$this->title;
}
publicfunctiongetAuthor():string{
return$this->author;
}
publicfunctionisAvailable():bool{
return$this->available;
}
publicfunctiongetPrintableTitle():string{
$result='<i>'.$this->title.'</i>-'.$this->author;
if(!$this->available){
$result.='<b>Notavailable</b>';
}
return$result;
}
publicfunctiongetCopy():bool{
if($this->available<1){
returnfalse;
}else{
$this->available--;
returntrue;
}
}
publicfunctionaddCopy(){
$this->available++;
}
}
Whenthenumberofclassesinyourapplication,andwithit,thenumberofrelationshipsbetweenclassesincreases,itishelpfultorepresenttheseclassesinadiagram.Let’scallthisdiagramaUMLdiagramofclasses,orjustanhierarchictree.Thehierarchictreeforourtwoclasseswouldlookasfollows:
Weonlyshowpublicmethods,astheprotectedorprivateonescannotbecalledfromoutsidetheclass,andthus,theyarenotusefulforadeveloperwhojustwantstousetheseclassesexternally.
StaticpropertiesandmethodsSofar,allthepropertiesandmethodswerelinkedtoaspecificinstance;sotwodifferentinstancescouldhavetwodifferentvaluesforthesameproperty.PHPallowsyoutohavepropertiesandmethodslinkedtotheclassitselfratherthantotheobject.Thesepropertiesandmethodsaredefinedwiththekeywordstatic.
privatestatic$lastId=0;
AddtheprecedingpropertytotheCustomerclass.ThispropertyshowsthelastIDassignedtoauser,andisusefulinordertoknowtheIDthatshouldbeassignedtoanewuser.Let’schangetheconstructorofourclassasfollows:
publicfunction__construct(
int$id,
string$name,
string$surname,
string$email
){
if($id==null){
$this->id=++self::$lastId;
}else{
$this->id=$id;
if($id>self::$lastId){
self::$lastId=$id;
}
}
$this->name=$name;
$this->surname=$surname;
$this->email=$email;
}
Notethatwhenreferringtoastaticproperty,wedonotusethevariable$this.Instead,weuseself::,whichisnottiedtoanyinstancebuttotheclassitself.Inthislastconstructor,wehavetwooptions.WeareeitherprovidedwithanIDvaluethatisnotnull,orwesendanullinitsplace.WhenthereceivedIDisnull,weusethestaticproperty$lastIdtoknowthelastIDused,increaseitbyone,andassignittotheproperty$id.IfthelastIDweinsertedwas5,thiswillupdatethestaticpropertyto6,andthenassignittotheinstanceproperty.Nexttimewecreateanewcustomer,the$lastIdstaticpropertywillbe6.Instead,ifwegetavalidIDaspartofthearguments,weassignit,andcheckiftheassigned$idisgreaterthanthestatic$lastId.Ifitis,weupdateit.Let’sseehowwewouldusethis:
$customer1=newCustomer(3,'John','Doe','johndoe@mail.com');
$customer2=newCustomer(null,'Mary','Poppins','mp@mail.com');
$customer3=newCustomer(7,'James','Bond','007@mail.com');
Intheprecedingexample,$customer1specifiesthathisIDis3,probablybecauseheisanexistingcustomerandwantstokeepthesameID.ThatsetsbothhisIDandthelaststaticIDto3.Whencreatingthesecondcustomer,wedonotspecifytheID,sotheconstructorwilltakethelastID,increaseitby1,andassignittothecustomer.So$customer2will
havetheID4,andthelatestIDwillbe4too.Finally,oursecretagentknowswhathewants,soheforcesthesystemtohavetheIDas7.ThelatestIDwillbeupdatedto7too.
Anotherbenefitofstaticpropertiesandmethodsisthatwedonotneedanobjecttousethem.Youcanrefertoastaticpropertyormethodbyspecifyingthenameoftheclass,followedby::,andthenameoftheproperty/method.Thatis,ofcourse,ifthevisibilityrulesallowyoutodothat,which,inthiscase,itdoesnot,asthepropertyisprivate.Let’saddapublicstaticmethodtoretrievethelastID:
publicstaticfunctiongetLastId():int{
returnself::$lastId;
}
Youcanreferenceiteitherusingtheclassnameoranexistinginstance,fromanywhereinthecode:
Customer::getLastId();
$customer1::getLastId();
NamespacesYouknowthatyoucannothavetwoclasseswiththesamename,sincePHPwouldnotknowwhichoneisbeingreferredtowhencreatinganewobject.Tosolvethisissue,PHPallowstheuseofnamespaces,whichactaspathsinafilesystem.Inthisway,youcanhaveasmanyclasseswiththesamenameasyouneed,aslongastheyarealldefinedindifferentnamespaces.Itisworthnotingthat,eventhoughnamespacesandthefilepathwillusuallybethesame,thisisenforcedbythedeveloperratherthanbythelanguage;youcouldactuallyuseanynamespacethathasnothingtodowiththefilesystem.
Specifyinganamespacehastobethefirstthingthatyoudoinafile.Inordertodothat,usethenamespacekeywordfollowedbythenamespace.Eachsectionofthenamespaceisseparatedby\,asifitwasadifferentdirectory.Ifyoudonotspecifythenamespace,theclasswillbelongtothebasenamespace,orroot.Atthebeginningofbothfiles—Book.php
andCustomer.php—addthefollowing:
<?php
namespaceBookstore\Domain;
TheprecedinglineofcodesetsthenamespaceofourclassesasBookstore\Domain.ThefullnameofourclassesthenisBookstore\Domain\BookandBookstore\Domain\Customer.Ifyoutrytoaccesstheinit.phpfilefromyourbrowser,youwillseeanerrorsayingthateithertheclassBookortheclassCustomerwerenotfound.Butweincludedthefiles,right?ThathappensbecausePHPthinksthatyouaretryingtoaccess\Bookand\Customerfromtheroot.Donotworry,thereareseveralwaystoamendthis.
Onewaywouldbetospecifythefullnameoftheclasseswhenreferencingthem,thatis,using$customer=newBookstore\Domain\Book();insteadof$book=newBook();.Butthatdoesnotsoundpractical,doesit?
Anotherwaywouldbetosaythattheinit.phpfilebelongstotheBookStore\Domainnamespace.Thatmeansthatallthereferencestoclassesinsideinit.phpwillhavetheBookStore\Domainprefixedtothem,andyouwillbeabletouseBookandCustomer.Thedownsideofthissolutionisthatyoucannoteasilyreferenceotherclassesfromothernamespaces,asanyreferencetoaclasswillbeprefixedwiththatnamespace.
Thebestsolutionistousethekeyworduse.Thiskeywordallowsyoutospecifyafullclassnameatthebeginningofthefile,andthenusethesimplenameoftheclassintherestofthatfile.Let’sseeanexample:
<?php
useBookstore\Domain\Book;
useBookstore\Domain\Customer;
require_once__DIR__.'/Book.php';
require_once__DIR__.'/Customer.php';
//...
Intheprecedingfile,eachtimethatwereferenceBookorCustomer,PHPwillknowthatweactuallywanttousethefullclassname,thatis,withBookstore\Domain\prefixedtoit.Thissolutionallowsyoutohaveacleancodewhenreferencingthoseclasses,andatthesametime,tobeabletoreferenceclassesfromothernamespacesifneeded.
Butwhatifyouwanttoincludetwodifferentclasseswiththesamenameinthesamefile?Ifyousettwousestatements,PHPwillnotknowwhichonetochoose,sowestillhavethesameproblemasbefore!Tofixthat,eitheryouusethefullclassname—withnamespace—eachtimeyouwanttoreferenceanyoftheclasses,oryouusealiases.
ImaginethatwehavetwoBookclasses,thefirstoneinthenamespaceBookstore\DomainandthesecondoneinLibrary\Domain.Tosolvetheconflict,youcoulddoasfollows:
useBookstore\Domain\Book;
useLibrary\Domain\BookasLibraryBook;
Thekeywordassetsanaliastothatclass.Inthatfile,wheneveryoureferencetheclassLibraryBook,youwillactuallybereferencingtheclassLibrary\Domain\Book.AndwhenreferencingBook,PHPwilljustusetheonefromBookstore.Problemsolved!
AutoloadingclassesAsyoualreadyknow,inordertouseaclass,youneedtoincludethefilethatdefinesit.Sofar,wehavebeenincludingthefilesmanually,asweonlyhadacoupleofclassesandusedtheminonefile.Butwhathappenswhenweuseseveralclassesinseveralfiles?Theremustbeasmarterway,right?Indeedthereis.Autoloadingtotherescue!
AutoloadingisaPHPfeaturethatallowsyourprogramtosearchandloadfilesautomaticallygivensomesetofpredefinedrules.EachtimeyoureferenceaclassthatPHPdoesnotknowabout,itwillasktheautoloader.Iftheautoloadercanfigureoutwhichfilethatclassisin,itwillloadit,andtheexecutionoftheprogramwillcontinueasnormal.Ifitdoesnot,PHPwillstoptheexecution.
So,whatistheautoloader?ItisnomorethanaPHPfunctionthatgetsaclassnameasaparameter,anditisexpectedtoloadafile.Therearetwowaysofimplementinganautoloader:eitherbyusingthe__autoloadfunctionorthespl_autoload_registerone.
Usingthe__autoloadfunctionDefiningafunctionnamed__autoloadtellsPHPthatthefunctionistheautoloaderthatitmustuse.Youcouldimplementaneasysolution:
function__autoload($classname){
$lastSlash=strpos($classname,'\\')+1;
$classname=substr($classname,$lastSlash);
$directory=str_replace('\\','/',$classname);
$filename=__DIR__.'/'.$directory.'.php';
require_once($filename);
}
OurintentionistokeepallPHPfilesinsrc,thatis,thesource.Insidethisdirectory,thedirectorytreewillemulatethenamespacetreeoftheclassesexcludingthefirstsectionBookStore,whichisusefulasanamespacebutnotnecessaryasadirectory.ThatmeansthatourBookclass,withfullclassnameBookStore\Domain\Book,willbeinsrc/Domain/Book.php.
Inordertoachievethat,our__autoloadfunctiontriestofindthefirstoccurrenceofthebackslash\withstrpos,andthenextractsfromthatpositionuntiltheendwithsubstr.This,inpractice,justremovesthefirstsectionofthenamespace,BookStore.Afterthat,wereplaceall\by/sothatthefilesystemcanunderstandthepath.Finally,weconcatenatethecurrentdirectory,theclassnameasadirectory,andthe.phpextension.
Beforetryingthat,remembertocreatethesrc/Domaindirectoryandmovethetwoclassesinsideit.Also,tomakesurethatwearetestingtheautoloader,savethefollowingasyourinit.php,andgotohttp://localhost:8000/init.php:
<?php
useBookstore\Domain\Book;
useBookstore\Domain\Customer;
function__autoload($classname){
$lastSlash=strpos($classname,'\\')+1;
$classname=substr($classname,$lastSlash);
$directory=str_replace('\\','/',$classname);
$filename=__DIR__.'/src/'.$directory.'.php'
require_once($filename);
}
$book1=newBook("1984","GeorgeOrwell",9785267006323,12);
$customer1=newCustomer(5,'John','Doe','johndoe@mail.com');
Thebrowserdoesnotcomplainnow,andthereisnoexplicitrequire_once.Alsorememberthatthe__autoloadfunctionhastobedefinedonlyonce,notineachfile.Sofromnowon,whenyouwanttouseyourclasses,assoonastheclassisinanamespaceandfilethatfollowstheconvention,youonlyneedtodefinetheusestatement.Waycleanerthanbefore,right?
Usingthespl_autoload_registerfunctionThe__autoloadsolutionlooksprettygood,butithasasmallproblem:whatifourcodeissocomplexthatwedonothaveonlyoneconvention,andweneedmorethanoneimplementationofthe__autoloadfunction?Aswecannotdefinetwofunctionswiththesamename,weneedawaytotellPHPtokeepalistofpossibleimplementationsoftheautoloader,soitcantryallofthemuntiloneworks.
Thatisthejobofspl_autoload_register.Youdefineyourautoloaderfunctionwithavalidname,andtheninvokethefunctionspl_autoload_register,sendingthenameofyourautoloaderasanargument.Youcancallthisfunctionasmanytimesasthedifferentautoloadersyouhaveinyourcode.Infact,evenifyouhaveonlyoneautoloader,usingthissystemisstillabetteroptionthanthe__autoloadone,asyoumakeiteasierforsomeoneelsewhohastoaddanewautoloaderlater:
functionautoloader($classname){
$lastSlash=strpos($classname,'\\')+1;
$classname=substr($classname,$lastSlash);
$directory=str_replace('\\','/',$classname);
$filename=__DIR__.'/'.$directory.'.php';
require_once($filename);
}
spl_autoload_register('autoloader');
InheritanceWehavepresentedtheobject-orientedparadigmasthepanaceaforcomplexdatastructures,andeventhoughwehaveshownthatwecandefineobjectswithpropertiesandmethods,anditlooksprettyandfancy,itisnotsomethingthatwecouldnotsolvewitharrays.Encapsulationwasonefeaturethatmadeobjectsmoreusefulthanarrays,buttheirtruepowerliesininheritance.
IntroducinginheritanceInheritanceinOOPistheabilitytopasstheimplementationoftheclassfromparentstochildren.Yes,classescanhaveparents,andthetechnicalwayofreferringtothisfeatureisthataclassextendsfromanotherclass.Whenextendingaclass,wegetallthepropertiesandmethodsthatarenotdefinedasprivate,andthechildclasscanusethemasiftheywereitsown.Thelimitationisthataclasscanonlyextendfromoneparent.
Toshowanexample,let’sconsiderourCustomerclass.Itcontainsthepropertiesfirstname,surname,email,andid.Acustomerisactuallyaspecifictypeofperson,onethatisregisteredinoursystem,sohe/shecangetbooks.Buttherecanbeothertypesofpersonsinoursystem,likelibrarianorguest.Andallofthemwouldhavesomecommonpropertiestoallpeople,thatis,firstnameandsurname.SoitwouldmakesenseifwecreateaPersonclass,andmaketheCustomerclassextendfromit.Thehierarchictreewouldlookasfollows:
NotehowCustomerisconnectedtoPerson.ThemethodsinPersonarenotdefinedinCustomer,astheyareimplicitfromtheextension.Nowsavethenewclassinsrc/Domain/Person.php,followingourconvention:
<?php
namespaceBookstore\Domain;
classPerson{
protected$firstname;
protected$surname;
publicfunction__construct(string$firstname,string$surname){
$this->firstname=$firstname;
$this->surname=$surname;
}
publicfunctiongetFirstname():string{
return$this->firstname;
}
publicfunctiongetSurname():string{
return$this->surname;
}
}
Theclassdefinedintheprecedingcodesnippetdoesnotlookspecial;wehavejustdefinedtwoproperties,aconstructorandtwogetters.Notethoughthatwedefinedthepropertiesasprotected,becauseifwedefinedthemasprivate,thechildrenwouldnotbeabletoaccessthem.NowwecanupdateourCustomerclassbyremovingtheduplicatepropertiesanditsgetters:
<?php
namespaceBookstore\Domain;
classCustomerextendsPerson{
privatestatic$lastId=0;
private$id;
private$email;
publicfunction__construct(
int$id,
string$name,
string$surname,
string$email
){
if(empty($id)){
$this->id=++self::$lastId;
}else{
$this->id=$id;
if($id>self::$lastId){
self::$lastId=$id;
}
}
$this->name=$name;
$this->surname=$surname;
$this->email=$email;
}
publicstaticfunctiongetLastId():int{
returnself::$lastId;
}
publicfunctiongetId():int{
return$this->id;
}
publicfunctiongetEmail():string{
return$this->email;
}
publicfunctionsetEmail($email):string{
$this->email=$email;
}
}
Notethenewkeywordextends;ittellsPHPthatthisclassisachildofthePersonclass.AsbothPersonandCustomerareinthesamenamespace,youdonothavetoaddanyusestatement,butiftheywerenot,youshouldletitknowhowtofindtheparent.Thiscodeworksfine,butwecanseethatthereisabitofduplicationofcode.TheconstructoroftheCustomerclassisdoingthesamejobastheconstructorofthePersonclass!Wewilltrytofixitreallysoon.
Inordertoreferenceamethodorpropertyoftheparentclassfromthechild,youcanuse$thisasifthepropertyormethodwasinthesameclass.Infact,youcouldsayitactuallyis.ButPHPallowsyoutoredefineamethodinthechildclassthatwasalreadypresentintheparent.Ifyouwanttoreferencetheparent’simplementation,youcannotuse$this,asPHPwillinvoketheoneinthechild.ToforcePHPtousetheparent’smethod,usethekeywordparent::insteadof$this.UpdatetheconstructoroftheCustomerclassasfollows:
publicfunction__construct(
int$id,
string$firstname,
string$surname,
string$email
){
parent::__construct($firstname,$surname);
if(empty($id)){
$this->id=++self::$lastId;
}else{
$this->id=$id;
if($id>self::$lastId){
self::$lastId=$id;
}
}
$this->email=$email;
}
Thisnewconstructordoesnotduplicatecode.Instead,itcallstheconstructoroftheparentclassPerson,sending$firstnameand$surname,andlettingtheparentdowhatitalreadyknowshowtodo.Weavoidcodeduplicationand,ontopofthat,wemakeiteasierforanyfuturechangestobemadeintheconstructorofPerson.IfweneedtochangetheimplementationoftheconstructorofPerson,wewillchangeitinoneplaceonly,insteadofinallthechildren.
OverridingmethodsAssaidbefore,whenextendingfromaclass,wegetallthemethodsoftheparentclass.Thatisimplicit,sotheyarenotactuallywrittendowninsidethechild’sclass.Whatwouldhappenifyouimplementanothermethodwiththesamesignatureand/orname?Youwillbeoverridingthemethod.
Aswedonotneedthisfeatureinourclasses,let’sjustaddsomecodeinourinit.phpfiletoshowthisbehavior,andthenyoucanjustremoveit.Let’sdefineaclassPops,aclassChildthatextendsfromtheparent,andasayHimethodinbothofthem:
classPops{
publicfunctionsayHi(){
echo"Hi,Iampops.";
}
}
classChildextendsPops{
publicfunctionsayHi(){
echo"Hi,Iamachild.";
}
}
$pops=newPops();
$child=newChild();
echo$pops->sayHi();//Hi,Iampops.
echo$child->sayHi();//Hi,IamChild.
Thehighlightedcodeshowsyouthatthemethodhasbeenoverridden,sowheninvokingitfromachild’spointofview,wewillbeusingitratherthantheoneinheritedfromitsfather.Butwhathappensifwewanttoreferencetheinheritedonetoo?Youcanalwaysreferenceitwiththekeywordparent.Let’sseehowitworks:
classChildextendsPops{
publicfunctionsayHi(){
echo"Hi,Iamachild.";
parent::sayHi();
}
}
$child=newChild();
echo$child->sayHi();//Hi,IamChild.HiIampops.
Nowthechildissayinghiforbothhimselfandhisfather.Itseemsveryeasyandhandy,right?Well,thereisarestriction.Imaginethat,asinreallife,thechildwasveryshy,andhewouldnotsayhitoeverybody.Wecouldtrytosetthevisibilityofthemethodasprotected,butseewhathappens:
classChildextendsPops{
protectedfunctionsayHi(){
echo"Hi,Iamachild.";
}
}
Whentryingthiscode,evenwithouttryingtoinstantiateit,youwillgetafatalerrorcomplainingabouttheaccesslevelofthatmethod.Thereasonisthatwhenoverriding,themethodhastohaveatleastasmuchvisibilityastheoneinherited.Thatmeansthatifweinheritaprotectedone,wecanoverrideitwithanotherprotectedorapublicone,butneverwithaprivateone.
AbstractclassesRememberthatyoucanextendonlyfromoneparentclasseachtime.ThatmeansthatCustomercanonlyextendfromPerson.Butifwewanttomakethishierarchictreemorecomplex,wecancreatechildrenclassesthatextendfromCustomer,andthoseclasseswillextendimplicitlyfromPersontoo.Let’screatetwotypesofcustomer:basicandpremium.ThesetwocustomerswillhavethesamepropertiesandmethodsfromCustomerandfromPerson,plusthenewonesthatweimplementineachoneofthem.
Savethefollowingcodeassrc/Domain/Customer/Basic.php:
<?php
namespaceBookstore\Domain\Customer;
useBookstore\Domain\Customer;
classBasicextendsCustomer{
publicfunctiongetMonthlyFee():float{
return5.0;
}
publicfunctiongetAmountToBorrow():int{
return3;
}
publicfunctiongetType():string{
return'Basic';
}
}
Andthefollowingcodeassrc/Domain/Customer/Premium.php:
<?php
namespaceBookstore\Domain\Customer;
useBookstore\Domain\Customer;
classPremiumextendsCustomer{
publicfunctiongetMonthlyFee():float{
return10.0;
}
publicfunctiongetAmountToBorrow():int{
return10;
}
publicfunctiongetType():string{
return'Premium';
}
}
ThingstonoteintheprecedingtwocodesarethatweextendfromCustomerintwo
differentclasses,anditisperfectlylegal—wecanextendfromclassesindifferentnamespaces.Withthisaddition,thehierarchictreeforPersonwouldlookasfollows:
Wedefinethesamemethodsinthesetwoclasses,buttheirimplementationsaredifferent.Theaimofthisapproachistousebothtypesofcustomersindistinctively,withoutknowingwhichoneitiseachtime.Forexample,wecouldtemporallyhavethefollowingcodeinourinit.php.RemembertoaddtheusestatementtoimporttheclassCustomerifyoudonothaveit.
functioncheckIfValid(Customer$customer,array$books):bool{
return$customer->getAmountToBorrow()>=count($books);
}
Theprecedingfunctionwouldtellusifagivencustomercouldborrowallthebooksinthearray.NoticethatthetypehintingofthemethodsaysCustomer,withoutspecifyingwhichone.ThiswillacceptobjectsthatareinstancesofCustomeroranyclassthatextendsfromCustomer,thatis,BasicorPremium.Lookslegit,right?Let’strytouseitthen:
$customer1=newBasic(5,'John','Doe','johndoe@mail.com');
var_dump(checkIfValid($customer1,[$book1]));//ok
$customer2=newCustomer(7,'James','Bond','james@bond.com');
var_dump(checkIfValid($customer2,[$book1]));//fails
Thefirstinvocationworksasexpected,butthesecondonefails,eventhoughwearesendingaCustomerobject.TheproblemarisesbecausetheparentdoesnotknowaboutanygetAmountToBorrowmethod!Italsolooksdangerousthatwerelyonthechildrentoalwaysimplementthatmethod.Thesolutionliesinusingabstractclasses.
Anabstractclassisaclassthatcannotbeinstantiated.Itssolepurposeistomakesurethatitschildrenarecorrectlyimplemented.Declaringaclassasabstractisdonewiththekeywordabstract,followedbythedefinitionofanormalclass.Wecanalsospecifythe
methodsthatthechildrenareforcedtoimplement,withoutimplementingthemintheparentclass.Thosemethodsarecalledabstractmethods,andaredefinedwiththekeywordabstractatthebeginning.Ofcourse,therestofthenormalmethodscanstaytheretoo,andwillbeinheritedbyitschildren:
<?php
abstractclassCustomerextendsPerson{
//...
abstractpublicfunctiongetMonthlyFee();
abstractpublicfunctiongetAmountToBorrow();
abstractpublicfunctiongetType();
//...
}
Theprecedingabstractionsolvesbothproblems.First,wewillnotbeabletosendanyinstanceoftheclassCustomer,becausewecannotinstantiateit.ThatmeansthatalltheobjectsthatthecheckIfValidmethodisgoingtoacceptareonlythechildrenfromCustomer.Ontheotherhand,declaringabstractmethodsforcesallthechildrenthatextendtheclasstoimplementthem.Withthat,wemakesurethatallobjectswillimplementgetAmountToBorrow,andourcodeissafe.
ThenewhierarchictreewilldefinethethreeabstractmethodsinCustomer,andwillomitthemforitschildren.Itistruethatweareimplementingtheminthechildren,butastheyareenforcedbyCustomer,andthankstoabstraction,wearesurethatallclassesextendingfromitwillhavetoimplementthem,andthatitissafetodoso.Let’sseehowthisisdone:
Withthelastnewaddition,yourinit.phpfileshouldfail.ThereasonisthatitistryingtoinstantiatetheclassCustomer,butnowitisabstract,soyoucannot.Instantiateaconcreteclass,thatis,onethatisnotabstract,tosolvetheproblem.
InterfacesAninterfaceisanOOPelementthatgroupsasetoffunctiondeclarationswithoutimplementingthem,thatis,itspecifiesthename,returntype,andarguments,butnottheblockofcode.Interfacesaredifferentfromabstractclasses,sincetheycannotcontainanyimplementationatall,whereasabstractclassescouldmixbothmethoddefinitionsandimplementedones.Thepurposeofinterfacesistostatewhataclasscando,butnothowitisdone.
Fromourcode,wecanidentifyapotentialusageofinterfaces.Customershaveanexpectedbehavior,butitsimplementationchangesdependingonthetypeofcustomer.So,Customercouldbeaninterfaceinsteadofanabstractclass.Butasaninterfacecannotimplementanyfunction,norcanitcontainproperties,wewillhavetomovetheconcretecodefromtheCustomerclasstosomewhereelse.Fornow,let’smoveituptothePersonclass.EditthePersonclassasshown:
<?php
namespaceBookstore\Domain;
classPerson{
privatestatic$lastId=0;
protected$id;
protected$firstname;
protected$surname;
protected$email;
publicfunction__construct(
int$id,
string$firstname,
string$surname,
string$email
){
$this->firstname=$firstname;
$this->surname=$surname;
$this->email=$email;
if(empty($id)){
$this->id=++self::$lastId;
}else{
$this->id=$id;
if($id>self::$lastId){
self::$lastId=$id;
}
}
}
publicfunctiongetFirstname():string{
return$this->firstname;
}
publicfunctiongetSurname():string{
return$this->surname;
}
publicstaticfunctiongetLastId():int{
returnself::$lastId;
}
publicfunctiongetId():int{
return$this->id;
}
publicfunctiongetEmail():string{
return$this->email;
}
}
NoteComplicatingthingsmorethannecessary
Interfacesareveryuseful,butthereisalwaysaplaceandatimeforeverything.Asourapplicationisverysimpleduetoitsdidacticnature,thereisnorealplaceforthem.Theabstractclassalreadydefinedintheprevioussectionisthebestapproachforourscenario.Butjustforthesakeofshowinghowinterfaceswork,wewillbeadaptingourcodetothem.
Donotworrythough,asmostofthecodethatwearegoingtointroducenowwillbereplacedbybetterpracticesonceweintroducedatabasesandtheMVCpatterninChapter5,UsingDatabases,andChapter6,AdaptingtoMVC.
Whenwritingyourownapplications,donottrytocomplicatethingsmorethannecessary.Itisacommonpatterntoseeverycomplexcodefromdevelopersthattrytoshowupalltheskillstheyhaveinaverysimplescenario.Useonlythenecessarytoolstoleavecleancodethatiseasytomaintain,andofcourse,thatworksasexpected.
ChangethecontentofCustomer.phpwiththefollowing:
<?php
namespaceBookstore\Domain;
interfaceCustomer{
publicfunctiongetMonthlyFee():float;
publicfunctiongetAmountToBorrow():int;
publicfunctiongetType():string;
}
Notethataninterfaceisverysimilartoanabstractclass.Thedifferencesarethatitisdefinedwiththekeywordinterface,andthatitsmethodsdonothavethewordabstract.Interfacescannotbeinstantiated,sincetheirmethodsarenotimplementedaswithabstractclasses.Theonlythingyoucandowiththemismakeaclasstoimplementthem.
Implementinganinterfacemeansimplementingallthemethodsdefinedinit,likewhenweextendedanabstractclass.Ithasallthebenefitsoftheextensionofabstractclasses,suchasbelongingtothattype—usefulwhentypehinting.Fromthedeveloper’spointofview,
usingaclassthatimplementsaninterfaceislikewritingacontract:youensurethatyourclasswillalwayshavethemethodsdeclaredintheinterface,regardlessoftheimplementation.Becauseofthat,interfacesonlycareaboutpublicmethods,whicharetheonesthatotherdeveloperscanuse.Theonlychangeyouneedtomakeinyourcodeistoreplacethekeywordsextendsbyimplements:
classBasicimplementsCustomer{
So,whywouldsomeoneuseaninterfaceifwecouldalwaysuseanabstractclassthatnotonlyenforcestheimplementationofmethods,butalsoallowsinheritingcodeaswell?Thereasonisthatyoucanonlyextendfromoneclass,butyoucanimplementmultipleinstancesatthesametime.Imaginethatyouhadanotherinterfacethatdefinedpayers.Thiscouldidentifysomeonethathastheabilitytopaysomething,regardlessofwhatitis.Savethefollowingcodeinsrc/Domain/Payer.php:
<?php
namespaceBookstore\Domain;
interfacePayer{
publicfunctionpay(float$amount);
publicfunctionisExtentOfTaxes():bool;
}
Nowourbasicandpremiumcustomerscanimplementboththeinterfaces.Thebasiccustomerwilllooklikethefollowing:
//...
useBookstore\Domain\Customer;
useBookstore\Domain\Person;
classBasicextendsPersonimplementsCustomer{
publicfunctiongetMonthlyFee():float{
//...
Andthepremiumcustomerwillchangeinthesameway:
//...
useBookstore\Domain\Customer;
useBookstore\Domain\Person;
classPremiumextendsPersonimplementsCustomer{
publicfunctiongetMonthlyFee():float{
//...
Youshouldseethatthiscodewouldnolongerwork.Thereasonisthatalthoughweimplementasecondinterface,themethodsarenotimplemented.Addthesetwomethodstothebasiccustomerclass:
publicfunctionpay(float$amount){
echo"Paying$amount.";
}
publicfunctionisExtentOfTaxes():bool{
returnfalse;
}
Addthesetwomethodstothepremiumcustomerclass:
publicfunctionpay(float$amount){
echo"Paying$amount.";
}
publicfunctionisExtentOfTaxes():bool{
returntrue;
}
Ifyouknowthatallcustomerswillhavetobepayers,youcouldevenmaketheCustomerinterfacetoinheritfromthePayerinterface:
interfaceCustomerextendsPayer{
Thischangedoesnotaffecttheusageofourclassesatall.OtherdeveloperswillseethatourbasicandpremiumcustomersinheritfromPayerandCustomer,andsotheycontainallthenecessarymethods.Thattheseinterfacesareindependent,ortheyextendfromeachotherissomethingthatwillnotaffecttoomuch.
Interfacescanonlyextendfromotherinterfaces,andclassescanonlyextendfromotherclasses.Theonlywaytomixthemiswhenaclassimplementsaninterface,butneitherdoesaclassextendfromaninterface,nordoesaninterfaceextendfromaclass.Butfromthepointofviewoftypehinting,theycanbeusedinterchangeably.
Tosummarizethissectionandmakethingsclear,let’sshowwhatthehierarchictreelookslikeafterallthenewadditions.Asinabstractclasses,themethodsdeclaredinaninterfaceareshownintheinterfaceratherthanineachoftheclassesthatimplementit.
PolymorphismPolymorphismisanOOPfeaturethatallowsustoworkwithdifferentclassesthatimplementthesameinterface.Itisoneofthebeautiesofobject-orientedprogramming.Itallowsthedevelopertocreateacomplexsystemofclassesandhierarchictrees,butoffersasimplewayofworkingwiththem.
Imaginethatwehaveafunctionthat,givenapayer,checkswhetheritisexemptoftaxesornot,andmakesitpaysomeamountofmoney.Thispieceofcodedoesnotreallymindifthepayerisacustomer,alibrarian,orsomeonewhohasnothingtodowiththebookstore.Theonlythingthatitcaresaboutisthatthepayerhastheabilitytopay.Thefunctioncouldbeasfollows:
functionprocessPayment(Payer$payer,float$amount){
if($payer->isExtentOfTaxes()){
echo"Whataluckyone…";
}else{
$amount*=1.16;
}
$payer->pay($amount);
}
Youcouldsendbasicorpremiumcustomerstothisfunction,andthebehaviorwillbedifferent.But,asbothimplementthePayerinterface,bothobjectsprovidedarevalidtypes,andbotharecapableofperformingtheactionsneeded.
ThecheckIfValidfunctiontakesacustomerandalistofbooks.Wealreadysawthatsendinganykindofcustomermakesthefunctionworkasexpected.ButwhathappensifwesendanobjectoftheclassLibrarian,whichextendsfromPayer?AsPayerdoesnotknowaboutCustomer(itisrathertheotherwayaround),thefunctionwillcomplainasthetypehintingisnotaccomplished.
OneusefulfeaturethatcomeswithPHPistheabilitytocheckwhetheranobjectisaninstanceofaspecificclassorinterface.Thewaytouseitistospecifythevariablefollowedbythekeywordinstanceofandthenameoftheclassorinterface.ItreturnsaBoolean,whichistrueiftheobjectisfromaclassthatextendsorimplementsthespecifiedone,orfalseotherwise.Let’sseesomeexamples:
$basic=newBasic(1,"name","surname","email");
$premium=newPremium(2,"name","surname","email");
var_dump($basicinstanceofBasic);//true
var_dump($basicinstanceofPremium);//false
var_dump($premiuminstanceofBasic);//false
var_dump($premiuminstanceofPremium);//true
var_dump($basicinstanceofCustomer);//true
var_dump($basicinstanceofPerson);//true
var_dump($basicinstanceofPayer);//true
Remembertoaddalltheusestatementsforeachoftheclassorinterface,otherwisePHPwillunderstandthatthespecifiedclassnameisinsidethenamespaceofthefile.
TraitsSofar,youhavelearnedthatextendingfromclassesallowsyoutoinheritcode(propertiesandmethodimplementations),butithasthelimitationofextendingonlyfromoneclasseachtime.Ontheotherhand,youcanuseinterfacestoimplementmultiplebehaviorsfromthesameclass,butyoucannotinheritcodeinthisway.Tofillthisgap,thatis,tobeabletoinheritcodefrommultipleplaces,youhavetraits.
Traitsaremechanismsthatallowyoutoreusecode,“inheriting”,orrathercopy-pastingcode,frommultiplesourcesatthesametime.Traits,asabstractclassesorinterfaces,cannotbeinstantiated;theyarejustcontainersoffunctionalitythatcanbeusedfromotherclasses.
Ifyouremember,wehavesomecodeinthePersonclassthatmanagestheassignmentofIDs.Thiscodeisnotreallypartofaperson,butratherpartofanIDsystemthatcouldbeusedbysomeotherentitythathastobeidentifiedwithIDstoo.OnewaytoextractthisfunctionalityfromPerson—andwearenotsayingthatitisthebestwaytodoso,butforthesakeofseeingtraitsinaction,wechoosethisone—istomoveittoatrait.
Todefineatrait,doasifyouweredefiningaclass,justusethekeywordtraitinsteadofclass.Defineitsnamespace,addtheusestatementsneeded,declareitspropertiesandimplementitsmethods,andplaceeverythinginafilethatfollowsthesameconventions.Addthefollowingcodetothesrc/Utils/Unique.phpfile:
<?php
namespaceBookstore\Utils;
traitUnique{
privatestatic$lastId=0;
protected$id;
publicfunctionsetId(int$id){
if(empty($id)){
$this->id=++self::$lastId;
}else{
$this->id=$id;
if($id>self::$lastId){
self::$lastId=$id;
}
}
}
publicstaticfunctiongetLastId():int{
returnself::$lastId;
}
publicfunctiongetId():int{
return$this->id;
}
}
Observethatthenamespaceisnotthesameasusual,sincewearestoringthiscodeina
differentfile.Thisisamatterofconventions,butyouareentirelyfreetousethefilestructurethatyouconsiderbetterforeachcase.Inthiscase,wedonotthinkthatthistraitrepresents“businesslogic”likecustomersandbooksdo;instead,itrepresentsautilityformanagingtheassignmentofIDs.
WeincludeallthecoderelatedtoIDsfromPerson.Thatincludestheproperties,thegetters,andthecodeinsidetheconstructor.Asthetraitcannotbeinstantiated,wecannotaddaconstructor.Instead,weaddedasetIdmethodthatcontainsthecode.Whenconstructinganewinstancethatusesthistrait,wecaninvokethissetIdmethodtosettheIDbasedonwhattheusersendsasanargument.
TheclassPersonwillhavetochangetoo.WehavetoremoveallreferencestoIDsandwewillhavetodefinesomehowthattheclassisusingthetrait.Todothat,weusethekeyworduse,likeinnamespaces,butinsidetheclass.Let’sseewhatitwouldlooklike:
<?php
namespaceBookstore\Domain;
useBookstore\Utils\Unique;
classPerson{
useUnique;
protected$firstname;
protected$surname;
protected$email;
publicfunction__construct(
int$id,
string$firstname,
string$surname,
string$email
){
$this->firstname=$firstname;
$this->surname=$surname;
$this->email=$email;
$this->setId($id);
}
publicfunctiongetFirstname():string{
return$this->firstname;
}
publicfunctiongetSurname():string{
return$this->surname;
}
publicfunctiongetEmail():string{
return$this->email;
}
publicfunctionsetEmail(string$email){
$this->email=$email;
}
}
WeaddtheuseUnique;statementtolettheclassknowthatitisusingthetrait.WeremoveeverythingrelatedtoIDs,eveninsidetheconstructor.WestillgetanIDasthefirstargumentoftheconstructor,butweaskthemethodsetIdfromthetraittodoeverythingforus.Notethatwerefertothatmethodwith$this,asifthemethodwasinsidetheclass.Theupdatedhierarchictreewouldlooklikethefollowing(notethatwearenotaddingallthemethodsforalltheclassesorinterfacesthatarenotinvolvedintherecentchangesinordertokeepthediagramassmallandreadableaspossible):
Let’sseehowitworks,eventhoughitdoessointhewaythatyouprobablyexpect.Addthiscodeintoyourinit.phpfile,includethenecessaryusestatements,andexecuteitinyourbrowser:
$basic1=newBasic(1,"name","surname","email");
$basic2=newBasic(null,"name","surname","email");
var_dump($basic1->getId());//1
var_dump($basic2->getId());//2
Theprecedingcodeinstantiatestwocustomers.ThefirstofthemhasaspecificID,whereasthesecondoneletsthesystemchooseanIDforit.TheresultisthatthesecondbasiccustomerhastheID2.Thatistobeexpected,asbothcustomersarebasic.Butwhatwouldhappenifthecustomersareofdifferenttypes?
$basic=newBasic(1,"name","surname","email");
$premium=newPremium(null,"name","surname","email");
var_dump($basic->getId());//1
var_dump($premium->getId());//2
TheIDsarestillthesame.Thatistobeexpected,asthetraitisincludedinthePersonclass,sothestaticproperty$lastIdwillbesharedacrossalltheinstancesoftheclassPerson,includingBasicandPremiumcustomers.IfyouusedthetraitfromBasicandPremiumcustomerinsteadofPerson(butyoushouldnot),youwouldhavethefollowing
result:
var_dump($basic->getId());//1
var_dump($premium->getId());//1
Eachclasswillhaveitsownstaticproperty.AllBasicinstanceswillsharethesame$lastId,differentfromthe$lastIdofPremiuminstances.Thisshouldmakeclearthatthestaticmembersintraitsarelinkedtowhicheverclassusesthem,ratherthanthetraititself.ThatcouldalsobereflectedontestingthefollowingcodewhichusesouroriginalscenariowherethetraitisusedfromPerson:
$basic=newBasic(1,"name","surname","email");
$premium=newPremium(null,"name","surname","email");
var_dump(Person::getLastId());//2
var_dump(Unique::getLastId());//0
var_dump(Basic::getLastId());//2
var_dump(Premium::getLastId());//2
Ifyouhaveagoodeyeforproblems,youmightstartthinkingaboutsomepotentialissuesaroundtheusageoftraits.Whathappensifweusetwotraitsthatcontainthesamemethod?Orwhathappensifyouuseatraitthatcontainsamethodthatisalreadyimplementedinthatclass?
Ideally,youshouldavoidrunningintothesekindsofsituations;theyarewarninglightsforpossiblebaddesign.Butastherewillalwaysbeextraordinarycases,let’sseesomeisolatedexamplesonhowtheywouldbehave.
Thescenariowherethetraitandtheclassimplementthesamemethodiseasy.Themethodimplementedexplicitlyintheclassistheonewithmoreprecedence,followedbythemethodimplementedinthetrait,andfinally,themethodinheritedfromtheparentclass.Let’sseehowitworks.Takeforexamplethefollowingtraitandclassdefinitions:
<?php
traitContract{
publicfunctionsign(){
echo"Signingthecontract.";
}
}
classManager{
useContract;
publicfunctionsign(){
echo"Signinganewplayer.";
}
}
Bothimplementthesignmethod,whichmeansthatwehavetoapplytheprecedencerulesdefinedpreviously.Themethoddefinedintheclasstakesprecedenceovertheonefromthetrait,sointhiscase,theexecutedmethodwillbetheonefromtheclass:
$manager=newManager();
$manager->sign();//Signinganewplayer.
Themostcomplicatedscenariowouldbeonewhereaclassusestwotraitswiththesamemethod.Therearenorulesthatsolvetheconflictautomatically,soyouhavetosolveitexplicitly.Checkthefollowingcode:
<?php
traitContract{
publicfunctionsign(){
echo"Signingthecontract.";
}
}
traitCommunicator{
publicfunctionsign(){
echo"Signingtothewaitress.";
}
}
classManager{
useContract,Communicator;
}
$manager=newManager();
$manager->sign();
Theprecedingcodethrowsafatalerror,asbothtraitsimplementthesamemethod.Tochoosetheoneyouwanttouse,youhavetousetheoperatorinsteadof.Touseit,statethetraitnameandthemethodthatyouwanttouse,followedbyinsteadofandthetraitthatyouarerejectingforuse.Optionally,usethekeywordastoaddanaliaslikewedidwithnamespacessothatyoucanuseboththemethods:
classManager{
useContract,Communicator{
Contract::signinsteadofCommunicator;
Communicator::signasmakeASign;
}
}
$manager=newManager();
$manager->sign();//Signingthecontract.
$manager->makeASign();//Signingtothewaitress.
YoucanseehowwedecidedtousethemethodofContractinsteadofCommunicator,butaddedthealiassothatbothmethodsareavailable.Hopefully,youcanseethateventheconflictscanbesolved,andtherearespecificcaseswherethereisnothingtodobutdealwiththem;ingeneral,theylooklikeabadsign—nopunintended.
HandlingexceptionsItdoesnotmatterhoweasyandintuitiveyourapplicationisdesignedtobe,therewillbebadusagefromtheuserorjustrandomerrorsofconnectivity,andyourcodehastobereadytohandlethesescenariossothattheuserexperienceisagoodaspossible.Wecallthesescenariosexceptions:anelementofthelanguagethatidentifiesacasethatisnotasweexpected.
Thetry…catchblockYourcodecanthrowexceptionsmanuallywheneveryouthinkitnecessary.Forexample,takethesetIdmethodfromtheUniquetrait.Thankstotypehinting,weareenforcingtheIDtobeanumericone,butthatisasfarasitgoes.WhatwouldhappenifsomeonetriestosetanIDthatisanegativenumber?Thecoderightnowallowsittogothrough,butdependingonyourpreferences,youwouldliketoavoidit.Thatwouldbeagoodplaceforanexceptiontohappen.Let’sseehowwewouldaddthischeckandconsequentexception:
publicfunctionsetId($id){
if($id<0){
thrownew\Exception('Idcannotbenegative.');
}
if(empty($id)){
$this->id=++self::$lastId;
}else{
$this->id=$id;
if($id>self::$lastId){
self::$lastId=$id;
}
}
}
Asyoucansee,exceptionsareobjectsoftheclassexception.Rememberaddingthebackslashtothenameoftheclass,unlessyouwanttoincludeitwithuseException;atthetopofthefile.TheconstructoroftheExceptionclasstakessomeoptionalarguments,thefirstoneofthembeingthemessageoftheexception.InstancesoftheclassExceptiondonothingbythemselves;theyhavetobethrowninordertobenoticedbytheprogram.
Let’stryforcingourprogramtothrowthisexception.Inordertodothat,let’strytocreateacustomerwithanegativeID.Inyourinit.phpfile,addthefollowing:
$basic=newBasic(-1,"name","surname","email");
Ifyoutryitnowinyourbrowser,PHPwillthrowafatalerrorsayingthattherewasanuncaughtexception,whichistheexpectedbehavior.ForPHP,anexceptionissomethingfromwhatitcannotrecover,soitwillstopexecution.Thatisfarfromideal,asyouwouldliketojustdisplayanerrormessagetotheuser,andletthemtryagain.
Youcan—andshould—captureexceptionsusingthetry…catchblocks.Youinsertthecodethatmightthrowanexceptioninthetryblockandifanexceptionhappens,PHPwilljumptothecatchblock.Let’sseehowitworks:
publicfunctionsetId(int$id){
try{
if($id<0){
thrownewException('Idcannotbenegative.');
}
if(empty($id)){
$this->id=++self::$lastId;
}else{
$this->id=$id;
if($id>self::$lastId){
self::$lastId=$id;
}
}
}catch(Exception$e){
echo$e->getMessage();
}
}
Ifwetestthelastcodesnippetinourbrowser,wewillseethemessageprintedfromthecatchblock.CallingthegetMessagemethodonanexceptioninstancewillgiveusthemessage—thefirstargumentwhencreatingtheobject.Butrememberthattheargumentoftheconstructorisoptional;so,donotrelyonthemessageoftheexceptiontoomuchifyouarenotsurehowitisgenerated,asitmightbeempty.
Notethataftertheexceptionisthrown,nothingelseinsidethetryblockisexecuted;PHPgoesstraighttothecatchblock.Additionally,theblockgetsanargument,whichistheexceptionthrown.Here,typehintingismandatory—youwillseewhyverysoon.Namingtheargumentas$eisawidelyusedconvention,eventhoughitisnotagoodpracticetousepoordescriptivenamesforvariables.
Beingabitcritical,sofar,thereisnotanyrealadvantagetobeseeninusingexceptionsinthisexample.Asimpleif…elseblockwoulddoexactlythesamejob,right?Buttherealpowerofexceptionsliesintheabilitytobepropagatedacrossmethods.Thatis,theexceptionthrownonthesetIdmethod,ifnotcaptured,willbepropagatedtowhereverthemethodwasinvoked,allowingustocaptureitthere.Thisisveryuseful,asdifferentplacesinthecodemightwanttohandletheexceptioninadifferentway.Toseehowthisisdone,let’sremovethetry…catchinsertedinsetId,andplacethefollowingpieceofcodeinyourinit.phpfile,instead:
try{
$basic=newBasic(-1,"name","surname","email");
}catch(Exception$e){
echo'Somethinghappenedwhencreatingthebasiccustomer:'
.$e->getMessage();
}
Theprecedingexampleshowshowusefulitistocatchpropagatedexceptions:wecanbemorespecificofwhathappens,asweknowwhattheuserwastryingtodowhentheexceptionwasthrown.Inthiscase,weknowthatweweretryingtocreatethecustomer,butthisexceptionmighthavebeenthrownwhentryingtoupdatetheIDofanexistingcustomer,whichwouldneedadifferenterrormessage.
ThefinallyblockThereisathirdblockthatyoucanusewhendealingwithexceptions:thefinallyblock.Thisblockisaddedafterthetry…catchone,anditisoptional.Infact,thecatchblockisoptionaltoo;therestrictionisthatatrymustbefollowedbyatleastoneofthem.Soyoucouldhavethesethreescenarios:
//scenario1:thewholetry-catch-finally
try{
//codethatmightthrowanexception
}catch(Exception$e){
//codethatdealswiththeexception
}finally{
//finallyblock
}
//scenario2:try-finallywithoutcatch
try{
//codethatmightthrowanexception
}finally{
//finallyblock
}
//scenario3:try-catchwithoutfinally
try{
//codethatmightthrowanexception
}catch(Exception$e){
//codethatdealswiththeexception
}
Thecodeinsidethefinallyblockisexecutedwheneitherthetryorthecatchblocksareexecutedcompletely.So,ifwehaveascenariowherethereisnoexception,afterallthecodeinsidethetryblockisexecuted,PHPwillexecutethecodeinsidefinally.Ontheotherhand,ifthereisanexceptionthrowninsidethetryblock,PHPwilljumptothecatchblock,andafterexecutingeverythingthere,itwillexecutethefinallyblocktoo.
Inordertotestthisfunctionality,let’simplementafunctionthatcontainsatry…catch…finallyblock,tryingtocreateacustomerwithagivenID(throughanargument),andloggingalltheactionsthattakeplace.Youcanaddthefollowingcodesnippetintoyourinit.phpfile:
functioncreateBasicCustomer($id)
{
try{
echo"\nTryingtocreateanewcustomer.\n";
returnnewBasic($id,"name","surname","email");
}catch(Exception$e){
echo"Somethinghappenedwhencreatingthebasiccustomer:"
.$e->getMessage()."\n";
}finally{
echo"Endoffunction.\n";
}
}
createBasicCustomer(1);
createBasicCustomer(-1);
Ifyoutrythis,yourbrowserwillshowyouthefollowingoutput—remembertodisplaythesourcecodeofthepagetoseeitformattedprettily:
Theresultmightnotbetheoneyouexpected.Thefirsttimeweinvokethefunction,weareabletocreatetheobjectwithoutanissue,andthatmeansweexecutethereturnstatement.Inanormalfunction,thisshouldbetheendofit,butsinceweareinsidethetry…catch…finallyblock,westillneedtoexecutethefinallycode!Thesecondexamplelooksmoreintuitive,jumpingfromthetrytothecatch,andthentothefinallyblock.
Thefinallyblockisveryusefulwhendealingwithexpensiveresourceslikedatabaseconnections.InChapter5,UsingDatabases,youwillseehowtousethem.Dependingonthetypeofconnection,youwillhavetocloseitafteruseforallowingotheruserstoconnect.Thefinallyblockisusedforclosingthoseconnections,regardlessofwhetherthefunctionthrowsanexceptionornot.
CatchingdifferenttypesofexceptionsExceptionshavealreadybeenprovenuseful,butthereisstilloneimportantfeaturetoshow:catchingdifferenttypesofexceptions.Asyoualreadyknow,exceptionsareinstancesoftheclassException,andaswithanyotherclass,theycanbeextended.Themaingoalofextendingfromthisclassistocreatedifferenttypesofexceptions,butwewillnotaddanylogicinside—eventhoughyoucan,ofcourse.Let’screateaclassthatextendsfromException,andwhichidentifiesexceptionsrelatedtoinvalidIDs.Putthiscodeinsidethesrc/Exceptions/InvalidIdException.phpfile:
<?php
namespaceBookstore\Exceptions;
useException;
classInvalidIdExceptionextendsException{
publicfunction__construct($message=null){
$message=$message?:'Invalididprovided.';
parent::__construct($message);
}
}
TheInvalidIdExceptionclassextendsfromtheclassException,andsoitcanbethrownasone.Theconstructoroftheclasstakesanoptionalargument,$message.Thefollowingtwolinesinsideitcontaininterestingcode:
The?:operatorisashorterversionofaconditional,andworkslikethis:theexpressionontheleftisreturnedifitdoesnotevaluatetofalse,otherwise,theexpressionontherightwillbereturned.Whatwewanthereistousethemessagegivenbytheuser,oradefaultoneincasetheuserdoesnotprovideany.Formoreinformationandusages,youcanvisitthePHPdocumentationathttp://php.net/manual/en/language.operators.comparison.php.parent::__constructwillinvoketheparent’sconstructor,thatis,theconstructoroftheclassException.Asyoualreadyknow,thisconstructorgetsthemessageoftheexceptionasthefirstargument.Youcouldarguethat,asweareextendingfromtheExceptionclass,wedonotreallyneedtocallanyfunctions,aswecaneditthepropertiesoftheclassstraightaway.Thereasonforavoidingthisistolettheparentclassmanageitsownproperties.Imaginethat,forsomereason,inafutureversionofPHP,Exceptionchangesthenameofthepropertyforthemessage.Ifyoumodifyitdirectly,youwillhavetochangethatinyourcode,butifyouusetheconstructor,youhavenothingtofear.Internalimplementationsaremorelikelytochangethanexternalinterfaces.
Wecanusethisnewexceptioninsteadofthegenericone.ReplaceitinyourUniquetraitasfollows:
thrownewInvalidIdException('Idcannotbeanegativenumber.');
Youcanseethatwearestillsendingamessage:thatisbecausewewanttobeevenmore
specific.Buttheexceptionwouldworkaswellwithoutone.Tryyourcodeagain,andyouwillseethatnothingchanges.
Nowimaginethatwehaveaverysmalldatabaseandwecannotallowmorethan50users.Wecancreateanewexceptionthatidentifiesthiscase,let’ssay,assrc/Exceptions/ExceededMaxAllowedException.php:
<?php
namespaceBookstore\Exceptions;
useException;
classExceededMaxAllowedExceptionextendsException{
publicfunction__construct($message=null){
$message=$message?:'Exceededmaxallowed.';
parent::__construct($message);
}
}
Let’smodifyourtraitinordertocheckforthiscase.WhensettinganID,ifthisIDisgreaterthan50,wecanassumethatwe’vereachedthemaximumnumberofusers:
publicfunctionsetId(int$id){
if($id<0){
thrownewInvalidIdException(
'Idcannotbeanegativenumber.'
);
}
if(empty($id)){
$this->id=++self::$lastId;
}else{
$this->id=$id;
if($id>self::$lastId){
self::$lastId=$id;
}
}
if($this->id>50){
thrownewExceededMaxAllowedException(
'Maxnumberofusersis50.'
);
}
}
Nowtheprecedingfunctionthrowstwodifferentexceptions:InvalidIdExceptionandExceededMaxAllowedException.Whencatchingthem,youmightwanttobehaveinadifferentwaydependingonthetypeofexceptioncaught.Rememberhowyouhavetodeclareanargumentinyourcatchblock?Well,youcanaddasmanycatchblocksasneeded,specifyingadifferentexceptionclassineachofthem.Thecodecouldlooklikethis:
functioncreateBasicCustomer(int$id)
{
try{
echo"\nTryingtocreateanewcustomerwithid$id.\n";
returnnewBasic($id,"name","surname","email");
}catch(InvalidIdException$e){
echo"Youcannotprovideanegativeid.\n";
}catch(ExceededMaxAllowedException$e){
echo"Nomorecustomersareallowed.\n";
}catch(Exception$e){
echo"Unknownexception:".$e->getMessage();
}
}
createBasicCustomer(1);
createBasicCustomer(-1);
createBasicCustomer(55);
Ifyoutrythiscode,youshouldseethefollowingoutput:
Notethatwecatchthreeexceptionshere:ourtwonewexceptionsandthegenericone.Thereasonfordoingthisisthatitmighthappenthatsomeotherpieceofcodethrowsanexceptionofadifferenttypethantheoneswedefined,andweneedtodefineacatchblockwiththegenericExceptionclasstogetit,asallexceptionswillextendfromit.Ofcourse,thisisabsolutelyoptional,andifyoudonotdoit,theexceptionwillbejustpropagated.
Bearinmindtheorderofthecatchblocks.PHPtriestousethecatchblocksintheorderthatyoudefinedthem.So,ifyourfirstcatchisforException,therestoftheblockswillbeneverexecuted,asallexceptionsextendfromthatclass.Tryitwiththefollowingcode:
try{
echo"\nTryingtocreateanewcustomerwithid$id.\n";
returnnewBasic($id,"name","surname","email");
}catch(Exception$e){
echo'Unknownexception:'.$e->getMessage()."\n";
}catch(InvalidIdException$e){
echo"Youcannotprovideanegativeid.\n";
}catch(ExceededMaxAllowedException$e){
echo"Nomorecustomersareallowed.\n";
}
DesignpatternsDevelopershavebeencreatingcodesincewaybeforetheappearanceofwithInternet,andtheyhavebeenworkingonanumberofdifferentareas,notjustwebapplications.Becauseofthat,alotofpeoplehavealreadyhadtoconfrontsimilarscenarios,carryingtheexperienceofpreviousattemptsforfixingthesamething.Inshort,itmeansthatalmostsurely,someonehasalreadydesignedagoodwayofsolvingtheproblemthatyouarefacingnow.
Alotofbookshavebeenwrittentryingtogroupsolutionstocommonproblems,alsoknownasdesignpatterns.Designpatternsarenotalgorithmsthatyoucopyandpasteintoyourprogram,showinghowtofixsomethingstep-by-step,butratherrecipesthatshowyou,inaheuristicway,howtolookfortheanswer.
Studyingthemisessentialifyouwanttobecomeaprofessionaldeveloper,notonlyforsolvingproblems,butalsoforcommunicatingwithotherdevelopers.Itisverycommontogetananswerlike“Youcoulduseafactoryhere”,whendiscussingyourprogramdesign.Itsavesalotoftimeknowingwhatafactoryis,ratherthanexplainingthepatterneachtimesomeonementionsit.
Aswesaid,thereareentirebooksthattalkaboutdesignpatterns,andwehighlyrecommendyoutohavealookatsomeofthem.Thegoalofthissectionistoshowyouwhatadesignpatternisandhowyoucanuseit.Additionally,wewillshowyousomeofthemostcommondesignpatternsusedwithPHPwhenwritingwebapplications,excludingtheMVCpattern,whichwewillstudyinChapter6,AdaptingtoMVC.
Otherthanbooks,youcouldalsovisittheopensourceprojectDesignPatternsPHPathttp://designpatternsphp.readthedocs.org/en/latest/README.html.Thereisagoodcollectionofthem,andtheyareimplementedinPHP,soitwouldbeeasierforyoutoadapt.
FactoryAfactoryisadesignpatternofthecreationalgroup,whichmeansthatitallowsyoutocreateobjects.Youmightthinkthatwedonotneedsuchathing,ascreatinganobjectisaseasyasusingthenewkeyword,theclass,anditsarguments.Butlettingtheuserdothatisdangerousfordifferentreasons.Apartfromtheincreaseddifficultycausedbyusingnewwhenunittestingourcode(youwilllearnaboutunittestinginChapter7,TestingWebApplications),alotofcouplingtoogetsaddedintoourcode.
Whenwediscussedencapsulation,youlearnedthatitisbettertohidetheinternalimplementationofaclass,andyoucouldconsidertheconstructoraspartofit.Thereasonisthattheuserneedstoknowatalltimeshowtocreateobjects,includingwhattheargumentsoftheconstructorare.Andwhatifwewanttochangeourconstructortoacceptdifferentarguments?Weneedtogoonebyonetoalltheplaceswherewehavecreatedobjectsandupdatethem.
Anotherreasonforusingfactoriesistomanagedifferentclassesthatinheritasuperclassorimplementthesameinterface.Asyouknow,thankstopolymorphism,youcanuseoneobjectwithoutknowingthespecificclassthatitinstantiates,aslongasyouknowtheinterfacebeingimplemented.Itmightsohappenthatyourcodeneedstoinstantiateanobjectthatimplementsaninterfaceanduseit,buttheconcreteclassoftheobjectmaynotbeimportantatall.
Thinkaboutourbookstoreexample.Wehavetwotypesofcustomers:basicandpremium.Butformostofthecode,wedonotreallycarewhattypeofcustomeraspecificinstanceis.Infact,weshouldimplementourcodetouseobjectsthatimplementtheCustomerinterface,beingunawareofthespecifictype.So,ifwedecideinthefuturetoaddanewtype,aslongasitimplementsthecorrectinterface,ourcodewillworkwithoutanissue.But,ifthatisthecase,whatdowedowhenweneedtocreateanewcustomer?Wecannotinstantiateaninterface,solet’susethefactorypattern.Addthefollowingcodeintosrc/Domain/Customer/CustomerFactory.php:
<?php
namespaceBookstore\Domain\Customer;
useBookstore\Domain\Customer;
classCustomerFactory{
publicstaticfunctionfactory(
string$type,
int$id,
string$firstname,
string$surname,
string$email
):Customer{
switch($type){
case'basic':
returnnewBasic($id,$firstname,$surname,$email);
case'premium':
returnnewPremium($id,$firstname,$surname,$email);
}
}
}
Thefactoryintheprecedingcodeislessthanidealfordifferentreasons.Inthefirstone,weuseaswitch,andaddacaseforalltheexistingcustomertypes.Twotypesdonotmakemuchdifference,butwhatifwehave19?Let’strytomakethisfactorymethodabitmoredynamic.
publicstaticfunctionfactory(
string$type,
int$id,
string$firstname,
string$surname,
string$email
):Customer{
$classname=__NAMESPACE__.'\\'.ucfirst($type);
if(!class_exists($classname)){
thrownew\InvalidArgumentException('Wrongtype.');
}
returnnew$classname($id,$firstname,$surname,$email);
}
Yes,youcandowhatwedidintheprecedingcodeinPHP.Instantiatingclassesdynamically,thatis,usingthecontentofavariableasthenameoftheclass,isoneofthethingsthatmakesPHPsoflexible…anddangerous.Usedwrongly,itwillmakeyourcodehorriblydifficulttoreadandmaintain,sobecarefulaboutit.Notetootheconstant__NAMESPACE__,whichcontainsthenamespaceofthecurrentfile.
Nowthisfactorylookscleaner,anditisalsoverydynamic.Youcouldaddmorecustomertypesand,aslongastheyareinsidethecorrectnamespaceandimplementtheinterface,thereisnothingtochangeonthefactoryside,norintheusageofthefactory.
Inordertouseit,let’schangeourinit.phpfile.Youcanremoveallourtests,andjustleavetheautoloadercode.Then,addthefollowing:
CustomerFactory::factory('basic',2,'mary','poppins',
'mary@poppins.com');
CustomerFactory::factory('premium',null,'james','bond',
'james@bond.com');
Thefactorydesignpatterncanbeascomplexasyouneed.Therearedifferentvariantsofit,andeachonehasitsownplaceandtime,butthegeneralideaisalwaysthesame.
SingletonIfsomeonewithabitofexperiencewithdesignpatterns,orwebdevelopmentingeneral,readsthetitleofthissection,theywillprobablystarttearingtheirhairoutandclaimingthatsingletonistheworstexampleofadesignpattern.Butjustbearwithme.
Whenexplaininginterfaces,Iaddedanoteabouthowdeveloperstendtocomplicatetheircodetoomuchjustsotheycanuseallthetoolstheyknow.Usingdesignpatternsisoneofthecaseswherethishappens.Theyhavebeensofamous,andpeopleclaimedthatgooduseofthemisdirectlylinkedtogreatdevelopers,thateverybodythatlearnsthemtriestousethemabsolutelyeverywhere.
ThesingletonpatternisprobablythemostinfamousofthedesignpatternsusedinPHPforwebdevelopment.Thispatternhasaveryspecificpurpose,andwhenthatisthecase,thepatternprovestobeveryuseful.Butthispatternissoeasytoimplementthatdeveloperscontinuouslytrytoaddsingletonseverywhere,turningtheircodeintosomethingunmaintainable.Itisforthisreasonthatpeoplecallthisananti-pattern,somethingthatshouldbeavoidedratherthanused.
Idoagreewiththispointofview,butIstillthinkthatyoushouldbeveryfamiliarwiththisdesignpattern.Eventhoughyoushouldavoiditsoveruse,peoplestilluseiteverywhere,andtheyrefertoitcountlesstimes,soyoushouldbeinapositiontoeitheragreewiththemorratherhaveenoughreasonstodiscouragethemtouseit.Havingsaidthat,let’sseewhattheaimofthesingletonpatternis.
Theideaissimple:singletonsareusedwhenyouwantoneclasstoalwayshaveoneuniqueinstance.Everytime,andeverywhereyouusethatclass,ithastobethroughthesameinstance.Thereasonistoavoidhavingtoomanyinstancesofsomeheavyresource,ortokeepalwaysthesamestateeverywhere—tobeglobal.Examplesofthisaredatabaseconnectionsorconfigurationhandlers.
Imaginethatinordertorun,ourapplicationneedssomeconfiguration,suchascredentialsforthedatabase,URLsofspecialendpoints,directorypathsforfindinglibrariesorimportantfiles,andsoon.Whenyoureceivearequest,thefirstthingyoudoistoloadthisconfigurationfromthefilesystem,andthenyoustoreitasanarrayorsomeotherdatastructure.Savethefollowingcodeasyoursrc/Utils/Config.phpfile:
<?php
namespaceBookstore\Utils;
useBookstore\Exceptions\NotFoundException;
classConfig{
private$data;
publicfunction__construct(){
$json=file_get_contents(__DIR__.'/../../config/app.json');
$this->data=json_decode($json,true);
}
publicfunctionget($key){
if(!isset($this->data[$key])){
thrownewNotFoundException("Key$keynotinconfig.");
}
return$this->data[$key];
}
}
Asyoucansee,thisclassusesanewexception.Createitundersrc/Utils/NotFoundException.php:
<?php
namespaceBookstore\Exceptions;
useException;
classNotFoundExceptionextendsException{
}
Also,theclassreadsafile,config/app.json.YoucouldaddthefollowingJSONmapinsideit:
{
"db":{
"user":"Luke",
"password":"Skywalker"
}
}
Inordertousethisconfiguration,let’saddthefollowingcodeintoyourinit.phpfile.
$config=newConfig();
$dbConfig=$config->get('db');
var_dump($dbConfig);
Thatseemsaverygoodwaytoreadconfiguration,right?Butpayattentiontothehighlightedline.WeinstantiatetheConfigobject,hence,wereadafile,transformitscontentsfromJSONtoarray,andstoreit.Whatifthefilecontainshundredsoflinesinsteadofjustsix?Youshouldnoticethenthatinstantiatingthisclassisveryexpensive.
Youdonotwanttoreadthefilesandtransformthemintoarrayseachtimeyouaskforsomedatafromyourconfiguration.Thatiswaytooexpensive!But,forsure,youwillneedtheconfigurationarrayinverydifferentplacesofyourcode,andyoucannotcarrythisarrayeverywhereyougo.Ifyouunderstoodstaticpropertiesandmethods,youcouldarguethatimplementingastaticarrayinsidetheobjectshouldfixtheproblem.Youinstantiateitonce,andthenjustcallastaticmethodthatwillaccessanalreadypopulatedstaticproperty.Theoretically,weskiptheinstantiation,right?
<?php
namespaceBookstore\Utils;
useBookstore\Exceptions\NotFoundException;
classConfig{
privatestatic$data;
publicfunction__construct(){
$json=file_get_contents(__DIR__.'/../config/app.json');
self::$data=json_decode($json,true);
}
publicstaticfunctionget($key){
if(!isset(self::$data[$key])){
thrownewNotFoundException("Key$keynotinconfig.");
}
returnself::$data[$key];
}
}
Thisseemstobeagoodidea,butitishighlydangerous.Howcanyoubeabsolutelysurethatthearrayhasalreadybeenpopulated?Andhowcanyoubesurethat,evenusingastaticcontext,theuserwillnotkeepinstantiatingthisclassagainandagain?Thatiswheresingletonscomeinhandy.
Implementingasingletonimpliesthefollowingpoints:
1. Maketheconstructoroftheclassprivate,soabsolutelynoonefromoutsidetheclasscaneverinstantiatethatclass.
2. Createastaticpropertynamed$instance,whichwillcontainaninstanceofitself—thatis,inourConfigclass,the$instancepropertywillcontainaninstanceoftheclassConfig.
3. Createastaticmethod,getInstance,whichwillcheckif$instanceisnull,andifitis,itwillcreateanewinstanceusingtheprivateconstructor.Eitherway,itwillreturnthe$instanceproperty.
Let’sseewhatthesingletonclasswouldlooklike:
<?php
namespaceBookstore\Utils;
useBookstore\Exceptions\NotFoundException;
classConfig{
private$data;
privatestatic$instance;
privatefunction__construct(){
$json=file_get_contents(__DIR__.'/../config/app.json');
$this->data=json_decode($json,true);
}
publicstaticfunctiongetInstance(){
if(self::$instance==null){
self::$instance=newConfig();
}
returnself::$instance;
}
publicfunctionget($key){
if(!isset($this->data[$key])){
thrownewNotFoundException("Key$keynotinconfig.");
}
return$this->data[$key];
}
}
Ifyourunthiscoderightnow,itwillthrowyouanerror,astheconstructorofthisclassisprivate.Firstachievementunlocked!Let’susethisclassproperly:
$config=Config::getInstance();
$dbConfig=$config->get('db');
var_dump($dbConfig);
Doesitconvinceyou?Itprovestobeveryhandyindeed.ButIcannotemphasizethisenough:becarefulwhenyouusethisdesignpattern,asithasvery,very,specificusecases.Avoidfallingintothetrapofimplementingiteverywhere!
AnonymousfunctionsAnonymousfunctions,orlambdafunctions,arefunctionswithoutaname.Astheydonothaveaname,inordertobeabletoinvokethem,weneedtostorethemasvariables.Itmightbestrangeatthebeginning,buttheideaisquitesimple.Atthispointoftime,wedonotreallyneedanyanonymousfunction,solet’sjustaddthecodeintoinit.php,andthenremoveit:
$addTaxes=function(array&$book,$index,$percentage){
$book['price']+=round($percentage*$book['price'],2);
};
Thisprecedinganonymousfunctiongetsassignedtothevariable$addTaxes.Itexpectsthreearguments:$book(anarrayasareference),$index(notused),and$percentage.Thefunctionaddstaxestothepricekeyofthebook,roundedto2decimalplaces(roundisanativePHPfunction).Donotmindtheargument$index,itisnotusedinthisfunction,butforcedbyhowwewilluseit,asyouwillsee.
Youcouldinstantiatealistofbooksasanarray,iteratethem,andthencallthisfunctioneachtime.Anexamplecouldbeasfollows:
$books=[
['title'=>'1984','price'=>8.15],
['title'=>'DonQuijote','price'=>12.00],
['title'=>'Odyssey','price'=>3.55]
];
foreach($booksas$index=>$book){
$addTaxes($book,$index,0.16);
}
var_dump($books);
Inordertousethefunction,youjustinvokeitasif$addTaxescontainedthenameofthefunctiontobeinvoked.Therestofthefunctionworksasifitwasanormalfunction:itreceivesarguments,itcanreturnavalue,andithasascope.Whatisthebenefitofdefiningitinthisway?Onepossibleapplicationwouldbetouseitasacallable.AcallableisavariabletypethatidentifiesafunctionthatPHPcancall.Yousendthiscallablevariableasanargument,andthefunctionthatreceivesitcaninvokeit.TakethePHPnativefunction,array_walk.Itgetsanarray,acallable,andsomeextraarguments.PHPwilliteratethearray,andforeachelement,itwillinvokethecallablefunction(justliketheforeachloop).So,youcanreplacethewholeloopbyjustthefollowing:
array_walk($books,$addTaxes,0.16);
Thecallablethatarray_walkreceivesneedstotakeatleasttwoarguments:thevalueandtheindexofthecurrentelementofthearray,andthus,the$indexargumentthatwewereforcedtoimplementpreviously.Itcanoptionallytakeextraarguments,whichwillbetheextraargumentssenttoarray_walk—inthiscase,the0.16as$percentage.
Actually,anonymousfunctionsarenottheonlycallableinPHP.Youcansendnormalfunctionsandevenclassmethods.Let’sseehow:
functionaddTaxes(array&$book,$index,$percentage){
if(isset($book['price'])){
$book['price']+=round($percentage*$book['price'],2);
}
}
classTaxes{
publicstaticfunctionadd(array&$book,$index,$percentage)
{
if(isset($book['price'])){
$book['price']+=round($percentage*$book['price'],2);
}
}
publicfunctionaddTaxes(array&$book,$index,$percentage)
{
if(isset($book['price'])){
$book['price']+=round($percentage*$book['price'],2);
}
}
}
//usingnormalfunction
array_walk($books,'addTaxes',0.16);
var_dump($books);
//usingstaticclassmethod
array_walk($books,['Taxes','add'],0.16);
var_dump($books);
//usingclassmethod
array_walk($books,[newTaxes(),'addTaxes'],0.16);
var_dump($books);
Intheprecedingexample,youcanseehowwecanuseeachcaseasacallable.Fornormalmethods,justsendthenameofthemethodasastring.Forstaticmethodsofaclass,sendanarraywiththenameoftheclassinawaythatPHPunderstands(eitherthefullnameincludingnamespace,oraddingtheusekeywordbeforehand),andthenameofthemethod,bothasstrings.Touseanormalmethodofaclass,youneedtosendanarraywithaninstanceofthatclassandthemethodnameasastring.
OK,soanonymousfunctionscanbeusedascallable,justasanyotherfunctionormethodcan.Sowhatissospecialaboutthem?Oneofthethingsisthatanonymousfunctionsarevariables,andsotheyhavealltheadvantages—ordisadvantages—thatavariablehas.Thatincludesscope—thatis,thefunctionisdefinedinsideascope,andassoonasthisscopeends,thefunctionwillnolongerbeaccessible.Thatcanbeusefulifyourfunctionisextremelyspecifictothatbitofcode,andthereisnowayyouwillwanttoreuseitsomewhereelse.Moreover,asitisnameless,youwillnothaveconflictswithanyotherexistingfunction.
Thereisanotherbenefitinusinganonymousfunctions:inheritingvariablesfromtheparentscope.Whenyoudefineananonymousfunction,youcanspecifysomevariablefromthescopewhereitisdefinedwiththekeyworduse,anduseitinsidethefunction.Thevalueofthevariablewillbetheoneithadatthemomentofdeclaringthefunction,
evenifitisupdatedlater.Let’sseeanexample:
$percentage=0.16;
$addTaxes=function(array&$book,$index)use($percentage){
if(isset($book['price'])){
$book['price']+=round($percentage*$book['price'],2);
}
};
$percentage=100000;
array_walk($books,$addTaxes);
var_dump($books);
Theprecedingexampleshowsyouhowtousethekeyworduse.Evenwhenweupdate$percentageafterdefiningthefunction,theresultshowsyouthatthetaxeswereonly16%.Thisisuseful,asitliberatesyoufromhavingtosend$percentageeverywhereyouwanttousethefunction$addTaxes.Ifthereisascenariowhereyoureallyneedtohavetheupdatedvalueoftheusedvariables,youcandeclarethemasareferenceasyouwouldwithanormalfunction’sargument:
$percentage=0.16;
$addTaxes=function(array&$book,$index)use(&$percentage){
if(isset($book['price'])){
$book['price']+=round($percentage*$book['price'],2);
}
};
array_walk($books,$addTaxes,0.16);
var_dump($books);
$percentage=100000;
array_walk($books,$addTaxes,0.16);
var_dump($books);
Inthislastexample,thefirstarray_walkusedtheoriginalvalue0.16,asthatwasstillthevalueofthevariable.Butonthesecondcall,$percentagehadalreadychanged,anditaffectedtheresultoftheanonymousfunction.
SummaryInthischapter,youhavelearnedwhatobject-orientedprogrammingis,andhowtoapplyittoourwebapplicationforcreatingacleancode,whichiseasytomaintain.Youalsoknowhowtomanageexceptionsproperly,thedesignpatternsthatareusedthemost,andhowtouseanonymousfunctionswhennecessary.
Inthenextchapter,wewillexplainhowtomanagethedataofyourapplicationusingdatabasessothatyoucancompletelyseparatedatafromcode.
Chapter5.UsingDatabasesDataisprobablythecornerstoneofmostwebapplications.Sure,yourapplicationhastobepretty,fast,error-free,andsoon,butifsomethingisessentialtousers,itiswhatdatayoucanmanageforthem.Fromthis,wecanextractthatmanagingdataisoneofthemostimportantthingsyouhavetoconsiderwhendesigningyourapplication.
Managingdataimpliesnotonlystoringread-onlyfilesandreadingthemwhenneeded,asweweredoingsofar,butalsoadding,fetching,updating,andremovingindividualpiecesofinformation.Forthis,weneedatoolthatcategorizesourdataandmakesthesetaskseasierforus,andthisiswhendatabasescomeintoplay.
Inthischapter,youwilllearnabout:
SchemasandtablesManipulatingandqueryingdataUsingPDOtoconnectyourdatabasewithPHPIndexingyourdataConstructingcomplexqueriesinjoiningtables
IntroducingdatabasesDatabasesaretoolstomanagedata.Thebasicfunctionsofadatabaseareinserting,searching,updating,anddeletingdata,eventhoughmostdatabasesystemsdomorethanthis.Databasesareclassifiedintotwodifferentcategoriesdependingonhowtheystoredata:relationalandnonrelationaldatabases.
Relationaldatabasesstructuredatainaverydetailedway,forcingtheusertouseadefinedformatandallowingthecreationofconnections—thatis,relations—betweendifferentpiecesofinformation.Nonrelationaldatabasesaresystemsthatstoredatainamorerelaxedway,asthoughtherewerenoapparentstructure.Eventhoughwiththeseveryvaguedefinitionsyoucouldassumethateverybodywouldliketouserelationaldatabases,bothsystemsareveryuseful;itjustdependsonhowyouwanttousethem.
Inthisbook,wewillfocusonrelationaldatabasesastheyarewidelyusedinsmallwebapplications,inwhichtherearenothugeamountsofdata.Thereasonisthatusuallytheapplicationcontainsdatathatisinterrelated;forexample,ourapplicationcouldstoresales,whicharecomposedofcustomersandbooks.
MySQLMySQLhasbeenthefavoritechoiceofPHPdevelopersforquitealongtime.ItisarelationaldatabasesystemthatusesSQLasthelanguagetocommunicatewiththesystem.SQLisusedinquiteafewothersystems,whichmakesthingseasierincaseyouneedtoswitchdatabasesorjustneedtounderstandanapplicationwithadifferentdatabasethantheoneyouareusedto.TherestofthechapterwillbefocusedonMySQL,butitwillbehelpfulforyouevenifyouchooseadifferentSQLsystem.
InordertouseMySQL,youneedtoinstalltwoapplications:theserverandtheclient.Youmightrememberserver-clientapplicationsfromChapter2,WebApplicationswithPHP.TheMySQLserverisaprogramthatlistensforinstructionsorqueriesfromclients,executesthem,andreturnsaresult.Youneedtostarttheserverinordertoaccessthedatabase;takealookatChapter1,SettingUptheEnvironment,onhowtodothis.Theclientisanapplicationthatallowsyoutoconstructinstructionsandsendthemtotheserver,anditistheonethatyouwilluse.
NoteGUIversuscommandline
TheGraphicalUserInterface(GUI)isverycommonwhenusingadatabase.Ithelpsyouinconstructinginstructions,andyoucanevenmanagedatawithoutthemusingjustvisualtables.Ontheotherhand,command-lineclientsforceyoutowriteallthecommandsbyhand,buttheyarelighterthanGUIs,fastertostart,andforceyoutorememberhowtowriteSQL,whichyouneedwhenyouwriteyourapplicationsinPHP.Also,ingeneral,almostanymachinewithadatabasewillhaveaMySQLclientbutmightnothaveagraphicalapplication.
Youcanchoosetheonethatyouaremorecomfortablewithasyouwillusuallyworkwithyourownmachine.However,keepinmindthatabasicknowledgeofthecommandlinewillsaveyourlifeonseveraloccasions.
Inordertoconnecttheclientwithaserver,youneedtoprovidesomeinformationonwheretoconnectandthecredentialsfortheusertouse.IfyoudonotcustomizeyourMySQLinstallation,youshouldatleasthavearootuserwithnopassword,whichistheonewewilluse.Youcouldthinkthatthisseemstobeahorriblesecurityhole,anditmightbeso,butyoushouldnotbeabletoconnectusingthisuserifyoudonotconnectfromthesamemachineonwhichtheserveris.Themostcommonargumentsthatyoucanusetoprovideinformationwhenstartingtheclientare:
-u<name>:Thisspecifiestheuser—inourcase,root.-p<password>:Withoutaspace,thisspecifiesthepassword.Aswedonothaveapasswordforouruser,wedonotneedtoprovidethis.-h<host>:Thisspecifieswheretoconnect.Bydefault,theclientconnectstothesamemachine.Asthisisourcase,thereisnoneedtospecifyany.Ifyouhadto,youcouldspecifyeitheranIPaddressorahostname.<schemaname>:Thisspecifiesthenameoftheschematouse.Wewillexplainina
bitwhatthismeans.
Withtheserules,youshouldbeabletoconnecttoyourdatabasewiththemysql-urootcommand.Youshouldgetanoutputverysimilartothefollowingone:
$mysql-uroot
WelcometotheMySQLmonitor.Commandsendwith;or\g.
YourMySQLconnectionidis2
Serverversion:5.1.73Sourcedistribution
Copyright(c)2000,2013,Oracleand/oritsaffiliates.Allrights
reserved.
OracleisaregisteredtrademarkofOracleCorporationand/orits
affiliates.Othernamesmaybetrademarksoftheirrespective
owners.
Type'help;'or'\h'forhelp.Type'\c'toclearthecurrentinput
statement.
mysql>
Theterminalwillshowyoutheversionoftheserverandsomeusefulinformationabouthowtousetheclient.Fromnowon,thecommandlinewillstartwithmysql>insteadofyournormalprompt,showingyouthatyouareusingtheMySQLclient.Inordertoexecutequeries,justtypethequery,enditwithasemicolon,andpressEnter.Theclientwillsendthequerytotheserverandwillshowtheresultofit.Toexittheclient,youcaneithertype\qandpressEnterorpressCtrl+D,eventhoughthislastoptionwilldependonyouroperatingsystem.
SchemasandtablesRelationaldatabasesystemsusuallyhavethesamestructure.Theystoredataindifferentdatabasesorschemas,whichseparatethedatafromdifferentapplications.Theseschemasarejustcollectionsoftables.Tablesaredefinitionsofspecificdatastructuresandarecomposedoffields.Afieldisabasicdatatypethatdefinesthesmallestcomponentofinformationasthoughtheyweretheatomsofthedata.So,schemasaregroupoftablesthatarecomposedoffields.Let’slookateachoftheseelements.
UnderstandingschemasAsdefinedbefore,schemasordatabases—inMySQL,theyaresynonyms—arecollectionsoftableswithacommoncontext,usuallybelongingtothesameapplication.Actually,therearenorestrictionsaroundthis,andyoucouldhaveseveralschemasbelongingtothesameapplicationifneeded.However,forsmallwebapplications,asitisourcase,wewillhavejustoneschema.
Yourserverprobablyalreadyhassomeschemas.TheyusuallycontainthemetadataneededforMySQLinordertooperate,andwehighlyrecommendthatyoudonotmodifythem.Instead,let’sjustcreateourownschema.Schemasarequitesimpleelements,andtheyonlyhaveamandatorynameandanoptionalcharset.Thenameidentifiestheschema,andthecharsetdefineswhichtypeofcodificationor“alphabet”thestringsshouldfollow.Asthedefaultcharsetislatin1,ifyoudonotneedtochangeit,youdonotneedtospecifyit.
UseCREATESCHEMAfollowedbythenameoftheschemainordertocreatetheschemathatwewilluseforourbookstore.Thenamehastoberepresentative,solet’snameitbookstore.Remembertoendyourlinewithasemicolon.Takealookatthefollowing:
mysql>CREATESCHEMAbookstore;
QueryOK,1rowaffected(0.00sec)
Ifyouneedtorememberhowaschemawascreated,youcanuseSHOWCREATESCHEMAtoseeitsdescription,asfollows:
mysql>SHOWCREATESCHEMAbookstore\G
***************************1.row***************************
Database:bookstore
CreateDatabase:CREATEDATABASE`bookstore`/*!40100DEFAULTCHARACTERSET
latin1*/
1rowinset(0.00sec)
Asyoucansee,weendedthequerywith\Ginsteadofasemicolon.Thistellstheclienttoformattheresponseinadifferentwaythanthesemicolondoes.WhenusingacommandoftheSHOWCREATEfamily,werecommendthatyouenditwith\Gtogetabetterunderstanding.
TipShouldyouuseuppercaseorlowercase?
Whenwritingqueries,youmightnotethatweuseduppercaseforkeywordsandlowercaseforidentifiers,suchasnamesofschemas.ThisisjustaconventionwidelyusedinordertomakeitclearwhatispartofSQLandwhatisyourdata.However,MySQLkeywordsarecase-insensitive,soyoucoulduseanycaseindistinctively.
Alldatamustbelongtoaschema.Therecannotbedatafloatingaroundoutsideallschemas.Thisway,youcannotdoanythingunlessyouspecifytheschemayouwanttouse.Inordertodothis,justafterstartingyourclient,usetheUSEkeywordfollowedbythenameoftheschema.Optionally,youcouldtelltheclientwhichschematousewhen
connectingtoit,asfollows:
mysql>USEbookstore;
Databasechanged
Ifyoudonotrememberwhatthenameofyourschemaisorwanttocheckwhichotherschemasareinyourserver,youcanruntheSHOWSCHEMAS;commandtogetalistofthem,asfollows:
mysql>SHOWSCHEMAS;
+--------------------+
|Database|
+--------------------+
|information_schema|
|bookstore|
|mysql|
|test|
+--------------------+
4rowsinset(0.00sec)
DatabasedatatypesAsinPHP,MySQLalsohasdatatypes.Theyareusedtodefinewhichkindofdataafieldcancontain.AsinPHP,MySQLisquiteflexiblewithdatatypes,transformingthemfromonetypetotheotherifneeded.Therearequiteafewofthem,butwewillexplainthemostimportantones.Wehighlyrecommendthatyouvisittheofficialdocumentationrelatedtodatatypesathttp://dev.mysql.com/doc/refman/5.7/en/data-types.htmlifyouwanttobuildapplicationswithmorecomplexdatastructures.
NumericdatatypesNumericdatacanbecategorizedasintegersordecimalnumbers.Forintegers,MySQLusestheINTdatatypeeventhoughthereareversionstostoresmallernumbers,suchasTINYINT,SMALLINT,orMEDIUMINT,orbiggernumbers,suchasBIGINT.Thefollowingtableshowswhatthesizesofthedifferentnumerictypesare,soyoucanchoosewhichonetousedependingonyoursituation:
Type Size/precision
TINYINT -128to127
SMALLINT -32,768to32,767
MEDIUMINT -8,388,608to8,388,607
INT -2,147,483,648to2,147,483,647
BIGINT -9,223,372,036,854,775,808to9,223,372,036,854,775,807
Numerictypescanbedefinedassignedbydefaultorunsigned;thatis,youcanallowornotallowthemtocontainnegativevalues.IfanumerictypeisdefinedasUNSIGNED,therangeofnumbersthatitcantakeisdoubledasitdoesnotneedtosavespacefornegativenumbers.
Fordecimalnumberswehavetwotypes:approximatevalues,whicharefastertoprocessbutarenotexactsometimes,andexactvaluesthatgiveyouexactprecisiononthedecimalvalue.Forapproximatevaluesorthefloating-pointtype,wehaveFLOATandDOUBLE.Forexactvaluesorthefixed-pointtypewehaveDECIMAL.
MySQLallowsyoutospecifythenumberofdigitsanddecimalpositionsthatthenumbercantake.Forexample,tospecifyanumberthatcancontainsfivedigitsanduptotwoofthemcanbedecimal,wewillusetheFLOAT(5,2)notation.Thisisusefulasaconstraint,asyouwillnotewhenwecreatetableswithprices.
StringdatatypesEventhoughthereareseveraldatatypesthatallowyoutostorefromsinglecharacterstobigchunksoftextorbinarycode,itisoutsidethescopeofthischapter.Inthissection,wewillintroduceyoutothreetypes:CHAR,VARCHAR,andTEXT.
CHARisadatatypethatallowsyoutostoreanexactnumberofcharacters.Youneedtospecifyhowlongthestringwillbeonceyoudefinethefield,andfromthispointon,allvaluesforthisfieldhavetobeofthislength.OnepossibleusageinourapplicationscouldbewhenstoringtheISBNofthebookasweknowitisalways13characterslong.
VARCHARorvariablecharisadatatypethatallowsyoutostorestringsupto65,535characterslong.Youdonotneedtospecifyhowlongtheyneedtobe,andyoucaninsertstringsofdifferentlengthswithoutanissue.Ofcourse,thefactthatthistypeisdynamicmakesitslowertoprocesscomparedwiththepreviousone,butafterafewtimesyouknowhowlongastringwillalwaysbe.YoucouldtellMySQLthatevenifyouwanttoinsertstringsofdifferentlengths,themaximumlengthwillbeadeterminednumber.Thiswillhelpitsperformance.Forexample,namesareofdifferentlengths,butyoucansafelyassumethatnonamewillbelongerthan64characters,soyourfieldcouldbedefinedasVARCHAR(64).
Finally,TEXTisadatatypeforreallybigstrings.Youcoulduseitifyouwanttostorelongcommentsfromusers,articles,andsoon.AswithINT,therearedifferentversionsofthisdatatype:TINYTEXT,TEXT,MEDIUMTEXT,andLONGTEXT.Eveniftheyareveryimportantinalmostanywebapplicationwithuserinteraction,wewillnotusetheminours.
ListofvaluesInMySQL,youcanforceafieldtohaveasetofvalidvalues.Therearetwotypesofthem:ENUM,whichallowsexactlyoneofthepossiblepredefinedvalues,andSET,whichallowsanynumberofthepredefinedvalues.
Forexample,inourapplication,wehavetwotypesofcustomers:basicandpremium.Ifwewanttostoreourcustomersinadatabase,thereisachancethatoneofthefieldswillbethetypeofcustomer.Asacustomerhastobeeitherbasicorpremium,agoodsolutionwouldbetodefinethefieldasanenumasENUM("basic","premium").Inthisway,wewillmakesurethatallcustomersstoredinourdatabasewillbeofacorrecttype.
Althoughenumsarequitecommontouse,theuseofsetsislesswidespread.Itisusuallyabetterideatouseanextratabletodefinethevaluesofthelist,asyouwillnotewhenwetalkaboutforeignkeysinthischapter.
DateandtimedatatypesDateandtimetypesarethemostcomplexdatatypesinMySQL.Eventhoughtheideaissimple,thereareseveralfunctionsandedgecasesaroundthesetypes.Wecannotgothroughallofthem,sowewilljustexplainthemostcommonuses,whicharetheoneswewillneedforourapplication.
DATEstoresdates—thatis,acombinationofday,month,andyear.TIMEstorestimes—thatis,acombinationofhour,minute,andsecond.DATETIMEaredatatypesforbothdateandtime.Foranyofthesedatatypes,youcanprovidejustastringspecifyingwhatthevalueis,butyouneedtobecarefulwiththeformatthatyouuse.Eventhoughyoucanalwaysspecifytheformatthatyouareenteringthedatain,youcanjustenterthedatesortimesinthedefaultformat—forexample,2014-12-31fordates,14:34:50fortime,and2014-12-31
14:34:50forthedateandtime.
AfourthtypeisTIMESTAMP.Thistypestoresaninteger,whichistherepresentationofthesecondsfromJanuary1,1970,whichisalsoknownastheUnixtimestamp.ThisisaveryusefultypeasinPHP,itisreallyeasytogetthecurrentUnixtimestampwiththenow()function,andtheformatforthisdatatypeisalwaysthesame,soitissafertoworkwithit.Thedownsideisthattherangeofdatesthatitcanrepresentislimitedascomparedtoothertypes.
Therearesomefunctionsthathelpyoumanagethesetypes.Thesefunctionsextractspecificpartsofthewholevalue,returnthevaluewithadifferentformat,addorsubtractdates,andsoon.Let’stakealookatashortlistofthem:
Functionname Description
DAY(),MONTH(),andYEAR() Extractsthespecificvaluefortheday,month,oryearfromtheDATEorDATETIMEprovidedvalue.
HOUR(),MINUTE(),andSECOND()
Extractsthespecificvalueforthehour,minute,orsecondfromtheTIMEorDATETIMEprovidedvalue.
CURRENT_DATE()andCURRENT_TIME()
Returnsthecurrentdateorcurrenttime.
NOW() Returnsthecurrentdateandtime.
DATE_FORMAT() ReturnstheDATE,TIMEorDATETIMEvaluewiththespecifiedformat.
DATE_ADD() Addsthespecifiedintervaloftimetoagivendateortimetype.
Donotworryifyouareconfusedonhowtouseanyofthesefunctions;wewillusethemduringtherestofthebookaspartofourapplication.Also,anextensivelistofallthetypescanbefoundathttp://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html.
ManagingtablesNowthatyouunderstandthedifferenttypesofdatathatfieldscantake,itistimetointroducetables.AsdefinedintheSchemasandtablessection,atableisacollectionoffieldsthatdefinesatypeofinformation.YoucouldcompareitwithOOPandthinkoftablesasclasses,fieldsbeingtheirproperties.Eachinstanceoftheclasswouldbearowonthetable.
Whendefiningatable,youhavetodeclarethelistoffieldsthatthetablecontains.Foreachfield,youneedtospecifyitsname,itstype,andsomeextrainformationdependingonthetypeofthefield.Themostcommonare:
NOTNULL:Thisisusedifthefieldcannotbenull—thatis,ifitneedsaconcretevalidvalueforeachrow.Bydefault,afieldcanbenull.UNSIGNED:Asmentionedearlier,thisisusedtoforbidtheuseofnegativenumbersinthisfield.Bydefault,anumericfieldacceptsnegativenumbers.DEFAULT<value>:Thisdefinesadefaultvalueincasetheuserdoesnotprovideany.Usually,thedefaultvalueisnullifthisclauseisnotspecified.
Tabledefinitionsalsoneedaname,aswithschemas,andsomeoptionalattributes.Youcandefinethecharsetofthetableoritsengine.Enginescanbeaquitelargetopictocover,butforthescopeofthischapter,let’sjustnotethatweshouldusetheInnoDBengineifweneedstrongrelationshipsbetweentables.Formoreadvancedreaders,youcanreadmoreaboutMySQLenginesathttps://dev.mysql.com/doc/refman/5.0/en/storage-engines.html.
Knowingthis,let’strytocreateatablethatwillkeepourbooks.Thenameofthetableshouldbebook,aseachrowwilldefineabook.ThefieldscouldhavethesamepropertiestheBookclasshas.Let’stakealookathowthequerytoconstructthetablewouldlook:
mysql>CREATETABLEbook(
->isbnCHAR(13)NOTNULL,
->titleVARCHAR(255)NOTNULL,
->authorVARCHAR(255)NOTNULL,
->stockSMALLINTUNSIGNEDNOTNULLDEFAULT0,
->priceFLOATUNSIGNED
->)ENGINE=InnoDb;
QueryOK,0rowsaffected(0.01sec)
Asyoucannote,wecanaddmorenewlinesuntilweendthequerywithasemicolon.Withthis,wecanformatthequeryinawaythatlooksmorereadable.MySQLwillletusknowthatwearestillwritingthesamequeryshowingthe->prompt.Asthistablecontainsfivefields,itisverylikelythatwewillneedtorefreshourmindsfromtimetotimeaswewillforgetthem.Inordertodisplaythestructureofthetable,youcouldusetheDESCcommand,asfollows:
mysql>DESCbook;
+--------+----------------------+------+-----+---------+-------+
|Field|Type|Null|Key|Default|Extra|
+--------+----------------------+------+-----+---------+-------+
|isbn|char(13)|NO||NULL||
|title|varchar(255)|NO||NULL||
|author|varchar(255)|NO||NULL||
|stock|smallint(5)unsigned|NO||0||
|price|floatunsigned|YES||NULL||
+--------+----------------------+------+-----+---------+-------+
5rowsinset(0.00sec)
WeusedSMALLINTforstockasitisveryunlikelythatwewillhavemorethanthousandsofcopiesofthesamebook.AsweknowthatISBNis13characterslong,weenforcedthiswhendefiningthefield.Finally,bothstockandpriceareunsignedasnegativevaluesdonotmakesense.Let’snowcreateourcustomertableviathefollowingscript:
mysql>CREATETABLEcustomer(
->idINTUNSIGNEDNOTNULL,
->firstnameVARCHAR(255)NOTNULL,
->surnameVARCHAR(255)NOTNULL,
->emailVARCHAR(255)NOTNULL,
->typeENUM('basic','premium')
->)ENGINE=InnoDb;
QueryOK,0rowsaffected(0.00sec)
Wealreadyanticipatedtheuseofenumforthefieldtypeaswhendesigningclasses,wecoulddrawadiagramidentifyingthecontentofourdatabase.Onthis,wecouldshowthetablesandtheirfields.Let’stakealookathowthediagramoftableswouldlooksofar:
Notethatevenifwecreatetablessimilartoourclasses,wewillnotcreateatableforPerson.Thereasonisthatdatabasesstoredata,andthereisn’tanydatathatwecouldstoreforthisclassasthecustomertablealreadycontainseverythingweneed.Also,sometimes,wemaycreatetablesthatdonotexistasclassesonourcode,sotheclass-tablerelationshipisaveryflexibleone.
KeysandconstraintsNowthatwehaveourmaintablesdefined,let’strytothinkabouthowthedatainsidewouldlook.Eachrowinsideatablewoulddescribeanobject,whichmaybeeitherabookoracustomer.Whatwouldhappenifourapplicationhasabugandallowsustocreatebooksorcustomerswiththesamedata?Howwillthedatabasedifferentiatethem?Intheory,wewillassignIDstocustomersinordertoavoidthesescenarios,buthowdoweenforcethattheIDnotberepeated?
MySQLhasamechanismthatallowsyoutoenforcecertainrestrictionsonyourdata.OtherthanattributessuchasNOTNULLorUNSIGNEDthatyoualreadysaw,youcantellMySQLthatcertainfieldsaremorespecialthanothersandinstructittoaddsomebehaviortothem.Thesemechanismsarecalledkeys,andtherearefourtypes:primarykey,uniquekey,foreignkey,andindex.Let’stakeacloserlookatthem.
PrimarykeysPrimarykeysarefieldsthatidentifyauniquerowfromatable.Therecannotbetwoofthesamevalueinthesametable,andtheycannotbenull.Addingaprimarykeytoatablethatdefinesobjectsisalmostamustasitwillassureyouthatyouwillalwaysbeabletodifferentiatetworowsbythisfield.
Anotherpartthatmakesprimarykeyssoattractiveistheirabilitytosettheprimarykeyasanautoincrementalnumericvalue;thatis,youdonothavetoassignavaluetotheID,andMySQLwilljustpickupthelatestinsertedIDandincrementitby1,aswedidwithourUniquetrait.Ofcourse,forthistohappen,yourfieldhastobeanintegerdatatype.Infact,wehighlyrecommendthatyoualwaysdefineyourprimarykeyasaninteger,evenifthereal-lifeobjectdoesnotreallyhavethisIDatall.ThereasonisthatyoushouldsearcharowbythisnumericID,whichisunique,andMySQLwilladdsomeperformanceimprovementsthatcomebysettingthefieldasakey.
Then,let’saddanIDtoourbooktable.Inordertoaddanewfield,weneedtoalterourtable.Thereisacommandthatallowsyoutodothis:ALTERTABLE.Withthiscommand,youcanmodifythedefinitionofanyexistingfield,addnewones,orremoveexistingones.Asweaddthefieldthatwillbeourprimarykeyandbeautoincremental,wecanaddallthesemodifierstothefielddefinition.Executethefollowingcode:
mysql>ALTERTABLEbook
->ADDidINTUNSIGNEDNOTNULLAUTO_INCREMENT
->PRIMARYKEYFIRST;
QueryOK,0rowsaffected(0.02sec)
Records:0Duplicates:0Warnings:0
NoteFIRSTattheendofthecommand.Whenaddingnewfields,ifyouwantthemtoappearonadifferentpositionthanattheendofthetable,youneedtospecifytheposition.ItcouldbeeitherFIRSTorAFTER<otherfield>.Forconvenience,theprimarykeyofatableisthefirstofitsfields.
AsthetablecustomeralreadyhasanIDfield,wedonothavetoadditagainbutrathermodifyit.Inordertodothis,wewilljustusetheALTERTABLEcommandwiththeMODIFYoption,specifyingthenewdefinitionofanalreadyexistingfield,asfollows:
mysql>ALTERTABLEcustomer
->MODIFYidINTUNSIGNEDNOTNULL
->AUTO_INCREMENTPRIMARYKEY;
QueryOK,0rowsaffected(0.00sec)
Records:0Duplicates:0Warnings:0
ForeignkeysLet’simaginethatweneedtokeeptrackoftheborrowedbooks.Thetableshouldcontaintheborrowedbook,whoborrowedit,andwhenitwasborrowed.So,whatkindofdatawouldyouusetoidentifythebookorthecustomer?Wouldyouusethetitleorthename?Well,weshouldusesomethingthatidentifiesauniquerowfromthesetables,andthis“something”istheprimarykey.Withthisaction,wewilleliminatethechangeofusingareferencethatcanpotentiallypointtotwoormorerowsatthesametime.
Wecouldthencreateatablethatcontainsbook_idandcustomer_idasnumericfields,containingtheIDsthatreferencethesetwotables.Asthefirstapproach,itmakessense,butwecanfindsomeweaknesses.Forexample,whathappensifweinsertwrongIDsandtheydonotexistinbookorcustomer?WecouldhavesomecodeinourPHPsidetomakesurethatwhenfetchinginformationfromborrowed_books,weonlydisplayedtheinformationthatiscorrect.Wecouldevenhavearoutinethatperiodicallychecksforwrongrowsandremovesthem,solvingtheissueofhavingwrongdatawastingspaceinthedisk.However,aswiththeUniquetraitversusaddingprimarykeysinMySQL,itisusuallybettertoallowthedatabasesystemtomanagethesethingsastheperformancewillusuallybebetter,andyoudonotneedtowriteextracode.
MySQLallowsyoutocreatekeysthatenforcereferencestoothertables.Thesearecalledforeignkeys,andtheyaretheprimaryreasonforwhichwewereforcedtousetheInnoDBtableengineinsteadofanyother.Aforeignkeydefinesandenforcesareferencebetweenthisfieldandanotherrowofadifferenttable.IftheIDsuppliedforthefieldwithaforeignkeydoesnotexistinthereferencedtable,thequerywillfail.Furthermore,ifyouhaveavalidborrowed_booksrowpointingtoanexistingbookandyouremovetheentryfromthebooktable,MySQLwillcomplainaboutit—eventhoughyouwillbeabletocustomizethisbehaviorsoon—asthisactionwouldleavewrongdatainthesystem.Asyoucannote,thisiswaymoreusefulthanhavingtowritecodetomanagethesecases.
Let’screatetheborrowed_bookstablewiththebook,customerreferences,anddates.Notethatwehavetodefinetheforeignkeysafterthedefinitionofthefieldsasopposedtowhenwedefinedprimarykeys,asfollows:
mysql>CREATETABLEborrowed_books(
->book_idINTUNSIGNEDNOTNULL,
->customer_idINTUNSIGNEDNOTNULL,
->startDATETIMENOTNULL,
->endDATETIMEDEFAULTNULL,
->FOREIGNKEY(book_id)REFERENCESbook(id),
->FOREIGNKEY(customer_id)REFERENCEScustomer(id)
->)ENGINE=InnoDb;
QueryOK,0rowsaffected(0.00sec)
AswithSHOWCREATESCHEMA,youcanalsocheckhowthetablelooks.ThiscommandwillalsoshowyouinformationaboutthekeysasopposedtotheDESCcommand.Let’stakealookathowitwouldwork:
mysql>SHOWCREATETABLEborrowed_books\G
***************************1.row***************************
Table:borrowed_books
CreateTable:CREATETABLE`borrowed_books`(
`book_id`int(10)unsignedNOTNULL,
`customer_id`int(10)unsignedNOTNULL,
`start`datetimeNOTNULL,
`end`datetimeDEFAULTNULL,
KEY`book_id`(`book_id`),
KEY`customer_id`(`customer_id`),
CONSTRAINT`borrowed_books_ibfk_1`FOREIGNKEY(`book_id`)REFERENCES
`book`(`id`),
CONSTRAINT`borrowed_books_ibfk_2`FOREIGNKEY(`customer_id`)REFERENCES
`customer`(`id`)
)ENGINE=InnoDBDEFAULTCHARSET=latin1
1rowinset(0.00sec)
Notetwoimportantthingshere.Ononehand,wehavetwoextrakeysthatwedidnotdefine.Thereasonisthatwhendefiningaforeignkey,MySQLalsodefinesthefieldasakeythatwillbeusedtoimproveperformanceonthetable;wewilllookintothisinamoment.TheotherelementtonoteisthefactthatMySQLdefinesnamestothekeysbyitself.Thisisnecessaryasweneedtobeabletoreferencethemincasewewanttochangeorremovethiskey.YoucanletMySQLnamethekeysforyou,oryoucanspecifythenamesyoupreferwhencreatingthem.
Wearerunningabookstore,andevenifweallowcustomerstoborrowbooks,wewanttobeabletosellthem.Asaleisaveryimportantelementthatweneedtotrackdownascustomersmaywanttoreviewthem,oryoumayjustneedtoprovidethisinformationfortaxationpurposes.Asopposedtoborrowing,inwhichknowingthebook,customer,anddatewasmorethanenough,here,weneedtosetIDstothesalesinordertoidentifythemtothecustomers.
However,thistableismoredifficulttodesignthantheotheronesandnotjustbecauseoftheID.Thinkaboutit:docustomersbuybooksonebyone?Ordotheyratherbuyanynumberofbooksatonce?Thus,weneedtoallowthetabletocontainanundefinedamountofbooks.WithPHP,thisiseasyaswewouldjustuseanarray,butwedonothavearraysinMySQL.Therearetwooptionstothisproblem.
OnesolutioncouldbetosettheIDofthesaleasanormalintegerfieldandnotasaprimarykey.Inthisway,wewouldbeabletoinsertseveralrowstothesalestable,oneforeachborrowedbook.However,thissolutionislessthanidealaswemisstheopportunityofdefiningaverygoodprimarykeybecauseithasthesalesID.Also,weareduplicatingthedataaboutthecustomeranddatesincetheywillalwaysbethesame.
Thesecondsolution,theonethatwewillimplement,isthecreationofaseparatedtablethatactsasa“list”.Wewillstillhaveoursalestable,whichwillcontaintheIDofthesaleasaprimarykey,thecustomerIDasaforeignkey,andthedates.However,wewillcreateasecondtablethatwecouldnamesale_book,andwewilldefinetheretheIDofthesale,theIDofthebook,andtheamountofbooksofthesamecopythatthecustomerbought.Inthisway,wewillhaveatoncetheinformationaboutthecustomeranddate,andwewillbeabletoinsertasmanyrowsasneededinoursale_booklist-tablewithout
duplicatinganydata.Let’stakealookathowwewouldcreatethese:
mysql>CREATETABLEsale(
->idINTUNSIGNEDNOTNULLAUTO_INCREMENTPRIMARYKEY,
->customer_idINTUNSIGNEDNOTNULL,
->dateDATETIMENOTNULL,
->FOREIGNKEY(customer_id)REFERENCEScustomer(id)
->)ENGINE=InnoDb;
QueryOK,0rowsaffected(0.00sec)
mysql>CREATETABLEsale_book(
->sale_idINTUNSIGNEDNOTNULL,
->book_idINTUNSIGNEDNOTNULL,
->amountSMALLINTUNSIGNEDNOTNULLDEFAULT1,
->FOREIGNKEY(sale_id)REFERENCESsale(id),
->FOREIGNKEY(book_id)REFERENCESbook(id)
->)ENGINE=InnoDb;
QueryOK,0rowsaffected(0.00sec)
Keepinmindthatyoushouldalwayscreatethesalestablefirstbecauseifyoucreatethesale_booktablewithaforeignkeyfirst,referencingatablethatdoesnotexistyet,MySQLwillcomplain.
Wecreatedthreenewtablesinthissection,andtheyareinterrelated.Itisagoodtimetoupdatethediagramoftables.Notethatwelinkthefieldswiththetableswhenthereisaforeignkeydefined.Takealook:
UniquekeysAsyouknow,primarykeysareextremelyusefulastheyprovideseveralfeatureswiththem.Oneoftheseisthatthefieldhastobeunique.However,youcandefineonlyoneprimarykeypertable,eventhoughyoumighthaveseveralfieldsthatareunique.Inordertoamendthislimitation,MySQLincorporatesuniquekeys.Theirjobistomakesurethatthefieldisnotrepeatedinmultiplerows,buttheydonotcomewiththerestofthefunctionalitiesofprimarykeys,suchasbeingautoincremental.Also,uniquekeyscanbenull.
Ourbookandcustomertablescontaingoodcandidatesforuniquekeys.Bookscanpotentiallyhavethesametitle,andsurely,therewillbemorethanonebookbythesameauthor.However,theyalsohaveanISBNwhichisunique;twodifferentbooksshouldnothavethesameISBN.Inthesameway,eveniftwocustomersweretohavethesamename,theire-mailaddresseswillbealwaysdifferent.Let’saddthetwokeyswiththeALTERTABLEcommand,thoughyoucanalsoaddthemwhencreatingthetableaswedidwithforeignkeys,asfollows:
mysql>ALTERTABLEbookADDUNIQUEKEY(isbn);
QueryOK,0rowsaffected(0.01sec)
Records:0Duplicates:0Warnings:0
mysql>ALTERTABLEcustomerADDUNIQUEKEY(email);
QueryOK,0rowsaffected(0.01sec)
Records:0Duplicates:0Warnings:0
IndexesIndexes,whichareasynonymforkeys,arefieldsthatdonotneedanyspecialbehaviorasdotherestofthekeysbuttheyareimportantenoughinourqueries.So,wewillaskMySQLtodosomeworkwiththeminordertoperformbetterwhenqueryingbythisfield.DoyourememberwhenaddingaforeignkeythatMySQLaddedextrakeystothetable?Thosewereindexestoo.
Thinkabouthowtheapplicationwillusethedatabase.Wewanttoshowthecatalogofbookstoourcustomers,butwecannotshowallofthematonceforsure.Thecustomerwillwanttofiltertheresults,andoneofthemostcommonwaysoffilteringisbyspecifyingthetitleofthebookthattheyarelookingfor.Fromthis,wecanextractthatthetitlewillbeusedtofilterbooksquiteoften,sowewanttoaddanindextothisfield.Let’saddtheindexviathefollowingcode:
mysql>ALTERTABLEbookADDINDEX(title);
QueryOK,0rowsaffected(0.01sec)
Records:0Duplicates:0Warnings:0
Rememberthatallotherkeysalsoprovideindexing.IDsofbooks,customersandsales,ISBNs,ande-mailsarealreadyindexed,sothereisnoneedtoaddanotherindexhere.Also,trynottoaddindexestoeverysinglefieldasindoingsoyouwillbeoverindexing,whichwouldmakesometypesofqueriesevenslowerthaniftheywerewithoutindexes!
InsertingdataWehavecreatedtheperfecttablestoholdourdata,butsofartheyareempty.Itistimethatwepopulatethem.Wedelayedthismomentasalteringtableswithdataismoredifficultthanwhentheyareempty.
Inordertoinsertthisdata,wewillusetheINSERTINTOcommand.Thiscommandwilltakethenameofthetable,thefieldsthatyouwanttopopulate,andthedataforeachfield.Notethatyoucanchoosenottospecifythevalueforafield,andtherearedifferentreasonstodothis,whichareasfollows:
Thefieldhasadefaultvalue,andwearehappyusingitforthisspecificrowEventhoughthefielddoesnothaveanexplicitdefaultvalue,thefieldcantakenullvalues;so,bynotspecifyingthefield,MySQLwillautomaticallyinsertanullhereThefieldisaprimarykeyandisautoincremental,andwewanttoletMySQLtakethenextIDforus
TherearedifferentreasonsthatcancauseanINSERTINTOcommandtofail:
IfyoudonotspecifythevalueofafieldandMySQLcannotprovideavaliddefaultvalueIfthevalueprovidedisnotofthetypeofthefieldandMySQLfailstofindavalidconversionIfyouspecifythatyouwanttosetthevalueforafieldbutyoufailtoprovideavalueIfyouprovideaforeignkeywithanIDbuttheIDdoesnotexistinthereferencedtable
Let’stakealookathowtoaddrows.Let’sstartwithourcustomertable,addingonebasicandonepremium,asfollows:
mysql>INSERTINTOcustomer(firstname,surname,email,type)
->VALUES("Han","Solo","han@tatooine.com","premium");
QueryOK,1rowaffected(0.00sec)
mysql>INSERTINTOcustomer(firstname,surname,email,type)
->VALUES("James","Kirk","enter@prise","basic");
QueryOK,1rowaffected(0.00sec)
NotethatMySQLshowsyousomereturninformation;inthiscase,itshowsthattherewasonerowaffected,whichistherowthatweinserted.WedidnotprovideanID,soMySQLjustaddedthenextonesinthelist.Asitisthefirsttimethatweareaddingdata,MySQLusedtheIDs1and2.
Let’strytotrickMySQLandaddanothercustomer,repeatingthee-mailaddressfieldthatwesetasuniqueintheprevioussection:
mysql>INSERTINTOcustomer(firstname,surname,email,type)
->VALUES("Mr","Spock","enter@prise","basic");
ERROR1062(23000):Duplicateentry'enter@prise'forkey'email'
Anerrorisreturnedwithanerrorcodeandanerrormessage,andtherowwasnot
inserted,ofcourse.Theerrormessageusuallycontainsenoughinformationinordertounderstandtheissueandhowtofixit.Ifthisisnotthecase,wecanalwaystrytosearchontheInternetusingtheerrorcodeandnotewhateithertheofficialdocumentationorotherusershavetosayaboutit.
Incaseyouneedtointroducemultiplerowstothesametableandtheycontainthesamefields,thereisashorterversionofthecommand,inwhichyoucanspecifythefieldsandthenprovidethegroupsofvaluesforeachrow.Let’stakealookathowtouseitwhenaddingbookstoourbooktable,asfollows:
mysql>INSERTINTObook(isbn,title,author,stock,price)VALUES
->("9780882339726","1984","GeorgeOrwell",12,7.50),
->("9789724621081","1Q84","HarukiMurakami",9,9.75),
->("9780736692427","AnimalFarm","GeorgeOrwell",8,3.50),
->("9780307350169","Dracula","BramStoker",30,10.15),
->("9780753179246","19minutes","JodiPicoult",0,10);
QueryOK,5rowsaffected(0.01sec)
Records:5Duplicates:0Warnings:0
Aswithcustomers,wewillnotspecifytheIDandletMySQLchoosetheappropriateone.Notealsothatnowtheamountofaffectedrowsis5asweinsertedfiverows.
Howcanwetakeadvantageoftheexplicitdefaultsthatwedefinedinourtables?Well,wecandothisinthesamewayaswedidwiththeprimarykeys:donotspecifytheminthefieldslistorinthevalueslist,andMySQLwilljustusethedefaultvalue.Forexample,wedefinedadefaultvalueof1forourbook.stockfield,whichisausefulnotationforthebooktableandthestockfield.Let’saddanotherrowusingthisdefault,asfollows:
mysql>INSERTINTObook(isbn,title,author,price)VALUES
->("9781416500360","Odyssey","Homer",4.23);
QueryOK,1rowaffected(0.00sec)
Nowthatwehavebooksandcustomers,let’saddsomehistoricdataaboutcustomersborrowingbooks.Forthis,usethenumericIDsfrombookandcustomer,asinthefollowingcode:
mysql>INSERTINTOborrowed_books(book_id,customer_id,start,end)
->VALUES
->(1,1,"2014-12-12","2014-12-28"),
->(4,1,"2015-01-10","2015-01-13"),
->(4,2,"2015-02-01","2015-02-10"),
->(1,2,"2015-03-12",NULL);
QueryOK,3rowsaffected(0.00sec)
Records:3Duplicates:0Warnings:0
QueryingdataIttookquitealotoftime,butwearefinallyinthemostexciting—anduseful—sectionrelatedtodatabases:queryingdata.QueryingdatareferstoaskingMySQLtoreturnrowsfromthespecifiedtableandoptionallyfilteringtheseresultsbyasetofrules.Youcanalsochoosetogetspecificfieldsinsteadofthewholerow.Inordertoquerydata,wewillusetheSELECTcommand,asfollows:
mysql>SELECTfirstname,surname,typeFROMcustomer;
+-----------+---------+---------+
|firstname|surname|type|
+-----------+---------+---------+
|Han|Solo|premium|
|James|Kirk|basic|
+-----------+---------+---------+
2rowsinset(0.00sec)
OneofthesimplestwaystoquerydataistospecifythefieldsofinterestafterSELECTandspecifythetablewiththeFROMkeyword.Aswedidnotaddanyfilters—mostlyknownasconditions—tothequery,wegotalltherowsthere.Sometimes,thisisthedesiredbehavior,butthemostcommonthingtodoistoaddconditionstothequerytoretrieveonlytherowsthatweneed.UsetheWHEREkeywordtoachievethis.
mysql>SELECTfirstname,surname,typeFROMcustomer
->WHEREid=1;
+-----------+---------+---------+
|firstname|surname|type|
+-----------+---------+---------+
|Han|Solo|premium|
+-----------+---------+---------+
1rowinset(0.00sec)
AddingconditionsisverysimilartowhenwecreatedBooleanexpressionsinPHP.Wewillspecifythenameofthefield,anoperator,andavalue,andMySQLwillretrieveonlytherowsthatreturntruetothisexpression.Inthiscase,weaskedforthecustomersthathadtheID1,andMySQLreturnedonerow:theonethathadanIDofexactly1.
Acommonquerywouldbetogetthebooksthatstartwithsometext.Wecannotconstructthisexpressionwithanycomparisonoperandthatyouknow,suchas=and<or>,sincewewanttomatchonlyapartofthestring.Forthis,MySQLhastheLIKEoperator,whichtakesastringthatcancontainwildcards.Awildcardisacharacterthatrepresentsarule,matchinganynumberofcharactersthatfollowstherule.Forexample,the%wildcardrepresentsanynumberofcharacters,sousingthe1%stringwouldmatchanystringthatstartswith1andisfollowedbyanynumberorcharacters,matchingstringssuchas1984or1Q84.Let’sconsiderthefollowingexample:
mysql>SELECTtitle,author,priceFROMbook
->WHEREtitleLIKE"1%";
+------------+-----------------+-------+
|title|author|price|
+------------+-----------------+-------+
|1984|GeorgeOrwell|7.5|
|1Q84|HarukiMurakami|9.75|
|19minutes|JodiPicoult|10|
+------------+-----------------+-------+
3rowsinset(0.00sec)
Weaskedforallthebookswhosetitlestartswith1,andwegotthreerows.Youcanimaginehowusefulthisoperatoris,especiallywhenweimplementasearchutilityinourapplication.
AsinPHP,MySQLalsoallowsyoutoaddlogicaloperators—thatis,operatorsthattakeoperandsandperformalogicaloperation,returningBooleanvaluesasaresult.Themostcommonlogicaloperatorsare,asinPHP,ANDandOR.ANDreturnstrueifboththeexpressionsaretrueandORreturnstrueifeitheroftheoperandsistrue.Let’sconsideranexample,asfollows:
mysql>SELECTtitle,author,priceFROMbook
->WHEREtitleLIKE"1%"ANDstock>0;
+------------+-----------------+-------+
|title|author|price|
+------------+-----------------+-------+
|1984|GeorgeOrwell|7.5|
|1Q84|HarukiMurakami|9.75|
+------------+-----------------+-------+
2rowsinset(0.00sec)
Thisexampleisverysimilartothepreviousone,butweaddedanextracondition.Weaskedforalltitlesstartingwith1andwhetherthereisstockavailable.Thisiswhyoneofthebooksdoesnotshowasitdoesnotsatisfybothconditions.YoucanaddasmanyconditionsasyouneedwithlogicaloperatorsbutbearinmindthatANDoperatorstakeprecedenceoverOR.Ifyouwanttochangethisprecedence,youcanalwayswrapexpressionswithaparenthesis,asinPHP.
Sofar,wehaveretrievedspecificfieldswhenqueryingfordata,butwecouldaskforallthefieldsinagiventable.Todothis,wewilljustusethe*wildcardinSELECT.Let’sselectallthefieldsforthecustomersviathefollowingcode:
mysql>SELECT*FROMcustomer\G
***************************1.row***************************
id:1
firstname:Han
surname:Solo
email:han@tatooine.com
type:premium
***************************2.row***************************
id:2
firstname:James
surname:Kirk
email:enter@prise
type:basic
2rowsinset(0.00sec)
Youcanretrievemoreinformationthanjustfields.Forexample,youcanuseCOUNTtoretrievetheamountofrowsthatsatisfythegivenconditionsinsteadofretrievingallthe
columns.Thiswayisfasterthanretrievingallthecolumnsandthencountingthembecauseyousavetimeinreducingthesizeoftheresponse.Let’sconsiderhowitwouldlook:
mysql>SELECTCOUNT(*)FROMborrowed_books
->WHEREcustomer_id=1ANDendISNOTNULL;
+----------+
|COUNT(*)|
+----------+
|1|
+----------+
1rowinset(0.00sec)
Asyoucannote,theresponsesays1,whichmeansthatthereisonlyoneborrowedbookthatsatisfiestheconditions.However,checktheconditions;youwillnotethatweusedanotherfamiliarlogicaloperator:NOT.NOTnegatestheexpression,as!doesinPHP.Notealsothatwedonotusetheequalsigntocomparewithnullvalues.InMySQL,youhavetouseISinsteadoftheequalssigninordertocomparewithNULL.So,thesecondconditionwouldbesatisfiedwhenaborrowedbookhasanenddatethatisnotnull.
Let’sfinishthissectionbyaddingtwomorefeatureswhenqueryingdata.Thefirstoneistheabilitytospecifyinwhatordertherowsshouldbereturned.Todothis,justusethekeywordORDERBYfollowedbythenameofthefieldthatyouwanttoorderby.Youcouldalsospecifywhetheryouwanttoorderinascendingmode,whichisbydefault,orinthedescendingmode,whichcanbedonebyappendingDESC.TheotherfeatureistheabilitytolimittheamountofrowstoreturnusingLIMITandtheamountofrowstoretrieve.Now,runthefollowing:
mysql>SELECTid,title,author,isbnFROMbook
->ORDERBYtitleLIMIT4;
+----+-------------+-----------------+---------------+
|id|title|author|isbn|
+----+-------------+-----------------+---------------+
|5|19minutes|JodiPicoult|9780753179246|
|1|1984|GeorgeOrwell|9780882339726|
|2|1Q84|HarukiMurakami|9789724621081|
|3|AnimalFarm|GeorgeOrwell|9780736692427|
+----+-------------+-----------------+---------------+
4rowsinset(0.00sec)
UsingPDOSofar,wehaveworkedwithMySQL,andyoualreadyhaveagoodideaofwhatyoucandowithit.However,connectingtotheclientandperformingqueriesmanuallyisnotourgoal.Whatwewanttoachieveisthatourapplicationcantakeadvantageofthedatabaseinanautomaticway.Inordertodothis,wewilluseasetofclassesthatcomeswithPHPandallowsyoutoconnecttothedatabaseandperformqueriesfromthecode.
PHPDataObjects(PDO)istheclassthatconnectstothedatabaseandallowsyoutointeractwithit.ThisisthepopularwaytoworkwithdatabasesforPHPdevelopers,eventhoughthereareotherwaysthatwewillnotdiscusshere.PDOallowsyoutoworkwithdifferentdatabasesystems,soyouarenottiedtoMySQLonly.Inthefollowingsections,wewillconsiderhowtoconnecttoadatabase,insertdata,andretrieveitusingthisclass.
ConnectingtothedatabaseInordertoconnecttothedatabase,itisgoodpracticetokeepthecredentials—thatis,theuserandpassword—separatedfromthecodeinaconfigurationfile.Wealreadyhavethisfileasconfig/app.jsonfromwhenweworkedwiththeConfigclass.Let’saddthecorrectcredentialsforourdatabase.Ifyouhavetheconfigurationbydefault,theconfigurationfileshouldlooksimilartothis:
{
"db":{
"user":"root",
"password":""
}
}
Developersusuallyspecifyotherinformationrelatedtotheconnection,suchasthehost,port,ornameofthedatabase.Thiswilldependonhowyourapplicationisinstalled,whetherMySQLisrunningonadifferentserver,andsoon,anditisuptoyouhowmuchinformationyouwanttokeeponyourcodeandinyourconfigurationfiles.
Inordertoconnecttothedatabase,weneedtoinstantiateanobjectfromthePDOclass.Theconstructorofthisclassexpectsthreearguments:DataSourceName(DSN),whichisastringthatrepresentsthetypeofdatabasetouse;thenameoftheuser;andthepassword.WealreadyhavetheusernameandpasswordfromtheConfigclass,butwestillneedtobuildDSN.
OneoftheformatsforMySQLdatabasesis<databasetype>:host=<host>;dbname=<schemaname>.AsourdatabasesystemisMySQL,itrunsonthesameserver,andtheschemanameisbookstore,DSNwillbemysql:host=127.0.0.1;dbname=bookstore.Let’stakealookathowwewillputeverythingtogether:
$dbConfig=Config::getInstance()->get('db');
$db=newPDO(
'mysql:host=127.0.0.1;dbname=bookstore',
$dbConfig['user'],
$dbConfig['password']
);
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE,PDO::FETCH_ASSOC);
NotealsothatwewillinvokethesetAttributemethodfromthePDOinstance.Thismethodallowsyoutosetsomeoptionstotheconnection;inthiscase,itsetstheformatoftheresultscomingfromMySQL.ThisoptionforcesMySQLtoreturnthearrayswhosekeysarethenamesofthefields,whichiswaymoreusefulthanthedefaultone,returningnumerickeysbasedontheorderofthefields.Settingthisoptionnowwillaffectallthequeriesperformedwiththe$dbinstance,ratherthansettingtheoptioneachtimeweperformaquery.
PerformingqueriesTheeasiestwaytoretrievedatafromyourdatabaseistousethequerymethod.Thismethodacceptsthequeryasastringandreturnsalistofrowsasarrays.Let’sconsideranexample:writethefollowingaftertheinitializationofthedatabaseconnection—forexample,intheinit.phpfile:
$rows=$db->query('SELECT*FROMbookORDERBYtitle');
foreach($rowsas$row){
var_dump($row);
}
Thisquerytriestogetallthebooksinthedatabase,orderingthembythetitle.ThiscouldbethecontentofafunctionsuchasgetAllBooks,whichisusedwhenwedisplayourcatalog.Eachrowisanarraythatcontainsallthefieldsaskeysandthedataasvalues.
Ifyouruntheapplicationonyourbrowser,youwillgetthefollowingresult:
Thequeryfunctionisusefulwhenwewanttoretrievedata,butinordertoexecutequeriesthatinsertrows,PDOprovidestheexecfunction.Thisfunctionalsoexpectsthefirst
parameterasastring,definingthequerytoexecute,butitreturnsaBooleanspecifyingwhethertheexecutionwassuccessfulornot.Agoodexamplewouldbetotrytoinsertbooks.Typethefollowing:
$query=<<<SQL
INSERTINTObook(isbn,title,author,price)
VALUES("9788187981954","PeterPan","J.M.Barrie",2.34)
SQL;
$result=$db->exec($query);
var_dump($result);//true
Thiscodealsousesanewwayofrepresentingstrings:heredoc.Wewillenclosethestringbetween<<<SQLandSQL;,bothindifferentlines,insteadofquotes.Thebenefitofthisistheabilitytowritestringsinmultiplelineswithtabulationsoranyotherblankspace,andPHPwillrespectit.Wecanconstructqueriesthatareeasytoreadratherthanwritingthemonasinglelineorhavingtoconcatenatethedifferentstrings.NotethatSQLisatokentorepresentthestartandendofthestring,butyoucoulduseanytextthatyouconsider.
Thefirsttimeyouruntheapplicationwiththiscode,thequerywillbeexecutedsuccessfully,andthus,theresultwillbetheBooleantrue.However,ifyourunitagain,itwillreturnfalseastheISBNthatweinsertedisthesamebutwesetitsrestrictiontobeunique.
Itisusefultoknowthataqueryfailed,butitisbetterifweknowwhy.ThePDOinstancehastheerrorInfomethodthatreturnsanarraywiththeinformationofthelasterror.Thekey2containsthedescription,soitisprobablytheonethatwewillusemoreoften.Updatethepreviouscodewiththefollowing:
$query=<<<SQL
INSERTINTObook(isbn,title,author,price)
VALUES("9788187981954","PeterPan","J.M.Barrie",2.34)
SQL;
$result=$db->exec($query);
var_dump($result);//false
$error=$db->errorInfo()[2];
var_dump($error);//Duplicateentry'9788187981954'forkey'isbn'
TheresultisthatthequeryfailedbecausetheISBNentrywasduplicated.Now,wecanbuildmoremeaningfulerrormessagesforourcustomersorjustfordebuggingpurposes.
PreparedstatementsTheprevioustwofunctionsareveryusefulwhenyouneedtorunquickqueriesthatarealwaysthesame.However,inthesecondexampleyoumightnotethatthestringofthequeryisnotveryusefulasitalwaysinsertsthesamebook.Althoughitistruethatyoucouldjustreplacethevaluesbyvariables,itisnotgoodpracticeasthesevariablesusuallycomefromtheusersideandcancontainmaliciouscode.Itisalwaysbettertofirstsanitizethesevalues.
PDOprovidestheabilitytoprepareastatement—thatis,aquerythatisparameterized.Youcanspecifyparametersforthefieldsthatwillchangeinthequeryandthenassignvaluestotheseparameters.Let’sconsiderfirstanexample,asfollows:
$query='SELECT*FROMbookWHEREauthor=:author';
$statement=$db->prepare($query);
$statement->bindValue('author','GeorgeOrwell');
$statement->execute();
$rows=$statement->fetchAll();
var_dump($rows);
Thequeryisanormaloneexceptthatithas:authorinsteadofthestringoftheauthorthatwewanttofind.Thisisaparameter,andwewillidentifythemusingtheprefix:.ThepreparemethodgetsthequeryasanargumentandreturnsaPDOStatementinstance.Thisclasscontainsseveralmethodstobindvalues,executestatements,fetchresults,andmore.Inthispieceofcode,weuseonlythreeofthem,asfollows:
bindValue:Thistakestwoarguments:thenameoftheparameterasdescribedinthequeryandthevaluetoassign.Ifyouprovideaparameternamethatisnotinthequery,thiswillthrowanexception.execute:ThiswillsendthequerytoMySQLwiththereplacementoftheparametersbytheprovidedvalues.Ifthereisanyparameterthatisnotassignedtoavalue,themethodwillthrowanexception.Asitsbrotherexec,executewillreturnaBoolean,specifyingwhetherthequerywasexecutedsuccessfullyornot.fetchAll:ThiswillretrievethedatafromMySQLincaseitwasaSELECTquery.Asaquery,fetchAllwillreturnalistofallrowsasarrays.
Ifyoutrythiscode,youwillnotethattheresultisverysimilartowhenusingquery;however,thistime,thecodeismuchmoredynamicasyoucanreuseitforanyauthorthatyouneed.
ThereisanotherwaytobindvaluestoparametersofaquerythanusingthebindValuemethod.Youcouldprepareanarraywherethekeyisthenameoftheparameterandthevalueisthevalueyouwanttoassigntoit,andthenyoucansenditasthefirstargumentoftheexecutemethod.ThiswayisquiteusefulasusuallyyoualreadyhavethisarraypreparedanddonotneedtocallbindValueseveraltimeswithitscontent.Addthiscodeinordertotestit:
$query=<<<SQL
INSERTINTObook(isbn,title,author,price)
VALUES(:isbn,:title,:author,:price)
SQL;
$statement=$db->prepare($query);
$params=[
'isbn'=>'9781412108614',
'title'=>'Iliad',
'author'=>'Homer',
'price'=>9.25
];
$statement->execute($params);
echo$db->lastInsertId();//8
Inthislastexample,wecreatedanewbookwithalmostalltheparameters,butwedidnotspecifytheID,whichisthedesiredbehavioraswewantMySQLtochooseavalidoneforus.However,whathappensifyouwanttoknowtheIDoftheinsertedrow?Well,youcouldqueryMySQLforthebookwiththesameISBNandthereturnedrowwouldcontaintheID,butthisseemslikealotofwork.Instead,PDOhasthelastInsertIdmethod,whichreturnsthelastIDinsertedbyaprimarykey,savingusfromoneextraquery.
JoiningtablesEventhoughqueryingMySQLisquitefast,especiallyifitisinthesameserverasourPHPapplication,weshouldtrytoreducethenumberofqueriesthatwewillexecutetoimprovetheperformanceofourapplication.Sofar,wehavequerieddatafromjustonetable,butthisisrarelythecase.Imaginethatyouwanttoretrieveinformationaboutborrowedbooks:thetablecontainsonlyIDsanddates,soifyouqueryit,youwillnotgetverymeaningfuldata,right?Oneapproachwouldbetoquerythedatainborrowed_books,andbasedonthereturningIDs,querythebookandcustomertablesbyfilteringbytheIDsweareinterestedin.However,thisapproachconsistsofatleastthreequeriestoMySQLandalotofworkwitharraysinPHP.Itseemsasthoughthereshouldbeabetteroption!
InSQL,youcanexecutejoinqueries.Ajoinqueryisaquerythatjoinstwoormoretablesthroughacommonfieldand,thus,allowsyoutoretrievedatafromthesetables,reducingtheamountofqueriesneeded.Ofcourse,theperformanceofajoinqueryisnotasgoodastheperformanceofanormalquery,butifyouhavethecorrectkeysandrelationshipsdefined,thisoptioniswaybetterthanqueryingseparately.
Inordertojointables,youneedtolinkthemusingacommonfield.Foreignkeysareveryusefulinthismatterasyouknowthatboththefieldsarethesame.Let’stakealookathowwewouldqueryforalltheimportantinforelatedtotheborrowedbooks:
mysql>SELECTCONCAT(c.firstname,'',c.surname)ASname,
->b.title,
->b.author,
->DATE_FORMAT(bb.start,'%d-%m-%y')ASstart,
->DATE_FORMAT(bb.end,'%d-%m-%y')ASend
->FROMborrowed_booksbb
->LEFTJOINcustomercONbb.customer_id=c.id
->LEFTJOINbookbONb.id=bb.book_id
->WHEREbb.start>="2015-01-01";
+------------+---------+---------------+----------+----------+
|name|title|author|start|end|
+------------+---------+---------------+----------+----------+
|HanSolo|Dracula|BramStoker|10-01-15|13-01-15|
|JamesKirk|Dracula|BramStoker|01-02-15|10-02-15|
|JamesKirk|1984|GeorgeOrwell|12-03-15|NULL|
+------------+---------+---------------+----------+----------+
3rowsinset(0.00sec)
Thereareseveralnewconceptsintroducedinthislastquery.Especiallywithjoiningqueries,aswejoinedthefieldsofdifferenttables,itmightoccurthattwotableshavethesamefieldname,andMySQLneedsustodifferentiatethem.Thewaywewilldifferentiatetwofieldsoftwodifferenttablesisbyprependingthenameofthetable.ImaginethatwewanttodifferentiatetheIDofacustomerfromtheIDofthebook;weshouldusethemascustomer.idandbook.id.However,writingthenameofthetableeachtimewouldmakeourqueriesendless.
MySQLhastheabilitytoaddanaliastoatablebyjustwritingnexttothetable’srealname,aswedidinborrowed_books(bb),customer(c)orbook(b).Onceyouaddanalias,
youcanuseittoreferencethistable,allowingustowritethingssuchasbb.customer_idinsteadofborrowed_books.customer_id.Itisalsogoodpracticetowritethetableofthefieldevenifthefieldisnotduplicatedanywhereelseasjoiningtablesmakesitabitconfusingtoknowwhereeachfieldcomesfrom.
Whenjoiningtables,youneedtowritethemintheFROMclauseusingLEFTJOIN,followedbythenameofthetable,anoptionalalias,andthefieldsthatconnectbothtables.Therearedifferentjoiningtypes,butlet’sfocusonthemostusefulforourpurposes.Leftjoinstakeeachrowfromthefirsttable—theoneontheleft-handsideofthedefinition—andsearchfortheequivalentfieldintheright-handsidetable.Onceitfindsit,itwillconcatenatebothrowsasiftheywereone.Forexample,whenjoiningborrowed_bookswithcustomerforeachborrowed_booksrow,MySQLwillsearchforanIDincustomerthatmatchesthecurrentcustomer_id,andthenitwilladdalltheinformationofthisrowinourcurrentrowinborrowed_booksasiftheywereonlyonebigtable.Ascustomer_idisaforeignkey,wearecertainthattherewillalwaysbeacustomertomatch.
Youcanjoinseveraltables,andMySQLwilljustresolvethemfromlefttoright;thatis,itwillfirstjointhetwofirsttablesasone,thentrytojointhisresultingonewiththethirdtable,andsoon.Thisis,infact,whatwedidinourexample:wefirstjoinedborrowed_bookswithcustomerandthenjoinedthesetwowithbook.
Asyoucannote,therearealsoaliasesforfields.Sometimes,wedomorethanjustgettingafield;anexamplewaswhenwegothowmanyrowsaquerymatchedwithCOUNT(*).However,thetitleofthecolumnwhenretrievingthisinformationwasalsoCOUNT(*),whichisnotalwaysuseful.Atothertimes,weusedtwotableswithcollidingfieldnames,anditmakeseverythingconfusing.Whenthishappens,justaddanaliastothefieldinthesamewaywedidwithtablenames;ASisoptional,butithelpstounderstandwhatyouaredoing.
Let’smovenowtotheusageofdatesinthisquery.Ononehand,wewilluseDATE_FORMATforthefirsttime.Itacceptsthedate/time/datetimevalueandthestringwiththeformat.Inthiscase,weused%d-%m-%y,whichmeansday-month-year,butwecoulduse%h-%i-%stospecifyhours-minutes-secondsoranyothercombination.
NotealsohowwecompareddatesintheWHEREclause.Giventwodatesortimevaluesofthesametype,youcanusethecomparisonoperatorsasiftheywerenumbers.Inthiscase,wewilldobb.start>="2015-01-01",whichwillgiveustheborrowedbooksfromJanuary1,2015,onward.
ThefinalthingtonoteaboutthiscomplexqueryistheuseoftheCONCATfunction.Insteadofreturningtwofields,oneforthenameandoneforthesurname,wewanttogetthefullname.Todothis,wewillconcatenatethefieldsusingthisfunction,sendingasmanystringsaswewantasargumentsofthefunctionandgettingbacktheconcatenatedstring.Asyoucansee,youcansendbothfieldsandstringsenclosedbysinglequotes.
Well,ifyoufullyunderstoodthisquery,youshouldfeelsatisfiedwithyourself;thiswasthemostcomplexquerywewillseeinthischapter.Wehopeyoucangetasenseofhowpowerfuladatabasesystemcanbeandthatfromnowon,youwilltrytoprocessthedata
GroupingqueriesThelastfeaturethatwewilldiscussaboutqueryingistheGROUPBYclause.Thisclauseallowsyoutogrouprowsofthesametablewithacommonfield.Forexample,let’ssaywewanttoknowhowmanybookseachauthorhasinjustonequery.Trythefollowing:
mysql>SELECT
->author,
->COUNT(*)ASamount,
->GROUP_CONCAT(titleSEPARATOR',')AStitles
->FROMbook
->GROUPBYauthor
->ORDERBYamountDESC,author;
+-----------------+--------+-------------------+
|author|amount|titles|
+-----------------+--------+-------------------+
|GeorgeOrwell|2|1984,AnimalFarm|
|Homer|2|Odyssey,Iliad|
|BramStoker|1|Dracula|
|HarukiMurakami|1|1Q84|
|J.M.Barrie|1|PeterPan|
|JodiPicoult|1|19minutes|
+-----------------+--------+-------------------+
5rowsinset(0.00sec)
TheGROUPBYclause,alwaysaftertheWHEREclause,getsafield—ormany,separatedbyacoma—andtreatsalltherowswiththesamevalueforthisfield,asthoughtheywerejustone.Thus,selectingbyauthorwillgroupalltherowsthatcontainthesameauthor.Thefeaturemightnotseemveryuseful,butthereareseveralfunctionsinMySQLthattakeadvantageofit.Inthisexample:
COUNT(*)isusedinquerieswithGROUPBYandshowshowmanyrowsthisfieldgroups.Inthiscase,wewilluseittoknowhowmanybookseachauthorhas.Infact,italwaysworkslikethis;however,forquerieswithoutGROUPBY,MySQLtreatsthewholesetofrowsasonegroup.GROUP_CONCATissimilartoCONCAT,whichwediscussedearlier.Theonlydifferenceisthatthistimethefunctionwillconcatenatethefieldsofalltherowsofagroup.IfyoudonotspecifySEPARATOR,MySQLwilluseasinglecoma.However,inourcase,weneededacomaandaspacetomakeitreadable,soweaddedSEPARATOR','attheend.NotethatyoucanaddasmanythingstoconcatenateasyouneedinCONCAT,theseparatorwilljustseparatetheconcatenationsbyrows.
Eventhoughitisnotaboutgrouping,notetheORDERclausethatweadded.Weorderedbytwofieldsinsteadofone.ThismeansthatMySQLwillorderalltherowsbytheamountfield;notethatthisisanalias,butyoucanuseithereaswell.Then,MySQLwillordereachgroupofrowswiththesameamountvaluebythetitlefield.
ThereisonelastthingtorememberaswealreadypresentedalltheimportantclausesthataSELECTquerycancontain:MySQLexpectstheclausesofthequerytobealwaysinthesameorder.Ifyouwritethesamequerybutchangethisorder,youwillgetanerror.The
UpdatinganddeletingdataWealreadyknowquitealotaboutinsertingandretrievingdata,butifapplicationscouldonlydothis,theywouldbequitestatic.Editingthisdataasweneediswhatmakesanapplicationdynamicandwhatgivestotheusersomevalue.InMySQL,andinmostdatabasesystems,youhavetwocommandstochangedata:UPDATEandDELETE.Let’sdiscussthemindetail.
UpdatingdataWhenupdatingdatainMySQL,themostimportantthingistohaveauniquereferenceoftherowthatyouwanttoupdate.Forthis,primarykeysareveryuseful;however,ifyouhaveatablewithnoprimarykeys,whichshouldnotbethecasemostofthetime,youcanstillupdatetherowsbasedonotherfields.Otherthanthereference,youwillneedthenewvalueand,ofcourse,thetablenameandfieldtoupdate.Let’stakealookataverysimpleexample:
mysql>UPDATEbookSETprice=12.75WHEREid=2;
QueryOK,1rowaffected(0.00sec)
Rowsmatched:1Changed:1Warnings:0
InthisUPDATEquery,wesetthepriceofthebookwiththeID2to12.75.TheSETclausedoesnotneedtospecifyonlyonechange;youcanspecifyseveralchangesonthesamerowassoonasyouseparatethembycommas—forexample,SETprice=12.75,stock=14.Also,notetheWHEREclause,inwhichwespecifywhichrowswewanttochange.MySQLgetsalltherowsofthistablebasedontheseconditionsasthoughitwereaSELECTqueryandapplythechangetothissetofrows.
WhatMySQLwillreturnisveryimportant:thenumberofrowsmatchedandthenumberofrowschanged.ThefirstoneisthenumberofrowsthatmatchtheconditionsintheWHEREclause.Thesecondonespecifiestheamountofrowsthatcanbechanged.Therearedifferentreasonsnottochangearow—forexamplewhentherowalreadyhasthesamevalue.Toseethis,let’srunthesamequeryagain:
mysql>UPDATEbookSETprice=12.75WHEREid=2;
QueryOK,0rowsaffected(0.00sec)
Rowsmatched:1Changed:0Warnings:0
Thesamerownowsaysthattherewas1rowmatched,asexpected,but0rowswerechanged.Thereasonisthatwealreadysetthepriceofthisbookto12.75,soMySQLdoesnotneedtodoanythingaboutthisnow.
Asmentionedbefore,theWHEREclauseisthemostimportantbitinthisquery.Waytoomanytimes,wefinddevelopersthatrunaprioriinnocentUPDATEqueriesendupchangingthewholetablebecausetheymisstheWHEREclause;thus,MySQLmatchesthewholetableasvalidrowstoupdate.Thisisusuallynottheintentionofthedeveloper,anditissomethingnotverypleasant,sotrytomakesureyoualwaysprovideavalidsetofconditions.ItisgoodpracticetofirstwritedowntheSELECTquerythatreturnstherowsyouneedtoedit,andonceyouaresurethattheconditionsmatchthedesiredsetofrows,youcanwritetheUPDATEquery.
However,sometimes,affectingmultiplerowsistheintendedscenario.Imaginethatwearegoingthroughtoughtimesandneedtoincreasethepriceofallourbooks.Wedecidethatwewanttoincreasethepriceby16%,whichisthesameasthecurrentpricetimes1.16.Wecanrunthefollowingquerytoperformthesechanges:
mysql>UPDATEbookSETprice=price*1.16;
QueryOK,8rowsaffected(0.00sec)
Rowsmatched:8Changed:8Warnings:0
ThisquerydoesnotcontainanyWHEREclauseaswewanttomatchallourbooks.AlsonotethattheSETclauseusesthepricefieldtogetthecurrentvaluefortheprice,whichisperfectlyvalid.Finally,notethenumberofrowsmatchedandchanged,whichis8—thewholesetofrowsforthistable.
Tofinishwiththissubsection,let’sconsiderhowwecanuseUPDATEqueriesfromPHPthroughPDO.Oneverycommonscenarioiswhenwewanttoaddcopiesofthealreadyexistingbookstoourinventory.GivenabookIDandanoptionalamountofbooks—bydefault,thisvaluewillbe1—wewillincreasethestockvalueofthisbookbythesemanycopies.Writethisfunctioninyourinit.phpfile:
functionaddBook(int$id,int$amount=1):void{
$db=newPDO(
'mysql:host=127.0.0.1;dbname=bookstore',
'root',
''
);
$query='UPDATEbookSETstock=stock+:nWHEREid=:id';
$statement=$db->prepare($query);
$statement->bindValue('id',$id);
$statement->bindValue('n',$amount);
if(!$statement->execute()){
thrownewException($statement->errorInfo()[2]);
}
}
Therearetwoarguments:$idand$amount.Thefirstonewillalwaysbemandatory,whereasthesecondonecanbeomitted,andthedefaultvaluewillbe1.Thefunctionfirstpreparesaquerysimilartothefirstoneofthissection,inwhichweincreasedtheamountofstockofagivenbook,thenbindsbothparameterstothestatement,andfinallyexecutesthequery.Ifsomethinghappensandexecutereturnsfalse,wewillthrowanexceptionwiththecontentoftheerrormessagefromMySQL.
Thisfunctionisveryusefulwhenweeitherbuymorestockoracustomerreturnsabook.Wecouldevenuseittoremovebooksbyprovidinganegativevalueto$amount,butthisisverybadpractice.Thereasonisthatevenifweforcedthestockfieldtobeunsigned,settingittoanegativevaluewillnottriggeranyerror,onlyawarning.MySQLwillnotsettherowtoanegativevalue,buttheexecuteinvocationwillreturntrue,andwewillnotknowaboutit.Itisbettertojustcreateasecondmethod,removeBook,andverifyfirstthattheamountofbookstoremoveislowerthanorequaltothecurrentstock.
ForeignkeybehaviorsOnetrickythingtomanagewhenupdatingordeletingrowsiswhentherowthatweupdateispartofaforeignkeysomewhereelse.Forexample,ourborrowed_bookstablecontainstheIDsofcustomersandbooks,andasyoualreadyknow,MySQLenforcesthattheseIDsarealwaysvalidandexistontheserespectivetables.Whatwouldhappen,then,ifwechangedtheIDofthebookitselfonthebooktable?Orevenworse,whatwouldhappenifweremovedoneofthebooksfrombook,andthereisarowinborrowed_booksthatreferencesthisID?
MySQLallowsyoutosetthedesiredreactionwhenoneofthesescenariostakesplace.Ithastobedefinedwhenaddingtheforeignkey;so,inourcase,wewillneedtofirstremovetheexistingonesandthenaddthemagain.Toremoveordropakey,youneedtoknowthenameofthiskey,whichwecanfindusingtheSHOWCREATETABLEcommand,asfollows:
mysql>SHOWCREATETABLEborrowed_books\G
***************************1.row***************************
Table:borrowed_books
CreateTable:CREATETABLE`borrowed_books`(
`book_id`int(10)unsignedNOTNULL,
`customer_id`int(10)unsignedNOTNULL,
`start`datetimeNOTNULL,
`end`datetimeDEFAULTNULL,
KEY`book_id`(`book_id`),
KEY`customer_id`(`customer_id`),
CONSTRAINT`borrowed_books_ibfk_1`FOREIGNKEY(`book_id`)REFERENCES
`book`(`id`),
CONSTRAINT`borrowed_books_ibfk_2`FOREIGNKEY(`customer_id`)REFERENCES
`customer`(`id`)
)ENGINE=InnoDBDEFAULTCHARSET=latin1
1rowinset(0.00sec)
Thetwoforeignkeysthatwewanttoremoveareborrowed_books_ibfk_1andborrowed_books_ibfk_2.Let’sremovethemusingtheALTERTABLEcommand,aswedidbefore:
mysql>ALTERTABLEborrowed_books
->DROPFOREIGNKEYborrowed_books_ibfk_1;
QueryOK,4rowsaffected(0.02sec)
Records:4Duplicates:0Warnings:0
mysql>ALTERTABLEborrowed_books
->DROPFOREIGNKEYborrowed_books_ibfk_2;
QueryOK,4rowsaffected(0.01sec)
Records:4Duplicates:0Warnings:0
Now,weneedtoaddtheforeignkeysagain.Theformatofthecommandwillbethesameaswhenweaddedthem,butappendingthenewdesiredbehavior.Inourcase,ifweremoveacustomerorbookfromourtables,wewanttoremovetherowsreferencingthesebooksandcustomersfromborrowed_books;so,weneedtousetheCASCADEoption.Let’sconsiderwhattheywouldlooklike:
mysql>ALTERTABLEborrowed_books
->ADDFOREIGNKEY(book_id)REFERENCESbook(id)
->ONDELETECASCADEONUPDATECASCADE,
->ADDFOREIGNKEY(customer_id)REFERENCEScustomer(id)
->ONDELETECASCADEONUPDATECASCADE;
QueryOK,4rowsaffected(0.01sec)
Records:4Duplicates:0Warnings:0
NotethatwecandefinetheCASCADEbehaviorforbothactions:whenupdatingandwhendeletingrows.ThereareotheroptionsinsteadofCASCADE—forexampleSETNULL,whichsetstheforeignkeyscolumnstoNULLandallowstheoriginalrowtobedeleted,orthedefaultone,RESTRICT,whichrejectstheupdate/deletecommands.
DeletingdataDeletingdataisalmostthesameasupdatingit.YouneedtoprovideaWHEREclausethatwillmatchtherowsthatyouwanttodelete.Also,aswithwhenupdatingdata,itishighlyrecommendedtofirstbuildtheSELECTquerythatwillretrievetherowsthatyouwanttodeletebeforeperformingtheDELETEcommand.Donotthinkthatyouarewastingtimewiththismethodology;asthesayinggoes,measuretwice,cutonce.Notalwaysisitpossibletorecoverdataafterdeletingrows!
Let’strytodeleteabookbyobservinghowtheCASCADEoptionwesetearlierbehaves.Forthis,let’sfirstqueryfortheexistingborrowedbookslistviathefollowing:
mysql>SELECTbook_id,customer_idFROMborrowed_books;
+---------+-------------+
|book_id|customer_id|
+---------+-------------+
|1|1|
|4|1|
|4|2|
|1|2|
+---------+-------------+
4rowsinset(0.00sec)
Therearetwodifferentbooks,1and4,witheachofthemborrowedtwice.Let’strytodeletethebookwiththeID4.First,buildaquerysuchasSELECT*FROMbookWHEREid=4tomakesurethattheconditionintheWHEREclauseistheappropriateone.Onceyouaresure,performthefollowingquery:
mysql>DELETEFROMbookWHEREid=4;
QueryOK,1rowaffected(0.02sec)
Asyoucannote,weonlyspecifiedtheDELETEFROMcommandfollowedbythenameofthetableandtheWHEREclause.MySQLtellsusthattherewas1rowaffected,whichmakessense,giventhepreviousSELECTstatementwemade.
Ifwegobacktoourborrowed_bookstableandqueryfortheexistingones,wewillnotethatalltherowsreferencingthebookwiththeID4aregone.Thisisbecausewhendeletingthemfromthebooktable,MySQLnoticedtheforeignkeyreference,checkedwhatitneededtodowhiledeleting—inthiscase,CASCADE—anddeletedalsotherowsinborrowed_books.Takealookatthefollowing:
mysql>SELECTbook_id,customer_idFROMborrowed_books;
+---------+-------------+
|book_id|customer_id|
+---------+-------------+
|1|1|
|1|2|
+---------+-------------+
2rowsinset(0.00sec)
WorkingwithtransactionsIntheprevioussection,wereiteratedhowimportantitistomakesurethatanupdateordeletequerycontainthedesirablematchingsetofrows.Eventhoughthiswillalwaysapply,thereisawaytorevertthechangesthatyoujustmade,whichisworkingwithtransactions.
AtransactionisastatewhereMySQLkeepstrackofallthechangesthatyoumakeinyourdatainordertobeabletorevertallofthemifneeded.Youneedtoexplicitlystartatransaction,andbeforeyouclosetheconnectiontotheserver,youneedtocommityourchanges.ThismeansthatMySQLdoesnotreallyperformthesechangesuntilyoutellittodoso.Ifduringatransactionyouwanttorevertthechanges,youshouldrollbackinsteadofmakingacommit.
PDOallowsyoutodothiswiththreefunctions:
beginTransaction:Thiswillstartthetransaction.commit:Thiswillcommityourchanges.KeepinmindthatifyoudonotcommitandthePHPscriptfinishesoryouclosetheconnectionexplicitly,MySQLwillrejectallthechangesyoumadeduringthistransaction.rollBack:Thiswillrollbackallthechangesthatweremadeduringthistransaction.
Onepossibleuseoftransactionsinyourapplicationiswhenyouneedtoperformmultiplequeriesandallofthemhavetobesuccessfulandthewholesetofqueriesshouldnotbeperformedotherwise.Thiswouldbethecasewhenaddingasaleintothedatabase.Rememberthatoursalesarestoredintwotables:oneforthesaleitselfandoneforthelistofbooksrelatedtothissale.Whenaddinganewone,youneedtomakesurethatallthebooksareaddedtothisdatabase;otherwise,thesalewillbecorrupted.Whatyoushoulddoisexecuteallthequeries,checkingfortheirreturningvalues.Ifanyofthemreturnsfalse,thewholesaleshouldberolledback.
Let’screateanaddSalefunctioninyourinit.phpfileinordertoemulatethisbehavior.Thecontentshouldbeasfollows:
functionaddSale(int$userId,array$bookIds):void{
$db=newPDO(
'mysql:host=127.0.0.1;dbname=bookstore',
'root',
''
);
$db->beginTransaction();
try{
$query='INSERTINTOsale(customer_id,date)'
.'VALUES(:id,NOW())';
$statement=$db->prepare($query);
if(!$statement->execute(['id'=>$userId])){
thrownewException($statement->errorInfo()[2]);
}
$saleId=$db->lastInsertId();
$query='INSERTINTOsale_book(book_id,sale_id)'
.'VALUES(:book,:sale)';
$statement=$db->prepare($query);
$statement->bindValue('sale',$saleId);
foreach($bookIdsas$bookId){
$statement->bindValue('book',$bookId);
if(!$statement->execute()){
thrownewException($statement->errorInfo()[2]);
}
}
$db->commit();
}catch(Exception$e){
$db->rollBack();
throw$e;
}
}
Thisfunctionisquitecomplex.ItgetsasargumentstheIDofthecustomerandthelistofbooksasweassumethatthedateofthesaleisthecurrentdate.Thefirstthingwewilldoisconnecttothedatabase,instantiatingthePDOclass.Rightafterthis,wewillbeginourtransaction,whichwilllastonlyduringthecourseofthisfunction.Oncewebeginthetransaction,wewillopenatry…catchblockthatwillenclosetherestofthecodeofthefunction.Thereasonisthatifwethrowanexception,thecatchblockwillcaptureit,rollingbackthetransactionandpropagatingtheexception.Thecodeinsidethetryblockjustaddsfirstthesaleandtheniteratesthelistofbooks,insertingthemintothedatabasetoo.Atalltimes,wewillchecktheresponseoftheexecutefunction,andifit’sfalse,wewillthrowanexceptionwiththeinformationoftheerror.
Let’strytousethisfunction.Writethefollowingcodethattriestoaddasaleforthreebooks;however,oneofthemdoesnotexist,whichistheonewiththeID200:
try{
addSale(1,[1,2,200]);
}catch(Exception$e){
echo'Erroraddingsale:'.$e->getMessage();
}
Thiscodewillechotheerrormessage,complainingaboutthenonexistentbook.IfyoucheckinMySQL,therewillbenorowsinthesalestableasthefunctionrolledbackwhentheexceptionwasthrown.
Finally,let’strythefollowingcodeinstead.Thisonewilladdthreevalidbookssothatthequeriesarealwayssuccessfulandthetryblockcangountiltheend,wherewewillcommitthechanges:
try{
addSale(1,[1,2,3]);
}catch(Exception$e){
echo'Erroraddingsale:'.$e->getMessage();
}
Testit,andyouwillseehowthereisnomessageprintedonyourbrowser.Then,goto
SummaryInthischapter,welearnedtheimportanceofdatabasesandhowtousethemfromourwebapplication:fromsettinguptheconnectionusingPDOandcreatingandfetchingdataondemandtoconstructingmorecomplexqueriesthatfulfillourneeds.Withallofthis,ourapplicationlookswaymoreusefulnowthanwhenitwascompletelystatic.
Inthenextchapter,wewilldiscoverhowtoapplythemostimportantdesignpatternsforwebapplicationsthroughModelViewController(MVC).Youwillgainasenseofclarityinyourcodewhenyouorganizeyourapplicationinthisway.
Chapter6.AdaptingtoMVCWebapplicationsaremorecomplexthanwhatwehavebuiltsofar.Themorefunctionalityyouadd,themoredifficultthecodeistomaintainandunderstand.Itisforthisreasonthatstructuringyourcodeinanorganizedwayiscrucial.Youcoulddesignyourownstructure,butaswithOOP,therealreadyexistsomedesignpatternsthattrytosolvethisproblem.
MVC(model-view-controller)hasbeenthefavoritepatternforwebdevelopers.Ithelpsusseparatethedifferentpartsofawebapplication,leavingthecodeeasytounderstandevenforbeginners.WewilltrytorefactorourbookstoreexampletousetheMVCpattern,andyouwillrealizehowquicklyyoucanaddnewfunctionalityafterthat.
Inthischapter,youwilllearnthefollowing:
UsingComposertomanagedependenciesDesigningarouterforyourapplicationOrganizingyourcodeintomodels,views,andcontrollersTwigasthetemplateengineDependencyinjection
TheMVCpatternSofar,eachtimewehavehadtoaddafeature,weaddedanewPHPfilewithamixtureofPHPandHTMLforthatspecificpage.Forchunksofcodewithasinglepurpose,andwhichwehavetoreuse,wecreatedfunctionsandaddedthemtothefunctionsfile.Evenforverysmallwebapplicationslikeours,thecodestartsbecomingveryconfusing,andtheabilitytoreusecodeisnotashelpfulasitcouldbe.Nowimagineanapplicationwithalargenumberoffeatures:thatwouldbeprettymuchchaositself.
Theproblemsdonotstophere.Inourcode,wehavemixedHTMLandPHPcodeinasinglefile.Thatwillgiveusalotoftroublewhentryingtochangethedesignofthewebapplication,orevenifwewanttoperformaverysmallchangeacrossallpages,suchaschangingthemenuorfooterofthepage.Themorecomplextheapplication,themoreproblemswewillencounter.
MVCcameupasapatterntohelpusdividethedifferentpartsoftheapplication.Thesepartsareknownasmodels,views,andcontrollers.Modelsmanagethedataand/orthebusinesslogic,viewscontainthetemplatesforourresponses(forexample,HTMLpages),andcontrollersorchestraterequests,decidingwhatdatatouseandhowtorendertheappropriatetemplate.Wewillgothroughtheminlatersectionsofthischapter.
UsingComposerEventhoughthisisnotanecessarycomponentwhenimplementingtheMVCpattern,ComposerhasbeenanindispensabletoolforanyPHPwebapplicationoverthelastfewyears.Themaingoalofthistoolistohelpyoumanagethedependenciesofyourapplication,thatis,thethird-partylibraries(ofcode)thatweneedtouseinourapplication.Wecanachievethatbyjustcreatingaconfigurationfilethatliststhem,andbyrunningacommandinyourcommandline.
YouneedtoinstallComposeronyourdevelopmentmachine(seeChapter1,SettingUptheEnvironment).Makesurethatyouhaveitbyexecutingthefollowingcommand:
$composer–version
ThisshouldreturntheversionofyourComposerinstallation.Ifitdoesnot,returntotheinstallationsectiontofixtheproblem.
ManagingdependenciesAswestatedearlier,themaingoalofComposeristomanagedependencies.Forexample,we’vealreadyimplementedourconfigurationreader,theConfigclass,butifweknewofsomeonethatimplementedabetterversionofit,wecouldjustusetheirsinsteadofreinventingthewheel;justmakesurethattheyallowyoutodoso!
NoteOpensource
Opensourcereferstothecodethatdeveloperswriteandsharewiththecommunityinordertobeusedbyotherswithoutrestrictions.Thereareactuallydifferenttypesoflicenses,andsomegiveyoumoreflexibilitythanothers,butthebasicideaisthatwecanreusethelibrariesthatotherdevelopershavewritteninourapplications.Thathelpsthecommunitytogrowinknowledge,aswecanlearnwhatothershavedone,improveit,andshareitafterwards.
We’vealreadyimplementedadecentconfigurationreader,butthereareotherelementsofourapplicationthatneedtobedone.Let’stakeadvantageofComposertoreusesomeoneelse’slibraries.Thereareacoupleofwaysofaddingadependencytoourproject:executingacommandinourcommandline,oreditingtheconfigurationfilemanually.AswestilldonothaveComposer’sconfigurationfile,let’susethefirstoption.Executethefollowingcommandintherootdirectoryofyourapplication:
$composerrequiremonolog/monolog
Thiscommandwillshowthefollowingresult:
Usingversion^1.17formonolog/monolog
./composer.jsonhasbeencreated
Loadingcomposerrepositorieswithpackageinformation
Updatingdependencies(includingrequire-dev)
-Installingpsr/log(1.0.0)
Downloading:100%
-Installingmonolog/monolog(1.17.2)
Downloading:100%
...
Writinglockfile
Generatingautoloadfiles
Withthiscommand,weaskedComposertoaddthelibrarymonolog/monologasadependencyofourapplication.Havingexecutedthat,wecannowseesomechangesinourdirectory:
Wehaveanewfilenamedcomposer.json.Thisistheconfigurationfilewherewecanaddourdependencies.Wehaveanewfilenamedcomposer.lock.ThisisafilethatComposerusesinordertotrackthedependenciesthathavealreadybeeninstalledandtheirversions.Wehaveanewdirectorynamedvendor.Thisdirectorycontainsthecodeofthe
dependenciesthatComposerdownloaded.
Theoutputofthecommandalsoshowsussomeextrainformation.Inthiscase,itsaysthatitdownloadedtwolibrariesorpackages,eventhoughweaskedforonlyone.ThereasonisthatthepackagethatweneededalsocontainedotherdependenciesthatwereresolvedbyComposer.AlsonotetheversionthatComposerdownloaded;aswedidnotspecifyanyversion,Composertookthemostrecentoneavailable,butyoucanalwaystrytowritethespecificversionthatyouneed.
Wewillneedanotherlibrary,inthiscasetwig/twig.Let’saddittoourdependencieslistwiththefollowingcommand:
$composerrequiretwig/twig
Thiscommandwillshowthefollowingresult:
Usingversion^1.23fortwig/twig
./composer.jsonhasbeenupdated
Loadingcomposerrepositorieswithpackageinformation
Updatingdependencies(includingrequire-dev)
-Installingtwig/twig(v1.23.1)
Downloading:100%
Writinglockfile
Generatingautoloadfiles
Ifwecheckthecomposer.jsonfile,wewillseethefollowingcontent:
{
"require":{
"monolog/monolog":"^1.17",
"twig/twig":"^1.23"
}
}
ThefileisjustaJSONmapthatcontainstheconfigurationofourapplication;inthiscase,thelistofthetwodependenciesthatweinstalled.Asyoucansee,thedependencies’namefollowsapattern:twowordsseparatedbyaslash.Thefirstofthewordsreferstothevendorthatdevelopedthelibrary.Thesecondofthemisthenameofthelibraryitself.Thedependencyhasaversion,whichcouldbetheexactversionnumber—asinthiscase—oritcouldcontainwildcardcharactersortagnames.Youcanreadmoreaboutthisathttps://getcomposer.org/doc/articles/aliases.md.
Finally,ifyouwouldliketoaddanotherdependency,oreditthecomposer.jsonfileinanyotherway,youshouldruncomposerupdateinyourcommandline,orwhereverthecomposer.jsonfileis,inordertoupdatethedependencies.
AutoloaderwithPSR-4Inthepreviouschapters,wealsoaddedanautoloadertoourapplication.Aswearenowusingsomeoneelse’scode,weneedtoknowhowtoloadtheirclassestoo.Soon,developersrealizedthatthisscenariowithoutastandardwouldbevirtuallyimpossibletomanage,andtheycameoutwithsomestandardsthatmostdevelopersfollow.Youcanfindalotofinformationonthistopicathttp://www.php-fig.org.
Nowadays,PHPhastwomainstandardsforautoloading:PSR-0andPSR-4.Theyareverysimilar,butwewillbeimplementingthelatter,asitisthemostrecentstandardpublished.Thisstandardbasicallyfollowswhatwe’vealreadyintroducedwhentalkingaboutnamespaces:thenamespaceofaclassmustbethesameasthedirectorywhereitis,andthenameoftheclassshouldbethenameofthefile,followedbytheextension.php.Forexample,thefileinsrc/Domain/Book.phpcontainstheclassBookinsidethenamespaceBookstore\Domain.
ApplicationsusingComposershouldfollowoneofthosestandards,andtheyshouldnoteintheirrespectivecomposer.jsonfilewhichonetheyareusing.ThismeansthatComposerknowshowtoautoloaditsownapplicationfiles,sowewillnotneedtotakecareofitwhenwedownloadexternallibraries.Tospecifythat,weeditourcomposer.jsonfile,andaddthefollowingcontent:
{
"require":{
"monolog/monolog":"^1.17",
"twig/twig":"^1.23"
},
"autoload":{
"psr-4":{
"Bookstore\\":"src"
}
}
}
TheprecedingcodemeansthatwewillusePSR-4inourapplication,andthatallthenamespacesthatstartwithBookstoreshouldbefoundinsidethesrc/directory.Thisisexactlywhatourautoloaderwasdoingalready,butreducedtoacoupleoflinesinaconfigurationfile.Wecansafelyremoveourautoloaderandanyreferencetoitnow.
Composergeneratessomemappingsthathelptospeeduptheloadingofclasses.Inordertoupdatethosemapswiththenewinformationaddedtotheconfigurationfile,weneedtorunthecomposerupdatecommandthatweranearlier.Thistime,theoutputwilltellusthatthereisnopackagetoupdate,buttheautoloadfileswillbegeneratedagain:
$composerupdate
Loadingcomposerrepositorieswithpackageinformation
Updatingdependencies(includingrequire-dev)
Nothingtoinstallorupdate
Writinglockfile
Generatingautoloadfiles
AddingmetadataInordertoknowwheretofindthelibrariesthatyoudefineasdependencies,Composerkeepsarepositoryofpackagesandversions,knownasPackagist.Thisrepositorykeepsalotofusefulinformationfordevelopers,suchasalltheversionsavailableforagivenpackage,theauthors,somedescriptionofwhatthepackagedoes(orawebsitepointingtothatinformation),andthedependenciesthatthispackagewilldownload.Youcanalsobrowsethepackages,searchingbynameorcategories.
ButhowdoesPackagistknowaboutthis?Itisallthankstothecomposer.jsonfileitself.Inthere,youcandefineallthemetadataofyourapplicationinaformatthatComposerunderstands.Let’sseeanexample.Addthefollowingcontenttoyourcomposer.jsonfile:
{
"name":"picahielos/bookstore",
"description":"Managesanonlinebookstore.",
"minimum-stability":"stable",
"license":"Apache-2.0",
"type":"project",
"authors":[
{
"name":"AntonioLopez",
"email":"antonio.lopez.zapata@gmail.com"
}
],
//...
}
TheconfigurationfilenowcontainsthenameofthepackagefollowingtheComposerconvention:vendorname,slash,andthepackagename—inthiscase,picahielos/bookstore.Wealsoaddadescription,license,authors,andothermetadata.IfyouhaveyourcodeinapubicrepositorysuchasGitHub,addingthiscomposer.jsonfilewillallowyoutogotoPackagistandinserttheURLofyourrepository.Packagistwilladdyourcodeasanewpackage,extractingtheinfofromyourcomposer.jsonfile.Itwillshowtheavailableversionsbasedonyourtagsorbranches.Inordertolearnmoreaboutit,weencourageyoutovisittheofficialdocumentationathttps://getcomposer.org/doc/04-schema.md.
Theindex.phpfileInMVCapplications,weusuallyhaveonefilethatgetsalltherequests,androutesthemtothespecificcontrollerdependingontheURL.Thislogiccangenerallybefoundintheindex.phpfileinourrootdirectory.Wealreadyhaveone,butasweareadaptingourfeaturestotheMVCpattern,wewillnotneedthecurrentindex.phpanymore.Hence,youcansafelyreplaceitwiththefollowing:
<?php
require_once__DIR__.'/vendor/autoload.php';
TheonlythingthatthisfilewilldonowisincludethefilethathandlesalltheautoloadingfromtheComposercode.Later,wewillinitializeeverythinghere,suchasdatabaseconnections,configurationreaders,andsoon,butrightnow,let’sleaveitempty.
WorkingwithrequestsAsyoumightrecallfrompreviouschapters,themainpurposeofawebapplicationistoprocessHTTPrequestscomingfromtheclientandreturnaresponse.Ifthatisthemaingoalofyourapplication,managingrequestsandresponsesshouldbeanimportantpartofyourcode.
PHPisalanguagethatcanbeusedforscripts,butitsmainusageisinwebapplications.Duetothis,thelanguagecomesreadywithalotofhelpersformanagingrequestsandresponses.Still,thenativewayisnotideal,andasgoodOOPdevelopers,weshouldcomeupwithasetofclassesthathelpwiththat.Themainelementsforthissmallproject—stillinsideyourapplication—aretherequestandtherouter.Let’sstart!
TherequestobjectAswestartourminiframework,weneedtochangeourdirectorystructureabit.Wewillcreatethesrc/Coredirectoryforalltheclassesrelatedtotheframework.Astheconfigurationreaderfromthepreviouschaptersisalsopartoftheframework(ratherthanfunctionalityfortheuser),weshouldmovetheConfig.phpfiletothisdirectorytoo.
Thefirstthingtoconsideriswhatarequestlookslike.IfyourememberChapter2,WebApplicationswithPHP,arequestisbasicallyamessagethatgoestoaURL,andhasamethod—GETorPOSTfornow.TheURLisatthesametimecomposedoftwoparts:thedomainofthewebapplication,thatis,thenameofyourserver,andthepathoftherequestinsidetheserver.Forexample,ifyoutrytoaccesshttp://bookstore.com/my-books,thefirstpart,http://bookstore.com,wouldbethedomainand/my-bookswouldbethepath.Infact,httpwouldnotbepartofthedomain,butwedonotneedthatlevelofgranularityforourapplication.Youcangetthisinformationfromtheglobalarray$_SERVERthatPHPpopulatesforeachrequest.
OurRequestclassshouldhaveapropertyforeachofthosethreeelements,followedbyasetofgettersandsomeotherhelpersthatwillbeusefulfortheuser.Also,weshouldinitializeallthepropertiesfrom$_SERVERintheconstructor.Let’sseewhatitwouldlooklike:
<?php
namespaceBookstore\Core;
classRequest{
constGET='GET';
constPOST='POST';
private$domain;
private$path;
private$method;
publicfunction__construct(){
$this->domain=$_SERVER['HTTP_HOST'];
$this->path=$_SERVER['REQUEST_URI'];
$this->method=$_SERVER['REQUEST_METHOD'];
}
publicfunctiongetUrl():string{
return$this->domain.$this->path;
}
publicfunctiongetDomain():string{
return$this->domain;
}
publicfunctiongetPath():string{
return$this->path;
}
publicfunctiongetMethod():string{
return$this->method;
}
publicfunctionisPost():bool{
return$this->method===self::POST;
}
publicfunctionisGet():bool{
return$this->method===self::GET;
}
}
Wecanseeintheprecedingcodethatotherthanthegettersforeachproperty,weaddedthemethodsgetUrl,isPost,andisGet.Theusercouldfindthesameinformationusingthealreadyexistinggetters,butastheywillbeneededalot,itisalwaysgoodtomakeiteasierfortheuser.Alsonotethatthepropertiesarecomingfromthevaluesofthe$_SERVERarray:HTTP_HOST,REQUEST_URI,andREQUEST_METHOD.
FilteringparametersfromrequestsAnotherimportantpartofarequestistheinformationthatcomesfromtheuser,thatis,theGETandPOSTparameters,andthecookies.Aswiththe$_SERVERglobalarray,thisinformationcomesfrom$_POST,$_GET,and$_COOKIE,butitisalwaysgoodtoavoidusingthemdirectly,withoutfiltering,astheusercouldsendmaliciouscode.
Wewillnowimplementaclassthatwillrepresentamap—key-valuepairs—thatcanbefiltered.WewillcallitFilteredMap,andwillincludeitinournamespace,Bookstore\Core.WewilluseittocontaintheparametersGETandPOSTandthecookiesastwonewpropertiesinourRequestclass.Themapwillcontainonlyoneproperty,thearrayofdata,andwillhavesomemethodstofetchinformationfromit.Toconstructtheobject,weneedtosendthearrayofdataasanargumenttotheconstructor:
<?php
namespaceBookstore\Core;
classFilteredMap{
private$map;
publicfunction__construct(array$baseMap){
$this->map=$baseMap;
}
publicfunctionhas(string$name):bool{
returnisset($this->map[$name]);
}
publicfunctionget(string$name){
return$this->map[$name]??null;
}
}
Thisclassdoesnotdomuchsofar.Wecouldhavethesamefunctionalitywithanormalarray.Theutilityofthisclasscomeswhenweaddfilterswhilefetchingdata.Wewillimplementthreefilters,butyoucanaddasmanyasyouneed:
publicfunctiongetInt(string$name){
return(int)$this->get($name);
}
publicfunctiongetNumber(string$name){
return(float)$this->get($name);
}
publicfunctiongetString(string$name,bool$filter=true){
$value=(string)$this->get($name);
return$filter?addslashes($value):$value;
}
Thesethreemethodsintheprecedingcodeallowtheusertogetparametersofaspecifictype.Let’ssaythatthedeveloperneedstogettheIDofthebookfromtherequest.The
bestoptionistousethegetIntmethodtomakesurethatthereturnedvalueisavalidinteger,andnotsomemaliciouscodethatcanmessupourdatabase.AlsonotethefunctiongetString,whereweusetheaddSlashedmethod.Thismethodaddsslashestosomeofthesuspiciouscharacters,suchasslashesorquotes,tryingtopreventmaliciouscodewithit.
NowwearereadytogettheGETandPOSTparametersaswellasthecookiesfromourRequestclassusingourFilteredMap.Thenewcodewouldlooklikethefollowing:
<?php
namespaceBookstore\Core;
classRequest{
//...
private$params;
private$cookies;
publicfunction__construct(){
$this->domain=$_SERVER['HTTP_HOST'];
$this->path=explode('?',$_SERVER['REQUEST_URI'])[0];
$this->method=$_SERVER['REQUEST_METHOD'];
$this->params=newFilteredMap(
array_merge($_POST,$_GET)
);
$this->cookies=newFilteredMap($_COOKIE);
}
//...
publicfunctiongetParams():FilteredMap{
return$this->params;
}
publicfunctiongetCookies():FilteredMap{
return$this->cookies;
}
}
Withthisnewaddition,adevelopercouldgetthePOSTparameterpricewiththefollowinglineofcode:
$price=$request->getParams()->getNumber('price');
Thisiswaysaferthantheusualcalltotheglobalarray:
$price=$_POST['price'];
MappingroutestocontrollersIfyoucanrecallfromanyURLthatyouusedaily,youwillprobablynotseeanyPHPfileaspartofthepath,likewehavewithhttp://localhost:8000/init.php.WebsitestrytoformattheirURLstomakethemeasiertorememberinsteadofdependingonthefilethatshouldhandlethatrequest.Also,aswe’vealreadymentioned,allourrequestsgothroughthesamefile,index.php,regardlessoftheirpath.Becauseofthis,weneedtokeepamapoftheURLpaths,andwhoshouldhandlethem.
Sometimes,wehaveURLsthatcontainparametersaspartoftheirpath,whichisdifferentfromwhentheycontaintheGETorPOSTparameters.Forexample,togetthepagethatshowsaspecificbook,wemightincludetheIDofthebookaspartoftheURL,suchas/book/12or/book/3.TheIDwillchangeforeachdifferentbook,butthesamecontrollershouldhandlealloftheserequests.Toachievethis,wesaythattheURLcontainsanargument,andwecouldrepresentitby/book/:id,whereidistheargumentthatidentifiestheIDofthebook.Optionally,wecouldspecifythekindofvaluethisargumentcantake,forexample,number,string,andsoon.
Controllers,theonesinchargeofprocessingrequests,aredefinedbyamethod’sclass.ThismethodtakesasargumentsalltheargumentsthattheURL’spathdefines,suchastheIDofthebook.Wegroupcontrollersbytheirfunctionality,thatis,aBookControllerclasswillcontainthemethodsrelatedtorequestsaboutbooks.
Havingdefinedalltheelementsofaroute—aURL-controllerrelationship—wearereadytocreateourroutes.jsonfile,aconfigurationfilethatwillkeepthismap.Eachentryofthisfileshouldcontainaroute,thekeybeingtheURL,andthevalue,amapofinformationaboutthecontroller.Let’sseeanexample:
{
"books/:page":{
"controller":"Book",
"method":"getAllWithPage",
"params":{
"page":"number"
}
}
}
TherouteintheprecedingexamplereferstoalltheURLsthatfollowthepattern/books/:page,withpagebeinganynumber.Thus,thisroutewillmatchURLssuchas/books/23or/books/2,butitshouldnotmatch/books/oneor/books.ThecontrollerthatwillhandlethisrequestshouldbethegetAllWithPagemethodfromBookController;wewillappendControllertoalltheclassnames.Giventheparametersthatwedefined,thedefinitionofthemethodshouldbesomethinglikethefollowing:
publicfunctiongetAllWithPage(int$page):string{
//...
}
Thereisonelastthingweshouldconsiderwhendefiningaroute.Forsomeendpoints,we
shouldenforcetheusertobeauthenticated,suchaswhentheuseristryingtoaccesstheirownsales.Wecoulddefinethisruleinseveralways,butwechosetodoitaspartoftheroute,addingtheentry"login":trueaspartofthecontroller’sinformation.Withthatinmind,let’saddtherestoftheroutesthatdefinealltheviewsthatweexpecttohave:
{
//...
"books":{
"controller":"Book",
"method":"getAll"
},
"book/:id":{
"controller":"Book",
"method":"get",
"params":{
"id":"number"
}
},
"books/search":{
"controller":"Book",
"method":"search"
},
"login":{
"controller":"Customer",
"method":"login"
},
"sales":{
"controller":"Sales",
"method":"getByUser",
"login":true
},
"sales/:id":{
"controller":"Sales",
"method":"get",
"login":true,
"params":{
"id":"number"
}
},
"my-books":{
"controller":"Book",
"method":"getByUser",
"login":true
}
}
Theseroutesdefineallthepagesweneed;wecangetallthebooksinapaginatedwayorspecificbooksbytheirID,wecansearchbooks,listthesalesoftheuser,showaspecificsalebyitsID,andlistallthebooksthatacertainuserhasborrowed.However,wearestilllackingsomeoftheendpointsthatourapplicationshouldbeabletohandle.Forallthoseactionsthataretryingtomodifydataratherthanrequestingit,thatis,borrowingabookorbuyingit,weneedtoaddendpointstoo.Addthefollowingtoyourroutes.jsonfile:
{
//...
"book/:id/buy":{
"controller":"Sales",
"method":"add",
"login":true
"params":{
"id":"number"
}
},
"book/:id/borrow":{
"controller":"Book",
"method":"borrow",
"login":true
"params":{
"id":"number"
}
},
"book/:id/return":{
"controller":"Book",
"method":"returnBook",
"login":true
"params":{
"id":"number"
}
}
}
TherouterTherouterwillbebyfarthemostcomplicatedpieceofcodeinourapplication.ThemaingoalistoreceiveaRequestobject,decidewhichcontrollershouldhandleit,invokeitwiththenecessaryparameters,andreturntheresponsefromthatcontroller.Themaingoalofthissectionistounderstandtheimportanceoftherouterratherthanitsdetailedimplementation,butwewilltrytodescribeeachofitsparts.Copythefollowingcontentasyoursrc/Core/Router.phpfile:
<?php
namespaceBookstore\Core;
useBookstore\Controllers\ErrorController;
useBookstore\Controllers\CustomerController;
classRouter{
private$routeMap;
privatestatic$regexPatters=[
'number'=>'\d+',
'string'=>'\w'
];
publicfunction__construct(){
$json=file_get_contents(
__DIR__.'/../../config/routes.json'
);
$this->routeMap=json_decode($json,true);
}
publicfunctionroute(Request$request):string{
$path=$request->getPath();
foreach($this->routeMapas$route=>$info){
$regexRoute=$this->getRegexRoute($route,$info);
if(preg_match("@^/$regexRoute$@",$path)){
return$this->executeController(
$route,$path,$info,$request
);
}
}
$errorController=newErrorController($request);
return$errorController->notFound();
}
}
Theconstructorofthisclassreadsfromtheroutes.jsonfile,andstoresthecontentasanarray.Itsmainmethod,route,takesaRequestobjectandreturnsastring,whichiswhatwewillsendasoutputtotheclient.Thismethoditeratesalltheroutesfromthearray,tryingtomatcheachwiththepathofthegivenrequest.Onceitfindsone,ittriestoexecutethecontrollerrelatedtothatroute.Ifnoneoftheroutesareagoodmatchtotherequest,therouterwillexecutethenotFoundmethodoftheErrorController,whichwill
thenreturnanerrorpage.
URLsmatchingwithregularexpressionsWhilematchingaURLwiththeroute,weneedtotakecareoftheargumentsfordynamicURLs,astheydonotletusperformasimplestringcomparison.PHP—andotherlanguages—hasaverystrongtoolforperformingstringcomparisonswithdynamiccontent:regularexpressions.Beinganexpertinregularexpressionstakestime,anditisoutsidethescopeofthisbook,butwewillgiveyouabriefintroductiontothem.
Aregularexpressionisastringthatcontainssomewildcardcharactersthatwillmatchthedynamiccontent.Someofthemostimportantonesareasfollows:
^:Thisisusedtospecifythatthematchingpartshouldbethestartofthewholestring$:Thisisusedtospecifythatthematchingpartshouldbetheendofthewholestring\d:Thisisusedtomatchadigit\w:Thisisusedtomatchaword+:Thisisusedforfollowingacharacterorexpression,toletthatcharacterorexpressiontoappearatleastonceormanytimes*:Thisisusedforfollowingacharacterorexpression,toletthatcharacterorexpressiontoappearzeroormanytimes.:Thisisusedtomatchanysinglecharacter
Let’sseesomeexamples:
Thepattern.*willmatchanything,evenanemptystringThepattern.+willmatchanythingthatcontainsatleastonecharacterThepattern^\d+$willmatchanynumberthathasatleastonedigit
InPHP,wehavedifferentfunctionstoworkwithregularexpressions.Theeasiestofthem,andtheonethatwewilluse,ispregmatch.Thisfunctiontakesapatternasitsfirstargument(delimitedbytwocharacters,usually@or/),thestringthatwearetryingtomatchasthesecondargument,andoptionally,anarraywherePHPstorestheoccurrencesfound.ThefunctionreturnsaBooleanvalue,beingtrueiftherewasamatch,falseotherwise.WeuseitasfollowsinourRouteclass:
preg_match("@^/$regexRoute$@",$path)
The$pathvariablecontainsthepathoftherequest,forexample,/books/2.Wematchusingapatternthatisdelimitedby@,hasthe^and$wildcardstoforcethepatterntomatchthewholestring,andcontainstheconcatenationof/andthevariable$regexRoute.Thecontentofthisvariableisgivenbythefollowingmethod;addthisaswelltoyourRouterclass:
privatefunctiongetRegexRoute(
string$route,
array$info
):string{
if(isset($info['params'])){
foreach($info['params']as$name=>$type){
$route=str_replace(
':'.$name,self::$regexPatters[$type],$route
);
}
}
return$route;
}
Theprecedingmethoditeratestheparameterslistcomingfromtheinformationoftheroute.Foreachparameter,thefunctionreplacesthenameoftheparameterinsidetheroutebythewildcardcharactercorrespondingtothetypeofparameter—checkthestaticarray,$regexPatterns.Toillustratetheusageofthisfunction,let’sseesomeexamples:
Theroute/bookswillbereturnedwithoutachange,asitdoesnotcontainanyargumentTheroutebooks/:id/borrowwillbechangedtobooks/\d+/borrow,astheURLargument,id,isanumber
ExtractingtheargumentsoftheURLInordertoexecutethecontroller,weneedthreepiecesofdata:thenameoftheclasstoinstantiate,thenameofthemethodtoexecute,andtheargumentsthatthemethodneedstoreceive.Wealreadyhavethefirsttwoaspartoftheroute$infoarray,solet’sfocusoureffortsonfindingthethirdone.AddthefollowingmethodtotheRouterclass:
privatefunctionextractParams(
string$route,
string$path
):array{
$params=[];
$pathParts=explode('/',$path);
$routeParts=explode('/',$route);
foreach($routePartsas$key=>$routePart){
if(strpos($routePart,':')===0){
$name=substr($routePart,1);
$params[$name]=$pathParts[$key+1];
}
}
return$params;
}
ThislastmethodexpectsthatboththepathoftherequestandtheURLoftheroutefollowthesamepattern.Withtheexplodemethod,wegettwoarraysthatshouldmatcheachoftheirentries.Weiteratethem,andforeachentryintheroutearraythatlookslikeaparameter,wefetchitsvalueintheURL.Forexample,ifwehadtheroute/books/:id/borrowandthepath/books/12/borrow,theresultofthismethodwouldbethearray[‘id’=>12].
ExecutingthecontrollerWeendthissectionbyimplementingthemethodthatexecutesthecontrollerinchargeofa
givenroute.Wealreadyhavethenameoftheclass,themethod,andtheargumentsthatthemethodneeds,sowecouldmakeuseofthecall_user_func_arraynativefunctionthat,givenanobject,amethodname,andtheargumentsforthemethod,invokesthemethodoftheobjectpassingthearguments.Wehavetomakeuseofitasthenumberofargumentsisnotfixed,andwecannotperformanormalinvocation.
Butwearestillmissingabehaviorintroducedwhencreatingourroutes.jsonfile.Therearesomeroutesthatforcetheusertobeloggedin,which,inourcase,meansthattheuserhasacookiewiththeuserID.Givenaroutethatenforcesauthorization,wewillcheckwhetherourrequestcontainsthecookie,inwhichcasewewillsetittothecontrollerclassthroughsetCustomerId.Iftheuserdoesnothaveacookie,insteadofexecutingthecontrollerforthecurrentroute,wewillexecutetheshowLoginmethodoftheCustomerControllerclass,whichwillrenderthetemplatefortheloginform.Let’sseehoweverythingwouldlookonaddingthelastmethodofourRouterclass:
privatefunctionexecuteController(
string$route,
string$path,
array$info,
Request$request
):string{
$controllerName='\Bookstore\Controllers\\'
.$info['controller'].'Controller';
$controller=new$controllerName($request);
if(isset($info['login'])&&$info['login']){
if($request->getCookies()->has('user')){
$customerId=$request->getCookies()->get('user');
$controller->setCustomerId($customerId);
}else{
$errorController=newCustomerController($request);
return$errorController->login();
}
}
$params=$this->extractParams($route,$path);
returncall_user_func_array(
[$controller,$info['method']],$params
);
}
Wehavealreadywarnedyouaboutthelackofsecurityinourapplication,asthisisjustaprojectwithdidacticpurposes.So,avoidcopyingtheauthorizationsystemimplementedhere.
MformodelImagineforamomentthatourbookstorewebsiteisquitesuccessful,sowethinkofbuildingamobileapptoincreaseourmarket.Ofcourse,wewouldwanttousethesamedatabasethatweuseforourwebsite,asweneedtosyncthebooksthatpeopleborroworbuyfrombothapps.Wedonotwanttobeinapositionwheretwopeoplebuythesamelastcopyofabook!
Notonlythedatabase,butthequeriesusedtogetbooks,updatethem,andsoon,havetobethesametoo,otherwisewewouldendupwithunexpectedbehavior.Ofcourse,oneapparentlyeasyoptionwouldbetoreplicatethequeriesinbothcodebases,butthathasahugemaintainabilityproblem.Whatifwechangeonesinglefieldofourdatabase?Weneedtoapplythesamechangetoatleasttwodifferentcodebases.Thatdoesnotseemtobeusefulatall.
Businesslogicplaysanimportantroleheretoo.Thinkofitasdecisionsyouneedtotakethataffectyourbusiness.Inourcase,thatapremiumcustomerisabletoborrow10booksandanormaloneonly3,isbusinesslogic.Thislogicshouldbeputinacommonplacetoo,because,ifwewanttochangeit,wewillhavethesameproblemsaswithourdatabasequeries.
Wehopethatbynowwe’veconvincedyouthatdataandbusinesslogicshouldbeseparatedfromtherestofthecodeinordertomakeitreusable.Donotworryifitishardforyoutodefinewhatshouldgoaspartofthemodeloraspartofthecontroller;alotofpeoplestrugglewiththisdistinction.Asourapplicationisverysimple,anditdoesnothavealotofbusinesslogic,wewilljustfocusonaddingallthecoderelatedtoMySQLqueries.
Asyoucanimagine,foranapplicationintegratedwithMySQL,oranyotherdatabasesystem,thedatabaseconnectionisanimportantelementofamodel.WechosetousePDOinordertointeractwithMySQL,andasyoumightremember,instantiatingthatclasswasabitofapain.Let’screateasingletonclassthatreturnsaninstanceofPDOtomakethingseasier.Addthiscodetosrc/Core/Db.php:
<?php
namespaceBookstore\Core;
usePDO;
classDb{
privatestatic$instance;
privatestaticfunctionconnect():PDO{
$dbConfig=Config::getInstance()->get('db');
returnnewPDO(
'mysql:host=127.0.0.1;dbname=bookstore',
$dbConfig['user'],
$dbConfig['password']
);
}
publicstaticfunctiongetInstance(){
if(self::$instance==null){
self::$instance=self::connect();
}
returnself::$instance;
}
}
Thisclass,definedintheprecedingcodesnippet,justimplementsthesingletonpatternandwrapsthecreationofaPDOinstance.Fromnowon,inordertogetadatabaseconnection,wejustneedtowriteDb::getInstance().
Althoughitmightnotbetrueforallmodels,inourapplication,theywillalwayshavetoaccessthedatabase.Wecouldcreateanabstractclasswhereallmodelsextend.Thisclasscouldcontaina$dbprotectedpropertythatwillbesetontheconstructor.Withthis,weavoidduplicatingthesameconstructorandpropertydefinitionacrossallourmodels.Copythefollowingclassintosrc/Models/AbstractModel.php:
<?php
namespaceBookstore\Models;
usePDO;
abstractclassAbstractModel{
private$db;
publicfunction__construct(PDO$db){
$this->db=$db;
}
}
Finally,tofinishthesetupofthemodels,wecouldcreateanewexception(aswedidwiththeNotFoundExceptionclass)thatrepresentsanerrorfromthedatabase.Itwillnotcontainanycode,butwewillbeabletodifferentiatewhereanexceptioniscomingfrom.Wewillsaveitinsrc/Exceptions/DbException.php:
<?php
namespaceBookstore\Exceptions;
useException;
classDbExceptionextendsException{
}
Nowthatwe’vesettheground,wecanstartwritingourmodels.Itisuptoyoutoorganizeyourmodels,butitisagoodideatomimicthedomainobjectsstructure.Inthiscase,wewouldhavethreemodels:CustomerModel,BookModel,andSalesModel.Inthefollowingsections,wewillexplainthecontentsofeachofthem.
ThecustomermodelLet’sstartwiththeeasiestone.Asourapplicationisstillveryprimitive,wewillnotallowthecreationofnewcostumers,andworkwiththeonesweinsertedmanuallyintothedatabaseinstead.Thatmeansthattheonlythingweneedtodowithcustomersistoquerythem.Let’screateaCustomerModelclassinsrc/Models/CustomerModel.phpwiththefollowingcontent:
<?php
namespaceBookstore\Models;
useBookstore\Domain\Customer;
useBookstore\Domain\Customer\CustomerFactory;
useBookstore\Exceptions\NotFoundException;
classCustomerModelextendsAbstractModel{
publicfunctionget(int$userId):Customer{
$query='SELECT*FROMcustomerWHEREcustomer_id=:user';
$sth=$this->db->prepare($query);
$sth->execute(['user'=>$userId]);
$row=$sth->fetch();
if(empty($row)){
thrownewNotFoundException();
}
returnCustomerFactory::factory(
$row['type'],
$row['id'],
$row['firstname'],
$row['surname'],
$row['email']
);
}
publicfunctiongetByEmail(string$email):Customer{
$query='SELECT*FROMcustomerWHEREemail=:user';
$sth=$this->db->prepare($query);
$sth->execute(['user'=>$email]);
$row=$sth->fetch();
if(empty($row)){
thrownewNotFoundException();
}
returnCustomerFactory::factory(
$row['type'],
$row['id'],
$row['firstname'],
$row['surname'],
$row['email']
);
}
}
TheCustomerModelclass,whichextendsfromtheAbstractModelclass,containstwomethods;bothofthemreturnaCustomerinstance,oneofthemwhenprovidingtheIDofthecustomer,andtheotheronewhenprovidingthee-mail.Aswealreadyhavethedatabaseconnectionasthe$dbproperty,wejustneedtopreparethestatementwiththegivenquery,executethestatementwiththearguments,andfetchtheresult.Asweexpecttogetacustomer,iftheuserprovidedanIDorane-mailthatdoesnotbelongtoanycustomer,wewillneedtothrowanexception—inthiscase,aNotFoundExceptionisjustfine.Ifwefindacustomer,weuseourfactorytocreatetheobjectandreturnit.
ThebookmodelOurBookModelclassgivesusabitmoreofwork.Customershadafactory,butitisnotworthhavingoneforbooks.WhatweuseforcreatingthemfromMySQLrowsisnottheconstructor,butafetchmodethatPDOhas,andthatallowsustomaparowintoanobject.Todoso,weneedtoadapttheBookdomainobjectabit:
ThenamesofthepropertieshavetobethesameasthenamesofthefieldsinthedatabaseThereisnoneedforaconstructororsetters,unlessweneedthemforotherpurposesTogowithencapsulation,propertiesshouldbeprivate,sowewillneedgettersforallofthem
ThenewBookclassshouldlooklikethefollowing:
<?php
namespaceBookstore\Domain;
classBook{
private$id;
private$isbn;
private$title;
private$author;
private$stock;
private$price;
publicfunctiongetId():int{
return$this->id;
}
publicfunctiongetIsbn():string{
return$this->isbn;
}
publicfunctiongetTitle():string{
return$this->title;
}
publicfunctiongetAuthor():string{
return$this->author;
}
publicfunctiongetStock():int{
return$this->stock;
}
publicfunctiongetCopy():bool{
if($this->stock<1){
returnfalse;
}else{
$this->stock--;
returntrue;
}
}
publicfunctionaddCopy(){
$this->stock++;
}
publicfunctiongetPrice():float{
return$this->price;
}
}
WeretainedthegetCopyandaddCopymethodseventhoughtheyarenotgetters,aswewillneedthemlater.Now,whenfetchingagroupofrowsfromMySQLwiththefetchAllmethod,wecansendtwoparameters:theconstantPDO::FETCH_CLASSthattellsPDOtomaprowstoaclass,andthenameoftheclassthatwewanttomapto.Let’screatetheBookModelclasswithasimplegetmethodthatfetchesabookfromthedatabasewithagivenID.ThismethodwillreturneitheraBookobjectorthrowanexceptionincasetheIDdoesnotexist.Saveitassrc/Models/BookModel.php:
<?php
namespaceBookstore\Models;
useBookstore\Domain\Book;
useBookstore\Exceptions\DbException;
useBookstore\Exceptions\NotFoundException;
usePDO;
classBookModelextendsAbstractModel{
constCLASSNAME='\Bookstore\Domain\Book';
publicfunctionget(int$bookId):Book{
$query='SELECT*FROMbookWHEREid=:id';
$sth=$this->db->prepare($query);
$sth->execute(['id'=>$bookId]);
$books=$sth->fetchAll(
PDO::FETCH_CLASS,self::CLASSNAME
);
if(empty($books)){
thrownewNotFoundException();
}
return$books[0];
}
}
Thereareadvantagesanddisadvantagesofusingthisfetchmode.Ononehand,weavoidalotofdullcodewhencreatingobjectsfromrows.Usually,weeitherjustsendalltheelementsoftherowarraytotheconstructoroftheclass,orusesettersforallitsproperties.IfweaddmorefieldstotheMySQLtable,wejustneedtoaddthepropertiestoourdomainclass,insteadofchangingeverywherewherewewereinstantiatingtheobjects.Ontheotherhand,youareforcedtousethesamenamesforthefieldsinboththetable’sas
wellastheclass’properties,whichmeanshighcoupling(alwaysabadidea).Thisalsocausessomeconflictswhenfollowingconventions,becauseinMySQL,itiscommontousebook_id,butinPHP,thepropertyis$bookId.
Nowthatweknowhowthisfetchmodeworks,let’saddthreeothermethodsthatfetchdatafromMySQL.Addthefollowingcodetoyourmodel:
publicfunctiongetAll(int$page,int$pageLength):array{
$start=$pageLength*($page-1);
$query='SELECT*FROMbookLIMIT:page,:length';
$sth=$this->db->prepare($query);
$sth->bindParam('page',$start,PDO::PARAM_INT);
$sth->bindParam('length',$pageLength,PDO::PARAM_INT);
$sth->execute();
return$sth->fetchAll(PDO::FETCH_CLASS,self::CLASSNAME);
}
publicfunctiongetByUser(int$userId):array{
$query=<<<SQL
SELECTb.*
FROMborrowed_booksbbLEFTJOINbookbONbb.book_id=b.id
WHEREbb.customer_id=:id
SQL;
$sth=$this->db->prepare($query);
$sth->execute(['id'=>$userId]);
return$sth->fetchAll(PDO::FETCH_CLASS,self::CLASSNAME);
}
publicfunctionsearch(string$title,string$author):array{
$query=<<<SQL
SELECT*FROMbook
WHEREtitleLIKE:titleANDauthorLIKE:author
SQL;
$sth=$this->db->prepare($query);
$sth->bindValue('title',"%$title%");
$sth->bindValue('author',"%$author%");
$sth->execute();
return$sth->fetchAll(PDO::FETCH_CLASS,self::CLASSNAME);
}
Themethodsaddedareasfollows:
getAllreturnsanarrayofallthebooksforagivenpage.RememberthatLIMITallowsyoutoreturnaspecificnumberofrowswithanoffset,whichcanworkasapaginator.getByUserreturnsallthebooksthatagivencustomerhasborrowed—wewillneedtouseajoinqueryforthis.Notethatwereturnb.*,thatis,onlythefieldsofthebooktable,skippingtherestofthefields.Finally,thereisamethodtosearchbyeithertitleorauthor,orboth.WecandothatusingtheoperatorLIKEandenclosingthepatternswith%.Ifwedonotspecifyoneof
theparameters,wewilltrytomatchthefieldwith%%,whichmatcheseverything.
Sofar,wehavebeenaddingmethodstofetchdata.Let’saddmethodsthatwillallowustomodifythedatainourdatabase.Forthebookmodel,wewillneedtobeabletoborrowbooksandreturnthem.Hereisthecodeforthosetwoactions:
publicfunctionborrow(Book$book,int$userId){
$query=<<<SQL
INSERTINTOborrowed_books(book_id,customer_id,start)
VALUES(:book,:user,NOW())
SQL;
$sth=$this->db->prepare($query);
$sth->bindValue('book',$book->getId());
$sth->bindValue('user',$userId);
if(!$sth->execute()){
thrownewDbException($sth->errorInfo()[2]);
}
$this->updateBookStock($book);
}
publicfunctionreturnBook(Book$book,int$userId){
$query=<<<SQL
UPDATEborrowed_booksSETend=NOW()
WHEREbook_id=:bookANDcustomer_id=:userANDendISNULL
SQL;
$sth=$this->db->prepare($query);
$sth->bindValue('book',$book->getId());
$sth->bindValue('user',$userId);
if(!$sth->execute()){
thrownewDbException($sth->errorInfo()[2]);
}
$this->updateBookStock($book);
}
privatefunctionupdateBookStock(Book$book){
$query='UPDATEbookSETstock=:stockWHEREid=:id';
$sth=$this->db->prepare($query);
$sth->bindValue('id',$book->getId());
$sth->bindValue('stock',$book->getStock());
if(!$sth->execute()){
thrownewDbException($sth->errorInfo()[2]);
}
}
Whenborrowingabook,youareaddingarowtotheborrower_bookstable.Whenreturningbooks,youdonotwanttoremovethatrow,butrathertosettheenddateinordertokeepahistoryofthebooksthatauserhasbeenborrowing.Bothmethodsneedtochangethestockoftheborrowedbook:whenborrowingit,reducingthestockbyone,andwhenreturningit,increasingthestock.Thatiswhy,inthelastcodesnippet,wecreatedaprivatemethodtoupdatethestockofagivenbook,whichwillbeusedfromboththeborrowandreturnBookmethods.
ThesalesmodelNowweneedtoaddthelastmodeltoourapplication:theSalesModel.Usingthesamefetchmodethatweusedwithbooks,weneedtoadaptthedomainclassaswell.Weneedtothinkabitmoreinthiscase,aswewillbedoingmorethanjustfetching.Ourapplicationhastobeabletocreatenewsalesondemand,containingtheIDofthecustomerandthebooks.Wecanalreadyaddbookswiththecurrentimplementation,butweneedtoaddasetterforthecustomerID.TheIDofthesalewillbegivenbytheautoincrementIDinMySQL,sothereisnoneedtoaddasetterforit.Thefinalimplementationwouldlookasfollows:
<?php
namespaceBookstore\Domain;
classSale{
private$id;
private$customer_id;
private$books;
private$date;
publicfunctionsetCustomerId(int$customerId){
$this->customer_id=$customerId;
}
publicfunctiongetId():int{
return$this->id;
}
publicfunctiongetCustomerId():int{
return$this->customer_id;
}
publicfunctiongetBooks():array{
return$this->books;
}
publicfunctiongetDate():string{
return$this->date;
}
publicfunctionaddBook(int$bookId,int$amount=1){
if(!isset($this->books[$bookId])){
$this->books[$bookId]=0;
}
$this->books[$bookId]+=$amount;
}
publicfunctionsetBooks(array$books){
$this->books=$books;
}
}
TheSalesModelwillbethemostdifficultonetowrite.Theproblemwiththismodelis
thatitincludesmanipulatingdifferenttables:saleandsale_book.Forexample,whengettingtheinformationofasale,weneedtogettheinformationfromthesaletable,andthentheinformationofallthebooksinthesale_booktable.Youcouldargueaboutwhethertohaveoneuniquemethodthatfetchesallthenecessaryinformationrelatedtoasale,ortohavetwodifferentmethods,onetofetchthesaleandtheothertofetchthebooks,andletthecontrollertodecidewhichonetouse.
Thisactuallystartsaveryinterestingdiscussion.Ononehand,wewanttomakethingseasierforthecontroller—havingoneuniquemethodtofetchtheentireSaleobject.ThismakessenseasthecontrollerdoesnotneedtoknowabouttheinternalimplementationoftheSaleobject,whichlowerscoupling.Ontheotherhand,forcingthemodeltoalwaysfetchthewholeobject,evenifweonlyneedtheinformationinthesaletable,isabadidea.Imagineifthesalecontainsalotofbooks;fetchingthemfromMySQLwilldecreaseperformanceunnecessarily.
Youshouldthinkhowyourcontrollersneedtomanagesales.Ifyouwillalwaysneedtheentireobject,youcanhaveonemethodwithoutbeingconcernedaboutperformance.Ifyouonlyneedtofetchtheentireobjectsometimes,maybeyoucouldaddbothmethods.Forourapplication,wewillhaveonemethodtorulethemall,sincethatiswhatwewillalwaysneed.
NoteLazyloading
Aswithanyotherdesignchallenge,otherdevelopershavealreadygivenalotofthoughttothisproblem.Theycameupwithadesignpatternnamedlazyload.Thispatternbasicallyletsthecontrollerthinkthatthereisonlyonemethodtofetchthewholedomainobject,butwewillactuallybefetchingonlywhatweneedfromdatabase.
Themodelfetchesthemostusedinformationfortheobjectandleavestherestofthepropertiesthatneedextradatabasequeriesempty.Oncethecontrollerusesagetterofapropertythatisempty,themodelautomaticallyfetchesthatdatafromthedatabase.Wegetthebestofbothworlds:thereissimplicityforthecontroller,butwedonotspendmoretimethannecessaryqueryingunuseddata.
Addthefollowingasyoursrc/Models/SaleModel.phpfile:
<?php
namespaceBookstore\Models;
useBookstore\Domain\Sale;
useBookstore\Exceptions\DbException;
usePDO;
classSaleModelextendsAbstractModel{
constCLASSNAME='\Bookstore\Domain\Sale';
publicfunctiongetByUser(int$userId):array{
$query='SELECT*FROMsaleWHEREs.customer_id=:user';
$sth=$this->db->prepare($query);
$sth->execute(['user'=>$userId]);
return$sth->fetchAll(PDO::FETCH_CLASS,self::CLASSNAME);
}
publicfunctionget(int$saleId):Sale{
$query='SELECT*FROMsaleWHEREid=:id';
$sth=$this->db->prepare($query);
$sth->execute(['id'=>$saleId]);
$sales=$sth->fetchAll(PDO::FETCH_CLASS,self::CLASSNAME);
if(empty($sales)){
thrownewNotFoundException('Salenotfound.');
}
$sale=array_pop($sales);
$query=<<<SQL
SELECTb.id,b.title,b.author,b.price,sb.amountasstock,b.isbn
FROMsales
LEFTJOINsale_booksbONs.id=sb.sale_id
LEFTJOINbookbONsb.book_id=b.id
WHEREs.id=:id
SQL;
$sth=$this->db->prepare($query);
$sth->execute(['id'=>$saleId]);
$books=$sth->fetchAll(
PDO::FETCH_CLASS,BookModel::CLASSNAME
);
$sale->setBooks($books);
return$sale;
}
}
Anothertrickymethodinthismodelistheonethattakescareofcreatingasaleinthedatabase.Thismethodhastocreateasaleinthesaletable,andthenaddallthebooksforthatsaletothesale_booktable.Whatwouldhappenifwehaveaproblemwhenaddingoneofthebooks?Wewouldleaveacorruptedsaleinthedatabase.Toavoidthat,weneedtousetransactions,startingwithoneatthebeginningofthemodel’sorthecontroller’smethod,andeitherrollingbackincaseoferror,orcommittingitattheendofthemethod.
Inthesamemethod,wealsoneedtotakecareoftheIDofthesale.WedonotsettheIDofthesalewhencreatingthesaleobject,becausewerelyontheautoincrementalfieldinthedatabase.Butwheninsertingthebooksintosale_book,wedoneedtheIDofthesale.Forthat,weneedtorequestthePDOforthelastinsertedIDwiththelastInsertIdmethod.Let’saddthenthecreatemethodintoyourSaleModel:
publicfunctioncreate(Sale$sale){
$this->db->beginTransaction();
$query=<<<SQL
INSERTINTOsale(customer_id,date)
VALUES(:id,NOW())
SQL;
$sth=$this->db->prepare($query);
if(!$sth->execute(['id'=>$sale->getCustomerId()])){
$this->db->rollBack();
thrownewDbException($sth->errorInfo()[2]);
}
$saleId=$this->db->lastInsertId();
$query=<<<SQL
INSERTINTOsale_book(sale_id,book_id,amount)
VALUES(:sale,:book,:amount)
SQL;
$sth=$this->db->prepare($query);
$sth->bindValue('sale',$saleId);
foreach($sale->getBooks()as$bookId=>$amount){
$sth->bindValue('book',$bookId);
$sth->bindValue('amount',$amount);
if(!$sth->execute()){
$this->db->rollBack();
thrownewDbException($sth->errorInfo()[2]);
}
}
$this->db->commit();
}
Onelastthingtonotefromthismethodisthatweprepareastatement,bindavaluetoit(thesaleID),andthenbindandexecutethesamestatementasmanytimesasthebooksinthearray.Onceyouhaveastatement,youcanbindthevaluesasmanytimesasyouwant.Also,youcanexecutethesamestatementasmanytimesasyouwant,andthevaluesstaythesame.
VforviewTheviewisthelayerthattakescareofthe…view.Inthislayer,youfindallthetemplatesthatrendertheHTMLthattheusergets.Althoughtheseparationbetweenviewsandtherestoftheapplicationiseasytosee,thatdoesnotmakeviewsaneasypart.Infact,youwillhavetolearnanewtechnologyinordertowriteviewsproperly.Let’sgetintothedetails.
IntroductiontoTwigInourfirstattemptatwritingviews,wemixedupPHPandHTMLcode.WealreadyknowthatthelogicshouldnotbemixedinthesameplaceasHTML,butthatisnottheendofthestory.WhenrenderingHTML,weneedsomelogictheretoo.Forexample,ifwewanttoprintalistofbooks,weneedtorepeatacertainblockofHTMLforeachbook.Andsinceaprioriwedonotknowthenumberofbookstoprint,thebestoptionwouldbeaforeachloop.
Oneoptionthatalotofpeopletakeisminimizingtheamountoflogicthatyoucanincludeinaview.Youcouldsetsomerules,suchasweshouldonlyincludeconditionalsandloops,whichisareasonableamountoflogicneededtorenderbasicviews.Theproblemisthatthereisnotawayofenforcingthiskindofrule,andotherdeveloperscaneasilystartaddingheavylogicinthere.WhilesomepeopleareOKwiththat,assumingthatnoonewilldoit,othersprefertoimplementmorerestrictivesystems.Thatwasthebeginningoftemplateengines.
Youcouldthinkofatemplateengineasanotherlanguagethatyouneedtolearn.Whywouldyoudothat?Becausethisnew“language”ismorelimitedthanPHP.Theselanguagesusuallyallowyoutoperformconditionalsandsimpleloops,andthatisit.ThedeveloperisnotabletoaddPHPtothatfile,sincethetemplateenginewillnottreatitasPHPcode.Instead,itwilljustprintthecodetotheoutput—theresponse’body—asifitwasplaintext.Also,asitisspeciallyorientedtowritetemplates,thesyntaxisusuallyeasiertoreadwhenmixedwithHTML.Almosteverythingisanadvantage.
TheinconvenienceofusingatemplateengineisthatittakessometimetotranslatethenewlanguagetoPHP,andthentoHTML.Thiscanbequitetimeconsuming,soitisveryimportantthatyouchooseagoodtemplateengine.Mostofthemalsoallowyoutocachetemplates,improvingtheperformance.Ourchoiceisaquitelightandwidelyusedone:Twig.Aswe’vealreadyaddedthedependencyinourComposerfile,wecanuseitstraightaway.
SettingupTwigisquiteeasy.OnthePHPside,youjustneedtospecifythelocationofthetemplates.Acommonconventionistousetheviewsdirectoryforthat.Createthedirectory,andaddthefollowingtwolinesintoyourindex.php:
$loader=newTwig_Loader_Filesystem(__DIR__.'/views');
$twig=newTwig_Environment($loader);
ThebookviewInthesesections,asweworkwithtemplates,itwouldbenicetoseetheresultofyourwork.Wehavenotyetimplementedanycontrollers,sowewillforceourindex.phptorenderaspecifictemplate,regardlessoftherequest.Wecanstartrenderingtheviewofasinglebook.Forthat,let’saddthefollowingcodeattheendofyourindex.php,aftercreatingyourtwigobject:
$bookModel=newBookModel(Db::getInstance());
$book=$bookModel->get(1);
$params=['book'=>$book];
echo$twig->loadTemplate('book.twig')->render($params);
Intheprecedingcode,werequestthebookwithID1totheBookModel,getthebookobject,andcreateanarraywherethebookkeyhasthevalueofthebookobject.Afterthat,wetellTwigtoloadthetemplatebook.twigandtorenderitbysendingthearray.Thistakesthetemplateandinjectsthe$bookobject,sothatyouareabletouseitinsidethetemplate.
Let’snowcreateourfirsttemplate.Writethefollowingcodeintoview/book.twig.Byconvention,allTwigtemplatesshouldhavethe.twigextension:
<h2>{{book.title}}</h2>
<h3>{{book.author}}</h3>
<hr>
<p>
<strong>ISBN</strong>{{book.isbn}}
</p>
<p>
<strong>Stock</strong>{{book.stock}}
</p>
<p>
<strong>Price</strong>{{book.price|number_format(2)}}€
</p>
<hr>
<h3>Actions</h3>
<formmethod="post"action="/book/{{book.id}}/borrow">
<inputtype="submit"value="Borrow">
</form>
<formmethod="post"action="/book/{{book.id}}/buy">
<inputtype="submit"value="Buy">
</form>
SincethisisyourfirstTwigtemplate,let’sgostepbystep.YoucanseethatmostofthecontentisHTML:someheaders,acoupleofparagraphs,andtwoformswithtwobuttons.YoucanrecognizetheTwigpart,sinceitisenclosedby{{}}.InTwig,everythingthatis
betweenthosecurlybracketswillbeprintedout.Thefirstonethatwefindcontainsbook.title.Doyourememberthatweinjectedthebookobjectwhenrenderingthetemplate?Wecanaccessithere,justnotwiththeusualPHPsyntax.Toaccessanobject’sproperty,use.insteadof->.So,thisbook.titlewillreturnthevalueofthetitlepropertyofthebookobject,andthe{{}}willmakeTwigprintitout.Thesameappliestotherestofthetemplate.
Thereisonethatdoesabitmorethanjustaccessanobject’sproperty.Thebook.price|number_format(2)getsthepriceofthebookandsendsitasanargument(usingthepipesymbol)tothefunctionnumber_format,whichhasalreadygot2asanotherargument.Thisbitofcodebasicallyformatsthepricetotwodigitalfigures.InTwig,youalsohavesomefunctions,buttheyaremostlyreducedtoformattingtheoutput,whichisanacceptableamountoflogic.
Areyouconvincednowabouthowcleanitistouseatemplateengineforyourviews?Youcantryitinyourbrowser:accessinganypath,yourwebservershouldexecutetheindex.phpfile,forcingthetemplatebook.twigtoberendered.
LayoutsandblocksWhenyoudesignyourwebapplication,usuallyyouwouldwanttoshareacommonlayoutacrossmostofyourviews.Inourcase,wewanttoalwayshaveamenuatthetopoftheviewthatallowsustogotothedifferentsectionsofthewebsite,oreventosearchbooksfromwherevertheuseris.Aswithmodels,wewanttoavoidcodeduplication,sinceifweweretocopyandpastethelayouteverywhere,updatingitwouldbeanightmare.Instead,Twigcomeswiththeabilitytodefinelayouts.
AlayoutinTwigisjustanothertemplatefile.ItscontentisjustthecommonHTMLcodethatwewanttodisplayacrossallviews(inourcase,themenuandsearchbar),andcontainssometaggedgaps(blocksinTwig’sworld),whereyouwillbeabletoinjectthespecificHTMLofeachview.Youcandefineoneofthoseblockswiththetag{%block%}.Let’sseewhatourviews/layout.twigfilewouldlooklike:
<html>
<head>
<title>{%blocktitle%}{%endblock%}</title>
</head>
<body>
<divstyle="border:solid1px">
<ahref="/books">Books</a>
<ahref="/sales">MySales</a>
<ahref="/my-books">MyBooks</a>
<hr>
<formaction="/books/search"method="get">
<label>Title</label>
<inputtype="text"name="title">
<label>Author</label>
<inputtype="text"name="author">
<inputtype="submit"value="Search">
</form>
</div>
{%blockcontent%}{%endblock%}
</body>
</html>
Asyoucanseeintheprecedingcode,blockshaveanamesothattemplatesusingthelayoutcanrefertothem.Inourlayout,wedefinedtwoblocks:oneforthetitleoftheviewandtheotherforthecontentitself.Whenatemplateusesthelayout,wejustneedtowritetheHTMLcodeforeachoftheblocksdefinedinthelayout,andTwigwilldotherest.Also,toletTwigknowthatourtemplatewantstousethelayout,weusethetag{%extends%}withthelayoutfilename.Let’supdateviews/book.twigtouseournewlayout:
{%extends'layout.twig'%}
{%blocktitle%}
{{book.title}}
{%endblock%}
{%blockcontent%}
<h2>{{book.title}}</h2>
//...
</form>
{%endblock%}
Atthetopofthefile,weaddthelayoutthatweneedtouse.Then,weopenablocktagwiththereferencename,andwewriteinsideittheHTMLthatwewanttouse.Youcanuseanythingvalidinsideablock,eitherTwigcodeorplainHTML.Inourtemplate,weusedthetitleofthebookasthetitleblock,whichreferstothetitleoftheview,andweputallthepreviousHTMLinsidethecontentblock.Notethateverythinginthefileisinsideablocknow.Tryitinyourbrowsernowtoseethechanges.
PaginatedbooklistLet’saddanotherview,thistimeforapaginatedlistofbooks.Inordertoseetheresultofyourwork,updatethecontentofindex.php,replacingthecodeoftheprevioussectionwiththefollowing:
$bookModel=newBookModel(Db::getInstance());
$books=$bookModel->getAll(1,3);
$params=['books'=>$books,'currentPage'=>2];
echo$twig->loadTemplate('books.twig')->render($params);
Intheprecedingsnippet,weforcetheapplicationtorenderthebooks.twigtemplate,sendinganarrayofbooksfrompagenumber1,andshowing3booksperpage.Thisarray,though,mightnotalwaysreturn3books,maybebecausethereareonly2booksinthedatabase.Weshouldthenusealooptoiteratethelistinsteadofassumingthesizeofthearray.InTwig,youcanemulateaforeachloopusing{%for<element>in<array>%}inordertoiterateanarray.Let’suseitforyourviews/books.twig:
{%extends'layout.twig'%}
{%blocktitle%}
Books
{%endblock%}
{%blockcontent%}
<table>
<thead>
<th>Title</th>
<th>Author</th>
<th></th>
</thead>
{%forbookinbooks%}
<tr>
<td>{{book.title}}</td>
<td>{{book.author}}</td>
<td><ahref="/book/{{book.id}}">View</a></td>
</tr>
{%endfor%}
</table>
{%endblock%}
WecanalsouseconditionalsinaTwigtemplate,whichworkthesameastheconditionalsinPHP.Thesyntaxis{%if<booleanexpression>%}.Let’suseittodecideifweshouldshowthepreviousand/orfollowinglinksonourpage.Addthefollowingcodeattheendofthecontentblock:
{%ifcurrentPage!=1%}
<ahref="/books/{{currentPage-1}}">Previous</a>
{%endif%}
{%ifnotlastPage%}
<ahref="/books/{{currentPage+1}}">Next</a>
{%endif%}
Thelastthingtonotefromthistemplateisthatwearenotrestrictedtousingonlyvariableswhenprintingoutcontentwith{{}}.WecanaddanyvalidTwigexpressionthatreturnsavalue,aswedidwith{{currentPage+1}}.
ThesalesviewWehavealreadyshownyoueverythingthatyouwillneedforusingtemplates,andnowwejusthavetofinishaddingallofthem.Thenextoneinthelististhetemplatethatshowsthelistofsalesforagivenuser.Updateyourindex.phpfilewiththefollowinghack:
$saleModel=newSaleModel(Db::getInstance());
$sales=$saleModel->getByUser(1);
$params=['sales'=>$sales];
echo$twig->loadTemplate('sales.twig')->render($params);
Thetemplateforthisviewwillbeverysimilartotheonelistingthebooks:atablepopulatedwiththecontentofanarray.Thefollowingisthecontentofviews/sales.twig:
{%extends'layout.twig'%}
{%blocktitle%}
Mysales
{%endblock%}
{%blockcontent%}
<table>
<thead>
<th>Id</th>
<th>Date</th>
</thead>
{%forsaleinsales%}
<tr>
<td>{{sale.id}}</td>
<td>{{sale.date}}</td>
<td><ahref="/sales/{{sale.id}}">View</a></td>
</tr>
{%endfor%}
</table>
{%endblock%}
Theotherviewrelatedtosalesiswherewewanttodisplayallthecontentofaspecificone.Thissale,again,willbesimilartothebookslist,aswewillbelistingthebooksrelatedtothatsale.Thehacktoforcetherenderingofthistemplateisasfollows:
$saleModel=newSaleModel(Db::getInstance());
$sale=$saleModel->get(1);
$params=['sale'=>$sale];
echo$twig->loadTemplate('sale.twig')->render($params);
AndtheTwigtemplateshouldbeplacedinviews/sale.twig:
{%extends'layout.twig'%}
{%blocktitle%}
Sale{{sale.id}}
{%endblock%}
{%blockcontent%}
<table>
<thead>
<th>Title</th>
<th>Author</th>
<th>Amount</th>
<th>Price</th>
<th></th>
</thead>
{%forbookinsale.books%}
<tr>
<td>{{book.title}}</td>
<td>{{book.author}}</td>
<td>{{book.stock}}</td>
<td>{{(book.price*book.stock)|number_format(2)}}€</td>
<td><ahref="/book/{{book.id}}">View</a></td>
</tr>
{%endfor%}
</table>
{%endblock%}
TheerrortemplateWeshouldaddaverysimpletemplatethatwillbeshowntotheuserwhenthereisanerrorinourapplication,ratherthanshowingaPHPerrormessage.ThistemplatewilljustexpecttheerrorMessagevariable,anditcouldlooklikethefollowing.Saveitasviews/error.twig:
{%extends'layout.twig'%}
{%blocktitle%}
Error
{%endblock%}
{%blockcontent%}
<h2>Error:{{errorMessage}}</h2>
{%endblock%}
Notethateventheerrorpageextendsfromthelayout,aswewanttheusertobeabletodosomethingelsewhenthishappens.
ThelogintemplateOurlasttemplatewillbetheonethatallowstheusertologin.Thistemplateisabitdifferentfromtheothers,asitwillbeusedintwodifferentscenarios.Inthefirstone,theuseraccessestheloginviewforthefirsttime,soweneedtoshowtheform.Inthesecondone,theuserhasalreadytriedtologin,andtherewasanerrorwhendoingso,thatis,thee-mailaddresswasnotfound.Inthiscase,wewilladdanextravariabletothetemplate,errorMessage,andwewilladdaconditionaltoshowitscontentsonlywhenthisvariableisdefined.Youcanusetheoperatorisdefinedtocheckthat.Addthefollowingtemplateasviews/login.twig:
{%extends'layout.twig'%}
{%blocktitle%}
Login
{%endblock%}
{%blockcontent%}
{%iferrorMessageisdefined%}
<strong>{{errorMessage}}</strong>
{%endif%}
<formaction="/login"method="post">
<label>Email</label>
<inputtype="text"name="email">
<inputtype="submit">
</form>
{%endblock%}
CforcontrollerItisfinallytimeforthedirectoroftheorchestra.Controllersrepresentthelayerinourapplicationthat,givenarequest,talkstothemodelsandbuildstheviews.Theyactlikethemanagerofateam:theydecidewhatresourcestousedependingonthesituation.
Aswestatedwhenexplainingmodels,itissometimesdifficulttodecideifsomepieceoflogicshouldgointothecontrollerorthemodel.Attheendoftheday,MVCisapattern,likearecipethatguidesyou,ratherthananexactalgorithmthatyouneedtofollowstepbystep.Therewillbescenarioswheretheanswerisnotstraightforward,soitwillbeuptoyou;inthesecases,justtrytobeconsistent.Thefollowingaresomecommonscenariosthatmightbedifficulttolocalize:
Therequestpointstoapaththatwedonotsupport.Thisscenarioisalreadycoveredinourapplication,anditistherouterthatshouldtakecareofit,notthecontroller.Therequesttriestoaccessanelementthatdoesnotexist,forexample,abookIDthatisnotinthedatabase.Inthiscase,thecontrollershouldaskthemodelifthebookexists,anddependingontheresponse,renderatemplatewiththebook’scontents,oranotherwitha“Notfound”message.Theusertriestoperformanaction,suchasbuyingabook,buttheparameterscomingfromtherequestarenotvalid.Thisisatrickyone.Oneoptionistogetalltheparametersfromtherequestwithoutcheckingthem,sendingthemstraighttothemodel,andleavingthetaskofsanitizingtheinformationtothemodel.Anotheroptionisthatthecontrollerchecksthattheparametersprovidedmakesense,andthengivesthemtothemodel.Thereareothersolutions,likebuildingaclassthatchecksiftheparametersarevalid,whichcanbereusedindifferentcontrollers.Inthiscase,itwilldependontheamountofparametersandlogicinvolvedinthesanitization.Forrequestsreceivingalotofdata,thethirdoptionlookslikethebestofthem,aswewillbeabletoreusethecodeindifferentendpoints,andwearenotwritingcontrollersthataretoolong.Butinrequestswheretheusersendsoneortwoparameters,sanitizingtheminthecontrollermightbegoodenough.
Nowthatwe’vesettheground,let’sprepareourapplicationtousecontrollers.Thefirstthingtodoistoupdateourindex.php,whichhasbeenforcingtheapplicationtoalwaysrenderthesametemplate.Instead,weshouldbegivingthistasktotherouter,whichwillreturntheresponseasastringthatwecanjustprintwithecho.Updateyourindex.phpfilewiththefollowingcontent:
<?php
useBookstore\Core\Router;
useBookstore\Core\Request;
require_once__DIR__.'/vendor/autoload.php';
$router=newRouter();
$response=$router->route(newRequest());
echo$response;
Asyoumightremember,therouterinstantiatesacontrollerclass,sendingtherequestobjecttotheconstructor.Butcontrollershaveotherdependenciesaswell,suchasthetemplateengine,thedatabaseconnection,ortheconfigurationreader.Eventhoughthisisnotthebestsolution(youwillimproveitoncewecoverdependencyinjectioninthenextsection),wecouldcreateanAbstractControllerthatwouldbetheparentofallcontrollers,andwillsetthosedependencies.Copythefollowingassrc/Controllers/AbstractController.php:
<?php
namespaceBookstore\Controllers;
useBookstore\Core\Config;
useBookstore\Core\Db;
useBookstore\Core\Request;
useMonolog\Logger;
useTwig_Environment;
useTwig_Loader_Filesystem;
useMonolog\Handler\StreamHandler;
abstractclassAbstractController{
protected$request;
protected$db;
protected$config;
protected$view;
protected$log;
publicfunction__construct(Request$request){
$this->request=$request;
$this->db=Db::getInstance();
$this->config=Config::getInstance();
$loader=newTwig_Loader_Filesystem(
__DIR__.'/../../views'
);
$this->view=newTwig_Environment($loader);
$this->log=newLogger('bookstore');
$logFile=$this->config->get('log');
$this->log->pushHandler(
newStreamHandler($logFile,Logger::DEBUG)
);
}
publicfunctionsetCustomerId(int$customerId){
$this->customerId=$customerId;
}
}
Wheninstantiatingacontroller,wewillsetsomepropertiesthatwillbeusefulwhenhandlingrequests.Wealreadyknowhowtoinstantiatethedatabaseconnection,theconfigurationreader,andthetemplateengine.Thefourthproperty,$log,willallowthedevelopertowritelogstoagivenfilewhennecessary.WewillusetheMonologlibrary
forthat,buttherearemanyotheroptions.Noticethatinordertoinstantiatethelogger,wegetthevalueoflogfromtheconfiguration,whichshouldbethepathtothelogfile.Theconventionistousethe/var/log/directory,socreatethe/var/log/bookstore.logfile,andadd"log":"/var/log/bookstore.log"toyourconfigurationfile.
Anotherthingthatisusefultosomecontrollers—butnotallofthem—istheinformationabouttheuserperformingtheaction.Asthisisonlygoingtobeavailableforcertainroutes,weshouldnotsetitwhenconstructingthecontroller.Instead,wehaveasetterfortheroutertosetthecustomerIDwhenavailable;infact,therouterdoesthatalready.
Finally,ahandyhelpermethodthatwecoulduseisonethatrendersagiventemplatewithparameters,asallthecontrollerswillenduprenderingonetemplateortheother.Let’saddthefollowingprotectedmethodtotheAbstractControllerclass:
protectedfunctionrender(string$template,array$params):string{
return$this->view->loadTemplate($template)->render($params);
}
TheerrorcontrollerLet’sstartbycreatingtheeasiestofthecontrollers:theErrorController.Thiscontrollerdoesnotdomuch;itjustrenderstheerror.twigtemplatesendingthe“Pagenotfound!”message.Asyoumightremember,therouterusesthiscontrollerwhenitcannotmatchtherequesttoanyoftheotherdefinedroutes.Savethefollowingclassinsrc/Controllers/ErrorController.php:
<?php
namespaceBookstore\Controllers;
classErrorControllerextendsAbstractController{
publicfunctionnotFound():string{
$properties=['errorMessage'=>'Pagenotfound!'];
return$this->render('error.twig',$properties);
}
}
ThelogincontrollerThesecondcontrollerthatwehavetoaddistheonethatmanagestheloginofthecustomers.Ifwethinkabouttheflowwhenauserwantstoauthenticate,wehavethefollowingscenarios:
Theuserwantstogettheloginforminordertosubmitthenecessaryinformationandlogin.Theusertriestosubmittheform,butwecouldnotgetthee-mailaddress.Weshouldrendertheformagain,lettingthemknowabouttheproblem.Theusersubmitstheformwithane-mail,butitisnotavalidone.Inthiscase,weshouldshowtheloginformagainwithanerrormessageexplainingthesituation.Theusersubmitsavalide-mail,wesetthecookie,andweshowthelistofbookssotheusercanstartsearching.Thisisabsolutelyarbitrary;youcouldchoosetosendthemtotheirborrowedbookspage,theirsales,andsoon.Theimportantthinghereistonoticethatwewillberedirectingtherequesttoanothercontroller.
Thereareuptofourpossiblepaths.Wewillusetherequestobjecttodecidewhichofthemtouseineachcase,returningthecorrespondingresponse.Let’screate,then,theCustomerControllerclassinsrc/Controllers/CustomerController.phpwiththeloginmethod,asfollows:
<?php
namespaceBookstore\Controllers;
useBookstore\Exceptions\NotFoundException;
useBookstore\Models\CustomerModel;
classCustomerControllerextendsAbstractController{
publicfunctionlogin(string$email):string{
if(!$this->request->isPost()){
return$this->render('login.twig',[]);
}
$params=$this->request->getParams();
if(!$params->has('email')){
$params=['errorMessage'=>'Noinfoprovided.'];
return$this->render('login.twig',$params);
}
$email=$params->getString('email');
$customerModel=newCustomerModel($this->db);
try{
$customer=$customerModel->getByEmail($email);
}catch(NotFoundException$e){
$this->log->warn('Customeremailnotfound:'.$email);
$params=['errorMessage'=>'Emailnotfound.'];
return$this->render('login.twig',$params);
}
setcookie('user',$customer->getId());
$newController=newBookController($this->request);
return$newController->getAll();
}
}
Asyoucansee,therearefourdifferentreturnsforthefourdifferentcases.Thecontrolleritselfdoesnotdoanything,butorchestratestherestofthecomponents,andmakesdecisions.First,wecheckiftherequestisaPOST,andifitisnot,wewillassumethattheuserwantstogettheform.Ifitis,wewillcheckforthee-mailintheparameters,returninganerrorifthee-mailisnotthere.Ifitis,wewilltrytofindthecustomerwiththate-mail,usingourmodel.Ifwegetanexceptionsayingthatthereisnosuchcustomer,wewillrendertheformwitha“Notfound”errormessage.Iftheloginissuccessful,wewillsetthecookiewiththeIDofthecustomer,andwillexecutethegetAllmethodofBookController(stilltobewritten),returningthelistofbooks.
Atthispoint,youshouldbeabletotesttheloginfeatureofyourapplicationendtoendwiththebrowser.Trytoaccesshttp://localhost:8000/logintoseetheform,addingrandome-mailstogettheerrormessage,andaddingavalide-mail(checkyourcustomertableinMySQL)tologinsuccessfully.Afterthis,youshouldseethecookiewiththecustomerID.
ThebookcontrollerTheBookControllerclasswillbethelargestofourcontrollers,asmostoftheapplicationreliesonit.Let’sstartbyaddingtheeasiestmethods,theonesthatjustretrieveinformationfromthedatabase.Savethisassrc/Controllers/BookController.php:
<?php
namespaceBookstore\Controllers;
useBookstore\Models\BookModel;
classBookControllerextendsAbstractController{
constPAGE_LENGTH=10;
publicfunctiongetAllWithPage($page):string{
$page=(int)$page;
$bookModel=newBookModel($this->db);
$books=$bookModel->getAll($page,self::PAGE_LENGTH);
$properties=[
'books'=>$books,
'currentPage'=>$page,
'lastPage'=>count($books)<self::PAGE_LENGTH
];
return$this->render('books.twig',$properties);
}
publicfunctiongetAll():string{
return$this->getAllWithPage(1);
}
publicfunctionget(int$bookId):string{
$bookModel=newBookModel($this->db);
try{
$book=$bookModel->get($bookId);
}catch(\Exception$e){
$this->log->error(
'Errorgettingbook:'.$e->getMessage()
);
$properties=['errorMessage'=>'Booknotfound!'];
return$this->render('error.twig',$properties);
}
$properties=['book'=>$book];
return$this->render('book.twig',$properties);
}
publicfunctiongetByUser():string{
$bookModel=newBookModel($this->db);
$books=$bookModel->getByUser($this->customerId);
$properties=[
'books'=>$books,
'currentPage'=>1,
'lastPage'=>true
];
return$this->render('books.twig',$properties);
}
}
There’snothingtoospecialinthisprecedingcodesofar.ThegetAllWithPageandgetAllmethodsdothesamething,onewiththepagenumbergivenbytheuserasaURLargument,andtheothersettingthepagenumberas1—thedefaultcase.Theyaskthemodelforthelistofbookstobedisplayedandpassedtotheview.Theinformationofthecurrentpage—andwhetherornotweareonthelastpage—isalsosenttothetemplateinordertoaddthe“previous”and“next”pagelinks.
ThegetmethodwillgettheIDofthebookthatthecustomerisinterestedin.Itwilltrytofetchitusingthemodel.Ifthemodelthrowsanexception,wewillrendertheerrortemplatewitha“Booknotfound”message.Instead,ifthebookIDisvalid,wewillrenderthebooktemplateasexpected.
ThegetByUsermethodwillreturnallthebooksthattheauthenticatedcustomerhasborrowed.WewillmakeuseofthecustomerIdpropertythatwesetfromtherouter.Thereisnosanitycheckhere,sincewearenottryingtogetaspecificbook,butratheralist,whichcouldbeemptyiftheuserhasnotborrowedanybooksyet—butthatisnotanissue.
Anothergettercontrolleristheonethatsearchesforabookbyitstitleand/orauthor.Thismethodwillbetriggeredwhentheusersubmitstheforminthelayouttemplate.Theformsendsboththetitleandtheauthorfields,sothecontrollerwillaskforboth.Themodelisreadytousetheargumentsthatareempty,sowewillnotperformanyextracheckinghere.AddthemethodtotheBookControllerclass:
publicfunctionsearch():string{
$title=$this->request->getParams()->getString('title');
$author=$this->request->getParams()->getString('author');
$bookModel=newBookModel($this->db);
$books=$bookModel->search($title,$author);
$properties=[
'books'=>$books,
'currentPage'=>1,
'lastPage'=>true
];
return$this->render('books.twig',$properties);
}
Yourapplicationcannotperformanyactions,butatleastyoucanfinallybrowsethelistofbooks,andclickonanyofthemtoviewthedetails.Wearefinallygettingsomethinghere!
BorrowingbooksBorrowingandreturningbooksareprobablytheactionsthatinvolvethemostlogic,togetherwithbuyingabook,whichwillbecoveredbyadifferentcontroller.Thisisagoodplacetostartloggingtheuser’sactions,sinceitwillbeusefullaterfordebuggingpurposes.Let’sseethecodefirst,andthendiscussitbriefly.AddthefollowingtwomethodstoyourBookControllerclass:
publicfunctionborrow(int$bookId):string{
$bookModel=newBookModel($this->db);
try{
$book=$bookModel->get($bookId);
}catch(NotFoundException$e){
$this->log->warn('Booknotfound:'.$bookId);
$params=['errorMessage'=>'Booknotfound.'];
return$this->render('error.twig',$params);
}
if(!$book->getCopy()){
$params=[
'errorMessage'=>'Therearenocopiesleft.'
];
return$this->render('error.twig',$params);
}
try{
$bookModel->borrow($book,$this->customerId);
}catch(DbException$e){
$this->log->error(
'Errorborrowingbook:'.$e->getMessage()
);
$params=['errorMessage'=>'Errorborrowingbook.'];
return$this->render('error.twig',$params);
}
return$this->getByUser();
}
publicfunctionreturnBook(int$bookId):string{
$bookModel=newBookModel($this->db);
try{
$book=$bookModel->get($bookId);
}catch(NotFoundException$e){
$this->log->warn('Booknotfound:'.$bookId);
$params=['errorMessage'=>'Booknotfound.'];
return$this->render('error.twig',$params);
}
$book->addCopy();
try{
$bookModel->returnBook($book,$this->customerId);
}catch(DbException$e){
$this->log->error(
'Errorreturningbook:'.$e->getMessage()
);
$params=['errorMessage'=>'Errorreturningbook.'];
return$this->render('error.twig',$params);
}
return$this->getByUser();
}
Aswementionedearlier,oneofthenewthingshereisthatwearelogginguseractions,likewhentryingtoborroworreturnabookthatisnotvalid.Monologallowsyoutowritelogswithdifferentprioritylevels:error,warning,andnotices.Youcaninvokemethodssuchaserror,warn,ornoticetorefertoeachofthem.Weusewarningswhensomethingunexpected,yetnotcritical,happens,forexample,tryingtoborrowabookthatisnotthere.Errorsareusedwhenthereisanunknownproblemfromwhichwecannotrecover,likeanerrorfromthedatabase.
Themodusoperandiofthesetwomethodsisasfollows:wegetthebookobjectfromthe3databasewiththegivenbookID.Asusual,ifthereisnosuchbook,wereturnanerrorpage.Oncewehavethebookdomainobject,wemakeuseofthehelpersaddCopyandgetCopyinordertoupdatethestockofthebook,andsendittothemodel,togetherwiththecustomerID,tostoretheinformationinthedatabase.Thereisalsoasanitycheckwhenborrowingabook,justincasetherearenomorebooksavailable.Inbothcases,wereturnthelistofbooksthattheuserhasborrowedastheresponseofthecontroller.
ThesalescontrollerWearriveatthelastofourcontrollers:theSalesController.Withadifferentmodel,itwillendupdoingprettymuchthesameasthemethodsrelatedtoborrowedbooks.Butweneedtocreatethesaledomainobjectinthecontrollerinsteadofgettingitfromthemodel.Let’saddthefollowingcode,whichcontainsamethodforbuyingabook,add,andtwogetters:onethatgetsallthesalesofagivenuserandonethatgetstheinfoofaspecificsale,thatis,getByUserandgetrespectively.Followingtheconvention,thefilewillbesrc/Controllers/SalesController.php:
<?php
namespaceBookstore\Controllers;
useBookstore\Domain\Sale;
useBookstore\Models\SaleModel;
classSalesControllerextendsAbstractController{
publicfunctionadd($id):string{
$bookId=(int)$id;
$salesModel=newSaleModel($this->db);
$sale=newSale();
$sale->setCustomerId($this->customerId);
$sale->addBook($bookId);
try{
$salesModel->create($sale);
}catch(\Exception$e){
$properties=[
'errorMessage'=>'Errorbuyingthebook.'
];
$this->log->error(
'Errorbuyingbook:'.$e->getMessage()
);
return$this->render('error.twig',$properties);
}
return$this->getByUser();
}
publicfunctiongetByUser():string{
$salesModel=newSaleModel($this->db);
$sales=$salesModel->getByUser($this->customerId);
$properties=['sales'=>$sales];
return$this->render('sales.twig',$properties);
}
publicfunctionget($saleId):string{
$salesModel=newSaleModel($this->db);
$sale=$salesModel->get($saleId);
DependencyinjectionAttheendofthechapter,wewillcoveroneofthemostinterestingandcontroversialofthetopicsthatcomewith,notonlytheMVCpattern,butOOPingeneral:dependencyinjection.Wewillshowyouwhyitissoimportant,andhowtoimplementasolutionthatsuitsourspecificapplication,eventhoughtherearequiteafewdifferentimplementationsthatcancoverdifferentnecessities.
Whyisdependencyinjectionnecessary?Westillneedtocoverthewaytounittestyourcode,henceyouhavenotexperienceditbyyourselfyet.Butoneofthesignsofapotentialsourceofproblemsiswhenyouusethenewstatementinyourcodetocreateaninstanceofaclassthatdoesnotbelongtoyourcodebase—alsoknownasadependency.UsingnewtocreateadomainobjectlikeBookorSaleisfine.Usingittoinstantiatemodelsisalsoacceptable.Butmanuallyinstantiating,whichsomethingelse,suchasthetemplateengine,thedatabaseconnection,orthelogger,issomethingthatyoushouldavoid.Therearedifferentreasonsthatsupportthisidea:
Ifyouwanttouseacontrollerfromtwodifferentplaces,andeachoftheseplacesneedsadifferentdatabaseconnectionorlogfile,instantiatingthosedependenciesinsidethecontrollerwillnotallowustodothat.Thesamecontrollerwillalwaysusethesamedependency.Instantiatingthedependenciesinsidethecontrollermeansthatthecontrollerisfullyawareoftheconcreteimplementationofeachofitsdependencies,thatis,thecontrollerknowsthatweareusingPDOwiththeMySQLdriverandthelocationofthecredentialsfortheconnection.Thismeansahighlevelofcouplinginyourapplication—so,badnews.Replacingonedependencywithanotherthatimplementsthesameinterfaceisnoteasyifyouareinstantiatingthedependencyexplicitlyeverywhere,asyouwillhavetosearchalltheseplaces,andchangetheinstantiationmanually.
Forallthesereasons,andmore,itisalwaysgoodtoprovidethedependenciesthataclasssuchasacontrollerneedsinsteadoflettingitcreateitsown.Thisissomethingthateverybodyagreeswith.Theproblemcomeswhenimplementingasolution.Therearedifferentoptions:
Wehaveaconstructorthatexpects(througharguments)allthedependenciesthatthecontroller,oranyotherclass,needs.Theconstructorwillassigneachoftheargumentstothepropertiesoftheclass.Wehaveanemptyconstructor,andinstead,weaddasmanysettermethodsasthedependenciesoftheclass.Ahybridofboth,wherewesetthemaindependenciesthroughaconstructor,andsettherestofthedependenciesviasetters.Sendinganobjectthatcontainsallthedependenciesasauniqueargumentfortheconstructor,andthecontrollergetsthedependenciesthatitneedsfromthatcontainer.
Eachsolutionhasitsprosandcons.Ifwehaveaclasswithalotofdependencies,injectingallofthemviatheconstructorwouldmakeitcounterintuitive,soitwouldbebetterifweinjectthemusingsetters,eventhoughaclasswithalotofdependencieslookslikebaddesign.Ifwehavejustoneortwodependencies,usingtheconstructorcouldbeacceptable,andwewillwritelesscode.Forclasseswithseveraldependencies,butnotallofthemmandatory,usingthehybridversioncouldbeagoodsolution.Thefourthoptionmakesiteasierwheninjectingthedependenciesaswedonotneedtoknowwhateachobjectexpects.Theproblemisthateachclassshouldknowhowtofetchitsdependency,
ImplementingourowndependencyinjectorOpensourcesolutionsfordependencyinjectorsarealreadyavailable,butwethinkthatitwouldbeagoodexperiencetoimplementasimpleonebyyourself.Theideaofourdependencyinjectorisaclassthatcontainsinstancesofthedependenciesthatyourcodeneeds.Thisclass,whichisbasicallyamapofdependencynamestodependencyinstances,willhavetwomethods:agetterandasetterofdependencies.Wedonotwanttouseastaticpropertyforthedependenciesarray,asoneofthegoalsistobeabletohavemorethanonedependencyinjectorwithadifferentsetofdependencies.Addthefollowingclasstosrc/Utils/DependencyInjector.php:
<?php
namespaceBookstore\Utils;
useBookstore\Exceptions\NotFoundException;
classDependencyInjector{
private$dependencies=[];
publicfunctionset(string$name,$object){
$this->dependencies[$name]=$object;
}
publicfunctionget(string$name){
if(isset($this->dependencies[$name])){
return$this->dependencies[$name];
}
thrownewNotFoundException(
$name.'dependencynotfound.'
);
}
}
Havingadependencyinjectormeansthatwewillalwaysusethesameinstanceofagivenclasseverytimeweaskforit,insteadofcreatingoneeachtime.Thatmeansthatsingletonimplementationsarenotneededanymore;infact,asmentionedinChapter4,CreatingCleanCodewithOOP,itispreferabletoavoidthem.Let’sgetridofthem,then.Oneoftheplaceswherewewereusingitwasinourconfigurationreader.Replacetheexistingcodewiththefollowinginthesrc/Core/Config.phpfile:
<?php
namespaceBookstore\Core;
useBookstore\Exceptions\NotFoundException;
classConfig{
private$data;
publicfunction__construct(){
$json=file_get_contents(
__DIR__.'/../../config/app.json'
);
$this->data=json_decode($json,true);
}
publicfunctionget($key){
if(!isset($this->data[$key])){
thrownewNotFoundException("Key$keynotinconfig.");
}
return$this->data[$key];
}
}
TheotherplacewhereweweremakinguseofthesingletonpatternwasintheDBclass.Infact,thepurposeoftheclasswasonlytohaveasingletonforourdatabaseconnection,butifwearenotmakinguseofit,wecanremovetheentireclass.So,deleteyoursrc/Core/DB.phpfile.
Nowweneedtodefineallthesedependenciesandaddthemtoourdependencyinjector.Theindex.phpfileisagoodplacetohavethedependencyinjectorbeforeweroutetherequest.AddthefollowingcodejustbeforeinstantiatingtheRouterclass:
$config=newConfig();
$dbConfig=$config->get('db');
$db=newPDO(
'mysql:host=127.0.0.1;dbname=bookstore',
$dbConfig['user'],
$dbConfig['password']
);
$loader=newTwig_Loader_Filesystem(__DIR__.'/../../views');
$view=newTwig_Environment($loader);
$log=newLogger('bookstore');
$logFile=$config->get('log');
$log->pushHandler(newStreamHandler($logFile,Logger::DEBUG));
$di=newDependencyInjector();
$di->set('PDO',$db);
$di->set('Utils\Config',$config);
$di->set('Twig_Environment',$view);
$di->set('Logger',$log);
$router=newRouter($di);
//...
Thereareafewchangesthatweneedtomakenow.ThemostimportantofthemreferstotheAbstractController,theclassthatwillmakeheavyuseofthedependencyinjector.Addapropertynamed$ditothatclass,andreplacetheconstructorwiththefollowing:
publicfunction__construct(
DependencyInjector$di,
Request$request
){
$this->request=$request;
$this->di=$di;
$this->db=$di->get('PDO');
$this->log=$di->get('Logger');
$this->view=$di->get('Twig_Environment');
$this->config=$di->get('Utils\Config');
$this->customerId=$_COOKIE['id'];
}
TheotherchangesrefertotheRouterclass,aswearesendingitnowaspartoftheconstructor,andweneedtoinjectittothecontrollersthatwecreate.Adda$dipropertytothatclassaswell,andchangetheconstructortothefollowingone:
publicfunction__construct(DependencyInjector$di){
$this->di=$di;
$json=file_get_contents(__DIR__.'/../../config/routes.json');
$this->routeMap=json_decode($json,true);
}
AlsochangethecontentoftheexecuteControllerandroutemethods:
publicfunctionroute(Request$request):string{
$path=$request->getPath();
foreach($this->routeMapas$route=>$info){
$regexRoute=$this->getRegexRoute($route,$info);
if(preg_match("@^/$regexRoute$@",$path)){
return$this->executeController(
$route,$path,$info,$request
);
}
}
$errorController=newErrorController(
$this->di,
$request
);
return$errorController->notFound();
}
privatefunctionexecuteController(
string$route,
string$path,
array$info,
Request$request
):string{
$controllerName='\Bookstore\Controllers\\'
.$info['controller'].'Controller';
$controller=new$controllerName($this->di,$request);
if(isset($info['login'])&&$info['login']){
if($request->getCookies()->has('user')){
$customerId=$request->getCookies()->get('user');
$controller->setCustomerId($customerId);
}else{
$errorController=newCustomerController(
$this->di,
$request
);
return$errorController->login();
}
}
$params=$this->extractParams($route,$path);
returncall_user_func_array(
[$controller,$info['method']],$params
);
}
Thereisonelastplacethatyouneedtochange.TheloginmethodofCustomerControllerwasinstantiatingacontrollertoo,soweneedtoinjectthedependencyinjectorthereaswell:
$newController=newBookController($this->di,$this->request);
SummaryInthischapter,youlearnedwhatMVCis,andhowtowriteanapplicationthatfollowsthatpattern.Youalsoknowhowtousearoutertorouterequeststocontrollers,Twigtowritetemplates,andComposertomanageyourdependenciesandautoloader.Youwereintroducedtodependencyinjection,andyouevenbuiltyourownimplementation,eventhoughitisaverycontroversialtopicwithmanydifferentpointsofview.
Inthenextchapter,wewillgothroughoneofthemostimportantpartsneededwhenwritinggoodcodeandgoodapplications:unittestingyourcodetogetquickfeedbackfromit.
Chapter7.TestingWebApplicationsWeareprettysureyouhaveheardtheterm“bug”whenspeakingaboutapplications.Sentencessuchas“Wefoundabugintheapplicationthat…”followedbysomeveryundesirablebehavioraremorecommonthanyouthink.Writingcodeisnottheonlytaskofadeveloper;testingitiscrucialtoo.Youshouldnotreleaseaversionofyourapplicationthathasnotbeentested.However,couldyouimaginehavingtotestyourentireapplicationeverytimeyouchangealine?Itwouldbeanightmare!
Well,wearenotthefirstonestohavethisissue,so,luckilyenough,developershavealreadyfoundaprettygoodsolutiontothisproblem.Infact,theyfoundmorethanonesolution,turningtestingintoaveryhottopicofdiscussion.Evenbeingatestdeveloperhasbecomequiteacommonrole.Inthischapter,wewillintroduceyoutooneoftheapproachesoftestingyourcode:unittests.
Inthischapter,youwilllearnabout:
HowunittestsworkConfiguringPHPUnittotestyourcodeWritingtestswithassertions,dataproviders,andmocksGoodandbadpracticeswhenwritingunittests
ThenecessityfortestsWhenyouworkonaproject,chancesarethatyouarenottheonlydeveloperwhowillworkwiththiscode.Eveninthecasewhereyouaretheonlyonewhowilleverchangeit,ifyoudothisafewweeksaftercreatingit,youwillprobablynotrememberalltheplacesthatthispieceofcodeisaffected.Okay,let’sassumethatyouaretheonlydeveloperandyourmemoryisbeyondlimits;wouldyoubeabletoverifythatachangeonafrequentlyusedobject,suchasarequest,willalwaysworkasexpected?Moreimportantly,wouldyouliketodoiteverysingletimeyoumakeatinychange?
TypesoftestsWhilewritingyourapplication,makingchangestotheexistingcode,oraddingnewfeatures,itisveryimportanttogetgoodfeedback.Howdoyouknowthatthefeedbackyougetisgoodenough?ItshouldaccomplishtheAEIOUprinciples:
Automatic:Gettingthefeedbackshouldbeaspainlessaspossible.Gettingitbyrunningjustonecommandisalwayspreferabletohavingtotestyourapplicationmanually.Extensive:Weshouldbeabletocoverasmanyusecasesaspossible,includingedgecasesthataredifficulttoforeseewhenwritingcode.Immediate:Youshouldgetitassoonaspossible.Thismeansthatthefeedbackthatyougetjustafterintroducingachangeiswaybetterthanthefeedbackthatyougetafteryourcodeisinproduction.Open:Theresultsshouldbetransparent,andalso,thetestsshouldgiveusinsighttootherdevelopersastohowtointegrateoroperatewiththecode.Useful:Itshouldanswerquestionssuchas“Willthischangework?”,“Willitbreaktheapplicationunexpectedly?”,or“Isthereanyedgecasethatdoesnotworkproperly?”.
So,eventhoughtheconceptisquiteweirdatthebeginning,thebestwaytotestyourcodeis…withmorecode.Exactly!Wewillwritecodewiththegoaloftestingthecodeofourapplication.Why?Well,itisthebestwayweknowtosatisfyalltheAEIUprinciples,andithasthefollowingadvantages:
WecanexecutethetestsbyjustrunningonecommandfromourcommandlineorevenfromourfavoriteIDE.Thereisnoneedtomanuallytestyourapplicationviaabrowsercontinually.Weneedtowritethetestjustonce.Atthebeginning,itmaybeabitpainful,butoncethecodeiswritten,youwillnotneedtorepeatitagainandagain.Thismeansthataftersomework,wewillbeabletotesteverysinglecaseeffortlessly.Ifwehadtotestitmanually,alongwithalltheusecasesandedgecases,itwouldbeanightmare.Youdonotneedtohavethewholeapplicationworkinginordertoknowwhetheryourcodeworks.Imaginethatyouarewritingyourrouter:inordertoknowwhetheritworks,youwillhavetowaituntilyourapplicationworksinabrowser.Instead,youcanwriteyourtestsandrunthemassoonasyoufinishyourclass.Whenwritingyourtests,youwillbeprovidedwithfeedbackonwhatisfailing.Thisisveryusefultoknowwhenaspecificfunctionoftherouterdoesnotworkandthereasonforthefailure,whichisbetterthangettinga500erroronourbrowser.
Wehopethatbynowwehavesoldyouontheideathatwritingtestsisindispensable.Thiswastheeasypart,though.Theproblemisthatweknowseveraldifferentapproaches.Dowewriteteststhattesttheentireapplicationorteststhattestspecificparts?Doweisolatethetestedareafromtherest?Dowewanttointeractwiththedatabaseorwithotherexternalresourceswhiletesting?Dependingonyouranswers,youwilldecideonwhichtypeoftestsyouwanttowrite.Let’sdiscussthethreemainapproachesthatdevelopers
agreewith:
Unittests:Theseareteststhathaveaveryfocusedscope.Theiraimistotestasingleclassormethod,isolatingthemfromtherestofcode.TakeyourSaledomainclassasanexample:ithassomelogicregardingtheadditionofbooks,right?Aunittestmightjustinstantiateanewsale,addbookstotheobject,andverifythatthearrayofbooksisvalid.Unittestsaresuperfastduetotheirreducedscope,soyoucanhaveseveraldifferentscenariosofthesamefunctionalityeasily,coveringalltheedgecasesyoucanimagine.Theyarealsoisolated,whichmeansthatwewillnotcaretoomuchabouthowallthepiecesofourapplicationareintegrated.Instead,wewillmakesurethateachpieceworksperfectlyfine.Integrationtests:Thesearetestswithawiderscope.Theiraimistoverifythatallthepiecesofyourapplicationworktogether,sotheirscopeisnotlimitedtoaclassorfunctionbutratherincludesasetofclassesorthewholeapplication.Thereisstillsomeisolationincasewedonotwanttousearealdatabaseordependonsomeotherexternalwebservice.AnexampleinourapplicationwouldbetosimulateaRequestobject,sendittotherouter,andverifythattheresponseisasexpected.Acceptancetests:Thesearetestswithanevenwiderscope.Theytrytotestawholefunctionalityfromtheuser’spointofview.Inwebapplications,thismeansthatwecanlaunchabrowserandsimulatetheclicksthattheuserwouldmake,assertingtheresponseinthebrowsereachtime.Andyes,allofthisthroughcode!Thesetestsareslowertorun,asyoucanimagine,becausetheirscopeislargerandworkingwithabrowserslowsthemdownquitealottoo.
So,withallthesetypesoftests,whichoneshouldyouwrite?Theanswerisallofthem.Thetrickistoknowwhenandhowmanyofeachtypeyoushouldwrite.Onegoodapproachistowritealotofunittests,coveringabsolutelyeverythinginyourcode,thenwritingfewerintegrationteststomakesurethatallthecomponentsofyourapplicationworktogether,andfinallywritingacceptancetestsbuttestingonlythemainflowsofyourapplication.Thefollowingtestpyramidrepresentsthisidea:
Thereasonissimple:yourrealfeedbackwillcomefromyourunittests.Theywilltellyouifyoumessedupsomethingwithyourchangesassoonasyoufinishwritingthembecauseexecutingunittestsiseasyandfast.Onceyouknowthatallyourclassesandfunctions
behaveasexpected,youneedtoverifythattheycanworktogether.However,forthis,youdonotneedtotestalltheedgecasesagain;youalreadydidthiswhenwritingunittests.Here,youneedtowritejustafewintegrationteststhatconfirmthatallthepiecescommunicateproperly.Finally,tomakesurethatnotonlythatthecodeworksbutalsotheuserexperienceisthedesiredone,wewillwriteacceptanceteststhatemulateausergoingthroughthedifferentviews.Here,testsareveryslowandonlypossibleoncetheflowiscomplete,sothefeedbackcomeslater.Wewilladdacceptanceteststomakesurethatthemainflowswork,butwedonotneedtotesteverysinglescenarioaswealreadydidthiswithintegrationandunittests.
UnittestsandcodecoverageNowthatyouknowwhattestsare,whyweneedthem,andwhichtypesoftestswehave,wewillfocustherestofthechapteronwritinggoodunittestsastheywillbetheonesthatwilloccupymostofyourtime.
Asweexplainedbefore,theideaofaunittestistomakesurethatapieceofcode,usuallyaclassormethod,worksasexpected.Astheamountofcodethatamethodcontainsshouldbesmall,runningthetestshouldtakealmostnotime.Takingadvantageofthis,wewillrunseveraltests,tryingtocoverasmanyusecasesaspossible.
Ifthisisnotthefirsttimeyou’veheardaboutunittests,youmightknowtheconceptofcodecoverage.Thisconceptreferstotheamountofcodethatourtestsexecute,thatis,thepercentageoftestedcode.Forexample,ifyourapplicationhas10,000linesandyourteststestatotalof7,500lines,yourcodecoverageis75%.Therearetoolsthatshowmarksonyourcodetoindicatewhetheracertainlineistestedornot,whichisveryusefulinordertoidentifywhichpartsofyourapplicationarenottestedandthuswarnyouthatitismoredangeroustochangethem.
However,codecoverageisadouble-edgesword.Whyisthisso?Thisisbecausedeveloperstendtogetobsessedwithcodecoverage,aimingfora100%coverage.However,youshouldbeawarethatcodecoverageisjustaconsequence,notyourgoal.Yourgoalistowriteunitteststhatverifyalltheusecasesofcertainpiecesofcodeinordertomakeyoufeelsafereachtimethatyouhavetochangethiscode.Thismeansthatforagivenmethod,itmightnotbeenoughtowriteonetestbecausethesamelinewithdifferentinputvaluesmaybehavedifferently.However,ifyourfocuswasoncodecoverage,writingonetestwouldsatisfyit,andyoumightnotneedtowriteanymoretests.
IntegratingPHPUnitWritingtestsisataskthatyoucoulddobyyourself;youjustneedtowritecodethatthrowsexceptionswhenconditionsarenotmetandthenrunthescriptanytimeyouneed.Luckily,otherdeveloperswerenotsatisfiedwiththismanualprocess,sotheyimplementedtoolstohelpusautomatethisprocessandgetgoodfeedback.ThemostusedinPHPisPHPUnit.PHPUnitisaframeworkthatprovidesasetoftoolstowritetestsinaneasiermanner,givesustheabilitytoruntestsautomatically,anddeliversusefulfeedbacktothedeveloper.
InordertousePHPUnit,traditionally,weinstalleditonourlaptop.Indoingso,weaddedtheclassesoftheframeworktoincludethepathofPHPandalsotheexecutabletorunthetests.Thiswaslessthanidealasweforceddeveloperstoinstallonemoretoolontheirdevelopmentmachine.Nowadays,Composer(refertoChapter6,AdaptingtoMVC,inordertorefreshyourmemory)helpsusinincludingPHPUnitasadependencyoftheproject.ThismeansthatrunningComposer,whichyouwilldoforsureinordertogettherestofthedependencies,willgetPHPUnittoo.Add,then,thefollowingintocomposer.json:
{
//...
"require":{
"monolog/monolog":"^1.17",
"twig/twig":"^1.23"
},
"require-dev":{
"phpunit/phpunit":"5.1.3"
},
"autoload":{
"psr-4":{
"Bookstore\\":"src"
}
}
}
Notethatthisdependencyisaddedasrequire-dev.Thismeansthatthedependencywillbedownloadedonlywhenweareonadevelopmentenvironment,butitwillnotbepartoftheapplicationthatwewilldeployonproductionaswedonotneedtorunteststhere.Togetthedependency,asalways,runcomposerupdate.
AdifferentapproachistoinstallPHPUnitgloballysothatalltheprojectsonyourdevelopmentenvironmentcanuseitinsteadofinstallingitlocallyeachtime.YoucanreadabouthowtoinstalltoolsgloballywithComposerathttps://akrabat.com/global-installation-of-php-tools-with-composer/.
Thephpunit.xmlfilePHPUnitneedsaphpunit.xmlfileinordertodefinethewaywewanttorunthetests.Thisfiledefinesasetofruleslikewherethetestsare,whatcodearetheteststesting,andsoon.Addthefollowingfileinyourrootdirectory:
<?xmlversion="1.0"encoding="UTF-8"?>
<phpunitbackupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuitename="BookstoreTestSuite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src</directory>
</whitelist>
</filter>
</phpunit>
Thisfiledefinesquitealotofthings.Themostimportantareexplainedasfollows:
SettingconvertErrorsToExceptions,convertNoticesToExceptions,andconvertWarningsToExceptionstotruewillmakeyourtestsfailifthereisaPHPerror,warning,ornotice.Thegoalistomakesurethatyourcodedoesnotcontainminorerrorsonedgecases,whicharealwaysthesourceofpotentialproblems.ThestopOnFailuretellsPHPUnitwhetheritshouldcontinueexecutingtherestoftestsornotwhenthereisafailedtest.Inthiscase,wewanttorunallofthemtoknowhowmanytestsarefailingandwhy.Thebootstrapdefineswhichfileweshouldexecutebeforestartingtorunthetests.Themostcommonusageistoincludetheautoloader,butyoucouldalsoincludeafilethatinitializessomedependencies,suchasdatabasesorconfigurationreaders.ThetestsuitesdefinesthedirectorieswherePHPUnitwilllookfortests.Inourcase,wedefined./tests,butwecouldaddmoreifwehadthemindifferentdirectories.Thewhitelistdefinesthelistofdirectoriesthatcontainthecodethatwearetesting.Thiscanbeusefultogenerateoutputrelatedtothecodecoverage.
WhenrunningthetestswithPHPUnit,justmakesurethatyourunthecommandfromthesamedirectorywherethephpunit.xmlfileis.Wewillshowyouhowinthenextsection.
YourfirsttestRight,that’senoughpreparationsandtheory;let’swritesomecode.Wewillwritetestsforthebasiccustomer,whichisadomainobjectwithlittlelogic.Firstofall,weneedtorefactortheUniquetraitasitstillcontainssomeunnecessarycodeafterintegratingourapplicationwithMySQL.WearetalkingabouttheabilitytoassignthenextavailableID,whichisnowhandledbytheautoincrementalfield.Removeit,leavingthecodeasfollows:
<?php
namespaceBookstore\Utils;
traitUnique{
protected$id;
publicfunctionsetId(int$id){
$this->id=$id;
}
publicfunctiongetId():int{
return$this->id;
}
}
Thetestswillbeinsidethetests/directory.Thestructureofdirectoriesshouldbethesameasinthesrc/directorysothatitiseasiertoidentifywhereeachtestshouldbe.ThefileandtheclassnamesneedtoendwithTestsothatPHPUnitknowsthatafilecontainstests.Knowingthis,ourtestshouldbeintests/Domain/Customer/BasicTest.php,asfollows:
<?php
namespaceBookstore\Tests\Domain\Customer;
useBookstore\Domain\Customer\Basic;
usePHPUnit_Framework_TestCase;
classBasicTestextendsPHPUnit_Framework_TestCase{
publicfunctiontestAmountToBorrow(){
$customer=newBasic(1,'han','solo','han@solo.com');
$this->assertSame(
3,
$customer->getAmountToBorrow(),
'Basiccustomershouldborrowupto3books.'
);
}
}
Asyoucannote,theBasicTestclassextendsfromPHPUnit_Framework_TestCase.Alltestclasseshavetoextendfromthisclass.Thisclasscomeswithasetofmethodsthatallowyoutomakeassertions.AnassertioninPHPUnitisjustacheckperformedona
value.Assertionscanbecomparisonstoothervalues,averificationofsomeattributesofthevalues,andsoon.Ifanassertionisnottrue,thetestwillbemarkedasfailed,outputtingthepropererrormessagetothedeveloper.TheexampleshowsanassertionusingtheassertSamemethod,whichwillcomparetwovalues,expectingthatbothofthemareexactlythesame.Thethirdargumentisanerrormessagethattheassertionwillshowincaseitfails.
Also,notethatthefunctionnamesthatstartwithtestaretheonesexecutedwithPHPUnit.Inthisexample,wehaveoneuniquetestnamedtestAmountToBorrowthatinstantiatesabasiccustomerandverifiesthattheamountofbooksthatthecustomercanborrowis3.Inthenextsection,wewillshowyouhowtorunthistestandgetfeedbackfromit.
Optionally,youcoulduseanyfunctionnameifyouaddthe@testannotationinthemethod’sDocBlock,asfollows:
/**
*@test
*/
publicfunctionthisIsATestToo(){
//...
}
RunningtestsInordertorunthetestsyouwrote,youneedtoexecutethescriptthatComposergeneratedinvendor/bin.RememberalwaystorunfromtherootdirectoryoftheprojectsothatPHPUnitcanfindyourphpunit.xmlconfigurationfile.Then,type./vendor/bin/phpunit.
Whenexecutingthisprogram,wewillgetthefeedbackgivenbythetests.Theoutputshowsusthatthereisonetest(onemethod)andoneassertionandwhethertheseweresatisfactory.Thisoutputiswhatyouwouldliketoseeeverytimeyourunyourtests,butyouwillgetmorefailedteststhanyouwouldlike.Let’stakealookatthembyaddingthefollowingtest:
publicfunctiontestFail(){
$customer=newBasic(1,'han','solo','han@solo.com');
$this->assertSame(
4,
$customer->getAmountToBorrow(),
'Basiccustomershouldborrowupto3books.'
);
}
ThistestwillfailaswearecheckingwhethergetAmountToBorrowreturns4,butyouknowthatitalwaysreturns3.Let’srunthetestsandtakealookatwhatkindofoutputweget.
Wecanquicklynotethattheoutputisnotgoodduetotheredcolor.Itshowsusthatthereisafailure,pointingtotheclassandtestmethodthatfailed.Thefeedbackpointsoutthetypeoffailure(as3isnotidenticalto4)andoptionally,theerrormessageweaddedwheninvokingtheassertmethod.
WritingunittestsLet’sstartdiggingintoallthefeaturesthatPHPUnitoffersusinordertowritetests.Wewilldividethesefeaturesindifferentsubsections:settingupatest,assertions,exceptions,anddataproviders.Ofcourse,youdonotneedtouseallofthesetoolseachtimeyouwriteatest.
ThestartandendofatestPHPUnitgivesyoutheopportunitytosetupacommonscenarioforeachtestinaclass.Forthis,youneedtousethesetUpmethod,which,ifpresent,isexecutedeachtimethatatestofthisclassisexecuted.TheinstanceoftheclassthatinvokesthesetUpandtestmethodsisthesame,soyoucanusethepropertiesoftheclasstosavethecontext.Onecommonusewouldbetocreatetheobjectthatwewilluseforourtestsincasethisisalwaysthesame.Foranexample,writethefollowingcodeintests/Domain/Customer/BasicTest.php:
<?php
namespaceBookstore\Tests\Domain\Customer;
useBookstore\Domain\Customer\Basic;
usePHPUnit_Framework_TestCase;
classBasicTestextendsPHPUnit_Framework_TestCase{
private$customer;
publicfunctionsetUp(){
$this->customer=newBasic(
1,'han','solo','han@solo.com'
);
}
publicfunctiontestAmountToBorrow(){
$this->assertSame(
3,
$this->customer->getAmountToBorrow(),
'Basiccustomershouldborrowupto3books.'
);
}
}
WhentestAmountToBorrowisinvoked,the$customerpropertyisalreadyinitializedthroughtheexecutionofthesetUpmethod.Iftheclasshadmorethanonetest,thesetUpmethodwouldbeexecutedeachtime.
Eventhoughitislesscommontouse,thereisanothermethodusedtocleanupthescenarioafterthetestisexecuted:tearDown.Thisworksinthesameway,butitisexecutedaftereachtestofthisclassisexecuted.Possibleuseswouldbetocleanupdatabasedata,closeconnections,deletefiles,andsoon.
AssertionsYouhavealreadybeenintroducedtotheconceptofassertions,solet’sjustlistthemostcommononesinthissection.Forthefulllist,werecommendyoutovisittheofficialdocumentationathttps://phpunit.de/manual/current/en/appendixes.assertions.htmlasitisquiteextensive;however,tobehonest,youwillprobablynotusemanyofthem.
ThefirsttypeofassertionthatwewillseeistheBooleanassertion,thatis,theonethatcheckswhetheravalueistrueorfalse.ThemethodsareassimpleasassertTrueandassertFalse,andtheyexpectoneparameter,whichisthevaluetoassert,andoptionally,atexttodisplayincaseoffailure.InthesameBasicTestclass,addthefollowingtest:
publicfunctiontestIsExemptOfTaxes(){
$this->assertFalse(
$this->customer->isExemptOfTaxes(),
'Basiccustomershouldbeexemptoftaxes.'
);
}
Thistestmakessurethatabasiccustomerisneverexemptoftaxes.Notethatwecoulddothesameassertionbywritingthefollowing:
$this->assertSame(
$this->customer->isExemptOfTaxes(),
false,
'Basiccustomershouldbeexemptoftaxes.'
);
Asecondgroupofassertionswouldbethecomparisonassertions.ThemostfamousonesareassertSameandassertEquals.Youhavealreadyusedthefirstone,butareyousureofitsmeaning?Let’saddanothertestandrunit:
publicfunctiontestGetMonthlyFee(){
$this->assertSame(
5,
$this->customer->getMonthlyFee(),
'Basiccustomershouldpay5amonth.'
);
}
Theresultofthetestisshowninthefollowingscreenshot:
Thetestfailed!ThereasonisthatassertSameistheequivalenttocomparingusingidentity,thatis,withoutusingtypejuggling.TheresultofthegetMonthlyFeemethodisalwaysafloat,andwewillcompareitwithaninteger,soitwillneverbethesame,astheerrormessagetellsus.ChangetheassertiontoassertEquals,whichcomparesusingequality,andthetestwillpassnow.
Whenworkingwithobjects,wecanuseanassertiontocheckwhetheragivenobjectisaninstanceoftheexpectedclassornot.Whendoingso,remembertosendthefullnameoftheclassasthisisaquitecommonmistake.Evenbetter,youcouldgettheclassnameusing::class,forexample,Basic::class.Addthefollowingtestintests/Domain/Customer/CustomerFactoryTest.php:
<?php
namespaceBookstore\Tests\Domain\Customer;
useBookstore\Domain\Customer\CustomerFactory;
usePHPUnit_Framework_TestCase;
classCustomerFactoryTestextendsPHPUnit_Framework_TestCase{
publicfunctiontestFactoryBasic(){
$customer=CustomerFactory::factory(
'basic',1,'han','solo','han@solo.com'
);
$this->assertInstanceOf(
Basic::class,
$customer,
'basicshouldcreateaCustomer\Basicobject.'
);
}
}
Thistestcreatesacustomerusingthecustomerfactory.Asthetypeofcustomerwasbasic,theresultshouldbeaninstanceofBasic,whichiswhatwearetestingwithassertInstanceOf.Thefirstargumentistheexpectedclass,thesecondistheobjectthatwearetesting,andthethirdistheerrormessage.Thistestalsohelpsustonotethebehaviorofcomparisonassertionswithobjects.Let’screateabasiccustomerobjectasexpectedandcompareitwiththeresultofthefactory.Then,runthetest,asfollows:
$expectedBasicCustomer=newBasic(1,'han','solo','han@solo.com');
$this->assertSame(
$customer,
$expectedBasicCustomer,
'Customerobjectisnotasexpected.'
);
Theresultofthistestisshowninthefollowingscreenshot:
Thetestfailedbecausewhenyoucomparetwoobjectswithidentitycomparison,youcomparingtheobjectreference,anditwillonlybethesameifthetwoobjectsareexactlythesameinstance.Ifyoucreatetwoobjectswiththesameproperties,theywillbeequalbutneveridentical.Tofixthetest,changetheassertionasfollows:
$expectedBasicCustomer=newBasic(1,'han','solo','han@solo.com');
$this->assertEquals(
$customer,
$expectedBasicCustomer,
'Customerobjectisnotasexpected.'
);
Let’snowwritethetestsforthesaledomainobjectattests/Domain/SaleTest.php.Thisclassisveryeasytotestandallowsustousesomenewassertions,asfollows:
<?php
namespaceBookstore\Tests\Domain\Customer;
useBookstore\Domain\Sale;
usePHPUnit_Framework_TestCase;
classSaleTestextendsPHPUnit_Framework_TestCase{
publicfunctiontestNewSaleHasNoBooks(){
$sale=newSale();
$this->assertEmpty(
$sale->getBooks(),
'Whennew,saleshouldhavenobooks.'
);
}
publicfunctiontestAddNewBook(){
$sale=newSale();
$sale->addBook(123);
$this->assertCount(
1,
$sale->getBooks(),
'Numberofbooksnotvalid.'
);
$this->assertArrayHasKey(
123,
$sale->getBooks(),
'Bookidcouldnotbefoundinarray.'
);
$this->assertSame(
$sale->getBooks()[123],
1,
'Whennotspecified,amountofbooksis1.'
);
}
}
Weaddedtwotestshere:onemakessurethatforanewsaleinstance,thelistofbooksassociatedwithitisempty.Forthis,weusedtheassertEmptymethod,whichtakesanarrayasanargumentandwillassertthatitisempty.Thesecondtestisaddingabooktothesaleandthenmakingsurethatthelistofbookshasthecorrectcontent.Forthis,wewillusetheassertCountmethod,whichverifiesthatthearray,thatis,thesecondargument,hasasmanyelementsasthefirstargumentprovided.Inthiscase,weexpectthatthelistofbookshasonlyoneentry.Thesecondassertionofthistestisverifyingthatthearrayofbookscontainsaspecifickey,whichistheIDofthebook,withtheassertArrayHasKeymethod,inwhichthefirstargumentisthekey,andthesecondoneisthearray.Finally,wewillcheckwiththealreadyknownassertSamemethodthatthe
amountofbooksinsertedis1.
Eventhoughthesetwonewassertionmethodsareusefulsometimes,allthethreeassertionsofthelasttestcanbereplacedbyjustanassertSamemethod,comparingthewholearrayofbookswiththeexpectedone,asfollows:
$this->assertSame(
[123=>1],
$sale->getBooks(),
'Booksarraydoesnotmatch.'
);
Thesuiteoftestsforthesaledomainobjectwouldnotbeenoughifwewerenottestinghowtheclassbehaveswhenaddingmultiplebooks.Inthiscase,usingassertCountandassertArrayHasKeywouldmakethetestunnecessarilylong,solet’sjustcomparethearraywithanexpectedoneviathefollowingcode:
publicfunctiontestAddMultipleBooks(){
$sale=newSale();
$sale->addBook(123,4);
$sale->addBook(456,2);
$sale->addBook(456,8);
$this->assertSame(
[123=>4,456=>10],
$sale->getBooks(),
'Booksarenotasexpected.'
);
}
ExpectingexceptionsSometimes,amethodisexpectedtothrowanexceptionforcertainunexpectedusecases.Whenthishappens,youcouldtrytocapturethisexceptioninsidethetestortakeadvantageofanothertoolthatPHPUnitoffers:expectingexceptions.Tomarkatesttoexpectagivenexception,justaddthe@expectedExceptionannotationfollowedbytheexception’sclassfullname.Optionally,youcanuse@expectedExceptionMessagetoassertthemessageoftheexception.Let’saddthefollowingteststoourCustomerFactoryTestclass:
/**
*@expectedException\InvalidArgumentException
*@expectedExceptionMessageWrongtype.
*/
publicfunctiontestCreatingWrongTypeOfCustomer(){
$customer=CustomerFactory::factory(
'deluxe',1,'han','solo','han@solo.com'
);
}
Inthistestwewilltrytocreateadeluxecustomerwithourfactory,butasthistypeofcustomerdoesnotexist,wewillgetanexception.ThetypeoftheexpectedexceptionisInvalidArgumentException,andtheerrormessageis“Wrongtype”.Ifyourunthetests,youwillseethattheypass.
Ifwedefinedanexpectedexceptionandtheexceptionisneverthrown,thetestwillfail;expectingexceptionsisjustanothertypeofassertion.Toseethishappen,addthefollowingtoyourtestandrunit;youwillgetafailure,andPHPUnitwillcomplainsayingthatitexpectedtheexception,butitwasneverthrown:
/**
*@expectedException\InvalidArgumentException
*/
publicfunctiontestCreatingCorrectCustomer(){
$customer=CustomerFactory::factory(
'basic',1,'han','solo','han@solo.com'
);
}
DataprovidersIfyouthinkabouttheflowofatest,mostofthetime,weinvokeamethodwithaninputandexpectanoutput.Inordertocoveralltheedgecases,itisnaturalthatwewillrepeatthesameactionwithasetofinputsandexpectedoutputs.PHPUnitgivesustheabilitytodoso,thusremovingalotofduplicatedcode.Thisfeatureiscalleddataproviding.
Adataproviderisapublicmethoddefinedinthetestclassthatreturnsanarraywithaspecificschema.Eachentryofthearrayrepresentsatestinwhichthekeyisthenameofthetest—optionally,youcouldusenumerickeys—andthevalueistheparameterthatthetestneeds.Atestwilldeclarethatitneedsadataproviderwiththe@dataProviderannotation,andwhenexecutingtests,thedataproviderinjectstheargumentsthatthetestmethodneeds.Let’sconsideranexampletomakeiteasier.WritethefollowingtwomethodsinyourCustomerFactoryTestclass:
publicfunctionproviderFactoryValidCustomerTypes(){
return[
'Basiccustomer,lowercase'=>[
'type'=>'basic',
'expectedType'=>'\Bookstore\Domain\Customer\Basic'
],
'Basiccustomer,uppercase'=>[
'type'=>'BASIC',
'expectedType'=>'\Bookstore\Domain\Customer\Basic'
],
'Premiumcustomer,lowercase'=>[
'type'=>'premium',
'expectedType'=>'\Bookstore\Domain\Customer\Premium'
],
'Premiumcustomer,uppercase'=>[
'type'=>'PREMIUM',
'expectedType'=>'\Bookstore\Domain\Customer\Premium'
]
];
}
/**
*@dataProviderproviderFactoryValidCustomerTypes
*@paramstring$type
*@paramstring$expectedType
*/
publicfunctiontestFactoryValidCustomerTypes(
string$type,
string$expectedType
){
$customer=CustomerFactory::factory(
$type,1,'han','solo','han@solo.com'
);
$this->assertInstanceOf(
$expectedType,
$customer,
'Factorycreatedthewrongtypeofcustomer.'
);
}
ThetesthereistestFactoryValidCustomerTypes,whichexpectstwoarguments:$typeand$expectedType.Thetestusesthemtocreateacustomerwiththefactoryandverifythetypeoftheresult,whichwealreadydidbyhardcodingthetypes.ThetestalsodeclaresthatitneedstheproviderFactoryValidCustomerTypesdataprovider.Thisdataproviderreturnsanarrayoffourentries,whichmeansthatthetestwillbeexecutedfourtimeswithfourdifferentsetsofarguments.Thenameofeachtestisthekeyofeachentry—forexample,“Basiccustomer,lowercase”.Thisisveryusefulincaseatestfailsbecauseitwillbedisplayedaspartoftheerrormessages.Eachentryisamapwithtwovalues,typeandexpectedType,whicharethenamesoftheargumentsofthetestmethod.Thevaluesoftheseentriesarethevaluesthatthetestmethodwillget.
ThebottomlineisthatthecodewewrotewouldbethesameasifwewrotetestFactoryValidCustomerTypesfourtimes,hardcoding$typeand$expectedTypeeachtime.Imaginenowthatthetestmethodcontainstensoflinesofcodeorwewanttorepeatthesametestwithtensofdatasets;doyouseehowpowerfulitis?
TestingwithdoublesSofar,wetestedclassesthatarequiteisolated;thatis,theydonothavemuchinteractionwithotherclasses.Nevertheless,wehaveclassesthatuseseveralclasses,suchascontrollers.Whatcanwedowiththeseinteractions?Theideaofunittestsistotestaspecificmethodandnotthewholecodebase,right?
PHPUnitallowsyoutomockthesedependencies;thatis,youcanprovidefakeobjectsthatlooksimilartothedependenciesthatthetestedclassneeds,buttheydonotusecodefromthoseclasses.Thegoalofthisistoprovideadummyinstancethattheclasscanuseandinvokeitsmethodswithoutthesideeffectofwhattheseinvocationsmighthave.Imagineasanexamplethecaseofthemodels:ifthecontrollerusesarealmodel,thenwheninvokingmethodsfromit,themodelwouldaccessthedatabaseeachtime,makingthetestsquiteunpredictable.
Ifweuseamockasthemodelinstead,thecontrollercaninvokeitsmethodsasmanytimesasneededwithoutanysideeffect.Evenbetter,wecanmakeassertionsoftheargumentsthatthemockreceivedorforceittoreturnspecificvalues.Let’stakealookathowtousethem.
InjectingmodelswithDIThefirstthingweneedtounderstandisthatifwecreateobjectsusingnewinsidethecontroller,wewillnotbeabletomockthem.Thismeansthatweneedtoinjectallthedependencies—forexample,usingadependencyinjector.Wewilldothisforallofthedependenciesbutone:themodels.Inthissection,wewilltesttheborrowmethodoftheBookControllerclass,sowewillshowthechangesthatthismethodneeds.Ofcourse,ifyouwanttotesttherestofthecode,youshouldapplythesesamechangestotherestofthecontrollers.
ThefirstthingtodoistoaddtheBookModelinstancetothedependencyinjectorinourindex.phpfile.Asthisclassalsohasadependency,PDO,usethesamedependencyinjectortogetaninstanceofit,asfollows:
$di->set('BookModel',newBookModel($di->get('PDO')));
Now,intheborrowmethodoftheBookControllerclass,wewillchangethenewinstantiationofthemodeltothefollowing:
publicfunctionborrow(int$bookId):string{
$bookModel=$this->di->get('BookModel');
try{
//...
CustomizingTestCaseWhenwritingyourunittest’ssuite,itisquitecommontohaveacustomizedTestCaseclassfromwhichalltestsextend.ThisclassalwaysextendsfromPHPUnit_Framework_TestCase,sowestillgetalltheassertionsandothermethods.Asalltestshavetoimportthisclass,let’schangeourautoloadersothatitcanrecognizenamespacesfromthetestsdirectory.Afterthis,runcomposerupdate,asfollows:
"autoload":{
"psr-4":{
"Bookstore\\Tests\\":"tests",
"Bookstore\\":"src"
}
}
Withthischange,wewilltellComposerthatallthenamespacesstartingwithBookstore\Testswillbelocatedunderthetestsdirectory,andtherestwillfollowthepreviousrules.
Let’saddnowourcustomizedTestCaseclass.Theonlyhelpermethodweneedrightnowisonetocreatemocks.Itisnotreallynecessary,butitmakesthingscleaner.Addthefollowingclassintests/AbstractTestClase.php:
<?php
namespaceBookstore\Tests;
usePHPUnit_Framework_TestCase;
useInvalidArgumentException;
abstractclassAbstractTestCaseextendsPHPUnit_Framework_TestCase{
protectedfunctionmock(string$className){
if(strpos($className,'\\')!==0){
$className='\\'.$className;
}
if(!class_exists($className)){
$className='\Bookstore\\'.trim($className,'\\');
if(!class_exists($className)){
thrownewInvalidArgumentException(
"Class$classNamenotfound."
);
}
}
return$this->getMockBuilder($className)
->disableOriginalConstructor()
->getMock();
}
}
ThismethodtakesthenameofaclassandtriestofigureoutwhethertheclassispartoftheBookstorenamespaceornot.Thiswillbehandywhenmockingobjectsofourown
codebaseaswewillnothavetowriteBookstoreeachtime.Afterfiguringoutwhattherealfullclassnameis,itusesthemockbuilderfromPHPUnittocreateoneandthenreturnsit.
Morehelpers!Thistime,theyareforcontrollers.Everysinglecontrollerwillalwaysneedthesamedependencies:logger,databaseconnection,templateengine,andconfigurationreader.Knowingthis,let’screateaControllerTestCaseclassfromwhereallthetestscoveringcontrollerswillextend.ThisclasswillcontainasetUpmethodthatcreatesallthecommonmocksandsetstheminthedependencyinjector.Additasyourtests/ControllerTestCase.phpfile,asfollows:
<?php
namespaceBookstore\Tests;
useBookstore\Utils\DependencyInjector;
useBookstore\Core\Config;
useMonolog\Logger;
useTwig_Environment;
usePDO;
abstractclassControllerTestCaseextendsAbstractTestCase{
protected$di;
publicfunctionsetUp(){
$this->di=newDependencyInjector();
$this->di->set('PDO',$this->mock(PDO::class));
$this->di->set('Utils\Config',$this->mock(Config::class));
$this->di->set(
'Twig_Environment',
$this->mock(Twig_Environment::class)
);
$this->di->set('Logger',$this->mock(Logger::class));
}
}
UsingmocksWell,we’vehadenoughofthehelpers;let’sstartwiththetests.Thedifficultparthereishowtoplaywithmocks.Whenyoucreateone,youcanaddsomeexpectationsandreturnvalues.Themethodsare:
expects:Thisspecifiestheamountoftimesthemock’smethodisinvoked.Youcansend$this->never(),$this->once(),or$this->any()asanargumenttospecify0,1,oranyinvocations.method:Thisisusedtospecifythemethodwearetalkingabout.Theargumentthatitexpectsisjustthenameofthemethod.with:Thisisamethodusedtosettheexpectationsoftheargumentsthatthemockwillreceivewhenitisinvoked.Forexample,ifthemockedmethodisexpectedtogetbasicasthefirstargumentand123asthesecond,thewithmethodwillbeinvokedaswith("basic",123).Thismethodisoptional,butifwesetit,PHPUnitwillthrowanerrorincasethemockedmethoddoesnotgettheexpectedarguments,soitworksasanassertion.will:Thisisusedtodefinewhatthemockwillreturn.Thetwomostcommonusagesare$this->returnValue($value)or$this->throwException($exception).Thismethodisalsooptional,andifnotinvoked,themockwillalwaysreturnnull.
Let’saddthefirsttesttoseehowitwouldwork.Addthefollowingcodetothetests/Controllers/BookControllerTest.phpfile:
<?php
namespaceBookstore\Tests\Controllers;
useBookstore\Controllers\BookController;
useBookstore\Core\Request;
useBookstore\Exceptions\NotFoundException;
useBookstore\Models\BookModel;
useBookstore\Tests\ControllerTestCase;
useTwig_Template;
classBookControllerTestextendsControllerTestCase{
privatefunctiongetController(
Request$request=null
):BookController{
if($request===null){
$request=$this->mock('Core\Request');
}
returnnewBookController($this->di,$request);
}
publicfunctiontestBookNotFound(){
$bookModel=$this->mock(BookModel::class);
$bookModel
->expects($this->once())
->method('get')
->with(123)
->will(
$this->throwException(
newNotFoundException()
)
);
$this->di->set('BookModel',$bookModel);
$response="Renderedtemplate";
$template=$this->mock(Twig_Template::class);
$template
->expects($this->once())
->method('render')
->with(['errorMessage'=>'Booknotfound.'])
->will($this->returnValue($response));
$this->di->get('Twig_Environment')
->expects($this->once())
->method('loadTemplate')
->with('error.twig')
->will($this->returnValue($template));
$result=$this->getController()->borrow(123);
$this->assertSame(
$result,
$response,
'Responseobjectisnottheexpectedone.'
);
}
}
ThefirstthingthetestdoesistocreateamockoftheBookModelclass.Then,itaddsanexpectationthatgoeslikethis:thegetmethodwillbecalledoncewithoneargument,123,anditwillthrowNotFoundException.Thismakessenseasthetesttriestoemulateascenarioinwhichwecannotfindthebookinthedatabase.
Thesecondpartofthetestconsistsofaddingtheexpectationsofthetemplateengine.Thisisabitmorecomplexastherearetwomocksinvolved.TheloadTemplatemethodofTwig_Environmentisexpectedtobecalledoncewiththeerror.twigargumentasthetemplatename.ThismockshouldreturnTwig_Template,whichisanothermock.Therendermethodofthissecondmockisexpectedtobecalledoncewiththecorrecterrormessage,returningtheresponse,whichisahardcodedstring.Afterallthedependenciesaredefined,wejustneedtoinvoketheborrowmethodofthecontrollerandexpectaresponse.
Rememberthatthistestdoesnothaveonlyoneassertion,butfour:theassertSamemethodandthethreemockexpectations.Ifanyofthemarenotaccomplished,thetestwillfail,sowecansaythatthismethodisquiterobust.
Withourfirsttest,weverifiedthatthescenarioinwhichthebookisnotfoundworks.Therearetwomorescenariosthatfailaswell:whentherearenotenoughcopiesofthebooktoborrowandwhenthereisadatabaseerrorwhentryingtosavetheborrowedbook.However,youcanseenowthatallofthemshareapieceofcodethatmocksthetemplate.Let’sextractthiscodetoaprotectedmethodthatgeneratesthemockswhenitisgiven
thetemplatename,theparametersaresenttothetemplate,andtheexpectedresponseisreceived.Runthefollowing:
protectedfunctionmockTemplate(
string$templateName,
array$params,
$response
){
$template=$this->mock(Twig_Template::class);
$template
->expects($this->once())
->method('render')
->with($params)
->will($this->returnValue($response));
$this->di->get('Twig_Environment')
->expects($this->once())
->method('loadTemplate')
->with($templateName)
->will($this->returnValue($template));
}
publicfunctiontestNotEnoughCopies(){
$bookModel=$this->mock(BookModel::class);
$bookModel
->expects($this->once())
->method('get')
->with(123)
->will($this->returnValue(newBook()));
$bookModel
->expects($this->never())
->method('borrow');
$this->di->set('BookModel',$bookModel);
$response="Renderedtemplate";
$this->mockTemplate(
'error.twig',
['errorMessage'=>'Therearenocopiesleft.'],
$response
);
$result=$this->getController()->borrow(123);
$this->assertSame(
$result,
$response,
'Responseobjectisnottheexpectedone.'
);
}
publicfunctiontestErrorSaving(){
$controller=$this->getController();
$controller->setCustomerId(9);
$book=newBook();
$book->addCopy();
$bookModel=$this->mock(BookModel::class);
$bookModel
->expects($this->once())
->method('get')
->with(123)
->will($this->returnValue($book));
$bookModel
->expects($this->once())
->method('borrow')
->with(newBook(),9)
->will($this->throwException(newDbException()));
$this->di->set('BookModel',$bookModel);
$response="Renderedtemplate";
$this->mockTemplate(
'error.twig',
['errorMessage'=>'Errorborrowingbook.'],
$response
);
$result=$controller->borrow(123);
$this->assertSame(
$result,
$response,
'Responseobjectisnottheexpectedone.'
);
}
Theonlynoveltyhereiswhenweexpectthattheborrowmethodisneverinvoked.Aswedonotexpectittobeinvoked,thereisnoreasontousethewithnorwillmethod.Ifthecodeactuallyinvokesthismethod,PHPUnitwillmarkthetestasfailed.
Wealreadytestedandfoundthatallthescenariosthatcanfailhavefailed.Let’saddatestnowwhereausercansuccessfullyborrowabook,whichmeansthatwewillreturnvalidbooksandcustomersfromthedatabase,thesavemethodwillbeinvokedcorrectly,andthetemplatewillgetallthecorrectparameters.Thetestlooksasfollows:
publicfunctiontestBorrowingBook(){
$controller=$this->getController();
$controller->setCustomerId(9);
$book=newBook();
$book->addCopy();
$bookModel=$this->mock(BookModel::class);
$bookModel
->expects($this->once())
->method('get')
->with(123)
->will($this->returnValue($book));
$bookModel
->expects($this->once())
->method('borrow')
->with(newBook(),9);
$bookModel
->expects($this->once())
->method('getByUser')
->with(9)
->will($this->returnValue(['book1','book2']));
$this->di->set('BookModel',$bookModel);
$response="Renderedtemplate";
$this->mockTemplate(
'books.twig',
[
'books'=>['book1','book2'],
'currentPage'=>1,
'lastPage'=>true
],
$response
);
$result=$controller->borrow(123);
$this->assertSame(
$result,
$response,
'Responseobjectisnottheexpectedone.'
);
}
Sothisisit.Youhavewrittenoneofthemostcomplextestsyouwillneedtowriteduringthisbook.Whatdoyouthinkofit?Well,asyoudonothavemuchexperiencewithtests,youmightbequitesatisfiedwiththeresult,butlet’strytoanalyzeitabitfurther.
DatabasetestingThiswillbethemostcontroversialofthesectionsofthischapterbyfar.Whenitcomestodatabasetesting,therearedifferentschoolsofthought.Shouldweusethedatabaseornot?Shouldweuseourdevelopmentdatabaseoroneinmemory?Itisquiteoutofthescopeofthebooktoexplainhowtomockthedatabaseorprepareafreshoneforeachtest,butwewilltrytosummarizesomeofthetechniqueshere:
Wewillmockthedatabaseconnectionandwriteexpectationstoalltheinteractionsbetweenthemodelandthedatabase.Inourcase,thiswouldmeanthatwewouldinjectamockofthePDOobject.Aswewillwritethequeriesmanually,chancesarethatwemightintroduceawrongquery.Mockingtheconnectionwouldnothelpusdetectthiserror.ThissolutionwouldbegoodifweusedORMinsteadofwritingthequeriesmanually,butwewillleavethistopicoutofthebook.Foreachtest,wewillcreateabrandnewdatabaseinwhichweaddthedatawewouldliketohaveforthespecifictest.Thisapproachmighttakealotoftime,butitassuresyouthatyouwillbetestingagainstarealdatabaseandthatthereisnounexpecteddatathatmightmakeourtestsfail;thatis,thetestsarefullyisolated.Inmostofthecases,thiswouldbethepreferableapproach,eventhoughitmightnotbetheonethatperformsfaster.Tosolvethisinconvenience,wewillcreatein-memorydatabases.Testsrunagainstanalreadyexistingdatabase.Usually,atthebeginningofthetestwestartatransactionthatwerollbackattheendofthetest,leavingthedatabasewithoutanychange.Thisapproachemulatesarealscenario,inwhichwecanfindallsortsofdataandourcodeshouldalwaysbehaveasexpected.However,usingashareddatabasealwayshassomesideeffects;forexample,ifwewanttointroducechangestothedatabaseschema,wewillhavetoapplythemtothedatabasebeforerunningthetests,buttherestoftheapplicationsordevelopersthatusethedatabasearenotyetreadyforthesechanges.
Inordertokeepthingssmall,wewilltrytoimplementamixtureofthesecondandthirdoptions.Wewilluseourexistingdatabase,butafterstartingthetransactionofeachtest,wewillcleanallthetablesinvolvedwiththetest.ThislooksasthoughweneedaModelTestCasetohandlethis.Addthefollowingintotests/ModelTestCase.php:
<?php
namespaceBookstore\Tests;
useBookstore\Core\Config;
usePDO;
abstractclassModelTestCaseextendsAbstractTestCase{
protected$db;
protected$tables=[];
publicfunctionsetUp(){
$config=newConfig();
$dbConfig=$config->get('db');
$this->db=newPDO(
'mysql:host=127.0.0.1;dbname=bookstore',
$dbConfig['user'],
$dbConfig['password']
);
$this->db->beginTransaction();
$this->cleanAllTables();
}
publicfunctiontearDown(){
$this->db->rollBack();
}
protectedfunctioncleanAllTables(){
foreach($this->tablesas$table){
$this->db->exec("deletefrom$table");
}
}
}
ThesetUpmethodcreatesadatabaseconnectionwiththesamecredentialsfoundintheconfig/app.ymlfile.Then,wewillstartatransactionandinvokethecleanAllTablesmethod,whichiteratesthetablesinthe$tablespropertyanddeletesallthecontentfromthem.ThetearDownmethodrollsbackthetransaction.
NoteExtendingfromModelTestCase
IfyouwriteatestextendingfromthisclassthatneedstoimplementeitherthesetUportearDownmethod,alwaysremembertoinvoketheonesfromtheparent.
Let’swritetestsfortheborrowmethodoftheBookModelclass.Thismethodusesbooksandcustomers,sowewouldliketocleanthetablesthatcontainthem.Createthetestclassandsaveitintests/Models/BookModelTest.php:
<?php
namespaceBookstore\Tests\Models;
useBookstore\Models\BookModel;
useBookstore\Tests\ModelTestCase;
classBookModelTestextendsModelTestCase{
protected$tables=[
'borrowed_books',
'customer',
'book'
];
protected$model;
publicfunctionsetUp(){
parent::setUp();
$this->model=newBookModel($this->db);
}
}
NotehowwealsooverrodethesetUpmethod,invokingtheoneintheparentandcreatingthemodelinstancethatalltestswilluse,whichissafetodoaswewillnotkeepanycontextonthisobject.Beforeaddingtheteststhough,let’saddsomemorehelperstoModelTestCase:onetocreatebookobjectsgivenanarrayofparametersandtwotosavebooksandcustomersinthedatabase.Runthefollowingcode:
protectedfunctionbuildBook(array$properties):Book{
$book=newBook();
$reflectionClass=newReflectionClass(Book::class);
foreach($propertiesas$key=>$value){
$property=$reflectionClass->getProperty($key);
$property->setAccessible(true);
$property->setValue($book,$value);
}
return$book;
}
protectedfunctionaddBook(array$params){
$default=[
'id'=>null,
'isbn'=>'isbn',
'title'=>'title',
'author'=>'author',
'stock'=>1,
'price'=>10.0,
];
$params=array_merge($default,$params);
$query=<<<SQL
insertintobook(id,isbn,title,author,stock,price)
values(:id,:isbn,:title,:author,:stock,:price)
SQL;
$this->db->prepare($query)->execute($params);
}
protectedfunctionaddCustomer(array$params){
$default=[
'id'=>null,
'firstname'=>'firstname',
'surname'=>'surname',
'email'=>'email',
'type'=>'basic'
];
$params=array_merge($default,$params);
$query=<<<SQL
insertintocustomer(id,firstname,surname,email,type)
values(:id,:firstname,:surname,:email,:type)
SQL;
$this->db->prepare($query)->execute($params);
}
Asyoucannote,weaddeddefaultvaluesforallthefields,sowearenotforcedtodefinethewholebook/customereachtimewewanttosaveone.Instead,wejustsenttherelevantfieldsandmergedthemtothedefaultones.
Also,notethatthebuildBookmethodusedanewconcept,reflection,toaccesstheprivatepropertiesofaninstance.Thisiswaybeyondthescopeofthebook,butifyouareinterested,youcanreadmoreathttp://php.net/manual/en/book.reflection.php.
Wearenowreadytostartwritingtests.Withallthesehelpers,addingtestswillbeveryeasyandclean.Theborrowmethodhasdifferentusecases:tryingtoborrowabookthatisnotinthedatabase,tryingtouseacustomernotregistered,andborrowingabooksuccessfully.Let’saddthemasfollows:
/**
*@expectedException\Bookstore\Exceptions\DbException
*/
publicfunctiontestBorrowBookNotFound(){
$book=$this->buildBook(['id'=>123]);
$this->model->borrow($book,123);
}
/**
*@expectedException\Bookstore\Exceptions\DbException
*/
publicfunctiontestBorrowCustomerNotFound(){
$book=$this->buildBook(['id'=>123]);
$this->addBook(['id'=>123]);
$this->model->borrow($book,123);
}
publicfunctiontestBorrow(){
$book=$this->buildBook(['id'=>123,'stock'=>12]);
$this->addBook(['id'=>123,'stock'=>12]);
$this->addCustomer(['id'=>123]);
$this->model->borrow($book,123);
}
Impressed?Comparedtothecontrollertests,thesetestsarewaysimpler,mainlybecausetheircodeperformsonlyoneaction,butalsothankstoallthemethodsaddedtoModelTestCase.Onceyouneedtoworkwithotherobjects,suchassales,youcanaddaddSaleorbuildSaletothissameclasstomakethingscleaner.
Test-drivendevelopmentYoumightrealizealreadythatthereisnouniquewaytodothingswhentalkingaboutdevelopinganapplication.Itisoutofthescopeofthisbooktoshowyouallofthem—andbythetimeyouaredonereadingtheselines,moretechniqueswillhavebeenincorporatedalready—butthereisoneapproachthatisveryusefulwhenitcomestowritinggood,testablecode:test-drivendevelopment(TDD).
Thismethodologyconsistsofwritingtheunittestsbeforewritingthecodeitself.Theidea,though,isnottowriteallthetestsatonceandthenwritetheclassormethodbutrathertodoitinaprogressiveway.Let’sconsideranexampletomakeiteasier.ImaginethatyourSaleclassisyettobeimplementedandtheonlythingweknowisthatwehavetobeabletoaddbooks.Renameyoursrc/Domain/Sale.phpfiletosrc/Domain/Sale2.phporjustdeleteitsothattheapplicationdoesnotknowaboutit.
NoteIsallthisverbositynecessary?
Youwillnoteinthisexamplethatwewillperformanexcessiveamountofstepstocomeupwithaverysimplepieceofcode.Indeed,theyaretoomanyforthisexample,buttherewillbetimeswhenthisamountisjustfine.Findingthesemomentscomeswithexperience,sowerecommendyoutopracticefirstwithsimpleexamples.Eventually,itwillcomenaturallytoyou.
ThemechanicsofTDDconsistoffoursteps,asfollows:
1. Writeatestforsomefunctionalitythatisnotyetimplemented.2. Runtheunittests,andtheyshouldfail.Iftheydonot,eitheryourtestiswrong,or
yourcodealreadyimplementsthisfunctionality.3. Writetheminimumamountofcodetomakethetestspass.4. Runtheunittestsagain.Thistime,theyshouldpass.
Wedonothavethesaledomainobject,sothefirstthing,asweshouldstartfromsmallthingsandthenmoveontobiggerthings,istoassurethatwecaninstantiatethesaleobject.Writethefollowingunittestintests/Domain/SaleTest.phpaswewillwritealltheexistingtests,butusingTDD;youcanremovetheexistingtestsinthisfile.
<?php
namespaceBookstore\Tests\Domain;
useBookstore\Domain\Sale;
usePHPUnit_Framework_TestCase;
classSaleTestextendsPHPUnit_Framework_TestCase{
publicfunctiontestCanCreate(){
$sale=newSale();
}
}
Runtheteststomakesurethattheyarefailing.Inordertorunonespecifictest,youcanmentionthefileofthetestwhenrunningPHPUnit,asshowninthefollowingscript:
Good,theyarefailing.ThatmeansthatPHPcannotfindtheobjecttoinstantiateit.Let’snowwritetheminimumamountofcoderequiredtomakethistestpass.Inthiscase,creatingtheclasswouldbeenough,andyoucandothisthroughthefollowinglinesofcode:
<?php
namespaceBookstore\Domain;
classSale{
}
Now,runtheteststomakesurethattherearenoerrors.
Thisiseasy,right?So,whatweneedtodoisrepeatthisprocess,addingmorefunctionalityeachtime.Let’sfocusonthebooksthatasaleholds;whencreated,thebook’slistshouldbeempty,asfollows:
publicfunctiontestWhenCreatedBookListIsEmpty(){
$sale=newSale();
$this->assertEmpty($sale->getBooks());
}
Runtheteststomakesurethattheyfail—theydo.Now,writethefollowingmethodintheclass:
publicfunctiongetBooks():array{
return[];
}
Now,ifyourun…wait,what?WeareforcingthegetBooksmethodtoreturnanemptyarrayalways?Thisisnottheimplementationthatweneed—northeonewedeserve—sowhydowedoit?Thereasonisthewordingofstep3:“Writetheminimumamountofcodetomakethetestspass.”.Ourtestsuiteshouldbeextensiveenoughtodetectthiskindofproblem,andthisisourwaytomakesureitdoes.Thistime,wewillwritebadcodeonpurpose,butnexttime,wemightintroduceabugunintentionally,andourunittestsshouldbeabletodetectitassoonaspossible.Runthetests;theywillpass.
Now,let’sdiscussthenextfunctionality.Whenaddingabooktothelist,weshouldseethisbookwithamount1.Thetestshouldbeasfollows:
publicfunctiontestWhenAddingABookIGetOneBook(){
$sale=newSale();
$sale->addBook(123);
$this->assertSame(
$sale->getBooks(),
[123=>1]
);
}
Thistestisveryuseful.NotonlydoesitforceustoimplementtheaddBookmethod,butalsoithelpsusfixthegetBooksmethod—asitishardcodedrightnow—toalwaysreturnanemptyarray.AsthegetBooksmethodnowexpectstwodifferentresults,wecannottrickthetestsanymore.Thenewcodefortheclassshouldbeasfollows:
classSale{
private$books=[];
publicfunctiongetBooks():array{
return$this->books;
}
publicfunctionaddBook(int$bookId){
$this->books[123]=1;
}
}
Anewtestwecanwriteistheonethatallowsyoutoaddmorethanonebookatatime,sendingtheamountasthesecondargument.Thetestwouldlooksimilartothefollowing:
publicfunctiontestSpecifyAmountBooks(){
$sale=newSale();
$sale->addBook(123,5);
$this->assertSame(
$sale->getBooks(),
[123=>5]
);
}
Now,thetestsdonotpass,soweneedtofixthem.Let’srefactoraddBooksothatitcanacceptasecondargumentastheamount:
publicfunctionaddBook(int$bookId,int$amount=1){
$this->books[123]=$amount;
}
Thenextfunctionalitywewouldliketoaddisthesamebookinvokingthemethodseveraltimes,keepingtrackofthetotalamountofbooksadded.Thetestcouldbeasfollows:
publicfunctiontestAddMultipleTimesSameBook(){
$sale=newSale();
$sale->addBook(123,5);
$sale->addBook(123);
$sale->addBook(123,5);
$this->assertSame(
$sale->getBooks(),
[123=>11]
);
}
Thistestwillfailasthecurrentexecutionwillnotaddalltheamountsbutwillinsteadkeepthelastone.Let’sfixitbyexecutingthefollowingcode:
publicfunctionaddBook(int$bookId,int$amount=1){
if(!isset($this->books[123])){
$this->books[123]=0;
}
$this->books[123]+=$amount;
}
Well,wearealmostthere.Thereisonelasttestweshouldadd,whichistheabilitytoaddmorethanonedifferentbook.Thetestisasfollows:
publicfunctiontestAddDifferentBooks(){
$sale=newSale();
$sale->addBook(123,5);
$sale->addBook(456,2);
$sale->addBook(789,5);
$this->assertSame(
$sale->getBooks(),
[123=>5,456=>2,789=>5]
);
}
ThistestfailsduetothehardcodedbookIDinourimplementation.Ifwedidnotdothis,thetestwouldhavealreadypassed.Let’sfixitthen;runthefollowing:
publicfunctionaddBook(int$bookId,int$amount=1){
if(!isset($this->books[$bookId])){
$this->books[$bookId]=0;
}
$this->books[$bookId]+=$amount;
}
Wearedone!Doesitlookfamiliar?Itisthesamecodewewroteonourfirstimplementationexceptfortherestoftheproperties.Youcannowreplacethesaledomainobjectwiththepreviousone,soyouhaveallthefunctionalitiesneeded.
TheoryversuspracticeAsmentionedbefore,thisisaquitelongandverboseprocessthatveryfewexperienceddevelopersfollowfromstarttoendbutonethatmostofthemencouragepeopletofollow.Whyisthisso?Whenyouwriteallyourcodefirstandleavetheunittestsfortheend,therearetwoproblems:
Firstly,intoomanycasesdevelopersarelazyenoughtoskiptests,tellingthemselvesthatthecodealreadyworks,sothereisnoneedtowritethetests.Youalreadyknowthatoneofthegoalsoftestsistomakesurethatfuturechangesdonotbreakthecurrentfeatures,sothisisnotavalidreason.Secondly,thetestswrittenafterthecodeusuallytestthecoderatherthanthefunctionality.Imaginethatyouhaveamethodthatwasinitiallymeanttoperformanaction.Afterwritingthemethod,wewillnotperformtheactionperfectlyduetoabugorbaddesign;instead,wewilleitherdotoomuchorleavesomeedgecasesuntreated.Whenwewritethetestafterwritingthecode,wewilltestwhatweseeinthemethod,notwhattheoriginalfunctionalitywas!
Ifyouinsteadforceyourselftowritethetestsfirstandthenthecode,youmakesurethatyoualwayshavetestsandthattheytestwhatthecodeismeanttodo,leadingtoacodethatperformsasexpectedandisfullycovered.Also,bydoingitinsmallintervals,yougetquickfeedbackanddon’thavetowaitforhourstoknowwhetherallthetestsandcodeyouwrotemakesenseatall.Eventhoughthisideaisquitesimpleandmakesalotofsense,manynovicedevelopersfindithardtoimplement.
Experienceddevelopershavewrittencodeforseveralyears,sotheyhavealreadyinternalizedallofthis.Thisisthereasonwhysomeofthemprefertoeitherwriteseveraltestsbeforestartingwiththecodeortheotherwayaround,thatis,writingcodeandthentestingitastheyaremoreproductivethisway.However,ifthereissomethingthatallofthemhaveincommonitisthattheirapplicationswillalwaysbefulloftests.
SummaryInthischapter,youlearnedtheimportanceoftestingyourcodeusingunittests.YounowknowhowtoconfigurePHPUnitonyourapplicationsothatyoucannotonlyrunyourtestsbutalsogetgoodfeedback.Yougotagoodintroductiononhowtowriteunittestsproperly,andnow,itissaferforyoutointroducechangesinyourapplication.
Inthenextchapter,wewillstudysomeexistingframeworks,whichyoucanuseinsteadofwritingyourowneverytimeyoustartanapplication.Inthisway,notonlywillyousavetimeandeffort,butalsootherdeveloperswillbeabletojoinyouandunderstandyourcodeeasily.
Chapter8.UsingExistingPHPFrameworksInthesamewaythatyouwroteyourframeworkwithPHP,otherpeopledidittoo.Itdidnottakelongforpeopletorealizethatentireframeworkswerereusabletoo.Ofcourse,oneman’smeatisanotherman’spoison,andaswithmanyotherexamplesintheITworld,loadsofframeworksstartedtoappear.Youwillneverhearaboutmostofthem,butahandfuloftheseframeworksgotquitealotofusers.
Aswewrite,therearefourorfivemainframeworksthatmostPHPdevelopersknowof:SymfonyandZendFrameworkwerethemaincharactersofthislastPHPgeneration,butLaravelisalsothere,providingalightweightandfastframeworkforthosewhoneedfewerfeatures.Duetothenatureofthisbook,wewillfocusonthelatestones,SilexandLaravel,astheyarequickenoughtolearninachapter—oratleasttheirbasicsare.
Inthischapter,youwilllearnabout:
TheimportanceofframeworksOtherfeaturesofframeworksWorkingwithLaravelWritingapplicationswithSilex
ReviewingframeworksInChapter6,AdaptingtoMVC,webarelyintroducedtheideaofframeworksusingtheMVCdesignpattern.Infact,wedidnotexplainwhataframeworkis;wejustdevelopedaverysimpleone.Ifyouarelookingforadefinition,hereitis:aframeworkisthestructurethatyouchoosetobuildyourprogramon.Let’sdiscussthisinmoredetail.
ThepurposeofframeworksWhenyouwriteanapplication,youneedtoaddyourmodels,views,andcontrollersifyouusetheMVCdesignpattern,whichwereallyencourageyoutodo.Thesethreeelements,togetherwiththeJavaScriptandCSSfilesthatcompleteyourviews,aretheonesthatdifferentiateyourapplicationfromothers.Thereisnowayyoucanskiponwritingthem.
Ontheotherhand,thereisasetofclassesthat,eventhoughyouneedthemforthecorrectfunctioningofyourapplication,theyarecommontoallotherapplications,oratleast,theyareverysimilar.Examplesoftheseclassesaretheoneswehaveinthesrc/Coredirectory,suchastherouter,theconfigurationreader,andsoon.
Thepurposeofframeworksisclearandnecessary:theyaddsomestructuretoyourapplicationandconnectthedifferentelementsofit.Inourexample,ithelpedusroutetheHTTPrequeststothecorrectcontroller,connecttothedatabase,andgeneratedynamicHTMLastheresponse.However,theideathathastostriveisthereusabilityofframeworks.Ifyouhadtowritetheframeworkeachtimeyoustartanapplication,wouldthatbeokay?
So,inorderforaframeworktobeuseful,itmustbeeasytoreuseindifferentenvironments.Thismeansthattheframeworkhastobedownloadedfromasource,andithastobeeasytoinstall.Downloadandinstalladependency?ItseemsComposerisgoingtobeusefulagain!Eventhoughthiswasquitedifferentsomeyearsago,nowadays,allthemainframeworkscanbeinstalledusingComposer.Wewillshowyouhowtoinabit.
ThemainpartsofaframeworkIfweopensourceourframeworksothatotherdeveloperscanmakeuseofit,weneedtostructureourcodeinawaythatisintuitive.Weneedtoreducethelearningcurveasmuchaswecan;nobodywantstospendweeksonlearninghowtoworkwithaframework.
AsMVCisthedefactowebdesignpatternusedinwebapplications,mostframeworkswillseparatethethreelayers,model,view,andcontroller,inthreedifferentdirectories.Dependingontheframework,theywillbeunderasrc/directory,eventhoughitisquitecommontofindtheviewsoutsideofthisdirectory,aswedidwithourown.Nevertheless,mostframeworkswillgiveyouenoughflexibilitytodecidewheretoplaceeachofthelayers.
Therestoftheclassesthatcompletetheframeworksusedtobeallgroupedinaseparatedirectory—forexample,src/Core.Itisimportanttoseparatetheseelementsfromyourssothatyoudonotmixthecodeandmodifyacoreclassunintentionally,thusmessingupthewholeframework.Evenbetter,thislastgenerationofPHPframeworksusedtoincorporatethecorecomponentsasindependentmodules,whichwillberequiredviaComposer.Indoingso,theframework’scomposer.jsonfilewillrequireallthedifferentcomponents,suchasrouters,configuration,databaseconnections,loggers,templateengine,andsoon,andComposerwilldownloadtheminthevendor/directory,makingthemavailablewiththeautogeneratedautoloader.
Separatingthedifferentcomponentsindifferentcodebaseshasmanybenefits.Firstofall,itallowsdifferentteamsofdeveloperstoworkinanisolatedwaywiththedifferentcomponents.Maintainingthemisalsoeasierasthecodeisseparatedenoughnottoaffecteachother.Finally,itallowstheendusertochoosewhichcomponentstogetforhisapplicationinanattempttocustomizetheframework,leavingoutthoseheavycomponentsthatarenotused.
Eithertheframeworkisorganizedinindependentmodulesoreverythingistogether;however,therearealwaysthesamecommoncomponents,whichare:
Therouter:Thisistheclassthat,givenanHTTPrequest,findsthecorrectcontroller,instantiatesit,andexecutesit,returningtheHTTPresponse.Therequest:Thiscontainsahandfulofmethodsthatallowsyoutoaccessparameters,cookies,headers,andsoon.Thisismostlyusedbytherouterandsenttothecontroller.Theconfigurationhandler:Thisallowsyoutogetthecorrectconfigurationfile,readit,anduseitscontentstoconfiguretherestofthecomponents.Thetemplateengine:ThismergesHTMLwithcontentfromthecontrollerinordertorenderthetemplatewiththeresponse.Thelogger:Thisaddsentriestoalogfilewiththeerrorsorothermessagesthatweconsiderimportant.Thedependencyinjector:Thismanagesallthedependenciesthatyourclassesneed.Maybetheframeworkdoesnothaveadependencyinjector,butithassomethingsimilar—thatis,aservicelocator—whichtriestohelpyouinasimilarway.
Thewayyoucanwriteandrunyourunittests:Mostofthetime,theframeworksincludePHPUnit,buttherearemoreoptionsinthecommunity.
OtherfeaturesofframeworksMostframeworkshavemorethanjustthefeaturesthatwedescribedintheprevioussection,eventhoughtheseareenoughtobuildsimpleapplicationsasyoualreadydidbyyourself.Still,mostwebapplicationshavealotmorecommonfeatures,sotheframeworkstriedtoimplementgenericsolutionstoeachofthem.Thankstothis,wedonothavetoreinventthewheelwithfeaturesthatvirtuallyallmediumandbigwebapplicationsneedtoimplement.Wewilltrytodescribesomeofthemostusefulonessothatyouhaveabetterideawhenchoosingaframework.
AuthenticationandrolesMostwebsitesenforceuserstoauthenticateinordertoperformsomeaction.Thereasonforthisistoletthesystemknowwhethertheusertryingtoperformcertainactionhastherighttodoso.Therefore,managingusersandtheirrolesissomethingthatyouwillprobablyendupimplementinginallyourwebapplications.Theproblemcomeswhenwaytoomanypeopletrytoattackyoursysteminordertogettheinformationofotherusersorperformingactionsauthenticatedassomeoneelse,whichiscalledimpersonification.Itisforthisreasonthatyourauthenticationandauthorizationsystemsshouldbeassecureaspossible—ataskthatisnevereasy.
Severalframeworksincludeaprettysecurewayofmanagingusers,permissions,andsessions.Mostofthetime,youcanmanagethisthroughaconfigurationfileprobablybypointingthecredentialstoadatabasewheretheframeworkcanaddtheuserdata,yourcustomizedroles,andsomeothercustomizations.Thedownsideisthateachframeworkhasitsownwayofconfiguringit,soyouwillhavetodigintothedocumentationoftheframeworkyouareusingatthistime.Still,itwillsaveyoumoretimethanifyouhadtoimplementitbyyourself.
ORMObject-relationalmapping(ORM)isatechniquethatconvertsdatafromadatabaseoranyotherdatastorageintoobjects.Themaingoalistoseparatethebusinesslogicasmuchaspossiblefromthestructureofthedatabaseandtoreducethecomplexityofyourcode.WhenusingORM,youwillprobablyneverwriteaqueryinMySQL;instead,youwilluseachainofmethods.Behindthescenes,ORMwillwritethequerywitheachmethodinvocation.
TherearegoodandbadthingswhenusingORM.Ononehand,youdonothavetorememberalltheSQLsyntaxallthetimeandonlythecorrectmethodstoinvoke,whichcanbeeasierifyouworkwithanIDEthatcanautocompletemethods.Itisalsogoodtoabstractyourcodefromthetypeofstoragesystem,becauseeventhoughitisnotverycommon,youmightwanttochangeitlater.IfyouuseORM,youprobablyhavetochangeonlythetypeofconnection,butifyouwerewritingrawqueries,youwouldhavealotofworktodoinordertomigrateyourcode.
ThearguabledownsideofusingORMcouldbethatitmaybequitedifficulttowritecomplicatedqueriesusingmethodchains,andyouwillendupwritingthemmanually.YouarealsoatthemercyofORMinordertospeeduptheperformanceofyourqueries,whereaswhenwritingthemmanually,itisyouwhocanchoosebetterwhatandhowtousewhenquerying.Finally,somethingthatOOPpuristscomplainaboutquitealotisthatusingORMfillsyourcodewithalargeamountofdummyobjects,similartothedomainobjectsthatyoualreadyknow.
Asyoucansee,usingORMisnotalwaysaneasydecision,butjustincaseyouchoosetouseit,mostofthebigframeworksincludeone.Takeyourtimeindecidingwhetherornottouseoneinyourapplications;incaseyoudo,choosewiselywhichone.YoumightenduprequiringanORMdifferentfromtheonethattheframeworkprovides.
CacheThebookstoreisaprettygoodexamplethatmayhelpindescribingthecachefeature.Ithasadatabaseofbooksthatisqueriedeverytimethatsomeoneeitherlistsallthebooksorasksforthedetailsofaspecificone.Mostofthetimetheinformationrelatedtobookswillbethesame;theonlychangewouldbethestockofthebooksfromtimetotime.Wecouldsaythatoursystemhaswaymorereadsthanwrites,wherereadsmeansqueryingfordataandwritesmeansupdatingit.Inthiskindofsystem,itseemslikeawasteoftimeandresourcestoaccessthedatabaseeachtime,knowingthatmostofthetime,wewillgetthesameresults.Thisfeelingincreasesifwedosomeexpensivetransformationtothedatathatweretrieve.
Acachelayerallowstheapplicationtostoretemporarydatainastoragesystemfasterthanourdatabase,usuallyinmemoryratherthandisk.Eventhoughcachesystemsaregettingmorecomplex,theyusuallyallowyoutostoredatabykey-valuepairs,asinanarray.
Theideaisnottoaccessthedatabasefordatathatweknowisthesameasthelasttimeweaccesseditinordertosavetimeandresources.Implementationscanvaryquitealot,butthemainflowisasfollows:
1. Youtrytoaccessacertainpieceofdataforthefirsttime.Weaskthecachewhetheracertainkeyisthere,whichitisnot.
2. Youquerythedatabase,gettingbacktheresult.Afterprocessingit—andmaybetransformingittoyourdomainobjects—youstoretheresultinthecache.Thekeywouldbethesameyouusedinstep1,andthevaluewouldbetheobject/array/JSONthatyougenerated.
3. Youtrytoaccessthesamepieceofdataagain.Youaskthecachewhetherthekeyisthere;here,itis,soyoudonotneedtoaccessthedatabaseatall.
Itseemseasy,right?Themainproblemwithcachescomeswhenweneedtoinvalidateacertainkey.Howandwhenshouldwedoit?Thereareacoupleofapproachesthatareworthmentioning:
Youwillsetanexpirationtimetothekey-valuepairinthecache.Afterthistimepasses,thecachewillremovethekey-valuepairautomatically,soyouwillhavetoquerythedatabaseagain.Eventhoughthissystemmightworkforsomeapplications,itdoesnotforours.Ifthestockchangesto0beforethecacheexpires,theuserwillseebooksthattheycannotborroworbuy.Thedataneverexpires,buteachtimewemakeachangeinthedatabase,wewillidentifywhichkeysinthecacheareaffectedbythischangeandthenpurgethem.Thisisidealsincethedatawillbeinthecacheuntilitisnolongervalid,whetherthisis2secondsor3weeks.Thedownsideisthatidentifyingthesekeyscouldbeahardtaskdependingonyourdatastructure.Ifyoumissdeletingsomeofthem,youwillhavecorrupteddatainyourcache,whichisquitedifficulttodebuganddetect.
Youcanseethatcacheisadouble-edgedsword,sowewouldrecommendyoutoonlyuse
itwhennecessaryandnotjustbecauseyourframeworkcomeswithit.AswithORM,ifyouarenotconvincedbythecachesystemthatyourframeworkprovides,usingadifferentoneshouldnotbedifficult.Infact,yourcodeshouldnotbeawareofwhichcachesystemyouareusingexceptwhencreatingtheconnectionobject.
InternationalizationEnglishisnottheonlylanguageoutthere,andyouwouldliketomakeyourwebsiteasaccessibleaspossible.Dependingonyourtarget,itwouldbeagoodideatohaveyourwebsitetranslatedtootherlanguagestoo,buthowdoyoudothis?Wehopethatbynowyoudidnotanswer:“Copy-pastingallthetemplatesandtranslatingthem”.Thisiswaytooinefficient;whenmakingalittlechangeinatemplate,youneedtoreplicatethechangeeverywhere.
Therearetoolsthatcanbeintegratedwitheithercontrollersand/ortemplateenginesinordertotranslatestrings.Youusuallykeepafileforeachlanguagethatyouhave,inwhichyouwilladdallthestringsthatneedtobetranslatedplustheirtranslation.OneofthemostcommonformatsforthisisPOfiles,inwhichyouhaveamapofkey-valuepairswithoriginallytranslatedpairs.Lateron,youwillinvokeatranslatemethodsendingtheoriginalstring,whichwillreturnthetranslatedstringdependingonthelanguageyouselected.
Whenwritingtemplates,itmightbetiringtoinvokethetranslationeachtimeyouwanttodisplayastring,butyouwillendupwithonlyonetemplate,whichismucheasiertomaintainthananyotheroption.
Usually,internationalizationisverymuchtiedtotheframeworkthatyouuse;however,ifyouhavetheopportunitytousethesystemofyourchoice,payspecialattentiontoitsperformance,thetranslationfilesituses,andhowitmanagesstringswithparameters—thatis,howwecanaskthesystemtotranslatemessagessuchas“Hello%s,whoareyou?”inwhich“%s”needstobeinjectedeachtime.
TypesofframeworksNowthatyouknowquitealotaboutwhataframeworkcanofferyou,youareinapositiontodecidewhatkindofframeworkyouwouldliketouse.Inordertomakethisdecision,itmightbeusefultoknowwhatkindsofframeworksareavailable.Thiscategorizationisnothingofficial,justsomeguidelinesthatweofferyoutomakeyourchoiceeasier.
CompleteandrobustframeworksThistypeofframeworkcomeswiththewholepackage.Itcontainsallthefeaturesthatwediscussedearlier,soitwillallowyoutodevelopverycompleteapplications.Usually,theseframeworksallowyoutocreateapplicationsveryeasilywithjustafewconfigurationfilesthatdefinethingssuchashowtoconnecttoadatabase,whatkindofrolesyouneed,orwhetheryouwanttouseacache.Otherthanthis,youwilljusthavetoaddyourcontrollers,views,andmodels,whichsavesyoualotoftime.
Theproblemwiththeseframeworksisthelearningcurve.Givenallthefeaturestheycontain,youneedtospendquitealotoftimeonlearninghowtouseeachone,whichisusuallynotverypleasant.Infact,mostcompanieslookingforwebdevelopersrequirethatyouhaveexperiencewiththeframeworktheyuse;otherwise,itwillbeabadinvestmentforthem.
Anotherthingyoushouldconsiderwhenchoosingtheseframeworksiswhethertheyarestructuredinmodulesorcomeasahugemonolith.Inthefirstcase,youwillbeabletochoosewhichmodulestousethataddalotofflexibility.Ontheotherhand,ifyouhavetostickwithallofthem,itmightmakeyourapplicationslowevenifyoudonotuseallofthefeatures.
LightweightandflexibleframeworksEvenwhenworkingonasmallapplication,youwouldliketouseaframeworktosaveyoualotoftimeandpain,butyoushouldavoidusingoneofthelargerframeworksastheywillbetoomuchtohandleforwhatyoureallyneed.Inthiscase,youshouldchoosealightweightframework,onethatcontainsveryfewfeatures,similartowhatweimplementedinpreviouschapters.
Thebenefitoftheseframeworksisthateventhoughyougetthebasicfeaturessuchasrouting,youarecompletelyfreetoimplementtheloginsystem,cachelayer,orinternationalizationsystemthatsuitsyourspecificapplicationbetter.Infact,youcouldbuildamorecompleteframeworkusingthisoneasthebaseandthenaddingallthecomplementsyouneed,makingitcompletelycustomized.
Asyoucannote,bothtypeshavetheirprosandcons.Itwillbeuptoyoutochoosethecorrectoneeachtime,dependingonyourneeds,thetimethatyoucanspend,andtheexperiencethatyouhavewitheachone.
AnoverviewoffamousframeworksYoualreadyhaveagoodideaaboutwhataframeworkcanofferandwhattypesthereare.Now,itistimetoreviewsomeofthemostimportantonesouttheresothatyougetanideaofwheretostartlookingforyournextPHPwebapplication.NotethatwiththereleaseofPHP7,therewillbequitealotofneworimprovedPHPframeworks.Trytoalwaysbeintheloop!
Symfony2Symfonyhasbeenoneofthemostfavoriteframeworksofdevelopersduringthelast10years.Afterreinventingitselfforitsversion2,Symfonyenteredthegenerationofframeworksbymodules.Infact,itisquitecommontofindotherprojectsusingSymfony2componentsmixedupwithsomeotherframeworkasyoujustneedtoaddthenameofthemoduleinyourComposerfiletouseit.
YoucanstartapplicationswithSymfony2byjustexecutingacommand.Symfony2createsallthedirectories,emptyconfigurationfiles,andsoonreadyforyou.Youcanalsoaddemptycontrollersfromthecommandline.TheyuseDoctrine2asORM,whichisprobablyoneofthemostreliableORMsthatPHPcanoffernowadays.Forthetemplateengine,youwillfindTwig,whichisthesameaswhatweusedinourframework.
Ingeneral,thisisaveryattractiveframeworkwithahugecommunitybehinditgivingsupport;plus,alotofcompaniesalsouseit.Itisalwaysworthatleastcheckingthelistofmodulesincaseyoudonotwanttousethewholeframeworkbutwanttotakeadvantageofsomebitsofit.
ZendFramework2ThesecondbigPHPframework,atleastsincelastyear,isZendFramework2.AswithSymfony,ithasbeenoutthereforquitealongtimetoo.Also,aswithanyothermodernframework,itisbuiltinanOOPway,tryingtoimplementallthegooddesignpatternsusedforwebapplications.Itiscomposedofmultiplecomponentsthatyoucanreuseinotherprojects,suchastheirwell-knownauthenticationsystem.Itlackssomeelements,suchasatemplateengine—usuallytheymixPHPandHTML—andORM,butyoucaneasilyintegratetheonesthatyouprefer.
ThereisalotofworkgoingoninordertoreleaseZendFramework3,whichwillcomewithsupportforPHP7,performanceimprovements,andsomeothernewcomponents.Werecommendyoutokeepaneyeonit;itcouldbeagoodcandidate.
OtherframeworksEventhoughSymfonyandZendFrameworkarethetwobigplayers,moreandmorePHPframeworkshaveappearedintheselastyears,evolvingquitefastandbringingtothegamemoreinterestingfeatures.NamessuchasCodeIgniter,Yii,PHPCake,andotherswillstarttosoundfamiliarassoonasyoustartbrowsingPHPprojects.AssomeofthemcameintoplaylaterthanSymfonyandZendFramework,theyimplementsomenewfeaturesthattheothersdonothave,suchascomponentsrelatedtoJavaScriptandjQuery,integrationwithSeleniumforUItesting,andothers.
Eventhoughitisalwaysagoodthingtohavediversificationsimplybecauseyouwillprobablygetexactlywhatyouneedfromoneortheother,besmartwhenchoosingyourframework.Thecommunityplaysanimportantroleherebecauseifyouhaveanyproblem,itwillhelpyoutofixitoryoucanjusthelpevolvetheframeworkwitheachnewPHPrelease.
TheLaravelframeworkEventhoughSymfonyandZendFrameworkhavebeenthebigplayersforquitealongtime,duringthislastcoupleofyears,athirdframeworkcameintoplaythathasgrowninpopularitysomuchthatnowadaysitisthefavoriteframeworkamongdevelopers.Simplicity,elegantcode,andhighspeedofdevelopmentarethetrumpcardsofthis“frameworkforartisans”.Inthissection,youwillhaveaglanceatwhatLaravelcando,takingthefirststepstocreateaverysimpleapplication.
InstallationLaravelcomeswithasetofcommand-linetoolsthatwillmakeyourlifeeasier.Becauseofthis,itisrecommendedtoinstallitgloballyinsteadofperproject—thatis,tohaveLaravelasanotherprograminyourenvironment.YoucanstilldothiswithComposerbyrunningthefollowingcommand:
$composerglobalrequire"laravel/installer"
ThiscommandshoulddownloadtheLaravelinstallerto~/.composer/vendor.Inordertobeabletousetheexecutablefromthecommandline,youwillneedtorunsomethingsimilartothis:
$sudoln-s~/.composer/vendor/bin/laravel/usr/bin/laravel
Now,youareabletousethelaravelcommand.Toensurethateverythingwentallright,justrunthefollowing:
$laravel–version
IfeverythingwentOK,thisshouldoutputtheversioninstalled.
ProjectsetupYes,weknow.Everysingletutorialstartsbycreatingablog.However,wearebuildingwebapplications,andthisistheeasiestapproachwecantakethataddssomevaluetoyou.Let’sstartthen;executethefollowingcommandwhereveryouwanttoaddyourapplication:
$laravelnewphp-blog
ThiscommandwilloutputsomethingsimilartowhatComposerdoes,simplybecauseitfetchesdependenciesusingComposer.Afterafewseconds,theapplicationwillhopefullytellyouthateverythingwasinstalledsuccessfullyandthatyouarereadytogo.
Laravelcreatedanewphp-blogdirectorywithquitealotofcontent.Youshouldhavesomethingsimilartothedirectorystructureshowninthefollowingscreenshot:
Let’ssetupthedatabase.Thefirstthingyoushoulddoisupdatethe.envfilewiththecorrectdatabasecredentials.UpdatetheDB_DATABASEvalueswithyourown;here’sanexample:
DB_HOST=localhost
DB_DATABASE=php_blog
DB_USERNAME=root
DB_PASSWORD=
Youwillalsoneedtocreatethephp_blogdatabase.Doitwithjustonecommand,asfollows:
$mysql-uroot-e"CREATESCHEMAphp_blog"
WithLaravel,youhaveamigrationssystem;thatis,youkeepallthedatabaseschemachangesunderdatabase/migrationssothatanyoneelseusingyourcodecanquicklysetuptheirdatabase.Thefirststepistorunthefollowingcommand,whichwillcreateamigrationsfilefortheblogstable:
$phpartisanmake:migrationcreate_posts_table--create=posts
Openthegeneratedfile,whichshouldbesomethingsimilartodatabase/migrations/<date>_create_posts_table.php.TheupmethoddefinesthetableblogswithanautoincrementalIDandtimestampfield.Wewouldliketoaddatitle,thecontentofthepost,andtheuserIDthatcreatedit.Replacetheupmethodwiththefollowing:
publicfunctionup()
{
Schema::create('posts',function(Blueprint$table){
$table->increments('id');
$table->timestamps();
$table->string('title');
$table->text('content');
$table->integer('user_id')->unsigned();
$table->foreign('user_id')
->references('id')->on('users');
});
}
Here,thetitlewillbeastring,whereasthecontentisatext.Thedifferenceisinthelengthofthesefields,stringbeingasimpleVARCHARandtextaTEXTdatatype.FortheuserIDwedefinedINTUNSIGNED,whichreferencestheidfieldoftheuserstable.Laravelalreadydefinedtheuserstablewhencreatingtheproject,soyoudonothavetoworryaboutit.Ifyouareinterestedinhowitlooks,checkthedatabase/migrations/2014_10_12_000000_create_users_table.phpfile.YouwillnotethatauseriscomposedbyanID,aname,theuniquee-mail,andthepassword.
Sofar,wehavejustwrittenthemigrationfiles.Inordertoapplythem,youneedtorunthefollowingcommand:
$phpartisanmigrate
Ifeverythingwentasexpected,youshouldhaveablogstablenowsimilartothefollowing:
Tofinishwithallthepreparations,weneedtocreateamodelforourblogstable.ThismodelwillextendfromIlluminate\Database\Eloquent\Model,whichistheORMthatLaraveluses.Togeneratethismodelautomatically,runthefollowingcommand:
$phpartisanmake:modelPost
Thenameofthemodelshouldbethesameasthatofthedatabasetablebutinsingular.Afterrunningthiscommand,youcanfindtheemptymodelinapp/Post.php.
AddingthefirstendpointLet’saddaquickendpointjusttounderstandhowroutesworkandhowtolinkcontrollerswithtemplates.Inordertoavoiddatabaseaccess,let’sbuildtheaddnewpostview,whichwilldisplayaformthatallowstheusertoaddanewpostwithatitleandtext.Let’sstartbyaddingtherouteandcontroller.Opentheapp/Http/routes.phpfileandaddthefollowing:
Route::group(['middleware'=>['web']],function(){
Route::get('/new',function(){
returnview('new');
});
});
Thesethreeverysimplelinessaythatforthe/newendpoint,wewanttoreplywiththenewview.Lateron,wewillcomplicatethingshereinthecontroller,butfornow,let’sfocusontheviews.
LaravelusesBladeasthetemplateengineinsteadofTwig,butthewaytheyworkisquitesimilar.Theycanalsodefinelayoutsfromwhereothertemplatescanextend.Theplaceforyourlayoutsisinresources/views/layouts.Createanapp.blade.phpfilewiththefollowingcontentinsidethisdirectory,asfollows:
<!DOCTYPEhtml>
<htmllang="en">
<head>
<title>PHPBlog</title>
<linkrel="stylesheet"href="{{URL::asset('css/layout.css')}}"
type="text/css">
@yield('css')
</head>
<body>
<divclass="navbar">
<ul>
<li><ahref="/new">Newarticle</a></li>
<li><ahref="/">Articles</a></li>
</ul>
</div>
<divclass="content">
@yield('content')
</div>
</body>
</html>
Thisisjustanormallayoutwithatitle,someCSS,andanullistofsectionsinthebody,whichwillbeusedasthenavigationbar.TherearetwoimportantelementstonotehereotherthantheHTMLcodethatshouldalreadysoundfamiliar:
Todefineablock,Bladeusesthe@yieldannotationfollowedbythenameoftheblock.Inourlayout,wedefinedtwoblocks:cssandcontent.ThereisafeaturethatallowsyoutobuildURLsintemplates.WewanttoincludetheCSSfileinpublic/css/layout.css,sowewilluseURL::assettobuildthisURL.It
isalsohelpfultoincludeJSfiles.
Asyousaw,weincludedalayout.cssfile.CSSandJSfilesarestoredunderthepublicdirectory.Createyoursinpublic/css/layout.csswiththefollowingcode:
.content{
position:fixed;
top:50px;
width:100%
}
.navbarul{
position:fixed;
top:0;
width:100%;
list-style-type:none;
margin:0;
padding:0;
overflow:hidden;
background-color:#333;
}
.navbarli{
float:left;
border-right:1pxsolid#bbb;
}
.navbarli:last-child{
border-right:none;
}
.navbarlia{
display:block;
color:white;
text-align:center;
padding:14px16px;
text-decoration:none;
}
.navbarlia:hover{
background-color:#111;
}
Now,wecanfocusonourview.Templatesarestoredinresources/views,and,aswithlayouts,theyneedthe.blade.phpfileextension.Createyourviewinresources/views/new.blade.phpwiththefollowingcontent:
@extends('layouts.app')
@section('css')
<linkrel="stylesheet"href="{{URL::asset('css/new.css')}}"
type="text/css">
@endsection
@section('content')
<h2>Addnewpost</h2>
<formmethod="post"action="/new">
<divclass="component">
<labelfor="title">Title</label>
<inputtype="text"name="title"/>
</div>
<divclass="component">
<label>Text</label>
<textarearows="20"name="content"></textarea>
</div>
<divclass="component">
<buttontype="submit">Save</button>
</div>
</form>
@endsection
Thesyntaxisquiteintuitive.Thistemplateextendsfromthelayouts’oneanddefinestwosectionsorblocks:cssandcontent.TheCSSfileincludedfollowsthesameformatasthepreviousone.Youcancreateitinpublic/css/new.csswithcontentsimilartothefollowing:
label{
display:block;
}
input{
width:80%;
}
button{
font-size:30px;
float:right;
margin-right:20%;
}
textarea{
width:80%;
}
.component{
padding:10px;
}
TherestofthetemplatejustdefinesthePOSTformpointingtothesameURLwithtitleandtextfields.Everythingisreadytotestitinyourbrowser!Tryaccessinghttp://localhost:8080/newortheportnumberofyourchoice.Youshouldseesomethingsimilartothefollowingscreenshot:
ManagingusersAsexplainedbefore,userauthenticationandauthorizationisoneofthefeaturesthatmostframeworkscontain.Laravelmakesourlivesveryeasybyprovidingtheusermodelandtheregistrationandauthenticationcontrollers.Itisquiteeasytomakeuseofthem:youjustneedtoaddtheroutespointingtothealreadyexistingcontrollersandaddtheviews.Let’sbegin.
Therearefiveroutesthatyouneedtoconsiderhere.Therearetwothatbelongtotheregistrationstep,onetogettheformandanotheronefortheformtosubmittheinformationprovidedbytheuser.Theotherthreearerelatedtotheauthenticationpart:onetogettheform,onetoposttheform,andoneforthelogout.AllfiveofthemareincludedintheAuth\AuthControllerclass.Addtoyourroutes.phpfilethefollowingroutes:
//Registrationroutes…
Route::get('auth/register','Auth\AuthController@getRegister');
Route::post('auth/register','Auth\AuthController@postRegister');
//Authenticationroutes…
Route::get('/login','Auth\AuthController@getLogin');
Route::post('login','Auth\AuthController@postLogin');
Route::get('logout','Auth\AuthController@getLogout');
Notehowwedefinedtheseroutes.Asopposedtotheonethatwecreatedpreviously,thesecondargumentoftheseisastringwiththeconcatenationofthecontroller’sclassnameandmethod.Thisisabetterwaytocreateroutesbecauseitseparatesthelogictoadifferentclassthatcanlaterbereusedand/orunittested.
Ifyouareinterested,youcanbrowsethecodeforthiscontroller.Youwillfindacomplexdesign,wherethefunctionstherouteswillinvokeareactuallypartoftwotraitsthattheAuthControllerclassuses:RegistersUsersandAuthenticatesUsers.Checkingthesemethodswillenableyoutounderstandwhatgoesonbehindthescenes.
Eachgetrouteexpectsaviewtorender.Fortheuser’sregistration,weneedtocreateatemplateinresources/views/auth/register.blade.php,andfortheloginview,weneedatemplateinresources/views/auth/login.blade.php.AssoonaswesendthecorrectPOSTparameterstothecorrectURL,wecanaddanycontentthatwethinknecessary.
UserregistrationLet’sstartwiththeregistrationform;thisformneedsfourPOSTparameters:name,e-mail,password,andpasswordconfirmation,andastheroutesays,weneedtosubmititto/auth/register.Thetemplatecouldlooksimilartothefollowing:
@extends('layouts.app')
@section('css')
<linkrel="stylesheet"href="{{URL::asset('css/register.css')}}"
type="text/css">
@endsection
@section('content')
<h2>Accountregistration</h2>
<formmethod="post"action="/auth/register">
{{csrf_field()}}
<divclass="component">
<labelfor="name">Name</label>
<inputtype="text"name="name"
value="{{old('name')}}"/>
</div>
<divclass="component">
<label>Email</label>
<inputtype="email"name="email"
value="{{old('email')}}"/>
</div>
<divclass="component">
<label>Password</label>
<inputtype="password"name="password"/>
</div>
<divclass="component">
<label>Passwordconfirmation</label>
<inputtype="password"name="password_confirmation"/>
</div>
<divclass="component">
<buttontype="submit">Create</button>
</div>
</form>
@endsection
Thistemplateisquitesimilartotheformfornewposts:itextendsthelayout,addsaCSSfile,andpopulatesthecontentsectionwithaform.Thenewadditionhereistheuseoftheoldfunctionthatretrievesthevaluesubmittedonthepreviousrequestincasethattheformwasnotvalidandweshoweditbacktotheuser.
Beforewetryit,weneedtoaddaregister.cssfilewiththestylesforthisform.Asimpleonecouldbeasfollows:
div.content{
text-align:center;
}
label{
display:block;
}
input{
width:250px;
}
button{
font-size:20px;
}
.component{
padding:10px;
}
Finally,weshouldeditthelayoutinordertoaddalinkonthemenupointingtothe
registrationandloginpages.Thisisassimpleasaddingthefollowinglielementsattheendoftheultag:
<liclass="right"><ahref="/auth/register">Signup</a></li>
<liclass="right"><ahref="/login">Signin</a></li>
Addalsothestylefortherightclassattheendoflayout.css:
div.alert{
color:red;
}
Tomakethingsevenmoreuseful,wecouldaddtheinformationforwhatwentwrongwhensubmittingtheform.Laravelflashestheerrorsintothesession,andtheycanbeaccessedviatheerrorstemplatevariable.Asthisiscommontoallformsandnotonlytotheregistrationone,wecouldaddittotheapp.blade.phplayout,asfollows:
<divclass="content">
@if(count($errors)>0)
<divclass="alert">
<strong>Whoops!Somethingwentwrong!</strong>
@foreach($errors->all()as$error)
<p>{{$error}}</p>
@endforeach
</div>
@endif
@yield('content')
Inthispieceofcode,wewilluseBlade’s@ifconditionaland@foreachloop.ThesyntaxisthesameasPHP;theonlydifferenceisthe@prefix.
Now,wearereadytogo.Launchyourapplicationandclickontheregistrationlinkontheright-handsideofthemenu.Attempttosubmittheform,butleavesomefieldsblanksothatwecannotehowtheerrorsaredisplayed.Theresultshouldbesomethingsimilartothis:
Onethingthatweshouldcustomizeiswheretheuserwillberedirectedoncetheregistrationissuccessful.Inthiscase,wecanredirectthemtotheloginpage.Inordertoachievethis,youneedtochangethevalueofthe$redirectTopropertyofAuthController.Sofar,weonlyhavethenewpostpage,butlater,youcouldaddanypaththatyouwantviathefollowing:
protected$redirectPath='/new;
UserloginTheuser’sloginhasafewmorechangesotherthantheregistration.Wenotonlyneedtoaddtheloginview,weshouldalsomodifythemenuinthelayoutinordertoacknowledgetheauthenticateduser,removetheregisterlink,andaddalogoutone.Thetemplate,asmentionedearlier,hastobesavedinresources/views/auth/login.blade.php.Theformneedsane-mailandpasswordandoptionallyacheckboxfortheremembermefunctionality.Anexamplecouldbethefollowing:
@extends('layouts.app')
@section('css')
<linkrel="stylesheet"href="{{URL::asset('css/register.css')}}"
type="text/css">
@endsection
@section('content')
<h2>Login</h2>
<formmethod="POST"action="/login">
{!!csrf_field()!!}
<divclass="component">
<label>Email</label>
<inputtype="email"name="email"
value="{{old('email')}}">
</div>
<divclass="component">
<label>Password</label>
<inputtype="password"name="password">
</div>
<divclass="component">
<inputclass="checkbox"type="checkbox"name="remember">
RememberMe
</div>
<divclass="component">
<buttontype="submit">Login</button>
</div>
</form>
@endsection
Thelayouthastobechangedslightly.Wherewedisplayedthelinkstoregisterandloginusers,nowweneedtocheckwhetherthereisauseralreadyauthenticated;ifso,weshouldrathershowalogoutlink.YoucangettheauthenticateduserthroughtheAuth::user()methodevenfromtheview.Iftheresultisnotempty,itmeansthattheuserwasauthenticatedsuccessfully.Changethetwolinksusingthefollowingcode:
<ul>
<li><ahref="/new">Newarticle</a></li>
<li><ahref="/">Articles</a></li>
@if(Auth::user()!==null)
<liclass="right">
<ahref="/logout">Logout</a>
</li>
@else
<liclass="right">
<ahref="/auth/register">Signup</a>
</li>
<liclass="right">
<ahref="/login">Signin</a>
</li>
@endif
</ul>
ProtectedroutesThislastpartoftheusermanagementsessionisprobablythemostimportantone.Oneofthemaingoalswhenauthenticatingusersistoauthorizethemtocertaincontent—thatis,toallowthemtovisitcertainpagesthatunauthenticateduserscannot.InLaravel,youcan
definewhichroutesareprotectedinthiswaybyjustaddingtheauthmiddleware.Updatethenewpostroutewiththefollowingcode:
Route::get('/new',['middleware'=>'auth',function(){
returnview('new');
}]);
Everythingisready!Trytoaccessthenewpostpageafterloggingout;youwillberedirectedautomaticallytotheloginpage.Canyoufeelhowpowerfulaframeworkcanbe?
SettinguprelationshipsinmodelsAswementionedbefore,LaravelcomeswithanORM,EloquentORM,whichmakesdealingwithmodelsaveryeasytask.Inoursimpledatabase,wedefinedonetableforposts,andwealreadyhadanotheroneforusers.PostscontaintheIDoftheuserthatownsit—thatis,user_id.Itisgoodpracticetousethesingularofthenameofthetablefollowedby_idsothatEloquentwillknowwheretolook.Thiswasallwedidregardingtheforeignkey.
Weshouldalsomentionthisrelationshiponthemodelside.Dependingonthetypeoftherelationship(onetoone,onetomany,ormanytomany),thecodewillbeslightlydifferent.Inourcase,wehaveaone-to-manyrelationshipbecauseoneusercanhavemanyposts.TosaysoinLaravel,weneedtoupdateboththePostandtheUsermodels.TheUsermodelneedstospecifythatithasmanyposts,soyouneedtoaddapostsmethodwiththefollowingcontent:
publicfunctionposts(){
return$this->hasMany('App\Post');
}
Thismethodsaysthatthemodelforusershasmanyposts.TheotherchangethatneedstobemadeinPostissimilar:weneedtoaddausermethodthatdefinestherelationship.Themethodshouldbesimilartothisone:
publicfunctionuser(){
return$this->belongsTo('App\User');
}
Itlookslikeverylittle,butthisisthewholeconfigurationthatweneed.Inthenextsection,youwillseehoweasyitistosaveandqueryusingthesetwomodels.
CreatingcomplexcontrollersEventhoughthetitleofthissectionmentionscomplexcontrollers,youwillnotethatwecancreatecompleteandpowerfulcontrollerswithverylittlecode.Let’sstartbyaddingthecodethatwillmanagethecreationofposts.Thiscontrollerneedstobelinkedtothefollowingroute:
Route::post('/new','Post\PostController@createPost');
Asyoucanimagine,now,weneedtocreatethePost\PostControllerclasswiththecreatePostmethodinit.Controllersshouldbestoredinapp/Http/Controllers,andiftheycanbeorganizedinfolders,itwouldbeevenbetter.Savethefollowingclassinapp/Http/Controllers/Post/PostController.php:
<?php
namespaceApp\Http\Controllers\Post;
useApp\Http\Controllers\Controller;
useIlluminate\Http\Request;
useIlluminate\Support\Facades\Auth;
useIlluminate\Support\Facades\Validator;
useApp\Post;
classPostControllerextendsController{
publicfunctioncreatePost(Request$request){
}
}
Sofar,theonlytwothingswecannotefromthisclassare:
ControllersextendfromtheApp\Http\Controllers\Controllerclass,whichcontainssomegeneralhelpersforallthecontrollers.MethodsofcontrollerscangettheIlluminate\Http\Requestargumentastheuser’srequest.Thisobjectwillcontainelementssuchasthepostedparameters,cookies,andsoon.Thisisverysimilartotheonewecreatedinourownapplication.
Thefirstthingweneedtodointhiskindofcontrollerischeckwhethertheparameterspostedarecorrect.Forthis,wewillusethefollowingcode:
publicfunctioncreatePost(Request$request){
$validator=Validator::make($request->all(),[
'title'=>'required|max:255',
'content'=>'required|min:20',
]);
if($validator->fails()){
returnredirect()->back()
->withInput()
->withErrors($validator);
}
}
Thefirstthingwedidiscreateavalidator.Forthis,weusedtheValidator::makefunctionandsenttwoarguments:thefirstonecontainsalltheparametersfromtherequest,andthesecondoneisanarraywiththeexpectedfieldsandtheirconstraints.Notethatweexpecttworequiredfields:titleandcontent.Here,thefirstonecanbeupto255characterslong,andthesecondoneneedstobeatleast20characterslong.
Oncethevalidatorobjectiscreated,wecancheckwhetherthedatapostedbytheusermatchestherequirementswiththefailsmethod.Ifitreturnstrue—thatis,thevalidationfails—wewillredirecttheuserbacktothepreviouspagewithredirect()->back().Toperformthisinvocation,wewilladdtwomoremethodcalls:withInputwillsendthesubmittedvaluessothatwecandisplaythemagain,andwithErrorswillsendtheerrorsthesamewayAuthControllerdid.
Atthispoint,itwouldbehelpfultotheuserifweshowthepreviouslysubmittedtitleandtextincasethepostisnotvalid.Forthis,usethealreadyknownoldmethodintheview:
{{--...--}}
<inputtype="text"name="title"
value="{{old('title')}}"/>
</div>
<divclass="component">
<label>Text</label>
<textarearows="20"name="content">
{{old('content')}}
</textarea>
{{--...--}}
Atthispoint,wecanalreadytesthowthecontrollerbehaveswhenthepostdoesnotmatchtherequiredvalidations.Ifyoumissanyoftheparametersortheydonothavecorrectlengths,youwillgetanerrorpagesimilartothefollowingone:
Let’snowaddthelogictosavethepostincaseitisvalid.Ifyouremembertheinteractionwiththemodelsfromourpreviousapplication,youwillbegladlysurprisedathoweasyitistoworkwiththemhere.Takealookatthefollowing:
publicfunctioncreatePost(Request$request){
$validator=Validator::make($request->all(),[
'title'=>'required|max:255',
'content'=>'required|min:20',
]);
if($validator->fails()){
returnredirect()->back()
->withInput()
->withErrors($validator);
}
$post=newPost();
$post->title=$request->title;
$post->content=$request->content;
Auth::user()->posts()->save($post);
returnredirect('/new');
}
Thefirstthingwewilldoiscreateapostobjectsettingthetitleandcontentfromtherequestvalues.Then,giventheresultofAuth::user(),whichgivesustheinstanceofthecurrentlyauthenticatedusermodel,wewillsavethepostthatwejustcreatedthroughposts()->save($post).Ifwewantedtosavethepostwithouttheinformationoftheuser,wecoulduse$post->save().Really,thatisall.
Let’squicklyaddanotherendpointtoretrievethelistofpostsforagivenusersothatwecantakealookathowEloquentORMallowsustofetchdataeasily.Addthefollowingroute:
Route::get('/',['middleware'=>'auth',function(){
$posts=Auth::user()
->posts()
->orderBy('created_at')
->get();
returnview('posts',['posts'=>$posts]);
}]);
Thewayweretrievedataisverysimilartohowwesaveit.Weneedtheinstanceofamodel—inthiscase,theauthenticateduser—andwewilladdaconcatenationofmethodinvocationsthatwillinternallygeneratethequerytoexecute.Inthiscase,wewillaskforthepostsorderedbythecreationdate.Inordertosendinformationtotheview,weneedtopassasecondargument,whichwillbeanarrayofparameternamesandvalues.
Addthefollowingtemplateasresources/views/posts.blade.php,whichwilldisplaythelistofpostsfortheauthenticateduserasatable.Notehowwewillusethe$postobject,whichisaninstanceofthemodel,inthefollowingcode:
@extends('layouts.app')
@section('css')
<linkrel="stylesheet"href="{{URL::asset('css/posts.css')}}"
type="text/css">
@endsection
@section('content')
<h2>Yourposts</h2>
<table>
@foreach($postsas$post)
<tr>
<td>{{$post->title}}</td>
<td>{{$post->created_at}}</td>
<td>{{str_limit($post->content,100)}}</td>
</tr>
@endforeach
</table>
@endsection
Thelistsofpostsarefinallydisplayed.Theresultshouldbesomethingsimilartothefollowingscreenshot:
AddingtestsInaveryshorttime,wecreatedanapplicationthatallowsyoutoregister,login,andcreateandlistpostsfromscratch.WewillendthissectionbytalkingabouthowtotestyourLaravelapplicationwithPHPUnit.
ItisextremelyeasytowritetestsinLaravelasithasaveryniceintegrationwithPHPUnit.Thereisalreadyaphpunit.xmlfile,acustomizedTestCaseclass,customizedassertions,andplentyofhelpersinordertotestwiththedatabase.Italsoallowsyoutotestroutes,emulatingtheHTTPrequestinsteadoftestingthecontrollers.Wewillvisitallthesefeatureswhiletestingthecreationofnewposts.
Firstofall,weneedtoremovetests/ExampleTest.phpbecauseittestedthehomepage,andaswemodifiedit,itwillfail.Donotworry;thisisanexampletestthathelpsdeveloperstostarttesting,andmakingitfailisnotaproblematall.
Now,weneedtocreateournewtest.Todothis,wecaneitheraddthefilemanuallyorusethecommandlineandrunthefollowingcommand:
$phpartisanmake:testNewPostTest
Thiscommandcreatesthetests/NewPostTest.phpfile,whichextendsfromTestCase.Ifyouopenit,youwillnotethatthereisalreadyadummytest,whichyoucanalsoremove.Eitherway,youcanrunPHPUnittomakesureeverythingpasses.Youcandoitinthesamewaywedidpreviously,asfollows:
$./vendor/bin/phpunit
ThefirsttestwecanaddisonewherewetrytoaddanewpostbutthedatapassedbythePOSTparametersisnotvalid.Inthiscase,weshouldexpectthattheresponsecontainserrorsandolddata,sotheusercanedititinsteadofrewritingeverythingagain.AddthefollowingtesttotheNewPostTestclass:
<?php
classNewPostTestextendsTestCase
{
publicfunctiontestWrongParams(){
$user=factory(App\User::class)
->make(['email'=>'test@user.laravel']);
$this->be($user);
$this->call(
'POST',
'/new',
['title'=>'thetitle','content'=>'ojhkjhg']
);
$this->assertSessionHasErrors('content');
$this->assertHasOldInput();
}
}
Thefirstthingwecannoteinthetestisthecreationofauserinstanceusingafactory.Youcanpassanarraywithanyparameterthatyouwanttosettothemakeinvocation;otherwise,defaultswillbeused.Afterwegettheuserinstance,wewillsendittothebemethodtoletLaravelknowthatwewantthatusertobetheauthorizedoneforthistest.
Oncewesetthegroundsforthetest,wewillusethecallhelperthatwillemulatearealHTTPrequest.Tothismethod,wehavetosendtheHTTPmethod(inthiscase,POST),theroutetorequest,andoptionallytheparameters.Notethatthecallmethodreturnstheresponseobjectincaseyouneedit.
Wewillsendatitleandthecontent,butthissecondoneisnotlongenough,sowewillexpectsomeerrors.Laravelcomeswithseveralcustomizedassertions,especiallywhentestingthesekindsofresponses.Inthiscase,wecouldusetwoofthem:assertSessionHasErrors,whichcheckswhetherthereareanyflasherrorsinthesession(inparticular,theonesforthecontentparameter),andassertHasOldInput,whichcheckswhethertheresponsecontainsolddatainordertoshowitbacktotheuser.
Thesecondtestthatwewouldliketoaddisthecasewheretheuserpostsvaliddatasothatwecansavethepostinthedatabase.Thistestistrickierasweneedtointeractwiththedatabase,whichisusuallyanotaverypleasantexperience.However,Laravelgivesusenoughtoolstohelpusinthistask.ThefirstandmostimportantistoletPHPUnitknowthatwewanttousedatabasetransactionsforeachtest.Then,weneedtopersisttheauthenticateduserinthedatabaseastheposthasaforeignkeypointingtoit.Finally,weshouldassertthatthepostissavedinthedatabasecorrectly.AddthefollowingcodetotheNewPostTestclass:
useDatabaseTransactions;
//...
publicfunctiontestNewPost(){
$postParams=[
'title'=>'thetitle',
'content'=>'Inaplacefarfaraway.'
];
$user=factory(App\User::class)
->make(['email'=>'test@user.laravel']);
$user->save();
$this->be($user);
$this->call('POST','/new',$postParams);
$this->assertRedirectedTo('http://localhost/new');
$this->seeInDatabase('posts',$postParams);
}
TheDatabaseTransactionstraitwillmakethetesttostartatransactionatthebeginningandthenrollitbackoncethetestisdone,sowewillnotleavethedatabasewithdatafromtests.Savingtheauthenticateduserinthedatabaseisalsoaneasytaskastheresultofthe
factoryisaninstanceoftheuser’smodel,andwecanjustinvokethesavemethodonit.
TheassertRedirectedToassertionwillmakesurethattheresponsecontainsthevalidheadersthatredirecttheusertothespecifiedURL.Moreinterestingly,seeInDatabasewillverifythatthereisanentityinthepoststable,whichisthefirstargument,withthedataprovidedinthearray,whichisthesecondargument.
Therearequitealotofassertions,butasyoucannote,theyareextremelyuseful,reducingwhatcouldbealongtesttoaveryfewlines.Werecommendyoutovisittheofficialdocumentationforthefulllist.
TheSilexmicroframeworkAfteratasteofwhatLaravelcanofferyou,youmostlikelydonotwanttohearaboutminimalistmicroframeworks.Still,wethinkitisgoodtoknowmorethanoneframework.Youcangettoknowdifferentapproaches,bemoreversatile,andeveryonewillwantyouintheirteam.
WechoseSilexbecauseitisamicroframework,whichisverydifferentfromLaravel,andalsobecauseitispartoftheSymfonyfamily.WiththisintroductiontoSilex,youwilllearnhowtouseyoursecondframework,whichisofatotallydifferenttype,andyouwillbeonestepclosertoknowingSymfonyaswell,whichisoneofthebigplayers.
Whatisthebenefitofmicroframeworks?Well,theyprovidetheverybasics—thatis,arouter,asimpledependencyinjector,requesthelpers,andsoon,butthisistheendofit.Youhaveplentyofroomtochooseandbuildwhatyoureallyneed,includingexternallibrariesorevenyourownones.Thismeansthatyoucanhaveaframeworkspeciallycustomizedforeachdifferentproject.Infact,Silexprovidesahandfulofbuilt-inserviceprovidersthatyoucanintegrateveryeasily,fromtemplateenginestologgingorsecurity.
InstallationThere’snonewshere.Composerdoeseverythingforyou,asitdoeswithLaravel.ExecutethefollowingcommandonyourcommandlineattherootofyournewprojectinordertoincludeSilexinyourcomposer.jsonfile:
$composerrequiresilex/silex
Youmayrequiremoredependencies,butlet’saddthemwhenweneedthem.
ProjectsetupSilex’smostimportantclassisSilex\Application.Thisclass,whichextendsfromPimple(alightweightdependencyinjector),managesalmostanything.YoucanuseitasanarrayasitimplementstheArrayAccessinterface,oryoucouldinvokeitsmethodstoadddependencies,registerservices,andsoon.Thefirstthingtodoistoinstantiateitinyourpublic/index.phpfile,asfollows:
<?php
useSilex\Application;
require_once__DIR__.'/../vendor/autoload.php';
$app=newApplication();
ManagingconfigurationOneofthefirstthingsweliketodoisloadtheconfiguration.Wecoulddosomethingverysimple,suchasincludingafilewithPHPorJSONcontent,butlet’smakeuseofoneoftheserviceproviders,ConfigServiceProvider.Let’sadditwithComposerviathefollowingline:
$composerrequireigorw/config-service-provider
Thisserviceallowsustohavemultipleconfigurationfiles,oneforeachenvironmentweneed.Imaginingthatwewanttohavetwoenvironments,prodanddev,thismeansweneedtwofiles:oneinconfig/prod.jsonandoneinconfig/dev.json.Theconfig/dev.jsonfilewouldlooksimilartothis:
{
"debug":true,
"cache":false,
"database":{
"user":"dev",
"password":""
}
}
Theconfig/prod.jsonfilewouldlooksimilartothis:
{
"debug":false,
"cache":true,
"database":{
"user":"root",
"password":"fsd98na9nc"
}
}
Inordertoworkinadevelopmentenvironment,youwillneedtosetthecorrectvaluetotheenvironmentvariablebyrunningthefollowingcommand:
exportAPP_ENV=dev
TheAPP_ENVenvironmentvariablewillbetheonetellinguswhichenvironmentwearein.Now,itistimetousethisserviceprovider.Inordertoregisteritbyreadingfromtheconfigurationfileofthecurrentenvironment,addthefollowinglinestoyourindex.phpfile:
$env=getenv('APP_ENV')?:'prod';
$app->register(
newIgorw\Silex\ConfigServiceProvider(
__DIR__."/../config/$env.json"
)
);
Thefirstthingwedidhereistogettheenvironmentfromtheenvironmentvariable.Bydefault,wesetittoprod.Then,weinvokedregisterfromthe$appobjecttoaddaninstanceofConfigServiceProviderbypassingthecorrectconfigurationfilepath.Fromnowon,the$app“array”willcontainthreeentries:debug,cache,anddbwiththecontentoftheconfigurationfiles.Wewillbeabletoaccessthemwheneverwehaveaccessto$app,whichwillbemostlyeverywhere.
SettingthetemplateengineAnotherofthehandyserviceprovidersisTwig.Asyoumightremember,Twigisthetemplateenginethatweusedinourownframework,anditis,infact,fromthesamepeoplethatdevelopedSymfonyandSilex.YoualsoalreadyknowhowtoaddthedependencywithComposer;simplyrunthefollowing:
$composerrequiretwig/twig
Toregistertheservice,wewillneedtoaddthefollowinglinesinourpublic/index.phpfile:
$app->register(
newSilex\Provider\TwigServiceProvider(),
['twig.path'=>__DIR__.'/../views']
);
Also,createtheviews/directorywherewewilllaterstoreourtemplates.Now,youhavetheTwig_Environmentinstanceavailablebyjustaccessing$app['twig'].
AddingaloggerThelastoneoftheserviceprovidersthatwewillregisterfornowisthelogger.Thistime,thelibrarytouseisMonolog,andyoucanincludethisviathefollowing:
$composerrequiremonolog/monolog
Thequickestwaytoregisteraserviceisbyjustprovidingthepathofthelogfile,whichcanbedoneasfollows:
$app->register(
newSilex\Provider\MonologServiceProvider(),
['monolog.logfile'=>__DIR__.'/../app.log']
);
Ifyouwouldliketoaddmoreinformationtothisserviceprovider,suchaswhatleveloflogsyouwanttosave,thenameofthelog,andsoon,youcanaddthemtothearraytogetherwiththelogfile.Takealookatthedocumentationathttp://silex.sensiolabs.org/doc/providers/monolog.htmlforthefulllistofparametersavailable.
Aswiththetemplateengine,fromnowon,youcanaccesstheMonolog\LoggerinstancefromtheApplicationobjectbyaccessing$app['monolog'].
AddingthefirstendpointItistimetoseehowtherouterworksinSilex.Wewouldliketoaddasimpleendpointforthehomepage.Aswealreadymentioned,the$appinstancecanmanagealmostanything,includingroutes.Addthefollowingcodeattheendofthepublic/index.phpfile:
$app->get('/',function(Application$app){
return$app['twig']->render('home.twig');
});
ThisisasimilarwayofaddingroutestotheonethatLaravelfollows.WeinvokedthegetmethodasitisaGETendpoint,andwepassedtheroutestringandtheApplicationinstance.Aswementionedhere,$appalsoactsasadependencyinjector—infact,itextendsfromone:Pimple—soyouwillnoticetheApplicationinstancealmosteverywhere.Theresultoftheanonymousfunctionwillbetheresponsethatwewillsendtotheuser—inthiscase,arenderedTwigtemplate.
Rightnow,thiswillnotdothetrick.InordertoletSilexknowthatyouaredonesettingupyourapplication,youneedtoinvoketherunmethodattheveryendofthepublic/index.phpfile.Rememberthatifyouneedtoaddanythingelsetothisfile,ithastobebeforethisline:
$app->run();
YouhavealreadyworkedwithTwig,sowewillnotspendtoomuchtimeonthis.Thefirstthingtoaddistheviews/home.twigtemplate:
{%extends"layout.twig"%}
{%blockcontent%}
<h1>Hivisitor!</h1>
{%endblock%}
Now,asyoumighthavealreadyguessed,wewilladdtheviews/layout.twigtemplate,asfollows:
<html>
<head>
<title>SilexExample</title>
</head>
<body>
{%blockcontent%}
{%endblock%}
</body>
</html>
Tryaccessingthehomepageofyourapplication;youshouldgetthefollowingresult:
AccessingthedatabaseForthissection,wewillwriteanendpointthatwillcreaterecipesforourcookbook.RunthefollowingMySQLqueriesinordertosetupthecookbookdatabaseandcreatetheemptyrecipestable:
mysql>CREATESCHEMAcookbook;
QueryOK,1rowaffected(0.00sec)
mysql>USEcookbook;
Databasechanged
mysql>CREATETABLErecipes(
->idINTUNSIGNEDNOTNULLAUTO_INCREMENTPRIMARYKEY,
->nameVARCHAR(255)NOTNULL,
->ingredientsTEXTNOTNULL,
->instructionsTEXTNOTNULL,
->timeINTUNSIGNEDNOTNULL);
QueryOK,0rowsaffected(0.01sec)
SilexdoesnotcomewithanyORMintegration,soyouwillneedtowriteyourSQLqueriesbyhand.However,thereisaDoctrineserviceproviderthatgivesyouasimplerinterfacethantheonePDOoffers,solet’strytointegrateit.Toinstallthis,runthefollowingcommand:
$composerrequire"doctrine/dbal:~2.2"
Now,wearereadytoregistertheserviceprovider.Aswiththerestofservices,addthefollowingcodetoyourpublic/index.phpbeforetheroutedefinitions:
$app->register(newSilex\Provider\DoctrineServiceProvider(),[
'dbs.options'=>[
[
'driver'=>'pdo_mysql',
'host'=>'127.0.0.1',
'dbname'=>'cookbook',
'user'=>$app['database']['user'],
'password'=>$app['database']['password']
]
]
]);
Whenregistering,youneedtoprovidetheoptionsforthedatabaseconnection.Someofthemwillbethesameregardlessoftheenvironment,suchasthedriveroreventhehost,butsomewillcomefromtheconfigurationfile,suchas$app['database']['user'].Fromnowon,youcanaccessthedatabaseconnectionvia$app['db'].
Withthedatabasesetup,let’saddtheroutesthatwillallowustoaddandfetchrecipes.AswithLaravel,youcanspecifyeithertheanonymousfunction,aswealreadydid,oracontrollerandmethodtoexecute.Replacethecurrentroutewiththefollowingthreeroutes:
$app->get(
'/',
'CookBook\\Controllers\\RecipesController::getAll'
);
$app->post(
'/recipes',
'CookBook\\Controllers\\RecipesController::create'
);
$app->get(
'/recipes',
'CookBook\\Controllers\\RecipesController::getNewForm'
);
Asyoucanobserve,therewillbeanewcontroller,CookBook\Controllers\RecipesController,whichwillbeplacedinsrc/Controllers/RecipesController.php.ThismeansthatyouneedtochangetheautoloaderinComposer.Edityourcomposer.jsonfilewiththefollowing:
"autoload":{
"psr-4":{"CookBook\\":"src/"}
}
Now,let’saddthecontrollerclass,asfollows:
<?php
namespaceCookBook\Controllers;
classRecipes{
}
ThefirstmethodwewilladdisthegetNewFormmethod,whichwilljustrendertheaddanewrecipepage.Themethodlookssimilartothis:
publicfunctiongetNewForm(Application$app):string{
return$app['twig']->render('new_recipe.twig');
}
Themethodwilljustrendernew_recipe.twig.Anexampleofthistemplatecouldbeasfollows:
{%extends"layout.twig"%}
{%blockcontent%}
<h1>Addrecipe</h1>
<formmethod="post">
<div>
<labelfor="name">Name</label>
<inputtype="text"name="name"
value="{{nameisdefined?name:""}}"/>
</div>
<div>
<labelfor="ingredients">Ingredients</label>
<textareaname="ingredients">
{{ingredientsisdefined?ingredients:""}}
</textarea>
</div>
<div>
<labelfor="instructions">Instructions</label>
<textareaname="instructions">
{{instructionsisdefined?instructions:""}}
</textarea>
</div>
<div>
<labelfor="time">Time(minutes)</label>
<inputtype="number"name="time"
value="{{timeisdefined?time:""}}"/>
</div>
<div>
<buttontype="submit">Save</button>
</div>
</form>
{%endblock%}
Thistemplatesendsthename,ingredients,instructions,andthetimethatittakestopreparethedish.Theendpointthatwillgetthisformneedstogettheresponseobjectinordertoextractthisinformation.InthesamewaythatwecouldgettheApplicationinstanceasanargument,wecangettheRequestonetooifwespecifyitinthemethoddefinition.AccessingthePOSTparametersisaseasyasinvokingthegetmethodbysendingthenameoftheparameterorcalling$request->request->all()togetallofthemasanarray.Addthefollowingmethodthatcheckswhetherallthedataisvalidandrenderstheformagainifitisnot,sendingthesubmitteddataanderrors:
publicfunctioncreate(Application$app,Request$request):string{
$params=$request->request->all();
$errors=[];
if(empty($params['name'])){
$errors[]='Namecannotbeempty.';
}
if(empty($params['ingredients'])){
$errors[]='Ingredientscannotbeempty.';
}
if(empty($params['instructions'])){
$errors[]='Instructionscannotbeempty.';
}
if($params['time']<=0){
$errors[]='Timehastobeapositivenumber.';
}
if(!empty($errors)){
$params=array_merge($params,['errors'=>$errors]);
return$app['twig']->render('new_recipe.twig',$params);
}
}
Thelayout.twigtemplateneedstobeeditedtooinordertoshowtheerrorsreturned.Wecandothisbyexecutingthefollowing:
{#...#}
{%iferrorsisdefined%}
<p>Somethingwentwrong!</p>
<ul>
{%forerrorinerrors%}
<li>{{error}}</li>
{%endfor%}
</ul>
{%endif%}
{%blockcontent%}
{#...#}
Atthispoint,youcanalreadytrytoaccesshttp://localhost/recipes,filltheformleavingsomethingempty,submitting,andgettingtheformbackwiththeerrors.Itshouldlooksomethingsimilartothis(withsomeextraCSSstyles):
Thecontinuationofthecontrollershouldallowustostorethecorrectdataasanewrecipeinthedatabase.Todoso,itwouldbeagoodideatocreateaseparateclass,suchasCookBook\Models\RecipeModel;however,tospeedthingsup,let’saddthefollowingfewlinesthatwouldgointothemodeltothecontroller.RememberthatwehavetheDoctrineserviceprovider,sothereisnoneedtousePDOdirectly:
$sql='INSERTINTOrecipes(name,ingredients,instructions,time)'
.'VALUES(:name,:ingredients,:instructions,:time)';
$result=$app['db']->executeUpdate($sql,$params);
if(!$result){
$params=array_merge($params,['errors'=>$errors]);
return$app['twig']->render('new_recipe.twig',$params);
}
return$app['twig']->render('home.twig');
Doctrinealsohelpswhenfetchingdata.Toseeitworking,checkthethirdandfinalmethod,inwhichwewillfetchalltherecipesinordertoshowtheuser:
publicfunctiongetAll(Application$app):string{
$recipes=$app['db']->fetchAll('SELECT*FROMrecipes');
return$app['twig']->render(
'home.twig',
['recipes'=>$recipes]
);
}
Withonlyoneline,weperformedaquery.ItisnotascleanastheEloquentORMofLaravel,butatleastitismuchlessverbosethanusingrawPDO.Finally,youcanupdateyourhome.twigtemplatewiththefollowingcontentinordertodisplaytherecipesthatwejustfetchedfromthedatabase:
{%extends"layout.twig"%}
{%blockcontent%}
<h1>Hivisitor!</h1>
<p>Checkourrecipes!</p>
<table>
<th>Name</th>
<th>Time</th>
<th>Ingredients</th>
<th>Instructions</th>
{%forrecipeinrecipes%}
<tr>
<td>{{recipe.name}}</td>
<td>{{recipe.time}}</td>
<td>{{recipe.ingredients}}</td>
<td>{{recipe.instructions}}</td>
</tr>
{%endfor%}
</table>
{%endblock%}
SilexversusLaravelEventhoughwedidsomesimilarcomparisonbeforestartingthechapter,itistimetorecapitulatewhatwesaidandcompareitwithwhatyounotedbyyourself.Laravelbelongstothetypeofframeworkthatallowsyoutocreategreatthingswithverylittlework.Itcontainsallthecomponentsthatyou,asawebdeveloper,willeverneed.Therehastobesomegoodreasonforhowfastitbecamethemostpopularframeworkoftheyear!
Ontheotherhand,Silexisamicroframework,whichbyitselfdoesverylittle.Itisjusttheskeletononwhichyoucanbuildtheframeworkthatyouexactlyneed.Italreadyprovidesquitealotofserviceproviders,andwedidnotdiscussevenhalfofthem;werecommendyoutovisithttp://silex.sensiolabs.org/doc/providers.htmlforthefulllist.However,ifyouprefer,youcanalwaysaddotherdependencieswithComposerandusethem.If,forsomereason,youstoplikingtheORMorthetemplateenginethatyouuse,oritjusthappensthatanewandbetteroneappearsinthecommunity,switchingthemshouldbeeasy.Ontheotherhand,whenworkingwithLaravel,youwillprobablysticktowhatitcomeswithit.
Thereisalwaysanoccasionforeachframework,andwewouldliketoencourageyoutobeopentoallthepossibilitiesthatthereareoutthere,keepuptodate,andexplorenewframeworksortechnologiesfromtimetotime.
SummaryInthischapter,youlearnedhowimportantitistoknowsomeofthemostimportantframeworks.Youalsolearnedthebasicsoftwofamousones:LaravelandSilex.Now,youarereadytoeitheruseyourframeworkortousethesetwoforyournextapplication.Withthis,youalsohavethecapacitytotakeanyothersimilarframeworkandunderstanditeasily.
Inthenextchapter,wewillstudywhatRESTAPIsareandhowtowriteonewithLaravel.Thiswillexpandyoursetofskillsandgiveyoumoreflexibilityforwhenyouneedtodecidewhichapproachtotakewhendesigningandwritingapplications.
Chapter9.BuildingRESTAPIsMostnon-developersprobablythinkthatcreatingapplicationsmeansbuildingeithersoftwareforyourPCorMac,games,orwebpages,becausethatiswhattheycanseeanduse.Butonceyoujointhedevelopers’community,eitherbyyourownorprofessionally,youwilleventuallyrealizehowmuchworkisdoneforapplicationsandtoolsthatdonothaveauserinterface.
Haveyoueverwonderedhowsomeone’swebsitecanaccessyourFacebookprofile,andlateron,postanautomaticmessageonyourwall?Orhowwebsitesmanagetosend/receiveinformationinordertoupdatethecontentofthepage,withoutrefreshingorsubmittinganyform?Allofthesefeatures,andmanymoreinterestingones,arepossiblethankstotheintegrationofapplicationsworking“behindthescenes”.Knowinghowtousethemwillopenthedoorsforcreatingmoreinterestingandusefulwebapplications.
Inthischapter,youwilllearnthefollowing:
IntroductiontoAPIsandRESTAPIs,andtheiruseThefoundationofRESTAPIsUsingthird-partyAPIsToolsforRESTAPIdevelopersDesigningandwritingRESTAPIswithLaravelDifferentwaysoftestingyourRESTAPIs
IntroducingAPIsAPIstandsforApplicationProgramInterface.Itsgoalistoprovideaninterfacesothatotherprogramscansendcommandsthatwilltriggersomeprocessinsidetheapplication,possiblyreturningsomeoutput.Theconceptmightseemabitabstract,butinfact,thereareAPIsvirtuallyineverythingwhichissomehowrelatedtocomputers.Let’sseesomereallifeexamples:
OperatingsystemsorOS,likeWindowsorLinux,aretheprogramsthatallowyoutousecomputers.Whenyouuseanyapplicationfromyourcomputer,itmostprobablyneedstotalktotheOSinonewayoranother,forexamplebyrequestingacertainfile,sendingsomeaudiotothespeakers,andsoon.AlltheseinteractionsbetweentheapplicationandtheOSarepossiblethankstotheAPIsthattheOSprovides.Inthisway,theapplicationneednotinteractwiththehardwarestraightaway,whichisaverytiringtask.Tointeractwiththeuser,amobileapplicationprovidesaGUI.Theinterfacecapturesalltheeventsthattheusertriggers,likeclickingortyping,inordertosendthemtotheserver.TheGUIcommunicateswiththeserverusinganAPIinthesamewaytheprogramcommunicateswiththeOSasexplainedearlier.Whenyoucreateawebsitethatneedstodisplaytweetsfromtheuser’sTwitteraccount,youneedtocommunicatewithTwitter.TheyprovideanAPIthatcanbeaccessedviaHTTP.Onceauthenticated,bysendingthecorrectHTTPrequests,youcanupdateand/orretrievedatafromtheirapplication.
Asyoucansee,therearedifferentplaceswhereAPIsareuseful.Ingeneral,whenyouhaveasystemthatshouldbeaccessedexternally,youneedtoprovidepotentialusersanAPI.Whenwesayexternally,wemeanfromanotherapplicationorlibrary,butitcanverywellbeinsidethesamemachine.
IntroducingRESTAPIsRESTAPIsareaspecifictypeofAPIs.TheyuseHTTPastheprotocoltocommunicatewiththem,soyoucanimaginethattheywillbethemostusedonesbywebapplications.Infact,theyarenotverydifferentfromthewebsitesthatyou’vealreadybuilt,sincetheclientsendsanHTTPrequest,andtheserverreplieswithanHTTPresponse.ThedifferencehereisthatRESTAPIsmakeheavyuseofHTTPstatuscodestounderstandwhattheresponseis,andinsteadofreturningHTMLresourceswithCSSandJS,theresponseusesJSON,XML,oranyotherdocumentformatwithjustinformation,andnotagraphicuserinterface.
Let’stakeanexample.TheTwitterAPI,onceauthenticated,allowsdeveloperstogetthetweetsofagivenuserbysendinganHTTPGETrequesttohttps://api.twitter.com/1.1/statuses/user_timeline.json.TheresponsetothisrequestisanHTTPmessagewithaJSONmapoftweetsasthebodyandthestatuscode200.We’vealreadymentionedstatuscodeinChapter2,WebApplicationswithPHP,butwewillreviewthemshortly.
TheRESTAPIalsoallowsdeveloperstoposttweetsonbehalfoftheuser.Ifyouwerealreadyauthenticated,asinthepreviousexample,youjustneedtosendaPOSTrequesttohttps://api.twitter.com/1.1/statuses/update.jsonwiththeappropriatePOSTparametersinthebody,likethetextthatyouwanttotweet.EventhoughthisrequestisnotaGET,andthus,youarenotrequestingdatabutrathersendingit,theresponseofthisrequestisquiteimportanttoo.Theserverwillusethestatuscodesoftheresponsetolettherequesterknowifthetweetwaspostedsuccessfully,oriftheycouldnotunderstandtherequest,therewasaninternalservererror,theauthenticationwasnotvalid,andsoon.Eachofthesescenarioshasadifferentstatuscode,whichisthesameacrossallapplications.ThismakesitveryeasytocommunicatewithdifferentAPIs,sinceyouwillnotneedtolearnanewlistofstatuscodeeachtime.Theservercanalsoaddsomeextrainformationtothebodyinordertothrowsomelightonwhytheerrorhappened,butthatwilldependontheapplication.
YoucanimaginethattheseRESTAPIsareprovidedtodeveloperssotheycanintegratethemwiththeirapplications.Theyarenotuser-friendly,butHTTP-friendly.
ThefoundationsofRESTAPIsEventhoughRESTAPIsdonothaveanofficialstandard,mostdevelopersagreeonthesamefoundation.IthelpsthatHTTP,whichistheprotocolthatthistechnologyusestocommunicate,doeshaveastandard.Inthissection,wewilltrytodescribehowRESTAPIsshouldwork.
HTTPrequestmethodsWe’vealreadyintroducedtheideaofHTTPmethodsinChapter2,WebApplicationswithPHP.WeexplainedthatanHTTPmethodisjusttheverboftherequest,whichdefineswhatkindofactionitistryingtoperform.We’vealreadydefinedthismethodwhenworkingwithHTMLforms:theformtagcangetanoptionalattribute,method,whichwillmaketheformsubmitwiththatspecificHTTPmethod.
YouwillnotuseformswhenworkingwithRESTAPIs,butyoucanstillspecifythemethodoftherequest.Infact,tworequestscangotothesameendpointwiththesameparameters,headers,andsoon,andyethavecompletelydifferentbehaviorsduetotheirmethods,whichmakesthemaveryimportantpartoftherequest.
AswearegivingsomuchimportancetoHTTPmethodsinordertoidentifywhatarequestistryingtodo,itisnaturalthatwewillneedahandfulofthem.Sofar,wehaveintroducedGETandPOST,butthereareactuallyeightdifferentmethods:GET,POST,PUT,DELETE,OPTIONS,HEAD,TRACE,andCONNECT.Youwillusuallyworkwithjustfourofthem.Let’slookatthemindetail.
GETWhenarequestusestheGETmethod,itmeansthatitisrequestingforinformationaboutagivenentity.Theendpointshouldcontaininformationofwhatthatentityis,liketheIDofabook.GETcanalsobeusedtoqueryforalistofobjects,eitherallofthem,filtered,orpaginated.
GETrequestscanaddextrainformationtotherequestwhenneeded.Forexample,ifwearetrytoretrieveallthebooksthatcontainthestring“rings”,orifwewantthepagenumber2ofthefulllistofbooks.Asyoualreadyknow,thisextrainformationisaddedtothequerystringasGETparameters,whichisalistofkey-valuepairsconcatenatedbyanampersand(&).So,thatmeansthattherequestforhttp://bookstore.com/books?year=2001&page3isprobablyusedforgettingthesecondpageofthelistofbookspublishedduring2001.
RESTAPIshaveextensivedocumentationontheavailableendpointsandparameters,soitshouldbeeasyforyoutolearntoqueryproperly.Still,eventhoughitwillbedocumented,youshouldexpectparameterswithintuitivenames,liketheonesintheexample.
POSTandPUTPOSTisthesecondtypeofHTTPmethodthatyoualreadyknowabout.Youuseditinformswiththeintentionof“posting”data,thatis,tryingtoupdatearesourceontheserverside.Whenyouwantedtoaddorupdateanewbook,yousentaPOSTrequestwiththedataofthebookasthePOSTparameters.
POSTparametersaresentinaformatsimilartotheGETparameters,butinsteadofbeingpartofthequerystring,theyareincludedaspartoftherequest’sbody.FormsinHTMLarealreadydoingthatforyou,butwhenyouneedtotalktoaRESTAPI,youshouldknowhowtodothisbyyourself.Inthenextsection,wewillshowyouhowtoperform
POSTusingtoolsotherthanforms.Alsonotethatyoucanaddanydatatothebodyoftherequest;itisquitecommontosendJSONinthebodyinsteadofPOSTparameters.
ThePUTmethodisquitesimilartothePOSTmethod.Thistootriestoaddorupdatedataontheserverside,andforthispurpose,italsoaddsextrainformationonthebodyoftherequest.Whyshouldwehavetwodifferentmethodsthatdothesamething?Thereareactuallytwomaindifferencesbetweenthesemethods:
PUTrequestseithercreatearesourceorupdateit,buttheaffectedresourceistheonedefinedbytheendpointandnothingelse.Thatmeansthatifwewanttoupdateabook,theendpointshouldstatethattheresourceisabook,andspecifyit,forexample,http://bookstore.com/books/8734.Ontheotherhand,ifyoudonotidentifytheresourcetobecreatedorupdatedintheendpoint,oryouaffectotherresourcesatthesametime,youshouldusePOSTrequests.Idempotentisacomplicatedwordforasimpleconcept.AnidempotentHTTPmethodisonethatcanbecalledmanytimes,andtheresultwillalwaysbethesame.Forexample,ifyouaretryingtoupdatethetitleofabookto“DonQuixote”,itdoesnotmatterhowmanytimesyoucallit,theresultwillalwaysbethesame:theresourcewillhavethetitle“DonQuixote”.Ontheotherhand,non-idempotentmethodsmightreturndifferentresultswhenexecutingthesamerequest.Anexamplecouldbeanendpointthatincreasesthestockofsomebook.Eachtimeyoucallit,youwillincreasethestockmoreandmore,andthus,theresultisnotthesame.PUTrequestsareidempotent,whereasPOSTrequestsarenot.
Evenwiththisexplanationinmind,misusingPOSTandPUTisquiteacommonmistakeamongdevelopers,especiallywhentheylackenoughexperienceindevelopingRESTAPIs.SinceformsinHTMLonlysenddatawithPOSTandnotPUT,thefirstoneismorepopular.YoumightfindRESTAPIswherealltheendpointsthatupdatedataarePOST,eventhoughsomeofthemshouldbePUT.
DELETETheDELETEHTTPmethodisquiteself-explanatory.Itisusedwhenyouwanttodeletearesourceontheserver.AswithPUTrequests,DELETEendpointsshouldidentifythespecificresourcetobedeleted.Anexamplewouldbewhenwewanttoremoveonebookfromourdatabase.WecouldsendaDELETErequesttoanendpointsimilartohttp://bookstore.com/books/23942.
DELETErequestsjustdeleteresources,andtheyarealreadydeterminedbytheURL.Still,ifyouneedtosendextrainformationtotheserver,youcouldusethebodyoftherequestasyoudowithPOSTorPUT.Infact,youcanalwayssendinformationwithinthebodyoftherequest,includingGETrequests,butthatdoesnotmeanitisagoodpracticetodoso.
StatuscodesinresponsesIfHTTPmethodsareveryimportantforrequests,statuscodesarealmostindispensableforresponses.Withjustonenumber,theclientwillknowwhathappenedwiththerequest.Thisisespeciallyusefulwhenyouknowthatstatuscodesareastandard,andtheyareextensivelydocumentedontheInternet.
We’vealreadydescribedthemostimportantonesinChapter2,WebApplicationswithPHP,butlet’slistthemagain,addingafewmorethatareimportantforRESTAPIs.Forthefulllistofstatuscodes,youcanvisithttps://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
2xx–successAllthestatuscodesthatstartwith2areusedforresponseswheretherequestwasprocessedsuccessfully,regardlessofwhetheritwasaGETorPOST.Someofthemostcommonlyusedonesinthiscategoryareasfollows:
200OK:Itisthegeneric“everythingwasOK”response.Ifyouwereaskingforaresource,youwillgetitinthebodyoftheresponse,andifyouwereupdatingaresource,thiswillmeanthatthenewdatahasbeensuccessfullysaved.201created:ItistheresponseusedwhenresourcesarecreatedsuccessfullywithPOSTorPUT.202accepted:Thisresponsemeansthattherequesthasbeenaccepted,butithasnotbeenprocessedyet.Thismightbeusefulwhentheclientneedsastraightforwardresponseforaveryheavyoperation:theserversendstheacceptedresponse,andthenstartsprocessingit.
3xx–redirectionEventhoughyoumightthinkthereisonlyonetypeofredirection,thereareafewrefinements:
301movedpermanently:ThismeansthattheresourcehasbeenmovedtoadifferentURL,sofromthenon,youshouldtrytoaccessitthroughtheURLprovidedinthebodyoftheresponse.303seeother:Thismeansthattherequesthasbeenprocessedbut,inordertoseetheresponse,youneedtoaccesstheURLprovidedinthebodyoftheresponse.
4xx–clienterrorThiscategoryhasstatuscodesdescribingwhatwentwrongduetotheclient’srequest:
400badrequest:Thisisagenericresponsetoamalformedrequest,thatis,thereisasyntaxerrorintheendpoint,orsomeoftheexpectedparameterswerenotprovided.401unauthorized:Thismeanstheclienthasnotbeenauthenticatedsuccessfullyyet,andtheresourcethatitistryingtoaccessneedsthisauthentication.403forbidden:Thiserrormessagemeansthateventhoughtheclienthasbeenauthenticated,itdoesnothaveenoughpermissionstoaccessthatresource.404notfound:Thespecificresourcehasnotbeenfound.
405methodnotallowed:Thismeansthattheendpointexists,butitdoesnotaccepttheHTTPmethodusedontherequest,forexample,weweretryingtousePUT,buttheendpointonlyacceptsPOSTrequests.
5xx–servererrorThereareupto11differenterrorsontheserverside,butweareonlyinterestedinone:the500internalservererror.Youcouldusethisstatuscodewhensomethingunexpected,likeadatabaseerror,happenswhileprocessingtherequest.
RESTAPIsecurityRESTAPIsareapowerfultoolsincetheyallowdeveloperstoretrieveand/orupdatedatafromtheserver.Butwithgreatpowercomesgreatresponsibility,andwhendesigningaRESTAPI,youshouldthinkaboutmakingyourdataassecureaspossible.Imagine—anyonecouldposttweetsonyourbehalfwithasimpleHTTPrequest!
Similartousingwebapplications,therearetwoconceptshere:authenticationandauthorization.Authenticatingsomeoneisidentifyingwhoheorsheis,thatis,linkinghisorherrequesttoauserinthedatabase.Ontheotherhand,authorizingsomeoneistoallowthatspecificusertoperformcertainactions.Youcouldthinkofauthenticationastheloginoftheuser,andauthorizationasgivingpermissions.
RESTAPIsneedtomanagethesetwoconceptsverycarefully.Justbecauseadeveloperhasbeenauthenticateddoesnotmeanhecanaccessallthedataontheserver.Sometimes,userscanaccessonlytheirowndata,whereassometimesyouwouldliketoimplementarolessystemwhereeachrolehasdifferentaccesslevels.Italwaysdependsonthetypeofapplicationyouarebuilding.
Althoughauthorizationhappensontheserverside,thatis,it’stheserver’sdatabasethatwilldecidewhetheragivenusercanaccessacertainresourceornot,authenticationshavetobetriggeredbytheclient.ThismeansthattheclienthastoknowwhatauthenticationsystemtheRESTAPIisusinginordertoproceedwiththeauthentication.EachRESTAPIwillimplementitsownauthenticationsystem,buttherearesomewellknownimplementations.
BasicaccessauthenticationBasicaccessauthentication—BAforshort—is,asitsnamesuggests,basic.Theclientaddstheinformationabouttheuserintheheadersofeachrequest,thatis,usernameandpassword.TheproblemisthatthisinformationisonlyencodedusingBASE64butnotencrypted,makingitextremelyeasyforanintrudertodecodetheheaderandobtainthepasswordinplaintext.Ifyoueverhavetouseit,since,tobehonest,itisaveryeasywayofimplementingsomesortofauthentication,wewouldrecommendyoutouseitwithHTTPS.
Inordertousethismethod,youneedtoconcatenatetheusernameandpasswordlikeusername:password,encodetheresultantstringusingBase64,andaddtheauthorizationheaderas:
Authorization:Basic<encoded-string>
OAuth2.0Ifbasicauthenticationwasverysimple,andinsecure,OAuth2.0isthemostsecuresystemthatRESTAPIsuseinordertoauthenticate,andsowasthepreviousOAuth1.0.Thereareactuallydifferentversionsofthisstandard,butallofthemworkonthesamefoundation:
1. Therearenousernamesandpasswords.Instead,theprovideroftheRESTAPIassignsapairofcredentials—atokenandthesecret—tothedeveloper.
2. Inordertoauthenticate,thedeveloperneedstosendaPOSTrequesttothe“token”endpoint,whichisdifferentineachRESTAPIbuthasthesameconcept.Thisrequesthastoincludetheencodeddevelopercredentials.
3. Theserverrepliestothepreviousrequestwithasessiontoken.This(andnotthecredentialsmentionedinthefirststep)istobeincludedineachrequestthatyoumaketotheRESTAPI.Thesessiontokenexpiresforsecurityreasons,soyouwillhavetorepeatthesecondstepagainwhenthathappens.
Eventhoughthisstandardiskindofrecent(2012onwards),severalbigcompanieslikeGoogleorFacebookhavealreadyimplementeditfortheirRESTAPIs.Itmightlookabitovercomplicated,butyouwillsoongettouseit,andevenimplementit.
Usingthird-partyAPIsThatwasenoughtheoryaboutRESTAPIs;itistimetodiveintoarealworldexample.Inthissection,wewillwriteasmallPHPapplicationthatinteractswithTwitter’sRESTAPI;thatincludesrequestingdevelopercredentials,authenticating,andsendingrequests.ThegoalistogiveyouyourfirstexperienceinworkingwithRESTAPIs,andshowingyouthatitiseasierthanyoucouldexpect.Itwillalsohelpyoutounderstandbetterhowtheywork,soitwillbeeasiertobuildyourownlater.
Gettingtheapplication’scredentialsRESTAPIsusuallyhavetheconceptofapplication.AnapplicationislikeanaccountontheirdevelopmentsitethatidentifieswhousestheAPI.ThecredentialsthatyouwillusetoaccesstheAPIwillbelinkedtothisapplication,whichmeansthatyoucanhavemultipleapplicationslinkedtothesameaccount.
AssumingthatyouhaveaTwitteraccount,gotohttps://apps.twitter.cominordertocreateanewapplication.ClickontheCreateNewAppbuttoninordertoaccesstheformforapplicationdetails.Thefieldsareveryself-explanatory—justanamefortheapplication,thedescription,andthewebsiteURL.ThecallbackURLisnotnecessaryhere,sincethatwillbeusedonlyforapplicationsthatrequireaccesstosomeoneelse’saccount.Agreewiththetermsandconditionsinordertoproceed.
Onceyouhavebeenredirectedtoyourapplication’spage,youwillseeallsortofinformationthatyoucanedit.Sincethisisjustanexample,let’sgostraighttowhatmatters:thecredentials.ClickontheKeysandAccessTokenstabtoseethevaluesofConsumerkey(APIkey)andConsumerSecret(APIsecret).Thereisnothingelsethatweneedfromhere.Youcansavethemonyourfilesystem,as~/.twitter_php7.json,forexample:
{
"key":"iTh4Mzl0EAPn9HAm98hEhAmVEXS",
"secret":"PfoWM9yq4Bh6rGbzzJhr893j4r4sMIAeVRaPMYbkDer5N6F"
}
TipSecuringyourcredentials
SecuringyourRESTAPIcredentialsshouldbetakenseriously.Infact,youshouldtakecareofallkindsofcredentials,likethedatabaseones.Butthedifferenceisthatyouwillusuallyhostyourdatabaseinyourserver,whichmakesthingsslightlymoredifficulttowhoeverwantstoattack.Ontheotherhand,thethird-partyRESTAPIisnotpartofyoursystem,andsomeonewithyourcredentialscanuseyouraccountfreelyonyourbehalf.
Neverincludeyourcredentialsinyourcodebase,especiallyifyouhaveyourcodeinGitHuborsomeotherrepository.Onesolutionwouldbetohaveafileinyourserver,outsideyourcode,withthecredentials;ifthatfileisencrypted,thatisevenbetter.Andtrytorefreshyourcredentialsregularly,whichyoucanprobablydoontheprovider’swebsite.
SettinguptheapplicationOurapplicationwillbeextremelysimple.Itwillconsistofoneclassthatwillallowustofetchtweets.Thiswillbemanagedbyourapp.phpscript.
AswehavetomakeHTTPrequests,wecaneitherwriteourownfunctionsthatusecURL(asetofPHPnativefunctions),ormakeuseofthefamousPHPlibrary,Guzzle.ThislibrarycanbefoundinPackagist,sowewilluseComposertoincludeit:
$composerrequireguzzlehttp/guzzle
WewillhaveaTwitterclass,whichwillgetthecredentialsfromtheconstructor,andonepublicmethod:fetchTwits.Fornow,justcreatetheskeletonsothatwecanworkwithit;wewillimplementsuchmethodsinlatersections.Addthefollowingcodetosrc/Twitter.php:
<?php
namespaceTwitterApp;
classTwitter{
private$key;
private$secret;
publicfunction__construct(String$key,String$secret){
$this->key=$key;
$this->secret=$secret;
}
publicfunctionfetchTwits(stringname,int$count):array{
return[];
}
}
SincewesetthenamespaceTwitterApp,weneedtoupdateourcomposer.jsonfilewiththefollowingaddition.Remembertoruncomposerupdatetoupdatetheautoloader.
"autoload":{
"psr-4":{"TwitterApp\\":"src"}
}
Finally,wewillcreateabasicapp.phpfile,whichincludestheComposerautoloader,readsthecredentialsfile,andcreatesaTwitterinstance:
<?php
useTwitterApp\Twitter;
require__DIR__.'/vendor/autoload.php';
$path=$_SERVER['HOME'].'/.twitter_php7.json';
$jsonCredentials=file_get_contents($path);
$credentials=json_decode($jsonCredentials,true);
RequestinganaccesstokenInarealworldapplication,youwouldprobablywanttoseparatethecoderelatedtoauthenticationfromtheonethatdealswithoperationslikefetchingorpostingdata.Tokeepthingssimplehere,wewilllettheTwitterclassknowhowtoauthenticatebyitself.
Let’sstartbyaddinga$clientpropertytotheclasswhichwillcontainaninstanceofGuzzle’sClientclass.ThisinstancewillcontainthebaseURIoftheTwitterAPI,whichwecanhaveastheconstantTWITTER_API_BASE_URI.Instantiatethispropertyintheconstructorsothattherestofthemethodscanmakeuseofit.Youcanalsoaddan$accessTokenpropertywhichwillcontaintheaccesstokenreturnedbytheTwitterAPIwhenauthenticating.Allthesechangesarehighlightedhere:
<?php
namespaceTwitterApp;
useException;
useGuzzleHttp\Client;
classTwitter{
constTWITTER_API_BASE_URI='https://api.twitter.com';
private$key;
private$secret;
private$accessToken;
private$client;
publicfunction__construct(String$key,String$secret){
$this->key=$key;
$this->secret=$secret;
$this->client=newClient(
['base_uri'=>self::TWITTER_API_BASE_URI]
);
}
//...
}
Thenextstepwouldbetowriteamethodthat,giventhekeyandsecretareprovided,requestsanaccesstokentotheprovider.Morespecifically:
Concatenatethekeyandthesecretwitha:.EncodetheresultusingBase64.SendaPOSTrequestto/oauth2/tokenwiththeencodedcredentialsastheAuthorizationheader.AlsoincludeaContent-Typeheaderandabody(checkthecodeformoreinformation).
WenowinvokethepostmethodofGuzzle’sclientinstancesendingtwoarguments:theendpointstring(/oauth2/token)andanarraywithoptions.Theseoptionsincludetheheadersandthebodyoftherequest,asyouwillseeshortly.Theresponseofthis
invocationisanobjectthatidentifiestheHTTPresponse.Youcanextractthecontent(body)oftheresponsewithgetBody.Twitter’sAPIresponseisaJSONwithsomearguments.Theonethatyoucareaboutthemostistheaccess_token,thetokenthatyouwillneedtoincludeineachsubsequentrequesttotheAPI.Extractitandsaveit.Thefullmethodlooksasfollows:
privatefunctionrequestAccessToken(){
$encodedString=base64_encode(
$this->key.':'.$this->secret
);
$headers=[
'Authorization'=>'Basic'.$encodedString,
'Content-Type'=>'application/x-www-form-urlencoded;charset=UTF-8'
];
$options=[
'headers'=>$headers,
'body'=>'grant_type=client_credentials'
];
$response=$this->client->post(self::OAUTH_ENDPOINT,$options);
$body=json_decode($response->getBody(),true);
$this->accessToken=$body['access_token'];
}
Youcanalreadytrythiscodebyaddingthesetwolinesattheendoftheconstructor:
$this->requestAccessToken();
var_dump($this->accessToken);
Runtheapplicationinordertoseetheaccesstokengivenbytheproviderusingthefollowingcommand.Remembertoremovetheprecedingtwolinesinordertoproceedwiththesection.
$phpapp.php
Keepinmindthat,eventhoughhavingakeyandsecretandgettinganaccesstokenisthesameacrossallOAuthauthentications,thespecificwayofencoding,theendpointused,andtheresponsereceivedfromtheproviderareexclusivefromTwitter’sAPI.Itcouldbethatseveralothersareexactlythesame,butalwayscheckthedocumentationforeachone.
FetchingtweetsWefinallyarrivetothesectionwhereweactuallymakeuseoftheAPI.WewillimplementthefetchTwitsmethodinordertogetalistofthelastNnumberoftweetsforagivenuser.Inordertoperformrequests,weneedtoaddtheAuthorizationheadertoeachone,thistimewiththeaccesstoken.Sincewewanttomakethisclassasreusableaspossible,let’sextractthistoaprivatemethod:
privatefunctiongetAccessTokenHeaders():array{
if(empty($this->accessToken)){
$this->requestAccessToken();
}
return['Authorization'=>'Bearer'.$this->accessToken];
}
Asyoucansee,theprecedingmethodalsoallowsustofetchtheaccesstokenfromtheprovider.Thisisuseful,sinceifwemakemorethanonerequest,wewilljustrequesttheaccesstokenonce,andwehaveoneuniqueplacetodoso.Addnowthefollowingmethodimplementation:
constGET_TWITS='/1.1/statuses/user_timeline.json';
//...
publicfunctionfetchTwits(string$name,int$count):array{
$options=[
'headers'=>$this->getAccessTokenHeaders(),
'query'=>[
'count'=>$count,
'screen_name'=>$name
]
];
$response=$this->client->get(self::GET_TWITS,$options);
$responseTwits=json_decode($response->getBody(),true);
$twits=[];
foreach($responseTwitsas$twit){
$twits[]=[
'created_at'=>$twit['created_at'],
'text'=>$twit['text'],
'user'=>$twit['user']['name']
];
}
return$twits;
}
Thefirstpartoftheprecedingmethodbuildstheoptionsarraywiththeaccesstokenheadersandthequerystringarguments—inthiscase,withthenumberoftweetstoretrieveandtheuser.WeperformtheGETrequestanddecodetheJSONresponseintoanarray.Thisarraycontainsalotofinformationthatwemightnotneed,soweiterateitinordertoextractthosefieldsthatwereallywant—inthisexample,thedate,thetext,andtheuser.
Inordertotesttheapplication,justinvokethefetchTwitsmethodattheendoftheapp.phpfile,specifyingtheTwitterIDofoneofthepeopleyouarefollowing,oryourself.
$twits=$twitter->fetchTwits('neiltyson',10);
var_dump($twits);
Youshouldgetaresponsesimilartoours,showninthefollowingscreenshot:
Onethingtokeepinmindisthataccesstokensexpireaftersometime,returninganHTTPresponsewitha4xxstatuscode(usually,401unauthorized).Guzzlethrowsanexceptionwhenthestatuscodeiseither4xxor5xx,soitiseasymanagethesescenarios.YoucouldaddthiscodewhenperformingtheGETrequest:
try{
$response=$this->client->get(self::GET_TWITS,$options);
}catch(ClientException$e){
if($e->getCode()==401){
$this->requestAccessToken();
$response=$this->client->get(self::GET_TWITS,$options);
}else{
throw$e;
}
}
ThetoolkitoftheRESTAPIdeveloperWhileyouaredevelopingyourownRESTAPI,orwritinganintegrationforathird-partyone,youmightwanttotestitbeforeyoustartwritingyourcode.Thereareahandfuloftoolsthatwillhelpyouwiththistask,whetheryouwanttouseyourbrowser,oryouareafanofthecommandline.
TestingAPIswithbrowsersThereareactuallyseveraladd-onsthatallowyoutoperformHTTPrequestsfrombrowsers,dependingonwhichoneyouuse.SomefamousnamesareAdvancedRestClientforChromeandRESTClientforFirefox.Attheendoftheday,allthoseclientsallowyoutoperformthesameHTTPrequests,whereyoucanspecifytheURL,themethod,theheaders,thebody,andsoon.Theseclientswillalsoshowyouallthedetailsyoucanimaginefromtheresponse,includingthestatuscode,thetimespent,andthebody.ThefollowingscreenshotdisplaysanexampleofarequestusingChrome’sAdvancedRestClient:
IfyouwanttotestGETrequestswithyourownAPI,andallthatyouneedistheURL,thatis,youdonotneedtosendanyheaders,youcanjustuseyourbrowserasifyouweretryingtoaccessanyotherwebsite.Ifyoudoso,andifyouareworkingwithJSONresponses,youcaninstallanotheradd-ontoyourbrowserthatwillhelpyouinviewing
TestingAPIsusingthecommandlineSomepeoplefeelmorecomfortableusingthecommandline;soluckily,forthemtherearetoolsthatallowthemtoperformanyHTTPrequestfromtheirconsoles.Wewillgiveabriefintroductiontooneofthemostfamousones:cURL.Thistoolhasquitealotoffeatures,butwewillfocusonlyontheonesthatyouwillbeusingmoreoften:theHTTPmethod,postparameters,andheaders:
-X<method>:ThisspecifiestheHTTPmethodtouse--data:Thisaddstheparametersspecified,whichcanbeaddedaskey-valuepairs,JSON,plaintext,andsoon--header:Thisaddsaheadertotherequest
ThefollowingisanexampleofthewaytosendaPOSTrequestwithcURL:
curl-XPOST--data"text=Thisissparta!"\
>--header"Authorization:Bearer8s8d7bf8asdbf8sbdf8bsa"\
>https://api.twitter.com/1.1/statuses/update.json
{"errors":[{"code":89,"message":"Invalidorexpiredtoken."}]}
IfyouareusingaUnixsystem,youwillprobablybeabletoformattheresultingJSONbyappending|python-mjson.toolsothatitgetseasiertoread:
$curl-XPOST--data"text=Thisissparta!"\
>--header"Authorization:Bearer8s8d7bf8asdbf8sbdf8bsa"\
>https://api.twitter.com/1.1/statuses/update.json\
>|python-mjson.tool
{
"errors":[
{
"code":89,
"message":"Invalidorexpiredtoken."
}
]
}
cURLisquiteapowerfultoolthatletsyoudoquiteafewtricks.Ifyouareinterested,goaheadandcheckthedocumentationorsometutorialonhowtouseallitsfeatures.
BestpracticeswithRESTAPIsWe’vealreadygonethroughsomeofthebestpracticeswhenwritingRESTAPIs,likeusingHTTPmethodsproperly,orchoosingthecorrectstatuscodeforyourresponses.Wealsodescribedtwoofthemostusedauthenticationsystems.ButthereisstillalottolearnaboutcreatingproperRESTAPIs.Rememberthattheyaremeanttobeusedbydeveloperslikeyourself,sotheywillalwaysbegratefulifyoudothingsproperly,andmaketheirliveseasier.Ready?
ConsistencyinyourendpointsWhendecidinghowtonameyourendpoints,trykeepingthemconsistent.Eventhoughyouarefreetochoose,thereisasetofspokenrulesthatwillmakeyourendpointsmoreintuitiveandeasytounderstand.Let’slistsomeofthem:
Forstarters,anendpointshouldpointtoaspecificresource(forexample,booksortweets),andyoushouldmakethatclearinyourendpoint.Ifyouhaveanendpointthatreturnsthelistofallbooks,donotnameit/library,asitisnotobviouswhatitwillbereturning.Instead,nameit/booksor/books/all.Thenameoftheresourcecanbeeitherpluralorsingular,butmakeitconsistent.Ifsometimesyouuse/booksandsometimes/user,itmightbeconfusing,andpeoplewillprobablymakemistakes.Wepersonallyprefertousethepluralform,butthatistotallyuptoyou.Whenyouwanttoretrieveaspecificresource,doitbyspecifyingtheIDwheneverpossible.IDsmustbeuniqueinyoursystem,andanyotherparametermightpointtotwodifferententities.SpecifytheIDnexttothenameoftheresource,suchas/books/249234-234-23-42.IfyoucanunderstandwhatanendpointdoesbyjusttheHTTPmethod,thereisnoneedtoaddthisinformationaspartoftheendpoint.Forexample,ifyouwanttogetabook,oryouwanttodeleteit,/books/249234-234-23-42alongwiththeHTTPmethodsGETandDELETEaremorethanenough.Ifitisnotobvious,stateitasaverbattheendoftheendpoint,like/employee/9218379182/promote.
DocumentasmuchasyoucanThetitlesayseverything.YouareprobablynotgoingtobetheoneusingtheRESTAPI,otherswill.Obviously,evenifyoudesignaveryintuitivesetofendpoints,developerswillstillneedtoknowthewholesetofavailableendpoints,whateachofthemdoes,whatoptionalparametersareavailable,andsoon.
Writeasmuchdocumentationaspossible,andkeepituptodate.TakealookatotherdocumentedAPIstogatherideasonhowtodisplaytheinformation.Thereareplentyoftemplatesandtoolsthatwillhelpyoudeliverawell-presenteddocumentation,butyouaretheonethathastobeconsistentandmethodical.Developershaveaspecialhatetowardsdocumentinganything,butwealsoliketofindclearandbeautifullypresenteddocumentationwhenweneedtousesomeoneelse’sAPIs.
FiltersandpaginationOneofthecommonusagesofanAPIistolistresourcesandfilterthembysomecriteria.Wealreadysawanexamplewhenwewerebuildingourownbookstore;wewantedtogetthelistofbooksthatcontainedacertainstringintheirtitlesorauthors.
Somedeveloperstrytohavebeautifulendpoints,whichaprioriisagoodthingtodo.Imaginethatyouwanttofilterjustbytitle,youmightenduphavinganendpointlike/books/title/<string>.Weaddalsotheabilitytofilterbyauthor,andwenowgettwomoreendpoints:/books/title/<string>/author/<string>and/books/author/<string>.Nowlet’saddthedescriptiontoo—doyouseewherewearegoing?
Eventhoughsomedevelopersdonotliketousequerystringsasarguments,thereisnothingwrongwithit.Infact,ifyouusethemproperly,youwillendupwithcleanerendpoints.Youwanttogetbooks?Fine,justuse/books,andaddwhicheverfilteryouneedusingthequerystring.
Paginationoccurswhenyouhavewaytoomanyresourcesofthesametypetoretrieveallatonce.YoushouldthinkofpaginationasanotheroptionalfiltertobespecifiedasaGETparameter.Youshouldhavepageswithadefaultsize,let’ssay10books,butitisagoodideatogivethedeveloperstheabilitytodefinetheirownsize.Inthiscase,developerscanspecifythelengthandthenumberofpagestoretrieve.
APIversioningYourAPIisareflectionofwhatyourapplicationcando.Chancesarethatyourcodewillevolve,improvingthealreadyexistingfeaturesoraddingnewones.YourAPIshouldbeupdatedtoo,exposingthosenewfeatures,updatingexistingendpoints,orevenremovingsomeofthem.
ImaginenowthatsomeoneelseisusingyourRESTAPI,andtheirwholewebsitereliesonit.Ifyouchangeyourexistingendpoints,theirwebsitewillstopworking!Theywillnotbehappyatall,andwilltrytofindsomeoneelsethatcandowhatyouweredoing.Notagoodscenario,butthen,howdoyouimproveyourAPI?
Thesolutionistouseversioning.WhenyoureleaseanewversionoftheAPI,donotnukedowntheexistingone;youshouldgivesometimetotheuserstoupgradetheirintegrations.AndhowcantwodifferentversionsoftheAPIcoexist?Youalreadysawoneoftheoptions—theonethatwerecommendyou:byspecifyingtheversionoftheAPItouseaspartoftheendpoint.DoyouremembertheendpointoftheTwitterAPI/1.1/statuses/user_timeline.json?The1.1referstotheversionthatwewanttouse.
UsingHTTPcacheIfthemainfeatureofRESTAPIsisthattheymakeheavyuseofHTTP,whynottakeadvantageofHTTPcache?Well,thereareactualreasonsfornotusingit,butmostofthemareduetoalackofknowledgeaboutusingitproperly.Itisoutofthescopeofthisbooktoexplaineverysingledetailofitsimplementation,butlet’strytogiveashortintroductiontothetopic.PlentyofresourcesontheInternetcanhelpyoutounderstandthepartsthatyouaremoreinterestedin.
HTTPresponsescanbedividedaspublicandprivate.PublicresponsesaresharedbetweenallusersoftheAPI,whereastheprivateonesaremeanttobeuniqueforeachuser.YoucanspecifywhichtypeofresponseisyoursusingtheCache-Controlheader,allowingtheresponsetobecachedifthemethodoftherequestwasaGET.Thisheadercanalsoexposetheexpirationofthecache,thatis,youcanspecifythedurationforwhichyourresponsewillremainthesame,andthus,canbecached.
Othersystemsrelyongeneratingahashoftherepresentationofaresource,andadditastheETag(Entitytag)headerinordertoknowiftheresourcehaschangedornot.Inasimilarway,youcansettheLast-Modifiedheadertolettheclientknowwhenwasthelasttimethatthegivenresourcechanged.Theideabehindthosesystemsistoidentifywhentheclientalreadycontainsvaliddata.Ifso,theproviderdoesnotprocesstherequest,butreturnsanemptyresponsewiththestatuscode304(notmodified)instead.Whentheclientgetsthatresponse,itusesitscachedcontent.
CreatingaRESTAPIwithLaravelInthissection,wewillbuildaRESTAPIwithLaravelfromscratch.ThisRESTAPIwillallowyoutomanagedifferentclientsatyourbookstore,notonlyviathebrowser,butviatheUIaswell.Youwillbeabletoperformprettymuchthesameactionsasbefore,thatis,listingbooks,buyingthem,borrowingforfree,andsoon.
OncetheRESTAPIisdone,youshouldremoveallthebusinesslogicfromthebookstorethatyoubuiltduringthepreviouschapters.ThereasonisthatyoushouldhaveoneuniqueplacewhereyoucanactuallymanipulateyourdatabasesandtheRESTAPI,andtherestoftheapplications,likethewebone,shouldabletocommunicatewiththeRESTAPIformanagingdata.Indoingso,youwillbeabletocreateotherapplicationsfordifferentplatforms,likemobileapps,thatwillusetheRESTAPItoo,andboththewebsiteandthemobileappwillalwaysbesynchronized,sincetheywillbeusingthesamesources.
AswithourpreviousLaravelexample,inordertocreateanewproject,youjustneedtorunthefollowingcommand:
$laravelnewbookstore_api
SettingOAuth2authenticationThefirstthingthatwearegoingtoimplementistheauthenticationlayer.WewilluseOAuth2inordertomakeourapplicationmoresecurethanbasicauthentication.LaraveldoesnotprovidesupportforOAuth2outofthebox,butthereisaserviceproviderwhichdoesthatforus.
InstallingOAuth2ServerToinstallOAuth2,additasadependencytoyourprojectusingComposer:
$composerrequire"lucadegasperi/oauth2-server-laravel:5.1.*"
Thisserviceproviderneedsquiteafewchanges.Wewillgothroughthemwithoutgoingintotoomuchdetailonhowthingsworkexactly.Ifyouaremoreinterestedinthetopic,orifyouwanttocreateyourownserviceprovidersforLaravel,werecommendyoutogothoughtheextensiveofficialdocumentation.
Tostartwith,weneedtoaddthenewOAuth2Serverserviceprovidertothearrayofprovidersintheconfig/app.phpfile.Addthefollowinglinesattheendoftheprovidersarray:
/*
*OAuth2ServerServiceProviders…
*/
LucaDegasperi\OAuth2Server\Storage\FluentStorageServiceProvider::class,
LucaDegasperi\OAuth2Server\OAuth2ServerServiceProvider::class,
Inthesameway,youneedtoaddanewaliastothealiasesarrayinthesamefile:
'Authorizer'=>LucaDegasperi\OAuth2Server\Facades\Authorizer::class,
Let’smovetotheapp/Http/Kernel.phpfile,whereweneedtomakesomechangestoo.Addthefollowingentrytothe$middlewarearraypropertyoftheKernelclass:
\LucaDegasperi\OAuth2Server\Middleware\OAuthExceptionHandlerMiddleware::cla
ss,
Addthefollowingkey-valuepairstothe$routeMiddlewarearraypropertyofthesameclass:
'oauth'=>\LucaDegasperi\OAuth2Server\Middleware\OAuthMiddleware::class,
'oauth-user'=>
\LucaDegasperi\OAuth2Server\Middleware\OAuthUserOwnerMiddleware::class,
'oauth-client'=>
\LucaDegasperi\OAuth2Server\Middleware\OAuthClientOwnerMiddleware::class,
'check-authorization-params'=>
\LucaDegasperi\OAuth2Server\Middleware\CheckAuthCodeRequestMiddleware::clas
s,
'csrf'=>\App\Http\Middleware\VerifyCsrfToken::class,
WeaddedaCSRFtokenverifiertothe$routeMiddleware,soweneedtoremovetheonealreadydefinedin$middlewareGroups,sincetheyareincompatible.Usethefollowing
linetodoso:
\App\Http\Middleware\VerifyCsrfToken::class,
SettingupthedatabaseLet’ssetupthedatabasenow.Inthissection,wewillassumethatyoualreadyhavethebookstoredatabaseinyourenvironment.Ifyoudonothaveit,gobacktoChapter5,UsingDatabases,tocreateitinordertoproceedwiththissetup.
Thefirstthingtodoistoupdatethedatabasecredentialsinthe.envfile.Theyshouldlooksomethingsimilartothefollowinglines,butwithyourusernameandpassword:
DB_HOST=localhost
DB_DATABASE=bookstore
DB_USERNAME=root
DB_PASSWORD=
InordertopreparetheconfigurationanddatabasemigrationfilesfromtheOAuth2Serverserviceprovider,weneedtopublishit.InLaravel,youdoitbyexecutingthefollowingcommand:
$phpartisanvendor:publish
Nowthedatabase/migrationsdirectorycontainsallthenecessarymigrationfilesthatwillcreatethenecessarytablesrelatedtoOAuth2inourdatabase.Toexecutethem,werunthefollowingcommand:
$phpartisanmigrate
Weneedtoaddatleastoneclienttotheoauth_clientstable,whichisthetablethatstoresthekeyandsecretsforallclientsthatwanttoconnecttoourRESTAPI.Thisnewclientwillbetheonethatyouwilluseduringthedevelopmentprocessinordertotestwhatyouhavedone.WecansetarandomID—thekey—andthesecretasfollows:
mysql>INSERTINTOoauth_clients(id,secret,name)
->VALUES('iTh4Mzl0EAPn90sK4EhAmVEXS',
->'PfoWM9yq4Bh6rGbzzJhr8oDDsNZwGlsMIAeVRaPM',
->'Toni');
QueryOK,1rowaffected,1warning(0.00sec)
Enablingclient-credentialsauthenticationSincewepublishedthepluginsinvendorinthepreviousstep,nowwehavetheconfigurationfilesfortheOAuth2Server.Thispluginallowsusdifferentauthenticationsystems(allofthemwithOAuth2),dependingonournecessities.Theonethatweareinterestedinforourprojectistheclient_credentialstype.ToletLaravelknow,addthefollowinglinesattheendofthearrayintheconfig/oauth2.phpfile:
'grant_types'=>[
'client_credentials'=>[
'class'=>
'\League\OAuth2\Server\Grant\ClientCredentialsGrant',
'access_token_ttl'=>3600
]
]
Theseprecedinglinesgrantaccesstotheclient_credentialstype,whicharemanagedbytheClientCredentialsGrantclass.Theaccess_token_ttlvaluereferstothetimeperiodoftheaccesstoken,thatis,forhowlongsomeonecanuseit.Inthiscase,itissetto1hour,thatis,3,600seconds.
Finally,weneedtoenablearoutesowecanpostourcredentialsinexchangeforanaccesstoken.Addthefollowingroutetotheroutesfileinapp/Http/routes.php:
Route::post('oauth/access_token',function(){
returnResponse::json(Authorizer::issueAccessToken());
});
RequestinganaccesstokenItistimetotestwhatwehavedonesofar.Todoso,weneedtosendaPOSTrequesttothe/oauth/access_tokenendpointthatweenabledjustnow.ThisrequestneedsthefollowingPOSTparameters:
client_idwiththekeyfromthedatabaseclient_secretwiththesecretfromthedatabasegrant_typetospecifythetypeofauthenticationthatwearetryingtoperform,inthiscaseclient_credentials
TherequestissuedusingtheAdvancedRESTClientadd-onfromChromelooksasfollows:
Theresponsethatyoushouldgetshouldhavethesameformatasthisone:
{
"access_token":"MPCovQda354d10zzUXpZVOFzqe491E7ZHQAhSAax"
"token_type":"Bearer"
"expires_in":3600
}
NotethatthisisadifferentwayofrequestingforanaccesstokenthanwhattheTwitterAPIdoes,buttheideaisstillthesame:givenakeyandasecret,theprovidergivesusanaccesstokenthatwillallowustousetheAPIforsometime.
PreparingthedatabaseEventhoughwe’vealreadydonethesameinthepreviouschapter,youmightthink:“Whydowestartbypreparingthedatabase?”.WecouldarguethatyoufirstneedtoknowthekindofendpointsyouwanttoexposeinyourRESTAPI,andonlythenyoucanstartthinkingaboutwhatyourdatabaseshouldlooklike.Butyoucouldalsothinkthat,sinceweareworkingwithanAPI,eachendpointshouldmanageoneresource,sofirstyouneedtodefinetheresourcesyouaredealingwith.Thiscodefirstversusdatabase/modelfirstisanongoingwarontheInternet.Butwhicheverwayyouthinkisbetter,thefactisthatwealreadyknowwhattheuserswillneedtodowithourRESTAPI,sincewealreadybuilttheUIpreviously;soitdoesnotreallymatter.
Weneedtocreatefourtables:books,sales,sales_books,andborrowed_books.RememberthatLaravelalreadyprovidesauserstable,whichwecanuseasourcustomers.Runthefollowingfourcommandstocreatethemigrationsfiles:
$phpartisanmake:migrationcreate_books_table--create=books
$phpartisanmake:migrationcreate_sales_table--create=sales
$phpartisanmake:migrationcreate_borrowed_books_table\
--create=borrowed_books
$phpartisanmake:migrationcreate_sales_books_table\
--create=sales_books
Nowwehavetogofilebyfiletodefinewhateachtableshouldlooklike.WewilltrytoreplicatethedatastructurefromChapter5,UsingDatabases,asmuchaspossible.Rememberthatthemigrationfilescanbefoundinsidethedatabase/migrationsdirectory.Thefirstfilethatwecaneditisthecreate_books_table.php.Replacetheexistingemptyupmethodbythefollowingone:
publicfunctionup()
{
Schema::create('books',function(Blueprint$table){
$table->increments('id');
$table->string('isbn')->unique();
$table->string('title');
$table->string('author');
$table->smallInteger('stock')->unsigned();
$table->float('price')->unsigned();
});
}
Thenextoneinthelistiscreate_sales_table.php.Rememberthatthisonehasaforeignkeypointingtotheuserstable.Youcanusereferences(field)->on(tablename)todefinethisconstraint.
publicfunctionup()
{
Schema::create('sales',function(Blueprint$table){
$table->increments('id');
$table->string('user_id')->references('id')->on('users');
$table->timestamps();
});
}
Thecreate_sales_books_table.phpfilecontainstwoforeignkeys:onepointingtotheIDofthesale,andonetotheIDofthebook.Replacetheexistingupmethodbythefollowingone:
publicfunctionup()
{
Schema::create('sales_books',function(Blueprint$table){
$table->increments('id');
$table->integer('sale_id')->references('id')->on('sales');
$table->integer('book_id')->references('id')->on('books');
$table->smallInteger('amount')->unsigned();
});
}
Finally,editthecreate_borrowed_books_table.phpfile,whichhasthebook_idforeignkeyandthestartandendtimestamps:
publicfunctionup()
{
Schema::create('borrowed_books',function(Blueprint$table){
$table->increments('id');
$table->integer('book_id')->references('id')->on('books');
$table->string('user_id')->references('id')->on('users');
$table->timestamp('start');
$table->timestamp('end');
});
}
Themigrationfilesarereadysowejustneedtomigratetheminordertocreatethedatabasetables.Runthefollowingcommand:
$phpartisanmigrate
Also,addsomebookstothedatabasemanuallysothatyoucantestlater.Forexample:
mysql>INSERTINTObooks(isbn,title,author,stock,price)VALUES
->("9780882339726","1984","GeorgeOrwell",12,7.50),
->("9789724621081","1Q84","HarukiMurakami",9,9.75),
->("9780736692427","AnimalFarm","GeorgeOrwell",8,3.50),
->("9780307350169","Dracula","BramStoker",30,10.15),
->("9780753179246","19minutes","JodiPicoult",0,10);
QueryOK,5rowsaffected(0.01sec)
Records:5Duplicates:0Warnings:0
SettingupthemodelsThenextthingtodoonthelististoaddtherelationshipsthatourdatahas,thatis,totranslatetheforeignkeysfromthedatabasetothemodels.Firstofall,weneedtocreatethosemodels,andforthatwejustrunthefollowingcommands:
$phpartisanmake:modelBook
$phpartisanmake:modelSale
$phpartisanmake:modelBorrowedBook
$phpartisanmake:modelSalesBook
Nowwehavetogomodelbymodel,andaddtheonetooneandonetomanyrelationshipsaswedidinthepreviouschapter.ForBookModel,wewillonlyspecifythatthemodeldoesnothavetimestamps,sincetheycomebydefault.Todoso,addthefollowinghighlightedlinetoyourapp/Book.phpfile:
<?php
namespaceApp;
useIlluminate\Database\Eloquent\Model;
classBookextendsModel
{
public$timestamps=false;
}
FortheBorrowedBookmodel,weneedtospecifythatithasonebook,anditbelongstoauser.Wealsoneedtospecifythefieldswewillfillonceweneedtocreatetheobject—inthiscase,book_idandstart.Addthefollowingtwomethodsinapp/BorrowedBook.php:
<?php
namespaceApp;
useIlluminate\Database\Eloquent\Model;
classBorrowedBookextendsModel
{
protected$fillable=['user_id','book_id','start'];
public$timestamps=false;
publicfunctionuser(){
return$this->belongsTo('App\User');
}
publicfunctionbook(){
return$this->hasOne('App\Book');
}
}
Salescanhavemany“salebooks”(weknowitmightsoundalittleawkward),andtheyalsobelongtojustoneuser.Addthefollowingtoyourapp/Sale.php:
<?php
namespaceApp;
useIlluminate\Database\Eloquent\Model;
classSaleextendsModel
{
protected$fillable=['user_id'];
publicfunctionbooks(){
return$this->hasMany('App\SalesBook');
}
publicfunctionuser(){
return$this->belongsTo('App\User');
}
}
Likeborrowedbooks,salebookscanhaveonebookandbelongtoonesaleinsteadoftooneuser.Thefollowinglinesshouldbeaddedtoapp/SalesBook.php:
<?php
namespaceApp;
useIlluminate\Database\Eloquent\Model;
classSaleBookextendsModel
{
public$timestamps=false;
protected$fillable=['book_id','sale_id','amount'];
publicfunctionsale(){
return$this->belongsTo('App\Sale');
}
publicfunctionbooks(){
return$this->hasOne('App\Book');
}
}
Finally,thelastmodelthatweneedtoupdateistheUsermodel.WeneedtoaddtheoppositerelationshiptothebelongsweusedearlierinSaleandBorrowedBook.Addthesetwofunctions,andleavetherestoftheclassintact:
<?php
namespaceApp;
useIlluminate\Foundation\Auth\UserasAuthenticatable;
classUserextendsAuthenticatable
{
//...
publicfunctionsales(){
return$this->hasMany('App\Sale');
}
publicfunctionborrowedBooks(){
return$this->hasMany('App\BorrowedBook');
}
}
DesigningendpointsInthissection,weneedtocomeupwiththelistofendpointsthatwewanttoexposetotheRESTAPIclients.Keepinmindthe“rules”explainedintheBestpracticeswithRESTAPIssection.Inshort,keepthefollowingrulesinmind:
OneendpointinteractswithoneresourceApossibleschemacouldbe<APIversion>/<resourcename>/<optionalid>/<optionalaction>
UseGETparametersforfilteringandpagination
Sowhatwilltheuserneedtodo?Wealreadyhaveagoodideaaboutthat,sincewecreatedtheUI.Abriefsummarywouldbeasfollows:
Listalltheavailablebookswithsomefiltering(bytitleandauthor),andpaginatedwhennecessary.Alsoretrievetheinformationonaspecificbook,giventheID.Allowtheusertoborrowaspecificbookifavailable.Inthesameway,theusershouldbeabletoreturnbooks,andlistthehistoryofborrowedbookstoo(filteredbydateandpaginated).Allowtheusertobuyalistofbooks.Thiscouldbeimproved,butfornowlet’sforcetheusertobuybookswithjustonerequest,includingthefulllistofbooksinthebody.Also,listthesalesoftheuserfollowingthesamerulesasthatwithborrowedbooks.
Wewillstartstraightawaywithourlistofendpoints,specifyingthepath,theHTTPmethod,andtheoptionalparameters.ItwillalsogiveyouanideaonhowtodocumentyourRESTAPIs.
GET/books
title:Optionalandfiltersbytitleauthor:Optionalandfiltersbyauthorpage:Optional,defaultis1,andspecifiesthepagetoreturnpage-size:Optional,defaultis50,andspecifiesthepagesizetoreturn
GET/books/<bookid>POST/borrowed-books
book-id:MandatoryandspecifiestheIDofthebooktoborrow
GET/borrowed-books
from:Optionalandreturnsborrowedbooksfromthespecifieddatepage:Optional,defaultis1,andspecifiesthepagetoreturnpage-size:Optional,defaultis50,andspecifiesthenumberofborrowedbooksperpage
PUT/borrowed-books/<borrowedbookid>/returnPOST/sales
books:MandatoryanditisanarraylistingthebookIDstobuyandtheir
amounts,thatis,{“book-id-1”:amount,“book-id-2”:amount,…}
GET/sales
from:Optionalandreturnsborrowedbooksfromthespecifieddatepage:Optional,defaultis1,andspecifiesthepagetoreturnpage-size:Optional,defaultis50,andspecifiesthenumberofsalesperpage
GET/sales/<salesid>
WeusePOSTrequestswhencreatingsalesandborrowedbooks,sincewedonotknowtheIDoftheresourcethatwewanttocreateapriori,andpostingthesamerequestwillcreatemultipleresources.Ontheotherhand,whenreturningabook,wedoknowtheIDoftheborrowedbook,andsendingthesamerequestmultipletimeswillleavethedatabaseinthesamestate.Let’stranslatetheseendpointstoroutesinapp/Http/routes.php:
/*
*Booksendpoints.
*/
Route::get('books',['middleware'=>'oauth',
'uses'=>'BookController@getAll']);
Route::get('books/{id}',['middleware'=>'oauth',
'uses'=>'BookController@get']);
/*
*Borrowedbooksendpoints.
*/
Route::post('borrowed-books',['middleware'=>'oauth',
'uses'=>'BorrowedBookController@borrow']);
Route::get('borrowed-books',['middleware'=>'oauth',
'uses'=>'BorrowedBookController@get']);
Route::put('borrowed-books/{id}/return',['middleware'=>'oauth',
'uses'=>'BorrowedBookController@returnBook']);
/*
*Salesendpoints.
*/
Route::post('sales',['middleware'=>'oauth',
'uses'=>'SalesController@buy]);
Route::get('sales',['middleware'=>'oauth',
'uses'=>'SalesController@getAll']);
Route::get('sales/{id}',['middleware'=>'oauth',
'uses'=>'SalesController@get']);
Intheprecedingcode,notehowweaddedthemiddlewareoauthtoalltheendpoints.Thiswillrequiretheusertoprovideavalidaccesstokeninordertoaccessthem.
AddingthecontrollersFromtheprevioussection,youcanimaginethatweneedtocreatethreecontrollers:BookController,BorrowedBookController,andSalesController.Let’sstartwiththeeasiestone:returningtheinformationofabookgiventheID.Createthefileapp/Http/Controllers/BookController.php,andaddthefollowingcode:
<?php
namespaceApp\Http\Controllers;
useApp\Book;
useIlluminate\Http\JsonResponse;
useIlluminate\Http\Response;
classBookControllerextendsController{
publicfunctionget(string$id):JsonResponse{
$book=Book::find($id);
if(empty($book)){
returnnewJsonResponse(
null,
JsonResponse::HTTP_NOT_FOUND
);
}
returnresponse()->json(['book'=>$book]);
}
}
Eventhoughthisprecedingexampleisquiteeasy,itcontainsmostofwhatwewillneedfortherestoftheendpoints.WetrytofetchabookgiventheIDfromtheURL,andwhennotfound,wereplywitha404(notfound)emptyresponse—theconstantResponse::HTTP_NOT_FOUNDis404.Incasewehavethebook,wereturnitasJSONwithresponse->json().Notehowweaddtheseeminglyunnecessarykeybook;itistruethatwedonotreturnanythingelseand,sinceweaskforthebook,theuserwillknowwhatwearetalkingabout,butasitdoesnotreallyhurt,itisgoodtobeasexplicitaspossible.
Let’stestit!Youalreadyknowhowtogetanaccesstoken—checktheRequestinganaccesstokensection.Sogetone,andtrytoaccessthefollowingURLs:
http://localhost/books/0?access_token=12345
http://localhost/books/1?access_token=12345
Assumingthat12345isyouraccesstoken,thatyouhaveabookinthedatabasewithID1,andyoudonothaveabookwithID0,thefirstURLshouldreturna404response,andthesecondone,aresponsesomethingsimilartothefollowing:
{
"book":{
"id":1
"isbn":"9780882339726"
"title":"1984"
"author":"GeorgeOrwell"
"stock":12
"price":7.5
}
}
Let’snowaddthemethodtogetallthebookswithfiltersandpagination.Itlooksquiteverbose,butthelogicthatweuseisquitesimple:
publicfunctiongetAll(Request$request):JsonResponse{
$title=$request->get('title','');
$author=$request->get('author','');
$page=$request->get('page',1);
$pageSize=$request->get('page-size',50);
$books=Book::where('title','like',"%$title%")
->where('author','like',"%$author%")
->take($pageSize)
->skip(($page-1)*$pageSize)
->get();
returnresponse()->json(['books'=>$books]);
}
Wegetalltheparametersthatcancomefromtherequest,andsetthedefaultvaluesofeachoneincasetheuserdoesnotincludethem(sincetheyareoptional).Then,weusetheEloquentORMtofilterbytitleandauthorusingwhere(),andlimitingtheresultswithtake()->skip().WereturntheJSONinthesamewaywedidwiththepreviousmethod.Inthisonethough,wedonotneedanyextracheck;ifthequerydoesnotreturnanybook,itisnotreallyaproblem.
YoucannowplaywithyourRESTAPI,sendingdifferentrequestswithdifferentfilters.Thefollowingaresomeexamples:
http://localhost/books?access_token=12345
http://localhost/books?access_token=12345&title=19&page-size=1
http://localhost/books?access_token=12345&page=2
ThenextcontrollerinthelistisBorrowedBookController.Weneedtoaddthreemethods:borrow,get,andreturnBook.Asyoualreadyknowhowtoworkwithrequests,responses,statuscodes,andtheEloquentORM,wewillwritetheentireclassstraightaway:
<?php
namespaceApp\Http\Controllers;
useApp\Book;
useApp\BorrowedBook;
useIlluminate\Http\JsonResponse;
useIlluminate\Http\Request;
useLucaDegasperi\OAuth2Server\Facades\Authorizer;
classBorrowedBookControllerextendsController{
publicfunctionget():JsonResponse{
$borrowedBooks=BorrowedBook::where(
'user_id','=',Authorizer::getResourceOwnerId()
)->get();
returnresponse()->json(
['borrowed-books'=>$borrowedBooks]
);
}
publicfunctionborrow(Request$request):JsonResponse{
$id=$request->get('book-id');
if(empty($id)){
returnnewJsonResponse(
['error'=>'Expectingbook-idparameter.'],
JsonResponse::HTTP_BAD_REQUEST
);
}
$book=Book::find($id);
if(empty($book)){
returnnewJsonResponse(
['error'=>'Booknotfound.'],
JsonResponse::HTTP_BAD_REQUEST
);
}elseif($book->stock<1){
returnnewJsonResponse(
['error'=>'Notenoughstock.'],
JsonResponse::HTTP_BAD_REQUEST
);
}
$book->stock--;
$book->save();
$borrowedBook=BorrowedBook::create(
[
'book_id'=>$book->id,
'start'=>date('Y-m-dH:i:s'),
'user_id'=>Authorizer::getResourceOwnerId()
]
);
returnresponse()->json(['borrowed-book'=>$borrowedBook]);
}
publicfunctionreturnBook(string$id):JsonResponse{
$borrowedBook=BorrowedBook::find($id);
if(empty($borrowedBook)){
returnnewJsonResponse(
['error'=>'Borrowedbooknotfound.'],
JsonResponse::HTTP_BAD_REQUEST
);
}
$book=Book::find($borrowedBook->book_id);
$book->stock++;
$book->save();
$borrowedBook->end=date('Y-m-dH:m:s');
$borrowedBook->save();
returnresponse()->json(['borrowed-book'=>$borrowedBook]);
}
}
Theonlythingtonoteintheprecedingcodeishowwealsoupdatethestockofthebookbyincreasingordecreasingthestock,andinvokethesavemethodtosavethechangesinthedatabase.WealsoreturntheborrowedbookobjectastheresponsewhenborrowingabooksothattheusercanknowtheborrowedbookID,anduseitwhenqueryingorreturningthebook.
Youcantesthowthissetofendpointsworkswiththefollowingusecases:
Borrowabook.Checkthatyougetavalidresponse.Getthelistofborrowedbooks.Theonethatyoujustcreatedshouldbetherewithavalidstartingdateandanemptyenddate.Gettheinformationofthebookyouborrowed.Thestockshouldbeoneless.Returnthebook.Fetchthelistofborrowedbookstochecktheenddateandthereturnedbooktocheckthestock.
Ofcourse,youcanalwaystrytotricktheAPIandaskforbookswithoutstock,non-existingborrowedbooks,andthelike.Alltheseedgecasesshouldrespondwiththecorrectstatuscodesanderrormessages.
Wefinishthissection,andtheRESTAPI,bycreatingtheSalesController.Thiscontrolleristheonethatcontainsmorelogic,sincecreatingasaleimpliesaddingentriestothesalesbookstable,priortocheckingforenoughstockforeachone.Addthefollowingcodetoapp/Html/SalesController.php:
<?php
namespaceApp\Http\Controllers;
useApp\Book;
useApp\Sale;
useApp\SalesBook;
useIlluminate\Http\JsonResponse;
useIlluminate\Http\Request;
useLucaDegasperi\OAuth2Server\Facades\Authorizer;
classSalesControllerextendsController{
publicfunctionget(string$id):JsonResponse{
$sale=Sale::find($id);
if(empty($sale)){
returnnewJsonResponse(
null,
JsonResponse::HTTP_NOT_FOUND
);
}
$sale->books=$sale->books()->getResults();
returnresponse()->json(['sale'=>$sale]);
}
publicfunctionbuy(Request$request):JsonResponse{
$books=json_decode($request->get('books'),true);
if(empty($books)||!is_array($books)){
returnnewJsonResponse(
['error'=>'Booksarrayismalformed.'],
JsonResponse::HTTP_BAD_REQUEST
);
}
$saleBooks=[];
$bookObjects=[];
foreach($booksas$bookId=>$amount){
$book=Book::find($bookId);
if(empty($book)||$book->stock<$amount){
returnnewJsonResponse(
['error'=>"Book$bookIdnotvalid."],
JsonResponse::HTTP_BAD_REQUEST
);
}
$bookObjects[]=$book;
$saleBooks[]=[
'book_id'=>$bookId,
'amount'=>$amount
];
}
$sale=Sale::create(
['user_id'=>Authorizer::getResourceOwnerId()]
);
foreach($bookObjectsas$key=>$book){
$book->stock-=$saleBooks[$key]['amount'];
$saleBooks[$key]['sale_id']=$sale->id;
SalesBook::create($saleBooks[$key]);
}
$sale->books=$sale->books()->getResults();
returnresponse()->json(['sale'=>$sale]);
}
publicfunctiongetAll(Request$request):JsonResponse{
$page=$request->get('page',1);
$pageSize=$request->get('page-size',50);
$sales=Sale::where(
'user_id','=',Authorizer::getResourceOwnerId()
)
->take($pageSize)
->skip(($page-1)*$pageSize)
->get();
foreach($salesas$sale){
$sale->books=$sale->books()->getResults();
}
returnresponse()->json(['sales'=>$sales]);
}
}
Intheprecedingcode,notehowwefirstchecktheavailabilityofallthebooksbeforecreatingthesalesentry.Thisway,wemakesurethatwedonotleaveanyunfinishedsaleinthedatabasewhenreturninganerrortotheuser.Youcouldchangethis,andusetransactionsinstead,andifabookisnotvalid,justrollbackthetransaction.
Inordertotestthis,wecanfollowsimilarstepsaswedidwithborrowedbooks.Justrememberthatthebooksparameter,whenpostingasale,isaJSONmap;forexample,{"1":2,"4":1}meansthatIamtryingtobuytwobookswithID1andonebookwithID4.
TestingyourRESTAPIsYouhavealreadybeentestingyourRESTAPIafterfinishingeachcontrollerbymakingsomerequestandexpectingaresponse.Asyoumightimagine,thiscanbehandysometimes,butitisforsurenotthewaytogo.Testingshouldbeautomatic,andshouldcoverasmuchaspossible.Wewillhavetothinkofasolutionsimilartounittesting.
InChapter10,BehavioralTesting,youwilllearnmoremethodologiesandtoolsfortestinganapplicationendtoend,andthatwillincludeRESTAPIs.However,duetothesimplicityofourRESTAPI,wecanaddsomeprettygoodtestswithwhatLaravelprovidesusaswell.Actually,theideaisverysimilartotheteststhatwewroteinChapter8,UsingExistingPHPFrameworks,wherewemadearequesttosomeendpoint,andexpectedaresponse.Theonlydifferencewillbeinthekindofassertionsthatweuse(whichcancheckifaJSONresponseisOK),andthewayweperformrequests.
Let’saddsometeststothesetofendpointsrelatedtobooks.Weneedsomebooksinthedatabaseinordertoquerythem,sowewillhavetopopulatethedatabasebeforeeachtest,thatis,usethesetUpmethod.Rememberthatinordertoleavethedatabasecleanoftestdata,weneedtousethetraitDatabaseTransactions.Addthefollowingcodetotests/BooksTest.php:
<?php
useIlluminate\Foundation\Testing\DatabaseTransactions;
useApp\Book;
classBooksTestextendsTestCase{
useDatabaseTransactions;
private$books=[];
publicfunctionsetUp(){
parent::setUp();
$this->addBooks();
}
privatefunctionaddBooks(){
$this->books[0]=Book::create(
[
'isbn'=>'293842983648273',
'title'=>'Iliad',
'author'=>'Homer',
'stock'=>12,
'price'=>7.40
]
);
$this->books[0]->save();
$this->books[0]=$this->books[0]->fresh();
$this->books[1]=Book::create(
[
'isbn'=>'9879287342342',
'title'=>'Odyssey',
'author'=>'Homer',
'stock'=>8,
'price'=>10.60
]
);
$this->books[1]->save();
$this->books[1]=$this->books[1]->fresh();
$this->books[2]=Book::create(
[
'isbn'=>'312312314235324',
'title'=>'TheIlluminati',
'author'=>'LarryBurkett',
'stock'=>22,
'price'=>5.10
]
);
$this->books[2]->save();
$this->books[2]=$this->books[2]->fresh();
}
}
Asyoucanseeintheprecedingcode,weaddthreebookstothedatabase,andtotheclassproperty$bookstoo.Wewillneedthemwhenwewanttoassertthataresponseisvalid.Alsonotetheuseofthefreshmethod;thismethodsynchronizesthemodelthatwehavewiththecontentinthedatabase.WeneedtodothisinordertogettheIDinsertedinthedatabase,sincewedonotknowitapriori.
Thereisanotherthingweneedtodobeforeweruneachtest:authenticatingourclient.WewillneedtomakeaPOSTrequesttotheaccesstokengenerationendpointsendingvalidcredentials,andstoringtheaccesstokenthatwereceivesothatitcanbeusedintheremainingrequests.Youarefreetochoosehowtoprovidethecredentials,sincetherearedifferentwaystodoit.Inourcase,wejustprovidethecredentialsofaclienttestthatweknowexistsinthedatabase,butyoumightprefertoinsertthatclientintothedatabaseeachtime.Updatethetestwiththefollowingcode:
<?php
useIlluminate\Foundation\Testing\DatabaseTransactions;
useApp\Book;
classBooksTestextendsTestCase{
useDatabaseTransactions;
private$books=[];
private$accessToken;
publicfunctionsetUp(){
parent::setUp();
$this->addBooks();
$this->authenticate();
}
//...
privatefunctionauthenticate(){
$this->post(
'oauth/access_token',
[
'client_id'=>'iTh4Mzl0EAPn90sK4EhAmVEXS',
'client_secret'=>'PfoWM9yq4Bh6rhr8oDDsNZM',
'grant_type'=>'client_credentials'
]
);
$response=json_decode(
$this->response->getContent(),true
);
$this->accessToken=$response['access_token'];
}
}
Intheprecedingcode,weusethepostmethodinordertosendaPOSTrequest.Thismethodacceptsastringwiththeendpoint,andanarraywiththeparameterstobeincluded.Aftermakingarequest,Laravelsavestheresponseobjectintothe$responseproperty.WecanJSON-decodeit,andextracttheaccesstokenthatweneed.
Itistimetoaddsometests.Let’sstartwithaneasyone:requestingabookgivenanID.TheIDisusedtomaketheGETrequestswiththeIDofthebook(donotforgettheaccesstoken),andcheckiftheresponsematchestheexpectedone.Rememberthatwehavethe$booksarrayalready,soitwillbeprettyeasytoperformthesechecks.
Wewillbeusingtwoassertions:seeJson,whichcomparesthereceivedJSONresponsewiththeonethatweprovide,andassertResponseOk,whichyoualreadyknowfromprevioustests—itjustchecksthattheresponsehasa200statuscode.Addthistesttotheclass:
publicfunctiontestGetBook(){
$expectedResponse=[
'book'=>json_decode($this->books[1],true)
];
$url='books/'.$this->books[1]->id
.'?'.$this->getCredentials();
$this->get($url)
->seeJson($expectedResponse)
->assertResponseOk();
}
privatefunctiongetCredentials():string{
return'grant_access=client_credentials&access_token='
.$this->accessToken;
}
Weusethegetmethodinsteadofpost,sincethisisaGETrequest.Alsonotethatweuse
thegetCredentialshelper,sincewewillhavetouseitineachtest.Toseeanotherexample,let’saddatestthatcheckstheresponsewhenrequestingthebooksthatcontainthegiventitle:
publicfunctiontestGetBooksByTitle(){
$expectedResponse=[
'books'=>[
json_decode($this->books[0],true),
json_decode($this->books[2],true)
]
];
$url='books/?title=Il&'.$this->getCredentials();
$this->get($url)
->seeJson($expectedResponse)
->assertResponseOk();
}
Theprecedingtestisprettymuchthesameasthepreviousone,isn’tit?Theonlychangesaretheendpointandtheexpectedresponse.Well,theremainingtestswillallfollowthesamepattern,sincesofar,wecanonlyfetchbooksandfilterthem.
Toseesomethingdifferent,let’scheckhowtotestanendpointthatcreatesresources.Therearedifferentoptions,oneofthembeingtofirstmaketherequest,andthengoingtothedatabasetocheckthattheresourcehasbeencreated.Anotheroption,theonethatweprefer,istofirstsendtherequestthatcreatestheresource,andthen,withtheinformationintheresponse,sendarequesttofetchthenewlycreatedresource.Thisispreferable,sincewearetestingonlytheRESTAPI,andwedonotneedtoknowthespecificschemathatthedatabaseisusing.Also,iftheRESTAPIchangesitsdatabase,thetestswillkeeppassing—andtheyshould—sincewetestthroughtheinterfaceonly.
Onegoodexamplecouldbeborrowingabook.ThetestshouldfirstsendaPOSTinordertoborrowthebook,specifyingthebookID,thenextracttheborrowedbookIDfromtheresponse,andfinallysendaGETrequestaskingforthatborrowedbook.Tosavetime,youcanaddthefollowingtesttothealreadyexistingtests/BooksTest.php:
publicfunctiontestBorrowBook(){
$params=['book-id'=>$this->books[1]->id];
$params=array_merge($params,$this->postCredentials());
$this->post('borrowed-books',$params)
->seeJsonContains(['book_id'=>$this->books[1]->id])
->assertResponseOk();
$response=json_decode($this->response->getContent(),true);
$url='borrowed-books'.'?'.$this->getCredentials();
$this->get($url)
->seeJsonContains(['id'=>$response['borrowed-book']['id']])
->assertResponseOk();
}
privatefunctionpostCredentials():array{
SummaryInthischapter,youlearnedtheimportanceofRESTAPIsinthewebworld.Nowyouareablenotonlytousethem,butalsowriteyourownRESTAPIs,whichhasturnedyouintoamoreresourcefuldeveloper.Youcanalsointegrateyourapplicationswiththird-partyAPIstogivemorefeaturestoyourusers,andformakingyourwebsitesmoreinterestinganduseful.
Inthenextandlastchapter,wewillendthisbookdiscoveringatypeoftestingotherthanunittesting:behavioraltesting,whichimprovesthequalityandreliabilityofyourwebapplications.
Chapter10.BehavioralTestingInChapter7,TestingWebApplications,youlearnedhowtowriteunittestsinordertotestsmallpiecesofcodeinanisolatedway.Eventhoughthisisamust,itisnotenoughalonetomakesureyourapplicationworksasitshould.Thescopeofyourtestcouldbesosmallthateventhoughthealgorithmthatyoutestmakessense,itwouldnotbewhatthebusinessaskedyoutocreate.
Acceptancetestswereborninordertoaddthislevelofsecuritytothebusinessside,complementingthealreadyexistingunittests.Inthesameway,BDDoriginatedfromTDDinordertowritecodebasedontheseacceptancetestsinanattempttoinvolvebusinessandmanagersinthedevelopmentprocess.AsPHPisoneofthefavoritelanguagesofwebdevelopers,itisjustnaturaltofindpowerfultoolstoimplementBDDinyourprojects.YouwillbepositivelysurprisedbywhatyoucandowithBehatandMink,thetwomostpopularBDDframeworksatthemoment.
Inthischapter,youwilllearnabout:
AcceptancetestsandBDDWritingfeatureswithGherkinImplementingandrunningtestswithBehatWritingtestsagainstbrowserswithMink
Behavior-drivendevelopmentWealreadyexposedinChapter7,TestingWebApplications,thedifferenttoolswecanuseinordertomakeourapplicationsbug-free,suchasautomatedtests.Wedescribedwhatunittestsareandhowtheycanhelpusachieveourgoals,butthisisfarfromenough.Inthissection,wewilldescribetheprocessofcreatingareal-worldapplication,howunittestsarenotenough,andwhatothertechniqueswecanincludeinthislifecycleinordertosucceedinourtask—inthiscase,behavioraltests.
IntroducingcontinuousintegrationThereisahugedifferencebetweendevelopingasmallwebapplicationbyyourselfandbeingpartofabigteamofdevelopers,managers,marketingpeople,andsoon,thatworksaroundthesamebigwebapplication.Workingonanapplicationusedbythousandsormillionsofusershasaclearrisk:ifyoumessitup,therewillbeahugenumberofunhappyaffectedusers,whichmaytranslateintosalesgoingdown,partnershipsterminated,andsoon.
Fromthisscenario,youcanimaginethatpeoplewouldbescaredwhentheyhavetochangeanythinginproduction.Beforedoingso,theywillmakesurethateverythingworksperfectlyfine.Forthisreason,thereisalwaysaheavyprocessaroundallthechangesaffectingawebapplicationinproduction,includingloadsoftestsofallkinds.
Somethinkthatbyreducingthenumberoftimestheydeploytoproduction,theycanreducetheriskoffailure,whichendsupwiththemhavingreleaseseveryseveralmonthswithanuncountablenumberofchanges.
Now,imaginereleasingtheresultoftwoorthreemonthsofcodechangesatonceandsomethingmysteriouslyfailsinproduction:doyouknowwheretoevenstartlookingforthecauseoftheproblem?Whatifyourteamisgoodenoughtomakeperfectreleases,buttheendresultisnotwhatthemarketneeds?Youmightendupwastingmonthsofwork!
Eventhoughtherearedifferentapproachesandnotallcompaniesusethem,let’strytodescribeoneofthemostfamousonesfromthelastfewyears:continuousintegration(CI).Theideaistointegratesmallpiecesofworkoftenratherthanbigoneseveryonceinawhile.Ofcourse,releasingisstillaconstraintinyoursystem,whichmeansthatittakesalotoftimeandresources.CItriestoautomatizethisprocessasmuchaspossible,reducingtheamountoftimeandresourcesthatyouneedtoinvest.Therearehugebenefitswiththisapproach,whichareasfollows:
Releasesdonottakeforevertobedone,andthereisn’tanentireteamfocusedonreleasingasthisisdoneautomatically.Youcanreleasechangesonebyoneastheycome.Ifsomethingfails,youknowexactlywhatthechangewasandwheretostartlookingfortheerror.Youcanevenrevertthechangeseasilyifyouneedto.Asyoureleasesooften,youcangetquickfeedbackfromeveryone.Youwillbeabletochangeyourplansintimeifyouneedtoinsteadofwaitingformonthstogetanyfeedbackandwastingalltheeffortyouputonthisrelease.
Theideaseemsperfect,buthowdoweimplementit?First,let’sfocusonthemanualpartoftheprocess:developingthefeaturesusingaversioncontrolsystem(VCS).Thefollowingdiagramshowsaverycommonapproach:
Aswealreadymentioned,aVCSallowsdeveloperstoworkonthesamecodebase,trackingallthechangesthateveryonemakesandhelpingontheresolutionofconflicts.AVCSusuallyallowsyoutohavedifferentbranches;thatis,youcandivergefromthemainlineofdevelopmentandcontinuetodoworkwithoutmessingwithit.Thepreviousgraphshowsyouhowtousebranchestowritenewfeaturesandcanbeexplainedasfollows:
A:AteamneedstostartworkingonfeatureA.Theycreateanewbranchfromthemaster,inwhichtheywilladdallthechangesforthisfeature.B:Adifferentteamalsoneedstostartworkingonafeature.Theycreateanewbranchfrommaster,sameasbefore.Atthispoint,theyarenotawareofwhatthefirstteamisdoingastheydoitontheirownbranch.C:Thesecondteamfinishestheirjob.Nooneelsechangedmaster,sotheycanmergetheirchangesstraightaway.Atthispoint,theCIprocesswillstartthereleaseprocess.D:Thefirstteamfinishesthefeature.Inordertomergeittomaster,theyneedtofirstrebasetheirbranchwiththenewchangesofmasterandsolveanyconflictsthatmighttakeplace.Theolderthebranchisthemorechancesofgettingconflictsyouwillhave,soyoucanimaginethatsmallerandfasterfeaturesarepreferred.
Now,let’stakealookathowtheautomatedsideoftheprocesslooks.Thefollowinggraphshowsyouallthestepsfromthemergingintomastertoproductiondeployment:
Untilyoumergeyourcodeintomaster,youareinthedevelopmentenvironment.TheCItoolwilllistentoallthechangesonthemasterbranchofyourproject,andforeachofthem,itwilltriggerajob.Thisjobwilltakecareofbuildingtheprojectifnecessaryandthenrunallthetests.Ifthereisanyerrorortestfailure,itwillleteveryonenow,andtheteamthattriggeredthisjobshouldtakecareoffixingit.Themasterbranchisconsideredunstableatthispoint.
Ifalltestspass,theCItoolwilldeployyourcodeintostaging.Stagingisanenvironmentthatemulatesproductionasmuchaspossible;thatis,ithasthesameserverconfiguration,databasestructure,andsoon.Oncetheapplicationishere,youcanrunalltheteststhatyouneeduntilyouareconfidenttocontinuethedeploymenttoproduction.Asyoumakesmallchanges,youdonotneedtomanuallytestabsolutelyeverything.Instead,youcantestyourchangesandthemainusecasesofyourapplication.
UnittestsversusacceptancetestsWesaidthatthegoalofCIistohaveaprocessasautomatizedaspossible.However,westillneedtomanuallytesttheapplicationinstaging,right?Acceptanceteststotherescue!
Writingunittestsisniceandamust,buttheytestonlysmallpiecesofcodeinanisolatedway.Evenifyourentireunittestssuitepasses,youcannotbesurethatyourapplicationworksatallasyoumightnotintegrateallthepartsproperlybecauseyouaremissingfunctionalitiesorthefunctionalitiesthatyoubuiltwerenotwhatthebusinessneeded.Acceptanceteststesttheentireflowofaspecificusecase.
Ifyourapplicationisawebsite,acceptancetestswillprobablylaunchabrowserandemulateuseractions,suchasclickingandtyping,inordertoassertthatthepagereturnswhatisexpected.Yes,fromafewlinesofcode,youcanexecutealltheteststhatwerepreviouslymanualinanautomatedway.
Now,imaginethatyouwroteacceptancetestsforallthefeaturesofyourapplication.Oncethecodeisinstaging,theCItoolcanautomaticallyrunallofthesetestsandmakesurethatthenewcodedoesnotbreakanyexistingfunctionality.Youcanevenrunthemusingasmanydifferentbrowsersasyouneedtomakesurethatyourapplicationworksfineinallofthem.Ifatestfails,theCItoolwillnotifytheteamresponsible,andtheywillhavetofixit.Ifallthetestspass,theCItoolcanautomaticallydeployyourcodeintoproduction.
Whydoweneedtowriteunitteststhen,ifacceptanceteststestwhatthebusinessreallycaresabout?Thereareseveralreasonstokeepbothacceptanceandunittests;infact,youshouldhavewaymoreunitteststhanacceptancetests.
Unittestschecksmallpiecesofcode,whichmakethemorders-of-magnitudefasterthanacceptancetests,whichtestthewholeflowagainstabrowser.Thatmeansthatyoucanrunallyourunittestsinafewsecondsorminutes,butitwilltakemuchlongertorunallyouracceptancetests.Writingacceptanceteststhatcoverabsolutelyallthepossiblecombinationsofusecasesisvirtuallyimpossible.Writingunitteststhatcoverahighpercentageofusecasesforagivenmethodorpieceofcodeisrathereasy.Youshouldhaveloadsofunitteststestingasmanyedgecasesaspossiblebutonlysomeacceptanceteststestingthemainusecases.
Whenshouldyouruneachtypeoftestthen?Asunittestsarefaster,theyshouldbeexecutedduringthefirststagesofdeployment.Onlyonceweknowthattheyallhavepasseddowewanttospendtimedeployingtostagingandrunningacceptancetests.
TDDversusBDDInChapter7,TestingWebApplications,youlearnedthatTDDortest-drivendevelopmentisthepracticeofwritingfirsttheunittestsandthenthecodeinanattempttowritetestableandcleanercodeandtomakesurethatyourtestsuiteisalwaysuptodate.Withtheappearanceofacceptancetests,TDDevolvedtoBDDorbehavior-drivendevelopment.
BDDisquitesimilartoTDD,inthatyoushouldwritethetestsfirstandthenthecodethatmakesthesetestspass.TheonlydifferenceisthatwithBDD,wewriteteststhatspecifythedesiredbehaviorofthecode,whichcanbetranslatedtoacceptancetests.Eventhoughitwillalwaysdependonthesituation,youshouldwriteacceptanceteststhattestaveryspecificpartoftheapplicationratherthanlongusecasesthatcontainseveralsteps.WithBDD,aswithTDD,youwanttogetquickfeedback,andifyouwriteabroadtest,youwillhavetowritealotofcodeinordertomakeitpass,whichisnotthegoalthatBDDwantstoachieve.
BusinesswritingtestsThewholepointofacceptancetestsandBDDistomakesurethatyourapplicationworksasexpected,notonlyyourcode.Acceptancetests,then,shouldnotbewrittenbydevelopersbutbythebusinessitself.Ofcourse,youcannotexpectthatmanagersandexecutiveswilllearnhowtocodeinordertocreateacceptancetests,butthereisabunchoftoolsthatallowyoutotranslateplainEnglishinstructionsorbehavioralspecificationsintoacceptancetests’code.Ofcourse,theseinstructionshavetofollowsomepatterns.Behavioralspecificationshavethefollowingparts:
Atitle,whichdescribesbriefly,butinaveryclearway,whatusecasethebehavioralspecificationcovers.Anarrative,whichspecifieswhoperformsthetest,whatthebusinessvalueis,andwhattheexpectedoutcomeis.Usuallytheformatofthenarrativeisthefollowing:
Inorderto<businessvalue>
Asa<stakeholder>
Iwantto<expectedoutcome>
Asetofscenarios,whichisadescriptionandasetofstepsofeachspecificusecasethatwewanttocover.EachscenariohasadescriptionandalistofinstructionsintheGiven-When-Thenformat;wewilldiscussmoreonthisinthenextsection.Acommonpatternsis:
Scenario:<shortdescription>
Given<setupscenario>
When<stepstotake>
Then<expectedoutcome>
Inthenexttwosections,wewilldiscovertwotoolsinPHPthatyoucanuseinordertounderstandbehavioralscenariosandrunthemasacceptancetests.
BDDwithBehatThefirstofthetoolswewillintroduceisBehat.BehatisaPHPframeworkthatcantransformbehavioralscenariosintoacceptancetestsandthenrunthem,providingfeedbacksimilartoPHPUnit.TheideaistomatcheachofthestepsinEnglishwiththescenariosinaPHPfunctionthatperformssomeactionorassertssomeresults.
Inthissection,wewilltrytoaddsomeacceptanceteststoourapplication.Theapplicationwillbeasimpledatabasemigrationscriptthatwillallowustokeeptrackofthechangesthatwewilladdtoourschema.Theideaisthateachtimethatyouwanttochangeyourdatabase,youwillwritethechangesonamigrationfileandthenexecutethescript.Theapplicationwillcheckwhatwasthelastmigrationexecutedandwillperformnewones.WewillfirstwritetheacceptancetestsandthenintroducethecodeprogressivelyasBDDsuggests.
InordertoinstallBehatonyourdevelopmentenvironment,youcanuseComposer.Thecommandisasfollows:
$composerrequirebehat/behat
Behatactuallydoesnotcomewithanysetofassertionfunctions,soyouwillhavetoeitherimplementyourownbywritingconditionalsandthrowingexceptionsoryoucouldintegrateanylibrarythatprovidesthem.DevelopersusuallychoosePHPUnitforthisastheyarealreadyusedtoitsassertions.Addit,then,toyourprojectviathefollowing:
$composerrequirephpunit/phpunit
AswithPHPUnit,Behatneedstoknowwhereyourtestsuiteislocated.Youcaneitherhaveaconfigurationfilestatingthisandotherconfigurationoptions,whichissimilartothephpunit.xmlconfigurationfileforPHPUnit,oryoucouldfollowtheconventionsthatBehatsetsandskiptheconfigurationstep.Ifyouchoosethesecondoption,youcanletBehatcreatethefolderstructureandPHPtestclassforyouwiththefollowingcommand:
$./vendor/bin/behat--init
Afterrunningthiscommand,youshouldhaveafeatures/bootstrap/FeatureContext.phpfile,whichiswhereyouneedtoaddthestepsofthePHPfunctions’matchingscenarios.Moreonthisshortly,butfirst,let’sfindouthowtowritebehavioralspecificationssothatBehatcanunderstandthem.
IntroducingtheGherkinlanguageGherkinisthelanguage,orrathertheformat,thatbehavioralspecificationshavetofollow.UsingGherkinnaming,eachbehavioralspecificationisafeature.Eachfeatureisaddedtothefeaturesdirectoryandshouldhavethe.featureextension.FeaturefilesshouldstartwiththeFeaturekeywordfollowedbythetitleandthenarrativeinthesameformatthatwealreadymentionedbefore—thatis,theInorderto–Asa–Ineedtostructure.Infact,Gherkinwillonlyprinttheselines,butkeepingitconsistentwillhelpyourdevelopersandbusinessknowwhattheyaretryingtoachieve.
Ourapplicationwillhavetwofeatures:oneforthesetupofourdatabasetoallowthemigrationstooltowork,andtheotheroneforthecorrectbehaviorwhenaddingmigrationstothedatabase.Addthefollowingcontenttothefeatures/setup.featurefile:
Feature:Setup
Inordertorundatabasemigrations
Asadeveloper
Ineedtobeabletocreatetheemptyschemaandmigrationstable.
Then,addthefollowingfeaturedefinitiontothefeatures/migrations.featurefile:
Feature:Migrations
Inordertoaddchangestomydatabaseschema
Asadeveloper
Ineedtobeabletorunthemigrationsscript
DefiningscenariosThetitleandnarrativeoffeaturesdoesnotreallydoanythingmorethangiveinformationtothepersonwhorunsthetests.Therealworkisdoneinscenarios,whicharespecificusecaseswithasetofstepstotakeandsomeassertions.Youcanaddasmanyscenariosasyouneedtoeachfeaturefileaslongastheyrepresentdifferentusecasesofthesamefeature.Forexample,forsetup.feature,wecanaddacoupleofscenarios:onewhereitisthefirsttimethattheuserrunsthescript,sotheapplicationwillhavetosetupthedatabase,andonewheretheuseralreadyexecutedthescriptpreviously,sotheapplicationdoesnotneedtogothroughthesetupprocess.
AsBehatneedstobeabletotranslatethescenarioswritteninplainEnglishtoPHPfunctions,youwillhavetofollowsomeconventions.Infact,youwillseethattheyareverysimilartotheonesthatwealreadymentionedinthebehavioralspecificationssection.
WritingGiven-When-ThentestcasesAscenariomuststartwiththeScenariokeywordfollowedbyashortdescriptionofwhatusecasethescenariocovers.Then,youneedtoaddthelistofstepsandassertions.Gherkinallowsyoutousefourkeywordsforthis:Given,When,Then,andAnd.Infact,theyallhavethesamemeaningwhenitcomestocode,buttheyaddalotofsemanticvaluetoyourscenarios.Let’sconsideranexample;addthefollowingscenarioattheendofyoursetup.featurefile:
Scenario:SchemadoesnotexistandIdonothavemigrations
GivenIdonothavethe"bdd_db_test"schema
AndIdonothavemigrationfiles
WhenIrunthemigrationsscript
ThenIshouldhaveanemptymigrationstable
AndIshouldget:
"""
Latestversionappliedis0.
"""
Thisscenariotestswhathappenswhenwedonothaveanyschemainformationandrunthemigrationsscript.First,itdescribesthestateofthescenario:GivenIdonothavethebdd_db_testschemaAndIdonothavemigrationfiles.Thesetwolineswillbetranslatedtoonemethodeach,whichwillremovetheschemaandallmigrationfiles.Then,thescenariodescribeswhattheuserwilldo:WhenIrunthemigrationsscript.Finally,wesettheexpectationsforthisscenario:ThenIshouldhaveanemptymigrationstableAndIshouldgetLatestversionappliedis0..
Ingeneral,thesamestepwillalwaysstartbythesamekeyword—thatis,IrunthemigrationsscriptwillalwaysbeprecededbyWhen.TheAndkeywordisaspecialoneasitmatchesallthethreekeywords;itsonlypurposeistohavestepsasEnglish-friendlyaspossible;althoughifyouprefer,youcouldwriteGivenIdonothavemigrationfiles.
Anotherthingtonoteinthisexampleistheuseofargumentsaspartofthestep.ThelineAndIshouldgetisfollowedbyastringenclosedby""".ThePHPfunctionwillgetthisstringasanargument,soyoucanhaveoneuniquestepdefinition—thatis,thefunction—
forawidevarietyofsituationsjustusingdifferentstrings.
ReusingpartsofscenariosItisquitecommonthatforagivenfeature,youalwaysstartfromthesamescenario.Forexample,setup.featurehasascenarioinwhichwecanrunthemigrationsforthefirsttimewithoutanymigrationfile,butwewillalsoaddanotherscenarioinwhichwewanttorunthemigrationsscriptforthefirsttimewithsomemigrationfilestomakesurethatitwillapplyallofthem.Bothscenarioshaveincommononething:theydonothavethedatabasesetup.
Gherkinallowsyoutodefinesomestepsthatwillbeappliedtoallthescenariosofthefeature.YoucanusetheBackgroundkeywordandalistofsteps,usuallyGiven.Addthesetwolinesbetweenthefeaturenarrativeandscenariodefinition:
Background:
GivenIdonothavethe"bdd_db_test"schema
Now,youcanremovethefirststepfromtheexistingscenarioasBackgroundwilltakecareofit.
WritingstepdefinitionsSofar,wehavewrittenfeaturesusingtheGherkinlanguage,butwestillhavenotconsideredhowanyofthestepsineachscenarioistranslatedtoactualcode.TheeasiestwaytonotethisisbyaskingBehattoruntheacceptancetests;asthestepsarenotdefinedanywhere,BehatwillprintoutallthefunctionsthatyouneedtoaddtoyourFeatureContextclass.Torunthetests,justexecutethefollowingcommand:
$./vendor/bin/behat
Thefollowingscreenshotshowstheoutputthatyoushouldgetifyouhavenostepdefinitions:
Asyoucannote,Behatcomplainedaboutsomemissingstepsandthenprintedinyellowthemethodsthatyoucoulduseinordertoimplementthem.Copyandpastethemintoyourautogeneratedfeatures/bootstrap/FeatureContext.phpfile.ThefollowingFeatureContextclasshasalreadyimplementedallofthem:
<?php
useBehat\Behat\Context\Context;
useBehat\Behat\Context\SnippetAcceptingContext;
useBehat\Gherkin\Node\PyStringNode;
require_once__DIR__.
'/../../vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
classFeatureContextimplementsContext,SnippetAcceptingContext
{
private$db;
private$config;
private$output;
publicfunction__construct(){
$configFileContent=file_get_contents(
__DIR__.'/../../config/app.json'
);
$this->config=json_decode($configFileContent,true);
}
privatefunctiongetDb():PDO{
if($this->db===null){
$this->db=newPDO(
"mysql:host={$this->config['host']};"
."dbname=bdd_db_test",
$this->config['user'],
$this->config['password']
);
}
return$this->db;
}
/**
*@GivenIdonothavethe"bdd_db_test"schema
*/
publicfunctioniDoNotHaveTheSchema()
{
$this->executeQuery('DROPSCHEMAIFEXISTSbdd_db_test');
}
/**
*@GivenIdonothavemigrationfiles
*/
publicfunctioniDoNotHaveMigrationFiles()
{
exec('rmdb/migrations/*.sql>/dev/null2>&1');
}
/**
*@WhenIrunthemigrationsscript
*/
publicfunctioniRunTheMigrationsScript()
{
exec('phpmigrate.php',$this->output);
}
/**
*@ThenIshouldhaveanemptymigrationstable
*/
publicfunctioniShouldHaveAnEmptyMigrationsTable()
{
$migrations=$this->getDb()
->query('SELECT*FROMmigrations')
->fetch();
assertEmpty($migrations);
}
privatefunctionexecuteQuery(string$query)
{
$removeSchemaCommand=sprintf(
'mysql-u%s%s-h%s-e"%s"',
$this->config['user'],
empty($this->config['password'])
?'':"-p{$this->config['password']}",
$this->config['host'],
$query
);
exec($removeSchemaCommand);
}
}
Asyoucannote,wereadtheconfigurationfromtheconfig/app.jsonfile.Thisisthesameconfigurationfilethattheapplicationwilluse,anditcontainsthedatabase’scredentials.WealsoinstantiatedaPDOobjecttoaccessthedatabasesothatwecouldaddorremovetablesortakealookatwhatthescriptdid.
Stepdefinitionsareasetofmethodswithacommentoneachofthem.Thiscommentisanannotationasitstartswith@andisbasicallyaregularexpressionmatchingtheplainEnglishstepdefinedinthefeature.Eachofthemhasitsimplementation:eitherremovingadatabaseormigrationfiles,executingthemigrationsscript,orcheckingwhatthemigrationstablecontains.
TheparameterizationofstepsInthepreviousFeatureContextclass,weintentionallymissedtheiShouldGetmethod.Asyoumightrecall,thisstephasastringargumentidentifiedbyastringenclosedbetween""".Theimplementationforthismethodlooksasfollows:
/**
*@ThenIshouldget:
*/
publicfunctioniShouldGet(PyStringNode$string)
{
assertEquals(implode("\n",$this->output),$string);
}
Notehowtheregularexpressiondoesnotcontainthestring.Thishappenswhenusinglongstringswith""".Also,theargumentisaninstanceofPyStringNode,whichisabitmorecomplexthananormalstring.However,fearnot;whenyoucompareitwithastring,PHPwilllookforthe__toStringmethod,whichjustprintsthecontentofthestring.
RunningfeaturetestsIntheprevioussections,wewroteacceptancetestsusingBehat,butwehavenotwrittenasinglelineofcodeyet.Beforerunningthem,though,addtheconfig/app.jsonconfigurationfilewiththecredentialsofyourdatabaseusersothattheFeatureContextconstructorcanfindit,asfollows:
{
"host":"127.0.0.1",
"schema":"bdd_db_test",
"user":"root",
"password":""
}
Now,let’sruntheacceptancetests,expectingthemtofail;otherwise,ourtestswillnotbevalidatall.Theoutputshouldbesomethingsimilartothis:
Asexpected,theThenstepsfailed.Let’simplementtheminimumcodenecessaryinordertomakethetestspass.Forstarters,addtheautoloaderintoyourcomposer.jsonfileandruncomposerupdate:
"autoload":{
"psr-4":{
"Migrations\\":"src/"
}
}
WewouldliketoimplementaSchemaclassthatcontainsthehelpersnecessarytosetupadatabase,runmigrations,andsoon.Rightnow,thefeatureisonlyconcernedaboutthesetupofthedatabase—thatis,creatingthedatabase,addingtheemptymigrationstabletokeeptrackofallthemigrationsadded,andtheabilitytogetthelatestmigrationregistered
assuccessful.Addthefollowingcodeassrc/Schema.php:
<?php
namespaceMigrations;
useException;
usePDO;
classSchema{
constSETUP_FILE=__DIR__.'/../db/setup.sql';
constMIGRATIONS_DIR=__DIR__.'/../db/migrations/';
private$config;
private$connection;
publicfunction__construct(array$config)
{
$this->config=$config;
}
privatefunctiongetConnection():PDO
{
if($this->connection===null){
$this->connection=newPDO(
"mysql:host={$this->config['host']};"
."dbname={$this->config['schema']}",
$this->config['user'],
$this->config['password']
);
}
return$this->connection;
}
}
Eventhoughthefocusofthischapteristowriteacceptancetests,let’sgothroughthedifferentimplementedmethods:
TheconstructorandgetConnectionjustreadtheconfigurationfileinconfig/app.jsonandinstantiatedthePDOobject.ThecreateSchemaexecutedCREATESCHEMAIFNOTEXISTS,soiftheschemaalreadyexists,itwilldonothing.WeexecutedthecommandwithexecinsteadofPDOasPDOalwaysneedstouseanexistingdatabase.ThegetLatestMigrationwillfirstcheckwhetherthemigrationstableexists;ifnot,wewillcreateitusingsetup.sqlandthenfetchthelastsuccessfulmigration.
Wealsoneedtoaddthemigrations/setup.sqlfilewiththequerytocreatethemigrationstable,asfollows:
CREATETABLEIFNOTEXISTSmigrations(
versionINTUNSIGNEDNOTNULL,
`time`TIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMP,
statusENUM('success','error'),
PRIMARYKEY(version,status)
);
Finally,weneedtoaddthemigrate.phpfile,whichistheonethattheuserwillexecute.Thisfilewillgettheconfiguration,instantiatetheSchemaclass,setupthedatabase,andretrievethelastmigrationapplied.Runthefollowingcode:
<?php
require_once__DIR__.'/vendor/autoload.php';
$configFileContent=file_get_contents(__DIR__.'/config/app.json');
$config=json_decode($configFileContent,true);
$schema=newMigrations\Schema($config);
$schema->createSchema();
$version=$schema->getLatestMigration();
echo"Latestversionappliedis$version.\n";
Youarenowgoodtorunthetestsagain.Thistime,theoutputshouldbesimilartothisscreenshot,whereallthestepsareingreen:
Nowthatouracceptancetestispassing,weneedtoaddtherestofthetests.Tomakethingsquicker,wewilladdallthescenarios,andthenwewillimplementthenecessarycodetomakethempass,butitwouldbebetterifyouaddonescenarioatatime.Thesecondscenarioofsetup.featurecouldlookasfollows(rememberthatthefeaturecontainsaBackgroundsection,inwhichwecleanthedatabase):
Scenario:SchemadoesnotexistsandIhavemigrations
GivenIhavemigrationfile1:
"""
CREATETABLEtest1(idINT);
"""
AndIhavemigrationfile2:
"""
CREATETABLEtest2(idINT);
"""
WhenIrunthemigrationsscript
ThenIshouldonlyhavethefollowingtables:
|migrations|
|test1|
|test2|
AndIshouldhavethefollowingmigrations:
|1|success|
|2|success|
AndIshouldget:
"""
Latestversionappliedis0.
Appliedmigration1successfully.
Appliedmigration2successfully.
"""
Thisscenarioisimportantasitusedparametersinsidethestepdefinitions.Forexample,theIhavemigrationfilestepispresentedtwice,eachtimewithadifferentmigrationfilenumber.Theimplementationofthisstepisasfollows:
/**
*@GivenIhavemigrationfile:version:
*/
publicfunctioniHaveMigrationFile(
string$version,
PyStringNode$file
){
$filePath=__DIR__."/../../db/migrations/$version.sql";
file_put_contents($filePath,$file->getRaw());
}
Theannotationofthismethod,whichisaregularexpression,used:versionasawildcard.AnystepthatstartswithGivenIhavemigrationfilefollowedbysomethingelsewillmatchthisstepdefinition,andthe“somethingelse”bitwillbereceivedasthe$versionargumentasastring.
Here,weintroducedyetanothertypeofargument:tables.TheThenIshouldonlyhavethefollowingtablesstepdefinedatableoftworowsofonecolumneach,andtheThenIshouldhavethefollowingmigrationsbitsentatableoftworowsoftwocolumnseach.Theimplementationforthenewstepsisasfollows:
/**
*@ThenIshouldonlyhavethefollowingtables:
*/
publicfunctioniShouldOnlyHaveTheFollowingTables(TableNode$tables){
$tablesInDb=$this->getDb()
->query('SHOWTABLES')
->fetchAll(PDO::FETCH_NUM);
assertEquals($tablesInDb,array_values($tables->getRows()));
}
/**
*@ThenIshouldhavethefollowingmigrations:
*/
publicfunctioniShouldHaveTheFollowingMigrations(
TableNode$migrations
){
$query='SELECTversion,statusFROMmigrations';
$migrationsInDb=$this->getDb()
->query($query)
->fetchAll(PDO::FETCH_NUM);
assertEquals($migrations->getRows(),$migrationsInDb);
}
ThetablesarereceivedasTableNodearguments.ThisclasscontainsagetRowsmethodthatreturnsanarraywiththerowsdefinedinthefeaturefile.
Theotherfeaturethatwewouldliketoaddisfeatures/migrations.feature.Thisfeaturewillassumethattheuseralreadyhasthedatabasesetup,sowewilladdaBackgroundsectionwiththisstep.Wewilladdonescenarioinwhichthemigrationfilenumbersarenotconsecutive,inwhichcasetheapplicationshouldstopatthelastconsecutivemigrationfile.Theotherscenariowillmakesurethatwhenthereisanerror,theapplicationdoesnotcontinuethemigrationprocess.Thefeatureshouldlooksimilartothefollowing:
Feature:Migrations
Inordertoaddchangestomydatabaseschema
Asadeveloper
Ineedtobeabletorunthemigrationsscript
Background:
GivenIhavethebdd_db_test
Scenario:Migrationsarenotconsecutive
GivenIhavemigration3
AndIhavemigrationfile4:
"""
CREATETABLEtest4(idINT);
"""
AndIhavemigrationfile6:
"""
CREATETABLEtest6(idINT);
"""
WhenIrunthemigrationsscript
ThenIshouldonlyhavethefollowingtables:
|migrations|
|test4|
AndIshouldhavethefollowingmigrations:
|3|success|
|4|success|
AndIshouldget:
"""
Latestversionappliedis3.
Appliedmigration4successfully.
"""
Scenario:Amigrationthrowsanerror
GivenIhavemigrationfile1:
"""
CREATETABLEtest1(idINT);
"""
AndIhavemigrationfile2:
"""
CREATETABLEtest1(idINT);
"""
AndIhavemigrationfile3:
"""
CREATETABLEtest3(idINT);
"""
WhenIrunthemigrationsscript
ThenIshouldonlyhavethefollowingtables:
|migrations|
|test1|
AndIshouldhavethefollowingmigrations:
|1|success|
|2|error|
AndIshouldget:
"""
Latestversionappliedis0.
Appliedmigration1successfully.
Errorapplyingmigration2:Table'test1'alreadyexists.
"""
Therearen’tanynewGherkinfeatures.Thetwonewstepimplementationslookasfollows:
/**
*@GivenIhavethebdd_db_test
*/
publicfunctioniHaveTheBddDbTest()
{
$this->executeQuery('CREATESCHEMAbdd_db_test');
}
/**
*@GivenIhavemigration:version
*/
publicfunctioniHaveMigration(string$version)
{
$this->getDb()->exec(
file_get_contents(__DIR__.'/../../db/setup.sql')
);
$query=<<<SQL
INSERTINTOmigrations(version,status)
VALUES(:version,'success')
SQL;
$this->getDb()
->prepare($query)
->execute(['version'=>$version]);
}
Now,itistimetoaddtheneededimplementationtomakethetestspass.Thereareonlytwochangesneeded.ThefirstoneisanapplyMigrationsFrommethodintheSchemaclassthat,givenaversionnumber,willtrytoapplythemigrationfileforthisnumber.Ifthemigrationissuccessful,itwilladdarowinthemigrationstable,withthenewversionaddedsuccessfully.Ifthemigrationfailed,wewouldaddtherecordinthemigrationstableasafailureandthenthrowanexceptionsothatthescriptisawareofit.Finally,ifthemigrationfiledoesnotexist,thereturningvaluewillbefalse.AddthiscodetotheSchemaclass:
publicfunctionapplyMigrationsFrom(int$version):bool
{
$filePath=self::MIGRATIONS_DIR."$version.sql";
if(!file_exists($filePath)){
returnfalse;
}
$connection=$this->getConnection();
if($connection->exec(file_get_contents($filePath))===false){
$error=$connection->errorInfo()[2];
$this->registerMigration($version,'error');
thrownewException($error);
}
$this->registerMigration($version,'success');
returntrue;
}
privatefunctionregisterMigration(int$version,string$status)
{
$query=<<<SQL
INSERTINTOmigrations(version,status)
VALUES(:version,:status)
SQL;
$params=['version'=>$version,'status'=>$status];
$this->getConnection()->prepare($query)->execute($params);
}
Theotherbitmissingisinthemigrate.phpscript.WeneedtocallthenewlycreatedapplyMigrationsFrommethodwithconsecutiveversionsstartingfromthelatestone,untilwegeteitherafalsevalueoranexception.Wealsowanttoprintoutinformationaboutwhatisgoingonsothattheuserisawareofwhatmigrationswereadded.Addthefollowingcodeattheendofthemigrate.phpscript:
do{
$version++;
try{
$result=$schema->applyMigrationsFrom($version);
if($result){
echo"Appliedmigration$versionsuccessfully.\n";
}
}catch(Exception$e){
$error=$e->getMessage();
echo"Errorapplyingmigration$version:$error.\n";
exit(1);
}
}while($result);
Now,runthetestsandvoilà!Theyallpass.Younowhavealibrarythatmanagesdatabasemigrations,andyouare100%surethatitworksthankstoyouracceptancetests.
TestingwithabrowserusingMinkSofar,wehavebeenabletowriteacceptancetestsforascript,butmostofyouarereadingthisbookinordertowriteniceandshinywebapplications.Howcanyoutakeadvantageofacceptanceteststhen?ItistimetointroducethesecondPHPtoolofthischapter:Mink.
MinkisactuallyanextensionofBehat,whichaddsimplementationsofseveralstepsrelatedtowebbrowsertesting.Forexample,ifyouaddMinktoyourapplication,youwillbeabletoaddscenarioswhereMinkwilllaunchabrowserandclickortypeasrequested,savingyoualotoftimeandeffortinmanualtesting.However,first,let’stakealookathowMinkcanachievethis.
TypesofwebdriversMinkmakesuseofwebdrivers—thatis,librariesthathaveanAPIthatallowsyoutointeractwithabrowser.Youcansendcommands,suchasgotothispage,clickonthislink,fillthisinputfieldwiththistext,andsoon,andthewebdriverwilltranslatethisintothecorrectinstructionforyourbrowser.Thereareseveralwebdrivers,eachofthemimplementedfollowingadifferentapproach.Itisforthisreasonthatdependingonthewebdriver,youwillhavesomefeaturesorothers.
Webdriverscanbedividedintotwogroupsdependingonhowtheywork:
Headlessbrowsers:Thesedriversdonotreallylaunchabrowser;theyonlytrytoemulateone.TheyactuallyrequestforthewebpageandrendertheHTMLandJavaScriptcode,sotheyareawareofhowthepagelooks,buttheydonotdisplayit.Theyhaveahugebenefit:theyareeasytoinstallandmanage,andastheydonothavetobuildthegraphicalrepresentation,theyareextremelyfast.ThedisadvantageisthattheyhavesevererestrictionsintermsofCSSandsomeJavaScriptfunctionalities,especiallyAJAX.Webdriversthatlaunchrealbrowserslikeauserwoulddo:Thesewebdriverscandoalmostanythingandarewaymorepowerfulthanheadlessbrowsers.Theproblemisthattheycanbeabittrickytoinstallandarevery,veryslow—asslowasarealusertryingtogothroughthescenarios.
So,whichoneshouldyouchoose?Asalways,itwilldependonwhatyourapplicationis.IfyouhaveanapplicationthatdoesnotmakeheavyuseofCSSandJavaScriptanditisnotcriticalforyourbusiness,youcoulduseheadlessbrowsers.Instead,iftheapplicationisthecornerstoneofyourbusinessandyouneedtobeabsolutelycertainthatalltheUIfeaturesworkasexpected,youmightwanttogoforwebdriversthatlaunchbrowsers.
InstallingMinkwithGoutteInthischapter,wewilluseGoutte,aheadlesswebdriverwrittenbythesameguysthatworkedonSymfony,toaddsomeacceptanceteststotherepositoriespageofGitHub.TherequiredcomponentsofyourprojectwillbeBehat,Mink,andtheGouttedriver.AddthemwithComposerviathefollowingcommands:
$composerrequirebehat/behat
$composerrequirebehat/mink-extension
$composerrequirebehat/mink-goutte-driver
Now,executethefollowinglinetoaskBehattocreatethebasicdirectorystructure:
$./vendor/bin/behat–init
TheonlychangewewilladdtotheFeatureContextclassiswhereitextendsfrom.Thistime,wewilluseMinkContextinordertogetallthestepdefinitionsrelatedtowebtesting.TheFeatureContextclassshouldlooksimilartothis:
<?php
useBehat\MinkExtension\Context\MinkContext;
require__DIR__.'/../../vendor/autoload.php';
classFeatureContextextendsMinkContext{
}
MinkalsoneedssomeconfigurationinordertoletBehatknowwhichwebdriverwewanttouseorwhatthebaseURLforourtestsis.Addthefollowinginformationtobehat.yml:
default:
extensions:
Behat\MinkExtension:
base_url:"https://github.com"
sessions:
default_session:
goutte:~
Withthisconfiguration,weletBehatknowthatweareusingtheMinkextension,thatMinkwilluseGoutteinallthesessions(youcouldactuallydefinedifferentsessionswithdifferentwebdriversifnecessary),andthatthebaseURLforthesetestsistheGitHubone.Behatisalreadyinstructedtolookforthebehat.ymlfileinthesamedirectorythatweexecuteditin,sothereisnothingelsethatweneedtodo.
InteractionwiththebrowserNow,let’slookatthemagic.Ifyouknowthestepstouse,writingacceptancetestswithMinkwillbelikeagame.First,addthefollowingfeatureinfeature/search.feature:
Feature:Search
Inordertofindrepositories
Asawebsiteuser
Ineedtobeabletosearchrepositoriesbyname
Background:
GivenIamon"/picahielos"
AndIfollow"Repositories"
Scenario:Searchingexistingrepository
WhenIfillin"zap"for"q"
AndIpress"Search"
ThenIshouldsee"picahielos/zap"
Scenario:Searchingnon-existingrepository
WhenIfillin"yolo"for"q"
AndIpress"Search"
ThenIshouldnotsee"picahielos/yolo"
ThefirstthingtonoteisthatwehaveaBackgroundsection.Thissectionassumesthattheuservisitedthehttps://github.com/picahielospageandclickedontheRepositorieslink.UsingIfollowwithsomestringistheequivalentoftryingtofindalinkwiththisstringandclickingonit.
ThefirstscenariousedtheWhenIfill<field>with<value>step,whichbasicallytriestofindtheinputfieldonthepage(youcaneitherspecifytheIDorname),andtypesthevalueforyou.Inthiscase,theqfieldwasthesearchbar,andwetypedzap.Then,similartowhenclickingonthelinks,theIpress<button>linewilltrytofindthebuttonbyname,ID,orvalue,andwillclickonit.Finally,ThenIshouldseefollowedbyastringwillassertthatthegivenstringcouldbefoundonthepage.Inshort,thetestlaunchedabrowser,goingtothespecifiedURL,clickingontheRepositorieslink,searchingforthezaprepository,andassertingthatitcouldfindit.Inasimilarway,thesecondscenariotriedtofindarepositorythatdoesnotexist.
Ifyourunthetests,theyshouldpass,butyouwillnotseeanybrowser.RememberthatGoutteisaheadlessbrowserwebdriver.However,checkhowfastthesetestsareexecuted;inmylaptop,ittooklessthan3seconds!Canyouimagineanyoneperformingthesetwotestsmanuallyinlessthanthistime?
Onelastthing:havingacheatsheetofpredefinedMinkstepsisoneofthehandiestthingstohavenearyourdesk;youcanfindoneathttp://blog.lepine.pro/images/2012-03-behat-cheat-sheet-en.pdf.Asyoucansee,wedidnotwriteasinglelineofcode,andwestillhavetwotestsmakingsurethatthewebsiteworksasexpected.Also,ifyouneedtoaddafancierstep,donotworry;youcanstillimplementyourstepdefinitionsaswedidinBehatpreviouslywhiletakingadvantageofthewebdriver’sinterfacethatMinkprovides.Werecommendyoutogothroughtheofficialdocumentationinordertotakealookatthe
SummaryInthisconcludingchapter,youlearnedhowimportantitistocoordinatethebusinesswiththeapplication.Forthis,yousawwhatBDDisandhowtoimplementitwithyourPHPwebapplicationsusingBehatandMink.ThisalsogivesyoutheabilitytotesttheUIwithwebdrivers,whichyoucouldnotdoitwithunittestsandPHPUnit.Now,youcanmakesurethatnotonlyisyourapplicationbug-freeandsecure,butalsothatitdoeswhatthebusinessneedsittodo.
Congratulationsonreachingtheendofthebook!Youstartedasaninexperienceddeveloper,butnowyouareabletowritesimpleandcomplexwebsitesandRESTAPIswithPHPandhaveanextensiveknowledgeofgoodtestpractices.YouhaveevenworkedwithacoupleoffamousPHPframeworks,soyouarereadytoeitherstartanewprojectwiththemorjoinateamthatusesoneofthem.
Now,youmightbewondering:whatdoIdonext?Youalreadyknowthetheory—well,someofit—sowewouldrecommendthatyoupracticealot.Thereareseveralwaysyoucandothis:bycreatingyourownapplication,joiningateamworkingonopensourceprojects,orworkingforacompany.Trytokeepuptodatewithnewreleasesofthelanguageorthetoolsandframeworks,discoveranewframeworkfromtimetotime,andneverstopreading.Expandingyoursetofskillsisalwaysagreatidea!
Ifyourunoutofideasonwhattoreadnext,herearesomehints.Wedidnotgothroughthefrontendparttoomuch,soyoumightbeinterestedinreadingaboutCSSandspeciallyJavaScript.JavaScripthasbecomethemaincharacterintheselastfewyears,sodonotmissitout.Ifyouareratherinterestedinthebackendsideandhowtomanageapplicationsproperly,trydiscoveringnewtechnologies,suchascontinuousintegrationtoolssimilartoJenkins.Finally,ifyouprefertofocusonthetheoryand“science”side,youcanreadabouthowtowritequalitycodewithCodeComplete,SteveMcConnell,orhowtomakegooduseofdesignpatternswithDesignPatterns:ElementsofReusableObject-OrientedSoftware,ErichGamma,JohnVlissides,RalphJohnson,andRichardHelm,agangoffour.
Alwaysenjoyandhavefunwhendeveloping.Always!
IndexA
abstractclassesabout/Abstractclasses
acceptancetestsabout/Typesoftestsversusunittests/Unittestsversusacceptancetests
aliasesURL/Managingdependencies
anonymousfunctionsabout/Anonymousfunctions
Apachereference/ThePHPbuilt-inserver
APIabout/IntroducingAPIs
APIstesting,withbrowsers/TestingAPIswithbrowserstesting,withcommandline/TestingAPIsusingthecommandline
argumentsbyvalueversusargumentsbyreference/Functionarguments
arithmeticoperatorsabout/Arithmeticoperators
arrayfunctionsabout/Otherarrayfunctions
arraysabout/Arraysinitializing/Initializingarrayspopulating/Populatingarraysaccessing/Accessingarraysissetfunction/Theemptyandissetfunctionsemptyfunction/Theemptyandissetfunctionselements,searchingin/Searchingforelementsinanarrayordering/Orderingarrays
assertionsabout/Assertionsreference/Assertions
assignmentoperatorsabout/Assignmentoperators
authenticationabout/RESTAPIsecurity
authorizationabout/RESTAPIsecurity
BBDD
versusTDD/TDDversusBDDBDD,withBehat
about/BDDwithBehatBehat
about/BDDwithBehatbehavior-drivendevelopment
about/Behavior-drivendevelopmentbehavioralspecifications
about/Businesswritingtestsbestpractices,RESTAPIs
about/BestpracticeswithRESTAPIsconsistency,inendpoints/Consistencyinyourendpointsdocumenting/Documentasmuchasyoucanfilters/Filtersandpaginationpagination/FiltersandpaginationAPIversioning/APIversioningHTTPcache,using/UsingHTTPcache
browsersAPIs,testingwith/TestingAPIswithbrowsers
businesswritingtestsabout/Businesswritingtests
Ccachelayer
about/Cachecallable
about/Anonymousfunctionscasting
about/Gettinginformationfromtheuserversustypejuggling/Gettinginformationfromtheuser
Cforcontrollerdefining/Cforcontrollererrorcontroller/Theerrorcontrollerlogincontroller/Thelogincontrollerbookcontroller/Thebookcontrollerbooks,borrowing/Borrowingbookssalescontroller/Thesalescontroller
classabout/Classesandobjects
classconstructorsabout/Classconstructors
classesconventions/Propertiesandmethodsvisibilityautoloading/Autoloadingclasses
classmethodsabout/Classmethods
classpropertiesabout/Classproperties
codecoverageabout/Unittestsandcodecoverage
commandlineAPIs,testingwith/TestingAPIsusingthecommandline
comparisonoperatorsabout/Comparisonoperators
components,frameworksrouter/Themainpartsofaframeworkrequest/Themainpartsofaframeworkconfigurationhandler/Themainpartsofaframeworktemplateengine/Themainpartsofaframeworklogger/Themainpartsofaframeworkdependencyinjector/Themainpartsofaframework
Composerreference/InstallingComposerusing/UsingComposerdependencies,managing/Managingdependencies
autoloader,withPSR-4/AutoloaderwithPSR-4metadata,adding/Addingmetadataindex.phpfile/Theindex.phpfile
conditionalsabout/Controlstructures,Conditionals
constraintsabout/Keysandconstraints
continuousintegration(CI)about/Introducingcontinuousintegration
controllersabout/TheMVCpattern
controlstructuresabout/Controlstructuresconditionals/Conditionalsswitch…case/Switch…caseloops/Loops
cookiesdata,persistingwith/Persistingdatawithcookies
CSSabout/HTML,CSS,andJavaScript
cURLabout/Settinguptheapplication
Ddata
persisting,withcookies/Persistingdatawithcookiesinserting/Insertingdataquerying/Queryingdataupdating/Updatinganddeletingdata,Updatingdatadeleting/Deletingdata
databasesversusfiles/Writingfilesabout/IntroducingdatabasesMySQL/MySQL
databases,datatypesabout/Databasedatatypesnumericdatatypes/Numericdatatypesstringdatatypes/Stringdatatypeslistofvalues/Listofvaluesdateandtimedatatypes/Dateandtimedatatypes
databasetestingabout/Databasetesting
dataprovidersabout/Dataproviders
dataprovidingabout/Dataproviders
DataSourceName(DSN)/Connectingtothedatabasedatatypes
about/DatatypesBooleans/Datatypesintegers/Datatypesfloats/Datatypesstrings/Datatypesreference/Databasedatatypes
dateandtimedatatypesabout/Dateandtimedatatypesreferencelink/Dateandtimedatatypes
decrementingoperatorsabout/Incrementinganddecrementingoperators
DELETEmethod/DELETEdependencyinjection
defining/Dependencyinjectionabout/Dependencyinjectionneedfor/Whyisdependencyinjectionnecessary?
dependencyinjectorimplementing/Implementingourowndependencyinjector
designpatternsabout/Designpatternsfactory/Factorysingleton/Singleton
DesignPatternsPHPreference/Designpatterns
DImodels,injectingwith/InjectingmodelswithDI
doublestestingwith/Testingwithdoubles
do…whileloop/Do…while
Eelements
searching,inarray/SearchingforelementsinanarrayEloquentJavaScript
reference/HTML,CSS,andJavaScriptemptyfunction
about/Theemptyandissetfunctionsencapsulation
about/Encapsulationenvironment
settingup,withVagrant/SettinguptheenvironmentwithVagrantenvironmentsetup,onOSX
about/SettinguptheenvironmentonOSXPHP,installing/InstallingPHPMySQL,installing/InstallingMySQLNginx,installing/InstallingNginxComposer,installing/InstallingComposer
environmentsetup,onUbuntuabout/SettinguptheenvironmentonUbuntuPHP,installing/InstallingPHPMySQL,installing/InstallingMySQLNginx,installing/InstallingNginx
environmentsetup,onWindowsabout/SettinguptheenvironmentonWindowsPHP,installing/InstallingPHPMySQL,installing/InstallingMySQLNginx,installing/InstallingNginxComposer,installing/InstallingComposer
escapecharactersabout/Workingwithstrings
exceptionhandlingtry…catchblock/Thetry…catchblockfinallyblock/Thefinallyblock
exceptionshandling/Handlingexceptionscatching/Catchingdifferenttypesofexceptions
exitcondition/Forexpectingexceptions
about/Expectingexceptionsexpression
about/Operators
Ffactorydesignpattern
about/Factoryfeature
about/IntroducingtheGherkinlanguagefeatures,frameworks
about/Otherfeaturesofframeworksauthentication/Authenticationandrolesroles/AuthenticationandrolesObject-relationalmapping(ORM)/ORMcache/Cacheinternationalization/Internationalization
featuretestsrunning/Runningfeaturetests
fetchmodeadvantages/Thebookmodeldisadvantages/Thebookmodel
fieldsabout/Schemasandtables
fields,tableNOTNULL/ManagingtablesUNSIGNED/ManagingtablesDEFAULT<value>/Managingtables
filesreading/Readingfileswriting/Writingfilesversusdatabases/Writingfiles
filesystemabout/Thefilesystem
filesystemfunctionsabout/Otherfilesystemfunctions
finallyblockabout/Thefinallyblock
foreachloop/Foreachforeignkeybehaviors/Foreignkeybehaviorsforeignkeys
about/Foreignkeysforloop/Forfoundations,RESTAPIs
HTTPrequestmethods/HTTPrequestmethodsstatuscodes,inresponses/StatuscodesinresponsesRESTAPIsecurity/RESTAPIsecurity
framework,types
about/Typesofframeworkscomplete/Completeandrobustframeworksrobust/Completeandrobustframeworkslightweight/Lightweightandflexibleframeworksflexible/Lightweightandflexibleframeworks
frameworksreviewing/Reviewingframeworkspurpose/Thepurposeofframeworksparts/Themainpartsofaframeworkcomponents/Themainpartsofaframeworkfeatures/Otherfeaturesofframeworksoverview/AnoverviewoffamousframeworksSymfony2/Symfony2ZendFramework2/ZendFramework2
functionarguments/Functionargumentsfunctions
about/Functionsdeclaring/Functiondeclaration
functions,arraysreference/Orderingarrays,Otherarrayfunctions
functions,dateandtimedatatypesDAY()/DateandtimedatatypesMONTH()/DateandtimedatatypesYEAR()/DateandtimedatatypesHOUR()/DateandtimedatatypesMINUTE()/DateandtimedatatypesSECOND()/DateandtimedatatypesCURRENT_DATE()/DateandtimedatatypesCURRENT_TIME()/DateandtimedatatypesNOW()/DateandtimedatatypesDATE_FORMAT()/DateandtimedatatypesDATE_ADD()/Dateandtimedatatypes
functions,PDObeginTransaction/Workingwithtransactionscommit/WorkingwithtransactionsrollBack/Workingwithtransactions
functions,PHPfilesinclude/PHPfilesrequire/PHPfilesinclude_once/PHPfilesrequire_once/PHPfiles
functions,stringsreference/Workingwithstringsstrlen/Workingwithstrings
trim/Workingwithstringsstrtolower/Workingwithstringsstrtoupper/Workingwithstringsstr_replace/Workingwithstringssubstr/Workingwithstringsstrpos/Workingwithstrings
GGETmethod/GETgetter
about/EncapsulationGherkin
about/IntroducingtheGherkinlanguageGiven-When-Thentestcases
writing/WritingGiven-When-ThentestcasesGoutte
Mink,installingwith/InstallingMinkwithGoutteGraphicalUserInterface(GUI)
about/MySQLGuzzle
about/Settinguptheapplication
HHTML
about/HTML,CSS,andJavaScriptHTMLforms
about/HTMLformsHTTP
about/TheHTTPprotocolHTTPmessage,parts
about/PartsofthemessageURI/URLHTTPmethod/TheHTTPmethodbody/Bodyheaders/Headersstatuscode/Thestatuscode
HTTPmethodabout/TheHTTPmethodGET/TheHTTPmethodPOST/TheHTTPmethodPUT/TheHTTPmethodDELETE/TheHTTPmethodOPTION/TheHTTPmethod
HTTPprotocolabout/TheHTTPprotocolinterchangeofmessages,example/Asimpleexamplecomplexexample/Amorecomplexexample
HTTPrequestmethodsabout/HTTPrequestmethodsGET/GETPOST/POSTandPUTPUT/POSTandPUTDELETE/DELETE
I500internalservererror/5xx–servererrorIlluminate\Database\Eloquent\Model/Projectsetupimpersonification
about/Authenticationandrolesincrementingoperators
about/Incrementinganddecrementingoperatorsindexes
about/Indexesinfiniteloops/Whileinformationhiding
about/Encapsulationinheritance
about/Inheritance,Introducinginheritancemethods,overriding/Overridingmethodsabstractclasses/Abstractclasses
installingVagrant/InstallingVagrantMink,withGoutte/InstallingMinkwithGoutte
integrationtestsabout/Typesoftests
interfaceabout/Interfaces
internationalizationabout/Internationalization
issetfunctionabout/Theemptyandissetfunctions
Llambdafunctions
about/AnonymousfunctionsLaravel
versusSilex/SilexversusLaravelLaravelframework
about/TheLaravelframeworkinstallation/Installationprojectsetup/Projectsetupfirstendpoint,adding/Addingthefirstendpointusers,managing/Managingusersrelationships,settingupinmodels/Settinguprelationshipsinmodelscomplexcontrollers,creating/Creatingcomplexcontrollerstests,adding/Addingtests
layoutabout/Layoutsandblocks
lazyloadabout/Thesalesmodel
leftjoinsabout/Joiningtables
listofvaluesabout/Listofvalues
listsabout/Arrays
logicaloperatorsabout/Logicaloperators
loopsabout/Controlstructures,Loopswhileloop/Whiledo…whileloop/Do…whileforloop/Forforeach/Foreach
Mmagicmethods
about/Magicmethods__toString/Magicmethods__call/Magicmethods__get/Magicmethods
mapsabout/Arrays
methodsoverriding/Overridingmethods
methodsvisibilityabout/Propertiesandmethodsvisibility
Mformodeldefining/Mformodelcustomermodel/Thecustomermodelbookmodel/Thebookmodelsalesmodel/Thesalesmodel
Minkused,fortestingwithbrowser/TestingwithabrowserusingMinkinstalling,withGoutte/InstallingMinkwithGouttebrowserinteraction/Interactionwiththebrowser
mocksusing/Usingmocks
modelsabout/TheMVCpatterninjecting,withDI/InjectingmodelswithDI
Monologabout/Addingaloggerreference/Addingalogger
MVCpatterndefining/TheMVCpattern
MySQLabout/MySQL
MySQLenginesreference/Managingtables
MySQLserverinstallerreference/InstallingMySQL,InstallingMySQL
MySQLWorkbenchreference/InstallingMySQL
Nnamespaces
about/NamespacesNginx
reference/ThePHPbuilt-inservernumericdatatypes
about/Numericdatatypes
OOAuth2authentication
database,settingup/Settingupthedatabaseclient-credentialsauthentication,enabling/Enablingclient-credentialsauthenticationaccesstoken,requesting/Requestinganaccesstoken
OAuth2.0about/OAuth2.0
OAuth2Serverinstalling/InstallingOAuth2Server
Object-relationalmapping(ORM)about/ORM
objectsabout/Classesandobjects
operatorprecedenceabout/Operatorprecedence
operatorsabout/Operatorsarithmeticoperators/Arithmeticoperatorsassignmentoperators/Assignmentoperatorscomparisonoperators/Comparisonoperatorslogicaloperators/Logicaloperatorsdecrementingoperators/Incrementinganddecrementingoperatorsincrementingoperators/Incrementinganddecrementingoperators
optionalarguments/Functionargumentsoverindexing
about/Indexesoverloadedfunctions/Functiondeclaration
PPackagist
about/Addingmetadata,Settinguptheapplicationreferences/Addingmetadata
PDOusing/UsingPDOconnecting,todatabase/Connectingtothedatabasequeries,performing/Performingqueriespreparedstatements/Preparedstatements
PHPreference/AutoloaderwithPSR-4
PHP,andHTMLmixing/Conditionals
PHP,inwebapplicationsabout/PHPinwebapplicationsinformation,obtainingfromuser/GettinginformationfromtheuserHTMLforms/HTMLformsdata,persistingwithcookies/Persistingdatawithcookies
PHPbuilt-inserverabout/ThePHPbuilt-inserver
PHPfilesabout/PHPfilesfunctions/PHPfiles
PHPfunctions,filesystemfile_exists/Otherfilesystemfunctionsis_writable/Otherfilesystemfunctionsreference/Otherfilesystemfunctions
PHPinstallerreference/InstallingPHP
PHPUnitabout/IntegratingPHPUnitintegrating/IntegratingPHPUnit
phpunit.xmlfileabout/Thephpunit.xmlfile
Pimpleabout/Projectsetup
polymorphismabout/Polymorphism
POSTmethod/POSTandPUTpreparedstatements/Preparedstatementsprimarykeys
about/Primarykeysproductionwebservers
about/ThePHPbuilt-inserverprojectsetup,Silexmicroframework
about/Projectsetupconfiguration,managing/Managingconfigurationtemplateengine,setting/Settingthetemplateenginelogger,adding/Addingalogger
propertiesvisibilityabout/Propertiesandmethodsvisibility
PUTmethod/POSTandPUT
Rreceiver
about/Asimpleexamplereflection
about/Databasetestingreference/Databasetesting
requestsworkingwith/Workingwithrequestsrequestobject/Therequestobjectparameters,filteringfrom/Filteringparametersfromrequestsroutes,mappingtocontrollers/Mappingroutestocontrollersrouter/Therouter
RESTAPI,creatingwithLaravelabout/CreatingaRESTAPIwithLaravelOAuth2authentication,setting/SettingOAuth2authenticationdatabase,preparing/Preparingthedatabasemodels,settingup/Settingupthemodelsendpoints,designing/Designingendpointscontrollers,adding/Addingthecontrollers
RESTAPIdevelopertoolkit/ThetoolkitoftheRESTAPIdeveloper
RESTAPIsabout/IntroducingRESTAPIsfoundations/ThefoundationsofRESTAPIsbestpractices/BestpracticeswithRESTAPIstesting/TestingyourRESTAPIs
RESTAPIsecurityabout/RESTAPIsecuritybasicaccessauthentication/BasicaccessauthenticationOAuth2.0/OAuth2.0
returnstatement/Thereturnstatementreturntype/TypehintingandreturntypesRFC2068standard
reference/TheHTTPprotocolrouter
about/TherouterURLsmatching,withregularexpressions/URLsmatchingwithregularexpressionsarguments,extractingofURL/ExtractingtheargumentsoftheURLcontroller,executing/Executingthecontroller
Sscenarios
defining/DefiningscenariosGiven-When-Thentestcases,writing/WritingGiven-When-Thentestcasesparts,reusingof/Reusingpartsofscenarios
schemasabout/Schemasandtables,Understandingschemas
senderabout/Asimpleexample
setterabout/Encapsulation
SilexversusLaravel/SilexversusLaravelreference/SilexversusLaravel
Silexmicroframeworkabout/TheSilexmicroframeworkinstallation/Installationprojectsetup/Projectsetupfirstendpoint,adding/Addingthefirstendpointdatabase,accessing/Accessingthedatabase
singletondesignpatternabout/Singleton
spl_autoload_registerfunctionusing/Usingthespl_autoload_registerfunction
standards,PHPPSR-0/AutoloaderwithPSR-4PSR-4/AutoloaderwithPSR-4
staticmethodsabout/Staticpropertiesandmethods
staticpropertiesabout/Staticpropertiesandmethods
statuscodes200/Thestatuscode401/Thestatuscode404/Thestatuscode500/Thestatuscodereference/Statuscodesinresponses
statuscodes,inresponsesabout/Statuscodesinresponses2xx-success/2xx–success3xx-redirection/3xx–redirection4xx-clienterror/4xx–clienterror5xx-servererror/5xx–servererror
stepdefinitionswriting/Writingstepdefinitions
stepsparameterization/Theparameterizationofsteps
stringdatatypesabout/Stringdatatypes
stringsworkingwith/Workingwithstrings
superglobalsabout/Othersuperglobalsreference/Othersuperglobals
switch…caseabout/Switch…case
Symfonyabout/InstallingMinkwithGoutte
Symfony2about/Symfony2
Ttables
about/Schemasandtablesmanaging/Managingtablesjoining/Joiningtables
TDDversusBDD/TDDversusBDD
test-drivendevelopment(TDD)about/Test-drivendevelopmenttheory,versuspractice/Theoryversuspractice
TestCasecustomizing/CustomizingTestCase
testsneedfor/Thenecessityforteststypes/Typesoftestsunittests/Typesoftestsintegrationtests/Typesoftestsacceptancetests/Typesoftestsabout/Yourfirsttestrunning/Runningtests
tests,featuresautomatic/Typesoftestsextensive/Typesoftestsimmediate/Typesoftestsopen/Typesoftestsuseful/Typesoftests
third-partyAPIsusing/Usingthird-partyAPIsapplication’scredentials,obtaining/Gettingtheapplication’scredentialsapplication,settingup/Settinguptheapplicationaccesstoken,requesting/Requestinganaccesstokentweets,fetching/Fetchingtweets
timestamps/Persistingdatawithcookiestoolsinstallation,withComposer
reference/IntegratingPHPUnittraits
about/Traitstransactions
workingwith/Workingwithtransactionstry…catchblock
about/Thetry…catchblockTwig
about/IntroductiontoTwig
Twitterreference/Gettingtheapplication’scredentials
typehinting/Typehintingandreturntypestypejuggling/Datatypes
versuscasting/Gettinginformationfromtheuser
Uuniquekeys
about/Uniquekeysunittests
about/Typesoftests,Unittestsandcodecoveragewriting/Writingunittestsstart/Thestartandendofatestend/Thestartandendofatestversusacceptancetests/Unittestsversusacceptancetests
usermanagement,Laravelframeworkabout/Managingusersuserregistration/Userregistrationuserlogin/Userloginprotectedroutes/Protectedroutes
VVagrant
environment,settingupwith/SettinguptheenvironmentwithVagrantabout/IntroducingVagrantinstalling/InstallingVagrantdownloadpagelink/InstallingVagrantusing/UsingVagrant
variableexpandingabout/Workingwithstrings
variablesabout/Variables
variablescope/Functiondeclarationversioncontrolsystems(VCS)
about/IntroducingcontinuousintegrationVforview
defining/VforviewTwig,defining/IntroductiontoTwigbookview/Thebookviewlayouts/Layoutsandblocksblocks/Layoutsandblockspaginatedbooklist/Paginatedbooklistsalesview/Thesalesviewerrortemplate/Theerrortemplatelogintemplate/Thelogintemplate
viewsabout/TheMVCpattern
visibilityabout/Propertiesandmethodsvisibilityprivate/Propertiesandmethodsvisibilityprotected/Propertiesandmethodsvisibilitypublic/Propertiesandmethodsvisibilityworking/Propertiesandmethodsvisibility
Wwebapplications
about/Webapplicationswebdrivers
types/Typesofwebdriverswebforms
submitting/Amorecomplexexamplewebpage
about/Webapplicationswebservers
about/Webserversworking/Howtheywork
websiteabout/Webapplications
whileloop/While
X2xx-successstatuscodes
200OK/2xx–success201created/2xx–success202accepted/2xx–success
3xx-redirectionstatuscodes301movedpermanently/3xx–redirection303seeother/3xx–redirection
4xx-clienterrorstatuscodes400badrequest/4xx–clienterror401unauthorized/4xx–clienterror403forbidden/4xx–clienterror404notfound/4xx–clienterror405methodnotallowed/4xx–clienterror
5xx-servererror/5xx–servererror