Learning PHP 7

606

Transcript of Learning PHP 7

LearningPHP7

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

LearningPHP7

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.

www.PacktPub.com

eBooks,discountoffers,andmoreDidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<[email protected]>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<[email protected]>,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<[email protected]>withalinktothesuspectedpiratedmaterial.

Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.

QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusat<[email protected]>,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:

Fortherestoftheinstallationprocess,youcanselectallthedefaultoptions.

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','[email protected]');

$customer2=newCustomer(2,'Mary','Poppins','[email protected]');

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','[email protected]');

$customer2=newCustomer(null,'Mary','Poppins','[email protected]');

$customer3=newCustomer(7,'James','Bond','[email protected]');

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','[email protected]');

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','[email protected]');

var_dump(checkIfValid($customer1,[$book1]));//ok

$customer2=newCustomer(7,'James','Bond','[email protected]');

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";

}

Theresultthatyougetfromthebrowserwillalwaysbefromthefirstcatch:

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',

'[email protected]');

CustomerFactory::factory('premium',null,'james','bond',

'[email protected]');

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","[email protected]","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:[email protected]

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

asmuchasyoucanonthedatabasesideinsteadofthePHPside.Ifyousetthecorrectindexes,itwillperformbetter.

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

orderisasfollows:

1. SELECT2. FROM3. WHERE4. GROUPBY5. ORDERBY

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

yourdatabasetomakesurethatthereisanewsalesrowandtherearethreebookslinkedtoit.

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":"[email protected]"

}

],

//...

}

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);

$properties=['sale'=>$sale];

return$this->render('sale.twig',$properties);

}

}

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,

thatis,thedependencyname,whichisnotideal.

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','[email protected]');

$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','[email protected]');

$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','[email protected]'

);

}

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','[email protected]'

);

$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','[email protected]');

$this->assertSame(

$customer,

$expectedBasicCustomer,

'Customerobjectisnotasexpected.'

);

Theresultofthistestisshowninthefollowingscreenshot:

Thetestfailedbecausewhenyoucomparetwoobjectswithidentitycomparison,youcomparingtheobjectreference,anditwillonlybethesameifthetwoobjectsareexactlythesameinstance.Ifyoucreatetwoobjectswiththesameproperties,theywillbeequalbutneveridentical.Tofixthetest,changetheassertionasfollows:

$expectedBasicCustomer=newBasic(1,'han','solo','[email protected]');

$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','[email protected]'

);

}

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','[email protected]'

);

}

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','[email protected]'

);

$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@[email protected];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'=>'[email protected]']);

$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'=>'[email protected]']);

$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);

$twitter=newTwitter($credentials['key'],$credentials['secret']);

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

yourJSONinamore“beautiful”way.LookforJSONViewonanybrowserforareallyhandyone.

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{

return[

'grant_access'=>'client_credentials',

'access_token'=>$this->accessToken

];

}

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

completelistofthingsthatyoucandowithMink.

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

autoloaderabout/Autoloadingclasses

autoloadingabout/Autoloadingclasses

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

JJavaScript

about/HTML,CSS,andJavaScriptjoinqueries

about/Joiningtables

Kkeys

about/Keysandconstraintsprimarykeys/Primarykeysforeignkeys/Foreignkeysuniquekeys/Uniquekeys

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

Qqueries

grouping/Groupingqueries

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

ZZendFramework2

about/ZendFramework2ZIPfile,Nginx

reference/InstallingNginx