You are on page 1of 21

(Theonlyproper)PDOtutorial

TherearemanytutorialsonPDOalready,butunfortunately,mostofthemfailtoexplaintherealbenefitsof
PDO,orevenpromoteratherbadpractices.Theonlytwoexceptions
arephptherightway.comandhashphp.org,buttheymissalotofimportantinformation.Asaresult,halfof
PDO'sfeaturesremaininobscurityandarealmostneverusedbyPHPdevelopers,who,asaresult,are
constantlytryingtoreinventthewheelwhichalreadyexistsinPDO.
Unlikethose,thistutorialiswrittenbysomeonewhohasusedPDOformanyyears,dugthroughit,and
answeredthousandsquestionsonStackOverflow(thesolegoldPDObadgebearer).Followingthemissionof
thissite,thisarticlewilldisprovevariousdelusionsandbadpractices,whileshowingtherightwayinstead.
Althoughthistutorialisbasedonmysqldriver,theinformation,ingeneral,isapplicableforanydriver
supported.

WhyPDO?
Firstthingsfirst.WhyPDOatall?
PDOisaDatabaseAbstractionLayer.Theabstraction,however,istwofold:oneiswidelyknownbutless
significant,whileanotherisobscurebutofmostimportance.
EveryoneknowsthatPDOoffersunifiedinterfacetoaccessmanydifferentdatabases.Althoughthisfeatureis
magnificentbyitself,itdoesn'tmakeabigdealfortheparticularapplication,whereonlyonedatabase
backendisusedanyway.And,despitesomerumors,itisimpossibletoswitchdatabasebackendsbychanging
asinglelineinPDOconfigduetodifferentSQLflavors(todoso,oneneedstouseanaveragedquery
languagelikeDQL).Thus,fortheaverageLAMPdeveloper,thispointisratherinsignificant,andtohim,PDO
isjustamorecomplicatedversionoffamiliarmysql(i)_query()function.However,itisnot;itismuch,much
more.
PDOabstractsnotonlyadatabaseAPI,butalsobasicoperationsthatotherwisehavetoberepeated
hundredsoftimesineveryapplication,makingyourcodeextremelyWET.Unlikemysqlandmysqli,bothof
whicharelowlevel,bareAPI,notintendedtobeuseddirectly(butonlyasabuildingmaterialforsome
higherlevelabstractionlayer),PDOissuchanabstractionalready.It'sstillincomplete,butatleastusable.
TherealPDObenefitsare:

security(preparedstatementsthatareusable)
usability(manyhelperfunctionstoautomateroutineoperations)
reusability(unifiedAPItoaccessmultitudeofdatabases,fromSQLitetoOracle)

NotethatalthoughPDOisthebestoutofnativedbdrivers,foramodernwebapplicationconsidertousean
ORMwithaQueryBuilder,oranyotherhigherlevelabstractionlibrary,withonlyoccasionalfallbackto
vanillaPDO.GoodORMsareEloquent,RedBean,andYii:AR.Aura.SQLisagoodexampleofPDOwrapper
withmanyadditionalfeatures.
Eitherway,it'sgoodtoknowthebasictoolsfirst.So,let'sbegin:

Connecting.DSN
PDOhasafancyconnectionmethodcalledDSN.It'snothingcomplicatedthoughinsteadofoneplainand
simplelistofoptions,PDOasksyoutoinputdifferentconfigurationdirectivesinthreedifferentplaces:

databasedriver,host,db(schema)nameandcharset,aswellaslessfrequently

usedportandunix_socketgointoDSN;
usernameandpasswordgotoconstructor;
allotheroptionsgointooptionsarray.

whereDSNisasemicolondelimitedstring,consistsofparam=valuepairs,thatbeginsfromthedrivername
andacolon:
mysql:host=localhost;dbname=test;port=3306;charset=utf8
driver^^colon^param=valuepair^semicolon

Notethatit'simportanttofollowtheproperformatnospacesorquotesorotherdecorationshavetobe
usedinDSN,butonlyparameters,valuesanddelimiters,asshowninthemanual.
Heregoesanexampleformysql:
$host='127.0.0.1';
$db='test';
$user='root';
$pass='';
$charset='utf8';

$dsn="mysql:host=$host;dbname=$db;charset=$charset";
$opt=[
PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES=>false,
];
$pdo=newPDO($dsn,$user,$pass,$opt);

Withallaforementionedvariablesproperlyset,wewillhaveproperPDOinstancein$pdovariable.
Importantnotesforthelatemysqlextensionusers:
1. Unlikeoldmysql_*functions,whichcanbeusedanywhereinthecode,PDOinstanceisstoredina
regularvariable,whichmeansitcanbeinaccessibleinsidefunctionsso,onehastomakeit

accessible,bymeansofpassingitviafunctionparametersorusingmoreadvancedtechniques,such
asIoCcontainer.
2. Theconnectionhastobemadeonlyonce!Noconnectsineveryfunction.Noconnectsineveryclass
constructor.Otherwise,multipleconnectionswillbecreated,whichwilleventuallykillyourdatabase
server.Thus,asolePDOinstancehastobecreatedandthenusedthroughwholescriptexecution.
3. ItisveryimportanttosetcharsetthroughDSNthat'stheonlyproperway.Forgetabout
runningSETNAMESquerymanually,eitherviaquery()orPDO::MYSQL_ATTR_INIT_COMMAND.Onlyif
yourPHPversionisunacceptablyoutdated(namelybelow5.3.6),doyouhavetouseSET
NAMESqueryandalwaysturnemulationmodeoff.

Errorhandling.Exceptions
AlthoughthereareseveralerrorhandlingmodesinPDO,theonlyproperoneisPDO::ERRMODE_EXCEPTION.So,
oneoughttoalwayssetitthisway,eitherbyaddingthislineaftercreationofPDOinstance,
$dbh>setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);

orasaconnectionoption,asdemonstratedintheexampleabove.Andthisisallyouneedforthebasicerror
reporting.

CatchingPDOexceptions
TL;DR:
Despitewhatallothertutorialssay,youdon'tneedatry..catchoperatortoreportPDOerrors.Catchan
exceptiononlyifyouhaveahandlingscenariootherthanjustreportingit.Otherwisejustletitbubbleuptoa
sitewidehandler(notethatyoudon'thavetowriteone,thereisabasicbuiltinhandlerinPHP,whichis
quitegood).
Alongrantonthematter:
Despiteawidespreaddelusion,youshouldnevercatcherrorstoreportthem.Amodule(likeadatabase
layer)shouldnotreportitserrors.Thisfunctionhastobedelegatedtoanapplicationwidehandler.Allwe
needistoraiseanerror(intheformofexception)whichwealreadydid.That'sall.Norshouldyou"always
wrapyourPDOoperationsinatry/catch"likethemostpopulartutorialfromtutsplusrecommends.Quite
contrary,catchinganexceptionshouldberatheranexceptionalcase(punintended).
Infact,thereisnothingspecialinPDOexceptionstheyareerrorsallthesame.Thus,youhavetotreatthem
exactlythesamewayasothererrors.Ifyouhadanerrorhandlerbefore,youshouldn'tcreateadedicated
oneforPDO.Ifyoudidn'tcareit'sallrighttoo,asPHPisgoodwithbasicerrorhandlingandwillconduct
PDOexceptionsallright.
ExceptionhandlingisoneoftheproblemswithPDOtutorials.Beingacquaintedwithexceptionsforthefirst
timewhenstartingwithPDO,authorsconsiderexceptionsdedicatedtothislibrary,andstartdiligently(but
improperly)handlingexceptionsforPDOonly.Thisisutternonsense.Ifonepaidnospecialattentiontoany

exceptionsbefore,theyshouldn'thavechangedtheirhabitforPDO.Ifonedidn'tusetry..catchbefore,they
shouldkeepwiththat,eventuallylearninghowtouseexceptionsandwhenitissuitabletocatchthem.
SonowyoucantellthatthePHPmanualiswrong,statingthat

IfyourapplicationdoesnotcatchtheexceptionthrownfromthePDOconstructor,thedefault
actiontakenbythezendengineistoterminatethescriptanddisplayabacktrace.Thisbacktrace
willlikelyrevealthefulldatabaseconnectiondetails,includingtheusernameandpassword.
However,thereisnosuchthingas"thedisplayingofabacktrace"!Whatzendenginereallydoesisjust
convertanuncaughtexceptionintoafatalerror.Andthenthisfatalerroristreatedlikeanyothererrorso
itwillbedisplayedonlyifappropriatephp.inidirectiveisset.Thus,althoughyoumayoryoumaynotcatchan
exception,ithasabsolutelynothingtodowithdisplayingsensitiveinformation,becauseit'satotally
differentconfigurationsettinginresponsetothis.So,donotcatchPDOexceptionstoreportthem.Instead,
configureyourserverproperly:
Onadevelopmentserverjustturndisplayingerrorson:
ini_set('display_errors',1);

Whileonaproductionserverturndisplayingerrorsoffwhileloggingerrorson:
ini_set('display_errors',0);
ini_set('log_errors',1);

keepinmindthatthereareothererrorsthatshouldn'tberevealedtotheuseraswell.

YoumaywanttocatchPDOerrorsonlyintwocases:
1. IfyouarewritingawrapperforPDO,andyouwanttoaugmenttheerrorinfowithsomeadditional
data,likequerystring.Inthiscase,catchtheexception,gathertherequiredinformation,andre
throwanotherException.
2. Ifyouhaveacertainscenarioforhandlingerrorsintheparticularpartofcode.Someexamplesare:
o iftheerrorcanbebypassed,youcanusetry..catchforthis.However,donotmakeitahabit.
Emptycatchineveryaspectworksaserrorsuppressionoperator,andsoequallyevilitis.
o ifthereisanactionthathastobetakenincaseoffailure,i.e.transactionrollback.
o ifyouarewaitingforaparticularerrortohandle.Inthiscase,catchtheexception,seeifthe
errorisoneyou'relookingfor,andthenhandlethisone.Otherwisejustthrowitagainsoit
willbubbleuptothehandlerintheusualway.
E.g.:
try{
$pdo>prepare("INSERTINTOusersVALUES(NULL,?,?,?,?)")>execute($data);
}catch(PDOException$e){

if($e>getCode()==1062){
//Takesomeactionifthereisakeyconstraintviolation,i.e.duplicatename
}else{
throw$e;
}
}

However,ingeneral,nodedicatedtreatmentforPDOexceptionsiseverneeded.Inshort,tohavePDOerrors
properlyreported:
1. SetPDOinexceptionmode.
2. Donotusetry..catchtoreporterrors.
3. ConfigurePHPforpropererrorreporting
o onalivesitesetdisplay_errors=offandlog_errors=on
o onadevelopmentsite,youmaywanttosetdisplay_errors=on
o ofcourse,error_reportinghastobesettoE_ALLinbothcases
Asaresult,youwillbealwaysnotifiedofalldatabaseerrorswithoutasinglelineofextracode!Further
reading.

Runningqueries.PDO::query()
TherearetwowaystorunaqueryinPDO.Ifnovariablesaregoingtobeusedinthequery,youcanuse
thePDO::query()method.ItwillrunyourqueryandreturnspecialobjectofPDOStatementclasswhichcanbe
roughlycomparedtoaresource,returnedbymysql_query(),especiallyinthewayyoucangetactualrowsout
ofit:
$stmt=$pdo>query('SELECTnameFROMusers');
while($row=$stmt>fetch())
{
echo$row['name']."\n";
}

Also,thequery()methodallowsustouseaneatmethodchainingforSELECTqueries,whichwillbeshown
below.

Preparedstatements.ProtectionfromSQLinjections
Thisisthemainandtheonlyimportantreasonwhyyouweredeprivedfromyour
belovedmysql_query()functionandthrownintotheharshworldofDataObjects:PDOhasprepared
statementssupportoutofthebox.Preparedstatementistheonlyproperwaytorunaquery,ifanyvariable
isgoingtobeusedinit.ThereasonwhyitissoimportantisexplainedindetailinTheHitchhiker'sGuideto
SQLInjectionprevention.

So,foreveryqueryyourun,ifatleastonevariableisgoingtobeused,youhavetosubstituteitwith
aplaceholder,thenprepareyourquery,andthenexecuteit,passingvariablesseparately.
Longstoryshort,itisnotashardasitseems.Inmostcases,youneedonlytwofunctions
prepare()andexecute().
Firstofall,youhavetoalteryourquery,addingplaceholdersinplaceofvariables.Say,acodelikethis
$sql="SELECT*FROMusersWHEREemail='$email'ANDstatus='$status'";

willbecome
$sql='SELECT*FROMusersWHEREemail=?ANDstatus=?';

or
$sql='SELECT*FROMusersWHEREemail=:emailANDstatus=:status';

NotethatPDOsupportspositional(?)andnamed(:email)placeholders.Alsonotethatnoquoteshavetobe
everusedaroundplaceholders.
Havingaquerywithplaceholders,youhavetoprepareit,usingthePDO::prepare()method.Thisfunctionwill
returnthesamePDOStatementobjectweweretalkingaboutabove,butwithoutanydataattachedtoit.
Finally,togetthequeryexecuted,youmustrunexecute()methodofthisobject,passingvariablesinit,inthe
formofarray.Andafterthat,youwillbeabletogettheresultingdataoutofstatement(ifapplicable):
$stmt=$pdo>prepare('SELECT*FROMusersWHEREemail=?ANDstatus=?');
$stmt>execute([$email,$status]);
$user=$stmt>fetch();
//or
$stmt=$pdo>prepare('SELECT*FROMusersWHEREemail=:emailANDstatus=:status');
$stmt>execute(['email'=>$email,'status'=>$status]);
$user=$stmt>fetch();

Asyoucansee,forthepositionalplaceholders,youhavetosupplyaregulararraywithvalues,whileforthe
namedplaceholders,ithastobeanassociativearray,wherekeyshavetomatchtheplaceholdernamesin
thequery.Youcannotmixpositionalandnamedplaceholdersinthesamequery.
Pleasenotethatpositionalplaceholdersletyouwriteshortercode,butaresensitivetotheorderof
arguments(whichhavetobeexactlythesameastheorderofthecorrespondingplaceholdersinthequery).
Whilenamedplaceholdersmakeyourcodemoreverbose,theyallowrandombindingorder.
Alsonotethatdespiteawidespreaddelusion,no":"inthekeysisrequired.
Aftertheexecutionyoumaystartgettingyourdata,usingallsupportedmethods,asdescribeddowninthis
article.

Bindingmethods
Passingdataintoexecute()shouldbeconsidereddefaultandmostconvenientmethod.Whenthismethodis
used,allvalueswillbeboundasstrings(saveforNULLvalues,thatwillbesenttothequeryasis,i.e.as
SQLNULL),butmostoftimeit'sallrightandwon'tcauseanyproblem.E.g.
However,sometimesit'sbettertosetthedatatypeexplicitly.Possiblecasesare:

LIMITclauseinemulationmodeoranyotherSQLclausethatjustcannotacceptastringoperand.
complexquerieswithnontrivialqueryplanthatcanbeaffectedbyawrongoperandtype
peculiarcolumntypes,likeBIGINTorBOOLEANthatrequireanoperandofexacttypetobebound
(notethatinordertobindaBIGINTvaluewithPDO::PARAM_INTyouneedamysqlndbased
installation).

Insuchacaseexplicitbindinghavetobeused,forwhichyouhaveachoiceoftwo
functions,bindValue()andbindParam().Theformeronehavetobepreferred,because,unlikebindParam()it
hasnosideeffectstodealwith.

Querypartsyoucanbind
Itisveryimportanttounderstandwhichquerypartsyoucanbindusingpreparedstatementsandwhichyou
cannot.Infact,thelistisoverwhelminglyshort:onlystringandnumericliteralscanbebound.Soyoucantell
thataslongasyourdatacanberepresentedinthequeryasanumericoraquotedstringliteralitcanbe
bound.ForallothercasesyoucannotusePDOpreparedstatementsatall:neitheranidentifier,oracomma
separatedlist,orapartofaquotedstringliteralorwhateverelsearbitraryquerypartcannotbeboundusing
apreparedstatement.
Workaroundsforthemostfrequentusecasescanbefoundinthecorrespondingpartofthearticle

Preparedstatements.Multipleexecution
Sometimesyoucanusepreparedstatementsforthemultipleexecutionofapreparedquery.Itisslightly
fasterthanperformingthesamequeryagainandagain,asitdoesqueryparsingonlyonce.Thisfeature
wouldhavebeenmoreusefulifitwaspossibletoexecuteastatementpreparedinanotherPHPinstance.But
alasitisnot.So,youarelimitedtorepeatingthesamequeryonlywithinthesameinstance,whichisseldom
neededinregularPHPscriptsandwhichislimitingtheuseofthisfeaturetorepeatedinsertsorupdates:
$data=[
1=>1000,
5=>300,
9=>200,
];
$stmt=$pdo>prepare('UPDATEusersSETbonus=bonus+?WHEREid=?');
foreach($dataas$id=>$bonus)
{

$stmt>execute([$bonus,$id]);
}

Notethatthisfeatureisabitoverrated.Notonlyitisneededtooseldomtotalkabout,buttheperformance
gainisnotthatbigqueryparsingisrealfastthesetimes.

RunningSELECTINSERT,UPDATE,orDELETEstatements
Comeonfolks.Thereisabsolutelynothingspecialinthesequeries.ToPDOtheyallthesame.Itdoesn't
matterwhichqueryyouarerunning.
Justlikeitwasshownabove,whatyouneedistoprepareaquerywithplaceholders,andthenexecuteit,
sendingvariablesseparately.EitherforDELETEandSELECTquerytheprocessisessentiallythesame.Theonly
differenceis(asDMLqueriesdonotreturnanydata),thatyoucanusethemethodchainingandthus
callexecute()rightalongwithprepare():
$sql="UPDATEusersSETname=?WHEREid=?";
$pdo>prepare($sql)>execute([$name,$id]);

However,ifyouwanttogetthenumberofaffectedrows,thecodewillhavetobethesameboresomethree
lines:
$stmt=$pdo>prepare("DELETEFROMgoodsWHEREcategory=?");
$stmt>execute([$cat]);
$deleted=$stmt>fetchColumn();

Gettingdataoutofstatement.foreach()
Themostbasicanddirectwaytogetmultiplerowsfromastatementwouldbeforeach()loop.Thanks
toTraversableinterface,PDOStatementcanbeiteratedoverbyusingforeach()operator:
$stmt=$pdo>query('SELECTnameFROMusers');
foreach($stmtas$row)
{
echo$row['name']."\n";
}

Notethatthismethodismemoryfriendly,asitdoesn'tloadalltheresultingrowsinthememorybutdelivers
themonebyone(thoughkeepinmindthisissue).

Gettingdataoutofstatement.fetch()
Wehaveseenthisfunctionalready,butlet'stakeacloserlook.Itfetchesasinglerowfromdatabase,and
movestheinternalpointerintheresultset,soconsequentcallstothisfunctionwillreturnalltheresulting
rowsonebyone.Whichmakesthismethodaroughanaloguetomysql_fetch_array()butitworksinaslightly
differentway:insteadofmanyseparatefunctions(mysql_fetch_assoc(),mysql_fetch_row(),etc),thereisonly

one,butitsbehaviorcanbechangedbyaparameter.TherearemanyfetchmodesinPDO,andwewill
discussthemlater,butherearefewforstarter:

PDO::FETCH_NUMreturnsenumeratedarray

PDO::FETCH_ASSOCreturnsassociativearray

PDO::FETCH_BOTHbothoftheabove

PDO::FETCH_OBJreturnsobject

PDO::FETCH_LAZYallowsallthree(numericassociativeandobject)methodswithoutmemory

overhead.
Fromtheaboveyoucantellthatthisfunctionhavetobeusedintwocases:
1. Whenonlyonerowisexpectedtogetthatonlyrow.Forexample,
$row=$stmt>fetch(PDO::FETCH_ASSOC);

Willgiveyousinglerowfromthestatement,intheformofassociativearray.
2. Whenweneedtoprocessthereturneddatasomehowbeforeuse.Inthiscaseithavetoberun
throughusualwhileloop,likeoneshownabove.
AnotherusefulmodeisPDO::FETCH_CLASS,whichcancreateanobjectofparticularclass
$news=$pdo>query('SELECT*FROMnews')>fetchAll(PDO::FETCH_CLASS,'News');

willproduceanarrayfilledwithobjectsofNewsclass,settingclasspropertiesfromreturnedvalues.Note
thatinthismode

propertiesaresetbeforeconstructorcall
forallundefinedproperties__setmagicmethodwillbecalled
ifthereisno__setmethodintheclass,thennewpropertywillbecreated
privatepropertieswillbefilledaswell,whichisabitunexpectedbutquitehandy

NotethatdefaultmodeisPDO::FETCH_BOTH,butyoucanchangeit
usingPDO::ATTR_DEFAULT_FETCH_MODEconfigurationoptionasshownintheconnectionexample.Thus,once
set,itcanbeomittedmostofthetime.

Returntypes.
OnlywhenPDOisbuiltuponmysqlndandemulationmodeisoff,thenPDOwillreturnintandfloatvalues
withrespectivetypes.Say,ifwecreateatable
createtabletypetest(stringvarchar(255),`int`int,`float`float,`null`int);
insertintotypetestvalues('foo',1,1.1,NULL);

AndthenqueryitfrommysqlndbasedPDOwithemulationturnedoff,theoutputwillbe
array(4){
["string"]=>string(3)"foo"
["int"]=>int(1)
["float"]=>float(1.1)
["null"]=>NULL
}

Otherwisefamiliarmysql_fetch_array()behaviorwillbefollowedallvaluesreturnedasstringswith
onlyNULLreturnedasNULL.

Gettingdataoutofstatement.fetchColumn()
Aneathelperfunctionthatreturnsvalueofthesingefieldofreturnedrow.Veryhandywhenweare
selectingonlyonefield:
//Gettingthenamebasedonid
$stmt=$pdo>prepare("SELECTnameFROMtableWHEREid=?");
$stmt>execute([$id]);
$name=$stmt>fetchColumn();

//gettingnumberofrowsinthetableutilizingmethodchaining
$count=$pdo>query("SELECTcount(*)FROMtable")>fetchColumn();

Gettingdataoutofstatementindozensdifferentformats.fetchAll()
That'smostinterestingfunction,withmostastonishingfeatures.Mostlythankstoitsexistenceonecancall
PDOawrapper,asthisfunctioncanautomatemanyoperationsotherwiseperformedmanually.
PDOStatement::fetchAll()returnsanarraythatconsistsofalltherowsreturnedbythequery.Fromthisfactwe

canmaketwoconclusions:
1. Thisfunctionshouldnotbeused,ifmanyrowshasbeenselected.Insuchacaseconventionalwhile
loopavetobeused,fetchingrowsonebyoneinsteadofgettingthemallintoarrayatonce."Many"
meansmorethanitissuitabletobeshownontheaveragewebpage.
2. Thisfunctionismostlyusefulinamodernwebapplicationthatneveroutputsdatarightawayduring
fetching,butratherpassesittotemplate.
You'dbeamazed,inhowmanydifferentformatsthisfunctioncanreturndatain(andhowlittleanaverage
PHPuserknowsofthem),allcontrolledbyPDO::FETCH_*variables.Someofthemare:

10

Gettingaplainarray.
Bydefault,thisfunctionwillreturnjustsimpleenumeratedarrayconsistsofallthereturnedrows.Row
formattingconstants,suchasPDO::FETCH_NUM,PDO::FETCH_ASSOC,PDO::FETCH_OBJetccanchangetherow
format.
$data=$pdo>query('SELECTnameFROMusers')>fetchAll();
var_export($data);
/*
array(
0=>array('John'),
1=>array('Mike'),
2=>array('Mary'),
3=>array('Kathy'),
)*/

Gettingacolumn.
Itisoftenveryhandytogetplainonedimensionalarrayrightoutofthequery,ifonlyonecolumnoutof
manyrowsbeingfetched.Hereyougo:
$data=$pdo>query('SELECTnameFROMusers')>fetchAll(PDO::FETCH_COLUMN);
/*array(
0=>'John',
1=>'Mike',
2=>'Mary',
3=>'Kathy',
)*/

Gettingkeyvaluepairs.
Alsoextremelyusefulformat,whenweneedtogetthesamecolumn,butindexednotbynumbersinorder
butbyanotherfield.HeregoesPDO::FETCH_KEY_PAIRconstant:
$data=$pdo>query('SELECTid,nameFROMusers')>fetchAll(PDO::FETCH_KEY_PAIR);
/*array(
104=>'John',
110=>'Mike',
120=>'Mary',
121=>'Kathy',
)*/

Notethatyouhavetoselectonlytwocolumnsforthismode,firstofwhichhavetobeunique.

Gettingrowsindexedbyuniquefield
Sameasabove,butgettingnotonecolumnbutfullrow,yetindexedbyanuniquefield,thanks
toPDO::FETCH_UNIQUEconstant:

11

$data=$pdo>query('SELECT*FROMusers')>fetchAll(PDO::FETCH_UNIQUE);
/*array(
104=>array(
'name'=>'John',
'car'=>'Toyota',
),
110=>array(
'name'=>'Mike',
'car'=>'Ford',
),
120=>array(
'name'=>'Mary',
'car'=>'Mazda',
),
121=>array(
'name'=>'Kathy',
'car'=>'Mazda',
),
)*/

Notethatfirstcolumnselectedhavetobeunique(inthisqueryitisassumedthatfirstcolumnisid,buttobe
surebetterlistitexplicitly).

Gettingrowsgroupedbysomefield
PDO::FETCH_GROUPwillgrouprowsintoanestedarray,whereindexeswillbeuniquevaluesfromthefirst

columns,andvalueswillbearrayssimilartoonesreturnedbyregularfetchAll().Thefollowingcode,for
example,willseparateboysfromgirlsandputthemintodifferentarrays:
$data=$pdo>query('SELECTsex,name,carFROMusers')>fetchAll(PDO::FETCH_GROUP);
array(
'male'=>array(
0=>array(
'name'=>'John',
'car'=>'Toyota',
),
1=>array(
'name'=>'Mike',
'car'=>'Ford',
),
),
'female'=>array(
0=>array(
'name'=>'Mary',
'car'=>'Mazda',
),
1=>array(
'name'=>'Kathy',
'car'=>'Mazda',
),
),
)

12

So,thisistheidealsolutionforsuchapopulardemandlike"groupeventsbydate"or"groupgoodsby
category".Somereallifeusecases:

Howtomultiplequeryresultsinordertoreducethequerynumber?

Othermodes
Ofcourse,thereisaPDO::FETCH_FUNCforthefunctionalprogrammingfans.
Moremodesarecomingsoon.

GettingrowcountwithPDO
Youdon'tneededit.
AlthoughPDOoffersafunctionforreturningthenumberofrowsfoundbythe
query,PDOstatement::rowCount(),youscarcelyneedit.Really.
Ifyouthinkitover,youwillseethatthisisamostmisusedfunctionintheweb.Mostoftimeitisusednot
tocountanything,butasamereflagjusttoseeiftherewasanydatareturned.Butforsuchacaseyouhave
thedataitself!Justgetyourdata,usingeitherfetch()orfetchAll()anditwillserveassuchaflagallright!Say,
toseeifthereisanyuserwithsuchaname,justselectarow:
$stmt=$pdo>prepare("SELECT1FROMusersWHEREname=?");
$stmt>execute([$name]);
$userExists=$stmt>fetchColumn();

Exactlythesamethingwithgettingeitherasingleroworanarraywithrows:
$data=$pdo>query("SELECT*FROMtable")>fetchAll();
if($data){
//Youhavethedata!NoneedfortherowCount()ever!
}

Rememberthathereyoudon'tneedthecount,theactualnumberofrows,butratherabooleanflag.Soyou
gotit.
Nottomentionthatthesecondmostpopularusecaseforthisfunctionshouldneverbeusedatall.One
shouldneverusetherowCount()tocountrowsindatabase!Instead,onehavetoaskadatabasetocount
them,andreturntheresultinasinglerow:
$count=$pdo>query("SELECTcount(1)FROMt")>fetchColumn();

istheonlyproperway.
Inessence:

ifyouneedtoknowhowmanyrowsinthetable,useSELECTCOUNT(*)query.

13

ifyouneedtoknowwhetheryourqueryreturnedanydatacheckthatdata.
ifyoustillneedtoknowhowmanyrowshasbeenreturnedbysomequery(thoughIhardlycan
imagineacase),thenyoucaneitheruserowCount()orsimplycallcount()onthearrayreturned
byfetchAll()(ifapplicable).

ThusyoucouldtellthatthetopanswerforthisquestiononStackOverflowisessentiallypointlessand
harmfulacalltorowCount()couldbeneversubstitutedwithSELECTcount(*)querytheirpurposeis
essentiallydifferent,whilerunninganextraqueryonlytogetthenumberofrowsreturnedbyotherquery
makesabsolutelynosense.

Affectedrowsandinsertid
PDOisusingthesamefunctionforreturningbothnumberofrowsreturnedbySELECTstatementand
numberofrowsaffectedbyDMLqueriesPDOstatement::rowCount().Thus,togetthenumberofrows
affected,justcallthisfunctionafterperformingaquery.
Anotherfrequentlyaskedquestioniscausedbythefactthatmysqlwon'tupdatetherow,ifnewvalueisthe
sameasoldone.ThusnumberofrowsaffectedcoulddifferfromthenumberofrowsmatchedbytheWHERE
clause.Sometimesitisrequiredtoknowthislatternumber.
AlthoughyoucantellrowCount()toreturnthenumberofrowsmatchedinsteadofrowsaffectedby
settingPDO::MYSQL_ATTR_FOUND_ROWSoptiontoTRUE,but,asthisisaconnectiononlyoptionandthusyou
cannotchangeit'sbehaviorduringruntime,youwillhavetosticktoonlyonemodefortheapplication,which
couldbenotveryconvenient.
Unfortunately,thereisnoPDOcounterpartforthemysql(i)_info()functionwhichoutputcanbeeasilyparsed
anddesirednumberfound.ThisisoneofminorPDOdrawbacks.
Anautogeneratedidentifierfromasequenceorauto_inclementfieldinmysqlcanbeobtainedfrom
thePDO::lastInsertIdfunction.Ananswertoafrequentlyaskedquestion,"whetherthisfunctionissafeto
useinconcurrentenvironment?"ispositive:yes,itissafe.BeingjustaninterfacetoMySQLC
APImysql_insert_id()functionit'sperfectlysafe.

PreparedstatementsandLIKEclause
DespitePDO'soveralleaseofuse,therearesomegotchasanyway,andIamgoingtoexplainsome.
OneofthemisusingplaceholderswithLIKESQLclause.Atfirstonewouldthinkthatsuchaquerywilldo:
$stmt=$pdo>prepare("SELECT*FROMtableWHEREnameLIKE'%?%'");

butsoontheywilllearnthatitwillproduceanerror.Tounderstanditsnatureonehavetounderstand
that,likeitwassaidabove,aplaceholderhavetorepresentacompletedataliteralonlyastringoranumber

14

namely.AndbynomeanscanitrepresenteitherapartofaliteralorsomearbitrarySQLpart.So,when
workingwithLIKE,wehavetoprepareourcompleteliteralfirst,andthensendittothequerytheusualway:
$search="%$search%";
$stmt=$pdo>prepare("SELECT*FROMtableWHEREnameLIKE?");
$stmt>execute([$search]);
$data=$stmt>fetchAll();

PreparedstatementsandINclause
Justlikeitwassaidabove,itisimpossibletosubstituteanarbitraryquerypartwithaplaceholder.Thus,fora
commaseparatedplaceholders,likeforIN()SQLoperator,onemustcreateasetof?smanuallyandputthem
intothequery:
$arr=[1,2,3];
$in=str_repeat('?,',count($arr)1).'?';
$sql="SELECT*FROMtableWHEREcolumnIN($in)";
$stm=$db>prepare($sql);
$stm>execute($arr);
$data=$stm>fetchAll();

Notveryconvenient,butcomparedtomysqliit'samazinglyconcise.

Preparedstatementsandtablenames
OnStackOverflowI'veseenoverwhelmingnumberofPHPusersimplementingthemostfatalPDOcode,
thinkingthatonlydatavalueshavetobeprotected.Butofcourseitisnot.
Unfortunately,PDOhasnoplaceholderforidentifiers(tableandfieldnames),soadevelopermustmanually
formatthem.
Formysqltoformatanidentifier,followthesetworules:

Encloseidentifierinbackticks.
Escapebackticksinsidebydoublingthem.

sothecodewouldbe:
$table="`".str_replace("`","``",$table)."`";

Aftersuchformatting,itissafetoinsertthe$tablevariableintoquery.
Forotherdatabasesruleswillbedifferentbutitisessentialtounderstandthatusingonlydelimitersisnot
enoughdelimitersthemselvesshouldbeescaped.
Itisalsoimportanttoalwayscheckdynamicidentifiersagainstalistofallowedvalues.Hereisabrief
example:

15

$orders=["name","price","qty"];//fieldnames
$key=array_search($_GET['sort'],$orders);//seeifwehavesuchaname
$orderby=$orders[$key];//ifnot,firstonewillbesetautomatically.smartenuf:)
$query="SELECT*FROM`table`ORDERBY$orderby";//valueissafe

Or,extendingthisapproachfortheINSERT/UPDATEstatements(asMysqlsupportsSETforboth),
$data=['name'=>'foo','submit'=>'submit'];//dataforinsert
$allowed=["name","surname","email"];//allowedfields
$values=[];
$set="";
foreach($allowedas$field){
if(isset($data[$field])){
$set.="`".str_replace("`","``",$field)."`"."=:$field,";
$values[$field]=$source[$field];
}
}
$set=substr($set,0,2);

ThiscodewillproducecorrectsequenceforSEToperatorthatwillcontainonlyallowedfieldsand
placeholders:
`name`=:foo

aswellas$valuesarrayforexecute(),whichcanbeusedlikethis
$stmt=$pdo>prepare("INSERTINTOusersSET$set");
$stmt>execute($values);

Yes,itlooksextremelyugly,butthatisallPDOcanoffer.

AproblemwithLIMITclause
AnotherproblemisrelatedtotheSQLLIMITclause.Wheninemulationmode(whichisonbydefault),PDO
substitutesplaceholderswithactualdata,insteadofsendingitseparately.Andwith"lazy"binding(using
arrayinexecute()),PDOtreatseveryparameterasastring.Asaresult,thepreparedLIMIT?,?query
becomesLIMIT'10','10'whichisinvalidsyntaxthatcausesquerytofail.
Therearetwosolutions:
Oneisturningemulationoff(asMySQLcansortallplaceholdersproperly).Todosoonecanrunthiscode:
$conn>setAttribute(PDO::ATTR_EMULATE_PREPARES,false);

Andparameterscanbekeptinexecute():
$conn>setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
$stmt=$pdo>prepare('SELECT*FROMtableLIMIT?,?');

16

$stmt>execute([$offset,$limit]);
$data=$stmt>fetchAll();

Anotherwaywouldbetobindthesevariablesexplicitlywhilesettingtheproperparamtype:
$stmt=$pdo>prepare('SELECT*FROMtableLIMIT?,?');
$stmt>bindParam(1,$offset,PDO::PARAM_INT);
$stmt>bindParam(2,$limit,PDO::PARAM_INT);
$stmt>execute();
$data=$stmt>fetchAll();

OnepeculiarthingaboutPDO::PARAM_INT:forsomereasonitdoesnotenforcethetypecasting.Thus,usingit
onanumberthathasastringtypewillcausetheaforementionederror:
$stmt=$pdo>prepare("SELECT1LIMIT?");
$stmt>bindValue(1,"1",PDO::PARAM_INT);
$stmt>execute();

Butchange"1"intheexampleto1andeverythingwillgosmooth.

CallingstoredproceduresinPDO
Thereisonethingaboutstoredproceduresanyprogrammerstumblesuponatfirst:everystoredprocedure
alwaysreturnsoneextraresultset:one(ormany)resultswithactualdataandonejustempty.Whichmeans
ifyoutrytocallaprocedureandthenproceedtoanotherquery,then"Cannotexecutequerieswhileother
unbufferedqueriesareactive"errorwilloccur,becauseyouhavetoclearthatextraemptyresultfirst.Thus,
aftercallingastoredprocedurethatisintendedtoreturnonlyoneresultset,just
callPDOStatement::nextRowset()once(ofcourseafterfetchingallthereturneddatafromstatement,oritwill
bediscarded):
$stmt=$pdo>prepare("CALLbar()");
$stmt>execute();
$data=$stmt>fetchAll();
$stmt>nextRowset();

Whileforthestoredproceduresreturningmanyresultsetsthebehaviorwillbethesameaswithmultiple
queriesexecution:
$stmt=$pdo>prepare("CALLfoo()");
$stmt>execute();
do{
$data=$stmt>fetchAll();
var_dump($data);
}while($stmt>nextRowset()&&$stmt>columnCount());

However,asyoucanseehereisanothertrickhavetobeused:rememberthatextraresultset?Itisso
essentiallyemptythatevenanattempttofetchfromitwillproduceanerror.So,wecannotusejustwhile

17

($stmt>nextRowset()).Instead,wehavetocheckalsoforemptyresult.Forwhich

purposePDOStatement::columnCount()isjustexcellent.
Thisfeatureisoneofessentialdifferencesbetweenoldmysqlextandmodernlibraries:aftercallingastored
procedurewithmysql_query()therewasnowaytocontinueworkingwiththesameconnection,because
thereisnonextResult()functionformysqlext.Onehadtoclosetheconnectionandthenopenanewoneagain
inordertorunotherqueriesaftercallingastoredprocedure.
CallingastoredprocedureisararecasewherebindParam()useisjustified,asit'stheonlywayto
handleOUTandINOUTparameters.Theexamplecanbefoundinthecorrespondingmanualchapter.
However,formysqlitdoesn'twork.YouhavetoresorttoanSQLvariableandanextracall.

RunningmultiplequerieswithPDO
Wheninemulationmode,PDOcanrunmutiplequeriesinthesamestatement,eitherviaquery()
orprepare()/execute().ToaccesstheresultofconsequentqueriesonehavetousePDOStatement::nextRowset():
$stmt=$pdo>prepare("SELECT?;SELECT?");
$stmt>execute([1,2]);
do{
$data=$stmt>fetchAll();
var_dump($data);
}while($stmt>nextRowset());

Withinthisloopyou'llbeabletogatheralltherelatedinformationfromtheeveryquery,likeaffectedrows,
autogeneratedidorerrorsoccurred.
Itisimportanttounderstandthatatthepointofexecute()PDOwillreporttheerrorforthefirstqueryonly.
Butiferroroccurredatanyofconsequentqueries,togetthaterroronehavetoiterateoverresults.Despite
someignorantopinions,PDOcannotandshouldnotreportalltheerrorsatonce.Somepeoplejustcannot
grasptheproblematwhole,anddon'tunderstandthaterrormessageisnottheonlyoutcomefromthe
query.Therecouldbeadatasetreturned,orsomemetadatalikeinsertid.Togetthese,onehavetoiterate
overresultsets,onebyone.Buttobeabletothrowanerrorimmediately,PDOwouldhavetoiterate
automatically,andthusdiscardsomeresults.Whichwouldbeaclearnonsense.
Unlikemysqli_multi_query()PDOdoesn'tmakeanasynchronouscall,soyoucan't"fireandforget"sendbulk
ofqueriestomysqlandcloseconnection,PHPwillwaituntillastquerygetsexecuted.

Emulationmode.PDO::ATTR_EMULATE_PREPARES
OneofthemostcontroversialPDOconfigurationoptionsisPDO::ATTR_EMULATE_PREPARES.Whatdoesitdo?
PDOcanrunyourqueriesintwoways:
1. Itcanusearealornativepreparedstatement:
Whenprepare()iscalled,yourquerywithplaceholdersgetssenttomysqlasis,withallthequestion

18

marksyouputin(incasenamedplaceholdersareused,theyaresubstitutedwith?saswell),while
actualdatagoeslater,whenexecute()iscalled.
2. Itcanuseemulatedpreparedstatement,whenyourqueryissenttomysqlasproperSQL,withall
thedatainplace,properlyformatted.Inthiscaseonlyoneroundtriptodatabasehappens,
withexecute()call.Forsomedrivers(includingmysql)emulationmodeisturnedONbydefault.
Bothmethodshastheirdrawbacksandadvantagesbut,andIhavetostressonitbothbeingequally
secure,ifusedproperly.DespiteratherappealingtoneofthepopulararticleonStackOverflow,intheendit
saysthatifyouareusingsupportedversionsofPHPandMySQLproperly,youare100%safe.Allyouhave
todoistosetencodingintheDSN,asitshownintheexampleabove,andyouremulatedprepared
statementswillbeassecureasrealones.
Notethatwhennativemodeisused,thedataisneverappearsinthequery,whichisparsedbytheengineas
is,withalltheplaceholdersinplace.Ifyou'relookingintoMysqlquerylogforyourpreparedquery,youhave
tounderstandthatit'sjustanartificialquerythathasbeencreatedsolelyforloggingpurpose,butnotareal
onethathasbeenexecuted.
Otherissueswithemulationmodeasfollows:
WhenemulationmodeisON,onecanuseahandyfeatureofnamedpreparedstatementsaplaceholder
withsamenamecouldbeusedanynumberoftimesinthesamequery,whilecorrespondingvariablehaveto
beboundonlyonce.Forsomeobscurereasonthisfunctionalityisdisabledwhenemulationmodeisoff:
$stmt=$pdo>prepare("SELECT*FROMtWHEREfooLIKE:searchORbarLIKE:search");
$stmt>execute(['search']=>"%$search%");`

Also,whenemulationisON,PDOisabletorunmultiplequeriesinonepreparedstatement.
Also,asnativepreparedstatementssupportonlycertainquerytypes,youcanrunsomequerieswith
preparedstatementsonlywhenemulationisON.Thefollowingcodewillreturntablenamesinemulation
modeanderrorotherwise:
$stmt=$pdo>prepare("SHOWTABLESLIKE?");
$stmt>execute(["%$name%"]);
var_dump($stmt>fetchAll());

Ontheotherhand,whenemulationmodeisOFF,onecouldbothernotwithparametertypes,asmysqlwill
sortallthetypesproperly.Thus,evenstringcanbeboundtoLIMITparameters,asitwasnotedin
thecorrespondingchapter.
It'shardtodecidewhichmodehavetobepreferred,butforusabilitysakeIwouldratherturnitOFF,toavoid
ahasslewithLIMITclause.Otherissuescouldbeconsiderednegligibleincomparison.

19

Mysqlndandbufferedqueries.Hugedatasets.
RecentlyallPHPextensionsthatworkwithmysqldatabasewereupdatedbasedonalowlevellibrary
calledmysqlnd,whichreplacedoldlibmysqlclient.ThussomechangesinthePDObehavior,mostlydescribed
aboveandonethatfollows:
Thereisonethingcalledbufferedqueries.Althoughyouprobablydidn'tnoticeit,youwereusingthemall
theway.Unfortunately,herearebadnewsforyou:unlikeoldPHPversions,whereyouwereusingbuffered
queriesvirtuallyforfree,modernversionsbuiltuponmysqlnddriverwon'tletyoutodothatanymore:

WhenusinglibmysqlclientaslibraryPHP'smemorylimitwon'tcountthememoryusedforresult
setsunlessthedataisfetchedintoPHPvariables.Withmysqlndthememoryaccountedforwill
includethefullresultset.
Thewholethingisaboutaresultset,whichstandsforallthedatafoundbythequery.
WhenyourSELECTquerygetsexecuted,therearetwowaystodelivertheresultsinyourscript:bufferedand
unbufferedone.Whenbufferedmethodisused,allthedatareturnedbythequerygetscopiedinthescript's
memoryatonce.Whileinunbufferedmodeadatabaseserverfeedsthefoundrowsonebyone.
Soyoucantellthatinbufferedmodearesultsetisalwaysburdeningupthememoryontheserverevenif
fetchingweren'tstartedatall.Whichiswhyitisnotadvisabletoselecthugedatasetsifyoudon'tneedall
thedatafromit.
Nonetheless,whenoldlibmysqlbasedclientswereused,thisproblemdidn'tbotherPHPuerstoomuch,
becausethememoryconsumedbytheresultsetdidn'tcountinthethememory_get_usage()andmemory_limit.
Butwithmysqlndthingsgotchanged,andtheresultsetreturnedbythebufferedquerywillbecounttowards
bothmemory_get_usage()andmemory_limit,nomatterwhichwayyouchoosetogettheresult:
$pdo>setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,FALSE);
$stmt=$pdo>query("SELECT*FROMBoard");
$mem=memory_get_usage();
while($row=$stmt>fetch());
echo"Memoryused:".round((memory_get_usage()$mem)/1024/1024,2)."M\n";

$pdo>setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,TRUE);
$stmt=$pdo>query("SELECT*FROMBoard");
$mem=memory_get_usage();
while($row=$stmt>fetch());
echo"Memoryused:".round((memory_get_usage()$mem)/1024/1024,2)."M\n";

willgiveyou(formydata)
Memoryused:0.02M
Memoryused:2.39M

whichmeansthatwithbufferedquerythememoryisconsumedevenifyou'refetchingrowsonebyone!

20

So,keepinmindthatifyouareselectingareallyhugeamountofdata,always
setPDO::MYSQL_ATTR_USE_BUFFERED_QUERYtoFALSE.
Ofcourse,therearesomedrawbacks,twominorones:
1. Withunbufferedqueryyoucan'tuserowCount()method(whichisuseless,aswelearnedabove)
2. Moving(seeking)thecurrentresultsetinternalpointerbackandforth(whichisuselessaswell).
Andaratherimportantone:
1. Whileanunbuffereredqueryisactive,youcannotexecuteanyotherquery.So,usethismodewisely.

21

You might also like