Tråd bedømmelse:
  • 2 Stemmer - 5 Gennemsnit
  • 1
  • 2
  • 3
  • 4
  • 5
Kom i gang med PDO
27-03-2015, 01:46
#1
Kom i gang med PDO
Nu har vi diskuteret PDO et par gange, så her er lidt om hvorfor du bør bruge PDO og hvordan du gør.

Hvorfor
PDO er populær af mange forskellige grunde, jeg vil kun nævne det mest grundlæggende, men det er ofte også grund nok til at skifte.

Når du bruger mysql/mysqli i PHP, ser din kode typisk sådan her ud:
$db = mysqli_connect($host, $user, $pass, $dbname);
$var = mysqli_escape_string($db, $var);
$result = mysqli_query($db, "SEL... WHERE id = $var");
Her er der to fundamentale problemer.
1. PHP er ikke typefast. Hvis PHP modtager noget der skaber forvirring med hensyn til typen, kan du få underlige resultater når det bliver escapet.
2. Hvad sker der hvis du glemmer at bruge mysqli_escape_string()? Dit input bliver ikke escapet og pludselig står du med en fatal fejl, som vi alle kender ved navnet SQL injection. Det er nøjagtigt dette der skete for FaceUp.
PDO og MySQL løser disse problemer ved hjælp af prepared statements. Prepared statements er udtryk der sendes til databasen før de eksekveres. Det giver en lille bonus på hastighedsfronten, da databasen kan forberede sig på din query før du kører den. Ydermere kræver dette at du bruger placeholders i stedet for de værdier du vil have i din query. Koden ovenfor i PDO ville se nogenlunde sådan her ud:
$db = new PDO($dsn, $user, $pass);
$st = $db->prepare("SEL... WHERE id = :id");
$st->bindParam('id', $id, PDO::PARAM_INT);
$st->execute();
Her laver vi først et prepared statement, men i stedet for at sætte variablen ind bruger vi en placeholder med en label (indikeret med et kolon). Herefter kan vi binde en variabel og gøre PDO opmærksom på, at den skal escapes som en integer. Når vi gør dette, tvinges vi til at sende vores data igennem bindParam, som automatisk sanitizer dens indhold før det sendes videre til databasen. Hvis du glemmer linien, kommer værdien slet ikke med og din query fejler. Smart, ikke? Det skal dog siges at mysqli også kan dette, men PDO er lidt mere elegant når den gør det. Se evt. dokumentationen:
http://php.net/manual/en/mysqli.quicksta...ements.php

En anden fordel ved PDO er fleksibilitet. Som du nok har bemærket ovenfor, angiver jeg ikke host, port eller databasenavn når jeg forbinder vha. PDO. I stedet bruger man et DSN (Data Source Name), der ser nogenlunde ud som flg.
mysql:host=localhost;port=3306;dbname=mindatabase;charset=utf8
Hvorfor er det så smart? Det er det fordi vi ikke længere snakker om MySQL databaser, men blot data-kilder generelt. Hvis du pludselig finder ud af, at PostgreSQL ville være bedre til dit formål, behøver du ikke længere at erstatte alle kald til mysqli_* med postgre_* (Hvis de da fandtes). I stedet ændrer du mysql: til postgre: i din DSN, og så kører resten igen. Du skal naturligvis sørge for at alle dine queries stadig er valide, men dette letter arbejdet betydeligt. Desuden fungerer PDO på samme måde for næsten alle database-systemer (OracleDB, MySQL, PGSQL, MSSQL m.fl.), så hvis du kan med MySQL så kan du også med alle andre.

En sidste grund må være opbygningen. PDO er objektorienteret (mysqli ligeså) og klarer det igen på en lidt mere elegant vis end mysqli.

Hvordan
PDO er ikke voldsomt svært at bruge. Der er bare nogle småting angående syntaks.
Forbind til serveren
Det er nemt at forbinde til din server med PDO. Bare instantier klassen som her:
$db = new PDO("mysql:host=localhost;dbname=mindb", "root", "password");
Hvis du er vant til den gamle "mysql_connect() or die(mysql_error())" struktur, så vil jeg benytte lejligheden til at sige at det er en elendig idé. Skaf dig en logger eller brug PHP's egen. Du kan håndtere fejl som her:
try {
$db = new PDO("mysql:host=localhost;dbname=mindb", "root", "password");
} catch (PDOException $ex) {
trigger_error("Kunne ikke forbinde til databasen: " . $ex->getMessage(), E_USER_ERROR);
}

Hent oplysninger fra (eller gem til) databasen
Lad os sige at din bruger har valgt en stol i din webshop, og sender dens ID i en request til databasen. Nu skal alle stolens oplysninger hentes. Du kan gøre dette på mange måder, men disse tre er de mest almindelige:
$st = $db->prepare("SELECT name, price FROM chairs WHERE id = :chairid");
// Queryen køres sådan her
$st->bindParam('chairid', $chairId, PDO::PARAM_INT);
$st->execute();
// Eller sådan her
$st->execute(array(
'chairid' => $chairId
));
Eller din placeholder kan være uden en label:
$st = $db->prepare("SELECT name, price FROM chairs WHERE id = ?");
$st->execute(array($chairId));
Kun den øverste metode lader dig definere typen eksplicit, men de to andre metoder sparer en betydelig mængde kode. Om man vil have typen med er lidt en smagssag, da input gerne skal valideres og/eller saniteres med filter_var alligevel. Det gør dog lidt ekstra for forståelsen når man kan læse typen på den måde, så det kan anbefales i projekter hvor du arbejder sammen med andre.

Vær opmærksom på at placeholders kun er til værdier. Det er ikke muligt at bruge en placeholder i stedet for et kolonne- eller tabel-navn, da de tjener et bredere formål end at hjælpe med escaping.
Hvis du ikke tager imod bruger-input kan du undlade at bruge prepared statements og blot bruge $db->exec("SELECT * FROM table"); men pas godt på at du ikke senere propper en variabel i den.

Når du har kørt din query skal du hente resultatet. Her kan du bruge metoderne fetchAll(), fetch(), fetchColumn(), fetchAssoc() og fetchObject(). Helt grundlæggende kan det siges at fetchAll() henter alle rækker af resultatet, mens fetch() kun henter den næste. fetchColumn lader dig hente et bestemt felt fra resultatet, hvilket kan være meget brugbart hvis du kun skal bruge én værdi. fetchAssoc() og fetchObject() henter resultatet til hhv. et associativt array eller et objekt. De to sidste metoder er blot shorthands for fetch(PDO::FETCH_ASSOC) og fetch(PDO::FETCH_OBJ). Desuden er der en smule metadata, som du nok også kender fra mysqli. rowCount() returnerer antallet af rækker og errorInfo() returnerer oplysninger om fejlen hvis der er opstået en (Mere om fejl senere).
Her er et par små eksempler (vi antager at vi er forbundet):
// Hent en liste over alle stole (rs = resultset)
$rs = $db->exec("SELECT id, name FROM chairs");
$chairs = $rs->fetchAll();

// Hent en bestemt stol
$chairId = filter_var(INPUT_GET, 'chairid', FILTER_SANITIZE_NUMBER_INT);
$st = $db->prepare("SELECT name, price, description FROM chairs WHERE id = ?");
$st->execute(array($chairId));
$chair = $st->fetch();

// Hent navnet på en bestemt stol
$chairId = filter_var(INPUT_GET, 'chairid', FILTER_SANITIZE_NUMBER_INT);
$st = $db->prepare("SELECT name FROM chairs WHERE id = ?");
$st->execute(array($chairId));
$chairname = $st->fetchColumn();

Nu skulle du gerne være nogenlunde i stand til at bruge PDO i stedet for mysqli. Det er dog vigtigt at vi dækker et sidste emne før du går i gang.

Fejlhåndtering
Der er mange latterlige ting i PHP, fejlhåndtering er en af dem. I PHP findes der 3 slags fejl: Fejl, exceptions og fatale fejl. En uncaught exception genererer en fatal fejl, men fejl gør ikke. En fatal fejl, er en fejl hvor udførsel af scriptet stopper.
Når du bruger PDO har du heldigvis frihed til at vælge hvordan fejl skal rapporteres, da det godt kan afhænge af dit workflow. Hvis du er glad for objektorienteret programmering, er det ofte rart at arbejde med exceptions. Inden du forbinder til din database vælger du blot PDO's "Error mode".
PDO vil altid lægge fejlen ind i det objekt hvor den opstod, så du kan hente den og sende den et sted hen. Her er de error modes du kan vælge imellem:
  • PDO::ERRMODE_SILENT - PDO brokker sig ikke når noget går galt, men du kan hente fejlbeskederne i hhv. $db->errorInfo() og $st->errorInfo()
  • PDO::ERRMODE_WARNING - PDO genererer en PHP warning. Warnings stopper ikke execution, men dukker op i din error log uden at du skal tilføje kode til det. Det er smart når du debugger, men ikke altid godt når din side er live, da dine fejlbeskeder kan misbruges til at exploite din side.
  • PDO::ERRMODE_EXCEPTION - PDO kaster en exception med typen PDOException, som indeholder fejloplysningerne. Dette er smart hvis du godt kan lide try/catch strukturen og kan ofte være en pænere måde at håndtere fejl på.
Af næsten åbenlyse årsager vil jeg blot forklare forskellen på SILENT og EXCEPTION, så her er nogle kolde eksempler på fejl ved oprettelse af forbindelse:
// ERRMODE_SILENT (Er valgt som standard)
$db = new PDO($dsn, $user, $password);
if($db->errorCode()) {
trigger_error("Der opstod en fejl: " . $db->errorInfo());
}

// ERRMODE_EXCEPTION
try {
$db = new PDO($dsn, $user, $password);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $ex) {
// Håndtér din fejl med en log-besked eller e-mail
trigger_error("Der opstod en fejl: " . $ex->getMessage());
}

Helt kort er forskellen at en exception stopper execution hvis den ikke bliver fanget og melder fejlen med backtrace i din fejllog, sammenlignet med silent, der bare vil fortsætte gennem din code med en masse nullpointers. Exceptions gør det således betydeligt lettere at debugge sin kode.

Jeg håber at denne tråd har opklaret det meste du måtte være i tvivl om. Hvis der er flere spørgsmål, så skal jeg nok se om jeg kan besvare dem og flette dem ind i tråden :)
Mangler du hjælp?
Regler |  E-mail (PGP)
Besøg denne brugers hjemmeside Find alle beskeder fra denne bruger
Citer denne besked i et svar
27-03-2015, 19:02
#2
RE: Kom i gang med PDO
Tusind tak for dit initiativ til at lave en tråd om dette Blue! ;)
Explore the world we must
Time's just to short for the best of us
Before death comes
Find alle beskeder fra denne bruger
Citer denne besked i et svar
27-03-2015, 19:37
#3
RE: Kom i gang med PDO
Ser godt ud, og mere skal der egentlig ikke til.
Har aldrig været den store fan af filter_var, men kan godt forstå du vælger at bruge det her.
Find alle beskeder fra denne bruger
Citer denne besked i et svar
27-03-2015, 20:34
#4
RE: Kom i gang med PDO
(27-03-2015, 19:37)MalcolmXI Skrev: Ser godt ud, og mere skal der egentlig ikke til.
Har aldrig været den store fan af filter_var, men kan godt forstå du vælger at bruge det her.

Jeg kan nu godt lide filter_var, selvom den godt kunne bruge nogle flere filtre at vælge imellem. Jeg synes det ser fornuftigt ud når jeg bruger dem i forbindelse med en simpel REST API, for eksempel. Den kommer jo sammen med filter_has_var (det er der ikke mange der er klar over) som gør den noget mere meningsfuld.
Jeg bruger det bl.a. sådan her:
public function checkMissingParams() {
$goterror = false;
foreach($this->params as $attr) {
if(!filter_has_var($this->method, $attr)) {
$goterror = true;
$this->logger->addDebug("Missing parameter", array($attr));
$this->addError(array(
"status" => 400,
"code" => "PARAM_MISSING",
"title" => "Missing parameter",
"description" => "The parameter $attr is required but none is given."
));
}
}

return $goterror;
}

Jeg synes at filter_var var værd at nævne når nu jeg var i gang med sanitization, og så gør den det åbenlyst for læseren at vi arbejder med brugerspecificeret input :)
Mangler du hjælp?
Regler |  E-mail (PGP)
Besøg denne brugers hjemmeside Find alle beskeder fra denne bruger
Citer denne besked i et svar
« Ældre | Nyere »




User(s) browsing this thread: 1 Gæst(er)