1.1
1.2
2.1
2.2
3.1
3.2
3.3
4.1
4.2
4.3
5.1
5.2
6.1
6.2
6.3
TableofContentsIntroduction
Abouttheauthors
Configurationzend-configforallyourconfigurationneeds
Manageyourapplicationwithzend-config-aggregator
DataManipulationConvertobjectstoarraysandbackwithzend-hydrator
ScrapeScreenswithzend-dom
Paginatingdatacollectionswithzend-paginator
LogandFeedsLoggingPHPapplications
DiscoverandReadRSSandAtomFeeds
CreateRSSandAtomFeeds
AuthenticationandAuthorizationManagepermissionswithzend-permissions-rbac
Managepermissionswithzend-permissions-acl
WebServicesImplementJSON-RPCwithzend-json-server
ImplementanXML-RPCserverwithzend-xmlrpc
ImplementaSOAPserverwithzend-soap
2
7.1
7.2
7.3
7.4
7.5
8.1
8.2
9.1
SecurityContext-specificescapingwithzend-escaper
Filterinputusingzend-filter
Validateinputusingzend-validator
Validatedatausingzend-inputfilter
End-to-endencryptionwithZendFramework3
DeploymentandVirtualizationCreateZPKstheEasyWay
UsingLaravelHomesteadwithZendFrameworkProjects
CopyrightCopyrightnote
3
ZendFramework3CookbookDuringtheyear2017,MatthewWeierO'PhinneyandEnricoZimuelstartedaseriesofblogpostsontheofficalZendFrameworkblogcoveringitscomponents.
ZendFrameworkiscomposedby60+componentscoveringawiderangeoffunctionality.Whiletheframeworkhastypicallybeenmarketedasafull-stackMVCframework,theindividualcomponentsthemselvestypicallyworkindependentlyandcanbeusedstandaloneorwithinotherframeworks.Theblogpostswerewrittentohighlightthisfact,anddemonstratehowtogetstartedwithanumberofthemorepopularandusefulcomponents.
WehopethisbookwillhelpyougetstartedusingZendFrameworkcomponents,nomatterwhatprojectyouarewriting!
Enjoyyourreading,MatthewWeierO'PhinneyandEnricoZimuelRogueWaveSoftware,Inc.
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.HeisaformerResearcherProgrammerfortheInformaticsInstituteoftheUniversityofAmsterdam.Enricospeaksregularlyatconferencesandevents,includingTEDxandinternationalPHPconferences.Heisalsotheco-founderofthePHPUserGroupofTorino(Italy).
Formoreinformation:
https://www.zimuel.it/
Abouttheauthors
5
https://www.roguewave.com/TEDxpresentation:https://www.youtube.com/watch?v=SienrLY40-wPHPUserGroupofTorino:http://torino.grusp.org/
Abouttheauthors
6
Zend-configforallyourconfigurationneedsbyMatthewWeierO'Phinney
Differentapplicationsandframeworkshavedifferentopinionsabouthowconfigurationshouldbecreated.SomepreferXML,othersYAML,somelikeJSON,otherslikeINI,andsomeevensticktotheJavaPropertiesformat;inZendFramework,wetendtopreferPHParrays,aseachoftheotherformatsessentiallygetcompiledtoPHParrayseventuallyanyways.
Atheart,though,weliketosupportdeveloperneeds,whatevertheymaybe,and,assuch,ourzend-configcomponent provideswaysofworkingwithavarietyofconfigurationformats.
Installationzend-configisinstallableviaComposer:
$composerrequirezendframework/zend-config
Thecomponenthastwodependencies:
zend-stdlib ,whichprovidessomecapabilitiesaroundconfigurationmerging.psr/container ,toallowreaderandwriterpluginsupportfortheconfigurationfactory.
Latestversion
Thisarticlecoversthemostrecentlyreleasedversionofzend-config,3.1.0,whichcontainsanumberoffeaturessuchasPSR-11supportthatwerenotpreviouslyavailable.IfyouareusingZendFrameworkMVClayer,youshouldbeabletosafelyprovidetheconstraint ̂ 2.6||^3.1,astheprimaryAPIsremainthesame.
RetrievingconfigurationOnceyou'veinstalledzend-config,youcanstartusingittoretrieveandaccessconfigurationfiles.ThesimplestwayistouseZend\Config\Factory,whichprovidestoolsforloadingconfigurationfromavarietyofformats,aswellascapabilitiesformerging.
1
23
zend-configforallyourconfigurationneeds
7
Ifyou'rejustpullinginasinglefile,useFactory::fromFile():
useZend\Config\Factory;
$config=Factory::fromFile($path);
Farmoreinterestingistousemultiplefiles,whichyoucandoviaFactory::fromFiles().Whenyoudo,theyaremergedintoasingleconfiguration,intheorderinwhichtheyareprovidedtothefactory.Thisisparticularlyinterestingusingglob():
useZend\Config\Factory;
$config=Factory::fromFiles(glob('config/autoload/*.*'));
Thismethodsupportsavarietyofformats:
PHPfilesreturningarrays(.phpextension)INIfiles(.iniextension)JSONfiles(.jsonextension)XMLfiles(usingPHP'sXMLReader;.xmlextension)YAMLfiles(usingext/yaml,installableviaPECL;.yamlextension)JavaPropertiesfiles(.javapropertiesextension)
Thismeansthatyoucanchoosetheconfigurationformatyouprefer,ormix-and-matchmultipleformats,ifyouneedtocombineconfigurationfrommultiplelibraries!
ConfigurationobjectsBydefault,Zend\Config\FactorywillreturnPHParraysforthemergedconfiguration.Somedependencyinjectioncontainersdonotsupportarraysasservices,however;moreover,youmaywanttopasssomesortofstructuredobjectinsteadofaplainarraywheninjectingdependencies.
Assuch,youcanpassasecond,optionalargumenttoeachoffromFile()andfromFiles(),abooleanflag.Whentrue,itwillreturnaZend\Config\Configinstance,whichimplementsCountable,Iterator,andArrayAccess,allowingittolookandactlikeanarray.
Whatisthebenefit?
First,itprovidespropertyoverloadingtoeachconfigurationkey:
zend-configforallyourconfigurationneeds
8
$debug=$config->debug??false;
Second,itoffersaconveniencemethod,get(),whichallowsyoutospecifyadefaultvaluetoreturnifthevalueisnotfound:
$debug=$config->get('debug',false);//Returnfalseifnotfound
Thisislargelyobviatedbythe??ternaryshortcutinmodernPHPversions,butveryusefulwhenmockinginyourtests.
Third,nestedsetsarealsoreturnedasConfiginstances,whichgivesyoutheabilitytousetheaboveget()methodonanesteditem:
if(isset($config->expressive)){
$config=$config->get('expressive');//sameAPI!
}
Fourth,youcanmarktheConfiginstanceasimmutable!Bydefault,itactsjustlikearrayconfiguration,whichis,ofcourse,mutable.However,thiscanbeproblematicwhenyouuseconfigurationasaservice,because,unlikeanarray,aConfiginstanceispassedbyreference,andchangestovalueswouldthenpropagatetoanyotherservicesthatdependontheconfiguration.
Ideally,youwouldn'tbechanginganyvaluesintheinstance,butZend\Config\Configcanenforcethatforyou:
$config->setReadOnly();//Nowimmutable!
Further,callingthiswillmarknestedConfiginstancesasread-onlyaswell,ensuringdataintegrityfortheentireconfigurationtree.
zend-configforallyourconfigurationneeds
9
Read-onlybydefault!
Onethingtonote:bydefault,Configinstancesareread-only!Theconstructoracceptsanoptional,secondargument,aflagindicatingwhetherornottheinstanceallowsmodifications,andthevalueisfalsebydefault.WhenyouusetheFactorytocreateaConfiginstance,itneverenablesthatflag,meaningthatifyoureturnaConfiginstance,itwillberead-only.
IfyouwantamutableinstancefromaFactory,usethefollowingconstruct:
useZend\Config\Config;
useZend\Config\Factory;
$config=newConfig(Factory::fromFiles($files),true);
IncludingotherconfigurationMostoftheconfigurationreaderpluginsalsosupport"includes":directiveswithinaconfigurationfilethatwillincludeconfigurationfromanotherfile.(JavaPropertiesistheonlyconfigurationformatwesupportthatdoesnothavethisfunctionalityincluded.)
Forinstance:
INIfilescanusethekey@includetoincludeanotherfilerelativetothecurrentone;valuesaremergedatthesamelevel:
webhost='www.example.com'
@include='database.ini'
ForXMLfiles,youcanuseXInclude:
<?xmlversion="1.0"encoding="utf-8">
<configxmlns:xi="http://www.w3.org/2001/XInclude">
<webhost>www.example.com</webhost>
<xi:includehref="database.xml"/>
</config>
JSONfilescanusean@includekey:
{
"webhost":"www.example.com",
"@include":"database.json"
}
zend-configforallyourconfigurationneeds
10
YAMLalsousesthe@includenotation:
webhost:www.example.com
@include:database.yaml
ChooseyourownYAMLOut-of-the-boxwesupporttheYAMLPECLextensionforourYAMLsupport.However,wehavemadeitpossibletousealternateparsers,suchasSpycortheSymfonyYAMLcomponent,bypassingacallbacktothereader'sconstructor:
useSymfony\Component\Yaml\YamlasSymfonyYaml;
useZend\Config\Reader\YamlasYamlConfig;
$reader=newYamlConfig([SymfonfyYaml::class,'parse']);
$config=$reader->fromFile('config.yaml');
Ofcourse,ifyou'regoingtodothat,youcouldjustusetheoriginallibrary,right?ButwhatifyouwanttomixYAMLandotherconfigurationwiththeFactoryclass?
Therearetwowaystoregisternewplugins.Oneistocreateaninstanceandregisteritwiththefactory:
useSymfony\Component\Yaml\YamlasSymfonyYaml;
useZend\Config\Factory;
useZend\Config\Reader\YamlasYamlConfig;
Factory::registerReader('yaml',newYamlConfig([SymfonyYaml::class,'parse']));
Alternately,youcanprovideanalternatereaderpluginmanager.YoucandothatbyextendingZend\Config\StandaloneReaderPluginManager,whichisabarebonesPSR-11containerforuseasapluginmanager:
zend-configforallyourconfigurationneeds
11
namespaceAcme;
useSymfony\Component\Yaml\YamlasSymfonyYaml;
useZend\Config\Reader\YamlasYamlConfig;
useZend\Config\StandaloneReaderPluginManager;
classReaderPluginManagerextendsStandaloneReaderPluginManager
{
/**
*@inheritDoc
*/
publicfunctionhas($plugin)
{
if(YamlConfig::class===$plugin
||'yaml'===strtolower($plugin)
){
returntrue;
}
returnparent::has($plugin);
}
/**
*@inheritDoc
*/
publicfunctionget($plugin)
{
if(YamlConfig::class!==$plugin
&&'yaml'!==strtolower($plugin)
){
returnparent::get($plugin);
}
returnnewYamlConfig([SymfonyYaml::class,'parse']);
}
}
ThenregisterthiswiththeFactory:
useAcme\ReaderPluginManager;
useZend\Config\Factory;
Factory::setReaderPluginManager(newReaderPluginManager());
Processingconfigurationzend-configalsoallowsyoutoprocessaZend\Config\Configinstanceand/oranindividualvalue.Processorsperformoperationssuchas:
zend-configforallyourconfigurationneeds
12
substitutingconstantvalueswithinstringsfilteringconfigurationdatareplacingtokenswithinconfigurationtranslatingconfigurationvalues
Whywouldyouwanttodoanyoftheseoperations?
Considerthis:deserializationofformatsotherthanPHPcannottakeintoaccountPHPconstantvaluesorclassnames!
WhilethismayworkinPHP:
return[
Acme\Component::CONFIG_KEY=>[
'host'=>Acme\Component::CONFIG_HOST,
'dependencies'=>[
'factories'=>[
Acme\Middleware\Authorization::class=>Acme\Middleware\AuthorizationF
actory::class,
],
],
],
];
ThefollowingJSONconfigurationwouldnot:
{
"Acme\\Component::CONFIG_KEY":{
"host":"Acme\\Component::CONFIG_HOST"
"dependencies":{
"factories":{
"Acme\\Middleware\\Authorization::class":"Acme\\Middleware\\Authoriza
tionFactory::class"
}
}
}
}
EntertheConstantprocessor!
Thisprocessorlooksforstringsthatmatchconstantnames,andreplacesthemwiththeirvalues.Processorsgenerallyonlyworkontheconfigurationvalues,buttheConstantprocessorallowsyoutoopt-intoprocessingthekeysaswell.
SinceprocessingmodifiestheConfiginstance,youwillneedtomanuallycreateaninstance,andthenprocessit.Let'slookatthat:
zend-configforallyourconfigurationneeds
13
useAcme\Component;
useZend\Config\Config;
useZend\Config\Factory;
useZend\Config\Processor;
$config=newConfig(Factory::fromFile('config.json'),true);
$processor=newProcessor\Constant();
$processor->enableKeyProcessing();
$processor->process($config);
$config->setReadOnly();
var_export($config->{Component::CONFIG_KEY}->dependencies->factories);
//['Acme\Middleware\Authorization'=>'Acme\Middleware\AuthorizationFactory']
Thisisareallypowerfulfeature,asitallowsyoutoaddmoreverificationsandvalidationstoyourconfigurationfiles,regardlessoftheformatyouuse.
Inversion3.1.0forward
Theabilitytoworkwithclassconstantsandprocesskeyswasaddedstartingwiththe3.1.0versionofzend-config.
Configallthethings!Thispostcoverstheparsingfeaturesofzend-config,butdoesnoteventouchonanothermajorcapability:theabilitytowriteconfiguration!We'llleavethattoanotherpost.
Intermsofconfigurationparsing,zend-configissimple,yetpowerful.Theabilitytoprocessanumberofcommonconfigurationformats,utilizeconfigurationincludes,andprocesskeysandvaluesmeansyoucanhighlycustomizeyourconfigurationprocesstosuityourneedsorintegratedifferentconfigurationsources.
Getmoreinformationfromthezend-configdocumentation .
Footnotes
.https://docs.zendframework.com/zend-config/↩
.https://docs.zendframework.com/zend-stdlib/↩
.https://github.com/php-fig/container↩
.https://docs.zendframework.com/zend-config/↩
4
1
2
3
4
zend-configforallyourconfigurationneeds
14
zend-configforallyourconfigurationneeds
15
Manageyourapplicationwithzend-config-aggregatorbyMatthewWeierO'Phinney
WiththeriseofPHPmiddleware,manydevelopersarecreatingcustomapplicationarchitectures,andrunningintoanissuemanyframeworksalreadysolve:howtoallowruntimeconfigurationoftheapplication.
configurationisoftennecessary,evenincustomapplications:
Someconfiguration,suchasAPIkeys,mayvarybetweenenvironments.Youmaywanttosubstituteservicesbetweendevelopmentandproduction.Somecodemaybedevelopedbyotherteams,andpulledintoyourapplicationseparately(perhapsviaComposer ),andrequireconfiguration.Youmaybewritingcodeinyourapplicationthatyouwilllaterwanttosharewithanotherteam,andrecognizeitshouldprovideservicewiringinformationorallowfordynamicconfigurationitself.
Facedwiththisreality,youthenhaveanewproblem:howcanyouconfigureyourapplication,aswellasaggregateconfigurationfromothersources?
AspartoftheExpressiveinitiative,wenowofferastandalonesolutionforyou:zend-config-aggregator .
InstallationFirst,youwillneedtoinstallzend-config-aggregator:
$composerrequirezendframework/zend-config-aggregator
Onefeatureofzend-config-aggregatoristheabilitytoconsumemultipleconfigurationformatsviazend-config .Ifyouwishtousethatfeature,youwillalsoneedtoinstallthatpackage:
$composerrequirezendframework/zend-config
Finally,ifyouareusingtheabove,andwanttoparseYAMLfiles,youwillneedtoinstalltheYAMLPECLextension .
1
2
3
4
Manageyourapplicationwithzend-config-aggregator
16
Configurationproviderszend-config-aggregatorallowsyoutoaggregateconfigurationfromconfigurationproviders.AconfigurationproviderisanyPHPcallablethatwillreturnanassociativearrayofconfiguration.
Bydefault,thecomponentprovidesthefollowingprovidersoutofthebox:
Zend\ConfigAggregator\ArrayProvider,whichacceptsanarrayofconfigurationandsimplyreturnsit.Thisisprimarilyusefulforprovidingglobaldefaultsforyourapplication.Zend\ConfigAggregator\PhpFileProvider,whichacceptsaglobpatterndescribingPHPfilesthateachreturnanassociativearray.Wheninvoked,itwillloopthrougheachfile,andmergetheresultswithwhatithaspreviouslystored.Zend\ConfigAggregator\ZendConfigProvider,whichactssimilarlytothePhpFileProvider,butwhichcanaggregateanyformatzend-configsupports,includingINI,XML,JSON,andYAML.
Moreinterestingly,however,isthefactthatyoucanwriteprovidersassimpleinvokableobjects:
namespaceAcme;
classConfigProvider
{
publicfunction__invoke()
{
return[
//associativearrayofconfiguration
];
}
}
Thisfeatureallowsyoutowriteconfigurationforspecificapplicationfeatures,andthenseedyourapplicationwithit.Inotherwords,thisfeaturecanbeusedasthefoundationforamodulararchitecture ,whichisexactlywhatwedidwithExpressive!5
Manageyourapplicationwithzend-config-aggregator
17
Generators
YoumayalsouseinvokableclassesorPHPcallablesthatdefinegeneratorsasconfigurationproviders!Asanexample,thePhpFileProvidercouldpotentiallyberewrittenasfollows:
useZend\Stdlib\Glob;
function(){
foreach(Glob::glob('config/*.php',Glob::GLOB_BRACE)as$file){
yieldinclude$file;
}
}
AggregatingconfigurationNowthatyouhaveconfigurationproviders,youcanaggregatethem.
Forthepurposesofthisexample,we'llassumethefollowing:
Wewillhaveasingleconfigurationfile,config.php,attherootofourapplicationwhichwillaggregateallotherconfiguration.Wehaveanumberofconfigurationfilesunderconfig/,includingYAML,JSON,andPHPfiles.Wehaveathird-party"module"thatexposestheclassUmbrella\ConfigProvider.Wehavedevelopedourown"module"forre-distributionthatexposestheclassBlanket\ConfigProvider.
Typically,youwillwantaggregateconfigurationsuchthatthird-partyconfigurationisloadedfirst,withapplication-specificconfigurationmergedlast,inordertooverridesettings.
Let'saggregateandreturnourconfiguration.
//inconfig.php:
useZend\ConfigAggregator\ConfigAggregator;
useZend\ConfigAggregator\ZendConfigProvider;
$aggregator=newConfigAggregator([
\Umbrella\ConfigProvider::class,
\Blanket\ConfigProvider::class,
newZendConfigProvider('config/*.{json,yaml,php}'),
]);
return$aggregator->getMergedConfig();
Manageyourapplicationwithzend-config-aggregator
18
Thisfileaggregatesthethird-partyconfigurationprovider,theoneweexposeinourownapplication,andthenaggregatesavarietyofdifferentconfigurationfilesinorderto,intheend,returnanassociativearrayrepresentingthemergedconfiguration!
Validconfigprofiderentries
You'llnotethattheConfigAggregatorexpectsanarrayofprovidersasthefirstargumenttotheconstructor.Thisarraymayconsistofanyofthefollowing:
AnyPHPcallable(functions,invokableobjects,closures,etc.)returninganarray.Aclassnameofaclassthatdefines__invoke(),andwhichrequiresnoconstructorarguments.
Thislatterisuseful,asithelpsreduceoperationaloverheadonceyouintroducecaching,whichwediscussbelow.Theaboveexampledemonstratesthisusage.
zend-configandPHPconfiguration
TheaboveexampleusesonlytheZendConfigProvider,andnotthePhpFileProvider.Thisisduetothefactthatzend-configcanalsoconsumePHPconfiguration.
IfyouareonlyusingPHP-basedconfigurationfiles,youcanusethePhpFileProviderinstead,asitdoesnotrequireadditionallyinstallingthezendframework/zend-configpackage.
Globbingandprecedence
Globbingworksasitdoesonmost*nixsystems.Assuch,youneedtopayparticularattentiontowhenyouusepatternsthatdefinealternatives,suchasthe{json,yaml,php}patternabove.Insuchcases,allJSONfileswillbeaggregated,followedbyYAMLfiles,andfinallyPHPfiles.Ifyouneedthemtoaggregateinadifferentorder,youwillneedtochangethepattern.
CachingYoulikelydonotwanttoaggregateconfigurationoneachandeveryapplicationrequest,particularlyifdoingsowouldresultinmanyfilesystemhits.Fortunately,zend-config-aggregatoralsohasbuilt-incachingfeatures.
Toenablethesefeatures,youwillneedtodotwothings:
First,youneedtoprovideasecondargumenttotheConfigAggregatorconstructor,
Manageyourapplicationwithzend-config-aggregator
19
specifyingthepathtothecachefiletocreateand/oruse.Second,youneedtoenablecachinginyourconfiguration,byspecifyingabooleantruevalueforthekeyConfigAggregator::ENABLE_CACHE.
Onecommonstrategyistoenablecachingbydefault,andthendisableitviaenvironment-specificconfiguration.
We'llupdatetheaboveexamplenowtoenablecachingtothefilecache/config.php:
useZend\ConfigAggregator\ArrayProvider;
useZend\ConfigAggregator\ConfigAggregator;
useZend\ConfigAggregator\PhpFileProvider;
useZend\ConfigAggregator\ZendConfigProvider;
$aggregator=newConfigAggregator(
[
newArrayProvider([ConfigAggregator::ENABLE_CACHE=>true]),
\Umbrella\ConfigProvider::class,
\Blanket\ConfigProvider::class,
newZendConfigProvider('config/{,*.}global.{json,yaml,php}'),
newPhpFileProvider('config/{,*.}local.php'),
],
'cache/config.php'
);
return$aggregator->getMergedConfig();
Theaboveaddsaninitialsettingthatenablesthecache,andtellsittocacheittocache/config.php.
NoticealsothatthisexamplechangestheZendConfigProvider,andaddsaPhpFileProviderentry.Let'sexaminethese.
TheZendConfigProviderglobpatternnowlooksforfilesnamedglobalwithoneoftheacceptedextensions,orthosenamed*.globalwithoneoftheacceptedextensions.Thisallowsustosegregateconfigurationthatshouldalwaysbepresentfromenvironment-specificconfiguration.
WethenaddaPhpFileProviderthataggregateslocal.phpand/or*.local.phpfilesspecifically.Aninterestingside-noteabouttheshippedprovidersisthatifnomatchingfilesarefound,theproviderwillreturnanemptyarray;thismeansthatwecanhavethisadditionalproviderthatislookingforseparateconfigurationsforthe"local"environment!Becausethisproviderisaggregatedlast,thesettingsitexposeswilloverrideanyothers.
Assuch,ifwewanttodisablecaching,wecancreateafilesuchasconfig/local.phpwiththefollowingcontents:
Manageyourapplicationwithzend-config-aggregator
20
<?php
useZend\ConfigAggregator\ConfigAggregator;
return[ConfigAggregator::ENABLE_CACHE=>false];
andtheapplicationwillnolongercacheaggregatedconfiguration!
Clearthecache!
Thesettingoutlinedaboveisusedtodeterminewhethertheconfigurationcachefileshouldbecreatedifitdoesnotalreadyexist.zend-config-aggregator,whenprovidedthelocationofaconfigurationcachefile,willloaddirectlyfromitifthefileispresent.
Assuch,ifyoumaketheaboveconfigurationchange,youwillfirstneedtoremoveanycachedconfiguration:
$rmcache/config.php
ThiscanevenbemadeintoaComposerscript:
"scripts":{
"clear-config-cache":"rmcache/config.php"
}
Allowingyoutodothis:
$composerclear-config-cache
Whichallowsyoutochangethelocationofthecachefilewithoutneedingtore-learnthelocationeverytimeyouneedtoclearthecache.
Auto-enablingthird-partyprovidersBeingabletoaggregateprovidersfromthird-partiesisprettystellar;itmeansthatyoucanbeassuredthatconfigurationthethird-partycodeexpectsisgenerallypresent—withtheexceptionofvaluesthatmustbeprovidedbytheconsumer,thatis!
However,there'soneminorproblem:youneedtoremembertoregistertheseconfigurationproviderswithyourapplication,bymanuallyeditingyourconfig.phpfileandaddingtheappropriateentries.
6
Manageyourapplicationwithzend-config-aggregator
21
ZendFrameworksolvesthisviathezf-component-installerComposerplugin .IfyourpackageisinstallableviaComposer,youcanaddanentrytoyourpackagedefinitionasfollows:
"extra":{
"zf":{
"config-provider":[
"Umbrella\\ConfigProvider"
]
}
}
Iftheend-user:
Hasrequiredzendframework/zend-component-installerintheirapplication(aseitheraproductionordevelopmentdependency),ANDhastheconfigaggregationscriptinconfig/config.php
thenthepluginwillpromptyou,askingifyouwouldliketoaddeachoftheconfig-providerentriesfoundintheinstalledpackageintotheconfigurationscript.
Assuch,forourexampletowork,wewouldneedtomoveourconfigurationscripttoconfig/config.php,andlikelymoveourotherconfigurationfilesintoasub-directory:
cache/
config.php
config/
config.php
autoload/
blanket.global.yaml
global.php
umbrella.global.json
ThisapproachisessentiallythattakenbyExpressive.
Whenthosechangesaremade,anypackageyouaddtoyourapplicationthatexposesconfigurationproviderswillpromptyoutoaddthemtoyourconfigurationaggregation,and,ifyouconfirm,willaddthemtothetopofthescript!
FinalnotesFirst,wewouldliketothankMateuszTymek ,whoseprototype'expressive-config-manager'projectbecamezend-config-aggregator.Thisisastellarexampleofacommunityprojectgettingadoptedintotheframework!
6
7
Manageyourapplicationwithzend-config-aggregator
22
Second,thisapproachhassomeaffinitytoaproposalfromthefolkswhobroughtusPSR-11,whichdefinestheContainerInterfaceusedwithinExpressiveforallowingusageofdifferentdependencyinjectioncontainers.Thatsamegroupisnowworkingonaserviceprovider proposalthatwouldstandardizehowstandalonelibrariesexposeservicestocontainers;werecommendlookingatthatprojectaswell.
Wehopethatthisposthelpsspawnideasforconfiguringyournextproject!
Footnotes
.https://getcomposer.org↩
.https://github.com/zendframework/zend-config-aggregator↩
.https://docs.zendframework.com/zend-config/↩
.http://www.php.net/manual/en/book.yaml.php↩
.https://docs.zendframework.com/zend-expressive/features/modular-applications/↩
.https://docs.zendframework.com/zend-component-installer/↩
.http://mateusztymek.pl/↩
.https://github.com/container-interop/service-provider↩
8
1
2
3
4
5
6
7
8
Manageyourapplicationwithzend-config-aggregator
23
Convertobjectstoarraysandbackwithzend-hydratorbyMatthewWeierO'Phinney
APIsarealltheragethesedays,andatremendousnumberofthemarebeingwritteninPHP.WhenAPIswerefirstgainingpopularity,thisseemedlikeamatchmadeinheaven:querythedatabase,passtheresultstojson_encode(),andvoilà!APIpayload!Inreverse,it'sjson_decode(),passthedatatothedatabase,anddone!
ModerndayprofessionalPHP,however,isskewingtowardsusageofvalueobjectsandentities,butwe'restillcreatingAPIs.HowcanwetaketheseobjectsandcreateourAPIresponsepayloads?Howcanwetakeincomingdataandtransformitintothedomainobjectsweneed?
ZendFramework'sanswertothatquestioniszend-hydrator.Hydratorscanextractanassociativearrayofdatafromanobject,andhydrateanobjectfromanassociativearrayofdata.
InstallationAswithourothercomponents,youcaninstallzend-hydratorbyitself:
$composerrequirezendframework/zend-hydrator
Out-of-the-box,itonlyrequireszend-stdlib,whichisusedinternallyfortransformingiteratorstoassociativearrays.However,thereareanumberofotherinteresting,ifoptional,featuresthatrequireothercomponents:
Youcancreateanaggregatehydratorwhereeachhydratorisresponsibleforasubsetofdata.Thisrequireszend-eventmanager.Youcanfilter/normalizethekeys/propertiesofdatausingnamingstrategies;theserequirezend-filter.Youcanmapobjecttypestohydrators,anddelegatehydrationofarbitraryobjectsusingtheDelegatingHydrator.ThisfeatureutilizestheprovidedHydratorPluginManager,whichrequireszend-servicemanager.
Inourexamplesbelow,we'llbedemonstratingnamingstrategiesandthedelegatinghydrator,sowewillinstallthedependenciesthoseneed:
Convertobjectstoarraysandbackwithzend-hydrator
24
$composerrequirezendframework/zend-filterzendframework/zend-servicemanager
ObjectstoarraysandbackagainLet'stakethefollowingclassdefinition:
namespaceAcme;
classBook
{
private$id;
private$title;
private$author;
publicfunction__construct(int$id,string$title,string$author)
{
$this->id=$id;
$this->title=$title;
$this->author=$author;
}
}
Whatwehaveisavalueobject,withnowaytopubliclygrabanygivendatum.WenowwanttorepresentitinourAPI.Howdowedothat?
Theanswerisviareflection,andzend-hydratorprovidesasolutionforthat:
useAcme\Book;
useZend\Hydrator\ReflectionasReflectionHydrator;
$book=newBook(42,'Hitchhiker\'sGuidetotheGalaxy','DouglasAdams');
$hydrator=newReflectionHydrator();
$data=$hydrator->extract($book);
WenowhaveanarrayrepresentationofourBookinstance!
Let'ssaythatsomebodyhasjustsubmittedabookviaawebformoranAPI.Wehavethevalues,butwanttocreateaBookoutofthem.
Convertobjectstoarraysandbackwithzend-hydrator
25
useAcme\Book;
useReflectionClass;
useZend\Hydrator\ReflectionasReflectionHydrator;
$hydrator=newReflectionHydrator();
$book=$hydrator->hydrate(
$incomingData,
(newReflectionClass(Book::class))->newInstanceWithoutConstructor()
);
AndnowwehaveaBookinstance!
ThenewInstanceWithoutConstructor()constructisnecessaryinthiscasebecauseourclasshasrequiredconstructorarguments.Anotherpossibilityistoprovideanalreadypopulatedinstance,andhopethatthesubmitteddatawilloverwritealldataintheclass.Alternately,youcancreateclassesthathaveoptionalconstructorarguments.
Mostofthetime,itcanbeassimpleasthis:createanappropriatehydratorinstance,anduseeitherextract()togetanarrayrepresentationoftheobject,orhydrate()tocreateaninstancefromanarrayofdata.
Weprovideanumberofstandardimplementations:
Zend\Hydrator\ArraySerializableworkswithArrayObjectimplementations.ItwillalsohydrateanyobjectimplementingeitherthemethodexchangeArray()orpopulate(),andextractfromanyobjectimplementinggetArrayCopy().Zend\Hydrator\ClassMethodswillusesetterandgettermethodstopopulateandextractobjects.Italsounderstandshas*()andis*()methodsasgetters.Zend\Hydrator\ObjectPropertywillusepublicinstanceproperties.Zend\Hydrator\Reflectioncanextractandpopulateinstancepropertiesofanyvisibility.
FilteringvaluesSinceacommonrationaleforextractingdatafromobjectsistocreatepayloadsforAPIs,youmayfindthereisdatainyourobjectyoudonotwanttorepresent.
zend-hydratorprovidesaZend\Hydrator\Filter\FilterInterfaceforaccomplishingthis.Filtersimplementthefollowing:
Convertobjectstoarraysandbackwithzend-hydrator
26
namespaceZend\Hydrator\Filter;
interfaceFilterInterface
{
/**
*@paramstring$property
*@returnbool
publicfunctionfilter($property);
}
Ifafilterreturnsabooleantrue,thevalueiskept;otherwise,itisomitted.
AFilterCompositeimplementationallowsattachingmultiplefilters;eachpropertyisthencheckedagainsteachfilter.(ThisclassalsoallowsattachingstandardPHPcallablesforfilters,insteadofFilterInterfaceimplementations.)AFilterEnabledInterfaceallowsahydratortoindicateitcomposesfilters.Tyingittogether,allshippedhydratorsinheritfromacommonbasethatimplementsFilterEnabledInterfacebycomposingaFilterComposite,whichmeansthatyoucanusefiltersimmediatelyinastandardfashion.
Asanexample,let'ssaywehaveaUserclassthathasapasswordproperty;weclearlydonotwanttoreturnthepasswordinourpayload,evenifitisproperlyhashed!Filterstotherescue!
useZend\Hydrator\ObjectPropertyasObjectPropertyHydrator;
$hydrator=newObjectPropertyHydrator();
$hydrator->addFilter('password',function($property){
return$property!=='password';
});
$data=$hydrator->extract($user);
Somehydratorsactuallyusefiltersinternallyinordertodotheirwork.Asanexample,theClassMethodshydratorcomposesthefollowingbydefault:
IsFilter,toidentifymethodsbeginningwithis,suchasisTransaction().HasFilter,toidentifymethodsbeginningwithhas,suchashasAuthor().GetFilter,toidentifymethodsbeginningwithget,suchasgetTitle().OptionalParametersFilter,toensureanygivenmatchedmethodcanbeexecutedwithoutrequiringanyarguments.
Thislatterpointbringsupaninterestingfeature:sincehydrationrunseachpotentialpropertynamethrougheachfilter,youmayneedtosetuprules.Forexample,withtheClassMethodshydrator,agivenmethodnameisvalidifthefollowingconditionismet:
Convertobjectstoarraysandbackwithzend-hydrator
27
(matches"is"||matches"has"||matches"get")&&matches"optionalparameters"
Assuch,whencallingaddFilter(),youcanspecifyanoptionalthirdargument:aflagindicatingwhethertoORorANDthegivenfilter(usingthevaluesFilterComposite::CONDITION_ORorFilterComposite::FILTER_AND);thedefaultistoORthenewfilter.
Filteringisverypowerfulandflexible.Ifyourememberonlytwothingsaboutfilters:
Theyonlyoperateduringextraction.Theycanonlybeusedtodeterminewhatvaluestokeepintheextracteddataset.
StrategiesWhatifyouwantedtoalterthevaluesreturnedduringextractionorhydration?zend-hydratorprovidesthesefeaturesviastrategies.
Astrategyprovidesfunctionalitybothforextractingandhydratingavalue,andsimplytransformsit;thinkofstrategiesasnormalizationfilters.EachimplementsZend\Hydrator\Strategy\StrategyInterface:
namespaceZend\Hydrator\Strategy;
interfaceStrategyInterface
{
publicfunctionextract($value;)
publicfunctionhydrate($value;)
}
Likefilters,aStrategyEnabledInterfaceallowsahydratortoindicateitacceptsstrategies,andtheAbstractHydratorimplementsthisinterface,allowingyoutousestrategiesoutoftheboxwiththeshippedhydrators.
UsingourpreviousUserexample,wecould,insteadofomittingthepasswordvalue,insteadreturnastatic********value;astrategycouldallowustodothat.Datasubmittedwouldbeinsteadhashedusingpassword_hash():
Convertobjectstoarraysandbackwithzend-hydrator
28
namespaceAcme;
useZend\Hydrator\Strategy\StrategyInterface;
classPasswordStrategyimplementsStrategyInterface
{
publicfunctionextract($value)
{
return'********';
}
publicfunctionhydrate($value)
{
returnpassword_hash($value);
}
}
Wewouldthenextractourdataasfollows:
useAcme\PasswordStrategy;
useZend\Hydrator\ObjectPropertyasObjectPropertyHydrator;
$hydrator=newObjectPropertyHydrator();
$hydrator->addStrategy('password',newPasswordStrategy());
$data=$hydrator->extract($user);
zend-hydratorshipswithanumberofreallyusefulstrategiesforcommondata:
BooleanStrategywillconvertbooleansintoothervalues(suchas0and1,orthestringstrueandfalse)andviceversa,accordingtoamapyouprovidetotheconstructor.ClosureStrategyallowsyoutoprovidecallbacksforeachofextractionandhydration,allowingyoutoforegotheneedtocreateacustomstrategyimplementation.DateTimeFormatterStrategywillconvertbetweenstringsandDateTimeinstances.ExplodeStrategyisawrapperaroundimplodeandexplode(),andexpectsadelimitertoitsconstructor.StrategyChainallowsyoutocomposemultiplestrategies;thereturnvalueofeachispassedasthevaluetothenext,providingafilterchain.
FilteringpropertynamesWecannowfilterpropertiestoomitfromourrepresentations,aswellasfilterornormalizethevaluesweultimatelywanttorepresent.Whataboutthepropertynames,though?
Convertobjectstoarraysandbackwithzend-hydrator
29
InPHP,weoftenusecamelCasetorepresentproperties,butsnake_caseistypicallymoreacceptedforAPIs.Additionally,whataboutwhenweusegettersforourvalues?Welikelydon'twanttousetheactualmethodnameasthepropertyname!
Forthisreason,zend-hydratorprovidesnamingstrategies.Theseworkjustlikestrategies,butinsteadofworkingonthevalue,theyworkonthepropertyname.Likebothfiltersandstrategies,aninterface,NamingStrategyEnabledInterface,allowsahydratortoindicatecanacceptanamingstrategy,andtheAbstractHydratorimplementsthatinterface,toallowoutoftheboxusageofnamingstrategiesontheshippedhydrators.
Asanexample,let'sconsiderthefollowingclass:
namespaceAcme;
classTransaction
{
public$isPublished;
public$publishedOn;
public$updatedOn;
}
Let'snowextractaninstanceofthatclass:
useAcme\Transaction;
useZend\Hydrator\NamingStrategy\UnderscoreNamingStrategy;
useZend\Hydrator\ObjectPropertyasObjectPropertyHydrator;
$hydrator=newObjectPropertyHydrator();
$hydrator->setNamingStrategy(newUnderscoreNamingStrategy());
$data=$hydrator->extract($transaction);
Theextracteddatawillnowhavethekeysis_published,published_on,andupdated_on!
ThisisusefulifyouknowallyourpropertieswillbecamelCased,butwhatifyouhaveotherneeds?Forinstance,whatifyouwanttorenameisPublishedtopublishedinstead?
ACompositeNamingStrategyclassallowsyoutodoexactlythat.Itacceptsanassociativearrayofobjectpropertynamesmappedtothenamingstrategytousewithit.So,asanexample:
Convertobjectstoarraysandbackwithzend-hydrator
30
useAcme\Transaction;
useZend\Hydrator\NamingStrategy\CompositeNamingStrategy;
useZend\Hydrator\NamingStrategy\MapNamingStrategy;
useZend\Hydrator\NamingStrategy\UnderscoreNamingStrategy;
useZend\Hydrator\ObjectPropertyasObjectPropertyHydrator;
$underscoreNamingStrategy=newUnderscoreNamingStrategy();
$namingStrategy=newCompositeNamingStrategy([
'isPublished'=>newMapNamingStrategy(['published'=>'isPublished']),
'publishedOn'=>$underscoreNamingStrategy,
'updatedOn'=>$underscoreNamingStrategy,
]);
$hydrator=newObjectPropertyHydrator();
$hydrator->setNamingStrategy($namingStrategy);
$data=$hydrator->extract($transaction);
Ourdatawillnowhavethekeyspublished,published_on,andupdated_on!
Unfortunately,ifwetryandhydrateusingourCompositeNamingStrategy,we'llrunintoissues;theCompositeNamingStrategydoesnotknowhowtomapthenormalized,extractedpropertynamestothosetheobjectacceptsbecauseitmapsapropertynametothenamingstrategy.So,tofixthat,weneedtoaddthereversekeys:
$mapNamingStrategy=newMapNamingStrategy(['published'=>'isPublished']);
$underscoreNamingStrategy=newUnderscoreNamingStrategy();
$namingStrategy=newCompositeNamingStrategy([
//Extraction:
'isPublished'=>$mapNamingStrategy,
'publishedOn'=>$underscoreNamingStrategy,
'updatedOn'=>$underscoreNamingStrategy,
//Hydration:
'published'=>$mapNamingStrategy,
'published_on'=>$underscoreNamingStrategy,
'updated_on'=>$underscoreNamingStrategy,
]);
DelegationSometimeswewanttocomposeasinglehydrator,butdon'tknowuntilruntimewhatobjectswe'llbeextractingorhydrating.Agreatexampleofthisiswhenusingzend-db'sHydratingResultSet,wherethehydratormayvarybasedonthetablefromwhichwepull
Convertobjectstoarraysandbackwithzend-hydrator
31
values.Othertimes,wemaywanttousethesamebasichydratortype,butcomposedifferentfilters,strategies,ornamingstrategiesbasedontheobjectwewishtohydrateorextract.
Toaccommodatethesescenarios,wehavetwofeatures.ThefirstisZend\Hydrator\HydratorPluginManager.ThisisaspecializedZend\ServiceManager\AbstractPluginManagerforretrievingdifferenthydratorinstances.Whenusedinzend-mvcorExpressiveapplications,itcanbeconfiguredviathehydratorsconfigurationkey,whichusesthesemanticsforzend-servicemanager,andmapstheservicetoHydratorManager.
Asanexample,wecouldhavethefollowingconfiguration:
return[
'hydrators'=>[
'factories'=>[
'Acme\BookHydrator'=>\Acme\BookHydratorFactory::class,
'Acme\AuthorHydrator'=>\Acme\AuthorHydratorFactory::class,
],
],
];
ManuallyconfiguringtheHydratorPluginManager
YoucanalsousetheHydratorPluginManagerprogrammatically:
$hydrators=newHydratorPluginManager();
$hydrators->setFactory('Acme\BookHydrator',\Acme\BookHydratorFactory::class);
$hydrators->setFactory('Acme\AuthorHydrator',\Acme\AuthorHydratorFactory::class)
;
Thefactoriesmightcreatestandardhydratorinstances,butconfigurethemdifferently:
Convertobjectstoarraysandbackwithzend-hydrator
32
namespaceAcme;
usePsr\Container\ContainerInterface;
useZend\Hydrator\ObjectProperty;
useZend\Hydrator\NamingStrategy\CompositeNamingStrategy;
useZend\Hydrator\NamingStrategy\UnderscoreNamingStrategy;
useZend\Hydrator\Strategy\DateTimeFormatterStrategy;
classBookHydratorFactory
{
publicfunction__invoke(ContainerInterface$container)
{
$hydrator=newObjectProperty();
$hydrator->addFilter('isbn',function($property){
return$property!=='isbn';
});
$hydrator->setNamingStrategy(newCompositeNamingStrategy([
'publishedOn'=>newUnderscoreNamingStrategy(),
]));
$hydrator->setStrategy(newCompositeNamingStrategy([
'published_on'=>newDateTimeFormatterStrategy(),
]));
return$hydrator;
}
}
classAuthorHydratorFactory
{
publicfunction__invoke(ContainerInterface$container)
{
$hydrator=newObjectProperty();
$hydrator->setNamingStrategy(newUnderscoreNamingStrategy());
return$hydrator;
}
}
YoucouldthencomposetheHydratorManagerserviceinyourownclass,andpullthesehydratorsinordertoextractorhydrateinstances:
$bookData=$hydrators->get('Acme\BookHydrator')->extract($book);
$authorData=$hydrators->get('Acme\AuthorHydrator')->extract($author);
TheDelegatingHydratorworksbycomposingaHydratorPluginManagerinstance,buthasanadditionalsemantic:itusestheclassnameoftheobjectitisextracting,ortheobjecttypetohydrate,astheservicenametopullfromtheHydratorPluginManager.Assuch,wewouldchangeourconfigurationofthehydratorsasfollows:
Convertobjectstoarraysandbackwithzend-hydrator
33
return[
'hydrators'=>[
'factories'=>[
\Acme\Book::class=>\Acme\BookHydratorFactory::class,
\Acme\Author::class=>\Acme\AuthorHydratorFactory::class,
],
],
];
Additionally,weneedtotellourapplicationabouttheDelegatingHydrator:
//zend-mvcapplications:
return[
'service_manager'=>[
'factories'=>[
\Zend\Hydrator\DelegatingHydrator::class=>\Zend\Hydrator\DelegatingHydra
torFactory::class
]
],
];
//Expressiveapplications
return[
'dependencies'=>[
'factories'=>[
\Zend\Hydrator\DelegatingHydrator::class=>\Zend\Hydrator\DelegatingHydra
torFactory::class
]
],
];
ManuallycreatingtheDelegatingHydrator
YoucaninstantiatetheDelegatingHydratormanually;whenyoudo,youpassitthe`HydratorPluginManagerinstance.
useZend\Hydrator\DelegatingHydrator;
useZend\Hydrator\HydratorPluginManager;
$hydrators=newHydratorPluginManager();
//...configurethepluginmanager...
$hydrator=newDelegatingHydrator($hydrators);
Technicallyspeaking,theDelegatingHydratorcanacceptanyPSR-11 containertoitsconstructor.
1
Convertobjectstoarraysandbackwithzend-hydrator
34
Fromthere,wecaninjecttheDelegatingHydratorintoanyofourownclasses,anduseittoextractorhydrateobjects:
$bookData=$hydrator->extract($book);
$authorData=$hydrator->extract($author);
Thisfeaturecanbequitepowerful,asitallowsyoutocreatethehydrationandextraction"recipes"forallofyourobjectswithintheirownfactories,ensuringthatanywhereyouneedthem,theyoperateexactlythesame.Italsomeansthatfortestingpurposes,youcansimplymocktheHydratorInterface(oritsparents,ExtractionInterfaceandHydrationInterface)insteadofcomposingaconcreteinstance.
OtherfeaturesWhilewe'vetriedtocoverthemajorityofthefunctionalityzend-hydratorprovidesinthisarticle,ithasanumberofotherusefulfeatures:
TheAggregateHydratorallowsyoutohandlecomplexobjectsthatimplementmultiplecommoninterfacesand/orhavenestedinstancescomposed;itevenexposeseventsyoucanlistentoduringeachofextractionandhydration.Youcanreadmoreaboutitinthedocumentation .YoucanwriteobjectsthatprovideandexposetheirownfiltersbyimplementingtheZend\Hydrator\Filter\FilterProviderInterface.YoucanhydrateorextractarraysofobjectsbyimplementingZend\Hydrator\Iterator\HydratingIteratorInterface.
Thecomponentcanbeseeninuseinanumberofplaces:zend-dbprovidesaHydratingResultSetthatleveragetheHydratorPluginManagerinordertohydrateobjectspulledfromadatabase.ApigilityusesthefeaturetoextractdataforHypertextApplicationLanguage(HAL)payloads.We'veevenseendeveloperscreatingcustomORMsfortheirapplicationusingthefeature!
Whatcanzend-hydratorhelpyoudotoday?
Footnotes
.https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md↩
.https://docs.zendframework.com/zend-hydrator/aggregate/↩
2
1
2
Convertobjectstoarraysandbackwithzend-hydrator
35
Convertobjectstoarraysandbackwithzend-hydrator
36
ScrapeScreenswithzend-dombyMatthewWeierO'Phinney
Eveninthisday-and-ageofreadilyavailableAPIsandRSS/Atomfeeds,manysitesoffernoneofthem.Howdoyougetatthedatainthosecases?Throughtheancientinternetartofscreenscraping.
Theproblemthenbecomes:howdoyougetatthedatayouneedinapileofHTMLsoup?YoucoulduseregularexpressionsoranyofthevariousstringfunctionsinPHP.Alloftheseareeasilysubjecttoerror,though,andoftenrequiresomeconvolutedcodetogetatthedataofinterest.
Alternately,youcouldtreattheHTMLasXML,andusetheDOMextension ,whichistypicallybuilt-intoPHP.Doingso,however,requiresmorethanapassingfamiliaritywithXPath ,whichissomethingofablackart.
IfyouuseJavaScriptlibrariesorwriteCSSfairlyoften,youmaybefamiliarwithCSSselectors,whichallowyoutotargeteitherspecificnodesorgroupsofnodeswithinanHTMLdocument.Thesearegenerallyratherintuitive:
jQuery('section.slideh2').each(function(node){
alert(node.textContent);
});
WhatifyoucoulddothatwithPHP?
Introducingzend-domzend-dom providesCSSselectorcapabilitiesforPHP,viatheZend\Dom\Queryclass,including:
elementtypes(h2,span,etc.)classattributes(.error,.next,etc.)elementidentifiers(#nav,#main,etc.)arbitraryelementattributes(div[onclick="foo"]),includingwordmatches(div[role~="navigation"])andsubstringmatches(div[role*="complement"])descendents(div.foospan)
WhileitdoesnotimplementthefullspectrumofCSSselectors,itdoesprovideenoughtogenerallyallowyoutogetattheinformationyouneedwithinapage.
1
2
3
ScrapeScreenswithzend-dom
37
Example:retrievinganavigationlistAsanexample,let'sfetchthenavigationlistfromtheZend\Dom\Querydocumentationpageitself:
useZend\Dom\Query;
$html=file_get_contents('https://docs.zendframework.com/zend-dom/query/');
$query=newQuery($html);
$results=$query->execute('ul.bs-sidenavlia');
printf("Received%dresults:\n",count($results));
foreach($resultsas$result){
printf("-[%s](%s)\n",$result->getAttribute('href'),$result->textContent);
}
Theabovequeriesforul.bs-sidenavlia—inotherwords,alllinkswithinlistitemsofthesidenavunorderedlist.
Whenyouexecute()aquery,youarereturnedaZend\Dom\NodeListinstance,whichdecoratesaDOMNodeList inordertoprovidefeaturessuchasCountable,andaccesstotheoriginalqueryanddocument.Intheexampleabove,wecount()theresults,andthenloopoverthem.
EachiteminthelistisaDOMNode ,givingyouaccesstoanyattributes,thetextcontent,andanychildelements.Inourcase,weaccessthehrefattribute(thelinktarget),andreportthetextcontent(thelinktext).
Theresultsare:
Received3results:
-[#querying-html-and-xml-documents](QueryingHTMLandXMLDocuments)
-[#theory-of-operation](TheoryofOperation)
-[#methods-available](MethodsAvailable)
OtherusesAnotherusecaseistesting.WhenyouhaveclassesthatreturnHTML,orifyouwanttoexecuterequestsandtestthegeneratedoutput,youoftendon'twanttotestexactcontents,butratherlookforspecificdataorfragmentswithinthedocument.
4
5
6 7
ScrapeScreenswithzend-dom
38
Weprovidethesecapabilitiesforzend-mvc applicationsviathezend-testcomponent ,whichprovidesanumberofCSSselectorassertions foruseinqueryingthecontentreturnedinyourMVCresponses.Havingthesecapabilitiesallowstestingfordynamiccontentaswellasstaticcontent,providinganumberofvectorsforensuringapplicationquality.
Startscraping!Wehopeyoucanappreciatethepowerfulcapabilitiesofthiscomponent!Wehaveusedthisfunctionalityinavarietyofways,fromtestingapplicationstocreatingfeedsbasedoncontentdifferencesinwebpages,tofindingandretrievingimageURIsfrompages.
Getmoreinformationfromthezend-domdocumentation .
Footnotes
.http://php.net/dom↩
.https://en.wikipedia.org/wiki/XPath↩
.https://docs.zendframework.com/zend-dom/↩
.http://php.net/class.domnodelist↩
.http://php.net/class.domnode↩
.https://docs.zendframework.com/zend-mvc/↩
.https://docs.zendframework.com/zend-test/↩
.https://docs.zendframework.com/zend-test/assertions/#css-selector-assertions↩
.https://docs.zendframework.com/zend-dom/↩
6 78
9
1
2
3
4
5
6
7
8
9
ScrapeScreenswithzend-dom
39
Paginatingdatacollectionswithzend-paginatorbyEnricoZimuel
zend-paginator isaflexiblecomponentforpaginatingcollectionsofdataandpresentingthatdatatousers.
Pagination isastandardUIsolutiontomanagethevisualizationoflistsofitems,likealistofpostsinablogoralistofproductsinanonlinestore.
zend-paginatorisverypopularamongZendFrameworkdevelopers,andit'softenusedwithzend-view ,thankstothepaginationcontrolviewhelperzend-viewprovides.
Itcanbeusedalsowithothertemplateengines.Inthisarticle,IwilldemonstratehowtouseitwithPlates .
Usageofzend-paginatorThecomponentcanbeinstalledviaComposer:
$composerrequirezendframework/zend-paginator
Toconsumethepaginatorcomponent,weneedacollectionofitems.zend-paginatorshipswithseveraldifferentadaptersforcommoncollectiontypes:
ArrayAdapter,whichworkswithPHParrays;Callback,whichallowsprovidingcallbacksforobtainingcountsofitemsandlistsofitems;DbSelect,toworkwithaSQLcollection(usingzend-db );DbTableGateway,toworkwithaTableDataGateway(usingtheTableGatewayfeaturefromzend-db.Iterator,toworkwithanyIterator instance.
Ifyourcollectiondoesnotfitoneoftheseadapters,youcancreateacustomadapter.Todoso,youwillneedtoimplementZend\Paginator\Adapter\AdapterInterface,whichdefinestwomethods:
count():int
getItems(int$offset,int$itemCountPerPage):array
1
2
3
4
5
6
Paginatingdatacollectionswithzend-paginator
40
Eachadapterneedstoreturnthetotalnumberofitemsinthecollection,implementingthecount()method,andaportion(apage)ofitemsstartingfrom$offsetpositionwithasizeof$itemCountPerPageperpage.
Withthesetwomethods,wecanusezend-paginatorwithanytypeofcollection.
Forinstance,imagineweneedtopaginateacollectionofblogpostsandwehaveaPostsclassthatmanagesalltheposts.Wecanimplementanadapterlikethis:
require'vendor/autoload.php';
useZend\Paginator\Adapter\AdapterInterface;
useZend\Paginator\Paginator;
useZend\Paginator\ScrollingStyle\Sliding;
classPostsimplementsAdapterInterface
{
private$posts=[];
publicfunction__construct()
{
//Readpostsfromfile/database/whatever
}
publicfunctioncount()
{
returncount($this->posts);
}
publicfunctiongetItems($offset,$itemCountPerPage)
{
returnarray_slice($this->posts,$offset,$itemCountPerPage);
}
}
$posts=newPosts();
$paginator=newPaginator($posts);
Paginator::setDefaultScrollingStyle(newSliding());
$paginator->setCurrentPageNumber(1);
$paginator->setDefaultItemCountPerPage(8);
foreach($paginatoras$post){
//Iterateoneachpost
}
$pages=$paginator->getPages();
var_dump($pages);
Paginatingdatacollectionswithzend-paginator
41
Inthisexample,wecreatedazend-paginatoradapterusingacustomPostsclass.Thisclassstoresthecollectionofpostsusingaprivatearray($posts).ThisadapteristhenpassedtoaninstanceofPaginator.
WhencreatingaPaginator,weneedtoconfigureitsbehavior.Thefirstsettingisthescrollingstyle.Intheexampleabove,weusedtheSliding style,aYahoo!-likescrollingstylethatpositionsthecurrentpagenumberascloseaspossibletothecenterofthepagerange.
Note:theSlidingscrollingstyleisthedefaultstyleusedbyzend-paginator.WeneedtosetitexplicitlyusingPaginator::setDefaultScrollingStyle()onlyifwedonotusezend-servicemanager asapluginmanager.Otherwise,thescrollingstyleisloadedbydefaultfromthepluginmanager.
Theothertwoconfigurationvaluesarethecurrentpagenumberandthenumberofitemsperpage.Intheexampleabove,westartedfrompage1,andwecount8itemsperpage.
Wecantheniterateonthe$paginatorobjecttoretrievethepostofthecurrentpageinthecollection.
Attheend,wecanretrievetheinformationregardingthepreviouspage,thenextpage,thetotalitemsinthecollection,andmore.TogetthesevaluesweneedtocallthegetPages()method.Wewillobtainanobjectlikethis:
7
8
Paginatingdatacollectionswithzend-paginator
42
object(stdClass)#81(13){
["pageCount"]=>
int(3)
["itemCountPerPage"]=>
int(8)
["first"]=>
int(1)
["current"]=>
int(1)
["last"]=>
int(3)
["next"]=>
int(2)
["pagesInRange"]=>
array(3){
[1]=>
int(1)
[2]=>
int(2)
[3]=>
int(3)
}
["firstPageInRange"]=>
int(1)
["lastPageInRange"]=>
int(3)
["currentItemCount"]=>
int(8)
["totalItemCount"]=>
int(19)
["firstItemNumber"]=>
int(1)
["lastItemNumber"]=>
int(8)
}
Usingthisinformation,wecaneasilybuildanHTMLfootertonavigateacrossthecollection.
Note:usingzend-view,wecanconsumethepaginationControl() helper,whichemitsanHTMLpaginationbar.
AnexampleusingPlatesPlates implementstemplatesusingnativePHP;itisfastandeasytouse,withoutanyadditionalmetalanguage;itisjustPHP.
Inourexample,wewillcreateaPlatestemplatetopaginateacollectionofdatausingzend-paginator.WewilluseBootstrap astheUIframework.
9
10
11
Paginatingdatacollectionswithzend-paginator
43
Forpurposesofthisexample,blogpostswillbeaccessibleviathefollowingURL:
/blog[/page/{page:\d+}]
where[/page/{page:\d+}]representstheoptionalpagenumber(usingtheregexp\d+tovalidateonlydigits).Ifweopenthe/blogURLwewillgetthefirstpageofthecollection.Toreturnthesecondpageweneedtoconnectto/blog/page/2,thirdpageto/blog/page/3,andsoon.
Forinstance,wecanmanagethepageparameterusingaPSR-7middlewareclassconsumingthepreviousPostsadapter,thatworksasfollow:
Paginatingdatacollectionswithzend-paginator
44
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\ServerRequestInterface;
useLeague\Plates\Engine;
useZend\Paginator\Paginator;
useZend\Paginator\ScrollingStyle\Sliding;
usePosts;
classPaginatorMiddleware
{
/**@varPosts*/
protected$posts;
/**@varEngine*/
protected$template;
publicfunction__construct(Posts$post,Engine$template=null)
{
$this->posts=$post;
$this->template=$template;
}
publicfunction__invoke(
ServerRequestInterface$request,
ResponseInterface$response,callable$next=null
){
$paginator=newPaginator($this->posts);
$page=$request->getAttribute('page',1);
Paginator::setDefaultScrollingStyle(newSliding());
$paginator->setCurrentPageNumber($page);
$paginator->setDefaultItemCountPerPage(8);
$pages=$paginator->getPages();
$response->getBody()->write(
$this->template->render('posts',[
'paginator'=>$paginator,
'pages'=>$pages,
])
);
return$response;
}
}
Weusedaposts.phptemplate,passingthepaginator($paginator)andthepages($pages)instances.Thattemplatecouldthenlooklikethefollowing:
Paginatingdatacollectionswithzend-paginator
45
<?php$this->layout('template',['title'=>'BlogPosts'])?>
<divclass="container">
<h1>BlogPosts</h1>
<?phpforeach($paginatoras$post):?>
<divclass="row">
<?php//printstheposttitle,date,author,...?>
</div>
<?phpendforeach?>
<?php$this->insert('page-navigation',['pages'=>$pages])?>
</div>
Thepage-navigation.phptemplatecontainstheHTMLcodeforthepagenavigationcontrol,withbuttonlikeprevious,next,andpagenumbers.
<navaria-label="Pagenavigation">
<ulclass="pagination">
<?phpif(!isset($pages->previous)):?>
<liclass="disabled"><ahref="#"aria-label="Previous"><spanaria-hidden="true">
«</span></a></li>
<?phpelse:?>
<li><ahref="/blog/page/<?=$pages->previous?>"aria-label="Previous"><spanari
a-hidden="true">«</span></a></li>
<?phpendif?>
<?phpforeach($pages->pagesInRangeas$num):?>
<?phpif($num===$pages->current):?>
<liclass="active"><ahref="/blog/page/<?=$num?>"><?=$num?><spanclass="s
r-only">(current)</span></a></li>
<?phpelse:?>
<li><ahref="/blog/page/<?=$num?>"><?=$num?></a></li>
<?phpendif?>
<?phpendforeach?>
<?phpif(!isset($pages->next)):?>
<liclass="disabled"><ahref="#"aria-label="Next"><spanaria-hidden="true">&raq
uo;</span></a></li>
<?phpelse:?>
<li><ahref="/blog/page/<?=$pages->next?>"aria-label="Next"><spanaria-hidden
="true">»</span></a></li>
<?phpendif?>
</ul>
</nav>
Summary
Paginatingdatacollectionswithzend-paginator
46
Thezend-paginatorcomponentofZendFrameworkisapowerfulandeasytousepackagethatprovidespaginationofdata.ItcanbeusedasstandalonecomponentinmanyPHPprojectsusingdifferentframeworksandtemplateengines.Inthisarticle,Idemonstratedhowtouseitingeneralpurposeapplications.Moreover,IshowedanexampleusingPlatesandBootstrap,inaPSR-7middlewarescenario.
Visitthezend-paginatordocumentation tofindoutwhatelseyoumightbeabletodowiththiscomponent!
Footnotes
.https://docs.zendframework.com/zend-paginator/↩
.https://en.wikipedia.org/wiki/Pagination↩
.https://docs.zendframework.co/zend-view/↩
.http://platesphp.com/↩
.https://docs.zendframework.com/zend-db/↩
.http://php.net/iterator↩
.https://github.com/zendframework/zend-paginator/blob/master/src/ScrollingStyle/Sliding.php↩
.https://docs.zendframework.com/zend-servicemanager/↩
.https://docs.zendframework.com/zend-paginator/usage/#rendering-pages-with-view-scripts↩
.http://platesphp.com/↩
.http://getbootstrap.com/↩
.https://docs.zendframework.com/zend-paginator/↩
12
1
2
3
4
5
6
7
8
9
10
11
12
Paginatingdatacollectionswithzend-paginator
47
LoggingPHPapplicationsbyEnricoZimuel
EveryPHPapplicationgenerateserrors,warnings,andnoticesandthrowsexceptions.Ifwedonotlogthisinformation,weloseawaytoidentifyandsolveproblemsatruntime.Moreover,wemayneedtologspecificactionssuchasauserloginandlogoutattempts.Allsuchinformationshouldbefilteredandstoredinanefficientway.
PHPoffersthefunctionerror_log() tosendanerrormessagetothedefinedsystemlogger,andthefunctionset_error_handler() tospecifyahandlerforinterceptingwarnings,errors,andnoticesgeneratedbyPHP.
Thesefunctionscanbeusedtocustomizeerrormanagement,butit'suptothedevelopertowritethelogictofilterandstorethedata.
ZendFrameworkoffersaloggingcomponent,zend-log ;thelibrarycanbeusedasageneralpurposeloggingsystem.Itsupportsmultiplelogbackends,formattingmessagessenttothelog,andfilteringmessagesfrombeinglogged.
Lastbutnotleast,zend-logiscompliantwithPSR-3 ,theloggerinterfacestandard.
InstallationYoucaninstallzend-log usingComposer:
composerrequirezendframework/zend-log
Usagezend-logcanbeusedtocreatelogentriesindifferentformatsusingmultiplebackends.Youcanalsofilterthelogdatafrombeingsaved,andprocessthelogeventpriortofilteringorwriting,allowingtheabilitytosubstitute,add,remove,ormodifythedatayoulog.
Basicusageofzend-logrequiresbothawriterandaloggerinstance.Awriterstoresthelogentryintoabackend,andtheloggerconsumesthewritertoperformloggingoperations.
Asanexample:
12
3
4
5
LoggingPHPapplications
48
useZend\Log\Logger;
useZend\Log\Writer\Stream;
$logger=newLogger;
$writer=newStream('php://output');
$logger->addWriter($writer);
$logger->log(Logger::INFO,'Informationalmessage');
Theaboveproducesthefollowingoutput:
2017-09-11T15:07:46+02:00INFO(6):Informationalmessage
Theoutputisastringcontainingatimestamp,apriority(INFO(6))andthemessage(Informationalmessage).TheoutputformatcanbechangedusingthesetFormatter()methodofthewriterobject($writer).Thedefaultlogformat,producedbytheSimpleformatter,isasfollows:
%timestamp%%priorityName%(%priority%):%message%%extra%
where%extra%isanoptionalvaluecontainingadditionalinformation.
Forinstance,ifyouwantedtochangetheformattoincludeonlylog%message%,youcoulddothefollowing:
$formatter=newZend\Log\Formatter\Simple('log%message%'.PHP_EOL);
$writer->setFormatter($formatter);
LogPHPeventszend-logcanalsobeusedtologPHPerrorsandexceptions.YoucanlogPHPerrorsusingthestaticmethodLogger::registerErrorHandler($logger)andinterceptexceptionsusingthestaticmethodLogger::registerExceptionHandler($logger).
6
LoggingPHPapplications
49
useZend\Log\Logger;
useZend\Log\Writer;
$logger=newLogger;
$writer=newWriter\Stream(__DIR__.'/test.log');
$logger->addWriter($writer);
//LogPHPerrors
Logger::registerErrorHandler($logger);
//Logexceptions
Logger::registerExceptionHandler($logger);
FilteringdataAsmentioned,wecanfilterthedatatobelogged;filteringremovesmessagesthatmatchthefiltercriteria,preventingthemfrombeinglogged.
WecanusetheaddFilter()methodoftheWriterinterface toaddaspecificfilter.
Forinstance,wecanfilterbypriority,acceptingonlylogentrieswithaprioritylessthanorequaltoaspecificvalue:
$filter=newZend\Log\Filter\Priority(Logger::CRIT);
$writer->addFilter($filter);
Intheaboveexample,theloggerwillonlystorelogentrieswithaprioritylessthanorequaltoLogger::CRIT(critical).TheprioritiesaredefinedbytheZend\Log\Loggerclass:
constEMERG=0;//Emergency:systemisunusable
constALERT=1;//Alert:actionmustbetakenimmediately
constCRIT=2;//Critical:criticalconditions
constERR=3;//Error:errorconditions
constWARN=4;//Warning:warningconditions
constNOTICE=5;//Notice:normalbutsignificantcondition
constINFO=6;//Informational:informationalmessages
constDEBUG=7;//Debug:debugmessages
Assuch,onlyemergency,alerts,orcriticalentrieswouldbelogged.
Wecanalsofilterlogdatabasedonregularexpressions,timestamps,andmore.Onepowerfulfilterusesazend-validator ValidatorInterfaceinstancetofilterthelog;onlyvalidentrieswouldbeloggedinsuchcases.
7
8
LoggingPHPapplications
50
ProcessingdataIfyouneedtoprovideadditionalinformationtologsinanautomatedfashion,youcanuseaZend\Log\Processerclass.Aprocessorisexecutedbeforethelogdataarepassedtothewriter.Theinputofaprocessorisalogevent,anarraycontainingalloftheinformationtolog;theoutputisalsoalogevent,butcancontainmodifiedoradditionalvalues.Aprocessormodifiesthelogeventtopriortosendingittothewriter.
Youcanreadaboutprocessoradaptersofferedbyzend-loginthedocumentation .
MultiplebackendsOneofthecoolfeatureofzend-logisthepossibilitytowritelogsusingmultiplebackends.Forinstance,youcanwritealogtobothafileandadatabaseusingthefollowingcode:
9
LoggingPHPapplications
51
useZend\Db\Adapter\AdapterasDbAdapter;
useZend\Log\Formatter;
useZend\Log\Writer;
useZend\Log\Logger;
//Createouradapter
$db=newDbAdapter([
'driver'=>'Pdo',
'dsn'=>'mysql:dbname=testlog;host=localhost',
'username'=>'root',
'password'=>'password'
]);
//Mapeventdatatodatabasecolumns
$mapping=[
'timestamp'=>'date',
'priority'=>'type',
'message'=>'event',
];
//Createourdatabaselogwriter
$writerDb=newWriter\Db($db,'log',$mapping);//logtable
$formatter=newFormatter\Base();
$formatter->setDateTimeFormat('Y-m-dH:i:s');//MySQLDATETIMEformat
$writerDb->setFormatter($formatter);
//Createourfilelogwriter
$writerFile=newWriter\Stream(__DIR__.'/test.log');
//Createourloggerandregisterbothwriters
$logger=newLogger();
$logger->addWriter($writerDb,1);
$logger->addWriter($writerFile,100);
//Loganinformationmessage
$logger->info('Informationalmessage');
Thedatabasewriterrequiresthecredentialstoaccessthetablewhereyouwillstoreloginformation.Youcancustomizethefieldnamesforthedatabasetableusinga$mappingarray,containinganassociativearraymappinglogfieldstodatabasecolumns.
Thedatabasewriteriscomposedin$writerDbandthefilewriterin$writerFile.ThewritersareaddedtotheloggerusingtheaddWriter()methodwithaprioritynumber;higherintegervaluesindicatehigherpriority(triggeredearliest).Wechosepriority1forthedatabasewriter,andpriority100forthefilewriter;thismeansthefilewriterwilllogfirst,followedbyloggingtothedatabase.
Note:weusedaspecialdateformatterforthedatabasewriter.ThisisrequiredtotranslatethelogtimestampintotheDATETIMEformatofMySQL.
LoggingPHPapplications
52
PSR-3supportIfyouneedtobecompatiblewithPSR-3 ,youcanuseZend\Log\PsrLoggerAdapter.ThisloggercanbeusedanywhereaPsr\Log\LoggerInterfaceisexpected.
Asanexample:
usePsr\Log\LogLevel;
useZend\Log\Logger;
useZend\Log\PsrLoggerAdapter;
$zendLogLogger=newLogger;
$psrLogger=newPsrLoggerAdapter($zendLogLogger);
$psrLogger->log(LogLevel::INFO,'WehaveaPSR-compatiblelogger');
ToselectaPSR-3backendforwriting,wecanusetheZend\Log\Writer\Psrclass.Inordertouseit,youneedtopassaPsr\Log\LoggerInterfaceinstancetothe$psrLoggerconstructorargument:
$writer=newZend\Log\Writer\Psr($psrLogger);
zend-logalsosupportsPSR-3messageplaceholders viatheZend\Log\Processor\PsrPlaceholderclass.Touseit,youneedtoaddaPsrPlaceholderinstancetoalogger,usingtheaddProcess()method.Placeholdernamescorrespondtokeysinthe"extra"arraypassedwhenloggingamessage:
useZend\Log\Logger;
useZend\Log\Processor\PsrPlaceholder;
$logger=newLogger;
$logger->addProcessor(newPsrPlaceholder);
$logger->info('Userwithemail{email}registered',['email'=>'[email protected]']);
AninformationallogentrywillbestoredwiththemessageUserwithemailuser@example.orgregistered.
LogginganMVCapplicationIfyouareusingazend-mvc basedapplication,youcanusezend-logasmodule.zend-logprovidesaModule.php class,whichregistersZend\Logasamoduleinyourapplication.
10
11
1213
LoggingPHPapplications
53
Inparticular,thezend-logmoduleprovidesthefollowingservices(underthenamespaceZend\Log):
Logger::class=>LoggerServiceFactory::class,
'LogFilterManager'=>FilterPluginManagerFactory::class,
'LogFormatterManager'=>FormatterPluginManagerFactory::class,
'LogProcessorManager'=>ProcessorPluginManagerFactory::class,
'LogWriterManager'=>WriterPluginManagerFactory::class,
TheLogger::classservicecanbeconfiguredusingthelogconfigkey;thedocumentationprovidesconfigurationexamples .
InordertousetheLoggerserviceinyourMVCstack,grabitfromtheservicecontainer.Forinstance,youcanpasstheLoggerserviceinacontrollerusingafactory:
useZend\Log\Logger;
useZend\ServiceManager\Factory\FactoryInterface;
classIndexControllerFactoryimplementsFactoryInterface
{
publicfunction__invoke(
ContainerInterface$container,
$requestedName,
array$options=null
){
returnnewIndexController(
$container->get(Logger::class)
);
}
}
viathefollowingserviceconfigurationfortheIndexController:
'controllers'=>[
'factories'=>[
IndexController::class=>IndexControllerFactory::class,
],
],
LoggingamiddlewareapplicationYoucanalsointegratezend-loginyourmiddlewareapplications.IfyouareusingExpressive ,addthecomponent'sConfigProvider toyourconfig/config.phpfile.
14
15 16
17
LoggingPHPapplications
54
Note:ifyouareusingzend-component-installer ,youwillbepromptedtoinstallzend-log'sconfigproviderwhenyouinstallthecomponentviaComposer.
Note:Thisconfigurationregistersthesameservicesprovidedinthezend-mvcexample,above.
Tousezend-loginmiddleware,grabitfromthedependencyinjectioncontainerandpassitasadependencytoyourmiddleware:
namespaceApp\Action;
usePsr\Container\ContainerInterface;
useZend\Log\Logger;
classHomeActionFactory
{
publicfunction__invoke(ContainerInterface$container):HomeAction
{
returnnewHomeAction(
$container->get(Logger::class)
);
}
}
Asanexampleoflogginginmiddleware:
17
LoggingPHPapplications
55
namespaceApp\Action;
useInterop\Http\ServerMiddleware\DelegateInterface;
useInterop\Http\ServerMiddleware\MiddlewareInterfaceasServerMiddlewareInterface;
usePsr\Http\Message\ServerRequestInterface;
useZend\Log\Logger;
classHomeActionimplementsServerMiddlewareInterface
{
private$logger;
publicfunction__construct(Logger$logger)
{
$this->logger=logger;
}
publicfunctionprocess(
ServerRequestInterface$request,
DelegateInterface$delegate
){
$this->logger->info(__CLASS__.'hasbeenexecuted');
//...
}
}
ListeningforerrorsinExpressiveExpressiveandStratigility provideadefaulterrorhandlermiddlewareimplementation,Zend\Stratigility\Middleware\ErrorHandlerwhichlistensforPHPerrorsandexceptions/throwables.Bydefault,itspitsoutasimpleerrorpagewhenanerroroccurs,butitalsoprovidestheabilitytoattachlisteners,whichcanthenactontheprovidederror.
Listenersreceivetheerror,therequest,andtheresponsethattheerrorhandlerwillbereturning.Wecanusethatinformationtologinformation!
First,wecreateanerrorhandlerlistenerthatcomposesalogger,andlogstheinformation:
18
LoggingPHPapplications
56
useException;
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\ServerRequestInterface;
useThrowable;
useZend\Log\Logger;
classLoggingErrorListener
{
/**
*Logmessagestringwithplaceholders
*/
constLOG_STRING='{status}[{method}]{uri}:{error}';
private$logger;
publicfunction__construct(Logger$logger)
{
$this->logger=$logger;
}
publicfunction__invoke(
$error,
ServerRequestInterface$request,
ResponseInterface$response
){
$this->logger->error(self::LOG_STRING,[
'status'=>$response->getStatusCode(),
'method'=>$request->getMethod(),
'uri'=>(string)$request->getUri(),
'error'=>$error->getMessage(),
]);
}
}
TheErrorHandlerimplementationcastsPHPerrorstoErrorExceptioninstances,whichmeansthat$errorisalwayssomeformofthrowable.
WecanthenwriteadelegatorfactorythatwillregisterthisasalistenerontheErrorHandler:
LoggingPHPapplications
57
useLoggingErrorListener;
usePsr\Container\ContainerInterface;
useZend\Log\Logger;
useZend\Log\Processor\PsrPlaceholder;
useZend\Stratigility\Middleware\ErrorHandler;
classLoggingErrorListenerFactory
{
publicfunction__invoke(
ContainerInterface$container,
$serviceName,
callable$callback
):ErrorHandler{
$logger=$container->get(Logger::class);
$logger->addProcessor(newPsrPlaceholder());
$listener=newLoggingErrorListener($logger);
$errorHandler=$callback();
$errorHandler->attachListener($listener);
return$errorHandler;
}
}
Andthenregisterthedelegatorinyourconfiguration:
//InaConfigProvider,oraconfig/autoload/*.global.phpfile:
useLoggingErrorListenerFactory;
useZend\Stratigility\Middleware\ErrorHandler;
return[
'dependencies'=>[
'delegators'=>[
ErrorHandler::class=>[
LoggingErrorListenerFactory::class,
],
],
],
];
Atthispoint,yourerrorhandlerwillnowalsologerrorstoyourconfiguredwriters!
SummaryThezend-logcomponentoffersawidesetoffeatures,includingsupportformultiplewriters,filteringoflogdata,compatibilitywithPSR-3 ,andmore.19
LoggingPHPapplications
58
Hopefullyyoucanusetheexamplesaboveforconsumingzend-loginyourstandalone,zend-mvc,Expressive,orgeneralmiddlewareapplications!
Learnmoreinthezend-logdocumentation .
Footnotes
.http://php.net/error_log↩
.http://php.net/set_error_handler↩
.https://docs.zendframework.com/zend-log/↩
.http://www.php-fig.org/psr/psr-3/↩
.https://docs.zendframework.com/zend-log/↩
.https://github.com/zendframework/zend-log/blob/master/src/Formatter/Simple.php↩
.https://github.com/zendframework/zend-log/blob/master/src/Writer/WriterInterface.php↩
.https://docs.zendframework.com/zend-validator/↩
.https://docs.zendframework.com/zend-log/processors/↩
.http://www.php-fig.org/psr/psr-3/↩
.http://www.php-fig.org/psr/psr-3/#12-message↩
.https://docs.zendframework.com/zend-mvc/↩
.https://github.com/zendframework/zend-log/blob/master/src/Module.php↩
.https://docs.zendframework.com/zend-log/service-manager/#zend-log-as-a-module↩
.https://docs.zendframework.com/zend-expressive/↩
.https://github.com/zendframework/zend-log/blob/master/src/ConfigProvider.php↩
.https://docs.zendframework.com/zend-component-installer/↩
.https://docs.zendframework.com/zend-stratigility/↩
.http://www.php-fig.org/psr/psr-3/↩
.https://docs.zendframework.com/zend-log/↩
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LoggingPHPapplications
59
LoggingPHPapplications
60
DiscoverandReadRSSandAtomFeedsbyMatthewWeierO'Phinney
RememberRSSandAtomfeeds?
Chancesare,youmayhavediscoveredthisbookbecauseitwasannouncedonafeed:
AnumberofTwitterservicespollfeedsandsendlinkswhennewentriesarediscovered.SomeofyoumaybeusingfeedreaderssuchasFeedly .Manynewsaggregatorservices,includingtoolssuchasGoogleNow,useRSSandAtomfeedsassources.
Aninterestingfact:AtomitselfisoftenusedasadatatransferformatforRESTservices,particularlycontentmanagementplatforms!Assuch,beingfamiliarwithfeedsandhavingtoolstoworkwiththemisanimportantskillforawebdeveloper!
Inthisfirstofatwopartseriesonfeeds,we'lllookatfeeddiscovery,aswellasreading,usingzend-feed'sReadersubcomponent.
GettingstartedFirst,ofcourse,youneedtoinstallzend-feed:
$composerrequirezendframework/zend-feed
Asofversion2.6.0,thecomponenthasaveryminimalsetofdependencies:itonlyrequireszendframework/zend-escaperandzendframework/zend-stdlibinordertowork.Ithasanumberofadditional,optionalrequirementsdependingonfeaturesyouwanttoopt-into:
psr/http-messageand/orzend-http,toallowpollingpagesforfeeds,feedsthemselves,orPubSubHubbubservices.zendframework/zend-cache,toallowcachingfeedsbetweenrequests.zendframework/zend-db,whichisusedwhenusingthePubSubHubbubsubcomponent,inorderforPuSHsubscriberstostoreupdates.zendframework/zend-validator,forvalidatingaddressesusedinAtomfeedsandentrieswhenusingtheWritersubcomponent.
Forourexamples,wewillneedanHTTPclientinordertofetchpages.Forthesakeofsimplicity,we'llgoaheadandusezendframework/zend-http;ifyouarealreadyusingGuzzleinyourapplication,youcancreateawrapperforitfollowinginstructionsinthezend-feed
1
2
DiscoverandReadRSSandAtomFeeds
61
manual .
$composerrequirezendframework/zend-http
Nowthatwehavethesepiecesinplace,wecanmoveontolinkdiscovery!
LinkdiscoveryTheReadersubcomponentcontainsfacilitiesforfindingAtomandRSSlinkswithinanHTMLpage.Let'strythisnow:
//Indiscovery.php:
useZend\Feed\Reader\Reader;
require'vendor/autoload.php';
$feedUrls=[];
$feedLinks=Reader::findFeedLinks('https://framework.zend.com');
foreach($feedLinksas$link){
switch($link['type']){
case'application/atom+xml':
$feedUrls[]=$link['href'];
break;
case'application/rss+xml':
$feedUrls[]=$link['href'];
break;
}
}
var_export($feedUrls);
Ifyouruntheabove,youshouldgetalistlikethefollowing(atthetimeofwriting):
array(
0=>'https://framework.zend.com/security/feed',
1=>'https://framework.zend.com/blog/feed-atom.xml',
2=>'https://framework.zend.com/blog/feed-rss.xml',
3=>'https://framework.zend.com/releases/atom.xml',
4=>'https://framework.zend.com/releases/rss.xml',
)
That'sratheruseful!Wecanpollapagetodiscoverlinks,andthenfollowthem!
2
DiscoverandReadRSSandAtomFeeds
62
Internally,thereturned$feedLinksisaZend\Feed\Reader\FeedSetinstance,whichisreallyjustanArrayObjectwhereeachitemitcomposesisitselfaFeedSetwithspecificattributesset(includingthetype,href,andrel,usually).Itonlyreturnslinksthatareknownfeedtypes;anyothertypeoflinkisignored.
ReadingafeedNowthatweknowwheresomefeedsare,wecanreadthem.
Todothat,wepassaURLforafeedtothereader,andthenpulldatafromthereturnedfeed:
//Inreader.php:
useZend\Feed\Reader\Reader;
require'vendor/autoload.php';
$feed=Reader::import('https://framework.zend.com/releases/rss.xml');
printf(
"[%s](%s):%s\n",
$feed->getTitle(),
$feed->getLink(),
$feed->getDescription()
);
Theabovewillresultin:
[ZendFrameworkReleases](https://github.com/zendframework):ZendFrameworkandzfcamp
usreleases
Theaboveisconsideredthefeedchanneldata;it'sinformationaboutthefeeditself.Mostlikely,though,wewanttoknowwhatentriesareinthefeed!
GettingfeedentriesThefeedreturnedbyReader::import()isitselfiterable,whicheachitemofiterationbeinganentry.Atitsmostbasic:
DiscoverandReadRSSandAtomFeeds
63
foreach($feedas$entry){
printf(
"[%s](%s):%s\n",
$entry->getTitle(),
$entry->getLink(),
$entry->getDescription()
);
}
Thiswillloopthrougheachentry,listingthetitle,thecanonicallinktotheitem,andadescriptionoftheentry.
Theabovewillworkacrossanytypeoffeed.However,feedcapabilitiesvarybasedontype.RSSandAtomfeedentrieswillhavedifferentdataavailable;infact,Atomisconsideredanextensibleprotocol,whichmeansthatsuchentriescanpotentiallyexposequitealotofadditionaldata!
Youmaywanttoreaduponwhat'savailable;followthefootnotestofindrelevantlinks:
RSSentrypropertiesAtomentries
Untilnexttimezend-feed'sReadersubcomponentoffersanumberofothercapabilities,including:
Importingactualfeedstrings(versusfetchingviaanHTTPclient)TheabilitytoutilizealternateHTTPclients.TheabilitytoextendtheAtomprotocolinordertoaccessadditionaldata.
Thezend-feedcomponenthasextensivedocumentation ,whichwillanswermostquestionsyoumayhaveatthispoint.
Wehopethisquickprimergetsyoustartedconsumingfeeds!
Footnotes
.https://feedly.com↩
.https://docs.zendframework.com/zend-feed/psr7-clients/↩
.https://docs.zendframework.com/zend-feed/consuming-rss/#get-properties↩
.https://docs.zendframework.com/zend-feed/consuming-atom/↩
34
5
1
2
3
4
5
DiscoverandReadRSSandAtomFeeds
64
.https://docs.zendframework.com/zend-feed/↩5
DiscoverandReadRSSandAtomFeeds
65
CreateRSSandAtomFeedsbyMatthewWeierO'Phinney
Inthepreviousarticle,wedetailedRSSandAtomfeeddiscoveryandparsing.Inthisarticlewe'regoingtocoveritscomplement:feedcreation!
zend-feedprovidestheabilitytocreatebothAtom1.0andRSS2.0feeds,andevensupportscustomextensionsduringfeedgeneration,including:
Atom(xmlns:atom;RSS2only):providelinkstoAtomfeedsandPubsubhubbubURIswithinyourRSSfeed.Content(xmlns:content;RSS2only):provideCDATAencodedcontentforindividualfeeditems.DublinCore(xmlns:dc;RSS2only):providemetadataaroundcommoncontentelementssuchasauthor/publisher/contributor/creator,dates,languages,etc.iTunes(xmlns:itunes):createpodcastfeedsanditemscompatiblewithiTunes.Slash(xmlns:slash;RSS2only):communicatecommentcountsperitem.Threading(xmlns:thr;RSS2only):providemetadataaroundthreadingfeeditems,includingindicatingwhatanitemisinreplyto,linkingtoreplies,andmetricsaroundeach.WellFormedWeb(xmlns:wfw;RSS2only):providealinktoaseparatecommentsfeedforagivenentry.
Youcanalsoprovideyourowncustomextensionsifdesired;thesearejustwhatweshipoutofthebox!Inmanycases,youdon'tevenneedtoknowabouttheextensions,aszend-feedwilltakecareofaddinginthosethatarerequired,basedonthedatayouprovideinthefeedandentries.
CreatingafeedThefirststep,ofcourse,ishavingsomecontent!I'llassumeyouhaveitemsyouwanttopublish,andthosewillbein$data,whichwe'llloopover.Howthatdatalookswillbedependentonyourapplication,sopleasebeawarethatyoumayneedtoadjustanyexamplesbelowtofityourowndatasource.
Next,weneedtohavezend-feedinstalled;dothatviaComposer:
$composerrequirezendframework/zend-feed
CreateRSSandAtomFeeds
66
Nowwecanfinallygetstarted.We'llbeginbycreatingafeed,andpopulatingitwithsomebasicmetadata:
useZend\Feed\Writer\Feed;
$feed=newFeed();
//Titleofthefeed
$feed->setTitle('TutorialFeed');
//Linktothefeed'starget,usuallyahomepage:
$feed->setLink('https://example.com/');
//Linktothefeeditself,andthefeedtype:
$feed->setFeedLink('https://example.com/feed.xml','rss');
//Feeddescription;onlyrequiredforRSS:
$feed->setDescription('Thisisatutorialfeedforexample.com');
Acouplethingstonote:First,youneedtoknowwhattypeoffeedyou'recreatingupfront,asitwillaffectwhatpropertiesmustbeset,aswellaswhichareactuallyavailable.Ipersonallyliketogeneratefeedsofbothtypes,soI'lldotheabovewithinamethodcallthatacceptsthefeedtypeasanargument,andthenputssomedeclarationswithinconditionalsbasedonthattype.
Second,you'llneedtoknowthefully-qualifiedURIstothefeedtargetandthefeeditself.Thesewillgenerallybesomethingyougenerate;mostroutinglibrarieswillhavethesecapabilities,andyou'llgeneratethesewithinyourapplication,insteadofhard-codingthemasIhavedonehere.
AddingitemsNowthatwehaveourfeed,we'llloopoverourdatasetandadditems.Itemsgenerallyhave:
atitlealinktotheitemanauthorthedateswhenitwasmodified,andlastupdatedcontent
Puttingittogether:
CreateRSSandAtomFeeds
67
$latest=newDateTime('@0');
foreach($dataas$datum){
//Createanemptyentry:
$entry=$feed->createEntry();
//Settheentrytitle:
$entry->setTitle($datum->getTitle());
//Setthelinktotheentry:
$entry->setLink(sprintf('%s%s.html',$baseUri,$datum->getId()));
//Addanauthor,ifyoucan.Eachauthorentryshouldbean
//arraycontainingminimallya"name"key,andzeroormoreof
//thekeys"email"or"uri".
$entry->addAuthor($datum->getAuthor());
//Setthedatecreated:
$entry->setDateCreated(newDateTime($datum->getDateCreated()));
//Andthedatelastupdated:
$modified=newDateTime($datum->getDateModified());
$entry->setDateModified($modified);
//Andfinally,somecontent:
$entry->setContent($datum->getContent());
//Addthenewentrytothefeed:
$feed->addEntry($entry);
//Andmemoizethedatemodified,ifit'smorerecent:
$latest=$modified>$latest?$modified:$latest;
}
Therearequiteafewotherpropertiesyoucanset,andsomeofthesewillvarybasedoncustomextensionsyoumightregisterwiththefeed;theabovearethetypicalitemsyou'llincludeinafeedentry,however.
Whatisthatbitabout$latest,though?
Feedsneedtohaveatimestampindicatingwhentheyweremostrecentlymodified.
Why?Becausefeedsareintendedtobereadbymachinesandaggregators,andneedtoknowwhennewcontentisavailable.
Youcouldsetthedateofmodificationtowhateverthecurrenttimestampisattimeofexecution,butit'sbettertohaveitinsyncwiththemostrecententryinthefeeditself.Assuch,theabovecodecreatesatimestampsettotimestamp0,andchecksforamodifieddatethatisneweroneachiteration.
CreateRSSandAtomFeeds
68
Oncewehavethatinplace,wecanaddthemodifieddatetothefeeditself:
$feed->setDateModified($latest);
RenderingthefeedRenderingthefeedinvolvesexportingit,whichrequiresknowingthefeedtype;thisisnecessarysothatthecorrectXMLmarkupisgenerated.
So,let'screateanRSSfeed:
$rss=$feed->export('rss');
Ifwewanted,andwehavethecorrectpropertiespresent,wecanalsorenderAtom:
$atom=$feed->export('atom');
Nowwhat?
Ioftenpre-generatefeedsandcachethemtothefilesystem.Inthatcase,afile_put_contents()call,usingthegeneratedfeedasthestringcontents,isallthat'sneeded.
Ifyou'reservingthefeedbackoverHTTP,youwillwanttosendbackthecorrectHTTPContent-Typewhenyoudo.Additionally,it'sgoodtosendbackaLast-Modifiedheaderwiththesamedateasthefeed'sownlastmodifieddate,and/oranETagwithahashofthefeed;theseallowclientsperformingHEADrequeststodeterminewhetherornottheyneedtoretrievethefullcontent,oriftheyalreadyhavethelatest.
IfyouareusingPSR-7middleware,theseprocessesmightlooklikethis:
CreateRSSandAtomFeeds
69
useZend\Diactoros\Response\TextResponse;
$commonHeaders=[
'Last-Modified'=>$feed->getDateModified()->format('c'),
'ETag'=>hash('sha256',$feed)
];
//ForanRSSfeed:
returnnewTextResponse($rss,200,array_merge(
$commonHeaders,
['Content-Type'=>'application/rss+xml']
));
//ForanAtomfeed:
returnnewTextResponse($atom,200,array_merge(
$commonHeaders,
['Content-Type'=>'application/atom+xml']
));
Summingupzend-feed'sgenerationcapabilitiesareincrediblyflexible,whilemakingthegeneraluse-casestraight-forward.Wehavecreatedfeedsforblogposts,releases,tweets,andcommentingsystemsusingthecomponent;itdoesexactlywhatitadvertises.
Visitthezend-feeddocumentation formoreinformation.
Footnotes
.https://docs.zendframework.com/zend-feed/↩
1
1
CreateRSSandAtomFeeds
70
Managepermissionswithzend-permissions-rbacbyMatthewWeierO'Phinney
InthearticleManagepermissionswithzend-permissions-acl,wecoverusageofAccessControlLists(ACL)formanaginguserpermissions.Inthisarticle,we'llcoveranotheroptionprovidedbyZendFramework,zend-permissions-rbac ,ourlightweightrole-basedaccesscontrol(RBAC)implementation.
Installingzend-permissions-rbacJustasyouwouldanyofourcomponents,installzend-permissions-rbacviaComposer:
$composerrequirezendframework/zend-permissions-rbac
ThecomponenthasnorequirementsatthistimeotherthanaPHPversionofatleast5.5.
VocabularyInRBACsystems,wehavethreeprimaryitemstotrack:
TheRBACsystemcomposeszeroormoreroles.Aroleisgrantedzeroormorepermissions.Weassertwhetherornotaroleisgrantedagivenpermission.
zend-permissions-rbacsupportsroleinheritance,evenallowingaroletoinheritpermissionsfrommultipleotherroles.Thisallowsyoutocreatesomefairlycomplexandfine-grainedpermissionsschemes!
BasicsAsabasicexample,we'llcreateanRBACforacontent-basedwebsite.Let'sstartwitha"guest"role,thatonlyallows"read"permissions.
1
Managepermissionswithzend-permissions-rbac
71
useZend\Permissions\Rbac\Rbac;
useZend\Permissions\Rbac\Role;
//Createsomeroles
$guest=newRole('guest');
$guest->addPermission('read');
$rbac=newRbac();
$rbac->addRole($guest);
Wecanthenassertifagivenroleisgrantedspecificpermissions:
$rbac->isGranted('guest','read');//true
$rbac->isGranted('guest','write');//false
Unknownroles
Onethingtonote:iftheroleusedwithisGranted()doesnotexist,thismethodraisesanexception,specificallyaZend\Permissions\Rbac\Exception\InvalidArgumentException,indicatingtherolecouldnotbefound.
Inmanysituations,thismaynotbewhatyouwant;youmaywanttohandlenon-existentrolesgracefully.Youcoulddothisinthreeways.
First,youcantesttoseeiftheroleexistsbeforeyoucheckthepermissions,usinghasRole():
if(!$rbac->hasRole($foo)){
//failed,duetomissingrole
}
if(!$rbac->isGranted($foo,$permission)){
//failed,duetomissingpermissions
}
Second,youcanwraptheisGranted()callinatry/catchblock:
Managepermissionswithzend-permissions-rbac
72
try{
if(!$rbac->isGranted($foo,$permission)){
//failed,duetomissingpermissions
}
}catch(RbacInvalidArgumentException$e){
if(!strstr($e->getMessage(),'couldbefound')){
//failed,duetomissingrole
}
//someothererroroccured
throw$e;
}
Personally,Idon'tliketouseexceptionsforapplicationflow;thatsaid,inmostcases,youwillbeworkingwitharoleinstancethatyou'vejustaddedtotheRBAC.
Third,zend-permissions-rbachasabuilt-inmechanismforthis:
$rbac->setCreateMissingRoles(true);
Aftercallingthismethod,anyisGranted()callsyoumakewithunknownroleidentifierswillsimplyreturnabooleanfalse.
Thismethodalsoensuresyoudonotencountererrorswhencreatingroleinheritancechainsandaddrolesout-of-order(e.g.,addingchildrenwhichhavenotyetbeencreatedtoaroleyouaredefining).
Assuch,pleaseassumethatallfurtherexampleshavecalledthismethodifcreationoftheRBACinstanceisnotdemonstrated.
RoleinheritanceLet'ssaywewanttobuildonthepreviousexample,andcreatean"editor"rolethatalsoincorporatesthepermissionsofthe"guest"role,andaddsa"write"permission.
Youmightbeinclinedtothinkofthe"editor"asinheritingfromthe"guest"role—inotherwords,thatitisadescendentorchildofit.However,inRBAC,inheritanceworksintheoppositedirection:aparentinheritsallpermissionsofitschildren.Assuch,we'llcreatetheroleasfollows:
Managepermissionswithzend-permissions-rbac
73
$editor=newRole('editor');
$editor->addChild($guest);
$editor->addPermission('write');
$rbac->addRole($editor);
$rbac->isGranted('editor','write');//true
$rbac->isGranted('editor','read');//true
$rbac->isGranted('guest','write');//false
Anotherrolemightbea"reviewer"whocan"moderate"content:
$reviewer=newRole('reviewer');
$reviewer->addChild($guest);
$reviewer->addPermission('moderate');
$rbac->addRole($reviewer);
$rbac->isGranted('reviewer','moderate');//true
$rbac->isGranted('reviewer','write');//false;editoronly!
$rbac->isGranted('reviewer','read');//true
$rbac->isGranted('guest','moderate');//false
Let'screateanother,an"admin"whocandoalloftheabove,butalsohaspermissionsfor"settings":
$admin=newRole('admin');
$admin->addChild($editor);
$admin->addChild($reviewer);
$admin->addPermission('settings');
$rbac->addRole($admin);
$rbac->isGranted('admin','settings');//true
$rbac->isGranted('admin','write');//true
$rbac->isGranted('admin','moderate');//true
$rbac->isGranted('admin','read');//true
$rbac->isGranted('editor','settings');//false
$rbac->isGranted('reviewer','settings');//false
$rbac->isGranted('guest','write');//false
Asyoucansee,permissionslookupsarerecursiveandcollective;theRBACexaminesallchildrenandeachoftheirdescendantsasfardownasitneedstodetermineifagivenpermissionisgranted!
CreatingyourRBAC
Managepermissionswithzend-permissions-rbac
74
WhenshouldyoucreateyourRBAC,exactly?Andshoulditcontainallrolesandpermissions?
Inmostcases,youwillbevalidatingasingleuser'spermissions.What'sinterestingaboutzend-permissions-rbacisthatifyouknowthatuser'srole,thepermissionstheyhavebeenassigned,andanychildroles(andtheirpermissions)towhichtherolebelongs,youhaveeverythingyouneed.Thismeansthatyoucandomostlookupson-the-fly.
Assuch,youwilltypicallydothefollowing:
Createafinitesetofwell-knownrolesandtheirpermissionsasaglobalRBAC.Addroles(andoptionallypermissions)forthecurrentuser.ValidatethecurrentuseragainsttheRBAC.
Asanexample,let'ssayIhaveauserMariowhohastherole"editor",andalsoaddsthepermission"update".IfourRBACisalreadypopulatedpertheaboveexamples,Imightdothefollowing:
$mario=newRole('mario');
$mario->addChild($editor);
$mario->addPermission('update');
$rbac->addRole($mario);
$rbac->isGranted($mario,'settings');//false;adminonly!
$rbac->isGranted($mario,'update');//true;marioonly!
$rbac->isGranted('editor','update');//false;marioonly!
$rbac->isGranted($mario,'write');//true;alleditors
$rbac->isGranted($mario,'read');//true;allguests
AssigningrolestousersWhenyouhavesomesortofauthenticationsysteminplace,itwillreturnsomesortofidentityoruserinstancegenerally.YouwillthenneedtomapthistoRBACroles.Buthow?
Hopefully,youcanstoreroleinformationwhereveryoupersistyouruserinformation.Sincerolesareessentiallystoredinternallyasstringsbyzend-permissions-rbac,thismeansthatyoucanstoretheuserroleasadiscretedatumwithyouruseridentity.
Onceyouhave,youhaveafewoptions:
Usetheroledirectlyfromyouridentitywhencheckingpermissions:e.g.,$rbac->isGranted($identity->getRole(),'write')
CreateaZend\Permissions\Rbac\Roleinstance(orotherconcreteclass)withtherolefetchedfromtheidentity,andusethatforpermissionschecks:$rbac->isGranted(new
Managepermissionswithzend-permissions-rbac
75
Role($identity->getRole()),'write')
UpdateyouridentityinstancetoimplementZend\Permissions\Rbac\RoleInterface,andpassitdirectlytopermissionschecks:$rbac->isGranted($identity,'write')
Thislatterapproachprovidesanicesolution,asitthenalsoallowsyoutostorespecificpermissionsand/orchildrolesaspartoftheuserdata.
TheRoleInterfacelookslikethefollowing:
Managepermissionswithzend-permissions-rbac
76
namespaceZend\Permissions\Rbac;
useRecursiveIterator;
interfaceRoleInterfaceextendsRecursiveIterator
{
/**
*Getthenameoftherole.
*
*@returnstring
*/
publicfunctiongetName();
/**
*Addpermissiontotherole.
*
*@param$name
*@returnRoleInterface
*/
publicfunctionaddPermission($name);
/**
*Checksifapermissionexistsforthisroleoranychildroles.
*
*@paramstring$name
*@returnbool
*/
publicfunctionhasPermission($name);
/**
*Addachild.
*
*@paramRoleInterface|string$child
*@returnRole
*/
publicfunctionaddChild($child);
/**
*@paramRoleInterface$parent
*@returnRoleInterface
*/
publicfunctionsetParent($parent);
/**
*@returnnull|RoleInterface
*/
publicfunctiongetParent();
}
Managepermissionswithzend-permissions-rbac
77
TheZend\Permissions\Rbac\AbstractRolecontainsbasicimplementationsofmostmethodsoftheinterface,includinglogicforqueryingchildpermissions,sowesuggestinheritingfromthatifyoucan.
Asanexample,youcouldstorethepermissionsasacomma-separatedstringandtheparentroleasastringinternallywhencreatingyouridentityinstance:
useZend\Permissions\Rbac\AbstractRole;
useZend\Permissions\Rbac\RoleInterface;
useZend\Permissions\Rbac\Role;
classIdentityextendsAbstractRole
{
/**
*@paramstring$username
*@paramstring$role
*@paramarray$permissions
*@paramarray$childRoles
*/
publicfunction__construct(
string$username,
array$permissions=[],
array$childRoles=[]
){
//$nameisdefinedinAbstractRole
$this->name=$username;
foreach($this->permissionsas$permission){
$this->addPermission($permission);
}
$childRoles=array_merge(['guest'],$childRoles);
foreach($this->childRolesas$childRole){
$this->addChild($childRole);
}
}
}
Assumingyourauthenticationsystemusesadatabasetable,andalookupreturnsanarray-likerowwiththeuserinformationonasuccessfullookup,youmightthenseedyouridentityinstanceasfollows:
$identity=newIdentity(
$row['username'],
explode(',',$row['permissions']),
explode(',',$row['roles'])
);
Managepermissionswithzend-permissions-rbac
78
Thisapproachallowsyoutoassignpre-determinedrolestoindividualusers,whilealsoallowingyoutoaddfine-grained,individualpermissions!
CustomassertionsSometimesastaticassertionisnotenough.
Asanexample,wemaywanttoimplementarulethatthecreatorofacontentiteminourwebsitealwayshasrightstoedittheitem.Howwouldweimplementthatwiththeabovesystem?
zend-permissions-rbacallowsyoutodosoviadynamicassertions.SuchassertionsareclassesthatimplementZend\Permissions\Rbac\AssertionInterface,whichdefinesthesinglemethodpublicfunctionassert(Rbac$rbac).
Forthesakeofthisexample,let'sassume:
Thecontentitemisrepresentedasanobject.TheobjecthasamethodgetCreatorUsername()thatwillreturnthesameusernameaswemighthaveinourcustomidentityfromthepreviousexample.
BecausewehavePHP7atourdisposal,we'llcreatetheassertionasananonymousclass:
useZend\Permissions\Rbac\AssertionInterface;
useZend\Permissions\Rbac\Rbac;
useZend\Permissions\Rbac\RoleInterface;
$assertion=newclass($identity,$content)implementsAssertionInterface{
private$content;
private$identity;
publicfunction__construct(RoleInterface$identity,$content)
{
$this->identity=$identity;
$this->content=$content;
}
publicfunctionassert(Rbac$rbac)
{
return$this->identity->getName()===$this->content->getCreatorUsername();
}
};
$rbac->isGranted($mario,'edit',$assertion);//returnstrueif$mariocreated$conte
nt
Thisopensevenmorepossibilitiesthaninheritance!
Managepermissionswithzend-permissions-rbac
79
Summaryzend-permissions-rbacisquitesimpletooperate,butthatsimplicityhidesagreatamountofflexibilityandpower;youcancreateincrediblyfine-grainedpermissionsschemesforyourapplicationsusingthiscomponent!
Footnotes
.https://docs.zendframework.com/zend-permissions-rbac/↩1
Managepermissionswithzend-permissions-rbac
80
Managepermissionswithzend-permissions-aclbyMatthewWeierO'Phinney
InthearticleManagepermissionswithzend-permissions-rbac,wecoverusageofRoleBasedAccessControls(RBAC).Inthisarticle,we'llexploreanotheroptionprovidedbyZendFramework,zend-permissions-acl ,whichimplementsAccessControlLists(ACL).
Thispostwillfollowthesamebasicformatastheonecoveringzend-permissions-rbac,usingthesamebasicexamples.
Installingzend-permissions-aclJustasyouwouldanyofourcomponents,installzend-permissions-aclviaComposer:
$composerrequirezendframework/zend-permissions-acl
ThecomponenthasnorequirementsatthistimeotherthanaPHPversionofatleast5.5.
VocabularyInACLsystems,wehavethreeconcepts:
aresourceissomethingtowhichwecontrolaccess.aroleissomethingthatwillrequestaccesstoaresource.Eachresourcehasprivilegesforwhichaccesswillberequestedtospecificroles.
Asanexample,anauthormightrequesttocreate(privilege)ablogpost(resource);later,aneditor(role)mightrequesttoedit(privilege)ablogpost(resource).
ThechiefdifferencetoRBACisthatRBACessentiallycombinestheresourceandprivilegeintoasingleitem.Byseparatingthem,youcancreateasetofdiscretepermissionsforyourentireapplication,andthencreateroleswithmultiple-inheritanceinordertoimplementfine-grainedpermissions.
ACLs
1
Managepermissionswithzend-permissions-acl
81
AnACLiscreatedbyinstantiatingtheAclclass:
useZend\Permissions\Acl\Acl;
$acl=newAcl();
Oncethatinstanceisavailable,wecanstartaddingroles,resources,andprivileges.
Forthisblogpost,ourACLwillbeusedforacontent-basedwebsite.
RolesRolesareaddedviathe$acl->addRole()method.Thismethodtakeseitherastringrolename,oraZend\Permissions\Acl\Role\RoleInterfaceinstance.
Let'sstartwitha"guest"role,thatonlyallows"read"permissions.
useZend\Permissions\Acl\Role\GenericRoleasRole;
//Createsomeroles
$guest=newRole('guest');
$acl->addRole($guest);
//OR
$acl->addRole('guest');
Referencingrolesandresources
Rolesaresimplystrings.Wemodelthemasobjectsinzend-permissions-aclinordertoprovidestrongtyping,buttheonlyrequirementisthattheyreturnastringrolename.Assuch,whencreatingpermissions,youcanuseeitheraroleinstance,ortheequivalentname.
Thesameistrueforresources,whichwecoverinalatersection.
Bydefault,zend-permissions-aclimplementsawhitelistapproach.Awhitelistdeniesaccesstoeverythingunlessitisexplicitlywhitelisted.(Thisisasopposedtoablacklist,whereaccessisallowedtoeverythingunlessitisintheblacklist.)Unlessyoureallyknowwhatyou'redoingwedonotsuggesttogglingthis;whitelistsarewidelyregardedasabestpracticeforsecurity.
Whatthatmeansisthat,outofthegate,whilewecandosomeprivilegeassertions:
Managepermissionswithzend-permissions-acl
82
$acl->isAllowed('guest','blog','read');
$acl->isAllowed('guest','blog','write');
thesewillalwaysreturnfalse,denyingaccess.So,weneedtostartaddingprivileges.
PrivilegesPrivilegesareassignedusing$acl->allow().
Fortheguestrole,we'llallowthereadprivilegeonanyresource:
$acl->allow('guest',null,'read');
Thesecondargumenttoallow()istheresource(orresources);specifyingnullindicatestheprivilegeappliestoallresources.Ifwere-runtheaboveassertions,wegetthefollowing:
$acl->isAllowed('guest','blog','read');//true
$acl->isAllowed('guest','blog','write');//false
Managepermissionswithzend-permissions-acl
83
Unknownrolesorresources
Onethingtonote:ifeithertheroleorresourceusedwithisAllowed()doesnotexist,thismethodraisesanexception,specificallyaZend\Permissions\Acl\Exception\InvalidArgumentException,indicatingtheroleorresourcecouldnotbefound.
Inmanysituations,thismaynotbewhatyouwant;youmaywanttohandlenon-existentrolesand/orresourcesgracefully.Youcoulddothisinacoupleways.First,youcantesttoseeiftheroleorresourceexistsbeforeyoucheckthepermissions,usinghasRole()and/orhasResource():
if(!$acl->hasRole($foo)){
//failed,duetomissingrole
}
if(!$acl->hasResource($bar)){
//failed,duetomissingresource
}
if(!$acl->isAllowed($foo,$bar,$privilege)){
//failed,duetoinvalidprivilege
}
Alternately,wraptheisAllowed()callinatry/catchblock:
try{
if(!$acl->isAllowed($foo,$bar,$privilege)){
//failed,duetomissingprivileges
}
}catch(AclInvalidArgumentException$e){
//failed,duetomissingroleorresource
}
Personally,Idon'tliketouseexceptionsforapplicationflow,soIrecommendthefirstsolution.Thatsaid,inmostcases,youwillbeworkingwitharoleinstancethatyou'vejustaddedtotheACL,andshouldonlyperformassertionsagainstknownresources.
ResourcesNowlet'saddsomeactualresources.Thesearealmostexactlylikerolesintermsofusage:youcreateaResourceInterfaceinstancetopasstotheACL,or,moresimply,astring;resourcesareaddedviathe$acl->addResource()method.
Managepermissionswithzend-permissions-acl
84
useZend\Permissions\Acl\Resource\GenericResourceasResource;
$resource=newResource('blog');
$acl->addResource($resource);
//OR:
$acl->addResource('blog');
Aresourceisanythingtowhichyouwanttoapplypermissions.Intheremainingexamplesofthispost,we'llusea"blog"astheresource,andprovideavarietyofpermissionsrelatedtoit.
InheritanceLet'ssaywewanttobuildonourpreviousexamples,andcreatean"editor"rolethatalsoincorporatesthepermissionsofthe"guest"role,andaddsa"write"permissiontothe"blog"resource.
UnlikeRBAC,rolesthemselvescontainnoinformationaboutinheritance;instead,theACLtakescareofthatwhenyouaddtheroletotheACL:
$editor=newRole('editor');
$acl->addRole($editor,$guest);//OR:
$acl->addRole($editor,'guest');
Theabovecreatesanewrole,editor,whichinheritsthepermissionsofourguestrole.Now,let'saddaprivilegeallowingeditorstowritetoourblog:
$acl->allow('editor','blog','write');
Withthisinplace,let'sdosomeassertions:
$acl->isAllowed('editor','blog','write');//true
$acl->isAllowed('editor','blog','read');//true
$acl->isAllowed('guest','blog','write');//false
Anotherrolemightbea"reviewer"whocan"moderate"content:
Managepermissionswithzend-permissions-acl
85
$acl->addRole('reviewer','guest');
$acl->allow('reviewer','blog','moderate');
$acl->isAllowed('reviewer','blog','moderate');//true
$acl->isAllowed('reviewer','blog','write');//false;editoronly!
$acl->isAllowed('reviewer','blog','read');//true
$acl->isAllowed('guest','blog','moderate');//false
Let'screateanother,an"admin"whocandoalloftheabove,butalsohaspermissionsfor"settings":
$acl->addRole('admin',['guest','editor','reviewer']);
$acl->allow('admin','blog','settings');
$acl->isAllowed('admin','blog','settings');//true
$acl->isAllowed('admin','blog','write');//true
$acl->isAllowed('admin','blog','moderate');//true
$acl->isAllowed('admin','blog','read');//true
$acl->isAllowed('editor','blog','settings');//false
$acl->isAllowed('reviewer','blog','settings');//false
$acl->isAllowed('guest','blog','write');//false
NotethattheaddRole()callhereprovidesanarrayofrolesasthesecondvaluethistime;whencalledthisway,thenewrolewillinherittheprivilegesofeveryrolelisted;thisallowsformultiple-inheritanceattherolelevel.
Resourceinheritance
ResourceinheritanceworksexactlythesameasRoleinheritance!AddoneormoreparentresourceswhencallingaddResource()ontheACL,andanyprivilegesassignedtothatparentresourcewillalsoapplytothenewresource.
Asanexample,Icouldhavea"news"sectioninmywebsitethathasthesameprivilegeandroleschemaasmyblog:
$acl->addResource('news','blog');
Funwithprivileges!Privilegesareassignedusingallow().Interestingly,likeaddRole()andaddResource(),theroleandresourceargumentspresentedmaybearraysofeach;infact,socantheprivilegesthemselves!
Managepermissionswithzend-permissions-acl
86
Asanexample,wecoulddothefollowing:
$acl->allow(
['reviewer','editor'],
['blog','homepage'],
['write','maintenance']
);
Thiswouldassignthe"write"and"maintenance"privilegesoneachofthe"blog"and"homepage"resourcestothe"reviewer"and"editor"roles!Duetoinheritance,the"admin"rolewouldalsogaintheseprivileges.
CreatingyourACLWhenshouldyoucreateyourACL,exactly?Andshoulditcontainallrolesandpermissions?
Typically,youwillcreateafinitenumberofapplicationordomainpermissions.Inouraboveexamples,wecouldomittheblogresourceandapplytheACLonlywithintheblogdomain(forexample,onlywithinamoduleofazend-mvcorExpressiveapplication);alternately,itcouldbeanapplication-wideACL,withresourcessegregatedbyspecificdomainwithintheapplication.
Ineithercase,youwillgenerally:
Createafinitesetofwell-knownroles,resources,andprivilegesasaglobalorper-domainACL.Createacustomroleforthecurrentuser,typicallyinheritingfromthesetofwell-knownroles.ValidatethecurrentuseragainsttheACL.
UnlikeRBAC,youtypicallywillnotaddcustompermissionsforauser.Thereasonforthisisduetothecomplexityofstoringthecombinationofroles,resources,andprivilegesinadatabase.Storingrolesistrivial:
user_id fullname roles
mario Mario editor,reviewer
Youcouldthencreatetherolebysplittingtherolesfieldandassigningeachasparents:
$acl->addRole($user->getId(),explode(',',$user->getRoles());
Managepermissionswithzend-permissions-acl
87
However,forfine-grainedpermissions,youwouldessentiallyneedanadditionallookuptablemappingtheusertoaresourceandlistofprivileges:
user_id resource privileges
mario blog update,delete
mario news update
Whileitcanbedone,itisresourceandcodeintensive.
Puttingitalltogether,let'ssaytheuser"mario"hasloggedin,withtherole"editor";further,let'sassumethattheidentityinstanceforouruserimplementsRoleInterface.IfourACLisalreadypopulatedpertheaboveexamples,Imightdothefollowing:
$acl->addRole($mario,$mario->getRoles());
$acl->isAllowed($mario,'blog','settings');//false;adminonly!
$acl->isAllowed($mario,'blog','write');//true;alleditors
$acl->isAllowed($mario,'blog','read');//true;allguests
Now,let'ssaywe'vegonetotheworkofcreatingthejointablenecessaryforstoringuserACLinformation;wemighthavesomethinglikethefollowingtofurtherpopulatetheACL:
foreach($mario->getPrivileges()as$resource=>$privileges){
$acl->allow($mario,$resource,explode(',',$privileges));
}
Wecouldthendothefollowingassertions:
$acl->isAllowed($mario,'blog','update');//true
$acl->isAllowed('editor','blog','update');//false;marioonly!
$acl->isAllowed($mario,'blog','delete');//true
$acl->isAllowed('editor','blog','delete');//false;marioonly!
CustomassertionsFine-grainedastheprivilegesystemcanbe,sometimesit'snotenough.
Asanexample,wemaywanttoimplementarulethatthecreatorofacontentiteminourwebsitealwayshasrightstoedittheitem.Howwouldweimplementthatwiththeabovesystem?
Managepermissionswithzend-permissions-acl
88
zend-permissions-aclallowsyoutodosoviadynamicassertions.SuchassertionsareclassesthatimplementZend\Permissions\Acl\Assertion\AssertionInterface,whichdefinesasinglemethod:
namespaceZend\Permissions\Assertion;
useZend\Permissions\Acl\Acl;
useZend\Permissions\Acl\Resource\ResourceInterface;
useZend\Permissions\Acl\Role\RoleInterface;
interfaceAssertionInterface
{
/**
*@returnbool
*/
publicfunctionassert(
Acl$acl,
RoleInterface$role=null,
ResourceInterface$resource=null,
$privilege=null
);
}
Forthesakeofthisexample,let'sassume:
WecastouridentitytoaRoleInterfaceinstanceafterretrieval.Thecontentitemisrepresentedasanobject.TheobjecthasamethodgetCreatorUsername()thatwillreturnthesameusernameaswemighthaveinourcustomidentityfromthepreviousexample.Iftheusernameisthesameasthecustomidentity,allowanyprivileges.
BecausewehavePHP7atourdisposal,we'llcreatetheassertionasananonymousclass:
Managepermissionswithzend-permissions-acl
89
useZend\Permissions\Acl\Acl;
useZend\Permissions\Acl\Assertion\AssertionInterface;
useZend\Permissions\Acl\Resource\ResourceInterface;
useZend\Permissions\Acl\Role\RoleInterface;
$assertion=newclass($identity,$content)implementsAssertionInterface{
private$content;
private$identity;
publicfunction__construct(RoleInterface$identity,$content)
{
$this->identity=$identity;
$this->content=$content;
}
/**
*@returnbool
*/
publicfunctionassert(
Acl$acl,
RoleInterface$role=null,
ResourceInterface$resource=null,
$privilege=null
){
if(null===$role||$role->getRoleId()!==$this->identity->getRoleId()){
returnfalse;
}
if(null===$resource||'blog'!==$resource->getResourceId()){
returnfalse;
}
return$this->identity->getRoleId()===$this->content->getCreatorUsername();
}
};
//Attachtheassertiontoallrolesontheblogresource;
//customassertionsareprovidedasafourthargumenttoallow().
$acl->allow(null,'blog',null,$assertion);
$acl->isAllowed('mario','blog','edit');//returnstrueif$mariocreated$content
Theabovecreatesanewassertionthatwilltriggerforthe"blog"resourcewhenaprivilegewedonotalreadyknowaboutisqueried.Inthatparticularcase,ifthecreatorofourcontentisthesameasthecurrentuser,itwillreturntrue,allowingaccess!
Bycreatingsuchassertionsin-placewithdataretrievedatruntime,youcanachieveanincredibleamountofflexibilityforyourACLs.
Managepermissionswithzend-permissions-acl
90
Wrappingupzend-permissions-aclprovidesahugeamountofpower,andtheabilitytoprovidebothroleandresourceinheritancecanvastlysimplifysetupofcomplexACLs.Additionally,theprivilegesystemprovidesmuch-neededgranularity.
IfyouwantedtouseACLsinmiddleware,theusageisquitesimilartozend-permissions-rbac:injectyourACLinstanceinyourmiddleware,retrieveyouruseridentity(andthusrole)fromtherequest,andperformqueriesagainsttheACLusingthecurrentmiddlewareorrouteasaresource,andeithertheHTTPmethodorthedomainactionyouwillperformastheprivilege.
Themaindifficultywithzend-permissions-aclisthatthereisno1:1relationshipbetweenaroleandaprivilege,whichmakesstoringACLinformationinadatabasemorecomplex.Ifyoufindyourselfstrugglingwiththatfact,youmaywanttouseRBACinstead.
Footnotes
.https://docs.zendframework.com/zend-permissions-acl/↩1
Managepermissionswithzend-permissions-acl
91
ImplementJSON-RPCwithzend-json-serverbyMatthewWeierO'Phinney
zend-json-server providesaJSON-RPC implementation.JSON-RPCissimilartoXML-RPCorSOAPinthatitimplementsaRemoteProcedureCallserveratasingleURIusingapredictablecallingsemantic.Likeeachoftheseotherprotocols,itprovidestheabilitytointrospecttheserverinordertodeterminewhatcallsareavailable,whatargumentseachcallexpects,andtheexpectedreturnvalue(s);JSON-RPCimplementsthisviaaServiceMappingDescription(SMD) ,whichisusuallyavailableviaanHTTPGETrequesttotheserver.
zend-json-serverwasdesignedtoworkstandalone,allowingyoutomapaURLtoaspecificscriptthatthenhandlestherequest:
$server=newZend\Json\Server\Server();
$server->setClass('Calculator');
//SMDrequest
if('GET'===$_SERVER['REQUEST_METHOD']){
//IndicatetheURLendpoint,andtheJSON-RPCversionused:
$server->setTarget('/json-rpc')
->setEnvelope(Zend\Json\Server\Smd::ENV_JSONRPC_2);
//GrabtheSMD
$smd=$server->getServiceMap();
//ReturntheSMDtotheclient
header('Content-Type:application/json');
echo$smd;
return;
}
//Normalrequest
$server->handle();
Whattheaboveexampledoesis:
Createaserver.Attachaclassorobjecttotheserver.Theserverintrospectsthatclassinordertoexposeanypublicmethodsonitascallsontheserveritself.IfanHTTPGETrequestoccurs,wepresenttheservicemappingdescription.
1 2
3
ImplementJSON-RPCwithzend-json-server
92
Otherwise,weattempttohandletherequest.
AllservercomponentsinZendFrameworkworksimilartotheabove.Introspectionviafunctionorclassreflectionallowsquicklycreatingandexposingservicesviatheseservers,aswellasenablestheserverstoprovideSMD,WSDL,orXML-RPCsysteminformation.
However,thisapproachcanleadtodifficulties:
WhatifIneedaccesstootherapplicationservices?orwanttousethefully-configuredapplicationdependencyinjectioncontainer?WhatifIwanttobeabletocontroltheURIviaarouter?WhatifIwanttobeabletoaddauthenticationorauthorizationinfrontoftheserver?
Inotherwords,howdoIusetheJSON-RPCserveraspartofalargerapplication?
Below,I'lloutlineusingzend-json-serverinbothaZendFrameworkMVCapplication,aswellasviaPSR-7middleware.Inbothcases,youmayassumethatAcme\ServiceModelisaclassexposingpublicmethodswewishtoexposeviatheserver.
Usingzend-json-serverwithinzend-mvcTousezend-json-serverwithinazend-mvcapplication,youwillneedto:
ProvideaZend\Json\Server\ResponseinstancetotheServerinstance.TelltheServerinstancetoreturntheresponse.PopulatetheMVC'sresponsefromtheServer'sresponse.ReturntheMVCresponse(whichwillshort-circuittheviewlayer).
Thisthirdsteprequiresabitoflogic,asthedefaultresponsetype,Zend\Json\Server\Response\Http,doessomelogicaroundsettingheadersthatyou'llneedtoduplicate.
Afullexamplewilllooklikethefollowing:
ImplementJSON-RPCwithzend-json-server
93
namespaceAcme\Controller;
useAcme\ServiceModel;
useZend\Json\Server\ResponseasJsonResponse;
useZend\Json\Server\ServerasJsonServer;
useZend\Mvc\Controller\AbstractActionController;
classJsonRpcControllerextendsAbstractActionController
{
private$model;
publicfunction__construct(ServiceModel$model)
{
$this->model=$model;
}
publicfunctionendpointAction()
{
$server=newJsonServer();
$server
->setClass($this->model)
->setResponse(newJsonResponse())
->setReturnResponse();
/**@varJsonResponse$jsonRpcResponse*/
$jsonRpcResponse=$server->handle();
/**@var\Zend\Http\Response$response*/
$response=$this->getResponse();
//Dowehaveanemptyresponse?
if(!$jsonRpcResponse->isError()
&&null===$jsonRpcResponse->getId()
){
$response->setStatusCode(204);
return$response;
}
//Setthecontent-type
$contentType='application/json-rpc';
if(null!==($smd=$jsonRpcResponse->getServiceMap())){
//SMDisbeingreturned;usealternatecontenttype,ifpresent
$contentType=$smd->getContentType()?:$contentType;
}
//Settheheadersandcontent
$response->getHeaders()->addHeaderLine('Content-Type',$contentType);
$response->setContent($jsonRpcResponse->toJson());
return$response;
}
}
ImplementJSON-RPCwithzend-json-server
94
Injectyourdependencies!
You'llnotethattheaboveexampleacceptstheAcme\ServiceModelinstanceviaitsconstructor.Thismeansthatyouwillneedtoprovideafactoryforyourcontroller,toensurethatitisinjectedwithafullyconfiguredinstance—andthatlikelyalsomeansafactoryforthemodel,too.
Tosimplifythis,youmaywanttocheckouttheConfigAbstractFactory orReflectionBasedAbstractFactory ,bothofwhichwereintroducedinversion3.2.0ofzend-servicemanager.
Usingzend-json-serverwithinPSR-7middlewareUsingzend-json-serverwithinPSR-7middlewareissimilartozend-mvc:
ProvideaZend\Json\Server\ResponseinstancetotheServerinstance.TelltheServerinstancetoreturntheresponse.CreateandreturnaPSR-7responsebasedontheServer'sresponse.
Thecodeendsuplookinglikethefollowing:
namespaceAcme\Controller;
useAcme\ServiceModel;
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\ServerRequestInterface;
useZend\Diactoros\Response\EmptyResponse;
useZend\Diactoros\Response\TextResponse;
useZend\Json\Server\ResponseasJsonResponse;
useZend\Json\Server\ServerasJsonServer;
classJsonRpcMiddleware
{
private$model;
publicfunction__construct(ServiceModel$model)
{
$this->model=$model;
}
publicfunction__invoke(
ServerRequestInterface$request,
ResponseInterface$response,
callable$next
){
$server=newJsonServer();
45
ImplementJSON-RPCwithzend-json-server
95
$server
->setClass($this->model)
->setResponse(newJsonResponse())
->setReturnResponse();
/**@varJsonResponse$jsonRpcResponse*/
$jsonRpcResponse=$server->handle();
//Dowehaveanemptyresponse?
if(!$jsonRpcResponse->isError()
&&null===$jsonRpcResponse->getId()
){
returnnewEmptyResponse();
}
//Getthecontent-type
$contentType='application/json-rpc';
if(null!==($smd=$jsonRpcResponse->getServiceMap())){
//SMDisbeingreturned;usealternatecontenttype,ifpresent
$contentType=$smd->getContentType()?:$contentType;
}
returnnewTextResponse(
$jsonRpcResponse->toJson(),
200,
['Content-Type'=>$contentType]
);
}
}
Intheaboveexample,Iuseacoupleofzend-diactoros -specificresponsetypestoensurethatwehavenoextraneousinformationinthereturnedresponses.IuseTextResponsespecifically,asthetoJson()methodonthezend-json-serverresponsereturnstheactualJSONstring,versusadatastructurethatcanbecasttoJSON.
Perthenoteabove,youwillneedtoconfigureyourdependencyinjectioncontainertoinjectthemiddlewareinstancewiththemodel.
Summaryzend-json-serverprovidesaflexible,robust,andsimplewaytocreateJSON-RPCservices.Thedesignofthecomponentmakesitpossibletouseitstandalone,orwithinanyapplicationframeworkyoumightbeusing.Hopefullytheexamplesabovewillaidyouinadaptingitforusewithinyourownapplication!
6
7
ImplementJSON-RPCwithzend-json-server
96
Visitthezend-json-serverdocumentation tofindoutwhatelseyoumightbeabletodowiththiscomponent!
Footnotes
.https://docs.zendframework.com/zend-json-server/↩
.http://groups.google.com/group/json-rpc/↩
.http://www.jsonrpc.org/specification↩
.https://docs.zendframework.com/zend-servicemanager/config-abstract-factory/↩
.https://docs.zendframework.com/zend-servicemanager/reflection-abstract-factory/↩
.https://docs.zendframework.com/zend-diactoros↩
.https://docs.zendframework.com/zend-json-server/↩
7
1
2
3
4
5
6
7
ImplementJSON-RPCwithzend-json-server
97
ImplementanXML-RPCserverwithzend-xmlrpcbyMatthewWeierO'Phinney
zend-xmlrpc providesafull-featuredXML-RPC clientandserverimplementation.XML-RPCisaRemoteProcedureCallprotocolusingHTTPasthetransportandXMLforencodingtherequestsandresponses.
EachXML-RPCrequestconsistsofamethodcall,whichnamestheprocedure(methodName)tocall,alongwithitsparameters.Theserverthenreturnsaresponse,thevaluereturnedbytheprocedure.
Asanexampleofarequest:
POST/xml-rpcHTTP/1.1
Host:api.example.com
Content-Type:text/xml
<?xmlversion="1.0"?>
<methodCall>
<methodName>add</methodName>
<params>
<param>
<value><i4>20</i4></value>
</param>
<param>
<value><i4>22</i4></value>
</param>
</params>
</methodCall>
Theaboveisessentiallyrequestingadd(20,22)fromtheserver.
Aresponsemightlooklikethis:
1 2
ImplementanXML-RPCserverwithzend-xmlrpc
98
HTTP/1.1200OK
Connection:close
Content-Type:text/xml
<?xmlversion="1.0"?>
<methodResponse>
<params>
<param>
<value><i4>42</i4></value>
</param>
</params>
</methodResponse>
Inthecaseofanerror,yougetafaultresponse,detailingtheproblem:
HTTP/1.1200OK
Connection:close
Content-Type:text/xml
<?xmlversion="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>4</int></value>
</member>
<member>
<name>faultString</name>
<value><string>Toofewparameters.</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>
Content-Length
ThespecificationindicatesthattheContent-Lengthheadermustbepresentinbothrequestsandresponses,andmustbecorrect.IhaveyettoworkwithanyXML-RPCclientsorserversthatfollowedthisrestriction.
Values
ImplementanXML-RPCserverwithzend-xmlrpc
99
XML-RPCismeanttobeintentionallysimple,andsupportsimpleproceduraloperationswithalimitedsetofallowedvalues.ItpredatesJSON,butsimilarlydefinesarestrictedlistofallowedvaluetypesinordertoallowrepresentingalmostanydatastructure—andnotethatterm,datastructure.Typedobjectswithbehaviorarenevertransferred,onlydata.(ThisishowSOAPdifferentiatesfromXML-RPC.)
KnowingwhatvaluetypesmaybetransmittedoverXML-RPCallowsyoutodeterminewhetherornotit'sagoodfitforyourwebserviceplatform.
Thevaluesallowedinclude:
Integers,viaeither<int>or<i4>tags.(<i4>pointstothefactthatthespecificationrestrictsintegerstofour-bytesignedintegers.)Booleans,via<boolean>;thevaluesareeither0or1.Strings,via<string>.Floatsordoubles,via<double>.Date/Timevalues,inISO-8601format,via<dateTime.iso8601>.Base64-encodedbinaryvalues,via<base64>.
Therearealsotwocompositevaluetypes,<struct>and<array>.A<struct>contains<member>values,whichinturncontaina<name>anda<value>:
<struct>
<member>
<name>minimum</name>
<value><int>0</int></value>
</member>
<member>
<name>maximum</name>
<value><int>100</int></value>
</member>
</struct>
ThesecanbevisualizedasassociativearraysinPHP.
An<array>consistsofa<data>elementcontaininganynumberof<value>items:
<array>
<data>
<value><int>0</int></value>
<value><int>10</int></value>
<value><int>20</int></value>
<value><int>30</int></value>
<value><int>50</int></value>
</data>
</array>
ImplementanXML-RPCserverwithzend-xmlrpc
100
Thevalueswithinanarrayorastructdonotneedtobeofthesametype,whichmakesthemverysuitablefortranslatingtoPHPstructures.
Whilethesevaluesareeasyenoughtocreateandparse,doingsomanuallyleadstoalotofoverhead,particularlyifyouwanttoensurethatyourserverand/orclientisrobust.zend-xmlrpcprovidesallthetoolstoworkwiththis
AutomaticallyservingclassmethodsTosimplifycreatingservers,zend-xmlrpcusesPHP'sReflectionAPI toscanfunctionsandclassmethodsinordertoexposethemasXML-RPCservices.ThisallowsyoutoaddanarbitrarynumberofmethodstoyourXML-RPCserver,whichcanthembehandledviaasingleendpoint.
InvanillaPHP,thisthenlookslike:
$server=newZend\XmlRpc\Server;
$server->setClass('Calculator');
echo$server->handle();
Internally,zend-xmlrpcwilltakecareoftypeconversionsfromtheincomingrequest.Todoso,however,youmayneedtodocumentyourtypesusingslightlydifferentnotationwithinyourdocblocks.Asexamples,thefollowingtypesdonothavedirectanaloguesinPHP:
dateTime.iso8601base64struct
Ifyouwanttoacceptorreturnanyofthesetypes,documentthem:
/**
*@paramdateTime.iso8601$data
*@parambase64$data
*@paramstruct$map
*@returnbase64
*/
functionmethodWithOddParameters($date,$data,array$map)
{
}
3
ImplementanXML-RPCserverwithzend-xmlrpc
101
Structs
zend-xmlrpcdoescontainlogictodetermineifanarrayvalueisanindexedarrayoranassociativearray,andwillgenerallyproperlyconvertthese.However,westillrecommenddocumentingthemorespecifictypesasnotedaboveforpurposesofusingthesystem.methodHelpfunctionality,whichisdetailedbelow.
Youmayalsoaddfunctions:
$server->addFunction('add');
Aservercanacceptmultiplefunctionsandclasses.However,beawarethatwhendoingso,youneedtobecarefulaboutnamingconflicts.Fortunately,zend-xmlrpchaswaystoresolvethose,aswell!
IfyoulookatmanyXML-RPCexamples,theywillusemethodnamessuchascalculator.addortransaction.process.zend-xmlrpc,whenperformingreflection,usesthemethodorfunctionnamebydefault,whichwillbetheportionfollowingthe.inthepreviousexamples.However,youcanalsonamespacethese,usinganadditionalargumenttoeitheraddFunction()orsetClass():
//ExposesCalculatormethodsundercalculator.*:
$server->setClass('Calculator','calculator');
//Exposestransaction.process:
$server->addFunction('process','transaction');
Thiscanbeparticularlyusefulwhenexposingmultipleclassesthatmayexposethesamemethodnames.
ServerintrospectionWhilenotanofficialpartofthestandard,manyserversandclientssupporttheXML-RPCIntrospectionprotocol .Theprotocoldefinesthreemethods:
system.listMethods,whichreturnsastructofmethodssupportedbytheserver.system.methodSignature,whichreturnsastructdetailingtheargumentstotherequestedmethod.system.methodHelp,whichreturnsastringdescriptionoftherequestedmethod.
Theserverimplementationinzend-xmlrpcsupportstheseout-of-the-box,allowingyourclientstogetinformationonexposedservices!
4
ImplementanXML-RPCserverwithzend-xmlrpc
102
zend-xmlrpcclientandintrospection
Theclientexposedwithinzend-xmlrpcwillnativelyusetheintrospectionprotocolinordertoprovideafluent,method-likewayofinvokingXML-RPCmethods:
$client=newZend\XmlRpc\Client('https://xmlrpc.example.com/');
$service=$client->getProxy();//invokesintrospection!
$value=$service->calculator->add(20,22);//invokescalculator.add(20,22)
FaultsandexceptionsBydefault,zend-xmlrpccatchesexceptionsinyourserviceclasses,andraisesfaultresponses.However,thesefaultresponsesomittheexceptiondetailsbydefault,topreventleakingsensitiveinformation.
Youcan,however,whitelistexceptiontypeswiththeserver:
useApp\Exception;
useZend\XmlRpc\Server\Fault;
Fault::attachFaultException(Exception\InvalidArgumentException::class);
Whenyoudoso,theexceptioncodeandmessagewillbeusedtogeneratethefaultresponse.Note:anyexceptioninthatparticularinheritancehierarchywillthenbeexposedaswell!
Integratingwithzend-mvcTheaboveexamplesalldemonstrateusageinstandalonescripts;whatifyouwanttousetheserverinsidezend-mvc?
Todoso,weneedtodotwothingsdifferently:
WeneedtocreateourownZend\XmlRpc\RequestandseeditfromtheMVCrequestcontent.WeneedtocasttheresponsereturnedbyZend\XmlRpc\Server::handle()toanMVCresponse.
ImplementanXML-RPCserverwithzend-xmlrpc
103
namespaceAcme\Controller;
useAcme\Model\Calculator;
useZend\XmlRpc\RequestasXmlRpcRequest;
useZend\XmlRpc\ResponseasXmlRpcResponse;
useZend\XmlRpc\ServerasXmlRpcServer;
useZend\Mvc\Controller\AbstractActionController;
classXmlRpcControllerextendsAbstractActionController
{
private$calculator;
publicfunction__construct(Calculator$calculator)
{
$this->calculator=$calculator;
}
publicfunctionendpointAction()
{
/**@var\Zend\Http\Request$request*/
$request=$this->getRequest();
//SeedtheXML-RPCrequest
$xmlRpcRequest=newXmlRpcRequest();
$xmlRpcRequest->loadXml($request->getContent());
//Createtheserver
$server=newXmlRpcServer();
$server->setClass($this->calculator,'calculator');
/**@varXmlRpcResponse$xmlRpcResponse*/
$xmlRpcResponse=$server->handle($xmlRpcRequest);
/**@var\Zend\Http\Response$response*/
$response=$this->getResponse();
//Settheheadersandcontent
$response->getHeaders()->addHeaderLine('Content-Type','text/xml');
$response->setContent($xmlRpcResponse->saveXml());
return$response;
}
}
ImplementanXML-RPCserverwithzend-xmlrpc
104
Injectyourdependencies!
You'llnotethattheaboveexampleacceptstheAcme\Model\Calculatorinstanceviaitsconstructor.Thismeansthatyouwillneedtoprovideafactoryforyourcontroller,toensurethatitisinjectedwithafullyconfiguredinstance—andthatlikelyalsomeansafactoryforthemodel,too.
Tosimplifythis,youmaywanttocheckouttheConfigAbstractFactory orReflectionBasedAbstractFactory ,bothofwhichwereintroducedinversion3.2.0ofzend-servicemanager.
Usingzend-xmlrpc'sserverwithinPSR-7middlewareUsingthezend-xmlrpcserverwithinPSR-7middlewareissimilartozend-mvc.
56
ImplementanXML-RPCserverwithzend-xmlrpc
105
namespaceAcme\Controller;
useAcme\Model\Calculator;
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\ServerRequestInterface;
useZend\Diactoros\Response\HtmlResponse;
useZend\XmlRpc\RequestasXmlRpcRequest;
useZend\XmlRpc\ResponseasXmlRpcResponse;
useZend\XmlRpc\ServerasXmlRpcServer;
classXmlRpcMiddleware
{
private$calculator;
publicfunction__construct(Calculator$calculator)
{
$this->calculator=$calculator;
}
publicfunction__invoke(
ServerRequestInterface$request,
ResponseInterface$response,
callable$next
){
//SeedtheXML-RPCrequest
$xmlRpcRequest=newXmlRpcRequest();
$xmlRpcRequest->loadXml((string)$request->getBody());
$server=newXmlRpcServer();
$server->setClass($this->calculator,'calculator');
/**@varXmlRpcResponse$xmlRpcResponse*/
$xmlRpcResponse=$server->handle($xmlRpcRequest);
returnnewHtmlResponse(
$xmlRpcResponse->saveXml(),
200,
['Content-Type'=>'text/xml']
);
}
}
Intheaboveexample,Iusethezend-diactoros -specificHtmlResponsetypetogeneratetheresponse;thiscouldbeanyotherresponsetype,aslongastheContent-Typeheaderissetcorrectly,andthestatuscodeissetto200.
Perthenoteabove,youwillneedtoconfigureyourdependencyinjectioncontainertoinjectthemiddlewareinstancewiththemodel.
7
ImplementanXML-RPCserverwithzend-xmlrpc
106
SummaryWhileXML-RPCmaynotbedujour,itisatriedandtruemethodofexposingwebservicesthathaspersistedforclosetotwodecades.zend-xmlrpc'sserverimplementationprovidesaflexible,robust,andsimplewaytocreateXML-RPCservicesaroundtheclassesandfunctionsyoudefineinPHP,makingitpossibletouseitstandalone,orwithinanyapplicationframeworkyoumightbeusing.Hopefullytheexamplesabovewillaidyouinadaptingitforusewithinyourownapplication!
Visitthezend-xmlrpcserverdocumentation tofindoutwhatelseyoumightbeabletodowiththiscomponent.
Footnotes
.https://docs.zendframework.com/zend-xmlrpc/↩
.http://xmlrpc.scripting.com/spec.html↩
.http://php.net/Reflection↩
.http://xmlrpc-c.sourceforge.net/introspection.html↩
.https://docs.zendframework.com/zend-servicemanager/config-abstract-factory/↩
.https://docs.zendframework.com/zend-servicemanager/reflection-abstract-factory/↩
.https://docs.zendframework.com/zend-diactoros↩
.https://docs.zendframework.com/zend-xmlrpc/server/↩
8
1
2
3
4
5
6
7
8
ImplementanXML-RPCserverwithzend-xmlrpc
107
ImplementaSOAPserverwithzend-soapbyMatthewWeierO'Phinney
zend-soap providesafull-featuredSOAP implementation.SOAPisanXML-basedwebprotocoldesignedtoallowdescribingmessages,and,optionally,operationstoperform.It'ssimilartoXML-RPC,butwithafewkeydifferences:
Arbitrarydatastructuresmaybedescribed;youarenotlimitedtothebasicscalar,list,andstructtypesofXML-RPC.Messagesareoftenserializationsofspecificobjecttypesoneitherorboththeclientandserver.TheSOAPmessagemayincludeinformationonitsownstructuretoallowtheserverorclienttodeterminehowtointerpretthemessage.
Multipleoperationsmaybedescribedinamessageaswell,versustheonecall,oneoperationstructureofXML-RPC.
Inotherwords,it'sanextensibleprotocol.Thisprovidesobviousbenefits,butalsoadisadvantage:creatingandparsingSOAPmessagescanquicklybecomequitecomplex!
Toalleviatethatcomplexity,ZendFrameworkprovidesthezend-soapcomponent,whichincludesaserverimplementation.
WhythesearticlesonRPCservices?
WeloveREST;oneofourprojectsisApigility ,whichallowsyoutosimplyandquicklybuildRESTAPIs.However,thereareoccasionswhereRPCmaybeabetterfit:
Ifyourservicesarelessresourceoriented,andmorefunctionoriented(e.g.,providingcalculations).
Ifconsumersofyourservicesmayneedmoreuniformityintheservicearchitectureinordertoensuretheycanquicklyandeasilyconsumetheservices,withoutneedingtocreateuniquetoolingforeachserviceexposed.WhilethegoalofRESTistoofferdiscovery,wheneverypayloadtosendorreceiveisdifferent,thiscanoftenleadtoanexplosionofcodewhenconsumingmanyservices.
Someorganizationsandcompaniesmaystandardizeoncertainwebserviceprotocolsduetoexistingtooling,abilitytotraindevelopers,etc.
WhileRESTmaybethepreferredwaytoarchitectwebservices,theseandotherreasonsoftendictateotherapproaches.Assuch,weprovidetheseRPCalternativesforPHPdevelopers.
1 2
3
ImplementaSOAPserverwithzend-soap
108
WhatbenefitsdoesitofferoverthePHPextension?PHPprovidesSOAPclientandservercapabilitiesalreadyviaitsSOAPextension ;whydoweofferacomponent?
Bydefault,PHP'sSoapServer::handle()will:
GrabthePOSTbody(php://input),unlessanXMLstringispassedtoit.EmittheheadersandSOAPXMLresponsebodytotheoutputbuffer.
ExceptionsorPHPerrorsraisedduringprocessingmayresultinaSOAPfaultresponse,withnodetails,orcanresultininvalid/emptySOAPresponsesreturnedtotheclient.
Theprimarybenefitzend-soapprovides,then,iserrorhandling.Youcanwhitelistexceptiontypes,and,whenencountered,faultresponsescontainingtheexceptiondetailswillbereturned.PHPerrorswillbeemittedasSOAPfaults.
Thenextthingthatzend-soapoffersisWSDLgeneration.WSDLallowsyoutodescribethewebservicesyouoffer,sothatclientsknowhowtoworkwithyourservices.ext/soapprovidesnofunctionalityaroundcreatingWSDL;itsimplyexpectsthatyouwillhaveavalidoneforusewiththeclientorserver.
zend-soapprovidesanAutoDiscoverclassthatusesreflectionontheclassesandfunctionsyoupassitinordertobuildavalidWSDLforyou;youcanthenprovidethistoyourserverandyourclients.
CreatingaserverTherearetwopartstoprovidingaSOAPserver:
Providingtheserveritself,whichwillhandlerequests.ProvidingtheWSDL.
Buildingeachfollowsthesameprocess;yousimplyemitthemwithdifferentHTTPContent-Typeheaders,andunderdifferentHTTPmethods(theserverwillalwaysreacttoPOSTrequests,whileWSDLshouldbeavailableviaGET).
First,let'sdefineafunctionforpopulatingaserverinstancewithclassesandfunctions:
4
ImplementaSOAPserverwithzend-soap
109
useAcme\Model;
functionpopulateServer($server,array$env)
{
//Exposeaclassanditsmethods:
$server->setClass(Model\Calculator::class);
//Orexposeanobjectinstanceanditsmethods.
//However,thisonlyworksforZend\Soap\Server,notAutoDiscover,so
//shouldnotbeusedhere.
//$server->setObject(newModel\Env($env));
//Exposeafunction:
$server->addFunction('Acme\Model\ping');
}
Notethat$serverisnottype-hinted;therationaleforthisdecisionwillbecomemoreobvioussoon.
Now,let'sassumethattheabovefunctionisavailabletous,anduseittocreateourWSDL:
//File/soap/wsdl.php
useZend\Soap\AutoDiscover;
if($_SERVER['REQUEST_METHOD']!=='GET'){
//OnlyhandleGETrequests
header('HTTP/1.1400ClientError');
exit;
}
$wsdl=newAutoDiscover();
populateServer($wsdl,$_ENV);
$wsdl->handle();
Done!TheabovewillemittheWSDLforeithertheclientorservertoconsume.
Now,let'screatetheserver.Theserverrequiresafewthings:
Thepublic,HTTP-accessiblelocationoftheWSDL.SoapServeroptions,includingtheactorURIfortheserverandSOAPversiontargeted.
Additionally,we'llneedtonotifytheserverofitscapabilities,viathepopulateServer()function.
ImplementaSOAPserverwithzend-soap
110
//File/soap/server.php
useZend\Soap\Server;
if($_SERVER['REQUEST_METHOD']!=='POST'){
//OnlyhandlePOSTrequests
header('HTTP/1.1400ClientError');
exit;
}
$server=newServer(dirname($_SERVER['REQUEST_URI']).'/wsdl.php',[
'actor'=>$_SERVER['REQUEST_URI'],
]);
populateServer($server,$_ENV);
$server->handle();
Thereasonforthelackoftype-hintshouldnowbeclear;boththeServerandAutoDiscoverclasseshavethesameAPIforpopulatingtheinstanceswithclasses,objects,andfunctions;havingacommonfunctionfordoingsoallowsustoensuretheWSDLandserverdonotgooutofsync.
Fromhere,youcanpointyourclientsat/soap/server.phponyourdomain,andtheywillhavealltheinformationtheyneedtoworkwithyourservice.
setObject()
Zend\Soap\ServeralsoexposesasetObject()method,whichwilltakeanobjectinstance,reflectit,andexposeitspublicmethodstotheserver.However,thismethodisonlyavailableintheServerclass,nottheAutoDiscoverclass.
Assuch,ifyouwanttocreatelogicthatcanbere-usedbetweentheServerandAutoDiscoverinstances,youmustconfineyourusagetosetClass().Ifthatclassrequiresconstructorargumentsorotherwaysofsettinginstancestate,youshouldvarythelogicforcreationoftheWSDLviaAutoDiscoverandcreationoftheserverviaServer.
Usingzend-soapwithinazend-mvcapplicationTheabovedetailsanapproachusingvanillaPHP;whataboutusingzend-soapwithinazend-mvccontext?
Todothis,we'llneedtolearnafewmorethings.
ImplementaSOAPserverwithzend-soap
111
First,youcanprovideServer::handle()withtherequesttoprocess.Thismustbeoneofthefollowing:
aDOMDocumentaDOMNodeaSimpleXMLElementanobjectimplementing__toString(),wherethatmethodreturnsanXMLstringanXMLstring
WecangrabthisinformationfromtheMVCrequestinstance'sbodycontent.
Second,wewillneedtheservertoreturntheresponse,sowecanuseittopopulatetheMVCresponseinstance.WecandothatbycallingServer::setReturnResponse(true).Whenwedo,Server::handle()willreturnanXMLstringrepresentingtheSOAPresponsemessage.
Let'sputitalltogether:
namespaceAcme\Controller;
useAcme\Model;
useZend\Soap\AutoDiscoverasWsdlAutoDiscover;
useZend\Soap\ServerasSoapServer;
useZend\Mvc\Controller\AbstractActionController;
classSoapControllerextendsAbstractActionController
{
private$env;
publicfunction__construct(Model\Env$env)
{
$this->env=$env;
}
publicfunctionwsdlAction()
{
/**@var\Zend\Http\Request$request*/
$request=$this->getRequest();
if(!$request->isGet()){
return$this->prepareClientErrorResponse('GET');
}
$wsdl=newWsdlAutoDiscover();
$this->populateServer($wsdl);
/**@var\Zend\Http\Response$response*/
$response=$this->getResponse();
$response->getHeaders()->addHeaderLine('Content-Type','application/wsdl+xml')
ImplementaSOAPserverwithzend-soap
112
;
$response->setContent($wsdl->toXml());
return$response;
}
publicfunctionserverAction()
{
/**@var\Zend\Http\Request$request*/
$request=$this->getRequest();
if(!$request->isPost()){
return$this->prepareClientErrorResponse('POST');
}
//Createtheserver
$server=newSoapServer(
$this->url()
->fromRoute('soap/wsdl',[],['force_canonical'=>true]),
[
'actor'=>$this->url()
->fromRoute('soap/server',[],['force_canonical'=>true]),
]
);
$server->setReturnResponse(true);
$this->populateServer($server);
$soapResponse=$server->handle($request->getContent());
/**@var\Zend\Http\Response$response*/
$response=$this->getResponse();
//Settheheadersandcontent
$response->getHeaders()->addHeaderLine('Content-Type','application/soap+xml')
;
$response->setContent($soapResponse);
return$response;
}
privatefunctionprepareClientErrorResponse($allowed)
{
/**@var\Zend\Http\Response$response*/
$response=$this->getResponse();
$response->setStatusCode(405);
$response->getHeaders()->addHeaderLine('Allow',$allowed);
return$response;
}
privatefunctionpopulateServer($server)
{
//Exposeaclassanditsmethods:
$server->setClass(Model\Calculator::class);
//Exposeanobjectinstanceanditsmethods:
ImplementaSOAPserverwithzend-soap
113
$server->setObject($this->env);
//Exposeafunction:
$server->addFunction('Acme\Model\ping');
}
}
Theaboveassumesyou'vecreatedroutessoap/serverandsoap/wsdl,andusesthosetogeneratetheURIsfortheserverandWSDL,respectively;thesoap/serverrouteshouldmaptotheSoapController::serverAction()methodandthesoap/wsdlrouteshouldmaptotheSoapController::wsdlAction()method.
Injectyourdependencies!
You'llnotethattheaboveexampleacceptstheAcme\Model\Envinstanceviaitsconstructor,allowingustoinjectafully-configuredinstanceintotheserverand/orWSDLautodiscovery.Thismeansthatyouwillneedtoprovideafactoryforyourcontroller,toensurethatitisinjectedwithafullyconfiguredinstance—andthatlikelyalsomeansafactoryforthemodel,too.
Tosimplifythis,youmaywanttocheckouttheConfigAbstractFactory orReflectionBasedAbstractFactory ,bothofwhichwereintroducedinversion3.2.0ofzend-servicemanager.
Usingzend-soapwithinPSR-7middlewareUsingzend-soapinPSR-7middlewareisessentiallythesameaswhatwedetailforzend-mvc:you'llneedtopulltherequestcontentfortheserver,andusetheSOAPresponsereturnedtopopulateaPSR-7responseinstance.
Theexamplebelowassumesthefollowing:
YouareusingtheUrlHelperandServerUrlHelperfromzend-expressive-helpers togenerateURIs.Youareroutingtoeachmiddlewaresuchthat:
The'soap.server'routewillmaptotheSoapServerMiddleware,andonlyallowPOSTrequests.The'soap.wsdl'routewillmaptotheWsdlMiddleware,andonlyallowGETrequests.
namespaceAcme\Middleware;
useAcme\Model;
56
7
ImplementaSOAPserverwithzend-soap
114
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\ServerRequestInterface;
useZend\Diactoros\Response\TextResponse;
useZend\Soap\AutoDiscoverasWsdlAutoDiscover;
useZend\Soap\ServerasSoapServer;
traitCommon
{
private$env;
private$urlHelper;
private$serverUrlHelper;
publicfunction__construct(
Model\Env$env,
UrlHelper$urlHelper,
ServerUrlHelper$serverUrlHelper
){
$this->env=$env;
$this->urlHelper=$urlHelper;
$this->serverUrlHelper=$serverUrlHelper;
}
privatefunctionpopulateServer($server)
{
//Exposeaclassanditsmethods:
$server->setClass(Model\Calculator::class);
//Exposeanobjectinstanceanditsmethods:
$server->setObject($this->env);
//Exposeafunction:
$server->addFunction('Acme\Model\ping');
}
}
classSoapServerMiddleware
{
useCommon;
publicfunction__invoke(
ServerRequestInterface$request,
ResponseInterface$response,
callable$next
){
$server=newSoapServer($this->generateUri('soap.wsdl'),[
'actor'=>$this->generateUri('soap.server')
]);
$server->setReturnResponse(true);
$this->populateServer($server);
$xml=$server->handle((string)$request->getBody());
ImplementaSOAPserverwithzend-soap
115
returnnewTextResponse($xml,200,[
'Content-Type'=>'application/soap+xml',
]);
}
privatefunctiongenerateUri($route)
{
return($this->serverUrlHelper)(
($this->urlHelper)($route)
);
}
}
classWsdlMiddleware
{
useCommon;
publicfunction__invoke(
ServerRequestInterface$request,
ResponseInterface$response,
callable$next
){
$server=newWsdlAutoDiscover();
$this->populateServer($server);
returnnewTextResponse($server->toXml(),200,[
'Content-Type'=>'application/wsdl+xml',
]);
}
}
Sinceeachmiddlewarehasthesamebasicconstruction,I'vecreatedatraitwiththecommonfunctionality,andcomposeditintoeachmiddleware.Asyouwillnote,theactualworkofeachmiddlewareisrelativelysimple;createaserver,andmarshalaresposnetoreturn.
Intheaboveexample,Iusethezend-diactoros -specificTextResponsetypetogeneratetheresponse;thiscouldbeanyotherresponsetype,aslongastheContent-Typeheaderissetcorrectly,andthestatuscodeissetto200.
Perthenoteabove,youwillneedtoconfigureyourdependencyinjectioncontainertoinjectthemiddlewareinstanceswiththemodelandhelpers.
Summary
8
ImplementaSOAPserverwithzend-soap
116
WhileSOAPisoftenmalignedinPHPcircles,itisstillinwideusewithinenterprises,andusedinmanycasestoprovidecross-platformwebserviceswithpredictablebehaviors.Itcanbequitecomplex,butzend-soaphelpssmoothoutthebulkofthecomplexity.Youcanuseitstandalone,withinaZendFrameworkMVCapplication,orwithinanyapplicationframeworkyoumightbeusing.
Visitthezend-soapdocumentation tofindoutwhatelseyoumightbeabletodowiththiscomponent.
Footnotes
.https://docs.zendframework.com/zend-soap/↩
.https://en.wikipedia.org/wiki/SOAP↩
.https://apigility.org↩
.http://php.net/soap↩
.https://docs.zendframework.com/zend-servicemanager/config-abstract-factory/↩
.https://docs.zendframework.com/zend-servicemanager/reflection-abstract-factory/↩
.https://docs.zendframework.com/zend-expressive/features/helpers/url-helper↩
.https://docs.zendframework.com/zend-diactoros↩
.https://docs.zendframework.com/zend-soap/↩
9
1
2
3
4
5
6
7
8
9
ImplementaSOAPserverwithzend-soap
117
Context-specificescapingwithzend-escaperbyMatthewWeierO'Phinney
SecurityofyourwebsiteisnotjustaboutmitigatingandpreventingthingslikeSQLinjection;it'salsoaboutprotectingyourusersastheybrowsethesitefromthingslikecross-sitescripting(XSS)attacks,cross-siterequestforgery(CSRF),andmore.Inparticular,youneedtobeverycarefulabouthowyougenerateHTML,CSS,andJavaScripttoensurethatyoudonotcreatesuchvectors.
Asthemantragoes,filterinput,andescapeoutput.
Believeitornot,escapinginPHPisnotterriblyeasytogetright.Forexample,toproperlyescapeHTML,youneedtousehtmlspecialchars(),withtheflagsENT_QUOTES|ENT_SUBSTITUTE,andprovideacharacterencoding.Whoreallywantstowrite
htmlspecialchars($string,ENT_QUOTES|ENT_SUBSTITUTE,'utf-8')
everysingletimetheyneedtoescapeastringforuseinHTML?
EscapingHTMLattributes,CSS,andJavaScripteachrequirearegularexpressiontoidentifyknownproblemstrings,andanumberofheuristicstoreplaceunicodecharacterswithhexentities,eachwithdifferentrules.Whilemuchofthiscanbedonewithbuilt-inPHPfeatures,thesefeaturesdonotcatchallpotentialattackvectors.Acomprehensivesolutionisrequired.
ZendFrameworkprovidesthezend-escaper componenttomanagethiscomplexityforyou,exposingfunctionalityforescapingHTML,HTMLattributes,JavaScript,CSS,andURLstoensuretheyaresafeforthebrowser.
Installationzend-escaperonlyrequiresPHP(ofatleastversion5.5atthetimeofwriting),andisinstallableviacomposer:
$composerrequirezendframework/zend-escaper
1
Context-specificescapingwithzend-escaper
118
UsageWhileweconsideredmakingzend-escaperactaseitherfunctionsorstaticmethods,therewasonethingintheway:properescapingrequiresknowledgeoftheintendedoutputcharacterset.Assuch,Zend\Escaper\Escapermustfirstbeinstantiatedwiththeoutputcharacterset;onceithas,youcallmethodsonit.
useZend\Escaper\Escaper;
$escaper=newEscaper('iso-8859-1');
Bydefault,ifnocharactersetisprovided,itassumesutf-8;werecommendusingUTF-8unlessthereisacompellingreasonnotto.Assuch,inmostcases,youcaninstantiateitwithnoarguments:
useZend\Escaper\Escaper;
$escaper=newEscaper();
Theclassprovidesfivemethods:
escapeHtml(string$html):stringwillescapethestringsoitmaybesafelyusedasHTML.Ingeneral,thismeans<,>,and&characters(aswellasothers)areescapedtopreventinjectionofunwantedtagsandentities.escapeHtmlAttr(string$value):stringescapesastringsoitmaysafelybeusedwithinanHTMLattributevalue.escapeJs(string$js):stringescapesastringsoitmaysafelybeusedwithina<script>tag.Inparticular,thisensuresthatthecodeinjectedcannotcontaincontinuationsandescapesequencesthatleadtoXSSvectors.escapeCss(string$css):stringescapesastringtouseasCSSwithin<style>tags;similartoJS,itpreventscontinuationsandescapesequencesthatcanleadtoXSSvectors.escapeUrl(string$urlPart):stringescapesastringtousewithinaURL;itshouldnotbeusedtoescapetheentireURLitself.ItshouldbeusedtoescapethingssuchastheURLpath,querystringparameters,andfragment,however.
So,asexamples:
Context-specificescapingwithzend-escaper
119
echo$escaper->escapeHtml('<script>alert("zf")</script>');
//resultsin"<script>alert("zf")</script>"
echo$escaper->escapeHtmlAttr("<script>alert('zf')</script>");
//resultsin"<script>alert('zf')</script>"
echo$escaper->escapeJs("bar";alert("zf");varxss="true");
//resultsin"bar\x26quot\x3B\x3B\x20alert\x28\x26quot\x3Bzf\x26quot\x3B\x29\x3B\x20v
ar\x20xss\x3D\x26quot\x3Btrue"
echo$escaper->escapeCss("background-image:url('/zf.png?</style><script>alert(\'zf\')
</script>');");
//resultsin"background\2Dimage\3A\20url\28\27\2Fzf\2Epng\3F\3C\2Fstyle\3E
\3Cscript\3Ealert\28\5C\27zf\5C\27\29\3C\2Fscript\3E\27\29\3B"
echo$escaper->escapeUrl('/foo"onmouseover="alert(\'zf\')');
//resultsin"%2Ffoo%20%22%20onmouseover%3D%22alert%28%27zf%27%29"
Asyoucanseefromtheseexamples,thecomponentaggresivelyfilterseachstringtoensureitisescapedcorrectlyforthecontextforwhichitisintended.
Howandwheremightyouusethis?
Withintemplates,toensureoutputisproperlyescaped.Forexample,zend-viewincludeshelpersforit;itwouldbeeasytoaddsuchfunctionalitytoPlates andothertemplatingsolutions.Inemailtemplates.InserializersforAPIs,toensurethingslikeURLsorXMLattributedataareproperlyescaped.Inerrorhandlers,toensureerrormessagesareescapedanddonotcontainXSSvectors.
Themainpointisthatescapingcanbeeasywithzend-escaper;startsecuringyouroutputtoday!
Footnotes
.https://docs.zendframework.com/zend-escaper↩
.https://docs.zendframework.com/zend-view↩
.http://platesphp.com↩
23
1
2
3
Context-specificescapingwithzend-escaper
120
Filterinputusingzend-filterbyMatthewWeierO'Phinney
Whensecuringyourwebsite,themantrais"Filterinput,escapeoutput."WecoveredescapingoutputintheContext-specificescapingwithzend-escaperarticle.We'renowgoingtoturntofilteringinput.
Filteringinputisrathercomplex,andspansanumberofpractices:
Filtering/normalizinginput.Asanexample,yourwebpagemayhaveaformthatallowssubmittingacreditcardnumber.Thesehaveavarietyofformatsthatmayincludespacesordashesordots—buttheonlycharactersthatareofimportancearethedigits.Assuch,youwillwanttonormalizesuchinputtostripouttheunwantedcharacters.Validatinginput.Onceyouhavedonesuchnormalization,youcanthenchecktoseethatthedataisactuallyvalidforitscontext.Thismayincludeoneormorerules.Usingourcreditcardexample,youmightfirstcheckitisofanappropriatelength,andthenverifythatitbeginswithaknownvendordigit,andonlyafterthosepass,validatethenumberagainstaonlineservice.
Fornow,we'regoingtolookatthefirstitem,filteringandnormalizinginput,usingthecomponentzend-filter .
InstallationToinstallzend-filter,useComposer:
$composerrequirezendframework/zend-filter
Currently,theonlyrequireddependencyiszend-stdlib.However,afewothercomponentsaresuggested,basedonwhichfiltersand/orfeaturseyoumaywanttouse:
zendframework/zend-servicemanagerisusedbytheFilterChaincomponentforlookingupfiltersbytheirshortname(versusfullyqualifiedclassname).zendframework/zend-cryptisusedbytheencryptionanddecryptionfilters.zendframework/zend-uriisusedbytheUriNormalizefilter.zendframework/zend-i18nisusedbyseveralfiltersthatprovideinternationalizationfeatures.
1
Filterinputusingzend-filter
121
Forourexamples,we'llbeusingtheFilterChainfunctionality,sowewillalsowanttoinstallzend-servicemanager:
$composerrequirezendframework/zend-servicemanager
FilterInterfaceFilterscanbeoneoftwothings:acallablethatacceptsasingleargument(thevaluetofilter),oraninstanceofZend\Filter\FilterInterface:
namespaceZend\Filter;
interfaceFilterInterface
{
publicfunctionfilter($value);
}
Thevaluecanbeliterallyanything,andthefiltercanreturnanythingitself.Generallyspeaking,ifafiltercannotoperateonthevalue,itisexpectedtoreturnitverbatim.
zend-filterprovidesafewdozenfiltersforcommonoperations,includingthingslike:
Normalizingstrings,integers,etc.totheircorrespondingbooleanvalues.Normalizingstringsrepresentingintegerstointegervalues.Normalizingemptyvaluestonullvalues.Normalizinginputsetsrepresentingdateand/ortimeselectionsfromformstoDateTimeinstances.NormalizingURIvalues.Comparingvaluestowhitelistsandblacklists.Trimmingwhitespace,strippingnewlines,andremovingHTMLtagsorentities.Upperandlowercasingwords.Strippingeverythingbutdigits.PerformingPCREregexpreplacements.Wordinflection(camel-casetounderscoresandviceversa,etc.).Decryptingandencryptingfilecontents,aswellascastingfilecontentstoloweroruppercase.Compressinganddecompressingvalues.Decryptingandencryptingvalues.
Anyofthesemaybeusedbythemselves.However,inmostcases,ifthat'sallyou'redoing,youmightaswelljustdothefunctionalityinline.So,what'sthebenefitofzend-filter?
Filterinputusingzend-filter
122
Chainingfilters!
FilterChainWhenwegetinputfromtheweb,itgenerallycomesasstrings,andistheresultofuserinput.Assuch,weoftengetalotofgarbage:extraspaces,unnecessarynewlines,HTMLcharacters,etc.
Whenfilteringsuchinput,wemightwanttoperformseveraloperations:
$value=$request->getParsedBody()['phone']??'';
$value=trim($value);
$value=preg_replace("/[^\n\r]/",'',$value);
$value=preg_replace('/[^\d]/','',$value);
Wethenneedtotestourcodetoensurethatwe'refilteringcorrectly.Additionally,ifatanypointwefailtore-assign,wemaylosethechangeswewereperforming!
Withzend-filter,wecaninsteaduseaFilterChain.Theaboveexamplebecomes:
useZend\Filter\FilterChain;
$filter=newFilterChain();
//attachByNameusestheclassname,minusthenamespace,and
$filter->attachByName('StringTrim');
$filter->attachByName('StripNewlines');
$filter->attachByName('Digits');
$value=$filter->filter($request->getParsedBody()['phone']??'');
Here'sanotherexample:let'ssaywehaveconfigurationkeysthatareinsnake_case_format,andwhichmaybereadfromafile,andwewishtoconvertthosevaluestoCamelCase.
useZend\Filter;
$filter=newFilter\FilterChain();
//attachletsyouprovidetheinstanceyouwishtouse;thiswillwork
//evenwithoutzend-servicemanagerinstalled.
$filter->attach(newFilter\StringTrim());
$filter->attach(newFilter\StripNewlines());//becausewemayhave\rcharacters
$filter->attach(newFilter\Word\UnderscoreToCamelCase());
$configKeys=array_map([$filter,'filter'],explode("\n",$fileContents));
Filterinputusingzend-filter
123
ThisnewexampledemonstratesakeyfeatureofaFilterChain:youcanre-useit!Insteadofhavingtoputthecodefornormalizingthevalueswithinanarray_mapcallback,wecaninsteaddirectlyuseouralreadyconfiguredFilterChain,invokingitonceforeachvalue!
Wrappingupzend-filtercanbeapowerfultoolinyourarsenalfordealingwithuserinput.Pairedwithgoodvalidation,youcanprotectyourapplicationfrommaliciousormalformedinput.
Footnotes
.https://docs.zendframework.com/zend-filter/↩1
Filterinputusingzend-filter
124
Validateinputusingzend-validatorbyMatthewWeierO'Phinney
InourarticleFilterinputusingzend-filter,wecoveredfilteringdata.Thefiltersinzend-filteraregenerallyusedtopre-filterornormalizeincomingdata.Thisisallwellandgood,butwestilldon'tknowifthedataisvalid.That'swherezend-validatorcomesin.
InstallationToinstallzend-validator,useComposer:
$composerrequirezendframework/zend-validator
Likezend-filter,theonlyrequireddependencyiszend-stdlib.However,afewothercomponentsaresuggested,basedonwhichfiltersand/orfeaturesyoumaywanttouse:
zendframework/zend-servicemanagerisusedbytheValidatorPluginManagerandValidatorChaintolookupvalidatorsbytheirshortname(versusfullyqualifiedclassname),aswellastoallowusageofvalidatorswithdependencies.zendframework/zend-dbisusedbyapairofvalidatorsthatcancheckifamatchingrecordexists(ordoesnot!).zendframework/zend-uriisusedbytheUrivalidator.TheCSRFvalidatorrequiresbothzendframework/zend-mathandzendframework/zend-session.zendframework/zend-i18nandzendframework/zend-i18n-resourcescanbeinstalledinordertoprovidetranslationofvalidationerrormessages.
Forourexamples,we'llbeusingtheValidatorChainfunctionalitywithaValidatorPluginManager,sowewillalsowanttoinstallzend-servicemanager:
$composerrequirezendframework/zend-servicemanager
ValidatorInterfaceThecurrentincarnationofzend-validatorisstateful;validationerrormessagesarestoredinthevalidatoritself.Assuch,validatorsmustimplementtheValidatorInterface:
Validateinputusingzend-validator
125
namespaceZend\Validator;
interfaceValidatorInterface
{
/**
*@parammixed$value
*@returnbool
*/
publicfunctionisValid($value);
/**
*@returnarray
*/
publicfunctiongetMessages();
}
The$valuecanbeliterallyanything;avalidatorexaminesittoseeifitisvalid,andreturnsabooleanresult.Ifitisinvalid,asubsequentcalltogetMessages()shouldreturnanassociativearraywiththekeysbeingmessageidentifiers,andthevaluesthehuman-readablemessagestrings.
Assuch,usagelookslikethefollowing:
if(!$validator->isValid($value)){
//Invalidvalue
echo"Failedvalidation:\n";
foreach($validator->getMessages()as$message){
printf("-%s\n",$message);
}
returnfalse;
}
//Validvalue!
returntrue;
Statelessvalidationsareplanned
Atthetimeofwriting,wehaveproposed anewvalidationcomponenttoworkinparallelwithzend-validator;thisnewcomponentwillimplementastatelessarchitecture.Itsproposedvalidationinterfacewillnolongerreturnaboolean,butratheraValidationResult.Thatinstancewillprovideamethodfordeterminingifthevalidationwassuccessful,encapsulatethevaluethatwasvalidated,and,forinvalidvalues,provideaccesstothevalidationerrormessages.Doingsowillallowbetterre-useofvalidatorswithinthesameexecutionprocess.
Thisproposalalsoincludescodeforadaptingexistingzend-validatorimplementationstoworkwiththestatelessdesign.
1
Validateinputusingzend-validator
126
zend-validatorprovidesafewdozenfiltersforcommonoperations,includingthingslike:
CommonconditionalslikeLessThan,GreaterThan,Identical,NotEmpty,IsInstanceOf,InArray,andBetween.Stringvalues,suchasStringLength,Regex.Network-relatedvaluessuchasHostname,Ip,Uri,andEmailAddress.BusinessvaluessuchasBarcode,CreditCard,GpsPoint,Iban,andUuid.DateandtimerelatedvaluessuchasDate,DateStep,andTimezone.
Anyofthesevalidatorsmaybeusedbythemselves.
Inmanycases,though,yourvalidationmayberelatedtoasetofvalidations:asanexample,thevaluemustbenon-empty,acertainnumberofcharacters,andfulfillaregularexpression.Likefilters,zend-validatorallowsyoutodothiswithchains.
ValidatorChainUsageofavalidatorchainissimilartofilterchains:attachvalidatorsyouwanttoexecute,andthenpassthevaluetothechain:
useZend\Validator;
$validator=newValidator\ValidatorChain();
$validator->attach(newValidator\NotEmpty());
$validator->attach(newValidator\StringLength(['min'=>6]));
$validator->attach(newValidator\Regex('/^[a-f0-9]{6,12}$/');
if(!$validator->isValid($value)){
//Failedvalidation
var_dump($validator->getMessages());
}
Theaboveusesvalidatorinstances,eliminatingtheneedforValidatorPluginManager,andthusavoidsusageofzend-servicemanager.However,ifwehavezend-servicemanagerinstalled,wecanreplaceusageofattach()withattachByName():
Validateinputusingzend-validator
127
useZend\Validator;
$validator=newValidator\ValidatorChain();
$validator->attachByName('NotEmpty');
$validator->attachByName('StringLength',['min'=>6]);
$validator->attachByName('Regex',['pattern'=>'/^[a-f0-9]{6,12}$/']);
if(!$validator->isValid($value)){
//Failedvalidation
var_dump($validator->getMessages());
}
BreakingthechainIfyouweretoruneitheroftheseexampleswith$value='',youmaydiscoversomethingunexpected:you'llgetvalidationerrormessagesforeverysinglevalidator!Thisseemswasteful;there'snoneedtoruntheStringLengthorRegexvalidatorsifthevalueisempty,isthere?
Tosolvethisproblem,whenattachingavalidator,wecantellthechaintobreakexecutionifthegivenvalidatorfails.Thisisdonebypassingabooleanflag:
asthesecondargumenttoattach()asthethirdargumenttoattachByName()(thesecondargumentisanarrayofconstructoroptions)
Let'supdatethesecondexample:
useZend\Validator;
$validator=newValidator\ValidatorChain();
$validator->attachByName('NotEmpty',[],$breakChainOnFailure=true);
$validator->attachByName('StringLength',['min'=>6],true);
$validator->attachByName('Regex',['pattern'=>'/^[a-f0-9]{6,12}$/']);
if(!$validator->isValid($value)){
//Failedvalidation
var_dump($validator->getMessages());
}
Theaboveaddsabooleantrueasthe$breakChainOnFailureargumenttotheattachByName()methodcallsoftheNotEmptyandStringLengthvalidators(wehadtoprovideanemptyarrayofoptionsfortheNotEmptyvalidatorsowecouldpasstheflag).Inthesecases,ifthevaluefailsvalidation,nofurthervalidatorswillbeexecuted.
Validateinputusingzend-validator
128
Thus:
$value=''willresultinasinglevalidationfailuremessage,producedbytheNotEmptyvalidator.$value='test'willresultinasinglevalidationfailuremessage,producedbytheStringLengthvalidator.$value='testthis'willresultinasinglevalidationfailuremessage,producedbytheRegexvalidator.
PrioritizationValidatorsareexecutedinthesameorderinwhichtheyareattachedtothechainbydefault.However,internally,theyarestoredinaPriorityQueue;thisallowsyoutoprovideaspecificorderinwhichtoexecutethevalidators.Highervaluesexecuteearlier,whilelowervalues(includingnegativevalues)executelast.Thedefaultpriorityis1.
Priorityvaluesmaybepassedasthethirdargumenttoattach()andfourthargumenttoattachByName().
Asanexample:
$validator=newValidator\ValidatorChain();
$validator->attachByName('StringLength',['min'=>6],true,1);
$validator->attachByName('Regex',['pattern'=>'/^[a-f0-9]{6,12}$/'],false,-100);
$validator->attachByName('NotEmpty',[],true,100);
Intheabove,whenexecutingthevalidationchain,theorderwillstillbeNotEmpty,followedbyStringLength,followedbyRegex.
Whyprioritize?
Whywouldyouusethisfeature?Themainreasonisifyouwanttodefinevalidationchainsviaconfiguration,andcannotguaranteetheorderinwhichtheitemswillbepresentinconfiguration.Byaddingapriorityvalue,youcanensurethatrecreationofthevalidationchainwillpreservetheexpectedorder.
ContextSometimeswemaywanttovaryhowwevalidateavaluebasedonwhetherornotanotherpieceofdataispresent,orbasedonthatotherpieceofdata'svalue.zend-validatoroffersanunofficialAPIforthat,viaanoptional$contextvalueyoucanpasstoisValid().The
Validateinputusingzend-validator
129
ValidatorChainacceptsthisvalue,and,ifpresent,willpassittoeachvalidatoritcomposes.
Asanexample,let'ssayyouwanttocaptureanemailaddress(formfield"contact"),butonlyiftheuserhasselectedaradiobuttonallowingyoutodoso(formfield"allow_contact").Wemightwritethatvalidatorasfollows:
useArrayAccess;
useArrayObject;
useZend\Validator\EmailAddress;
useZend\Validator\ValidatorInterface;
classContactEmailValidatorimplementsValidatorInterface
{
constERROR_INVALID_EMAIL='contact-email-invalid';
/**@varstring*/
private$contextVariable;
/**@varEmailAddress*/
private$emailValidator;
/**@varstring[]*/
private$messages=[];
/**@varstring[]*/
private$messageTemplates=[
self::ERROR_INVALID_EMAIL=>'Emailaddress"%s"isinvalid',
];
publicfunction__construct(
EmailAddress$emailValidator=null,
string$contextVariable='allow_contact'
){
$this->emailValidator=$emailValidator?:newEmailAddress();
$this->contextVariable=$contextVariable;
}
publicfunctionisValid($value,$context=null)
{
$this->messages=[];
if(!$this->allowsContact($context)){
//Valuewillbediscarded,soalwaysvalid.
returntrue;
}
if($this->emailValidator->isValid($value)){
returntrue;
}
$this->messages[self::ERROR_INVALID_EMAIL]=sprintf(
$this->messageTemplates[self::ERROR_INVALID_EMAIL],
Validateinputusingzend-validator
130
var_export($value,true)
);
returnfalse;
}
publicfunctiongetMessages()
{
return$this->messages;
}
privatefunctionallowsContact($context):bool
{
if(!$context||
!(is_array($context)
||$contextinstanceofArrayObject
||$contextinstanceofArrayAccess)
){
returnfalse;
}
$allowsContact=$context[$this->contextVariable]??false;
return(bool)$allowsContact;
}
}
Wewouldthenaddittothevalidatorchain,andcallitlikeso:
$validator->attach(newContactEmailValidator());
if(!$validator->isValid($data['contact'],$data)){
//Failedvalidation!
}
Thisapproachcanallowforsomequitecomplexvalidationroutines,particularlyifyounestvalidationchainswithincustomvalidators!
Registeringyourownvalidators.Ifyouwriteyourownvalidators,chancesareyou'llwanttousethemwiththeValidatorChain.ThisclasscomposesaValidatorPluginManager,whichisapluginmanagerbuiltontopofzend-servicemanager.Assuch,youcanregisteryourvalidatorswithit:
Validateinputusingzend-validator
131
$plugins=$validator->getPluginManager();
$plugins->setFactory(ContactEmailValidator::class,ContactEmailValidatorFactory::class
);
$plugins->setService(ContactEmailValidator::class,$contactEmailValidator);
Alternately,ifusingzend-mvcorExpressive,youcanprovideconfigurationviathevalidatorsconfigurationkey:
return[
'validators'=>[
'factories'=>[
ContactEmailValidator::class=>ContactEmailValidatorFactory::class,
],
],
];
Ifyouwanttousea"shortname"toidentifyyourvalidator,werecommendusinganalias,aliasingtheshortnametothefullyqualifiedclassname.
WrappingupBetweenusingzend-filtertonormalizeandpre-filtervalues,andzend-validatortovalidatethevalues,youcanstartlockingdowntheinputyouruserssubmittoyourapplication.
Thatsaid,whatwe'vedemonstratedsofarishowtoworkwithsinglevalues.Mostformssubmitsetsofvalues;usingtheapproachessofarcanleadtoalotofcode!
Wehaveasolutionforthisaswell,viaourzend-inputfiltercomponent.ReadthearticleValidatedatausingzend-inputfilterformoreinformation.
Footnotes
.https://discourse.zendframework.com/t/rfc-new-validation-component/208/↩1
Validateinputusingzend-validator
132
Validatedatausingzend-inputfilterbyMatthewWeierO'Phinney
InourarticlesFilterinputusingzend-filterandValidateinputusingzend-validator,wecoveredtheusageofzend-filterandzend-validator.Withthesetwocomponents,younowhavethetoolsnecessarytoensureanygivenuserinputisvalid,fulfillingthefirsthalfofthe"filterinput,escapeoutput"mantra.
However,aswediscussedinthezend-validatorarticle,aspowerfulasvalidationchainsare,theyonlyallowyoutovalidateasinglevalueatatime.Howdoyougoaboutvalidatingsetsofvalues—suchasdatasubmittedfromaform,oraresourceforanAPI?
Tosolvethatproblem,ZendFrameworkprovideszend-inputfilter .Aninputfilteraggregatesoneormoreinputs,anyoneofwhichmayalsobeanotherinputfilter,allowingyoutovalidatecomplex,multi-set,andnestedsetvalues.
InstallationToinstallzend-inputfilter,useComposer:
$composerrequirezendframework/zend-inputfilter
zend-inputfilteronlydirectlyrequireszend-filter,zend-stdlib,andzend-validator.Touseitspowerfulfactoryfeature,however,you'llalsoneedzend-servicemanager,asitgreatlysimplifiescreationofinputfilters:
$composerrequirezendframework/zend-servicemanager
TheoryofoperationAninputfiltercomposesoneormoreinputs,anyofwhichmayalsobeaninputfilter(andthusrepresentasetofdatavalues).
Anygiveninputisconsideredrequiredbydefault,butcanbeconfiguredtobeoptional.Whenrequired,aninputwillbeconsideredinvalidifthevalueisnotpresentinthedataset,orisempty.Whenoptional,ifthevalueisnotpresent,orisempty,itisconsideredvalid.An
1
Validatedatausingzend-inputfilter
133
additionalflag,allow_empty,canbeusedtoallowemptyvaluesforrequiredelements;stillanotherflag,continue_if_empty,willforcevalidationtooccurforeitherrequiredoroptionalvaluesifthevalueispresentbutempty.
Whenvalidatingavalue,twostepsoccur:
Thevalueispassedtoafilterchaininordertonormalizethevalue.Typicalnormalizationsincludestrippingnon-digitcharactersforphonenumbersandcreditcardnumbers;trimmingwhitespace;etc.Thevalueisthenpassedtoavalidatorchaintodetermineifthenormalizedvalueisvalid.
Aninputfilteraggregatestheinputs,aswellasthevaluesthemselves.Youpasstheuserinputtotheinputfilterafterithasbeenconfigured,andthenchecktoseeifitisvalid.Ifitis,youcanpullthenormalizedvaluesfromit(aswellastherawvalues,ifdesired).Ifanyvalueisinvalid,youwouldthenpullthevalidationerrormessagesfromit.
Statelessoperation
Thecurrentapproachisstateful:valuesarepassedtotheinputfilterbeforeyouexecuteitsisValid()method,andthenthevaluesandanyvalidationerrormessagesarestoredwithintheinputfilterinstanceforlaterretrieval.Thiscancauseissuesifyouwishtousethesameinputfiltermultipletimesinthesamerequest.
Forthisreason,weareplanninganew,parallelcomponentthatprovidesstatelessvalidation:callingisValid()willrequirepassingthevalue(s)tovalidate,andbothinputsandinputfiltersalikewillreturnaresultobjectfromthismethodwiththerawandnormalizedvalues,theresultofvalidation,andanyvalidationerrormessages.
GettingstartedLet'sconsideraregistrationformwherewewanttocaptureauseremailandtheirpassword.Inourfirstexample,wewilluseexplicitusage,whichdoesnotrequiretheuseofpluginmanagers.
Validatedatausingzend-inputfilter
134
useZend\Filter;
useZend\InputFilter\Input;
useZend\InputFilter\InputFilter;
useZend\Validator;
$email=newInput('email');
$email->getFilterChain()
->attach(newFilter\StringTrim());
$email->getValidatorChain()
->attach(newValidator\EmailAddress());
$password=newInput('password');
$password->getValidatorChain()
->attach(newValidator\StringLength(8),true)
->attach(newValidator\Regex('/[a-z]/'))
->attach(newValidator\Regex('/[A-Z]/'))
->attach(newValidator\Regex('/[0-9]/'))
->attach(newValidator\Regex('/[.!@#$%^&*;:]/'));
$inputFilter=newInputFilter();
$inputFilter->add($email);
$inputFilter->add($password);
$inputFilter->setData($_POST);
if($inputFilter->isValid()){
echo"Theformisvalid\n";
$values=$inputFilter->getValues();
}else{
echo"Theformisnotvalid\n";
foreach($inputFilter->getInvalidInput()as$error){
var_dump($error->getMessages());
}
}
Theabovecreatestwoinputs,oneeachfortheincomingemailaddressandpassword.Theemailaddresswillbetrimmedofwhitespace,andthenvalidated.Thepasswordwillbevalidatedonly,checkingthatwehaveavalueofatleast8characters,withatleastoneeachoflowercase,uppercase,digit,andspecialcharacters.Further,ifanygivencharacterismissing,we'llgetavalidationerrormessagesothattheuserknowshowtocreatetheirpassword.
Eachinputisaddedtoaninputfilterinstance.Wepasstheformdata(viathe$_POSTsuperglobal),andthenchecktoseeifitisvalid.Ifso,wegrabthevaluesfromit(wecangettheoriginalvaluesviagetRawValues()).Ifnot,wegraberrormessagesfromit.
Bydefault,allinputsareconsideredrequired.Let'ssaywealsowantedtocollecttheuser'sfullname,butmakethatoptional.Wecouldcreateaninputlikethefollowing:
Validatedatausingzend-inputfilter
135
$name=newInput('user_name');
$name->setRequired(false);//OPTIONAL!
$name>getFilterChain()
->attach(newFilter\StringTrim());
InputspecificationsAsnotedinthe"Installation"section,wecanleveragezend-servicemanagerandthevariouspluginmanagerscomposedinitinordertocreateourfilters.
Zend\InputFilter\InputFilterinternallycomposesZend\InputFilter\Factory,whichitselfcomposes:
Zend\InputFilter\InputFilterPluginManager,apluginmanagerformanagingZend\InputFilter\InputandZend\InputFilter\InputFilterinstances.Zend\Filter\FilterPluginManager,apluginmanagerforfilters.Zend\Validator\ValidatorPluginManager,apluginmanagerforvalidators.
Theupshotisthatwecanoftenusespecificationsinsteadofinstancestocreateourinputsandinputfilters.
Assuch,ouraboveexamplescanbewrittenlikethis:
Validatedatausingzend-inputfilter
136
useZend\InputFilter\InputFilter;
$inputFilter=newInputFilter();
$inputFilter->add([
'name'=>'email',
'filters'=>[
['name'=>'StringTrim']
],
'validators'=>[
['name'=>'EmailAddress']
],
]);
$inputFilter->add([
'name'=>'user_name',
'required'=>false,
'filters'=>[
['name'=>'StringTrim']
],
]);
$inputFilter->add([
'name'=>'password',
'validators'=>[
[
'name'=>'StringLength',
'options'=>['min'=>8],
'break_chain_on_failure'=>true,
],
['name'=>'Regex','options'=>['pattern'=>'/[a-z]/'],
['name'=>'Regex','options'=>['pattern'=>'/[A-Z]/'],
['name'=>'Regex','options'=>['pattern'=>'/[0-9]/'],
['name'=>'Regex','options'=>['pattern'=>'/[.!@#$%^&*;:]/'],
],
]);
Thereareanumberofotherfieldsyoucoulduse:
typeallowsyoutospecifytheinputorinputfilterclasstousewhencreatingtheinput.error_messageallowsyoutospecifyasingleerrormessagetoreturnforaninputonvalidationfailure.Thisisoftenusefulasotherwiseyou'llgetanarrayofmessagesforeachinput.allow_emptyandcontinue_if_empty,whichwerediscussedearlier,andcontrolhowvalidationoccurswhenemptyvaluesareencountered.
Whywouldyoudothisinsteadofusingtheprogrammaticinterface,though?
First,thisapproachleveragesthevariouspluginmanagers,whichmeansthatanygiveninput,inputfilter,filter,orvalidatorwillbepulledfromtheirrespectivepluginmanager.Thisallowsyoutoprovideadditionaltypeseasily,but,moreimportantly,overrideexistingtypes.
Validatedatausingzend-inputfilter
137
Second,theconfiguration-basedapproachallowsyoutostorethedefinitionsinconfiguration,andpotentiallyevenoverridethedefinitionsviaconfigurationmerging!Apigilityutilizesthisfeatureheavily,inparttoprovidedifferentinputfiltersbasedonAPIversion.
Managingthepluginmanagers
Toensurethatyoucanusealreadyconfiguredpluginmanagers,youcaninjectthemintotheZend\InputFilter\Factorycomposedinyourinputfilter.Asanexample,consideringthefollowingservicefactoryforaninputfilter:
function(ContainerInterface$container)
{
$filters=$container->get('FilterManager');
$validators=$container->get('ValidatorManager');
$inputFilters=$container->get('InputFilterManager');
$inputFilter=newInputFilter();
$inputFilterFactory=$inputFilter->getFactory();
$inputFilterFactory->setDefaultFilterChain($filters);
$inputFilterFactory->setDefaultValidatorChain($validators);
$inputFilterFactory->setInputFilterManager($inputFilters);
//addinputstothe$inputFilter,andfinallyreturnit...
return$inputFilter;
}
ManagingInputFiltersTheInputFilterPluginManagerallowsyoutodefineinputfilterswithdependencies,whichgivesyoutheabilitytocreatere-usable,complexinputfilters.OnekeyaspecttousingthisfeatureisthattheInputFilterPluginManageralsoensurestheconfiguredfilterandvalidatorpluginmanagersareinjectedinthefactoryusedbytheinputfilter,ensuringanyoverridesorcustomfiltersandvalidatorsyou'vedefinedarepresent.
Tomakethiswork,thebaseInputFilterimplementationalsoimplementsZend\Stdlib\InitializableInterface,whichdefinesaninit()method;theInputFilterPluginManagercallsthisafterinstantiatingyourinputfilterandinjectingitwithafactorycomposingallthevariouspluginmanagerservices.
Whatthismeansisthatifyouusethismethodtoadd()yourinputsandnestedinputfilters,everythingwillbeproperlyconfigured!
Validatedatausingzend-inputfilter
138
Asanexample,let'ssaywehavea"transaction_id"field,andthatweneedtocheckifthattransactionidentifierexistsinthedatabase.Assuch,wemayhaveacustomvalidatorthatdependsonadatabaseconnectiontodothis.Wecouldwriteourinputfilterasfollows:
namespaceMyBusiness;
useZend\InputFilter\InputFilter;
classOrderInputFilterextendsInputFilter
{
publicfunctioninit()
{
$this->add([
'name'=>'transaction_id',
'validators'=>[
['name'=>TransactionIdValidator::class],
],
]);
}
}
Wewouldthenregisterthisinourinput_filtersconfiguration:
//inconfig/autoload/input_filters.global.php
return[
'input_filters'=>[
'invokables'=>[
MyBusiness\OrderInputFilter::class=>MyBusiness\OrderInputFilter::class,
],
],
'validators'=>[
'factories'=>[
MyBusiness\TransactionIdValidator::class=>MyBusiness\TransactionIdValida
torFactory::class,
],
],
];
Thisapproachworksbestwiththespecificationform;otherwiseyouneedtopullthevariouspluginmanagersfromthecomposedfactoryandpassthemtotheindividualinputs:
$transId=newInput();
$transId->getValidatorChain()
->setValidatorManager($this->getFactory()->getValidatorManager());
$transId->getValidatorChain()
->attach(TransactionIdValidator::class);
Validatedatausingzend-inputfilter
139
Specification-driveninputfiltersFinally,wecanlookatspecification-driveninputfilters.
ThecomponentprovidesanInputFilterAbstractServiceFactory.WhenyourequestaninputfilterorinputthatisnotdirectlyintheInputFilterPluginManager,thisabstractfactorywillthenchecktoseeifacorrespondingvalueispresentintheinput_filter_specsconfigurationarray.Ifso,itwillpassthatspecificationtoaZend\InputFilter\Factoryconfiguredwiththevariouspluginmanagersinordertocreatetheinstance.
Usingouroriginalexample,wecoulddefinetheregistrationforminputfilterasfollows:
return[
'input_filter_specs'=>[
'registration_form'=>[
[
'name'=>'email',
'filters'=>[
['name'=>'StringTrim']
],
'validators'=>[
['name'=>'EmailAddress']
],
],
[
'name'=>'user_name',
'required'=>false,
'filters'=>[
['name'=>'StringTrim']
],
],
[
'name'=>'password',
'validators'=>[
[
'name'=>'StringLength',
'options'=>['min'=>8],
'break_chain_on_failure'=>true,
],
['name'=>'Regex','options'=>['pattern'=>'/[a-z]/'],
['name'=>'Regex','options'=>['pattern'=>'/[A-Z]/'],
['name'=>'Regex','options'=>['pattern'=>'/[0-9]/'],
['name'=>'Regex','options'=>['pattern'=>'/[.!@#$%^&*;:]/'],
],
],
],
],
];
Validatedatausingzend-inputfilter
140
Wewouldthenretrieveitfromtheinputfilterpluginmanager:
$inputFilter=$inputFilters->get('registration_form');
Consideringmostinputfiltersdonotneedtocomposedependenciesotherthantheinputsandinputfilterstheyaggregate,thisapproachmakesforadynamicwaytodefineinputvalidation.
Topicsnotcoveredzend-inputfilterhasatonofotherfeaturesaswell:
Inputandinputfiltermerging.Handlingofarrayvalues.Collections(repeateddatasetsofthesamestructure).Filteringoffileuploads.
Ontopofallthis,itprovidesanumberofinterfacesagainstwhichyoucanprograminordertowritecompletelycustomfunctionality!
Onehugestrengthofzend-inputfilteristhatitcanbeusedforanysortofdatasetyouneedtovalidate:forms,obviously,butalsoAPIpayloads,dataretrievedfromamessagequeue,andmore.
Footnotes
.https://docs.zendframework.com/zend-inputfilter↩1
Validatedatausingzend-inputfilter
141
End-to-endencryptionwithZendFramework3byEnricoZimuel
zend-crypt 3.1.0includesahybridcryptosystem ,afeaturethatcanbeusedtoimplementanend-to-endencryption schemainPHP.
Ahybridcryptosystemisacryptographicmechanismthatusessymmetricencyption(e.g.AES )toencryptamessage,andpublic-keycryptography(e.g.RSA )toprotecttheencryptionkey.Thismethodologyguaranteetwoadvantages:thespeedofasymmetricalgorithmandthesecurityofpublic-keycryptography.
BeforeIpresentthePHPimplementation,let'sexplorethehybridmechanisminmoredetail.Belowisadiagramdemonstratingahybridencryptionschema:
Auser(thesender)wantstosendaprotectedmessagetoanotheruser(thereceiver).He/shegeneratesarandomsessionkey(one-timepad)andusesthiskeywithasymmetricalgorithmtoencryptthemessage(inthefigure,Blockcipherrepresentsanauthenticatedencryption algorithm).Atthesametime,thesenderencryptsthesessionkeyusingthepublickeyofthereceiver.Thisoperationisdoneusingapublic-keyalgorithm,e.g.,RSA.Oncetheencryptionisdone,thesendercansendtheencryptedsessionkeyalongwiththeencryptedmessagetothereceiver.Thereceivercandecryptthesessionkeyusinghis/herprivatekey,andconsequentlydecryptthemessage.
Thisideaofcombiningtogethersymmetricandasymmetric(public-key)encryptioncanbeusedtoimplementend-to-endencryption(E2EE).E2EEisacommunicationsystemthatencryptsmessagesexchangedbytwouserswiththepropertythatonlythetwouserscandecryptthemessage.End-to-endencryptionhasbecomequitepopularinthelastyearsinsoftware,andparticularlymessagingsystems,suchasWhatsApp .Moregenerally,whenyouhavesoftwareusedbymanyusers,end-to-endencryptioncanbeusedtoprotectinformationexchangedbyusers.Onlytheuserscanaccess(decrypt)exchangedinformation;eventheadministratorofthesystemisnotabletoaccessthisdata.
Buildend-to-endencryptioninPHP
1 23
4 5
6
7
End-to-endencryptionwithZendFramework3
142
Wewanttoimplementend-to-endencryptionforawebapplicationwithuserauthentication.Wewillusezend-crypt3.1.0toimplementourcryptographicschemas.ThiscomponentofZendFrameworkusesPHP'sOpenSSLextension foritscryptographicprimitives.
Thefirststepistocreatepublicandprivatekeysforeachusers.Typically,thisstepcanbedonewhentheusercredentialsarecreated.Togenerarethepairsofkeys,wecanuseZend\Crypt\PublicKey\RsaOptions.Belowisanexampledemonstratinghowtogeneratepublicandprivatekeystostoreinthefilesystem:
useZend\Crypt\PublicKey\RsaOptions;
useZend\Crypt\BlockCipher;
$username='alice';
$password='test';//user'spassword
//Generatepublicandprivatekey
$rsaOptions=newRsaOptions();
$rsaOptions->generateKeys([
'private_key_bits'=>2048
]);
$publicKey=$rsaOptions->getPublicKey()->toString();
$privateKey=$rsaOptions->getPrivateKey()->toString();
//storethepublickeyina.pubfile
file_put_contents($username.'.pub',$publicKey);
//encryptandstoretheprivatekeyinafile
$blockCipher=BlockCipher::factory('openssl',array('algo'=>'aes'));
$blockCipher->setKey($password);
file_put_contents($username,$blockCipher->encrypt($privateKey));
Intheaboveexample,wegeneratedaprivatekeyof2048bits.Ifyouarewonderingwhynot4096bits,thisisquestionableanddependsontherealusecase.Forthemajorityofapplications,2048isstillagoodkeysize,atleastuntil2030.Ifyouwantmoresecurityandyoudon'tcareabouttheadditionalCPUtime,youcanincreasethekeysizeto4096.Isuggestreadingthefollowingblogpostsformoreinformationonkeykeysize:
RSAKeySizes:2048or4096bits?:https://danielpocock.com/rsa-key-sizes-2048-or-4096-bitsTheBigDebate,2048vs.4096,Yubico’sPosition:https://www.yubico.com/2015/02/big-debate-2048-4096-yubicos-stand/HTTPSPerformance,2048-bitvs4096-bit:https://blog.nytsoi.net/2015/11/02/nginx-https-performance
8
End-to-endencryptionwithZendFramework3
143
Intheexampleabove,wedidnotgeneratetheprivatekeyusingapassphrase;thisisbecausetheOpenSSLextensionofPHPdoesnotsupportAEAD(AuthenticatedEncryptwithAssociatedData)modeforciphersyet,whichisrequiredinordertousepassphrases.
ThedefaultpassphraseencryptionalgorithmforOpenSSLisdes-ede3-cbc usingPBKDF2 with2048iterationsforgeneratingtheencryptionkeyfromtheuser'spassword.Evenifthisencryptionalgorithmisquitegood,thenumberofiterationsofPBKDF2isnotoptimal;zend-cryptimprovesonthisinavarietyofways,out-of-the-box.Asdemonstratedabove,IuseZend\Crypt\BlockCiphertoencrypttheprivatekey;thisclassprovidesencrypt-then-authenticate usingtheAES-256algorithmforencryptionandHMAC-SHA-256forauthentication.Moreover,BlockCipherusesthePBKDF2 algorithmtoderivatetheencryptionkeyfromtheuser'skey(password).ThedefaultnumberofiterationsforPBKDF2is5000,andyoucanincreaseitusingtheBlockCipher::setKeyIteration()method.
Intheexample,Istoredthepublicandprivatekeysintwofilesnamed,respectively,$username.puband$username.Becausetheprivatefileisencrypted,usingtheuser'spassword,itcanbeaccessonlybytheuser.Thisisaveryimportantaspectforthesecurityoftheentiresystem(wetakeforgrantedthatthewebapplicationstoresthehashesoftheuser'spasswordsusingasecurealgorithmsuchasbcrypt ).
Oncewehavethepublicandprivatekeysfortheusers,wecanstartusingthehybridcryptosystemprovidedbyzend-crypt.Forinstance,imagineAlicewantstosendanencryptedmessagetoBob:
1011
1211
13
End-to-endencryptionwithZendFramework3
144
useZend\Crypt\Hybrid;
useZend\Crypt\BlockCipher;
$sender='alice';
$receiver='bob';
$password='test';//bob'spassword
$msg=sprintf('Asecretmessagefrom%s!',$sender);
//encryptthemessageusingthepublickeyofthereceiver
$publicKey=file_get_contents($receiver.'.pub');
$hybrid=newHybrid();
$ciphertext=$hybrid->encrypt($msg,$publicKey);
//sendtheciphertexttothereceiver
//decrypttheprivatekeyofbob
$blockCipher=BlockCipher::factory('openssl',['algo'=>'aes']);
$blockCipher->setKey($password);
$privateKey=$blockCipher->decrypt(file_get_contents($receiver));
$plaintext=$hybrid->decrypt($ciphertext,$privateKey);
printf("%s\n",$msg===$plaintext?"Themessageis:$msg":'Error!');
Theaboveexampledemonstratesencryptinginformationbetweentwousers.Ofcourse,inthiscase,thesender(Alice)knowsthemessagebecauseshewroteit.Moreingeneral,ifweneedtostoreasecretbetweenmultipleusers,weneedtospecifythepublickeystobeusedforencryption.
Thehybridcomponentofzend-cryptsupportsencryptingmessagesformultiplerecipients.Todoso,passanarrayofpublickeysinthe$publicKeyparameterofZend\Crypt\Hybrid::encrypt($data,$publicKey).
Belowdemonstratesencryptingafilefortwousers,AliceandBob.
End-to-endencryptionwithZendFramework3
145
useZend\Crypt\Hybrid;
useZend\Crypt\BlockCipher;
$data=file_get_contents('path/to/file/to/protect');
$pubKeys=[
'alice'=>file_get_contents('alice.pub'),
'bob'=>file_get_contents('bob.pub')
];
$hybrid=newHybrid();
//Encryptusingthepublickeysofbothaliceandbob
$ciphertext=$hybrid->encrypt($data,$pubKeys);
file_put_contents('file.enc',$ciphertext);
$blockCipher=BlockCipher::factory('openssl',['algo'=>'aes']);
$passwords=[
'alice'=>'passwordofAlice',
'bob'=>'passwordofBob'
];
//decryptusingtheprivatekeysofaliceandbob,oneattime
foreach($passwordsas$id=>$pass){
$blockCipher->setKey($pass);
$privateKey=$blockCipher->decrypt(file_get_contents($id));
$plaintext=$hybrid->decrypt($ciphertext,$privateKey,null,$id);
printf("%sfor%s\n",$data===$plaintext?'Decryptionok':'Error',$id);
}
Fordecryption,Iusedahardcodedpasswordfortheusers.Usually,theuser'spasswordisprovidedduringtheloginprocessofawebapplicationandshouldnotbestoredaspermanentdata;forinstance,theuser'spasswordcanbesavedinaPHPsessionvariablefortemporaryusage.Ifyouusesessionstosavetheuser'spassword,ensurethatdataisprotected;thePHP-Secure-Session libraryortheSuhosin PHPextensionwillhelpyoudoso.
Todecryptthefile,IusedtheZend\Crypt\Hybrid::decrypt()method,whereIspecifiedthe$privateKey,anullpassphrase,andfinallythe$idoftheprivateKey.Thisparametersarenecessarytofindthecorrectkeytouseintheheaderoftheencryptedmessage.
Footnotes
.https://github.com/zendframework/zend-crypt↩
.https://docs.zendframework.com/zend-crypt/hybrid/↩
14 15
1
2
3
End-to-endencryptionwithZendFramework3
146
.https://en.wikipedia.org/wiki/End-to-end_encryption↩
.https://en.wikipedia.org/wiki/Advanced_Encryption_Standard↩
.https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29↩
.https://en.wikipedia.org/wiki/Authenticated_encryption↩
.https://www.whatsapp.com/faq/en/general/28030015↩
.http://php.net/manual/en/book.openssl.php↩
.https://wiki.php.net/rfc/openssl_aead↩
.https://en.wikipedia.org/wiki/Triple_DES↩
.https://en.wikipedia.org/wiki/PBKDF2↩
.http://www.daemonology.net/blog/2009-06-24-encrypt-then-mac.html↩
.https://en.wikipedia.org/wiki/Bcrypt↩
.https://github.com/ezimuel/PHP-Secure-Session↩
.https://suhosin.org↩
3
4
5
6
7
8
9
10
11
12
13
14
15
End-to-endencryptionwithZendFramework3
147
CreateZPKstheEasyWaybyEnricoZimuel
ZendServer providestheabilitytodeployapplicationstoasingleserverorclusterofserversviatheZPK packageformat.Weofferthepackagezfcampus/zf-deploy forcreatingZPKpackagesfromZendFrameworkandApigilityapplications,buthowcanyoucreatetheseforExpressive,or,really,anyPHPapplication?
RequirementsTocreatetheZPK,youneedafewthings:
Thezipbinary.ZPKsareZIPfileswithspecificartifacts.Thecomposerbinary,soyoucaninstalldependencies.An.htaccessfile,ifyourZendServerinstallationisusingApache.Adeployment.xmlfile.
htaccessIfyouareusingApache,you'llwanttomakesurethatyousetupthingslikerewriterulesforyourapplication.WhilethiscanbedonewhendefiningthevhostintheZendServeradminUI,usingan.htaccessfilemakesiteasiertomakechangestotherulesbetweendeployments.
Thefollowing.htaccessfilewillworkformany(most?)PHPprojects.Placeitrelativetoyourproject'sfrontcontrollerscript;inthecaseofExpressive,ZendFramework,andApigility,thatwouldmeanpublic/index.php,andthuspublic/.htaccess:
12 3
CreateZPKstheEasyWay
148
RewriteEngineOn
#ThefollowingruletellsApachethatiftherequestedfilename
#exists,simplyserveit.
RewriteCond%{REQUEST_FILENAME}-s[OR]
RewriteCond%{REQUEST_FILENAME}-l[OR]
RewriteCond%{REQUEST_FILENAME}-d
RewriteRule^.*$-[NC,L]
#Thefollowingrewritesallotherqueriestoindex.php.The
#conditionensuresthatifyouareusingApachealiasestodo
#massvirtualhosting,thebasepathwillbeprependedto
#allowproperresolutionoftheindex.phpfile;itwillwork
#innon-aliasedenvironmentsaswell,providingasafe,one-size
#fitsallsolution.
RewriteCond%{REQUEST_URI}::$1^(/.+)(.+)::\2$
RewriteRule^(.*)-[E=BASE:%1]
RewriteRule^(.*)$%{ENV:BASE}index.php[NC,L]
deployment.xmlThedeployment.xmltellsZendServerabouttheapplicationyouaredeploying.WhatislistedbelowwillworkforExpressive,ZendFramework,andApigilityapplications,andlikelyanumberofotherPHPapplications.Themainthingstopayattentiontoare:
Thenameshouldtypicallymatchtheapplicationnameyou'vesetupinZendServer.Theversion.releasevalueshouldbeupdatedforeachrelease;thisallowsyoutouserollbackfeatures.Theappdirvalueistheprojectroot.Anemptyvalueindicatesthesamedirectoryasthedeployment.xmllivesin.Thedocrootvalueisthedirectoryfromwhichthevhostwillservefiles.
So,asanexample:
<?xmlversion="1.0"encoding="utf-8"?>
<packageversion="2.0"xmlns="http://www.zend.com/server/deployment-descriptor/1.0">
<type>application</type>
<name>API</name>
<summary>APIforallthethings!</summary>
<version>
<release>1.0</release>
</version>
<appdir></appdir>
<docroot>public</docroot>
</package>
CreateZPKstheEasyWay
149
InstallingdependenciesWhenyou'rereadytobuildapackage,youshouldinstallyourdependencies.However,don'tinstallthemanyoldway;installtheminaproduction-readyway.Thismeans:
Specifyingthatcomposeroptimizetheautoloader(--optimize-autoloader).Useproductiondependenciesonly(--no-dev).Preferdistributionpackages(versussourceinstalls)(--prefer-dist).
So:
$composerinstall--no-dev--prefer-dist--optimize-autoloader
CreatetheZPKFinally,wecannowcreatetheZPK,usingthezipcommand:
$zip-rapi-1.0.0.zpk.-xapi-1.0.0.zpk-x'*.git/*'
Thiscreatesthefileapi-1.0.0.zpkwithallcontentsofthecurrentdirectoryminusthe.gitdirectoryandtheZPKitself(theseareexcludedviathe-xflags).(Youmaywant/needtospecifyadditionalexclusions;theabovearetypical,however.)
YoucanthenuploadtheZPKtothewebinterface,orusetheZendServerSDK .
Simpleexample:single-directoryPoCLet'ssayyouwanttodoaproof-of-concept,andwillbecreatinganindex.phpintheprojectroottotestoutanidea.Youwouldusetheabove.htaccess,butkeepitintheprojectroot.Yourdeployment.xmlwouldlookthesame,exceptthatthedocrootvaluewouldbeempty:
4
CreateZPKstheEasyWay
150
<?xmlversion="1.0"encoding="utf-8"?>
<packageversion="2.0"xmlns="http://www.zend.com/server/deployment-descriptor/1.0">
<type>application</type>
<name>POC</name>
<summary>Proof-of-conceptofaverycoolidea</summary>
<version>
<release>0.1.0</release>
</version>
<appdir></appdir>
<docroot></docroot>
</package>
You'dthenrun:
$zip-rpoc-0.1.0.zpk.-xpoc-0.1.0.zpk
Done!
FinZPKsmakecreatingandstagingdeploymentpackagesfairlyeasy—onceyouknowhowtocreatethepackages.WehopethatthisposthelpsdemystifythefirststepsincreatingaZPKforyourapplication.
VisittheZendServerdocumentation formoreinformationonZPKstructure.
Footnotes
.http://www.zend.com/en/products/zend_server↩
.http://files.zend.com/help/Zend-Server/content/application_package.htm↩
.https://github.com/zfcampus/zf-deploy↩
.https://github.com/zend-patterns/ZendServerSDK↩
.http://files.zend.com/help/Zend-Server/content/understanding_the_application_package_structure.htm↩
5
1
2
3
4
5
CreateZPKstheEasyWay
151
UsingLaravelHomesteadwithZendFrameworkProjectsbyEnricoZimuel
LaravelHomestead isaninterestingprojectbytheLaravelcommunitythatprovidesaVagrant boxforPHPdevelopers.ItincludesafullsetofservicesforPHPdevelopers,suchastheNginxwebserver,PHP7.1,MySQL,Postgres,Redis,Memcached,Node,andmore.
Onethemostinterestingfeaturesofthisprojectistheabilitytoenableitperproject.ThismeansyoucanrunavagrantboxforyourspecificPHPproject.
Inthisarticle,we'llexamineusingitforZendFrameworkMVC,Expressive,andApigilityprojects.Ineachcase,installationandusageisexactlythesame.
InstalltheVagrantboxThefirststepistoinstallthelaravel/homestead vagrantbox.Thisboxworkswithavarietyofproviders:VirtualBox5.1 ,VMWare ,orParallels .
WeusedVirtualBoxandthefollowingcommandtoinstallthelaravel/homesteadbox:
$vagrantboxaddlaravel/homestead
Theboxis981MB,soitwilltakesomeminutestodownload.
Homestead,bydefault,usesthehostnamehomestead.app,andrequiresthatyouupdateyoursystemhostsfiletopointthatdomaintothevirtualmachineIPaddress.Tofaciliatethat,Homesteadprovidesintegrationwiththevagrant-hostsupdater Vagrantplugin.Werecommendinstallingthatbeforeyourinitialrunofthevirtualmachine:
$vagrantplugininstallvagrant-hostsupdater
UseHomesteadinZFprojectsOnceyouhaveinstalledthelaravel/homesteadvagrantbox,youcanuseitgloballyorperproject.
12
34 5 6
7
UsingLaravelHomesteadwithZendFrameworkProjects
152
IfweinstallHomesteadper-project,wewillhaveafulldevelopmentserverconfigureddirectlyinthelocalfolder,withoutsharingserviceswithotherprojects.Thisisabigplus!
TouseHomesteadper-project,weneedtoinstallthelaravel/homestead packagewithinourZendFramework,Apigility,orExpressiveproject.ThiscanbedoneusingComposer withthefollowingcommand:
$composerrequire--devlaravel/homestead
Afterinstallation,executethehomesteadcommandtobuildtheVagrantfile:
$vendor/bin/homesteadmake
ThiscommandcreatesboththeVagrantFileandaHomestead.yamlconfigurationfile.
ConfiguringHomesteadBydefault,thevagrantboxissetupataddress192.168.10.10withthehostnamehomestead.app.YoucanchangetheIPaddressinHomestead.yamlifyouwant,aswellasthehostname(viathesites[].mapkey).
TheHomestead.yamlconfigurationfilecontainsalldetailsaboutthevagrantboxconfiguration.Thefollowingisanexample:
89
UsingLaravelHomesteadwithZendFrameworkProjects
153
---
ip:"192.168.10.10"
memory:2048
cpus:1
hostname:expressive-homestead
name:expressive-homestead
provider:virtualbox
authorize:~/.ssh/id_rsa.pub
keys:
-~/.ssh/id_rsa
folders:
-map:"/home/enrico/expressive-homestead"
to:"/home/vagrant/expressive-homestead"
sites:
-map:homestead.app
to:"/home/vagrant/expressive-homestead/public"
databases:
-homestead
Thisconfigurationfileisverysimpleandintuitive;forinstance,thefolderstobeusedarereportedinthefolderssection;themapvalueisthelocalfolderoftheproject,thetovalueisthefolderonthevirtualmachine.
IfyouwanttoaddorchangemorefeaturesinthevirtualmachineyoucanusedtheHomestead.yamlconfigurationfile.Forinstance,ifyouprefertoaddMariaDBinsteadofMySQL,youneedtoaddthemariadboption:
ip:"192.168.10.10"
memory:2048
cpus:1
hostname:expressive-homestead
name:expressive-homestead
provider:virtualbox
mariadb:true
ThisoptionwillremoveMySQLandinstallMariaDB.
UsingLaravelHomesteadwithZendFrameworkProjects
154
SSHkeysmanagedbyGPG
Oneofourteamusesthegpg-agentasanssh-agent,whichcausedsomeconfigurationproblemsinitially,asthe~/.ssh/id_rsaandits.pubsiblingwerenotpresent.
Whenusinggpg-agentforservingSSHkeys,youcanexportthekeyusingssh-add-L.Thismaylistseveralkeys,butyoushouldbeabletofindthecorrectone.Copyittothefile~/.ssh/gpg_key.pub,andthencopythatfileto~/.ssh/gpg_key.pub.pub.UpdatetheHomestead.yamlfiletoreflectthesenewfiles:
authorize:~/.ssh/gpg_key.pub.pub
keys:
-~/.ssh/gpg_key.pub
Thegpg-agentwilltakecareofsendingtheappropriatekeyfromthere.
RunningHomesteadTorunthevagrantbox,executethefollowingwithinyourprojectroot:
$vagrantup
Ifyouopenabrowsertohttp://homestead.appyoushouldnowseeyourapplicationrunning.
Manuallymanagingyourhostsfile
Ifyouchosenottousevagrant-hostsupdater,youwillneedtoupdateyoursystemhostsfile.
OnLinuxandMac,updatethe/etc/hostsfiletoaddthefollowingline:
192.168.10.10homestead.app
OnWindows,thehostfileislocatedinC:\Windows\System32\drivers\etc\hosts.
Moreinformation
UsingLaravelHomesteadwithZendFrameworkProjects
155
We'vetestedthissetupwitheachoftheZendFrameworkzend-mvcskeletonapplication,Apigility,andExpressive,andfoundthesetup"justworked"!Wefeelitprovidesexcellentflexibilityinsettingupdevelopmentenvironments,givingdevelopersawiderangeoftoolsandtechnologiestoworkwithastheydevelopapplications.
FormoreinformationaboutLaravelHomestead,visittheofficialdocumentation oftheproject.
Footnotes
.https://laravel.com/docs/5.4/homestead↩
.https://www.vagrantup.com/↩
.https://atlas.hashicorp.com/laravel/boxes/homestead↩
.https://www.virtualbox.org/wiki/Downloads↩
.https://www.vmware.com/↩
.http://www.parallels.com/products/desktop/↩
.https://github.com/cogitatio/vagrant-hostsupdater↩
.https://github.com/laravel/homestead↩
.https://getcomposer.org/↩
.https://laravel.com/docs/5.4/homestead↩
10
1
2
3
4
5
6
7
8
9
10
UsingLaravelHomesteadwithZendFrameworkProjects
156
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
157