Ebben a leckében az általános bevezető után konkrét környezetet választva, a PHP és MySQL kapcsán nézzük meg az adatbázisok elérésének gyakorlati megvalósulását tipikus feladatokon keresztül.
A lecke végére tudnunk kell szerveroldali környezetünkből az adatbázisokat elérni, és ezt meg is kell tudni valósítani PHP és MySQL komponensek esetén. Implementálni kell tudni tetszőleges lekérdezéseket, módosító utasításokat, tárolt eljárások hívását, tranzakciókezelést, és megfelelő hibavizsgálatnak is szerepelnie kell a kódban.
Az előző leckében láthattuk, hogy mit érdemes tudni a MySQL szerver eléréséről PHP-ban. A következőkben ezt az architektúrát feltételezve néhány olyan tipikus utasításcsoportot tekintünk át, amelyekkel a feladatok nagy része általában elvégezhető.
Láttuk ugyan, hogy a mysqli bővítmény használata a javasolt, azonban egy rövid példa erejéig, ahol ennek van értelme, megmutatjuk a mysql bővítmény használatát is. Tesszük ezt azért, mert mindkét interfészt érdemes ismerni, hiszen a programozói környezetünket nem mindig mi magunk választjuk meg, és néha (elavult) kódot is örökölhetünk másoktól.
A következő tipikus feladatokat mutatjuk be az alábbiakban:
Minden esetben az alábbi lépéseket tesszük meg:
Mindegyik feladattípusnál bemutatjuk a használatukhoz szükséges utasításcsoportokat, PHP függvényeket, majd a példaalkalmazáson keresztül alkalmazzuk azokat. Nem célja az alábbi bemutatásnak az, hogy minden PHP függvényről részletes leírást adjon. Ezt a funkciót a PHP dokumentáció látja el. Sokkal inkább az a célunk, hogy tipikus feladatokra tipikus megoldási sémákat mutassunk be.
Az alábbi példák innen tölthetők:
A következőkben a 2. fejezetben az alkalmazás tervezésekor előkészített oldalakon mutatjuk be, hogyan érhető el a MySQL adatbázis PHP-ból. A példák mindegyike a bejelentkezett felhasználó bemutatóihoz kapcsolódik. A bemutatólistán mutatjuk be, hogy egy select lekérdezést milyen módon tudunk feldolgozni a különböző technológiákkal. Ezt követően további adatbázis-műveleteket mutatunk be a többi oldal kapcsán, így az adatmódosítást (konkrétan az insert-et) az új bemutató létrehozásánál, a tranzakciókezelést a bemutató módosításánál, tárolt eljárások hívását pedig a bemutató törlésénél fogjuk ismertetni.
Első lépésként készítsük elő a generált oldalunkat. Ez azt jelenti, hogy mindenféle PHP kód nélkül készítsünk el egy olyan HTML vázlatot, amely megfelel a végső célnak. Ebben minden olyan tudásunkat felhasználhatjuk, amely kapcsolatos a honlapszerkesztéssel: az ott tanult szabályokat érvényesítsük itt is. Ez magában foglalja a megfelelő dokumentumtípus kiválasztását, valid oldalak létrehozását, metainformációk beállítását, szemantikai tagolását az oldalnak, a CSS-sel történő formázást, amit lehetőleg külön állományban tegyünk. Ezzel a lépéssel biztosítjuk, hogy a generált tartalom a nekünk megfelelő lesz. Gyakran már ebben a lépésben érdemes elgondolkoznunk az oldalelrendezésen (layout). Ezt a lépést már megtettük a 2. fejezetben az oldalaink előkészítésénél: ott mindegyik oldal designterve alapján elkészült az oldalak sablonja, megfelelően jelölve a dinamikus részeket benne.
A sablon szerint a bemutatólistánál az alábbi struktúrába várjuk a dinamikus tartalmat:
<!DOCTYPE html> <html> <head> <title>DYSS</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link type="text/css" href="styles/dyss.css" rel="stylesheet" /> <link type="text/css" href="styles/keplista.css" rel="stylesheet" /> </head> <body> <header> <div> <h1><a href="#">DYSS</a></h1> <p><a href="#" class="button"><span>XY fiókja</span></a></p> </div> </header> <div id="content"> <aside> <nav> <ul> <li><a href="#">Bemutatóim</a></li> <li><a href="#">Profilom</a></li> <li><a href="#">Kilépés</a></li> </ul> </nav> </aside> <div id="main_content"> <div id="inner_content"> <h2>XY bemutatói</h2> <p><a href="#" class="button"><span>Új bemutató létrehozása</span></a></p> <ul class="lista"> <!-- Dinamikus tartalom kezdete: bemutatólista --> <li> <div class="fo"> <a href="#"> <img src="http://1.bp.blogspot.com/-n98sHI1x0_Q/TdGRC9HaVjI/AAAAAAAAA0E/6Bsq5yzy5bE/s1600/2009-04-03_151125.png" /><br /> </a> <div class="info"> <h4>Példa bemutató</h4> <p class="kisbetu">Példa bemutató leírása</p> <p class="kisbetu"> Megtekintések: 64<br /> Kedvencek: 1<br /> Publikus<br /> Létrehozva: 2012.01.02. </p> </div> </div> <div class="funkciok"> <ul> <li><a href="#">Megtekint</a></li> <li><a href="#">Szerkeszt</a></li> <li><a href="#">Töröl</a></li> </ul> </div> </li> <!-- Dinamikus tartalom vége: bemutatólista --> </ul> <div class="separator"></div> </div> </div> <div class="separator"></div> </div> <footer>© Horváth Győző, Eötvös Loránd Tudományegyetem, Informatika Kar</footer> </body> </html>
A HTML vázlatunkban jelöltük, hogy melyik részét kell dinamikusan tölteni. Ebben az esetben a meghatározott struktúrájú listaelem generálása a cél, minden egyéb változatlan.
Az alábbi példákban mindegyik oldal felépítése (layout) ugyanaz: felül a címsor, bal oldalt a menü, alul a lábléc. A konkrét tartalom pedig az oldal legnagyobb részét tölti ki. Itt jelenik meg a bemutatólista, itt visszük fel az új bemutató adatait, vagy szerkesztjük egy meglévőét. Mindegyik oldal konkrét tartalmában tér el, az alapvető felépítése az oldalaknak ugyanaz. Így az alábbi példákban felesleges lenne a fejléchez, menühöz, vagy lábléchez tartozó HTML kódokat újra és újra megmutatni, így ezeket a <!-- SABLON ELEJE --> és a <!-- SABLON VÉGE --> szavakkal jelöljük. A konkrét tartalom az inner_content azonosítójú div-ben szerepel, így a továbbiakban mindig csak ennek tartalmát mutatjuk be. A fenti bemutatólista-sablon tehát így néz ki:
<!-- SABLON ELEJE --> <h2>XY bemutatói</h2> <p><a href="#" class="button"><span>Új bemutató létrehozása</span></a></p> <ul class="lista"> <!-- Dinamikus tartalom kezdete: bemutatólista --> <li> <div class="fo"> <a href="#"> <img src="http://1.bp.blogspot.com/-n98sHI1x0_Q/TdGRC9HaVjI/AAAAAAAAA0E/6Bsq5yzy5bE/s1600/2009-04-03_151125.png" /><br /> </a> <div class="info"> <h4>Példa bemutató</h4> <p class="kisbetu">Példa bemutató leírása</p> <p class="kisbetu"> Megtekintések: 64<br /> Kedvencek: 1<br /> Publikus<br /> Létrehozva: 2012.01.02. </p> </div> </div> <div class="funkciok"> <ul> <li><a href="#">Megtekint</a></li> <li><a href="#">Szerkeszt</a></li> <li><a href="#">Töröl</a></li> </ul> </div> </li> <!-- Dinamikus tartalom vége: bemutatólista --> </ul> <div class="separator"></div> <!-- SABLON VÉGE -->
A lekérdezések (SELECT) az egyik leggyakrabban használt utasítások, hiszen általában sokkal több adatot jelenítünk meg, mint amennyit módosítunk egy alkalmazásban. Ugyanakkor a legösszetettebb feldolgozást kívánják meg, hiszen nem elég az adatokat elküldeni az adatbázisszervernek, az onnan visszajövőket fel is kell dolgozni. A használandó függvények szempontjából a dolgot ráadásul az is bonyolítja, hogy milyen bővítménnyel dolgozunk, puffereljük-e vagy sem az eredményhalmazt, vagy éppen előkészített utasításokon keresztül szeretnénk az adatokat lekérdezni.
A tipikus utasításcsoportok a következők:
A függvények pontos paraméterezése és várható visszatérési értékei megtalálhatók a PHP leírásában, konkrét példákat pedig lentebb adunk meg. Amit érdemes kiemelni, hogy a mysqli::query() működésében megegyezik a mysqli::real_query() és a mysqli::store_result() vagy mysqli::use_result() utasítások egymás utáni futtatásával (ez utóbbi két függvényt attól függően kell használni, hogy pufferelt vagy nem pufferelt lekérdezést hajtunk végre). A mysqli::real_query() függvény mindig a művelet sikerességét jelző logikai értékkel tér vissza. Az eredményhalmazt képviselő mysqli_result típusú objektumot pedig a mysqli::store_result() és a mysqli::use_result() függvények adják vissza. A mysqli::query() függvény – mivel ezeket a tulajdonságokat egyszerre tartalmazza – lekérdezéseknél egy mysqli_result objektummal, egyéb esetekben a művelet sikerességét jelző logikai értékkel tér vissza.
Ha előkészített utasításokat használunk, akkor központi végrehajtó függvényünk a mysqli_stmt::execute(). Ez minden esetben a művelet sikerességét jelző logikai értékkel tér vissza. Az eredményhalmazt a mysqli_stmt::fetch() függvénnyel érhetjük el. Végül csak kiegészítő információként említjük, hogy a SELECT által visszaadott sorok számát a mysqli_result::$num_rows vagy a mysqli_stmt::$num_rows adattagon kereszül kérdezhetjük le.
Előkészített utasítások esetén az eredményhalmaz az oszlop típusának megfelelő típusként tartalmazza az eredményeket, míg a mysqli::query() függvénnyel történő feldolgozás esetén az oszlop típusától függetlenül minden adatot szövegként kapunk meg.
Az alábbiakban nézzük meg ezek használatát példaalkalmazásunkban.
Ha már megvan a bemutatólistánk sablonja, akkor második lépésként koncentráljunk az adatbázis elérésére és az adatok kiolvasására. Ehhez először a mysql bővítményt használjuk pufferelt lekérdezéssel. A fenti utasításcsoportok közül tehát az ide illőt kell választanunk, mely szerint: kapcsolatot kell teremtenünk az adatbázissal (mysql_connect()), ha sikerült, akkor kiválasztjuk a megfelelő adatbázist (mysql_select_db()), összeállítjuk a lekérdező utasításunkat ($query), ezt kiadjuk az adatbázis-kezelő felé (mysql_query()), majd az adatokat egyesével kiolvasva (mysql_fetch_assoc()) legeneráljuk a megfelelő HTML tartalmat (echo). Az alábbi PHP kódot a dinamikus tartalmat jelölő HTML megjegyzések közé tesszük (bemutatok_2.php):
<?php $conn = mysql_connect('localhost', 'dyss', 'jelszo'); mysql_select_db('dyss', $conn); mysql_query('set names utf8'); $q = 'select * from bemutato'; $result = mysql_query( $q ); while ($sor = mysql_fetch_assoc($result)) { $sor['publikus'] = $sor['publikus'] ? 'Igen' : 'Nem'; echo <<<VEGE <li> <div class="fo"> <a href="#"> <img src="{$sor['indexfajl']}" /><br /> </a> <div class="info"> <h4>{$sor['cim']}</h4> <p class="kisbetu">{$sor['leiras']}</p> <p class="kisbetu"> Megtekintések: {$sor['megtekintes_db']}<br /> Publikus: {$sor['publikus']}<br /> Létrehozva: {$sor['letrehozas_datuma']} </p> </div> </div> <div class="funkciok"> <ul> <li><a href="#">Megtekint</a></li> <li><a href="#">Szerkeszt</a></li> <li><a href="#">Töröl</a></li> </ul> </div> </li> VEGE; } mysql_free_result($result); mysql_close($conn); ?>
Kiírásra a PHP heredoc verzióját használtuk, mert ekkor viszonylag könnyedén megtartható az eredeti szövegformátum. E helyett nyugodtan használhatjuk az idézőjeles kiíratást, hiszen az is képes több sor megjelenítésére, de ebben az esetben az első sor formázása kicsit körülményes.
A 4. sorban megjelenő SQL utasítás futtatásával (set names utf8) biztosíthatjuk a megfelelő karakterkódolást az adatbázis és PHP között.
A fenti php állományt böngészőből meghívva a várt formátumot kapjuk, természetesen a dinamikusnak jelölt részen belül az adatbázisbeli értékekkel feltöltve.
A mysqli bővítményt használva az alábbiak szerint változik a dinamikus rész. Mivel ez az interfész objektumorientált megközelítést használ, ezért először létrehozunk egy mysqli objektumot, amelynek konstruktora a kapcsolatot is megpróbálja létrehozni. A folyamat többi része ugyanaz, mint az előző esetben, csupán a használat során az objektumorientált lehetőséget választottuk (ld. megfelelő tipikus utasításcsoportot). Az egyes függényeket a megfelelő osztály (mysqli, mysqli_result) példányain keresztül hívjuk meg (bemutatok_3.php).
<?php $mysqli = new mysqli('localhost', 'dyss', 'jelszo', 'dyss'); $mysqli->query('set names utf8'); $q = 'select * from bemutato'; $result = $mysqli->query( $q ); while ($sor = $result->fetch_assoc()) { $sor['publikus'] = $sor['publikus'] ? 'Igen' : 'Nem'; echo <<<VEGE <li> <div class="fo"> <a href="#"> <img src="{$sor['indexfajl']}" /><br /> </a> <div class="info"> <h4>{$sor['cim']}</h4> <p class="kisbetu">{$sor['leiras']}</p> <p class="kisbetu"> Megtekintések: {$sor['megtekintes_db']}<br /> Publikus: {$sor['publikus']}<br /> Létrehozva: {$sor['letrehozas_datuma']} </p> </div> </div> <div class="funkciok"> <ul> <li><a href="#">Megtekint</a></li> <li><a href="#">Szerkeszt</a></li> <li><a href="#">Töröl</a></li> </ul> </div> </li> VEGE; } $result->free(); $mysqli->close(); ?>
Az előző példákban az egész bemutato táblát kilistáztuk. Ezen az oldalon legalább a bejelentkezett felhasználóra kellene szűrni (az azonosítója egyelőre beégetve szerepel), de tegyük fel, hogy címében csak bizonyos szövegrészletet tartalmazó bemutatókat szeretnénk kilistázni, így ezzel is szűrve az eredményhalmazt (a szövegrészlet is egyelőre beégetett információ). A szűrőfeltételeknek megfelelő értékek két változóban, $felhasznalo_id és $cim kaptak helyet. A lekérdezést ennek megfelelően ki kell egészítenünk egy where feltétellel, ahol a fenti két mezőre szűrünk.
<?php $mysqli = new mysqli('localhost', 'dyss', 'jelszo', 'dyss'); $mysqli->query('set names utf8'); $felhasznalo_id = 1; $cim = 'új'; $q = "select * from bemutato where cim like '%{$cim}%' and felhasznalo_id = {$felhasznalo_id}"; $result = $mysqli->query( $q ); ?>
A $result feldolgozása a korábban már ismertett módon történik. A biztonsági kérdéseknél már jeleztük, hogy a direkt szövegösszefűzés helyett érdemes sablonokkal operáló függvénnyel dolgozni a paraméterek helyét jelezve, és a beszúrandó paramétereket megfelelően ellenőrizni és a speciális karaktereket kiescape-elni. PHP-ban a szövegsablonokat az sprintf() függvény biztosítja, az SQL befecskendezés elleni védelmet pedig a mysql_real_escape_string() függvénnyel érhetjük el.
<?php $q = sprintf("select * from bemutato where cim like '%%%s%%' and felhasznalo_id = %d", $mysqli->real_escape_string($cim), $felhasznalo_id); ?>
A sok % jel oka, hogy '%par%' formában szeretnénk a LIKE kifejezést megadni, a % escape-elése pedig a % jellel történik (bemutatok_4.php).
Az elméleti részben láthattuk, hogy az adatkötés használatával biztonságosabbá, hatékonyabbá és átláthatóbb kódot eredményezővé tehető a kliens (PHP) és az adatbázisszerver közötti adatátadás, paraméterezés. Az adatkötés során az SQL utasítás paramétereit, illetve eredményeit változókhoz köthetjük, illetve megmondhatjuk a paraméterként átadott adat típusát.
Adatkötött paraméterek használatánál a megfelelő utasításcsoportot kell használnunk: a lekérdezést át kell írnunk paraméteresre, ezt előkészíttetnünk a MySQL szerverrel (mysqli::prepare()), a paramétereket PHP változókhoz kötni (mysqli_stmt::bind_param()), futtatni a lekérdezést (mysqli_stmt::execute()), az oszlopokat a megfelelő változókhoz kötni (mysqli_stmt::bind_result()), végül feldolgozni az eredményhalmazt soronként (mysqli_stmt::fetch()).
<?php $mysqli = new mysqli('localhost', 'dyss', 'jelszo', 'dyss'); $mysqli->query('set names utf8'); $felhasznalo_id = 1; $cim = 'új'; $q = 'select id, cim, leiras, indexfajl, megtekintes_db, publikus, letrehozas_datuma from bemutato where cim like ? and felhasznalo_id = ?'; $stmt = $mysqli->prepare($q); $stmt->bind_param('si', $cim, $felhasznalo_id); $felhasznalo_id = 1; $cim = "%{$cim}%"; $stmt->execute(); $stmt->bind_result($id, $cim, $leiras, $indexfajl, $megtekintes_db, $publikus, $letrehozas_datuma); while ($sor = $stmt->fetch()) { $publikus = $publikus ? 'Igen' : 'Nem'; echo <<<VEGE <li> <div class="fo"> <a href="#"> <img src="{$indexfajl}" /><br /> </a> <div class="info"> <h4>{$cim}</h4> <p class="kisbetu">{$leiras}</p> <p class="kisbetu"> Megtekintések: {$megtekintes_db}<br /> Publikus: {$publikus}<br /> Létrehozva: {$letrehozas_datuma} </p> </div> </div> <div class="funkciok"> <ul> <li><a href="#">Megtekint</a></li> <li><a href="#">Szerkeszt</a></li> <li><a href="#">Töröl</a></li> </ul> </div> </li> VEGE; } $stmt->free_result(); $mysqli->close(); ?>
Ez a feldolgozás nem puffereli le az eredményhalmazt. Ha erre szükségünk van, akkor a $stmt->execute(); utasítás után közvetlenül ki kell adnunk a $stmt->store_result(); parancsot is (ld. bemutatok_5.php).
A fenti megoldásban látható, hogy a mysqli_stmt::bind_result() függvénynél expliciten meg kellett adni, hogy melyik oszlopot melyik változóhoz kötjük. Ez addig kiválóan működik, amíg ismerem a lekérdezett oszlopokat. De mi a helyzet akkor, ha nem ismerem, vagy nem tudom, milyen sorrendben érkeznek, például egy select * from bemutato esetén? Mit tegyünk, ha azt szeretnénk, hogy az egyes változók helyett egy asszociatív tömbben kapjuk vissza az eredményt, valahogy így: $stmt->bind_result($sor);, ahol $sor egy tömb? Sajnos ez ilyen módon nem működik. Az egyik lehetséges megoldást a mysqli_stmt::get_result() függvény adja, amely egy mysqli_result objektumban adja vissza az eredményhalmazt. Ezen aztán a szokásos mysqli_result::fetch_assoc() függvénnyel végigmehetünk. Ha mégsem szeretnénk azonban ezt megoldást, például azért, mert nem mysqlnd könyvtárral dolgozunk, akkor más megoldás után kell néznünk.
Egy select * from bemutato utasítás futtatásakor tetszőleges számú és nevű eredménymezőnk lehet, ezek tárolását egy tömbben szeretnénk elvégezni ($sor). A futtatás után lekérdezhető az eredmény metaadataiból, hogy hány és milyen nevű eredménymező lesz (mysqli_stmt::result_metadata() és mysqli_stmt::fetch_fields() függvény). Végig kell menni az összes mezőn, létre kell hozni a $sor tömb megfelelő elemét. A mysqli_stmt::bind_result()-nak most ennek a tömbnek az elemeit kellene megadnunk. Ebben segítségünkre a call_user_func_array() függvény lesz, mely egy adott objektum metódusát hívja meg, és paraméterül egy számokkal indexelt tömböt vár, amelyet az adott metódus paramétereiként ad át. A $sor tömbnek azonban szöveggel indexelt elemei vannak, ezért be kell vezetni a $params tömböt, és annak referencia szerint át kell adni a $sor tömb egyes elemeit. Így amikor a mysqli_stmt::bind_result() a $params tömb egy eleméhez rendeli az értéket, azt elérjük a $sor tömb megfelelő elemeként. Az $stmt->execute(); utáni rész tehát így alakul:
<?php $sor = array(); $meta = $stmt->result_metadata(); foreach ($meta->fetch_fields() as $field) { $params[] = &$sor[$field->name]; } call_user_func_array( array($stmt, 'bind_result'), $params ); while ($stmt->fetch()) { $sor['publikus'] = $sor['publikus'] ? 'Igen' : 'Nem'; echo <<<VEGE <li> <div class="fo"> <a href="#"> <img src="{$sor['indexfajl']}" /><br /> </a> <div class="info"> <h4>{$sor['cim']}</h4> <p class="kisbetu">{$sor['leiras']}</p> <p class="kisbetu"> Megtekintések: {$sor['megtekintes_db']}<br /> Publikus: {$sor['publikus']}<br /> Létrehozva: {$sor['letrehozas_datuma']} </p> </div> </div> <div class="funkciok"> <ul> <li><a href="#">Megtekint</a></li> <li><a href="bemutato_szerkeszt.php?id={$sor['id']}">Szerkeszt</a></li> <li><a href="bemutato_torol.php?id={$sor['id']}">Töröl</a></li> </ul> </div> </li> VEGE; } $stmt->free_result(); $mysqli->close(); ?>
A végeredményt a bemutatok_6.php állomány megtekintésével lehet megnézni.
Adatmódosító utasítások (insert, update, delete) kiadása az alkalmazandó függvények szempontjából csak annyiban különbözik a lekérdezéseknél látott utasításcsoportoktól, hogy ezek esetében nincsen feldolgozandó eredményhalmaz. A mysqli::query() ebben az esetben sikeres művelet esetén igaz értékkel tér vissza (ekkor tehát nincs igazán különbség a mysqli::real_query() függvényhez képest). A művelet során a módosított sorok számát a mysqli::$affected_rows adattag tartalmazza. Előkészített utasításoknál a mysqli_stmt::execute() továbbra is igaz értékkel tér vissza sikeres végrehajtás esetén, az érintett sorok számát a mysqli_stmt::$affected_rows tulajdonságon keresztül kérdezhetjük le.
Gyakran szükségünk van egy beszúrás után az újonnan beszúrt sor id-jára. Az id sokszor egy AUTO_INCREMENT típusú mezőben van. Ebben az esetben egy insert parancs után beszúrt id-t a mysqli::$insert_id vagy a mysqli_stmt::$insert_id adattagon keresztül kaphatjuk vissza (utóbbit előkészített utasításoknál kell használni).
A tipikus utasításcsoport összességében tehát nem igazán különbözik a lekérdezéseknél megismertekkel:
A példaalkalmazásban az új bemutató hozzáadása esetén mutatjuk be az adatmódosító utasítások használatát (uj_bemutato.php).
<?php $errors = array(); $cim = ''; $leiras = ''; if ($_POST) { $cim = $_POST['cim']; $leiras = $_POST['leiras']; if (trim($cim) == '') { $errors[] = 'A név kitöltése kötelező!'; } if (trim($leiras) == '') { $errors[] = 'A leírás kitöltése kötelező!'; } if (empty($errors)) { $mysqli = new mysqli('localhost', 'dyss', 'jelszo', 'dyss'); $mysqli->query('set names utf8'); $stmt = $mysqli->prepare( "insert into bemutato (cim, leiras, felhasznalo_id) values (?, ?, 1)"); $stmt->bind_param('ss', $cim, $leiras); $stmt->execute(); $stmt->free_result(); $mysqli->close(); header('Location: bemutatok_6.php'); exit(); } } ?> <!-- SABLON ELEJE --> <h2>Új bemutató adatai</h2> <?php if (!empty($errors)) : ?> <div class="error"> <ul> <?php foreach ($errors as $error) : ?> <li><?php echo $error; ?></li> <?php endforeach; ?> </ul> </div> <?php endif; ?> <form action="uj_bemutato.php" method="post" accept-charset="utf-8" class="big"> <dl> <dt><label for="cim">Cím</label></dt> <dd><input type="text" name="cim" value="<?php echo $cim; ?>" id="cim" class="text" /></dd> <dt><label for="leiras">Leírás</label></dt> <dd><textarea name="leiras" cols="90" rows="12" id="leiras" class="text" ><?php echo $leiras; ?></textarea></dd> <dd><input type="submit" name="uj_bemutato" value="Létrehoz" class="button" /></dd> </dl> </form> <!-- SABLON VÉGE -->
Új bemutató létrehozásakor egy űrlapot kell kitölteni, ahol a bemutató címét és leírását kell megadni. Ehhez egy üres űrlapot generálunk le. Mivel mindkét mező kitöltése kötelező, és ennek hiányából fakadó hibaüzeneteket ugyanezen az oldalon szeretnénk jelezni, az oldal adatait ugyanez a PHP szkript dolgozza fel. A szkript elején megvizsgáljuk, hogy érkezett-e POST-olt adat (if ($_POST) { }). Ha először jövünk az oldalra, akkor tipikusan GET kéréssel történik az oldal kiszolgálása, tehát a $_POST tömb üres lesz. Ha viszont felküldjük az adatot, akkor ki lesz töltve. Ebben az esetben megvizsgáljuk a felküldött adatokat, egy $errors tömbben gyűjtjük a hibákat, ha vannak, majd ha ez üres, azaz nincsen hiba, akkor csatlakozunk az adatbázishoz, és adatkötéssel meghívjuk a megfelelő insert parancsot. Ennek végeztével az oldalt átirányítjuk a listaoldalra.
Validációs hiba esetén újra megjelenik az oldal, tetején egy felsorolásban a hibákkal, az űrlap mezőiben pedig az előzőleg megadott értékekkel. Így gondoskodunk az űrlap állapot-megőrzéséről. Mivel az űrlapelemekbe akkor is beírjuk a $cim és $leiras értékét, amikor először érkezünk az oldalra, ezért szükséges ezen változókat kezdetben üresre állítani, ahogy az a szkript elején látható.
A MySQL adatbázisok bizonyos táblatípusok esetén (INNODB) támogatják a tranzakciókat. PHP-ban az alapértelmezett beállítás az az, hogy egy utasítás végrehajtása után (mysqli::query() vagy mysqli_stmt::execute()) automatikusan történik egy commit parancs is. Ezt hívják autocommit-nak. A mysqli::autocommit() függvények logikai értéket adva ez ki- vagy bekapcsolható. Tranzakciókezelésnél tehát ezt ki kell kapcsolni ($mysqli->autocommit(FALSE);). Az ez után kiadott utasítások pedig mind egy tranzakcióba tartoznak egészen addig, amíg vagy a mysqli::commit(), vagy a mysqli::rollback() utasítást ki nem adjuk. A szkript vége egy implicit rollback-kel ér fel. Az autocommit kikapcsolása után opcionálisan kiadhatunk egy adatbázisoldali start_transaction parancsot.
A megfelelő utasításcsoportok tehát így alakulnak:
Az alkalmazásunkban egy kellően bonyolult oldalt választottunk a tranzakciókezelés bemutatásához. Egy bemutató adatainak módosításakor a cím és leírás mellett beállíthatjuk, hogy publikus-e a bemutató, illetve megadhatunk címkéket is. Az első három mező a bemutato táblához tartozik, a címkék azonban egy külön táblában tárolódnak, és egy kapcsolótáblán keresztül határozzuk meg, hogy a bemutatóhoz melyik címkék tartoznak. Sikeres adatküldés esetén egy tranzakció keretén belül a következőket hajtjuk végre:
A szkript, illetve az oldal felépítése és működése az új bemutató beszúrásához hasonlóan történik: űrlap megadása, oldal önmagát hívja. Különbség csupán annyi, hogy itt akkor is adatbázishoz kell fordulnunk, amikor először jöttünk az oldalra, hiszen a jelenlegi adatokkal előzetesen fel kell tölteni a mezőt. Validációs hiba esetén a felküldött adatokat írjuk vissza az űrlapmezőkbe (ld. bemutato_szerkeszt.php).
<?php $id = $_GET['id']; $errors = array(); if ($_POST) { $cim = $_POST['cim']; $leiras = $_POST['leiras']; $publikus = isset($_POST['publikus']) ? 1 : 0; $cimkeszoveg = $_POST['cimkek']; if (trim($cim) == '') { $errors[] = 'A név kitöltése kötelező!'; } if (trim($leiras) == '') { $errors[] = 'A leírás kitöltése kötelező!'; } if (empty($errors)) { $mysqli = new mysqli('localhost', 'dyss', 'jelszo', 'dyss'); $mysqli->query('set names utf8'); $mysqli->autocommit(FALSE); //$mysqli->query('start transaction'); $q = "update bemutato set cim = ?, leiras = ?, publikus = ? where id = ?"; $stmt = $mysqli->prepare($q); $stmt->bind_param('ssii', $cim, $leiras, $publikus, $id); $stmt->execute(); $stmt->free_result(); $stmt = $mysqli->prepare('delete from bemutato_cimke where bemutato_id = ?'); $stmt->bind_param('i', $id); $stmt->execute(); $stmt->free_result(); $cimkek = explode(',', $cimkeszoveg); foreach ($cimkek as $cimke) { $cimke = strtolower(trim($cimke)); $q = 'select id from cimke where cimke = ?'; $stmt = $mysqli->prepare($q); $stmt->bind_param('s', $cimke); $stmt->execute(); $stmt->store_result(); $stmt->bind_result($cimke_id); if ($stmt->num_rows > 0) { $stmt->fetch(); $cimke_id; $stmt->free_result(); } else { $q = 'insert into cimke (cimke) values (?)'; $stmt = $mysqli->prepare($q); $stmt->bind_param('s', $cimke); $stmt->execute(); $cimke_id = $stmt->insert_id; $stmt->free_result(); } $q = 'insert into bemutato_cimke (bemutato_id, cimke_id) values (?, ?)'; $stmt = $mysqli->prepare($q); $stmt->bind_param('ii', $id, $cimke_id); $stmt->execute(); $stmt->free_result(); } $mysqli->commit(); //$mysqli->query('commit'); $mysqli->close(); header('Location: bemutatok_6.php'); exit(); } } else { $mysqli = new mysqli('localhost', 'dyss', 'jelszo', 'dyss'); $mysqli->query('set names utf8'); $q = 'select cim, leiras, publikus from bemutato where id = ?'; $stmt = $mysqli->prepare($q); $stmt->bind_param('i', $id); $stmt->execute(); $stmt->store_result(); $stmt->bind_result($cim, $leiras, $publikus); $stmt->fetch(); $stmt->free_result(); $q = 'select c.cimke from cimke c, bemutato_cimke bc where c.id = bc.cimke_id and bc.bemutato_id = ?'; $stmt = $mysqli->prepare($q); $stmt->bind_param('i', $id); $stmt->execute(); $stmt->store_result(); $stmt->bind_result($cimke); $cimkek = array(); while ($stmt->fetch()) { $cimkek[] = $cimke; } $stmt->free_result(); $cimkeszoveg = implode(',', $cimkek); $mysqli->close(); } ?> <!-- SABLON ELEJE --> <h3>Bemutató adatainak szerkesztése</h3> <?php if (!empty($errors)) : ?> <div class="error"> <ul> <?php foreach ($errors as $error) : ?> <li><?php echo $error; ?></li> <?php endforeach; ?> </ul> </div> <?php endif; ?> <form action="bemutato_szerkeszt.php?id=<?php echo $id; ?>" method="post" accept-charset="utf-8" class="big"> <dl> <dt><label for="cim">Cím</label></dt> <dd><input type="text" name="cim" value="<?php echo $cim; ?>" id="cim" class="text" /></dd> <dt><label for="leiras">Leírás</label></dt> <dd><textarea name="leiras" cols="90" rows="12" id="leiras" class="text" ><?php echo $leiras; ?></textarea></dd> <dt><label for="Publikus">Publikus</label></dt> <dd><input type="checkbox" name="publikus" value="publikus" id="publikus" <?php echo $publikus ? 'checked="checked"' : ''; ?> /></dd> <dt><label for="cimkek">Címkék</label></dt> <dd><input type="text" name="cimkek" value="<?php echo $cimkeszoveg; ?>" id="cimkek" class="text" /></dd> <dd><input type="submit" name="szerkeszt" value="Bemutató adatainak módosítása" class="button" /></dd> </dl> </form> <!-- SABLON VÉGE -->
A MySQL adatbázisok az 5-ös verziótól kezdve támogatják a tárolt eljárásokat (és függvényeket). Ezek meghívásához nincsen külön PHP függvény, a mysqli::query() és a mysqli_stmt::execute() függvényeket kell használni megfelelő SQL utasítással. Ha létrehozunk egy tárolt eljárást (pl. sp_bemutato_torlese()), akkor azt a CALL sp_bemutato_torlese() SQL paranccsal tudjuk meghívni. Ezt kell a fenti két végrehajtó függvénynek átadni.
A tárolt eljárásoknak lehetnek bemeneti (IN) és kimeneti (OUT) paraméterei is. A bemeneti paramétereket ugyanúgy kell megadni, mint ahogy például egy szűrőfeltétel esetén; érdemes itt is az előkészített utasításokat használni és a bemeneti paramétereket adatkötéssel átadni. A kimeneti paraméter esetében a híváskor a következőképpen adjuk meg a kimeneti paramétert: CALL tarolt_eljaras(@kipar);, majd ezt követően a következő utasítással tudjuk kiolvasni: SELECT @kipar; (ld. ezt az oldalt). Tárolt függvények esetén a visszatérési értéket a mysqli_result objektumon, vagy a mysqli_stmt::fetch() függvényen keresztül érjük el.
Példaalkalmazásunkban egy bemutató törlését végezzük el a következő tárolt eljárás segítségével:
DELIMITER $$ create procedure sp_bemutato_torlese( p_bemutato_id INT ) begin declare exit handler for not found rollback; declare exit handler for sqlexception rollback; declare exit handler for sqlwarning rollback; start transaction; SET SQL_SAFE_UPDATES=0; delete from kep where bemutato_id = p_bemutato_id; delete from bemutato_cimke where bemutato_id = p_bemutato_id; delete from cimke where not exists (select * from bemutato_cimke where bemutato_cimke.cimke_id = cimke.id); delete from kedvenc where bemutato_id = p_bemutato_id; delete from bemutato where id = p_bemutato_id; SET SQL_SAFE_UPDATES=1; commit; end; $$
A tárolt eljárás egyetlen paramétert, a törlendő bemutató azonosítóját várja bemenő paraméterként. Az eljárás törzsében látható, hogy egy bemutató törlése nemcsak a bemutato táblából törli ki a megfelelő sort, hanem az összes vonatkozó adatot (címkéket, kedvenceket) is kitörli, mindezt egy tranzakció keretében.
Az alkalmazásban ennek a funkciónak külön HTML felület nem szükséges. A Törlés hivatkozásra kattintva az alábbi PHP szkript hívódik meg:
<?php $id = $_GET['id']; $mysqli = new mysqli('localhost', 'dyss', 'jelszo', 'dyss'); $mysqli->query('set names utf8'); $q = 'CALL sp_bemutato_torlese(?)'; $stmt = $mysqli->prepare($q); $stmt->bind_param('i', $id); $stmt->execute(); $stmt->free_result(); header('Location: bemutatok_6.php'); ?>
Ebben az adatbázishoz való kapcsolódás után adatkötve átadva az azonosítót meghívjuk a tárolt eljárást, majd annak végeztével az oldalt átirányítjuk a listázó oldalra.
Az eddigi példákban mindig feltételeztük, hogy a kiadott utasításaink hiba nélkül hajtódnak végre. A valós alkalmazásokban azonban foglalkozni kell azzal, hogy hogyan reagáljon alkalmazásunk arra, ha véletlenül valamelyik műveletünk végzése során hiba lép fel. A hiba sokféle lehet: mi magunk is elírhatjuk az SQL kifejezést, de az is előfordulhat, hogy a kapcsolatban vagy adatbázis oldalon lép fel egy tőlünk független hiba. Nem szabad feltételeznünk azt, hogy mindig minden rendben megtörténik, sőt mindig fel kell készülnünk a hiba bekövetkeztére és annak kezelésére.
Korábban már érintettük, hogy az SQL utasításokat végrehajtó mysqli::real_query() vagy mysqli_stmt::execute() parancsok siker esetén igaz értékkel térnek vissza. Ha a végrehajtás során hiba lép fel, akkor a visszatérési értékük hamis. De ez nemcsak erre a két függvényre igaz. Általában elmondható, hogy az adatbázissal kapcsolatos függvények hamissal térnek vissza hiba esetén (még akkor is, ha helyes működés esetén nem logikai típusú a visszatérési értékük). (Természetesen az egyes függvények részletes működéséről a PHP referenciában lehet utánaolvasni.) A visszatérési érték alapján tehát már detektálhatjuk a hibát, annak lekérdezésére pedig, hogy pontosan milyen hiba lépett fel, dedikált függvényeket kapunk:
Az errno függvények a hibakódot adják vissza, az error függvények pedig a hiba szöveges leírását adják.
A PHP-ban beállítás kérdése, hogy hiba esetén maga a PHP kiír-e figyelmeztetést vagy hibát. Ha ez be van állítva, akkor elképzelhető, hogy a PHP is megjeleníti a maga szövegét, és mi magunk is kiírjuk a hibaüzenetet. A hibaüzenetek kiírását a @ hibavezérlő operátor segítségével lehet letiltani. Ez az operátor bármilyen kifejezés előtt állhat. A minket érdeklő témakörben tipikusan az adatbázisfüggvények elé szoktuk írni, valahogy így: $result = @$mysqli->query($q);.
A bemutatólistát mysqli bővítménnyel megjelenítő oldal esetén nézzük meg, hogy milyen módon lehet a fenti alkotóelemeket összerakni.
Ha fel szeretnénk készülni az összes hiba kezelésére, akkor szinte minden esetleg hibára vezető függvényt el kell látnunk a @ operátorral, és vagy magát a függvényhívást, vagy a visszatérési értéket egy elágazásban meg kell vizsgálni nem hamis értékkel tért-e vissza. Ha igen, akkor kiírjuk a hibaüzenetet és megállítjuk a szkript futását (bemutatok_3_hiba_if.php).
<!-- SABLON ELEJE --> <h2>Bejelentkezés után tölteni kell! bemutatói</h2> <p><a href="#" class="button"><span>Új bemutató létrehozása</span></a></p> <ul class="lista"> <!-- Dinamikus tartalom kezdete: bemutatólista --> <?php $mysqli = @new mysqli('localhost', 'dyss', 'jelszo', 'dyss'); if ($mysqli->connect_errno) { die('Kapcsolódási hiba (' . $mysqli->connect_errno . ') : ' . $mysqli->connect_error); } if (!@$mysqli->query('set names utf8')) { die('Adatbázis hiba (' . $mysqli->errno . ') : ' . $mysqli->error); } $q = 'select * from bemutato'; $result = @$mysqli->query( $q ); if (!$result) { die('Adatbázis hiba (' . $mysqli->errno . ') : ' . $mysqli->error); } while ($sor = $result->fetch_assoc()) { $sor['publikus'] = $sor['publikus'] ? 'Igen' : 'Nem'; echo <<<VEGE <li> <div class="fo"> <a href="#"> <img src="{$sor['indexfajl']}" /><br /> </a> <div class="info"> <h4>{$sor['cim']}</h4> <p class="kisbetu">{$sor['leiras']}</p> <p class="kisbetu"> Megtekintések: {$sor['megtekintes_db']}<br /> Publikus: {$sor['publikus']}<br /> Létrehozva: {$sor['letrehozas_datuma']} </p> </div> </div> <div class="funkciok"> <ul> <li><a href="#">Megtekint</a></li> <li><a href="#">Szerkeszt</a></li> <li><a href="#">Töröl</a></li> </ul> </div> </li> VEGE; } $result->free(); if (!@$mysqli->close()) { die('Adatbázis hiba (' . $mysqli->errno . ') : ' . $mysqli->error); } ?> <!-- Dinamikus tartalom vége: bemutatólista --> </ul> <div class="separator"></div> <!-- SABLON VÉGE -->
A kapcsolódás sikerességét ellenőrző kódrészlet csak bizonyos PHP verzió fölött használható, ha nem működik, akkor a függvények procedurális formáját kell használnunk:
<?php if (mysqli_connect_errno()) { die('Kapcsolódási hiba (' . mysqli_connect_errno() . ') : ' . mysqli_connect_error()); } ?>
A fenti megoldás a sok elágazás miatt eléggé olvashatatlan kódot eredményez. A die miatt még így sem kellett sok egymásba ágyazott megoldást alkalmaznunk. Sokkal tömörebb és olvashatóbb kódot kapunk, ha kihasználjuk az or operátor azon tulajdonságát, hogy ha az első fele hamisként értékelődik ki, akkor kiértékeli (végrehajtja) a másik felét is. A fenti kódrészlet a következőképpen néz ki ennek használatával:
<?php $mysqli = @new mysqli('localhost', 'dyss', 'jelszo', 'dyss'); !$mysqli->connect_errno or die('Kapcsolódási hiba (' . $mysqli->connect_errno . ') : ' . $mysqli->connect_error); @$mysqli->query('set namess utf8') or die('Adatbázis hiba (' . $mysqli->errno . ') : ' . $mysqli->error); $q = 'select * from bemutato'; $result = @$mysqli->query( $q ) or die('Adatbázis hiba (' . $mysqli->errno . ') : ' . $mysqli->error); while ($sor = $result->fetch_assoc()) { $sor['publikus'] = $sor['publikus'] ? 'Igen' : 'Nem'; echo <<<VEGE ... VEGE; } $result->free(); @$mysqli->close() or die('Adatbázis hiba (' . $mysqli->errno . ') : ' . $mysqli->error); ?>
A fenti megoldás még mindig egy kicsit bőbeszédű a hibakezelést illetően. Másik hibája, hogy a hibakezelés jellege mindenhol meg van ismételve. Ha a hibakezelési logikán, kiírási módon változtatni szeretnénk, akkor minden sorban ezt át kellene írni. A gyakran ismétlődő (vagy ad absurdum a már legalább kétszer előforduló) kódrészleteket emeljük ki külön függvénybe. Így rövidebb és rugalmasabb megoldást kapunk. Hívás tekintetében mind az elágazásoknál, mind az or operátor használatánál alkalmazható:
<?php if (!@$mysqli->query('set names utf8')) { adatbazis_hiba($mysqli); } // Vagy az or operátorral: @$mysqli->query('set names utf8') or adatbazis_hiba($mysqli); ?>
Az adatbazis_hiba() függvény paraméterül kapja meg azt a mysqli példányt, amelyhez tartozó hibakódok kiírásra kerülnek. A függvény a következő módon nézhet ki:
<?php function adatbazis_hiba($mysqli) { die('Adatbázis hiba (' . $mysqli->errno . ') : ' . $mysqli->error); } ?>
A függvénybeli logikát több irányban tovább lehet fejleszteni. Elég csúnya megoldást eredményezhet egy szkript közepén kiírt die() függvény: az oldal félig legenerálva hibát jelenít meg. Szebb megoldás, ha hiba esetén egy külön oldalra (error.php) irányítjuk át az alkalmazást, ahol tetszőleges hibaüzenetet írhatunk ki:
<?php function adatbazis_hiba($mysqli) { header('Location: error.php'); exit(); } ?>
A hibakezelő függvény tovább finomítható, ha egy konfigurálási állományban tárolt, a debugolást mértékét jelző változó értékétől függően ágazunk el. Így az is lehet, bizonyos értéknél die() függvénnyel állunk le, más értékeknél átirányítjuk a hibaoldalra, vagy átirányítás előtt fájlba vagy adatbázisba logolunk.
Az adatbázis függvényeknél alkalmazott hibakezelések még hibakezelő függvény bevezetése mellett is bőbeszédűek, hiszen legalább az or és az adatbazis_hiba() függvény hívásának ott kell lennie. Az ismétlődő részeket megint csak kiemelhetjük egy külön függvénybe. Például a mysqli::query() függvény esetén írhatunk egy sajat_mysqli_query() függvényt, mely paraméterül megkapja a $mysqli változót, no meg az SQL utasítást. A függvényen belül pedig tetszőleges módon kezelhetjük a hibákat. Valahogy így:
<?php function sajat_mysqli_query($mysqli, $query) { $result = $mysqli->query($query); if(!$result) { //Ide jöhet tetszőleges hibakezelés: die, redirect } } ?>
Hátulütője ennek a módszernek, hogy ahány védendő függvényünk van, annyi saját függvényt kell írnunk.
Másik lehetőség, hogy a függvényen belül hibát dobunk (throw Exception()), és az adatbázist kezelő függvényeinket vesszük védett részbe.
A sok külön saját hibakezelő függvényre jelenthet megoldást, ha egy saját adatbáziskezelő osztályt hozunk létre a mysqli osztályból származtatva. Ebben bevezethetjük a saját hibakezelő függvényünket ($mysqli->adatbazis_hiba()), vagy felüldefiniálhatjuk az eredeti függvényeket, vagy egyedi hibakezelőket hozhatunk létre (ld. még set_exception_handler() és restore_exception_handler() függvényt). Ehhez kapcsolódik, de már a következő fejezetek felé nyit utat, ha saját adatelérési réteget (osztályt) hozunk létre. Erre egy kiváló példa a később kifejtésre kerülő PDO.
http://stackoverflow.com/questions/1820421/extending-the-mysqli-class||Absztrakt elérési osztály létrehozása
Manapság az SQL nyelvre épülő relációs adatbázis-kezelők mellett egyre több, más filozófián alapuló adatbázis-kezelő jelenik meg. Ezeket – mivel nem SQL alapúak – NoSQL adatbázis-kezelőknek szokták hívni. Létrejöttük oka az volt, hogy előfordultak olyan esetek, ahol a tárolandó adatokat csak nagy nehézségek árán lehetett a relációs struktúrába erőszakolni. A NoSQL adatbázisok tehát tipikusan olyan adatbázisok, amelyek bizonyos jellegű adatok tárolására megfelelőek, vagy bizonyos probléma megoldását tartják kiemelkedően fontosnak. Ennek megfelelően a NoSQL adatbázis-kezelők esetében az e megnevezés alá tartozó adatbázis-kezelők sokkal jobban különböznek egymástól, mint az SQL alapú adatbázis-kezelők egyes dialektusai. Bizonyos NoSQL adatbázis-kezelők alternatív formátumú dokumentumok tárolására sepcializálódott, mások a grafikus adatok tárolását tartják célnak, egyesek a gyors írást tartják szem előtt, mások a hibatolerancia maximalizálását, stb. Egy feladat megoldására manapság tehát már sokféle alternatíva létezik, ezekből kell tudni kiválasztani az adott részfeladatnak megfelelő megoldást.
További feladatok: