View
217
Download
0
Category
Preview:
Citation preview
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
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
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
Recommended