expressive cookbook - roguewave.com · engineer at rogue wave software as a core developer of the...

109

Upload: doantruc

Post on 31-Aug-2018

217 views

Category:

Documents


0 download

TRANSCRIPT

1.1

1.2

2.1

2.2

3.1

3.2

3.3

3.4

4.1

4.2

4.3

4.4

4.5

5.1

6.1

TableofContentsIntroduction

Abouttheauthors

PSR-7andDiactorosSpecializedResponseImplementationsinDiactoros

EmittingResponseswithDiactoros

ToolingandConfigurationMigratingtoExpressive2.0

Expressivetooling

NestedMiddlewareinExpressive

ErrorHandlinginExpressive

CookbookUsingConfiguration-DrivenRoutesinExpressive

HandlingOPTIONSandHEADRequestswithExpressive

Cachingmiddleware

Middlewareauthentication

AuthorizeusersusingMiddleware

ExperimentalFeaturesRESTRepresentationsforExpressive

CopyrightCopyrightnote

2

3

ExpressiveCookbookThisbookcontainsacollectionofarticlesonExpressive ,aPSR-7 microframeworkforbuildingmiddlewareapplicationsinPHP.ItcollectsmostofthearticlesonPSR-7andExpressivepublishedin2017byMatthewWeierO'PhinneyandEnricoZimuelontheofficialZendFrameworkblog .

ThegoalofthisbookistoguidePHPdevelopersintheusageofExpressive.Wefeelmiddlewareisanelegantwaytowritewebapplicationsastheapproachallowsyoutowritetargeted,single-purposecodeforinteractingwithanHTTPrequestinordertoproduceanHTTPresponse.Eachmiddlewareshoulddoexactlyonething,anddevelopersshouldmodelcomplexworkflowsbystackingmiddleware.Inthisbook,wedemonstrateanumberofrecipesthatdemonstratethis,andourgoalistohelpyou,thedeveloper,gainmasteryofthemiddlewarepatterns.

WethinkthatPSR-7andmiddlewarerepresentthefutureofwebdevelopmentinPHP,fromsmalltocomplexenterpriseprojects.

Enjoyyourreading,MatthewWeierO'PhinneyandEnricoZimuelRogueWaveSoftware,Inc.

Links

.https://docs.zendframework.com/zend-expressive/↩

.http://www.php-fig.org/psr/psr-7/↩

.https://framework.zend.com/blog↩

1 2

3

1

2

3

Introduction

4

Abouttheauthors

MatthewWeierO'PhinneyisaPrincipalEngineeratRogueWaveSoftware,andprojectleadfortheZendFramework,Apigility,andExpressiveprojects.He’sresponsibleforarchitecture,planning,andcommunityengagementforeachproject,whichareusedbythousandsofdevelopersworldwide,andshippedinprojectsfrompersonalwebsitestomultinationalmediaconglomerates,andeverythinginbetween.Whennotinfrontofacomputer,you'llfindhimwithhisfamilyanddogsontheplainsofSouthDakota.

Formoreinformation:

https://mwop.net/https://www.roguewave.com/

EnricoZimuelhasbeenasoftwaredevelopersince1996.HeworksasaSeniorSoftwareEngineeratRogueWaveSoftwareasacoredeveloperoftheZendFramework,Apigility,andExpressiveprojects.HeisaformerResearcherProgrammerfortheInformaticsInstitute

Abouttheauthors

5

oftheUniversityofAmsterdam.Enricospeaksregularlyatconferencesandevents,includingTEDxandinternationalPHPconferences.Heisalsotheco-founderofthePHPUserGroupofTorino(Italy).

Formoreinformation:

https://www.zimuel.it/https://www.roguewave.com/TEDxpresentation:https://www.youtube.com/watch?v=SienrLY40-wPHPUserGroupofTorino:http://torino.grusp.org/

Abouttheauthors

6

SpecializedResponseImplementationsinDiactorosByMatthewWeierO'Phinney

WhenwritingPSR-7 middleware,atsomepointyou'llneedtoreturnaresponse.

Maybeyou'llbereturninganemptyresponse,indicatingsomethingalongthelinesofsuccessfuldeletionofaresource.MaybeyouneedtoreturnsomeHTML,orJSON,orjustplaintext.Maybeyouneedtoindicatearedirect.

Buthere'stheproblem:agenericresponsetypicallyhasaverygenericconstructor.Take,forexample,Zend\Diactoros\Response:

publicfunction__construct(

$body='php://memory',

$status=200,

array$headers=[]

)

$bodyinthissignatureallowseitheraPsr\Http\Message\StreamInterfaceinstance,aPHPresource,orastringidentifyingaPHPstream.Thismeansthatit'snotterriblyeasytocreateevenasimpleHTMLresponse!

Tobefair,therearegoodreasonsforagenericconstructor:itallowssettingtheinitialstateinsuchawaythatyou'llhaveafullypopulatedinstanceimmediately.However,themeansfordoingso,inordertobegeneric,leadstoconvolutedcodeformostconsumers.

Fortunately,Diactorosprovidesanumberofconvenienceimplementationstohelpsimplifythemostcommonusecases.

EmptyResponseThestandardresponsefromanAPIforasuccessfuldeletionisgenerallya204NoContent.Sitesemittingwebhookpayloadsoftenexpecta202Acceptedwithnocontent.ManyAPIsthatallowcreationofresourceswillreturna201Created;thesemayormaynothavecontent,dependingonimplementation,withsomebeingempty,butreturningaLocationheaderwiththeURIofthenewlycreatedresource.

1

SpecializedResponseImplementationsinDiactoros

7

Clearly,insuchcases,ifyoudon'tneedcontent,whywouldyoubebotheredtocreateastream?Toanswerthis,wehaveZend\Diactoros\Response\EmptyResponse,withthefollowingconstructor:

publicfunction__construct($status=204,array$headers=[])

So,aDELETEendpointmightreturnthisonsuccess:

returnnewEmptyResponse();

Awebhookendpointmightdothis:

returnnewEmptyResponse(StatusCodeInterface::STATUS_ACCEPTED);

AnAPIthatjustcreatedaresourcemightdothefollowing:

returnnewEmptyResponse(

StatusCodeInterface::STATUS_CREATED,

['Location'=>$resourceUri]

);

RedirectResponseRedirectsarecommonwithinwebapplications.Wemaywanttoredirectausertoaloginpageiftheyarenotcurrentlyloggedin;wemayhavechangedwheresomeofourcontentislocated,andredirectusersrequestingtheoldURIs;etc.

Zend\Diactoros\Response\RedirectResponseprovidesasimplewaytocreateandreturnaresponseindicatinganHTTPredirect.Thesignatureis:

publicfunction__construct($uri,$status=302,array$headers=[])

where$urimaybeeitherastringURI,oraPsr\Http\Message\UriInterfaceinstance.ThisvaluewillthenbeusedtoseedaLocationHTTPheader.

returnnewRedirectResponse('/login');

You'llnotethatthe$statusdefaultsto302.Ifyouwanttosetapermanentredirect,pass301forthatargument:

SpecializedResponseImplementationsinDiactoros

8

returnnewRedirectResponse('/archives',301);

//or,usingfig/http-message-util:

returnnewRedirectResponse('/archives',StatusCodeInterface::STATUS_PERMANENT_REDIREC

T);

Sometimesyoumaywanttosetanheaderaswell;dothatbypassingthethirdargument,anarrayofheaderstoprovide:

returnnewRedirectResponse(

'/login',

StatusCodeInterface::STATUS_TEMPORARY_REDIRECT,

['X-ORIGINAL_URI'=>$uri->getPath()]

);

TextResponseSometimesyoujustwanttoreturnsometext,whetherit'splaintext,XML,YAML,etc.Whendoingthat,takingtheextrasteptocreateastreamfeelslikeoverhead:

$stream=newStream('php://temp','wb+');

$stream->write($content);

Tosimplifythis,weofferZend\Diactoros\Response\TextResponse,withthefollowingsignature:

publicfunction__construct($text,$status=200,array$headers=[])

Bydefault,itwilluseaContent-Typeoftext/plain,whichmeansyou'lloftenneedtosupplyaContent-Typeheaderwiththisresponse.

Let'sreturnsomeplaintext:

returnnewTextResponse('Hello,world!');

Now,let'stryreturningaProblemDetailsXMLresponse:

returnnewTextResponse(

$xmlPayload,

StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY,

['Content-Type'=>'application/problem+xml']

);

SpecializedResponseImplementationsinDiactoros

9

Ifyouhavesometextualcontent,thisistheresponseforyou.

HtmlResponseThemostcommonresponsefromwebapplicationsisHTML.Ifyou'rereturningHTML,eventheTextResponsemayseemabitmuch,asyou'reforcedtoprovidetheContent-Typeheader.Toanswerthat,weprovideZend\Diactoros\Response\HtmlResponse,whichisexactlythesameasTextResponse,butwithadefaultContent-Typeheaderspecifyingtext/html;charset=utf-8instead.

Asanexample:

returnnewHtmlResponse($renderer->render($template,$view));

JsonResponseForwebAPIs,JSONisgenerallythelinguafranca.WithinPHP,thisgenerallymeanspassinganarrayorobjecttojson_encode(),andsupplyingaContent-Typeheaderofapplication/jsonorapplication/{type}+json,where{type}isamorespecificmediatype.

LiketextandHTML,youlikelydon'twanttodothismanuallyeverytime:

$json=json_encode(

$data,

JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_UNESCAPED_SLASHES

);

$stream=newStream('php://temp','wb+');

$stream->write($json);

$response=newResponse(

$stream,

StatusCodeInterface::STATUS_OK,

['Content-Type'=>'application/json']

);

Tosimplifythis,weprovideZend\Diactoros\Response\JsonResponse,withthefollowingconstructorsignature:

SpecializedResponseImplementationsinDiactoros

10

publicfunction__construct(

$data,

$status=200,

array$headers=[],

$encodingOptions=self::DEFAULT_JSON_FLAGS

){

where$encodingOptionsdefaultstotheflagsspecifiedinthepreviousexample.

Thismeansourmostcommonusecasenowbecomesthis:

returnnewJsonResponse($data);

WhatifwewanttoreturnaJSON-formattedProblemDetailsresponse?

returnnewJsonResponse(

$details,

StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY,

['Content-Type'=>'application/problem+json']

);

Onecommonworkflowwe'veseenwithJSONresponsesisthatdevelopersoftenwanttomanipulatethemonthewayoutthroughmiddleware.Asanexample,theymaywanttoaddadditional_linkselementstoHALresponses,oraddcountsforcollections.

Startinginversion1.5.0,weprovideafewextramethodsonthisparticularresponsetype:

publicfunctiongetPayload():mixed;

publicfunctiongetEncodingOptions():int;

publicfunctionwithPayload(mixed$data):JsonResponse;

publicfunctionwithEncodingOptions(int$options):JsonResponse;

Essentially,whathappensiswenowstorenotonlytheencoded$datainternally,buttherawdata;thisallowsyoutopullit,manipulateit,andthencreateanewinstancewiththeupdateddata.Additionally,weallowspecifyingadifferentsetofencodingoptionslater;thiscanbeuseful,forinstance,foraddingtheJSON_PRETTY_PRINTflagwhenindevelopment.Whentheoptionsarechanged,thenewinstancewillalsore-encodetheexistingdata.

First,let'slookatalteringthepayloadonthewayout.zend-expressive-halinjects_total_items,_page,and_page_countproperties,andyoumaywanttoremovetheunderscoreprefixforeachofthese:

SpecializedResponseImplementationsinDiactoros

11

function(ServerRequestInterface$request,DelegateInterface$delegate):ResponseInte

rface

{

$response=$delegate->process($request);

if(!$responseinstanceofJsonResponse){

return$response;

}

$payload=$response->getPayload();

if(!isset($payload['_total_items'])){

return$response;

}

$payload['total_items']=$payload['_total_items'];

unset($payload['_total_items']);

if(isset($payload['_page'])){

$payload['page']=$payload['_page'];

$payload['page_count']=$payload['_page_count'];

unset($payload['_page'],$payload['_page_count']);

}

return$response->withPayload($payload);

}

Now,let'swritemiddlewarethatsetstheJSON_PRETTY_PRINToptionwhenindevelopmentmode:

function(

ServerRequestInterface$request,

DelegateInterface$delegate

):ResponseInterfaceuse($isDevelopmentMode){

$response=$delegate->process($request);

if(!$isDevelopmentMode||!$responseinstanceofJsonResponse){

return$response;

}

$options=$response->getEncodingOptions();

return$response->withEncodingOptions($options|JSON_PRETTY_PRINT);

}

ThesefeaturescanbereallypowerfulwhenshapingyourAPI!

Summary

SpecializedResponseImplementationsinDiactoros

12

ThegoalofPSR-7istoprovidetheabilitytostandardizeoninterfacesforyourHTTPinteractions.However,atsomepointyouneedtochooseanactualimplementation,andyourchoicewilloftenbeshapedbythefeaturesoffered,particularlyiftheyprovideconvenienceinyourdevelopmentprocess.Ourgoalwiththesevariouscustomresponseimplementationsistoprovideconveniencetodevelopers,allowingthemtofocusonwhattheyneedtoreturn,nothowtoreturnit.

YoucancheckoutmoreintheDiactorosdocumentation .

Footnotes

.http://www.php-fig.org/psr/psr-7/↩

.https://docs.zendframework.com/zend-diactoros↩

2

1

2

SpecializedResponseImplementationsinDiactoros

13

EmittingResponseswithDiactorosByMatthewWeierO'Phinney

Whenwritingmiddleware-basedapplications,atsomepointyouwillneedtoemityourresponse.

PSR-7 definesthevariousinterfacesrelatedtoHTTPmessages,butdoesnotdefinehowtheywillbeused.Diactoros definesseveralutilityclassesforthesepurposes,includingaServerRequestFactoryforgeneratingaServerRequestinstancefromthePHPSAPIinuse,andasetofemitters,foremittingresponsesbacktotheclient.Inthispost,we'lldetailthepurposeofemitters,theemittersshippedwithDiactoros,andsomestrategiesforemittingcontenttoyourusers.

Whatisanemitter?InvanillaPHPapplications,youmightcalloneormoreofthefollowingfunctionsinordertoprovidearesponsetoyourclient:

http_response_code()foremittingtheHTTPresponsecodetouse;thismustbecalledbeforeanyoutputisemitted.header()foremittingresponseheaders.Likehttp_response_code(),thismustbecalledbeforeanyoutputisemitted.Itmaybecalledmultipletimes,inordertosetmultipleheaders.echo(),printf(),var_dump(),andvar_export()willeachemitoutputtothecurrentoutputbuffer,or,ifnoneispresent,directlytotheclient.

OneaspectPSR-7aimstoresolveistheabilitytogeneratearesponsepiece-meal,includingaddingcontentandheadersinwhateverorderyourapplicationrequires.Toaccomplishthis,itprovidesaResponseInterfacewithwhichyourapplicationinteracts,andwhichaggregatestheresponsestatuscode,itsheaders,andallcontent.

Onceyouhaveacompleteresponse,however,youneedtoemitit.

Diactorosprovidesemitterstosolvethisproblem.EmittersallimplementZend\Diactoros\Response\EmitterInterface:

12

EmittingResponseswithDiactoros

14

namespaceZend\Diactoros\Response;

usePsr\Http\Message\ResponseInterface;

interfaceEmitterInterface

{

/**

*Emitaresponse.

*

*Emitsaresponse,includingstatusline,headers,andthemessagebody,

*accordingtotheenvironment.

*

*Implementationsofthismethodmaybewritteninsuchawayastohave

*sideeffects,suchasusageofheader()orpushingoutputtothe

*outputbuffer.

*

*ImplementationsMAYraiseexceptionsiftheyareunabletoemitthe

*response;e.g.,ifheadershavealreadybeensent.

*

*@paramResponseInterface$response

*/

publicfunctionemit(ResponseInterface$response);

}

Diactorosprovidestwoemitterimplementations,bothgearedtowardsstandardPHPSAPIimplementations:

Zend\Diactoros\Emitter\SapiEmitter

Zend\Diactoros\Emitter\SapiStreamEmitter

Internally,theyoperateverysimilarly:theyemittheresponsestatuscode,allheaders,andtheresponsebodycontent.Priortodoingso,however,theycheckforthefollowingconditions:

Headershavenotyetbeensent.Ifanyoutputbuffersexist,nocontentispresent.

Ifeitheroftheseconditionsisnottrue,theemittersraiseanexception.Thisisdonetoensurethatconsistentcontentcanbeemitted;mixingPSR-7andglobaloutputleadstounexpectedandinconsistentresults.Ifyouareusingmiddleware,usethingsliketheerrorlog,loggers,etc.ifyouwanttodebug,insteadofmixingstrategies.

Emittingfiles

EmittingResponseswithDiactoros

15

Asnotedabove,oneofthetwoemittersistheSapiStreamEmitter.ThenormalSapiEmitteremitstheresponsebodyatonceviaasingleechostatement.ThisworksformostgeneralmarkupandJSONpayloads,butwhenreturningfiles(forexample,whenprovidingfiledownloadsviayourapplication),thisstrategycanquicklyexhausttheamountofmemoryPHPisallowedtoconsume.

TheSapiStreamEmitterisdesignedtoanswertheproblemoffiledownloads.Itemitsachunkatatime(8192bytesbydefault).Whilethiscanmeanabitmoreperformanceoverheadwhenemittingalargefile,asyou'llhavemoremethodcalls,italsoleadstoreducedmemoryoverhead,aslesscontentisinmemoryatanygiventime.

TheSapiStreamEmitterhasanotherimportantfeature,however:itallowssendingcontentranges.

Clientscanopt-intoreceivingsmallchunksofafileatatime.Whilethismeansmorenetworkcalls,itcanalsohelppreventcorruptionoflargefilesbyallowingtheclienttore-tryfailedrequestsinordertostitchtogetherthefullfile.Doingsoalsoallowsprovidingprogressstatus,orevenbufferingstreamingcontent.

Whenrequestingcontentranges,theclientwillpassaRangeheader:

Range:bytes=1024-2047

Itisuptotheserverthentodetectsuchaheaderandreturntherequestedrange.ServersindicatethattheyaredoingsobyrespondingwithaContent-Rangeheaderwiththerangeofbytesbeingreturnedandthetotalnumberofbytespossible;theresponsebodythenonlycontainsthosebytes.

Content-Range:bytes=1024-2047/11576

Asanexample,middlewarethatallowsreturningacontentrangemightlooklikethefollowing:

EmittingResponseswithDiactoros

16

function(ServerRequestInterface$request,DelegateInterface$delegate):ResponseInte

rface

{

$stream=newStream('path/to/download/file','r');

$response=newResponse($stream);

$range=$request->getHeaderLine('range');

if(empty($range)){

return$response;

}

$size=$body->getSize();

$range=str_replace('=','',$range);

$range.='/'.$size;

return$response->withHeader('Content-Range',$range);

}

You'lllikelywanttovalidatethattherangeiswithinthesizeofthefile,too!

TheabovecodeemitsaContent-RangeresponseheaderifaRangeheaderisintherequest.However,howdoweensureonlythatrangeofbytesisemitted?

ByusingtheSapiStreamEmitter!ThisemitterwilldetecttheContent-Rangeheaderanduseittoreadandemitonlythebytesspecifiedbythatheader;noextraworkisnecessary!

MixingandmatchingemittersTheSapiEmitterisperfectforcontentgeneratedwithinyourapplication—HTML,JSON,XML,etc.—assuchcontentisusuallyofreasonablelength,andwillnotexceednormalmemoryandresourcelimits.

TheSapiStreamEmitterisidealforreturningfiledownloads,butcanleadtoperformanceoverheadwhenemittingstandardapplicationcontent.

Howcanyoumixandmatchthetwo?

ExpressiveanswersthisquestionbyprovidingZend\Expressive\Emitter\EmitterStack.Theclassactsasastack(lastin,firstout),executingeachemittercomposeduntiloneindicatesithashandledtheresponse.

ThisclasscapitalizesonthefactthatthereturnvalueofEmitterInterfaceisundefined.Emittersthatreturnabooleanfalseindicatetheywereunabletohandletheresponse,allowingtheEmitterStacktomovetothenextemitterinthestack.Thefirstemittertoreturnanon-falsevaluehaltsexecution.

EmittingResponseswithDiactoros

17

Boththeemittersdefinedinzend-diactorosreturnnullbydefault.So,ifwewanttocreateastackthatfirsttriesSapiStreamEmitter,andthendefaultstoSapiEmitter,wecoulddothefollowing:

usePsr\Http\Message\ResponseInterface;

useZend\Diactoros\Response\EmitterInterface;

useZend\Diactoros\Response\SapiEmitter;

useZend\Diactoros\Response\SapiStreamEmitter;

useZend\Expressive\Emitter\EmitterStack;

$emitterStack=newEmitterStack();

$emitterStack->push(newSapiEmitter());

$emitterStack->push(newclassimplementsEmitterInterface{

publicfunctionemit(ResponseInterface$response)

{

$contentSize=$response->getBody()->getSize();

if(''===$response->getHeaderLine('content-range')

&&$contentSize<8192

){

returnfalse;

}

$emitter=newSapiStreamEmitter();

return$emitter->emit($response);

}

});

Theabovewillexecuteouranonymousclassasthefirstemitter.IftheresponsehasaContent-Rangeheader,orifthesizeofthecontentisgreaterthan8k,itwillusetheSapiStreamEmitter;otherwise,itreturnsfalse,allowingthenextemitterinthestack,SapiEmitter,toexecute.Sincethatemitteralwaysreturnsnull,itactsasadefaultemitterimplementation.

InExpressive,ifyouweretowraptheaboveinafactorythatreturnsthe$emitterStack,andassignthatfactorytotheZend\Diactoros\Emitter\EmitterInterfaceservice,thentheabovestackwillbeusedbyZend\Expressive\Applicationforthepurposeofemittingtheapplicationresponse!

SummaryEmittersprovideyoutheabilitytoreturntheresponseyouhaveaggregatedinyourapplicationtotheclient.Theyareintendedtohaveside-effects:sendingtheresponsecode,responseheaders,andbodycontent.Differentemitterscanusedifferentstrategieswhen

EmittingResponseswithDiactoros

18

emittingresponses,fromsimplyechoingcontent,toiteratingthroughchunksofcontent(astheSapiStreamEmitterdoes).UsingExpressive'sEmitterStackcanprovideyouwithawaytoselectdifferentemittersforspecificresponsecriteria.

Formoreinformation:

ReadtheDiactorosemitterdocumentation:https://docs.zendframework.com/zend-diactoros/emitting-responses/ReadtheExpressiveemitterdocumentation:https://docs.zendframework.com/zend-expressive/features/emitters/

Footnotes

.http://www.php-fig.org/psr/psr-7/↩

.https://docs.zendframework.org/zend-diactoros/↩

1

2

EmittingResponseswithDiactoros

19

MigratingtoExpressive2.0byMatthewWeierO'Phinney

ZendExpressive2wasreleasedinMarch2017 .Anewmajorversionimpliesbreakingchanges,whichoftenposesaproblemwhenmigrating.Thatsaid,wedidalotofworkbehindthescenestotryandensurethatmigrationscanhappenwithouttoomucheffort,includingprovidingmigrationtoolstoeasethetransition.

Inthistutorial,wewilldetailmigratinganexistingExpressiveapplicationfromversion1toversion2.

Howwetestedthis

WeusedAdamCulp'sexpressive-blastoff repositoryasatest-bedforthistutorial,andyoucanfollowalongfromthereifyouwant,bycheckingoutthe1.0tagofthatrepository:

$gitclonehttps://github.com/adamculp/expressive-blastoff

$cdexpressive-blastoff

$gitcheckout1.0

$composerinstall

Wehavealsosuccessfullymigratedanumberofotherapplications,includingtheZendFrameworkwebsiteitself,usingessentiallythesameapproach.Asisthecasewithanysuchtutorial,yourownexperiencemayvary.

UpdatingdependenciesFirst,createanewfeaturebranchforthemigration,toensureyoudonotclobberworkingcode.Ifyouareusinggit,thismightlooklikethis:

$gitcheckout-bfeature/expressive-2

Ifyouhavenotyetinstalleddependencies,werecommenddoingso:

$composerinstall

1

2

MigratingtoExpressive2.0

20

Now,we'llupdatedependenciestogetExpressive2.Doingsoonanexistingprojectrequiresanumberofotherupdatesaswell:

Youwillneedtoupdatewhicheverrouterimplementationyouuse,aswehavereleasednewmajorversionsofallrouters,totakeadvantageofanewmajorversionofthezend-expressive-routerRouterInterface.Youcanpintheseto ̂ 2.0.

Youwillneedtoupdatethezend-expressive-helperspackage,asitnowalsodependsonthenewRouterInterfacechanges.Youcanpinthisto ̂ 3.0.

Youwillneedtoupdateyourtemplaterenderer,ifyouhaveoneinstalled.Thesereceivedminorversionbumpsinordertoaddcompatibilitywiththenewzend-expressive-helpersrelease;however,sincewe'llbeissuingarequirestatementtoupgradeExpressive,weneedtospecifythenewtemplaterendererversionaswell.Constraintsforthesupportedrenderersare:

zendframework/zend-expressive-platesrenderer:^1.2

zendframework/zend-expressive-twigrenderer:^1.3

zendframework/zend-expressive-zendviewrenderer:^1.3

Asanexample,ifyouareusingtherecommendedpackageszendframework/zend-expressive-fastrouteandzendframework/zend-expressive-platesrenderer,youwillupdatetoExpressive2.0usingthefollowingstatement:

$composerupdate--with-dependencies"zendframework/zend-expressive:^2.0"\

>"zendframework/zend-expressive-fastroute:^2.0"\

>"zendframework/zend-expressive-helpers:^3.0"\

>"zendframework/zend-expressive-platesrenderer:^1.2"

Atthispoint,tryoutyoursite.Inmanycases,itshouldcontinueto"justwork."

Commonerrors

Wesayshouldforareason.Thereareanumberoffeaturesthatwillnotwork,butwerenotcommonlyusedbyend-users,includingaccessingpropertiesontherequest/responsedecoratorsthatStratigility1shipped(onwhichExpressive1wasbased),andusageofStratigility1"errormiddleware"(whichwasremovedintheversion2releases).Whiletheseweredocumented,manyuserswerenotawareofthefeaturesand/ordidnotusethem.Ifyoudid,however,youwillnoticeyoursitewillnotrunfollowingtheupgrade.Don'tworry;wecovertoolsthatwillsolvetheseissuesinthenextsection!

MigratingtoExpressive2.0

21

MigrationAtthispoint,there'safewmorestepsyoushouldtaketofullymigrateyourapplication;insomecases,yourapplicationiscurrentlybroken,andwillrequirethesechangestoworkinthefirstplace!

WeprovideCLItoolingthatassistsinthesemigrationsviathepackagezendframework/zend-expressive-tooling.Addthisasadevelopmentrequirementtoyourapplicationnow:

$composerrequire--dev--update-with-dependencieszendframework/zend-expressive-tool

ing

(The--update-with-dependenciesmaybenecessarytopickupnewerversionsofzend-stdlibandzend-code,amongothers.)

Expressive1wasbasedonStratigility1,whichdecoratedtherequestandresponseobjectswithwrappersthatprovideaccesstotheoriginalincomingrequest,URI,andresponse.WithStratigility2andExpressive2,thesedecoratorshavebeenremoved;howeveraccesstotheseartifactsisavailableviarequestattributes.Assuch,weprovideatooltoscanforusageoftheseandfixthemwhenpossible.Let'sinvokeitnow:

$./vendor/bin/expressive-migrate-original-messagesscan

(Ifyourcodeisinadirectoryotherthansrc/,thenusethe--helpswitchforoptionsonspecifyingthatdirectory.)

Mostlikelythetoolwon'tfindanything.Insomecases,itwillfindsomething,andtrytocorrectit.TheonethingitcannotcorrectarecallstogetOriginalResponse();insuchcases,thetooldetailshowtocorrectthoseproblems,andinwhatfilestheyoccur.

Next,we'llscanforlegacyerrormiddleware.ThiswasmiddlewaredefinedinStratigilitywithanalternatesignature:

function(

$error,

ServerRequestInterface$request,

ResponseInterface$response,

callable$next

):ResponseInterface

Suchmiddlewarewasinvokedbycalling$nextwithathirdargument:

MigratingtoExpressive2.0

22

$response=$next($request,$response,$error);

ThisstyleofmiddlewarehasbeenremovedfromStratigility2andExpressive2,andwillnotworkatall.Weprovideanothertoolforfindingbotherrormiddleware,aswellasinvocationsoferrormiddleware:

$./vendor/bin/expressive-scan-for-error-middlewarescan

(Ifyourcodeisinadirectoryotherthansrc/,thenusethe--helpswitchforoptionsonspecifyingthatdirectory.)

Thistooldoesnotchangeanycode,butitwilltellyoufilesthatcontainproblems,andgiveyouinformationonhowtocorrecttheissues.

Finally,we'llmigratetoaprogrammaticpipeline.InExpressive1,theskeletondefinedthepipelineandroutesviaconfiguration.ManyusershaveindicatedthatusingtheExpressiveAPItendstobeeasiertolearnandunderstandthantheconfiguration;additionally,IDEsandstaticanalyzersarebetterabletodetermineifprogrammaticpipelinesandroutingarecorrectthanconfiguration-drivenones.

Aswiththeothermigrationtasks,weprovideatoolforthis:

$./vendor/bin/expressive-pipeline-from-configgenerate

Thistoolloadsyourexistingconfiguration,andthendoesthefollowing:

Createsconfig/autoload/programmatic-pipeline.global.php,whichcontainsdirectivestotellExpressivetoignoreconfiguredpipelinesandrouting,anddefinesdependenciesfornewerrorhandlingandpipelinemiddleware.Createsconfig/pipeline.phpwithyourapplicationmiddlewarepipeline.Createsconfig/routes.phpwithyourapplicationroutingdefinitions.Updatespublic/index.phptoincludetheabovetwofilespriortocalling$app->run().

Thetoolwillalsotellyouifitencounterslegacyerrormiddlewareinyourconfiguration;ifitdoes,itskipsaddingdirectivestocomposeitintheapplicationpipeline,butnotifiesyouitisdoingso.Beawareofthat,ifyoudependedonthefeaturepreviously;inmostcases,ifyou'vebeenfollowingthistutorialstep-by-step,you'vealreadyeliminatedthem.

Atthispoint,tryoutyourapplicationagain!Ifallwentwell,thisshould"justwork."

Bonussteps!

MigratingtoExpressive2.0

23

Whiletheabovewillgetyourapplicationmigrated,V2oftheskeletonapplicationoffersthreeadditionalfeaturesthatwerenotpresentintheoriginalv1releases:

self-invokingfunctioninpublic/index.phpinordertopreventglobalvariabledeclarations.abilitytodefineand/orusemiddlewaremodules,viazend-config-aggregator.developmentmode.

Self-invokingfunction

Thepointofthischangeistopreventadditionofvariablesintothe$GLOBALscope.Thisisdonebycreatingaself-invokingfunctionaroundthedirectivesinpublic/index.phpthatcreateandusevariables.

Aftercompletingtheearliersteps,youshouldhavelineslikethefollowinginyourpublic/index.php:

/**@var\Interop\Container\ContainerInterface$container*/

$container=require'config/container.php';

/**@var\Zend\Expressive\Application$app*/

$app=$container->get(\Zend\Expressive\Application::class);

require'config/pipeline.php';

require'config/routes.php';

$app->run();

We'llcreateaself-invokingfunctionaroundthem.IfyouareusingPHP7+,thislookslikethefollowing:

(function(){

/**@var\Interop\Container\ContainerInterface$container*/

$container=require'config/container.php';

/**@var\Zend\Expressive\Application$app*/

$app=$container->get(\Zend\Expressive\Application::class);

require'config/pipeline.php';

require'config/routes.php';

$app->run();

})();

Ifyou'restillusingPHP5.6,youneedtousecall_user_func():

MigratingtoExpressive2.0

24

call_user_func(function(){

/**@var\Interop\Container\ContainerInterface$container*/

$container=require'config/container.php';

/**@var\Zend\Expressive\Application$app*/

$app=$container->get(\Zend\Expressive\Application::class);

require'config/pipeline.php';

require'config/routes.php';

$app->run();

});

zend-config-aggregator

zendframework/zend-config-aggregatorisattheheartofthemodularmiddlewaresystem .Itworksasfollows:

ModulesarejustlibrariesorpackagesthatdefineaConfigProviderclass.Theseclassesarestatelessanddefinean__invoke()methodthatreturnsanarrayofconfiguration.Theconfig/config.phpfilethenusesZend\ConfigAggregator\ConfigAggregatorto,well,aggregateconfigurationfromavarietyofsources,includingConfigProviderclasses,aswellasotherspecializedproviders(e.g.,PHPfileproviderforaggregatingPHPconfigurationfiles,arrayproviderforsupplyinghard-codedarrayconfiguration,etc.).Thispackageprovidesbuilt-insupportforconfigurationcachingaswell.

WealsoprovideaComposerplugin,zend-component-installer,thatworkswithconfigurationfilesthatutilizetheConfigAggregator.Itexecutesduringinstalloperations,andchecksthepackagebeinginstalledforconfigurationindicatingitprovidesaConfigProvider;ifso,itwillthenpromptyou,askingifyouwanttoaddittoyourconfiguration.Thisisagreatwaytoautomateadditionofdependenciesandmodule-specificconfigurationtoyourapplication!

Togetstarted,let'saddzend-config-aggregatortoourapplication:

$composerrequirezendframework/zend-config-aggregator

We'llalsoaddthezend-component-installer,butasadevelopmentrequirementonly:

$composerrequire--devzendframework/zend-component-installer

(Note:itwilllikelyalreadyhavebeeninstalledwithzend-expressive-tooling;requiringitlikethis,however,ensuresitstayspresentifyoudecidetoremovethatpackagelater.)

Toupdateyourapplication,youwillneedtoupdateyourconfig/config.phpfile.

3

MigratingtoExpressive2.0

25

Ifyou'vemadenomodificationstotheshippedversion,itwilllooklikethefollowing:

<?php

useZend\Stdlib\ArrayUtils;

useZend\Stdlib\Glob;

/**

*Configurationfilesareloadedinaspecificorder.First``global.php``,then``*.

global.php``.

*then``local.php``andfinally``*.local.php``.Thiswaylocalsettingsoverwriteg

lobalsettings.

*

*Theconfigurationcanbecached.Thiscanbedonebysetting``config_cache_enabled

``to``true``.

*

*Obviously,ifyouuseclosuresinyourconfigyoucan'tcacheit.

*/

$cachedConfigFile='data/cache/app_config.php';

$config=[];

if(is_file($cachedConfigFile)){

//Trytoloadthecachedconfig

$config=include$cachedConfigFile;

}else{

//Loadconfigurationfromautoloadpath

foreach(Glob::glob('config/autoload/{{,*.}global,{,*.}local}.php',Glob::GLOB_BRA

CE)as$file){

$config=ArrayUtils::merge($config,include$file);

}

//Cacheconfigifenabled

if(isset($config['config_cache_enabled'])&&$config['config_cache_enabled']===

true){

file_put_contents($cachedConfigFile,'<?phpreturn'.var_export($config,true

).';');

}

}

//ReturnanArrayObjectsowecaninjecttheconfigasaserviceinAura.Di

//andstillusearraycheckslike``is_array``.

returnnewArrayObject($config,ArrayObject::ARRAY_AS_PROPS);

Youcanreplaceitdirectlywiththis,then:

MigratingtoExpressive2.0

26

<?php

useZend\ConfigAggregator\ArrayProvider;

useZend\ConfigAggregator\ConfigAggregator;

useZend\ConfigAggregator\PhpFileProvider;

$cacheConfig=[

'config_cache_path'=>'data/config-cache.php',

];

$aggregator=newConfigAggregator([

newArrayProvider($cacheConfig),

newPhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),

],$cacheConfig['config_cache_path']);

return$aggregator->getMergedConfig();

Ifyouwant,youcansettheconfig_cache_pathtomatchtheonefromyourpreviousversion;thisshouldonlybenecessaryifyouhavetoolingalreadyinplaceforcacheclearing,however.

ZFcomponents

AnyZendFrameworkcomponentthatprovidesserviceconfigurationexposesaConfigProvider.Thismeansthatifyouaddthesetoyourapplicationaftermakingtheabovechanges,theywillexposetheirservicestoyourapplicationimmediatelyfollowinginstallation!

Ifyou'veinstalledZFcomponentspriortothischange,checktoseewhichonesexposeConfigProviderclasses(youcanlookforaConfigProviderundertheirnamespace,orlookforanextra.zf.config-providerdeclarationintheircomposer.json).Ifyoufindany,addthemtoyourconfig/config.phpfile;usingthefullyqualifiedclassnameoftheprovider.Asanexample:\Zend\Db\ConfigProvider::class.

Developmentmode

Wehavebeenusingzf-development-modewithzend-mvcandApigilityapplicationsforafewyearsnow,andfeelitoffersanelegantsolutionforshippingstandarddevelopmentconfigurationforusewithyourteam,aswellastogglingbackandforthbetweendevelopmentandproductionconfiguration.(Thatsaid,config/autoload/*.local.phpfilesmayclearlyvaryinyourdevelopmentenvironmentversusyourproductionenvironment,sothisisnotentirelyfool-proof!)

Let'saddittoourapplication:

MigratingtoExpressive2.0

27

$composerrequire--devzfcampus/zf-development-mode

Notethatwe'readdingitasadevelopmentrequirement;chancesare,youdonotwanttoaccidentallyenableitinproduction!

Next,weneedtoaddacouplefilestoourtree.Thefirstwe'lladdisconfig/development.config.php.dist,withthefollowingcontents:

<?php

/**

*Filerequiredtoallowenablementofdevelopmentmode.

*

*Forusewiththezf-development-modetool.

*

*Usage:

*$composerdevelopment-disable

*$composerdevelopment-enable

*$composerdevelopment-status

*

*DONOTMODIFYTHISFILE.

*

*Provideyourowndevelopment-modesettingsbyeditingthefile

*`config/autoload/development.local.php.dist`.

*

*Becausethisfileisaggregatedlast,itsimplyensures:

*

*-The`debug`flagis_enabled_.

*-Configurationcachingis_disabled_.

*/

useZend\ConfigAggregator\ConfigAggregator;

return[

'debug'=>true,

ConfigAggregator::ENABLE_CACHE=>false,

];

Next,we'lladdaconfig/autoload/development.local.php.dist.Thecontentsofthisonewillvarybasedonwhatyouareusinginyourapplication.

IfyouarenotusingWhoopsforerrorreporting,startwiththis:

<?php

return[

];

MigratingtoExpressive2.0

28

Ifyouare,thisisachancetoconfigurethatcorrectlyforyournewlyupdatedapplication.Createthefilewiththesecontents:

<?php

useWhoops\Handler\PrettyPageHandler;

useZend\Expressive\Container;

useZend\Expressive\Middleware\ErrorResponseGenerator;

useZend\Expressive\Whoops;

useZend\Expressive\WhoopsPageHandler;

return[

'dependencies'=>[

'invokables'=>[

WhoopsPageHandler::class=>PrettyPageHandler::class,

],

'factories'=>[

ErrorResponseGenerator::class=>Container\WhoopsErrorResponseGeneratorFac

tory::class,

Whoops::class=>Container\WhoopsFactory::class,

],

],

'whoops'=>[

'json_exceptions'=>[

'display'=>true,

'show_trace'=>true,

'ajax_only'=>true,

],

],

];

Next,ifyoustartedwiththeV1skeletonapplication,youwilllikelyhaveafilenamedconfig/autoload/errorhandler.local.php,anditwillhavesimilarcontents,forthepurposeofseedingthelegacy"finalhandler"system.Youcannowremovethatfile.

Afterthat'sdone,weneedtoaddsomedirectivessothatgitwillignorethenon-distfiles.Editthe.gitignorefileinyourproject'srootdirectorytoaddthefollowingentry:

config/development.config.php

Theconfig/autoload/.gitignorefileshouldalreadyhavearulethatomits*.local.php.

Nowweneedtohaveourconfigurationloadthedevelopmentconfigurationifit'spresent.Thefollowingassumesyoualreadyconvertedyourapplicationtousezend-config-aggregator.AddthefollowinglineasthelastelementofthearraypassedwheninstantiatingyourConfigAggregator:

MigratingtoExpressive2.0

29

newPhpFileProvider('config/development.config.php'),

Ifthefileismissing,thatproviderwillreturnanemptyarray;ifit'spresent,itreturnswhateverconfigurationthefilereturns.Bymakingitthelastelementmerged,wecandothingslikeoverrideconfigurationcaching,andforcedebugmode,whichiswhatourconfig/development.config.php.distfiledoes!

Finally,let'saddsomeconveniencescriptstocomposer.Openyourcomposer.jsonfile,findthescriptssection,andaddthefollowingtoit:

"development-disable":"zf-development-modedisable",

"development-enable":"zf-development-modeenable",

"development-status":"zf-development-modestatus",

Nowwecantryitout!

Run:

$composerdevelopment-status

Thisshouldtellyouthatdevelopmentmodeiscurrentlydisabled.

Next,run:

$composerdevelopment-enable

Thiswillenabledevelopmentmode.

Ifyouwanttotestandensureyou'reindevelopmentmode,editoneofyourmiddlewaretohaveitraiseanexception,andseewhathappens!

CleanupIfyourapplicationisworkingcorrectly,youcannowdosomeadditionalcleanup.

Edityourconfig/autoload/middleware-pipeline.global.phpfiletoremovethemiddleware_pipelinekeyanditscontents.Edityourconfig/autoload/routes.global.phpfiletoremovetherouteskeyanditscontents.SearchforanyreferencestoaFinalHandlerwithinyourdependencyconfiguration,andremovethem.

MigratingtoExpressive2.0

30

Atthispoint,youshouldhaveafullyworkingExpressive2application!

Finalstep:UpdatingyourmiddlewareNowthattheinitialmigrationiscomplete,youcantakesomemoresteps!

OneofthebigchangesisthatExpressive2prefersmiddlewareimplementinghttp-interop/http-middleware'sMiddlewareInterface.Thisrequiresafewchangestoyourmiddleware.

First,let'slookattheinterfacesdefinedbyhttp-interop/http-middleware:

namespaceInterop\Http\ServerMiddleware;

usePsr\Http\Message\ResponseInterface;

usePsr\Http\Message\ServerRequestInterface;

interfaceMiddlewareInterface

{

/**

*@returnResponseInterface

*/

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te);

}

interfaceDelegateInterface

{

/**

*@returnResponseInterface

*/

publicfunctionprocess(ServerRequestInterface$request);

}

Thefirstinterfacedefinesmiddleware.UnlikeExpressive1,http-interopmiddlewaredoesnotreceivearesponseinstance.Thereareavarietyofreasonsforthis,butAnthonyFerrarasumsthemupbestinablogposthewroteinMay2016 .

Anotherdifferenceisthatinsteadofacallable$nextargument,wehaveaDelegateInterface$delegate.Thisprovidesbettertype-safety,and,becauseeachoftheMiddlewareInterfaceandDelegateInterfacedefinethesameprocess()method,ensuresthatimplementationsofmiddlewareanddelegatesarediscreteanddonotmixconcerns.Delegatesareclassesthatcanprocessarequestifthecurrentmiddlewarecannotfullydoso.Examplesmightincludemiddlewarethatwillinjectadditionalresponseheaders,ormiddlewarethatonlyactswhencertainrequestcriteriaarepresent(suchasHTTPcachingheaders).

4

MigratingtoExpressive2.0

31

Theupshotisthatwhenrewritingyourmiddlewaretousethenewinterfaces,youneedtodoseveralthings:

First,importthehttp-interopinterfacesintoyourclassfile:

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterface;

Second,renamethe__invoke()methodtoprocess().

Third,updatethesignatureofyournewprocessmethodtobe:

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

Fourth,lookforcallsto$next().Asanexample,thefollowing:

return$next($request,$response);

Becomes:

return$delegate->process($request);

Theseupdateswillvaryonacase-by-casebasis:insomecases,youmaybecallingmethodsontherequestinstance;inothercases,youmaybecapturingthereturnedresponse

Lookforcaseswhereyouwereusingthepassed$responseinstance,andeliminatethose.Youmaydosoasfollows:

Usetheresponsereturnedbycalling$delegate->process()instead.Createanewconcreteresponseinstanceandoperateonit.Composea"responseprototype"inyourmiddlewareifyoudonotwanttocreateanewresponseinstancedirectly,andoperateonit.Doingsowillrequirethatyouupdateanyfactoryassociatedwiththemiddlewareclass,however.

Asanexample,let'slookatasimplemiddlewarethataddsaresponseheader:

MigratingtoExpressive2.0

32

namespaceApp\Middleware;

usePsr\Http\Message\ResponseInterface;

usePsr\Http\Message\ServerRequestInterface;

classTheClacksMiddleware

{

publicfunction__invoke(ServerRequestInterface$request,ResponseInterface$respo

nse,callable$next)

{

$response=$next($request,$response);

return$response->withHeader('X-Clacks-Overhead',['GNUTerryPratchett']);

}

}

Whenwerefactorittobehttp-interopmiddleware,itbecomes:

namespaceApp\Middleware;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterface;

usePsr\Http\Message\ServerRequestInterface;

classTheClacksMiddlewareimplementsMiddlewareInterface

{

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

{

$response=$delegate->process($request);

return$response->withHeader('X-Clacks-Overhead',['GNUTerryPratchett']);

}

}

SummaryMigrationconsistsof:

Updatingdependencies.Runningmigrationscriptsprovidedbyzendframework/zend-expressive-tooling.Optionallyaddingaself-invokingfunctionaroundcodecreatingvariablesinpublic/index.php.Optionallyupdatingyourapplicationtousezendframework/zend-config-aggregatorforconfigurationaggregation.Optionallyaddingzfcampus/zf-development-modeintegrationtoyourapplication.

MigratingtoExpressive2.0

33

Optionallyupdatingyourmiddlewaretoimplementhttp-interop/http-middleware.

Asnoted,manyofthesechangesareoptional.Yourapplicationwillcontinuetorunwithoutthem.Updatingthemwillmodernizeyourapplication,however,andmakeitmorefamiliartodevelopersfamiliarwiththeExpressive2skeleton.

Wehopethisguidegetsyousuccessfullymigrated!Ifyourunintoissuesnotcoveredhere,pleaseletusknowviaanissueontheExpressiverepository .

Footnotes

.https://framework.zend.com/blog/2017-03-07-expressive-2.html↩

.https://github.com/adamculp/expressive-blastoff↩

.https://docs.zendframework.com/zend-expressive/features/modular-applications/↩

.http://blog.ircmaxell.com/2016/05/all-about-middleware.html↩

.https://github.com/zendframework/zend-expressive/issues/new↩

5

1

2

3

4

5

MigratingtoExpressive2.0

34

DevelopExpressiveApplicationsRapidlyUsingCLIToolingbyMatthewWeierO'Phinney

Firstimpressionsmatter,particularlywhenyoustartusinganewframework.Assuch,we'restrivingtoimproveyourfirsttaskswithExpressive.

Withthe2.0release,weprovidedseveralmigrationtools,aswellastoolingforcreating,registering,andderegisteringmiddlewaremodules.Eachwasshippedasaseparatescript,withlittleunificationbetweenthem.

Today,we'vepushedaunifiedscript,expressive,whichprovidesaccesstoallthemigrationtooling,moduletooling,andnewtoolingtohelpyoucreatehttp-interopmiddleware.OurhopeistomakeyourfirstfewminuteswithExpressiveabiteasier,soyoucanstartwritingpowerfulapplications.

GettingthetoolingIfyouhaven'tcreatedanapplicationyet:

$composercreate-projectzendframework/zend-expressive-skeleton

willcreateanewprojectusingthelatest2.0.2release,whichcontainsthenewexpressivescript.

IfyouarealreadyusingExpressive2,youcangetthelatesttoolingusingthefollowing,regardlessofwhetherornotyou'vepreviouslyinstalledit:

$composerrequire--dev"zendframework/zend-expressive-tooling:^0.4.1"

Whattoolingdoyouget?Theexpressivescripthasthreegeneralcategoriesofcommands:

migrate:*:theseareintendedforExpressive1userswhoaremigratingtoExpressive2.We'llignorethesefornow,aswecoveredtheminthepreviouschapter.module:*Create,register,andderegisterExpressivemiddlewaremodules.

Expressivetooling

35

middleware:*:Createhttp-interopmiddlewareclassfiles.

CreateyourfirstmoduleForpurposesofillustration,we'llconsiderthatyouwanttocreateanAPIforlistingbooks.Youanticipatethatthefunctionalitycanbeself-contained,andthatyoumaywanttopotentiallyextractitlatertore-useelsewhere.Assuch,youhaveagoodcaseforcreatingamodule .

Let'sgetstarted:

$./vendor/bin/expressivemodule:createBooksApi

Theabovedoesthefollowing:

ItcreatesadirectorytreeforaBooksApimoduleundersrc/BooksApi/,withasubtreeforsourcecode,andanotherfortemplates.ItcreatestheclassBooksApi\ConfigProviderinthefilesrc/BooksApi/src/ConfigProvider.php

ItaddsaPSR-4autoloaderentryforBooksApiinyourcomposer.json,andrunscomposerdump-autoloadtoensurethenewautoloaderruleisgeneratedwithinyourapplication.ItaddsanentryforthegeneratedBooksApi\ConfigProvidertoyourconfig/config.phpfile.

Atthispoint,wehaveamodulewithnocode!Let'srectifythatsituation!

CreatemiddlewareWeknowwewillwanttolistbooks,sowe'llcreatemiddlewareforthat:

$./vendor/bin/expressivemiddleware:create"BooksApi\Action\ListBooksAction"

Usequotes!

PHP'snamespaceseparatoristhebackslash,whichistypicallyinterpretedasanescapecharacterinmostshells.Assuch,usedoubleorsinglequotesaroundthemiddlewarenametoensureitispassedcorrectlytothecommand!

1

Expressivetooling

36

ThiscreatestheclassBooksApi\Action\ListBooksActioninthefilesrc/BooksApi/src/Action/ListBooksAction.php.Indoingso,itcreatesthesrc/BooksApi/src/Action/directory,asitdidnotpreviouslyexist!

Theclassfilecontentswilllooklikethis:

namespaceBooksApi\Action;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterface;

usePsr\Http\ServerRequestInterface;

classListBooksActionimplementsMiddlewareInterface

{

/**

*{@inheritDoc}

*/

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

{

//$response=$delegate->process($request);

}

}

Atthispoint,you'rereadytostartcoding!

FuturedirectionThistoolingisjustastart;we'rewellawarethatdeveloperswillwantandneedmoretoolingtomakedevelopmentmoreconvenient.Assuch,wehaveacalltoaction:pleaseopenissues torequestmorecommandsthatwillmakeyourlifeeasier,oropenpullrequeststhatimplementthetoolsyouneed.Ifyouareunsurehowtodoso,usetheexistingcodetogetanideaofhowtoproceed,oraskinthe#expressive-contribSlackchannel .

Footnotes

.https://docs.zendframework.com/zend-expressive/features/modular-applications/↩

.https://github.com/zendframework/zend-expressive-tooling/issues/new↩

.GetaninvitetoourSlackathttps://zendframework-slack.herokuapp.com↩

2

3

1

2

3

Expressivetooling

37

NestedMiddlewareinExpressivebyMatthewWeierO'Phinney

Amajorreasontoadoptamiddlewarearchitectureistheabilitytocreatecustomworkflowsforyourapplication.MosttraditionalMVCarchitectureshaveaveryspecificworkflowtherequestfollows.Whilethisisoftencustomizableviaeventlisteners,theeventsandgeneralrequestlifecycleisthesameforeachandeveryresourcetheapplicationserves.

Withmiddleware,however,youcandefineyourownworkflowbycomposingmiddleware.

ExpressivepipelinesInExpressive,wecalltheworkflowtheapplicationpipeline,andyoucreateitbypipingmiddlewareintotheapplication.Asanexample,thedefaultpipelineinstalledwiththeskeletonapplicationlookslikethis:

//Inconfig/pipeline.php:

useZend\Expressive\Helper\ServerUrlMiddleware;

useZend\Expressive\Helper\UrlHelperMiddleware;

useZend\Expressive\Middleware\ImplicitHeadMiddleware;

useZend\Expressive\Middleware\ImplicitOptionsMiddleware;

useZend\Expressive\Middleware\NotFoundHandler;

useZend\Stratigility\Middleware\ErrorHandler;

$app->pipe(ErrorHandler::class);

$app->pipe(ServerUrlMiddleware::class);

$app->pipeRoutingMiddleware();

$app->pipe(ImplicitHeadMiddleware::class);

$app->pipe(ImplicitOptionsMiddleware::class);

$app->pipe(UrlHelperMiddleware::class);

$app->pipeDispatchMiddleware();

$app->pipe(NotFoundHandler::class);

Inthisparticularworkflow,whathappenswhenarequestisprocessedisthefollowing:

TheErrorHandlermiddleware(whichhandlesexceptionsandPHPerrors)isprocessed,whichinturn:

processestheServerUrlMiddleware(whichinjectstherequestURIintotheServerUrlhelper),whichinturn:

processtheroutingmiddleware,whichinturn:processtheImplicitHeadMiddleware(whichprovidesresponsesforHEAD

NestedMiddlewareinExpressive

38

requestsifthematchedmiddlewaredoesnothandlethatmethod),whichinturn:

processestheImplicitOptionsMiddleware(whichprovidesresponsesforOPTIONSrequestsifthematchedmiddlewaredoesnothandlethatmethod),whichinturn:

processestheUrlHelperMiddleware(whichinjectstheUrlHelperwiththeRouteResultfromrouting,ifdiscovered),whichinturn:

processesthedispatchmiddleware,whichinturn:processesthematchedmiddleware,ifpresentprocessestheNotFoundHandler,ifnomiddlewarewasmatchedbyrouting,orthatmiddlewarecannothandletherequest.

Atanypointintheworkflow,middlewarecanchoosetoreturnaresponse.Forinstance,theImplicitHeadMiddlewareandImplicitOptionsMiddlewaremayreturnaresponseifthemiddlewarematchedbyroutingcannothandlethespecifiedmethod.Whentheydo,nolayersbelowareexecuted!

Scenario:AddingAuthenticationNow,let'ssaywewanttoaddauthenticationtoourapplication.

Forpurposesofthisexample,we'llusetheBasicAuthenticationmiddleware fromthemiddlewares/http-authenticationpackage :

$composerrequiremiddlewares/http-authentication

Whenthismiddlewareexecutes,itlooksatthevariousHTTPrequestheadersusedforHTTPBasicAuthentication,andthenattemptstoverifythecredentialsagainstalistcomposedintheinstance.Ifloginfails,themiddlewarereturnsa401response;otherwise,itdelegatestothenextmiddleware.

Themiddlewareacceptsalistofusername/passwordpairstoitsconstructor.Italsoallowsyoutoprovideanauthenticationrealmviatherealm()method,andtheattributetowhichtosavethenameoftheauthenticateduserwithintherequestusedtodispatchthenextmiddlewarewhenauthenticationsucceeds.We'llcreateafactorytoconfigurethemiddleware:

12

NestedMiddlewareinExpressive

39

<?php

namespaceAcme;

useMiddlewares\BasicAuthentication

usePsr\Container\ContainerInterface;

classBasicAuthenticationFactory

{

/**

*@returnBasicAuthentication

*/

publicfunction__invoke(ContainerInterface$container)

{

$config=$container->has('config')?$container->get('config'):[];

$credentials=$config['authentication']['credentials']??[];

$realm=$config['authentication']['realm']??__NAMESPACE__;

$attribute=$config['authentication']['attribute']

??BasicAuthentication::class;

$middleware=newBasicAuthentication($credentials);

$middleware->realm($realm);

$middleware->attribute($attribute);

return$middleware;

}

}

Wirethisinyourdependenciessomewhere;werecommendeitherthefileconfig/autoload/dependencies.global.phportheclassAcme\ConfigProviderifyouhavedefinedit:

'dependencies'=>[

'factories'=>[

Middlewares\BasicAuthentication::class=>Acme\BasicAuthenticationFactory::cla

ss,

],

],

Now,we'lladdthistothepipeline.

Ifyouwanteveryrequesttorequireauthentication,youcanpipethisinearly,sometimeaftertheErrorHandlerandanymiddlewareyouwanttorunforeveryrequest:

//Inconfig/pipeline.php:

$app->pipe(ErrorHandler::class);

$app->pipe(ServerUrlMiddleware::class);

$app->pipe(\Middlewares\BasicAuthentication::class);

NestedMiddlewareinExpressive

40

Done!

But...thismeansthatallpagesoftheapplicationnowrequireauthentication!Youlikelydon'twanttorequireauthenticationforthehomepage,andpotentiallymanyothers.

Let'slookatsomeoptions.

SegregatingbypathOneoptionavailableinExpressiveispathsegregation.Ifyouknoweveryrouterequiringauthenticationwillhavethesamepathprefix,youcanusethisapproach.

Asanexample,let'ssayyouonlywantauthenticationforyourAPI,andallAPIpathsfallunderthepathprefix/api.Thismeansyoucoulddothefollowing:

$app->pipe('/api',\Middlewares\BasicAuthentication::class);

Thismiddlewarewillonlyexecuteiftherequestpathmatches/api.

ButwhatifyouonlyreallyneedauthenticationforspecificroutesundertheAPI?

NestedmiddlewareWefinallygettothepurposeofthistutorial!

Let'ssayourAPIdefinesthefollowingroutes:

//Inconfig/routes.php:

$app->get('/api/books',Acme\Api\BookListMiddleware::class,'api.books');

$app->post('/api/books',Acme\Api\CreateBookMiddleware::class);

$app->get('/api/books/{book_id:\d+}',Acme\Api\BookMiddleware::class,'api.book');

$app->patch('/api/books/{book_id:\d+}',Acme\Api\UpdateBookMiddleware::class);

$app->delete('/api/books/{book_id:\d+}',Acme\Api\DeleteBookMiddleware::class);

Inthisscenario,wewanttorequireauthenticationonlyfortheCreateBookMiddleware,UpdateBookMiddleware,andDeleteBookMiddleware.Howdowedothat?

Expressiveallowsyoutoprovidealistofmiddlewarebothwhenpipingandrouting,insteadofasinglemiddleware.Justaswhenyouspecifyasinglemiddleware,eachentrymaybeoneof:

callablemiddleware

NestedMiddlewareinExpressive

41

middlewareinstanceservicenameresolvingtomiddleware

Internally,ExpressivecreatesaZend\Stratigility\MiddlewarePipeinstancewiththespecifiedmiddleware,andprocessesthispipelinewhenthegivenmiddlewareismatched.

So,goingbacktoourpreviousexample,wherewedefinedroutes,wecanrewritethemasfollows:

//Inconfig/routes.php:

$app->get('/api/books',Acme\Api\BookListMiddleware::class,'api.books');

$app->post('/api/books',[

Middlewares\BasicAuthentication::class,

Acme\Api\CreateBookMiddleware::class,

]);

$app->get('/api/books/{book_id:\d+}',Acme\Api\BookMiddleware::class,'api.book');

$app->patch('/api/books/{book_id:\d+}',[

Middlewares\BasicAuthentication::class,

Acme\Api\UpdateBookMiddleware::class,

]);

$app->delete('/api/books/{book_id:\d+}',[

Middlewares\BasicAuthentication::class,

Acme\Api\DeleteBookMiddleware::class,

]);

Inthisparticularcase,thismeansthattheBasicAuthenticationmiddlewarewillonlyexecuteforoneofthefollowing:

POSTrequeststo/api/booksPATCHrequeststo/api/books/123(oranyvalididentifier)DELETErequeststo/api/books/123(oranyvalididentifier)

Ineachcase,ifauthenticationfails,thelatermiddlewareinthelistwillnotbeprocessed,astheBasicAuthenticationmiddlewarewillreturna401response.

Thistechniqueallowsforsomepowerfulworkflows.Forinstance,whencreatingabookviathe/api/booksmiddleware,wecouldalsoaddinmiddlewaretocheckthecontenttype,parsetheincomingrequest,andvalidatethesubmitteddata:

NestedMiddlewareinExpressive

42

//Inconfig/routes.php:

$app->post('/api/books',[

Middlewares\BasicAuthentication::class,

Acme\ContentNegotiationMiddleware::class,

Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware::class,

Acme\Api\BookValidationMiddleware::class,

Acme\Api\CreateBookMiddleware::class,

]);

(Weleaveimplementationofmostoftheabovemiddlewareasanexerciseforthereader!)

Byusingservicenames,youalsoensurethatoptimalperformance;themiddlewarewillnotbeinstantiatedunlesstherequestmatches,andthemiddlewareisexecuted.Infact,ifoneofthepipelinemiddlewareforthegivenroutereturnsaresponseearly,eventhemiddlewarelaterinthequeuewillnotbeinstantiated!

Anoteaboutorder

Whenyoucreatemiddlewarepipelinessuchastheabove,aswellasinthefollowingexamples,ordermatters.Pipelinesaremanagedinternallyasqueues,andthusarefirst-in-first-out(FIFO).Assuch,puttingtherespondingCreateBookMiddleware(whichwillmostlikelyreturnaresponsewiththeAPIpayload)willresultintheothermiddlewareneverexecuting!

Assuch,ensurethatyourpipelinescontainmiddlewarethatwilldelegatefirst,andyourprimarymiddlewarethatreturnsaresponselast.

MiddlewarepipelinesAnotherapproachwouldbetosetupamiddlewarepipelinemanuallywithinthefactoryfortherequestedmiddleware.ThefollowingexamplescreatesandreturnsaZend\Stratigility\MiddlewarePipeinstancethatcomposesthesamemiddlewareasinthepreviousexamplethatusedalistofmiddlewarewhenrouting,returningtheMiddlewarePipeinsteadoftherequestedCreateBookMiddleware(butcomposingitnonetheless):

NestedMiddlewareinExpressive

43

namespaceAcme\Api;

useAcme\ContentNegotiationMiddleware;

useMiddlewares\BasicAuthentication;

usePsr\Container\ContainerInterface;

useZend\Expressive\Helper\BodyParams\BodyParamsMiddleware;

useZend\Stratigility\MiddlewarePipe;

classCreateBookMiddlewareFactory

{

publicfunction__invoke(ContainerInterface$container)

{

$pipeline=newMiddlewarePipe();

$pipeline->pipe($container->get(BasicAuthentication::class));

$pipeline->pipe($container->get(ContentValidationMiddleware::class));

$pipeline->pipe($container->get(BodyParamsMiddleware::class));

$pipeline->pipe($container->get(BookValidationMiddleware::class));

//Ifdependenciesareneeded,pullthemfromthecontainerandpass

//themtotheconstructor:

$nested->pipe(newCreateBookMiddleware());

return$pipeline;

}

}

Thisapproachisinferiortousinganarrayofmiddleware,however.Internally,ExpressivewillwrapthevariousmiddlewareservicesyoulistinLazyLoadingMiddlewareinstances;thismeansthatifaserviceearlierinthepipelinereturnsearly,theservicewillneverbepulledfromthecontainer.Thiscanbeimportantifanyservicesmightestablishnetworkconnectionsorperformfileoperationsduringinitialization!

NestedapplicationsSinceExpressivedoestheworkoflazyloadingservices,anotheroptionwouldbetocreateanotherExpressiveApplicationinstance,andfeedit,insteadofcreatingaMiddlewarePipe:

NestedMiddlewareinExpressive

44

namespaceAcme\Api;

useAcme\ContentNegotiationMiddleware;

useMiddlewares\BasicAuthentication;

usePsr\Container\ContainerInterface;

useZend\Expressive\Application;

useZend\Expressive\Helper\BodyParams\BodyParamsMiddleware;

useZend\Expressive\Router\RouterInterface;

classCreateBookMiddlewareFactory

{

publicfunction__invoke(ContainerInterface$container)

{

$nested=newApplication(

$container->get(RouterInterface::class),

$container

);

$nested->pipe(BasicAuthentication::class);

$nested->pipe(ContentValidationMiddleware::class);

$nested->pipe(BodyParamsMiddleware::class);

$nested->pipe(BookValidationMiddleware::class);

//Ifdependenciesareneeded,pullthemfromthecontainerandpass

//themtotheconstructor:

$nested->pipe(newCreateBookMiddleware());

return$nested;

}

}

Thebenefitthisapproachhasisthatyougetthelazy-loadingmiddlewareinstanceswithouteffort.However,itmakesdiscoveryofwhatthemiddlewareconsistsmoredifficult—youcan'tjustlookattheroutesanymore,butneedtolookatthefactoryitselftoseewhattheworkflowlookslike.Whenyouconsiderre-distributionandre-use,though,thisapproachhasalottooffer,asitcombinestheperformanceofdefininganapplicationpipelinewiththeabilitytore-usethatsameworkflowanytimeyouusethatparticularmiddlewareinanapplication.

(Theabovecouldevenuseseparaterouterandcontainerinstancesentirely,inordertokeeptheservicesandroutingforthemiddlewarepipelinecompletelyseparatefromthoseofthemainapplication!)

Usingtraitsforcommonworkflows

NestedMiddlewareinExpressive

45

Theaboveapproachofcreatinganestedapplication,aswellastheoriginalexampleofnestedmiddlewareprovidedviaarrays,hasonedrawback:ifseveralmiddlewareneedtheexactsameworkflow,you'llhaverepetition.

Oneapproachistocreateatrait forcreatingtheApplicationinstanceandpopulatingtheinitialpipeline.

namespaceAcme\Api;

useAcme\ContentNegotiationMiddleware;

useMiddlewares\BasicAuthentication;

usePsr\Container\ContainerInterface;

useZend\Expressive\Application;

useZend\Expressive\Helper\BodyParams\BodyParamsMiddleware;

useZend\Expressive\Router\RouterInterface;

traitCommonApiPipelineTrait

{

privatefunctioncreateNestedApplication(ContainerInterface$container)

{

$nested=newApplication(

$container->get(RouterInterface::class),

$container

);

$nested->pipe(BasicAuthentication::class);

$nested->pipe(ContentValidationMiddleware::class);

$nested->pipe(BodyParamsMiddleware::class);

$nested->pipe(BookValidationMiddleware::class);

return$nested;

}

}

OurCreateBookMiddlewareFactorythenbecomes:

3

NestedMiddlewareinExpressive

46

namespaceAcme\Api;

usePsr\Container\ContainerInterface;

classCreateBookMiddlewareFactory

{

useCommonApiPipelineTrait;

publicfunction__invoke(ContainerInterface$container)

{

$nested=$this->createNestedApplication($container);

//Ifdependenciesareneeded,pullthemfromthecontainerandpass

//themtotheconstructor:

$nested->pipe(newCreateBookMiddleware());

return$nested;

}

}

Anymiddlewarethatwouldneedthesameworkflowcannowprovideafactorythatusesthesametrait.This,ofcourse,meansthatthefactoriesforanygivenmiddlewarethatadoptsthespecificworkflowreflectthat,meaningtheycannotere-usedwithoutusingthatspecificworkflow.

DelegatorfactoriesTosolvethislatterproblem—allowingre-useofmiddlewarewithoutrequiringthespecificpipeline—weprovideanotherapproach:delegatorfactories .

Availablesinceversion2oftheExpressiveskeleton,delegatorfactoriesinterceptcreationofaservice,andallowyoutoactontheservicebeforereturningit,orreplaceitwithanotherinstanceentirely!

Theabovetraitcouldberewrittenasadelegatorfactory:

4

NestedMiddlewareinExpressive

47

namespaceAcme\Api;

useMiddlewares\BasicAuthentication;

useAcme\ContentNegotiationMiddleware;

usePsr\Container\ContainerInterface;

useZend\Expressive\Application;

useZend\Expressive\Helper\BodyParams\BodyParamsMiddleware;

useZend\Expressive\Router\RouterInterface;

classCommonApiPipelineDelegatorFactory

{

publicfunction__invoke(ContainerInterface$container,$name,callable$callback)

{

$nested=newApplication(

$container->get(RouterInterface::class),

$container

);

$nested->pipe(BasicAuthentication::class);

$nested->pipe(ContentValidationMiddleware::class);

$nested->pipe(BodyParamsMiddleware::class);

$nested->pipe(BookValidationMiddleware::class);

//Injectthemiddlewareservicerequested:

$nested->pipe($callback());

return$nested;

}

}

Youcouldthenregisterthiswithanyservicethatneedsthepipeline,withoutneedingtochangetheirfactories.Asanexample,youcouldhavethefollowingineithertheconfig/autoload/dependencies.global.phpfileortheAcme\ConfigProviderclass,ifdefined:

NestedMiddlewareinExpressive

48

'dependencies'=>[

'factories'=>[

\Acme\Api\CreateBookMiddleware::class=>\Acme\Api\CreateBookMiddlewareFactory

::class,

\Acme\Api\DeleteBookMiddleware::class=>\Acme\Api\DeleteBookMiddlewareFactory

::class,

\Acme\Api\UpdateBookMiddleware::class=>\Acme\Api\UpdateBookMiddlewareFactory

::class,

],

'delegators'=>[

\Acme\Api\CreateBookMiddleware::class=>[

\Acme\Api\CommonApiPipelineDelegatorFactory::class,

],

\Acme\Api\DeleteBookMiddleware::class=>[

\Acme\Api\CommonApiPipelineDelegatorFactory::class,

],

\Acme\Api\UpdateBookMiddleware::class=>[

\Acme\Api\CommonApiPipelineDelegatorFactory::class,

],

],

],

Thisapproachoffersre-usabilityevenwhenagivenmiddlewaremaynothaveexpectedtobeusedinaspecificworkflow!

Middlewareallthewaydown!WehopethistutorialdemonstratesthepowerandflexibilityofExpressive,andhowyoucancreateworkflowsthataregranulareventospecificmiddleware.Wecoveredanumberoffeaturesinthispost:

Pipelinemiddlewarethatoperatesforallrequests.Path-segregatedmiddleware.Middlewarenestingvialistsofmiddleware.Returningpipelinesorapplicationsfromindividualservicefactories.Usingdelegatorfactoriestocreateandreturnnestedpipelinesorapplications.

Footnotes

.https://github.com/middlewares/http-authentication#basicauthentication↩

.https://github.com/middlewares/http-authentication↩

.http://php.net/trait↩

1

2

3

4

NestedMiddlewareinExpressive

49

.https://docs.zendframework.com/zend-expressive/features/container/delegator-factories/↩

4

NestedMiddlewareinExpressive

50

ErrorHandlinginExpressivebyMatthewWeierO'Phinney

OneofthebigimprovementsinExpressive2ishowerrorhandlingisapproached.Whiletheerrorhandlingdocumentation coversthefeatureindetail,moreexamplesareneverabadthing!

OurscenarioForourexample,we'llcreateanAPIresourcethatreturnsalistofbooksread.BeinganAPI,wewanttoreturnJSON;thisistrueevenwhenwewanttopresenterrordetails.Ourchallenge,then,willbetoadderrorhandlingthatpresentsJSONerrordetailswhentheAPIisinvoked—butusetheexistingerrorhandlingotherwise.

ThemiddlewareThemiddlewarelookslikethefollowing:

//Insrc/Acme/BooksRead/ListBooksRead.php:

namespaceAcme\BooksRead;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterface;

usePDO;

usePsr\Http\Message\ServerRequestInterface;

useZend\Diactoros\Response\JsonResponse;

classListBooksReadimplementsMiddlewareInterface

{

constSORT_ALLOWED=[

'author',

'date',

'title',

];

constSORT_DIR_ALLOWED=[

'ASC',

'DESC',

];

private$pdo;

1

ErrorHandlinginExpressive

51

publicfunction__construct(PDO$pdo)

{

$this->pdo=$pdo;

}

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

{

$query=$request->getQueryParams();

$page=$this->validatePageOrPerPage((int)($query['page']??1));

$perPage=$this->validatePageOrPerPage((int)($query['per_page']??25));

$sort=$this->validateSort($query['sort']??'date');

$sortDir=$this->validateSortDirection($query['sort_direction']??'DESC');

$offset=($page-1)*$perPage;

$statement=$pdo->prepare(sprintf(

'SELECT*FROMbooks_readORDERBY%s%sLIMIT%dOFFSET%d',

$sort,

$sortDir,

$perPage,

$offset

));

try{

$statement->execute([]);

}catch(PDOException$e){

throwException\ServerError::create(

'Databaseerroroccurred',

sprintf('Adatabaseerroroccurred:%s',$e->getMessage()),

['trace'=>$e->getTrace()]

);

}

$books=$statement->fetchAll(PDO::FETCH_ASSOC);

returnnewJsonResponse(['books'=>$books]);

}

privatefunctionvalidatePageOrPerPage($value,$param)

{

if($value>1){

return$value;

}

throwException\InvalidRequest::create(

sprintf('Invalid%svaluespecified',$param),

sprintf('The%sspecifiedmustbeanintegergreaterthan1',$param)

);

}

privatefunctionvalidateSort(string$sort)

ErrorHandlinginExpressive

52

{

if(in_array($sort,self::SORT_ALLOWED,true)){

return$sort;

}

throwException\InvalidRequest::create(

'Invalidsorttypespecified',

sprintf(

'Thesorttypespecifiedmustbeoneof[%s]',

implode(',',self::SORT_ALLOWED)

)

);

}

privatefunctionvalidateSortDirection(string$direction)

{

if(in_array($direction,self::SORT_DIR_ALLOWED,true)){

return$direction;

}

throwException\InvalidRequest::create(

'Invalidsortdirectionspecified',

sprintf(

'Thesortdirectionspecifiedmustbeoneof[%s]',

implode(',',self::SORT_DIR_ALLOWED)

)

);

}

}

You'llnoticethatthismiddlewarethrowsexceptionsforerrorhandling,andusessomecustomexceptiontypes.Let'sexaminethosenext.

Theexceptions

OurAPIwillhavecustomexceptions.Inordertoprovideusefuldetailstoourusers,we'llhaveourexceptionscomposeadditionaldetailsthatwecanreport.Assuch,we'llhaveaspecialinterfaceforourAPIexceptionsthatexposesthecustomdetails.

We'llalsodefineafewspecifictypes.Sincemuchoftheworkwillbethesamebetweenthesetypes,we'lluseatraittodefinethecommoncode,andcomposethatintoeach.

ErrorHandlinginExpressive

53

//Insrc/Acme/BooksRead/Exception/MiddlewareException.php:

namespaceAcme\BooksRead\Exception;

interfaceMiddlewareException

{

publicstaticfunctioncreate():MiddlewareException;

publicfunctiongetStatusCode():int;

publicfunctiongetType():string;

publicfunctiongetTitle():string;

publicfunctiongetDescription():string;

publicfunctiongetAdditionalData():array;

}

//Insrc/Acme/BooksRead/Exception/MiddlewareExceptionTrait.php:

namespaceAcme\BooksRead\Exception;

traitMiddlewareExceptionTrait

{

private$statusCode;

private$title;

private$description;

private$additionalData=[];

publicfunctiongetStatusCode():int

{

return$this->statusCode;

}

publicfunctiongetTitle():string

{

return$this->title;

}

publicfunctiongetDescription():string

{

return$this->description;

}

publicfunctiongetAdditionalData():array

{

return$this->additionalData;

}

}

ErrorHandlinginExpressive

54

//Insrc/Acme/BooksRead/Exception/ServerError.php:

namespaceAcme\BooksRead\Exception;

useRuntimeException;

classServerErrorextendsRuntimeExceptionimplementsMiddlewareException

{

useMiddlewareExceptionTrait;

publicstaticfunctioncreate(string$title,string$description,array$additiona

lData=[])

{

$e=newself($description,500);

$e->statusCode=500;

$e->title=$title;

$e->additionalData=$additionalData;

return$e;

}

publicfunctiongetType():string

{

return'https://example.com/api/problems/server-error';

}

}

ErrorHandlinginExpressive

55

//Insrc/Acme/BooksRead/Exception/InvalidRequest.php:

namespaceAcme\BooksRead\Exception;

useRuntimeException;

classInvalidRequestextendsRuntimeExceptionimplementsMiddlewareException

{

useMiddlewareExceptionTrait;

publicstaticfunctioncreate(string$title,string$description,array$additiona

lData=[])

{

$e=newself($description,400);

$e->statusCode=400;

$e->title=$title;

$e->additionalData=$additionalData;

return$e;

}

publicfunctiongetType():string

{

return'https://example.com/api/problems/invalid-request';

}

}

Thesespecializedexceptiontypeshaveadditionalmethodsforretrievingadditionaldata.Furthermore,theysetdefaultexceptioncodes,whichmayberepurposedasstatuscodes.

AProblemDetailserrorhandlerWhatwewanttohavehappenisforourAPItoreturndatainProblemDetails format.

Toaccomplishthis,we'llcreateanewmiddlewarethatwillcatchourdomain-specificexceptiontypeinordertocreateanappropriateresponseforus.

2

ErrorHandlinginExpressive

56

//Insrc/Acme/BooksRead/ProblemDetailsMiddleware.php:

namespaceAcme\BooksRead;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterface;

usePsr\Http\Message\ServerRequestInterface;

useThrowable;

useZend\Diactoros\Response\JsonResponse;

classProblemDetailsMiddlewareimplementsMiddlewareInterface

{

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

{

try{

$response=$delegate->process($request);

return$response;

}catch(Exception\MiddlewareException$e){

//caught;we'llhandleitfollowingthetry/catchblock

}catch(Throwable$e){

throw$e;

}

$problem=[

'type'=>$e->getType(),

'title'=>$e->getTitle(),

'detail'=>$e->getDescription(),

];

$problem=array_merge($e->getAdditionalData(),$problem);

returnnewJsonResponse($problem,$e->getStatusCode(),[

'Content-Type'=>'application/problem+json',

]);

}

}

Thismiddlewarealwaysdelegatesprocessingoftherequest,butdoessoinatry/catchblock.IfitcatchesourspecialMiddlewareException,itwillprocessit;otherwise,itre-throwsthecaughtexception,toallowmiddlewareinanouterlayertohandleit.

ComposingtheerrorhandlerInourpreviousarticle,NestedMiddlewareinExpressive,wedetailhowtonestmiddlewarepipelinesandcreatenestedmiddlewarepipelinesforroutedmiddleware.We'llusethosetechniqueshere.Pleasereadthatbeforecontinuing.

ErrorHandlinginExpressive

57

AssumingwehavealreadydefinedafactoryforourListBooksReadmiddleware(likelyclassAcme\BooksRead\ListBooksReadFactory,insrc/Acme/BooksRead/ListBooksReadFactory.php),wehaveafewoptions.First,wecouldcomposethiserrorhandlerinamiddlewarepipelinewithinourroutingconfiguration:

//Inconfig/routes.php:

$app->get('/api/books-read',[

\Acme\BooksRead\ProblemDetailsMiddleware::class,

\Acme\BooksRead\ListBooksRead::class,

],'api.books-read')

Ifthereareotherconcerns—suchasauthentication,authorization,contentnegotiation,etc.—youmaywanttoinsteadcreateadelegatorfactory;thiscanthenbere-usedforotherAPIresourcesthatneedthesamesetofmiddleware.Asanexample:

//Insrc/Acme/BooksRead/ApiMiddlewareDelegatorFactory.php:

namespaceAcme\BooksRead;

usePsr\Container\ContainerInterface;

useZend\Expressive\Application;

useZend\Expressive\Router\RouterInterface;

classApiMiddlewareDelegatorFactory

{

publicfunction__invoke(ContainerInterface$container,$name,callable$callback)

{

$apiPipeline=newApplication(

$container->get(RouterInterface::class),

$container

);

$apiPipeline->pipe(ProblemDetailsMiddleware::class);

//..andpipeothermiddlewareasnecessary...

$apiPipeline->pipe($callback());

return$apiPipeline;

}

}

TheabovewouldthenberegisteredasadelegatorwithyourListBooksReadservice:

ErrorHandlinginExpressive

58

//InAcme\BooksRead\ConfigProvider,oranyconfig/autoload/*.global.php:

return[

'dependencies'=>[

'delegators'=>[

\Acme\BooksRead\ListBooksRead::class=>[

\Acme\BooksRead\ApiMiddlewareDelegatorFactory::class,

],

],

]

];

EndresultOnceyouhavecreatedthepipeline,youshouldgetsomeniceerrors:

HTTP/1.1400ClientError

Content-Type:application/problem+json

{

"type":"https://example.com/api/problems/invalid-request",

"title":"Invalidsortdirectionspecified",

"detail":"Thesortdirectionspecifiedmustbeoneof[ASC,DESC]"

}

Thisapproachtoerrorhandlingallowsyoutobeasgranularorasgenericasyoulikewithregardstohowerrorsarehandled.Theshippederrorhandlertakesanall-or-nothingapproach,handlingbothPHPerrorsandexceptions/throwables,buttreatingthemallthesame.Bysprinklingmorespecificerrorhandlersintoyourroutedmiddlewarepipelines,youcanhavemorecontroloverhowyourapplicationbehaves,basedonthecontextinwhichitexecutes.

WhilethisarticledemonstratesanapproachtobuildingerrormiddlewareforreportinginProblemDetailsformat,youwilllikelywanttocheckouttheofficialofferingviathezendframework/zend-problem-detailspackage.WedetailthatinthechapterRESTRepresentationsforExpressive.

Footnotes

.https://docs.zendframework.com/zend-expressive/features/error-handling/↩

.https://tools.ietf.org/html/rfc7807↩

1

2

ErrorHandlinginExpressive

59

ErrorHandlinginExpressive

60

UsingConfiguration-DrivenRoutesinExpressivebyMatthewWeierO'Phinney

Expressive1usedconfiguration-drivenpipelinesandrouting;Expressive2switchestouseprogrammaticpipelinesandroutesinstead.Theprogrammaticapproachwaschosenasmanydevelopershaveindicatedtheyfinditeasiertounderstandandeasiertoread,andensurestheydonothaveanyconfigurationconflicts.

However,therearetimesyoumaywanttouseconfiguration.Forexample,whenyouarewritingre-usablemodules,it'softeneasiertoprovideconfigurationforroutedmiddleware,thantoexpectuserstocut-and-pasteexamples,orusefeaturessuchasdelegatorfactories .

Fortunately,startinginExpressive2,weofferacoupledifferentmechanismstosupportconfiguration-drivenpipelinesandrouting.

ConfigurationOnlyBydefaultinExpressive2,andifyouruntheexpressive-pipeline-from-configtooltomigratefromv1tov2,weenableaspecificflagtoforceusageofprogrammaticpipelines:

//Withinconfig/autoload/zend-expressive.global.phpinv2,

//andconfig/autoload/programmatic-pipeline.global.phpforv1projectsthat

//migrateusingthetooling:

return[

'zend-expressive'=>[

'programmatic_pipeline'=>true,

],

];

Byremovingthissetting,ortogglingittofalse,youcangobacktotheoriginalExpressive1behaviorwherebythepipelineandroutingarecompletelygeneratedbyconfiguration.YoucanreadthedocumentationontheApplicationFactory fordetailsonhowtoconfigurethepipelineandroutesinthissituation.

1

2

UsingConfiguration-DrivenRoutesinExpressive

61

Beware!

Ifyoualsohaveprogrammaticdeclarationsinyourconfig/pipeline.phpand/orconfig/routes.phpfiles,andthesearestillincludedfromyourpublic/index.php,youmayrunintoconflictswhenyoudisableprogrammaticpipelines!Commentouttherequirelinesinyourpublic/index.phpaftertogglingtheconfigurationvaluetobesafe!

Thekeyadvantagetousingconfigurationisthatyoucanoverrideconfigurationbyprovidingconfig/autoload/*.local.phpfiles;thisgivestheabilitytosubstitutedifferentmiddlewarewhendesired.Thatsaid,ifyouusearraysofmiddlewaretocreatecustompipelines,configurationoverridingmaynotworkasexpected.

SelectiveConfigurationThereareafewdrawbackstogoingconfiguration-only:

Mostpipelineswillbestatic.Configurationismoreverbosethanprogrammaticdeclarations.

Fortunately,startingwithExpressive2,youcancombinethetwoapproaches,duetotheadditionoftwomethodstoZend\Expressive\Application:

publicfunctioninjectPipelineFromConfig(array$config=null):void;

publicfunctioninjectRoutesFromConfig(array$config=null):void;

(Ineachcase,ifpassednovalues,theywillusetheconfigservicecomposedinthecontainertheApplicationinstanceuses.)

InthecaseofinjectPipelineFromConfig(),themethodpullsthemiddleware_pipelinevaluefromthepassedconfiguration;injectRoutesFromConfig()pullsfromtheroutesvalue.

Wherewouldyouusethis?

OneplacetouseitiswhenmodulesprovideroutingintheirConfigProvider.Forinstance,let'ssayIhaveaBooksApi\ConfigProviderclassthatreturnsarouteskeywiththedefaultsetofroutesIfeelshouldbedefined:

<?php

//insrc/BooksApi/ConfigProvider.php:

namespaceBooksApi;

classConfigProvider

UsingConfiguration-DrivenRoutesinExpressive

62

{

publicfunction__invoke():array

{

return[

'dependencies'=>$this->getDependencies(),

'routes'=>$this->getRoutes(),

];

}

publicfunctiongetDependencies():array

{

//...

}

publicfunctiongetRoutes():array

{

return[

[

'name'=>'books'

'path'=>'/api/books',

'middleware'=>Action\ListBooks::class,

'allowed_methods'=>['GET'],

],

[

'path'=>'/api/books',

'middleware'=>Action\CreateBook::class,

'allowed_methods'=>['POST'],

],

[

'name'=>'book'

'path'=>'/api/books/{id:\d+}',

'middleware'=>Action\DisplayBook::class,

'allowed_methods'=>['GET'],

],

[

'path'=>'/api/books/{id:\d+}',

'middleware'=>Action\UpdateBook::class,

'allowed_methods'=>['PATCH'],

],

[

'path'=>'/api/books/{id:\d+}',

'middleware'=>Action\DeleteBook::class,

'allowed_methods'=>['DELETE'],

],

];

}

}

IfI,asanapplicationdeveloper,feelthosedefaultsdonotconflictwithmyapplication,Icoulddothefollowingwithinmyconfig/routes.phpfile:

UsingConfiguration-DrivenRoutesinExpressive

63

<?php

//config/routes.php:

$app->get('/',App\HomePageAction::class,'home');

$app->injectRoutesFromConfig((newBooksApi\ConfigProvider())());

//...

ByinvokingtheBooksApi\ConfigProvider,IcanbeassuredI'monlyinjectingthoseroutesdefinedbythatgivenmodule,andnotallroutesdefinedanywhereinmyconfiguration.I'vealsosavedmyselfafairbitofcopy-pasta!

Caution:pipelines

Wedonotrecommendmixingprogrammaticandconfiguration-drivenpipelines,duetoissuesofordering.

Whenyoucreateaprogrammaticpipeline,thepipelineiscreatedinexactlytheorderinwhichyoudeclareit:

$app->pipe(OriginalMessages::class);

$app->pipe(XClacksOverhead::class);

$app->pipe(ErrorHandler::class);

$app->pipe(ServerUrlMiddleware::class);

$app->pipeRoutingMiddleware();

$app->pipe(ImplicitHeadMiddleware::class);

$app->pipe(ImplicitOptionsMiddleware::class);

$app->pipe(UrlHelperMiddleware::class);

$app->pipeDispatchMiddleware();

$app->pipe(NotFoundHandler::class);

Inotherwords,whenyoulookatthepipeline,youknowimmediatelywhattheoutermostmiddlewareis,andthepathtotheinnermostmiddleware.

Configuration-drivenmiddlewareallowsyoutospecifypriorityvaluestospecifytheorderinwhichmiddlewareispiped;highervaluesarepipedearlies,lowest(includingnegative!)valuesarepipedlast.

Whathappenswhenyoumixthesystems?Itdependsonwhenyouinjectconfiguration-drivenmiddleware:

UsingConfiguration-DrivenRoutesinExpressive

64

//Middlewarefromconfigurationappliesfirst:

$app->injectPipelineFromConfig($pipelineConfig)

$app->pipe(/*...*/);

//Middlewarefromconfigurationapplieslast:

$app->pipe(/*...*/);

$app->injectPipelineFromConfig($pipelineConfig)

//Ormixitup?

$app->pipe(/*...*/);

$app->injectPipelineFromConfig($pipelineConfig)

$app->pipe(/*...*/);

Thiscanleadtosometrickysituations.Wesuggeststickingtooneortheother,toensureyoucanfullyvisualizetheentirepipelineatonce.

SummaryThenewApplication::injectRoutesFromConfig()methodofferedinExpressive2providesyouwithausefultoolforprovidingroutingwithinyourExpressivemodules.

Thisisnottheonlywaytoproviderouting,however.Wedetailanotherapproachtoautowiringroutesinthemanual thatprovidesawaytokeeptheprogrammaticapproach,bydecoratinginstantiationoftheApplicationinstance.

WehopethisopenssomecreativeroutingpossibilitiesforExpressivedevelopers,particularlythosecreatingreusablemodules!

Footnotes

.https://docs.zendframework.com/zend-expressive/cookbook/autowiring-routes-and-pipelines/#delegator-factories↩

.https://docs.zendframework.com/zend-expressive/features/container/factories/#applicationfactory↩

.https://docs.zendframework.com/zend-expressive/cookbook/autowiring-routes-and-pipelines/↩

3

1

2

3

UsingConfiguration-DrivenRoutesinExpressive

65

HandlingOPTIONSandHEADRequestswithExpressivebyMatthewWeierO'Phinney

Inv1releasesofExpressive,ifyoudidnotdefineroutesthatincludedtheOPTIONSorHEADHTTPrequestmethods,routingwouldresultin404NotFoundstatuses,evenifaspecifiedroutematchedthegivenURI.RFC7231 ,however,statesthatbothoftheserequestmethodsSHOULDworkforagivenresourceURI,solongasitexistsontheserver.Thisleftusersinabitofabind:iftheywantedtocomplywiththespecification(whichisoftennecessarytoworkcorrectlywithHTTPclientsoftware),theywouldneedtoeither:

injectadditionalroutesforhandlingthesemethods,oroverloadexistingmiddlewaretoalsoacceptthesemethods.

InthecaseofaHEADrequest,thespecificationindicatesthattheresultingresponseshouldbeidenticaltothatofaGETrequesttothesameURI,onlywithnobodycontent.Thiswouldmeanhavingthesameresponseheaders.

InthecaseofanOPTIONSrequest,typicallyyouwouldrespondwitha200OKresponsestatus,andatleastanAllowheaderindicatingwhatHTTPrequestmethodstheresourceallows.

Soundslikethesecouldbeautomated,doesn'tit?

InExpressive2,wedid!

HandlingHEADrequestsIfyouareusingthev2releaseoftheExpressiveskeleton,orhaveusedtheexpressive-pipeline-from-configtooltomigrateyourapplicationtov2,thenyoualreadyhavesupportforimplicitlyaddingHEADsupporttoyourroutes.Ifnot,pleasegoreadthedocumentation .

Asnotedinthedocumentation,thesupportisprovidedbyZend\Expressive\Middleware\ImplicitHeadMiddleware,anditoperates:

IftherequestmethodisHEAD,ANDtherequestcomposesaRouteResultattribute,ANDtherouteresultcomposesaRouteinstance,ANDtheroutereturnstruefortheimplicitHead()method,THENthemiddlewarewillreturnaresponse.

1

2

HandlingOPTIONSandHEADRequestswithExpressive

66

WhenthematchedroutesupportstheGETmethod,itwilldispatchit,andtheninjectthereturnedresponsewithanemptybodybeforereturningit;thispreservestheoriginalresponseheaders,allowingittooperateperRFC7231asdescribedabove.IfGETisnotsupported,itsimplyreturnsanemptyresponse.

WhatifyouwanttocustomizewhathappenswhenHEADiscalledforagivenroute?

That'seasy:registercustommiddleware!Asasimple,inlineexample:

//Inconfig/routes.php:

useInterop\Http\ServerMiddleware\MiddlewareInterface;

useInterop\Http\ServerMiddleware\DelegateInterface;

usePsr\Http\Message\ServerRequestInterface;

useZend\Diactoros\Response\EmptyResponse;

$app->route(

'/foo',

newclassimplementsMiddlewareInterface

{

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$de

legate)

{

//Returnacustom,emptyresponse

$response=newEmptyResponse(200,[

'X-Foo'=>'Bar',

]);

}

},

['HEAD']

);

HandlingOPTIONSrequestsLikeHEADrequestsabove,ifyou'reusingExpressive2,themiddlewareforimplicitlyhandlingOPTIONSrequestsisalreadyenabled;ifnot,pleasegoreadthedocumentation .

OPTIONSrequestsarehandledbyZend\Expressive\Middleware\ImplicitOptionsMiddleware,which:

IftherequestmethodisOPTIONS,ANDtherequestcomposesaRouteResultattribute,ANDtherouteresultcomposesaRouteinstance,ANDtheroutereturnstruefortheimplicitOptions()method,THENthemiddlewarewillreturnaresponsewithanAllowheaderindicatingmethodstherouteallows.

3

HandlingOPTIONSandHEADRequestswithExpressive

67

TheExpressivecontributorsworkedtoensurethisisconsistentacrosssupportedrouterimplementations;beaware,however,thatifyouareusingacustomrouter,it'spossiblethatthismayresultinAllowheadersthatonlycontainasubsetofallallowedHTTPmethods.

WhathappensifyouwanttoprovideacustomOPTIONSresponse?Forexample,anumberofprominentAPIdeveloperssuggesthavingOPTIONSpayloadswithusageinstructions,suchasthis:

HandlingOPTIONSandHEADRequestswithExpressive

68

HTTP/1.1200OK

Allow:GET,POST

Content-Type:application/json

{

"GET":{

"query":{

"page":"int;pageofresultstoreturn",

"per_page":"int;numberofresultstoreturnperpage"

},

"response":{

"total":"Totalnumberofitems",

"count":"Totalnumberofitemsreturnedonthispage",

"_links":{

"self":"URItocollection",

"first":"URItofirstpageofresults",

"prev":"URItopreviouspageofresults",

"next":"URItonextpageofresults",

"last":"URItolastpageofresults",

"search":"URItemplateforsearching"

},

"_embedded":{

"books":[

"See...fordetails"

]

}

}

},

"POST":{

"data":{

"title":"string;titleofbook",

"author":"string;authorofbook",

"info":"string;bookdescriptionandnotes"

},

"response":{

"_links":{

"self":"URItobook"

},

"id":"string;generatedUUIDforbook",

"title":"string;titleofbook",

"author":"string;authorofbook",

"info":"string;bookdescriptionandnotes"

}

}

}

TheansweristhesameaswithHEADrequests:registeracustomroute!

<?php

//Inconfig/routes.php:

HandlingOPTIONSandHEADRequestswithExpressive

69

useInterop\Http\ServerMiddleware\MiddlewareInterface;

useInterop\Http\ServerMiddleware\DelegateInterface;

usePsr\Http\Message\ServerRequestInterface;

useZend\Diactoros\Response\JsonResponse;

$app->route(

'/books',

newclassimplementsMiddlewareInterface

{

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$de

legate)

{

//Returnacustomresponse

$response=newJsonResponse([

'GET'=>[

'query'=>[

'page'=>'int;pageofresultstoreturn',

'per_page'=>'int;numberofresultstoreturnperpage',

],

'response'=>[

'total'=>'Totalnumberofitems',

'count'=>'Totalnumberofitemsreturnedonthispage',

'_links'=>[

'self'=>'URItocollection',

'first'=>'URItofirstpageofresults',

'prev'=>'URItopreviouspageofresults',

'next'=>'URItonextpageofresults',

'last'=>'URItolastpageofresults',

'search'=>'URItemplateforsearching',

],

'_embedded'=>[

'books'=>[

'See...fordetails',

],

],

],

],

'POST'=>[

'data'=>[

'title'=>'string;titleofbook',

'author'=>'string;authorofbook',

'info'=>'string;bookdescriptionandnotes',

],

'response'=>[

'_links'=>[

'self'=>'URItobook',

],

'id'=>'string;generatedUUIDforbook',

'title'=>'string;titleofbook',

'author'=>'string;authorofbook',

'info'=>'string;bookdescriptionandnotes',

],

],

HandlingOPTIONSandHEADRequestswithExpressive

70

],200,['Allow'=>'GET,POST']);

}

},

['OPTIONS']

);

FinalwordObviously,youmaynotwanttouseinlineclassesasdescribedabove,buthopefullywiththeaboveexamples,youcanbegintoseethepossibilitiesforhandlingHEADandOPTIONSrequestsinExpressive.Thesimplestoption,whichwilllikelysufficeforthemajorityofusecases,isnowbuilt-intotheskeleton,andaddedbydefaultwhenusingthemigrationtools.Forthoseothercaseswhereyouneedfurthercustomization,Expressive'sroutingcapabilitiesgiveyoutheflexibilityandpowertoaccomplishwhateveryoumightneed.

Formoreinformationonthebuilt-incapabilities,visitthedocumentation .

Footnotes

.https://tools.ietf.org/html/rfc7231↩

.https://docs.zendframework.com/zend-expressive/features/middleware/implicit-methods-middleware/#implicitheadmiddleware↩

.https://docs.zendframework.com/zend-expressive/features/middleware/implicit-methods-middleware/#implicitoptionsmiddleware↩

.https://docs.zendframework.com/zend-expressive/features/middleware/implicit-methods-middleware/↩

4

1

2

3

4

HandlingOPTIONSandHEADRequestswithExpressive

71

CachingmiddlewarewithExpressivebyEnricoZimuel

Performanceisoneofthekeyfeatureforwebapplication.UsingamiddlewarearchitecturemakesitverysimpletoimplementacachingsysteminPHP.

ThegeneralideaistostoretheresponseoutputofaURLinafile(orinmemory,usingmemcached )anduseitforsubsequentrequests.Inthiswaywecanbypasstheexecutionofmiddlewarenestedfurtherinthepipelinestartingfromthesecondrequest.

Ofcourse,thistechniquecanonlybeappliedforstaticcontentthatdoesnotchangebetweenrequests.

ImplementacachingmiddlewareImaginewewanttocreateasimplecachesystemwithExpressive.Wecanuseanimplementationlikethefollowing:

1

Cachingmiddleware

72

namespaceApp\Action;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterfaceasServerMiddlewareInterface;

usePsr\Http\Message\ServerRequestInterface;

useZend\Diactoros\Response\HtmlResponse;

classCacheMiddlewareimplementsServerMiddlewareInterface

{

private$config;

publicfunction__construct(array$config)

{

$this->config=$config;

}

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

{

$url=str_replace('/','_',$request->getUri()->getPath());

$file=$this->config['path'].$url.'.html';

if($this->config['enabled']&&file_exists($file)&&

(time()-filemtime($file))<$this->config['lifetime']){

returnnewHtmlResponse(file_get_contents($file));

}

$response=$delegate->process($request);

if($responseinstanceofHtmlResponse&&$this->config['enabled']){

file_put_contents($file,$response->getBody());

}

return$response;

}

}

Theideaofthismiddlewareisquitesimple.IfthecachingsystemisenabledandiftherequestedURLmatchesanexistingcachefile,wereturnthecachecontentasanHtmlResponse ,endingtheexecutionflow.

IftherequestedURLpathdoesnotexistincache,wedelegatetothenextmiddlewareinourqueue,and,ifcachingisenabled,cachetheresponsebeforereturningit.

ConfiguringthecachesystemTomanagethecache,weusedaconfigurationkeycachetospecifythepathofthecachefiles,thelifetimeinsecondsandtheenabledvaluetoturnthecachingsystemonandoff.

2

Cachingmiddleware

73

Sinceweuseafiletostorethecachecontent,wecanusethefilemodificationtimetomanagethelifetimeofthecacheviathePHPfunctionfilemtime() .

Note:ifyouwanttousememcachedinsteadofthefilesystem,youneedtoreplacethefile_get_contents()andfile_put_contents()functionswithMemcached::get()andMemcached::set().Moreover,youdonotneedtocheckforlifetimebecausewhenyouwillinsteadsetanexpirationtimewhenpushingcontenttomemcached.

Inordertopassthe$configdependency,wewillcreateafactoryclass:

namespaceApp\Action;

useInterop\Container\ContainerInterface;

useException;

classCacheFactory

{

publicfunction__invoke(ContainerInterface$container)

{

$config=$container->get('config');

$config=$config['cache']??[];

if(!array_key_exists('enabled',$config)){

$config['enabled']=false;

}

if($config['enabled']){

if(!isset($config['path'])){

thrownewException('Thecachepathisnotconfigured');

}

if(!isset($config['lifetime'])){

thrownewException('Thecachelifetimeisnotconfigured');

}

}

returnnewCacheMiddleware($config);

}

}

WecanstorethisconfigurationinaplainPHPfileintheconfig/autoload/directory;forexample,wecouldputitinconfig/autoload/cache.local.phpwiththefollowingcontents:

return[

'cache'=>[

'enabled'=>true,

'path'=>'data/cache/',

'lifetime'=>3600//inseconds

]

];

3

Cachingmiddleware

74

Intheabove,wespecifydata/cache/asthecachedirectory;sinceExpressivesetstheworkingdirectorytotheapplicationroot,thiswill,resolvetothatlocation.

Thecontentofthisfoldershouldbeomittedfromyourversioncontrol;asanexample,withGit,youcanplacea.gitignorefileinsidethecachefolderwiththefollowingcontent:

*

!.gitignore

Next,inordertoactivatethecachingsystem,weneedtoaddtheCacheMiddlewareclassasservice.Wedothatviaaglobalconfigurationfilesuchas/config/autoload/cache.global.phpwiththefollowingcontent:

return[

'dependencies'=>[

'factories'=>[

App\Action\CacheMiddleware::class=>App\Action\CacheFactory::class

]

]

];

EnablingcachingforspecificroutesWementionedearlierthatthiscachingmechanismonlyworksforstaticcontent.Thatmeansweneedawaytoenablethecacheonlyforspecificroutes.Wedothisbyspecifyinganarrayofmiddlewareforthoseroutes,andaddingtheCacheMiddlewareclassasfirstmiddlewaretobeexecutedinthoseroutes.

Forinstance,imaginewehavean/aboutroutethatdisplaysan"About"pageforyourwebsite.WecanaddtheCacheMiddlewareasfollows:

useApp\Action;

$app->get('/about',[

Action\CacheMiddleware::class,

Action\AboutAction::class

],'about');

BecausetheCacheMiddlewareappearsfirstinthearray,itwillexecutefirst,deliveringthecachedcontentsafterthefirstrequesttotheroute.(The$appobjectisaninstanceofZend\Expressive\Application.)

Cachingmiddleware

75

ConclusionInthisarticle,wedemonstratedbuildingalightweigthcachingsystemforaPHPmiddlewareapplication.AmiddlewarearchitecturefacilitatesthedesignofacachelayerbecauseitallowscomposingmiddlewaretocreateanHTTPrequestworkflow.

Footnotes

.https://memcached.org↩

.https://docs.zendframework.com/zend-diactoros/custom-responses/#html-responses↩

.http://php.net/filemtime↩

1

2

3

Cachingmiddleware

76

MiddlewareauthenticationbyEnricoZimuel

Manywebapplicationsrequirerestrictingspecificareastoauthenticatedusers,andmayfurtherrestrictspecificactionstoauthorizeduserroles.ImplementingauthenticationandauthorizationinaPHPapplicationisoftennon-trivialasdoingsorequiresalteringtheapplicationworkflow.Forinstance,ifyouhaveanMVCdesign,youmayneedtochangethedispatchlogictoaddanauthenticationlayerasaninitialeventintheexecutionflow,andperhapsapplyrestrictionswithinyourcontrollers.

Usingamiddlewareapproachissimplerandmorenatural,asmiddlewareeasilyaccommodatesworkflowchanges.Inthisarticle,wewilldemonstratehowtoprovideauthenticationinaPSR-7middlewareapplicationusingExpressiveandzend-authentication .Wewillbuildasimpleauthenticationsystemusingaloginpagewithusernameandpasswordcredentials.

Wedetailauthorizationinthenextarticle.

GettingstartedThisarticleassumesyouhavealreadycreatedanExpressiveapplication.Forthepurposesofourapplication,we'llcreateanewmodule,Auth,inwhichwe'llputourclasses,middleware,andgeneralconfiguration.

First,ifyouhavenotalready,installthetoolingsupport:

$composerrequire--devzendframework/zend-expressive-tooling

Next,we'llcreatetheAuthmodule:

$./vendor/bin/expressivemodule:createAuth

Withthatoutoftheway,wecangetstarted.

Authentication

1

Middlewareauthentication

77

Thezend-authenticationcomponentoffersanadapter-basedauthenticationsolution,withbothanumberofconcreteadaptersaswellasmechanismsforcreatingandconsumingcustomadapters.

ThecomponentexposesZend\Authentication\Adapter\AdapterInterface,whichdefinesasingleauthenticate()method:

namespaceZend\Authentication\Adapter;

interfaceAdapterInterface

{

/**

*Performsanauthenticationattempt

*

*@return\Zend\Authentication\Result

*@throwsException\ExceptionInterfaceifauthenticationcannotbeperformed

*/

publicfunctionauthenticate();

}

Adaptersimplementingtheauthenticate()methodperformthelogicnecessarytoauthenticatearequest,andreturntheresultsviaaZend\Authentication\Resultobject.ThisResultobjectcontainstheauthenticationresultcodeand,inthecaseofsuccess,theuser'sidentity.Theauthenticationresultcodesaredefinedusingthefollowingconstants:

namespaceZend\Authentication;

classResult

{

constSUCCESS=1;

constFAILURE=0;

constFAILURE_IDENTITY_NOT_FOUND=-1;

constFAILURE_IDENTITY_AMBIGUOUS=-2;

constFAILURE_CREDENTIAL_INVALID=-3;

constFAILURE_UNCATEGORIZED=-4;

}

Ifwewanttoimplementaloginpagewithusernameandpasswordauthentication,wecancreateacustomadaptersuchasthefollowing:

Middlewareauthentication

78

//Insrc/Auth/src/MyAuthAdapter.php:

namespaceAuth;

useZend\Authentication\Adapter\AdapterInterface;

useZend\Authentication\Result;

classMyAuthAdapterimplementsAdapterInterface

{

private$password;

private$username;

publicfunction__construct(/*anydependencies*/)

{

//Likelyassigndependenciestoproperties

}

publicfunctionsetPassword(string$password):void

{

$this->password=$password;

}

publicfunctionsetUsername(string$username):void

{

$this->username=$username;

}

/**

*Performsanauthenticationattempt

*

*@returnResult

*/

publicfunctionauthenticate()

{

//Retrievetheuser'sinformation(e.g.fromadatabase)

//andstoretheresultin$row(e.g.associativearray).

//Ifyoudosomethinglikethis,alwaysstorethepasswordsusingthe

//PHPpassword_hash()function!

if(password_verify($this->password,$row['password'])){

returnnewResult(Result::SUCCESS,$row);

}

returnnewResult(Result::FAILURE_CREDENTIAL_INVALID,$this->username);

}

}

Wewillwantafactoryforthisserviceaswell,sothatwecanseedtheusernameandpasswordtoitlater:

Middlewareauthentication

79

//Insrc/Auth/src/MyAuthAdapterFactory.php:

namespaceAuth;

useInterop\Container\ContainerInterface;

useZend\Authentication\AuthenticationService;

classMyAuthAdapterFactory

{

publicfunction__invoke(ContainerInterface$container)

{

//Retrieveanydependenciesfromthecontainerwhencreatingtheinstance

returnnewMyAuthAdapter(/*anydependencies*/);

}

}

ThisfactoryclasscreatesandreturnsaninstanceofMyAuthAdapter.Wemayneedtopasssomedependenciestoitsconstructor,suchasadatabaseconnection;thesewouldbefetchedfromthecontainer.

AuthenticationServiceWecannowcreateaZend\Authentication\AuthenticationServicethatcomposesouradapter,andthenconsumetheAuthenticationServiceinmiddlewaretocheckforavaliduser.Let'snowcreateafactoryfortheAuthenticationService:

//insrc/Auth/src/AuthenticationServiceFactory.php:

namespaceAuth;

useInterop\Container\ContainerInterface;

useZend\Authentication\AuthenticationService;

classAuthenticationServiceFactory

{

publicfunction__invoke(ContainerInterface$container)

{

returnnewAuthenticationService(

null,

$container->get(MyAuthAdapter::class)

);

}

}

ThisfactoryclassretrievesaninstanceoftheMyAuthAdapterserviceanduseittoreturnanAuthenticationServiceinstance.TheAuthenticationServiceclassacceptstwoparameters:

Middlewareauthentication

80

Astorageserviceinstance,forpersistingtheuseridentity.Ifnoneisprovided,thebuilt-inPHPsessionmechanismswillbeused.Theactualadaptertouseforauthentication.

Nowthatwehavecreatedboththecustomadapter,aswellasfactoriesfortheadapterandtheAuthenticationService,weneedtoconfigureourapplicationdependenciestousethem:

//Insrc/Auth/src/ConfigProvider.php:

//Addthefollowingimportstatementatthetopoftheclassfile:

useZend\Authentication\AuthenticationService;

//Andupdatethefollowingmethod:

publicfunctiongetDependencies()

{

return[

'factories'=>[

AuthenticationService::class=>AuthenticationServiceFactory::class,

MyAuthAdapter::class=>MyAuthAdapterFactory::class,

],

];

}

AuthenticateusingaloginpageWithanauthenticationmechanisminplace,wenowneedtocreatemiddlewaretorendertheloginform.Thismiddlewarewilldothefollowing:

forGETrequests,itwillrendertheloginform.forPOSTrequests,itwillcheckforcredentialsandthenattempttovalidatethem.

forvalidauthenticationrequests,wewillredirecttoawelcomepageforinvalidrequests,wewillprovideanerrormessageandredisplaytheform.

Let'screatethemiddlewarenow:

//Insrc/Auth/src/Action/LoginAction.php:

namespaceAuth\Action;

useAuth\MyAuthAdapter;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterfaceasServerMiddlewareInterface;

usePsr\Http\Message\ServerRequestInterface;

useZend\Authentication\AuthenticationService;

useZend\Diactoros\Response\HtmlResponse;

useZend\Diactoros\Response\RedirectResponse;

useZend\Expressive\Template\TemplateRendererInterface;

Middlewareauthentication

81

classLoginActionimplementsServerMiddlewareInterface

{

private$auth;

private$authAdapter;

private$template;

publicfunction__construct(

TemplateRendererInterface$template,

AuthenticationService$auth,

MyAuthAdapter$authAdapter

){

$this->template=$template;

$this->auth=$auth;

$this->authAdapter=$authAdapter;

}

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

{

if($request->getMethod()==='POST'){

return$this->authenticate($request);

}

returnnewHtmlResponse($this->template->render('auth::login'));

}

publicfunctionauthenticate(ServerRequestInterface$request)

{

$params=$request->getParsedBody();

if(empty($params['username'])){

returnnewHtmlResponse($this->template->render('auth::login',[

'error'=>'Theusernamecannotbeempty',

]));

}

if(empty($params['password'])){

returnnewHtmlResponse($this->template->render('auth::login',[

'username'=>$params['username'],

'error'=>'Thepasswordcannotbeempty',

]));

}

$this->authAdapter->setUsername($params['username']);

$this->authAdapter->setPassword($params['password']);

$result=$this->auth->authenticate();

if(!$result->isValid()){

returnnewHtmlResponse($this->template->render('auth::login',[

'username'=>$params['username'],

'error'=>'Thecredentialsprovidedarenotvalid',

]));

Middlewareauthentication

82

}

returnnewRedirectResponse('/admin');

}

}

Thismiddlewaremanagestwoactions:renderingtheloginform,andauthenticatingtheuser'scredentialswhensubmittedviaaPOSTrequest.

Youwillalsoneedtoensurethatyouhave:

Createdalogintemplate.Addedconfigurationtomaptheauthtemplatenamespacetooneormorefilesystempaths.

Weleavethosetasksasanexercisetothereader.

Wenowneedtocreateafactorytoprovidethedependenciesforthismiddleware:

//Insrc/Auth/src/Action/LoginActionFactory.php:

namespaceAuth\Action;

useAuth\MyAuthAdapter;

useInterop\Container\ContainerInterface;

useZend\Authentication\AuthenticationService;

useZend\Expressive\Template\TemplateRendererInterface;

classLoginActionFactory

{

publicfunction__invoke(ContainerInterface$container)

{

returnnewLoginAction(

$container->get(TemplateRendererInterface::class),

$container->get(AuthenticationService::class),

$container->get(MyAuthAdapter::class)

);

}

}

MapthemiddlewaretothisfactoryinyourdependenciesconfigurationwitintheConfigProvider:

Middlewareauthentication

83

//Insrc/Auth/src/ConfigProvider.php,

//Updatethefollowingmethodtoreadasfollows:

publicfunctiongetDependencies()

{

return[

'factories'=>[

Action\LoginAction::class=>Action\LoginActionFactory::class,

AuthenticationService::class=>AuthenticationServiceFactory::class,

MyAuthAdapter::class=>MyAuthAdapterFactory::class,

],

];

}

Usezend-servicemanager'sReflectionBasedAbstractFactory

Ifyouareusingzend-servicemanagerinyourapplication,youcouldskipthestepofcreatingthefactory,andinsteadmapthemiddlewaretoZend\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory.

Finally,wecancreateappropriateroutes.We'llmap/logintotheLoginActionnow,andallowittoreacttoeithertheGETorPOSTmethods:

//inconfig/routes.php:

$app->route('/login',Auth\Action\LoginAction::class,['GET','POST'],'login');

Alternately,theabovecouldbewrittenastwoseparatestatements:

//inconfig/routes.php:

$app->get('/login',Auth\Action\LoginAction::class,'login');

$app->post('/login',Auth\Action\LoginAction::class);

AuthenticationmiddlewareNowthatwehavetheauthenticationserviceanditsadapterandtheloginmiddlewareinplace,wecancreatemiddlewarethatchecksforauthenticatedusers,havingitredirecttothe/loginpageiftheuserisnotauthenticated.

Middlewareauthentication

84

//Insrc/Auth/src/Action/AuthAction.php:

namespaceAuth\Action;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterfaceasServerMiddlewareInterface;

usePsr\Http\Message\ServerRequestInterface;

useZend\Authentication\AuthenticationService;

useZend\Diactoros\Response\RedirectResponse;

classAuthActionimplementsServerMiddlewareInterface

{

private$auth;

publicfunction__construct(AuthenticationService$auth)

{

$this->auth=$auth;

}

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

{

if(!$this->auth->hasIdentity()){

returnnewRedirectResponse('/login');

}

$identity=$this->auth->getIdentity();

return$delegate->process($request->withAttribute(self::class,$identity));

}

}

ThismiddlewarechecksforavalididentityusingthehasIdentity()methodofAuthenticationService.Ifnoidentityispresent,weredirecttheredirectconfigurationvalue.

Iftheuserisauthenticated,wecontinuetheexecutionofthenextmiddleware,storingtheidentityinarequestattribute.Thisfacilitatesconsumptionoftheidentityinformationinsubsequentmiddlewarelayers.Forinstance,imagineyouneedtoretrievetheuser'sinformation:

Middlewareauthentication

85

namespaceApp\Action;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterfaceasServerMiddlewareInterface;

usePsr\Http\Message\ServerRequestInterface;

classFooAction

{

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

{

$user=$request->getAttribute(AuthAction::class);

//$userwillcontainstheuser'sidentity

}

}

TheAuthActionmiddlewareneedssomedependencies,sowewillneedtocreateandregisterafactoryforitaswell.

First,thefactory:

//Insrc/Auth/src/Action/AuthActionFactory.php:

namespaceAuth\Action;

useInterop\Container\ContainerInterface;

useZend\Authentication\AuthenticationService;

useException;

classAuthActionFactory

{

publicfunction__invoke(ContainerInterface$container)

{

returnnewAuthAction($container->get(AuthenticationService::class));

}

}

Andthenmappingit:

Middlewareauthentication

86

//Insrc/Auth/src/ConfigProvider.php:

//Updatethefollowingmethodtoreadasfollows:

publicfunctiongetDependencies()

{

return[

'factories'=>[

Action\AuthAction::class=>Action\AuthActionFactory::class,

Action\LoginAction::class=>Action\LoginActionFactory::class,

AuthenticationService::class=>AuthenticationServiceFactory::class,

MyAuthAdapter::class=>MyAuthAdapterFactory::class,

],

];

}

LiketheLoginActionFactoryabove,youcouldskipthefactorycreationandinsteadusetheReflectionBasedAbstractFactoryifusingzend-servicemanager.

RequireauthenticationforspecificroutesNowthatwebuilttheauthenticationmiddleware,wecanuseittoprotectspecificroutesthatrequireauthentication.Forinstance,foreachroutethatneedsauthentication,wecanmodifytheroutingtocreateapipelinethatincorporatesourAuthActionmiddlewareearly:

$app->get('/admin',[

Auth\Action\AuthAction::class,

App\Action\DashBoardAction::class

],'admin');

$app->get('/admin/config',[

Auth\Action\AuthAction::class,

App\Action\ConfigAction::class

],'admin.config');

Theorderofexecutionforthemiddlewareistheorderofthearrayelements.SincetheAuthActionmiddlewareisprovidedasthefirstelement,ifauserisnotauthenticatedwhenrequestingeithertheadmindashboardorconfigpage,theywillbeimmediatelyredirectedtotheloginpageinstead.

Conclusion

Middlewareauthentication

87

Therearemanywaystoaccommodateauthenticationwithinmiddlewareapplications;thisisjustone.Ourgoalwastodemonstratetheeasewithwhichyoumaycomposeauthenticationintoexistingworkflowsbycreatingmiddlewarethatinterceptstherequestearlywithinapipeline.

Youcouldcertainlymakeanumberofimprovementstotheworkflow:

Thepathtotheloginpagecouldbeconfigurable.Youcouldcapturetheoriginalrequestpathinordertoallowredirectingtoitfollowingsuccessfullogin.Youcouldintroduceratelimitingofloginrequests.

Theseareeachinterestingexercisesforyoutotry!

Footnotes

.https://docs.zendframework.com/zend-authentication↩1

Middlewareauthentication

88

AuthorizeusersusingMiddlewarebyEnricoZimuel

Inthepreviousarticle,wedemonstratedhowtoauthenticateamiddlewareapplicationinPHP.Inthispostwewillcontinuethediscussion,showinghowtomanageauthorization.

Wewillstartfromanauthenticateduseranddemonstratehowtoallowordisableactionsforspecificusers.WewillcollectusersbygroupsandwewilluseaRole-BasedAccessControl(RBAC)systemtomanagetheauthorizations.

ToimplementRBAC,wewillconsumezendframework/zend-permissions-rbac .

IfyouarenotfamiliarwithRBACandtheusageofzend-permissions-rbac,wecoverthetopiconourblog ,aswellasintheZendFramework3Cookbook.

GettingstartedThisarticleassumesyouhavealreadycreatedtheAuthmodule,asdescribedinourpreviousarticleonauthentication.Forthepurposesofourapplication,we'llcreateanewmodule,Permission,inwhichwe'llputourclasses,middleware,andgeneralconfiguration.

First,ifyouhavenotalready,installthetoolingsupport:

$composerrequire--devzendframework/zend-expressive-tooling

Next,we'llcreatethePermissionmodule:

$./vendor/bin/expressivemodule:createPermission

Withthatoutoftheway,wecangetstarted.

AuthenticationAsalreadymentioned,wewillreusetheAuthmodulecreatedinourpreviouspost.WewillreusetheAuth\Action\AuthAction::classtogettheauthenticateduser'sdata.

Authorization

1

2

AuthorizeusersusingMiddleware

89

Inordertomanageauthorization,wewilluseaRBACsystemusingtheuser'srole.Auser'sroleisastringthatrepresentsthepermissionlevel;asanexample,theroleadministratormightprovideaccesstoallpermissions.

Inourscenario,wewanttoallowordisableaccessofspecificroutestoaroleorsetofroles.EachrouterepresentsapermissioninRBACterminology.

Wecanusezendframework/zend-permissions-rbac tomanagetheRBACsystemusingaPHPconfigurationfilestoringthelistofrolesandpermissions.Usingzend-permissions-rbac,wecanalsomanagepermissionsinheritance.

Forinstance,imagineimplementingablogapplication;wemightdefinethefollowingroles:

administrator

editor

contributor

Acontributorcancreate,edit,anddeleteonlythepostscreatedbytheirself.Theeditorcancreate,edit,anddeleteallpostsandpublishposts(thatmeansenablingpublicviewofapostinthewebsite).Theadministratorcanperformallactions,includingchangingtheblog'sconfiguration.

Thisisaperfectusecaseforusingpermissioninheritance.Infact,theadministratorrolewouldinheritthepermissionsoftheeditor,andtheeditorroleinheritsthepermissionsofthecontributor.

Tomanagethepreviousscenario,wecanusethefollowingconfigurationfile:

3

AuthorizeusersusingMiddleware

90

//Insrc/Permission/config/rbac.php:

return[

'roles'=>[

'administrator'=>[],

'editor'=>['admin'],

'contributor'=>['editor'],

],

'permissions'=>[

'contributor'=>[

'admin.dashboard',

'admin.posts',

],

'editor'=>[

'admin.publish',

],

'administrator'=>[

'admin.settings',

],

],

];

Inthisfilewehavespecifiedthreeroles,includingtheinheritancerelationshipusinganarrayofrolenames.Theparentofadministatorisanemptyarray,meaningnoparents.

Thepermissionsareconfiguredusingthepermissionskey.Eachrolehasthelistofpermissions,specifiedwithanarrayofroutenames.

Alltherolescanaccesstherouteadmin.dashboardandadmin.posts.Theeditorrolecanalsoaccessadmin.publish.Theadministratorcanaccessalltherolesofcontributorandeditor.Moreover,onlytheadministratorcanaccesstheadmin.settingsroute.

WeusedtheroutenamesasRBACpermissionsbecauseinthiswaywecanallowURLandHTTPmethodsusingasingleresourcename.Moreover,inExpressivewehaveaconfig/routes.phpfilecontainingalltheroutesandwecaneasilyuseittoaddauthorization,aswedidforauthentication.

AuthorizationmiddlewareNowthatwehavetheRBACconfigurationinplace,wecancreateamiddlewarethatperformstheuserauthorizationverifications.

WecancreateanAuthorizationActionmiddlewareinourPermissionmoduleasfollows:

//insrc/Permission/src/Action/AuthorizationAction.php:

AuthorizeusersusingMiddleware

91

namespacePermission\Action;

useAuth\Action\AuthAction;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterfaceasMiddlewareInterface;

usePermission\Entity\PostasPostEntity;

usePermission\Service\PostService;

usePsr\Http\Message\ServerRequestInterface;

useZend\Diactoros\Response\EmptyResponse;

useZend\Expressive\Router\RouteResult;

useZend\Permissions\Rbac\AssertionInterface;

useZend\Permissions\Rbac\Rbac;

useZend\Permissions\Rbac\RoleInterface;

classAuthorizationActionimplementsMiddlewareInterface

{

private$rbac;

private$postService;

publicfunction__construct(Rbac$rbac,PostService$postService)

{

$this->rbac=$rbac;

$this->postService=$postService;

}

publicfunctionprocess(ServerRequestInterface$request,DelegateInterface$delega

te)

{

$user=$request->getAttribute(AuthAction::class,false);

if(false===$user){

returnnewEmptyResponse(401);

}

//ifapostattributeispresentanduseriscontributor

$postUrl=$request->getAttribute('post',false);

if(false!==$postUrl&&'contributor'===$user['role']){

$post=$this->postService->getPost($postUrl);

$assert=newclass($user['username'],$post)implementsAssertionInterfa

ce{

private$post;

private$username;

publicfunction__construct(string$username,PostEntity$post)

{

$this->username=$username;

$this->post=$post;

}

publicfunctionassert(Rbac$rbac)

{

return$this->username===$this->post->getAuthor();

}

};

AuthorizeusersusingMiddleware

92

}

$route=$request->getAttribute(RouteResult::class);

$routeName=$route->getMatchedRoute()->getName();

if(!$this->rbac->isGranted($user['role'],$routeName,$assert??null)){

returnnewEmptyResponse(403);

}

return$delegate->process($request);

}

}

Iftheuserisnotpresent,theAuthAction::classattributewillbefalse.Inthiscasewearereturninga401error,indicatingwehaveanunauthenticateduser,andhaltingexecution.

IfauserisreturnedfromAuthAction::classattribute,thismeansthatwehaveanauthenticateduser.

TheauthenticationisperformedbytheAuth\Action\AuthActionclassthatstorestheAuthAction::classattributeintherequest.Seethepreviousarticleformoreinformation.

ThismiddlewareperformstheauthorizationcheckusingisGranted($role,$permission)where$roleistheuser'srole($user['role'])and$permissionistheroutename,retrievedbytheRouteResult::classattribute.Iftheroleisgranted,wecontinuetheexecutionflowwiththedelegatemiddleware.Otherwise,westoptheexecutionwitha403error,indicatinglackofauthorization.

Wemanagealsothecasewhentheuserisacontributorandthereisapostattributeintherequest(e.g./admin/posts/{post}).Thatmeanssomeoneisperformingsomeactiononaspecificpost.Toperformthisaction,werequirethattheownerofthepostshouldbethesameastheauthenticateduser.

Thiswillpreventacontributortochangethecontentofapostifhe/sheisnottheauthor.Wemanagedthisspecialcaseusingadynamicassertion ,builtusingananonymousclass;itchecksiftheauthenticatedusernameisthesameoftheauthor'spost.WeusedageneralPostEntityclasswithagetAuthor()function.

Inordertoretrievefortheroutename,weusedtheRouteResult::classattributeprovidedbyExpressive.Thisattributefacilitatesaccesstothematchedroute.

TheAuthorizationActionmiddlewarerequirestheRbacandthePostServicedependencies.ThefirstisaninstanceofZend\Permissions\Rbac\Rbacandthesecondisageneralservicetomanageblogposts,i.e.aclassthatperformssomelookuptoretrievethepostdatafromadatabase.

4

AuthorizeusersusingMiddleware

93

Toinjectthesedependencies,weuseanAuthorizationFactorylikethefollowing:

namespacePermission\Action;

useInterop\Container\ContainerInterface;

useZend\Permissions\Rbac\Rbac;

useZend\Permissions\Rbac\Role;

usePermission\Service\PostService;

useException;

classAuthorizationFactory

{

publicfunction__invoke(ContainerInterface$container)

{

$config=$container->get('config');

if(!isset($config['rbac']['roles'])){

thrownewException('Rbacrolesarenotconfigured');

}

if(!isset($config['rbac']['permissions'])){

thrownewException('Rbacpermissionsarenotconfigured');

}

$rbac=newRbac();

$rbac->setCreateMissingRoles(true);

//rolesandparents

foreach($config['rbac']['roles']as$role=>$parents){

$rbac->addRole($role,$parents);

}

//permissions

foreach($config['rbac']['permissions']as$role=>$permissions){

foreach($permissionsas$perm){

$rbac->getRole($role)->addPermission($perm);

}

}

$post=$container->get(PostService::class);

returnnewAuthorizationAction($rbac,$post);

}

}

ThisfactoryclassbuildstheRbacobjectusingtheconfigurationfilestoredinsrc/Permission/config/rbac.php.Wereadalltherolesandthepermissionsfollowingtheorderinthearray.ItisimportanttoenablethecreationofmissingrolesintheRbacobjectusingthefunctionsetCreateMissingRoles(true).Thisisrequiredtobesuretocreatealltherolesevenifweadditoutoforder.Forinstance,withoutthissetting,thefollowingconfigurationwillthrowanexception:

AuthorizeusersusingMiddleware

94

return[

'roles'=>[

'contributor'=>['editor'],

'editor'=>['administrator'],

'administrator'=>[],

],

];

becausetheeditorandtheadministratorrolesarespecifiedasparentsofotherrolesbeforetheywerecreated.

Finally,wecanconfigurethePermissionmoduleaddingthefollowingdependencies:

//Insrc/Permission/src/ConfigProvider.php:

//Updatethefollowingmethods:

publicfunction__invoke()

{

return[

'dependencies'=>$this->getDependencies(),

'rbac'=>include__DIR__.'/../config/rbac.php',

];

}

publicfunctiongetDependencies()

{

return[

'factories'=>[

Service\PostService::class=>Service\PostFactory::class,

Action\AuthorizationAction::class=>Action\AuthorizationFactory::class,

],

];

}

ConfiguretherouteforauthorizationToenableauthorizationonaspecificroute,weneedtoaddthePermission\Action\AuthorizationActionmiddlewareintheroute,asfollows:

$app->get('/admin/dashboard',[

Auth\Action\AuthAction::class,

Permission\Action\AuthorizationAction::class,

Admin\Action\DashboardAction::class

],'admin.dashboard');

AuthorizeusersusingMiddleware

95

ThisisanexampleoftheGET/admin/dashboardroutewithadmin.dashboardasthename.WeaddAuthActionandAuthorizationActionbeforeexecutionoftheDashboardAction.Theorderofthemiddlewarearrayisimportant;authenticationmusthappenfirst,andauthorizationmusthappenbeforeexecutingthedashboardmiddleware.

AddtheAuthorizationActionmiddlewaretoallroutesrequiringauthorization.

ConclusionThisarticle,togetherwiththeprevious,demonstrateshowtoaccomodateauthenticationandauthorizationwithinmiddlewareinPHP.

WedemonstatedhowtocreatetwoseparateExpressivemodules,AuthandPermission,toprovideauthenticationandauthorizationusingzend-authenticationandzend-permissions-rbac.

Wealsoshowedtheusageofadynamicassertionforspecificpermissionsbasedontheroleandusernameofanauthenticateduser.

Theblogusecaseproposedinthisarticleisquitesimple,butthearchitectureusedcanbeappliedalsoincomplexscenarios,tomanagepermissionsbasedondifferentrequirements.

Footnotes

.https://docs.zendframework.com/zend-permissions-rbac↩

.https://framework.zend.com/blog/2017-04-27-zend-permissions-rbac.html↩

.https://docs.zendframework.com/zend-permissions-rbac↩

.https://docs.zendframework.com/zend-permissions-rbac/intro/#dynamic-assertions↩

1

2

3

4

AuthorizeusersusingMiddleware

96

RESTRepresentationsforExpressivebyMatthewWeierO'Phinney

Atthetimeofwriting(September2017),wehavepublishedtwoexperimentalcomponentsforprovidingRESTresponserepresentationsformiddlewareapplications:

zend-problem-details:https://github.com/zendframework/zend-problem-detailszend-expressive-hal:https://github.com/zendframework/zend-expressive-hal

ThesecomponentsprovideresponserepresentationsforAPIsbuiltwithPSR-7middleware.Specifically,theyprovide:

ProblemDetailsforHTTPAPIs(RFC7807)HypertextApplicationLanguage(HAL)

ThesetwoformatsprovidebothJSONandXMLrepresentationoptions(thelatterthroughasecondaryproposal ).

What'sinarepresentation?Soyou'redevelopinganAPI!

WhatcanclientsexpectwhentheymakearequesttoyourAPI?Willtheygetawalloftext?orsomesortofserialization?Ifit'saserializedformat,whichonesdoyousupport?Andhowisthedatastructured?

Thetypicalanswerwillbe,"we'llprovideJSONresponses."Thatanswerstheserializationaspect,butnotthedatastructure;forthat,youmightdevelopandpublishaschemaforyourendusers,sotheyknowhowtoparsetheresponse.

Butyoumaystillhaveunansweredquestions:

Howdoestheconsumerknowwhatactionscannextbetaken,orwhatresourcesmightberelatedtotheonerequested?Iftheresourcecontainsotherentities,howcantheyidentifywhichonestheycanrequestseparately,versusthosethatarejustpartofthedatastructure?

Theseandallofthepreviousarequestionsthatarepresentationformatanswers.Awell-consideredrepresentationformatwill:

Providelinkstotheactionsthatmaybeperformednext,aswellastorelatedresources.Indicatewhichdataelementsrepresentotherrequestableresources.

1

23

4

RESTRepresentationsforExpressive

97

Beextensible,toallowrepresentingarbitrarydata.

Itendtothinkofrepresentationsasfallingintotwobuckets:

Representationsoferrors.Representationsofapplicationresources.

Errorsneedseparaterepresentation,astheyarenotrequestableontheirown;theyarereturnedwhensomethinggoeswrong,andneedtoprovideenoughdetailthattheconsumercandeterminewhattheyneedtochangeinordertoperformanewrequest.

TheProblemDetailsspecificationprovidesexactlythis.Asanexample:

{

"type":"https://example.com/problems/rate-limit-exceeded",

"title":"YouhaveexceededyourAPIratelimit.",

"detail":"Youhavehityourratelimitof5000requestsperhour.",

"requests_this_hour":5025,

"rate_limit":5000,

"rate_limit_reset":"2017-05-03T14:39-0500"

}

WechoseProblemDetailstostandardizeonwhenstartingtheApigilityprojectasithasveryfewrequirements,butcanmodelanyerroreasily.Theabilitytolinktodocumentationdetailinggeneralerrortypesprovidestheabilitytocommunicatewithyourconsumersaboutknownerrorsandhowtocorrectthem.

Applicationresourcesgenerallyshouldhavetheirownschema,buthavingapredictablestructureforprovidingrelationallinks(answeringthe"whatcanIdonext"question)andembeddingrelatedresourcescanhelpthosemakingclientsorthoseconsumingyourAPItoautomatemanyoftheirprocesses.InsteadofhavingalistofURLstheycanaccess,theycanhitoneresource,andstartfollowingthecomposedlinks;whentheypresentdata,theycanalsopresentcontrolsfortheembeddedresources,makingitsimplertomakerequeststotheseotheritems.

HALprovidesthesedetailsinasimpleway:relationallinksareobjectsunderthe_linkselement,andembeddedresourcesareunderthe_embeddedelement;allotherdataisrepresentedasnormalkey/valuepairs,allowingforarbitrarynestingofstructures.Anexamplepayloadmightlooklikethefollowing:

RESTRepresentationsforExpressive

98

{

"_links":{

"self":{"href":"/api/books?page=7"},

"first":{"href":"/api/books?page=1"},

"prev":{"href":"/api/books?page=6"},

"next":{"href":"/api/books?page=8"},

"last":{"href":"/api/books?page=17"}

"search":{

"href":"/api/books?query={searchTerms}",

"templated":true

}

},

"_embedded":{

"book":[

{

"_links":{

"self":{"href":"/api/books/1234"}

}

"id":1234,

"title":"Hitchhiker'sGuidetotheGalaxy",

"author":"Adams,Douglas"

},

{

"_links":{

"self":{"href":"/api/books/6789"}

}

"id":6789,

"title":"AncillaryJustice",

"author":"Leckie,Ann"

}

]

},

"_page":7,

"_per_page":2,

"_total":33

}

Theaboveprovidescontrolstoallowaconsumertonavigatethrougharesultset,aswellastoperformanothersearchagainsttheAPI.Itprovidesdataabouttheresultset,andalsoembedsanumberofresources,withlinkssothattheconsumercanmakerequestsagainstthoseindividually.HavinglinkspresentinthepayloadsmeansthatiftheURIschemechangeslater,awell-writtenclientwillbeunaffected,asitwillfollowthelinksdeliveredinresponsepayloadsinsteadofhard-codingthem.ThisallowsourAPItoevolve,withoutaffectingtherobustnessofclients.

Anumberofotherrepresentationformatshavebecomepopularovertheyears,including:

JSONAPICollection+JSON

56

7

RESTRepresentationsforExpressive

99

Siren

Eacharepowerfulandflexibleintheirownright.WestandardizedonHALforApigilityoriginallyasitwasoneofthefirstpublishedspecifications;we'vecontinuedwithitasitisaformatthat'sbotheasytogenerateaswellasparse,andextensibleenoughtoanswertheneedsofmostAPIrepresentations.

zend-problem-detailsThepackagezendframework/zend-problem-details providesaProblemDetailsimplementationforPHP,andspecificallyforgeneratingPSR-7responses.Itprovidesamulti-facetedapproachtoprovidingerrordetailstoyourusers.

First,youcancomposetheProblemDetailsResponseFactoryintoyourmiddleware,anduseittogenerateandreturnyourerrorresponses:

return$this->problemDetails->createResponse(

$request,//PSR-7request

422,//HTTPstatus

'Invaliddatadetectedinbooksubmission',//Detail

'Invalidbook',//Problemtitle

'https://example.com/api/doc/errors/invalid-book',//Problemtype(URLtodetail

s)

['messages'=>$validator->getMessages()]//Additionaldata

);

Therequestinstanceispassedtothefactorytoallowittoperformcontentnegotiation;zend-problem-detailsusestheAcceptheadertodeterminewhethertoserveaJSONoranXMLrepresentation,defaultingtoXMLifitisunabletomatchtoeitherformat.

Theabovewillgeneratearesponselikethefollowing:

HTTP/1.1422UnprocessableEntity

Content-Type:application/problem+json

{

"status":422,

"title":"InvalidBook",

"type":"https://example.com/api/doc/errors/invalid-book",

"detail":"Invaliddatadetectedinbooksubmission",

"messages":[

"Missingtitle",

"Missingauthor"

]

}

7

8

RESTRepresentationsforExpressive

100

ProblemDetailsFactoryisagnosticofPSR-7implementation,andallowsyoutoinjectaresponseprototypeandstreamfactoryduringinstantiation.Bydefault,ituseszend-diactorosfortheseartifactsifnoneareprovided.

Second,youcancreatearesponsefromacaughtexceptionorthrowable:

return$this->problemDetails->createResponseFromThrowable(

$request,

$throwable

);

Currently,thefactoryusestheexceptionmessageforthedetail,and4XXand5XXexceptioncodesforthestatus(defaultingto500foranyothervalue).

Atthetimeofpublication,wearecurrentlyevaluatingaproposalthatwouldhavecaughtexceptionsgenerateacannedProblemDetailsresponsewithastatusof500,sotheabovebehaviormaychangeinthefuture.Ifyouwanttoguaranteethecodeandmessageareused,youcancreatecustomexceptions,asoutlinedbelow.

Third,extendingontheabilitytocreatedetailsfromthrowables,weprovideacustomexceptioninterface,ProblemDetailsException.ThisinterfacedefinesmethodsforpullingadditionalinformationtoprovidetoaProblemDetailsresponse:

namespaceZend\ProblemDetails\Exception;

interfaceProblemDetailsException

{

publicfunctiongetStatus():int;

publicfunctiongetType():string;

publicfunctiongetTitle():string;

publicfunctiongetDetail():string;

publicfunctiongetAdditionalData():array;

}

Ifyouthrowanexceptionthatimplementsthisinterface,thecreateResponseFromThrowable()methodshownabovewillpulldatafromthesemethodsinordertocreatetheresponse.Thisallowsyoutodefinedomain-specificexceptionsthatcanprovideadditionaldetailswhenusedinanAPIcontext.

Finally,wealsoprovideoptionalmiddleware,ProblemDetailsMiddleware,thatdoesthefollowing:

RegistersanerrorhandlerthatcastsPHPerrorsinthecurrenterror_reportingbitmasktoErrorExceptioninstances.Wrapscallstothedelegateinatry/catchblock.

RESTRepresentationsforExpressive

101

PassesanycaughtthrowablestothecreateResponseFromThrowable()factoryinordertoreturnProblemDetailsresponses.

Werecommendusingcustomexceptionsandthismiddleware,asthecombinationallowsyoutofocusyoureffortsonthepositiveoutcomepathswithinyourmiddleware.

UsingitinExpressive

WhenusingExpressive,youcanthencomposetheProblemDetailsMiddlewarewithinroute-specificpipelines,allowingyoutohaveseparateerrorhandlersfortheAPIpartsofyourapplication:

//Inconfig/routes.php:

//Perroute:

$app->get('/api/books',[

Zend\ProblemDetails\ProblemDetailsMiddleware::class,

Books\Action\ListBooksAction::class,

],'books');

$app->post('/api/books',[

Zend\ProblemDetails\ProblemDetailsMiddleware::class,

Books\Action\CreateBookAction::class,

]);

Alternately,ifallAPIendpointshaveacommonURIpathprefix,registeritaspath-segregatedmiddleware:

//Inconfig/pipeline.php:

$app->pipe('/api',Zend\ProblemDetails\ProblemDetailsMiddleware::class);

Theseapproachesallowyoutodeliverconsistentlystructured,usefulerrorstoyourAPIconsumers.

zend-expressive-halThepackagezendframework/zend-expressive-hal providesaHALimplementationforPSR-7applications.Currently,itallowscreatingPSR-7responsepayloadsonly;wemayconsiderparsingHALrequestsatafuturedate,however.

zend-expressive-halimplementsPSR-13(LinkDefinitionInterfaces) ,andprovidesstructuresfor:

Definingrelationallinks

9

10

RESTRepresentationsforExpressive

102

DefiningHALresourcesComposingrelationallinksinHALresourcesEmbeddingHALresourcesinotherHALresources

Theseutilitiescanbeusedmanually,withoutanyotherrequirements:

useZend\Expressive\Hal\HalResource;

useZend\Expressive\Hal\Link;

$author=newHalResource($authorDataArray);

$author=$author->withLink(

newLink('self','/authors/'.$authorDataArray['id'])

);

$book=newHalResource($bookDataArray);

$book=$book->withLink(

newLink('self','/books/'.$bookDataArray['id'])

);

$book=$book->embed('authors',[$author]);

BothLinkandHalResourceareimmutable;assuch,ifyouwishtomakeiterativechanges,youwillneedtore-assigntheoriginalvalue.

Theseclasesallowyoutomodelthedatatoreturninyourrepresentation,butwhataboutreturningaresponsebasedonthem?Tohandlethat,wehavetheHalResponseFactory,whichwillgeneratearesponsefromaresourceprovidedtoit:

return$halResponseFactory->createResponse($request,$book);

LiketheProblemDetailsFactory,theHalResponseFactoryisagnosticofPSR-7implementation,andallowsyoutoinjectaresponseprototypeandstreamfactoryduringinstantiation.

Also,it,too,usescontentnegotiationinordertodeterminewhetheraJSONorXMLresponseshouldbegenerated.

Theabovemightgeneratethefollowingresponse:

RESTRepresentationsforExpressive

103

HTTP/1.1200OK

Content-Type:application/hal+json

{

"_links":{

"self":{"href":"/books/42"}

},

"id":42

"title":"TheHitchHiker'sGuidetotheGalaxy",

"_embedded":{

"authors":[

{

"_links":{

"self":{"href":"/author/12"}

},

"id":12,

"name":"DouglasAdams"

}

]

}

}

IfyourresourcesmightbeusedinmultipleAPIendpoints,youmayfindthatcreatingthemmanuallyeverywhereyouneedthemisabitofachore!

Oneofthemostpowerfulpiecesofzend-expressive-halisthatitprovidestoolsformappingobjecttypestohowtheyshouldberepresented.Thisisdoneviaametadatamap,whichmapsclasstypestozend-hydratorextractorsforthepurposeofgeneratingarepresentation.Additionally,weprovidetoolsforgeneratinglinkURIsbasedondefinedroutes,whichallowsmetadatatoprovidedynamiclinkgenerationforgeneratedresources.

Iwon'tgointothearchitectureofhowallthisworks,asthere'safairamountofdetail.Inpractice,whatwillgenerallyhappenis:

You'lldefineametadatamapinyourapplicationconfiguration,mappingyourownclassestodetailsonhowtorepresentthem.You'llcomposeaZend\Expressive\Hal\ResourceGenerator(whichwilluseametadatamapbasedonyourconfiguration)andaHalResponseFactoryinyourmiddleware.You'llpassanobjecttotheResourceGeneratorinordertoproduceaHalResource.You'llpassthegeneratedHalResourcetoyourHalResponseFactorytoproducearesponse.

So,asanexample,Imightdefinethefollowingmetadatamapconfiguration:

namespaceBooks;

useZend\Expressive\Hal\Metadata\MetadataMap;

RESTRepresentationsforExpressive

104

useZend\Expressive\Hal\Metadata\RouteBasedCollectionMetadata;

useZend\Expressive\Hal\Metadata\RouteBasedResourceMetadata;

useZend\Hydreator\ObjectPropertyasObjectPropertyHydrator;

classConfigProvider

{

publicfunction__invoke():array

{

return[

'dependencies'=>$this->getDependencies(),

MetadataMap::class=>$this->getMetadataMap(),

];

}

publicfunctiongetDependencies():array

{

return[/*...*/];

}

publicfunctiongetMetadataMap():array

{

return[

[

'__class__'=>RouteBasedResourceMetadata::class,

'resource_class'=>Author::class,

'route'=>'author',

'extractor'=>ObjectPropertyHydrator::class,

],

[

'__class__'=>RouteBasedCollectionMetadata::class,

'collection_class'=>AuthorCollection::class,

'collection_relation'=>'authors',

'route'=>'authors',

],

[

'__class__'=>RouteBasedResourceMetadata::class,

'resource_class'=>Book::class,

'route'=>'book',

'extractor'=>ObjectPropertyHydrator::class,

],

[

'__class__'=>RouteBasedCollectionMetadata::class,

'collection_class'=>BookCollection::class,

'collection_relation'=>'books',

'route'=>'books',

],

];

}

}

RESTRepresentationsforExpressive

105

Theabovedefinesmetadataforauthorsandbooks,bothasindividualresourcesaswellascollections.Thisallowsustothenembedanauthorasapropertyofabook,andhaveitrepresentedasanembeddedresource!

Fromthere,wecouldhavemiddlewarethatcomposesbothaResourceGeneratorandaHalResponseFactoryinordertoproducerepresentations:

namespaceBooks\Action;

useBooks\Repository;

useInterop\Http\ServerMiddleware\DelegateInterface;

useInterop\Http\ServerMiddleware\MiddlewareInterface;

usePsr\Http\Message\ResponseInterface;

usePsr\Http\Message\ServerRequestInterface;

useZend\Expressive\Hal\HalResponseFactory;

useZend\Expressive\Hal\ResourceGenerator;

classListBooksActionimplementsMiddlewareInterface

{

private$repository;

private$resourceGenerator;

private$responseFactory;

publicfunction__construct(

Repository$repository,

ResourceGenerator$resourceGenerator,

HalResponseFactory$responseFactory

){

$this->repository=$repository;

$this->resourceGenerator=$resourceGenerator;

$this->responseFactory=$responseFactory;

}

publicfunctionprocess(

ServerRequestInterface$request,

DelegateInterface$delegate

):ResponseInterface{

/**@var\Books\BookCollection$books*/

$books=$this->repository->fetchAll();

return$this->responseFactory->createResponse(

$request,

$this->resourceGenerator->fromObject($books)

);

}

}

Whenusingzend-expressive-haltogenerateyourresponses,themajorityofyourmiddlewarewilllookalmostexactlylikethis!

RESTRepresentationsforExpressive

106

Weprovideanumberofotherfeaturesinthepackageaswell:

Youcandefineyourownmetadatatypes,andstrategyclassesforproducingrepresentationsbasedonobjectsmatchingthatmetadata.Youcanspecifycustommediatypesforyourgeneratedresponses.Youcanprovideyourownlinkgeneration(usefulifyou'renotusingExpressive).YoucanprovideyourownJSONandXMLrenderers,ifyouwanttovarytheoutputforsomereason(e.g.,alwaysaddingspecificlinks).

UseAnywhere!Thesetwopackages,whilepartoftheZendFrameworkandExpressiveecosystems,canbeusedanywhereyouusePSR-7middleware.TheProblemDetailscomponentprovidesafactoryforproducingaPSR-7ProblemDetailsresponse,andoptionallymiddlewareforautomatingreportingoferrors.TheHALcomponentprovidesonlyafactoryforproducingaPSR-7HALresponse,andanumberoftoolsformodelingthedatatoreturninthatresponse.

Assuch,weencourageSlim,Lumen,andotherPSR-7frameworkuserstoconsiderusingthesecomponentsinyourAPIapplicationstoprovidestandard,robust,andextensiblerepresentationstoyourusers!

Formoredetailsandexamples,visitthedocsforeachcomponent:

zend-problem-detailsdocumentation:https://docs.zendframework.com/zend-problem-detailszend-expressive-haldocumentation:https://docs.zendframework.com/zend-expressive-hal

Footnotes

.http://www.php-fig.org/psr/psr-7/↩

.https://tools.ietf.org/html/rfc7807↩

.https://tools.ietf.org/html/draft-kelly-json-hal-08↩

.https://tools.ietf.org/html/draft-michaud-xml-hal-01↩

.http://jsonapi.org↩

.http://amundsen.com/media-types/collection/format/↩

.http://hyperschema.org/mediatypes/siren↩

1

2

3

4

5

6

7

8

RESTRepresentationsforExpressive

107

.https://github.com/zendframework/zend-problem-details↩

.https://github.com/zendframework/zend-expressive-hal↩

.http://www.php-fig.org/psr/psr-13/↩

8

9

10

RESTRepresentationsforExpressive

108

Copyrightnote

RogueWavehelpsthousandsofglobalenterprisecustomerstacklethehardestandmostcomplexissuesinbuilding,connecting,andsecuringapplications.Since1989,ourplatforms,tools,components,andsupporthavebeenusedacrossfinancialservices,technology,healthcare,government,entertainment,andmanufacturing,todelivervalueandreducerisk.FromAPImanagement,webandmobile,embeddableanalytics,staticanddynamicanalysistoopensourcesupport,wehavethesoftwareessentialstoinnovatewithconfidence.

https://www.roguewave.com/

©2017RogueWaveSoftware,Inc.Allrightsreserved

Copyrightnote

109