Vissza az előzőleg látogatott oldalra (nem elérhető funkció)Vissza a modul kezdőlapjáraUgrás a tananyag előző oldaláraUgrás a tananyag következő oldaláraFogalom megjelenítés (nem elérhető funckió)Fogalmak listája (nem elérhető funkció)Oldal nyomtatása (nem elérhető funkció)Oldaltérkép megtekintéseSúgó megtekintése

Tanulási útmutató

Összefoglalás

A leckében megismerkedünk azzal, hogy milyen módon lehet az eddigi szkriptjeinket funkcionálisan szétválasztani, és ezzel utat nyitunk a magasabb szintű kódszervezések irányába.

Követelmény

A lecke végére meg kell tudni különböztetni szkriptünk egyes funkcionális részeit, és meglévő kódjainkat eszerint át kell tudni strukturálni.

Önállóan megoldható feladatok

Alkalmazások funkcionális strukturálása

A fejezetben tárgyalt példák kipróbálhatók az alábbi állomány letöltésével:

Problémafelvetés

Az előző fejezetben bemutattuk, hogyan lehet PHP-ból elérni a MySQL adatbázist, lekérdezéseket, módosításokat végrehajtani benne. Láthattuk, hogy ott, ahol adatbázisból jövő dinamikus adatokra volt szükségünk a HTML kódban, kapcsolódni kellett az adatbázishoz, lekérdezni az eredményhalmazt, majd azon végigmenve a megfelelő HTML kódot kellett előállítani. Tulajdonképpen bármelyik példát nézhetnénk az előző fejezetből, de vegyük például a bemutatók listázását. Az eredmény valami ilyesmi volt:

<!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>gyozke 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>Bejelentkezés után tölteni kell! bemutatói</h2>
                    <p><a href="uj_bemutato.php" 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');
$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();

$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();

?>
                    <!-- 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 fenti kóddal kapcsolatban – amellett, hogy jól oldja meg a feladatot – van pár probléma. Az első kifogásolható pont az, hogy benne a PHP és HTML kód keveredik. Bár ebben a példában nem kritikus, hiszen egy jól lokalizálható helyen jelenik meg a dinamikus kódrészlet, de egy bonyolultabb oldalnál, ahol mondjuk több helyen kellene adatbázisból adatot kinyerni, ott mindannyiszor, ahányszor ilyen adatot jelenítenénk meg, megjelenne a PHP blokk, és elvégeznénk a lekérdezést és az adatok HTML kódban történő kiírását. Ha sokszor fordul ez elő, akkor az oldal kódja hamar áttekinthetetlenné, olvashatatlanná válik, ami hosszú távon hátráltatja a további fejlesztést, és megnehezíti az alkamazás karbantartását. De miért baj, hogy váltakozik a PHP és HTML kód? A dinamikus tartalom kiírásához a statikus tartalomban szükségszerűen meg kell jelennie a PHP kódnak, nem? Ez valóban így van. A fenti kódrészlettel az a baj, hogy nemcsak a megfelelő adat kiírását tartalmazza a megfelelő helyen megjelenő PHP szkript, hanem egyéb funkciójú kódrészeket is.

És ezzel el is értünk a második problémához, miszerint egy-egy PHP kódrészletben – most függetlenül attól, hol jelenik meg – különböző funkcionalitású kódok keverednek: az inputadatok feldolgozása, az adatbáziskapcsolat kialakítása, egy-egy lekérdezés futtatása, majd eredményeinek kiírása. Bár ezek szükségszerű és logikailag szükségszerűen egymás után következő feladatok, de ezeknek nem feltétlenül kell egy helyen, egymást követő, sokszor összefolyó kódsorokban manifesztálódniuk. Jó lenne az egyes funkcionális részeket fizikailag is külön választani. Ezáltal ugyanis könnyen újrahasznosítható részekhez juthatunk, könnyebb a kód továbbfejlesztése, későbbi karbantartása, és lehetővé válik, hogy a különböző funkciójú részeken az adott funkcióhoz legjobban értő szakemberek dolgozzanak, azaz lehetővé teszik a csapatmunkát.

Végül harmadik problémaként az kifogásolható, hogy kódunk nagyon erősen támaszkodik a MySQL adatbázisra. Ez sokszor nem probléma, hiszen a projekt elején kiderül, hogy az alkalmazás mögött milyen adatbázis-kezelő áll. Ritka esetben azonban előfordulhat, hogy fejlesztés közben megváltoztatják a projekt alatti adatbázis-kezelőt (anyagi, licencelési, performancia okokból). Ekkor nekünk minden egyes oldalon a MySQL-specifikus függvényeket le kell cserélnünk a másik adatbázis-kezelő interfészében használatos függvényekre. Hasonló problémát okoz az is, ha MySQL-en belül bővítményt kell változtatnunk, például mysql-ről át kell térnünk valamilyen oknál fogva mysqli-re. Egy másik érv az adatbázis-kezelő specifikus kód írása ellen az, hogy kódunk kevésbé lesz általános, újrahasznosítható, projektek között hordozható.

Ezeknek a problémáknak a megoldását a fenti kódrészlet esetében nézzük végig az alábbiakban.

Vissza a tartalomjegyzékhez

HTML és PHP kód szétválasztása (nézet)

Láthattuk az első probléma felvetésénél, hogy nem feltétlenül az a baj, hogy a HTML és PHP kód váltogatja egymást, hanem az, hogy a PHP kódon belül milyen logika valósul meg. A példaoldalunknál az a nagy baj, hogy ott, ahol listaelemek megjelenítésére kerülne a sor, nemcsak megjelenítési logika, hanem sok, egyéb funkciójú kód is helyet foglal. Ezek alapján elmondhatjuk, hogy ilyen szempontból kétféle PHP kódblokkot különböztetünk meg: egy olyat, amely a feldolgozási logikát tartalmazza, és egy olyat, amely a megjelenítésért felelős. Ezt a kettőt válasszuk szét! Ennek gyakorlati következménye az, hogy onnantól, hogy elkezdjük a HTML oldalunkat generálni (<doctype html>), már csak kiíró PHP utasítás jelenhet meg. A szkriptünk felépítése ennek megfelelően úgy változik, hogy a szkript elején megtörténnek az adatokkal kapcsolatos műveletek (kapcsolódás, lekérdezés, stb.), a szkript második felében pedig az oldal megjelenítésére kerül sor. Ezzel gyakorlatilag minden szkriptet két nagy részre osztunk: logikára és megjelenítésre. A megjelenítési részt szoktuk nézetnek nevezni, hiszen a logikai részben megjelenő adatoknak egyfajta HTML sablonbeli nézetéről van szó.

A nézet leválasztásával azonban egy másik problémával szembesülünk. Amikor helyben kérdeztük le az adatokat az adatbázisból, akkor az eredménylista feldolgozásakor azonnal generálhattuk a megfelelő kimenetet. Most azonban az adatok lekérdezése (és itt akár több lekérdezésről is szó lehet!), és megjelenítése időben elválasztódik. Az adatbázisból lekérve az adatokat el kell tárolni azokat a logikai részben addig, míg a megjelenítés fel nem használja. A nézet leválasztásának tehát ára van: a megjelenítendő adatokat a logikai részben elő kell készíteni és tárolni kell a memóriában. A leválasztás előnye az, hogy a funkcionális részek elválasztódnak, és így könnyebben karbantartható, fejleszthető, a csapatmunkát és a specializálódást jobban elősegítő kódot kapunk.

A nézet – ahogy azt fent is megfogalmaztuk – tulajdonképpen az adataink HTML sablonja. Az adatokat egy HTML sablon megfelelő helyére kell beilleszteni. A nézetben tehát gyakran csak a kiíró parancs jelenik meg egy változó értékének megjelenítésekor. Természetesen előfordul az is, hogy egy HTML kódrészlet egy változó értékétől függően megjelenik vagy sem, ebben az esetben elágazás is előfordulhat. Végül sorozatok megjelenítéséhez ciklusokat használunk. Összegezve, a nézetben alapvetően csak háromféle utasítás fordulhat elő: echo, if és foreach. Ügyeljünk arra, hogy az echo parancs általában nem generálhat HTML-t, csupán csak egy változó értékét írathatja ki a HTML sablon megfelelő helyére.

Mivel a nézet alapvetően egy PHP nyelven megvalósított sablon, ezért az if és a foreach esetében is a sablonnyelvekhez közel álló, ún. alternatív szintaxisukat használjuk, amely egyrészt jobban olvashatóbb kódot eredményez, másrészt a szintaxissal is jelezzük ezen a PHP kód eltérő funkcióját.

Az alábbi példa összegzi a nézet PHP utasításait és szintaxisát:

<!-- Kiírás -->
<?php echo $valtozo; ?>

<!-- Egyirányú elágazás -->
<?php if (felt) : ?>
    HTML kód igaz esetén
<?php endif; ?>

<!-- Kétirányú elágazás -->
<?php if (felt) : ?>
    HTML kód igaz esetén
<?php else : ?>
    HTML kód hamis esetén
<?php endif; ?>

<!-- Ciklus -->
<?php foreach ($tomb as $ertek) : ?>
    HTML kód
<?php endforeach; ?>
Fontos

Figyelem! Az alternatív szintaxist csakis a nézetben szabad használni! A logikai részben továbbra is a { } zárójeles blokkok használata javasolt!

A nézet leválasztásnak megvan az a nagy előnye, hogy az előkészített adatainkhoz könnyedén tudunk más megjelenítési formát csatolni: akár egy másik HTML oldalt (pl. egy weboldal mobilos változata esetén), akár egy PDF dokumentumot, akár egy képet, vagy egy XML dokumentumot (pl. RSS hírlevél esetén).

A bemutatólistánk esetén a szétválasztás a következőképpen történhet meg:

<?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();

$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
);

$bemutatok = array();
while ($stmt->fetch()) {
    $bemutatok[] = array(
        'id'                => $sor['id'],
        'indexfajl'         => $sor['indexfajl'],
        'cim'               => $sor['cim'],
        'leiras'            => $sor['leiras'],
        'megtekintes_db'    => $sor['megtekintes_db'],
        'publikus'          => $sor['publikus'] ? 'Igen' : 'Nem',
        'letrehozas_datuma' => $sor['letrehozas_datuma'],
    );
}

$stmt->free_result();
$mysqli->close();

?>
<!-------------------------------------------------------------------------->
<!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>gyozke 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>Bejelentkezés után tölteni kell! bemutatói</h2>
                    <p><a href="uj_bemutato.php" class="button"><span>Új bemutató létrehozása</span></a></p>
                    <ul class="lista">
                    <!-- Dinamikus tartalom kezdete: bemutatólista -->
                    <?php foreach ($bemutatok as $bemutato) : ?>
                        <li>
                            <div class="fo">
                                <a href="#">
                                    <img src="<?php echo $bemutato['indexfajl']; ?>" /><br />
                                </a>
                                <div class="info">
                                    <h4><?php echo $bemutato['cim']; ?></h4>
                                    <p class="kisbetu"><?php echo $bemutato['leiras']; ?></p>
                                    <p class="kisbetu">
                                        Megtekintések: <?php echo $bemutato['megtekintes_db']; ?><br />
                                        Publikus: <?php echo $bemutato['publikus']; ?><br />
                                        Létrehozva: <?php echo $bemutato['letrehozas_datuma']; ?>
                                    </p>
                                </div>
                            </div>
                            <div class="funkciok">
                                <ul>
                                    <li><a href="#">Megtekint</a></li>
                                    <li><a href="bemutato_szerkeszt.php?id=<?php echo $bemutato['id']; ?>">Szerkeszt</a></li>
                                    <li><a href="bemutato_torol.php?id=<?php echo $bemutato['id']; ?>">Töröl</a></li>
                                </ul>
                            </div>
                        </li>
                    <?php endforeach; ?>
                    <!-- 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>

Látható, hogy a logikai részen csupán csak annyi változás van az eredeti verzióhoz képest, hogy be kellett vezetnünk a $bemutatok tömböt. Ebben tároltuk el a lekérdezés eredményét, és ennek alapján végeztük el a kiíratást. A megjelenítés sokkal tisztább lett, csupán csak a dinamikus adatoknak megfelelő echo és foreach parancsok jelennek meg benne.

A nézetet fizikailag is különválaszhatjuk a logikai feldolgozástól. A nézetet külön fájlba rakva azt már csak be kell emelni a logikai rész végére egy include paranccsal. Ezzel még modulárisabb és rugalmasabb kódot kapunk végképp megnyitva az utat az adott adattartalomhoz tartozó különböző nézetek felé.

<?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();

$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
);

$bemutatok = array();
while ($stmt->fetch()) {
    $bemutatok[] = array(
        'id'                => $sor['id'],
        'indexfajl'         => $sor['indexfajl'],
        'cim'               => $sor['cim'],
        'leiras'            => $sor['leiras'],
        'megtekintes_db'    => $sor['megtekintes_db'],
        'publikus'          => $sor['publikus'] ? 'Igen' : 'Nem',
        'letrehozas_datuma' => $sor['letrehozas_datuma'],
    );
}

$stmt->free_result();
$mysqli->close();

include('bemutatok_3_nezet.php');

A bemutatok_3_nezet.php állományban pedig a HTML sablonunk foglal helyet. A logikai részben nem jelenhet meg HTML kód, hiszen az a kimenethez tartozik. Végül a logikai részben szándékosan nincsen a PHP blokk lezárva. Ez a szkript végén automatikusan lezáródik. Azzal, hogy mi expliciten kirakjuk, akár hibát is vihetünk alkalmazásunkba, ugyanis a ?> rész utáni karakterek automatikusan kiírásra kerülnek tartalomként, amelyek megakadályozhatják további HTTP fejlécek leküldését.

Vissza a tartalomjegyzékhez

Az adatokkal kapcsolatos műveletek szétválasztása (modell)

A nézet leválasztásával irányítsuk figyelmünket a szkriptünk logikai részére. Ahogy a problémafelvetésnél említettük a keletkezett PHP kóddal kapcsolatban további kifogásaink lehetnek. Ugyan a HTML generálását száműztük ebből a részből, de továbbra is számos, különböző funkciójú kódrészlet keveredik benne. Az adatbázishoz való kapcsolódás után az inputadatok beolvasása, előkészítése történik, majd az adatok lekérdezése és a nézetnek átadandó adatstruktúra feltöltése. Bár a konkrét példa nem tartalmazza, de könnyen el tudjuk képzelni, hogy a logikai rész még számos egyéb funkciót is tartalmazhat: konfigurációs állományok beolvasását, munkamenet-indítást, felhasználói jogosultságok ellenőrzését, inputadatok elő-feldolgozását, stb. Egy bonyolultabb szkript esetében a logikai rész még a HTML generálás nélkül is könnyen áttekinthetetlenné válhat, ami hosszú távon hátráltathatja az alkalmazás fejlesztését vagy a későbbi karbantartását, továbbfejlesztését.

Erre a problémára sokféle nyelvi elemmel válaszolhatunk (pl. az egyes funkcióknak megfelelő osztályokba történő sorolásával). Láthatjuk azonban, hogy a logikai rész legnagyobb részét mégis az adatokkal való műveletek teszik ki. Ez érthető is, hiszen – ahogy ezt az egész tananyagra nézve megfogalmaztuk – egy alapvetően adatvezérelt webes alkalmazás írását tűztük ki célul. De mi van akkor, ha a bemutatók listáját egy másik szkriptünkben is használni szeretnénk, például ha egy RSS hírfolyamot szeretnénk generálni a publikus bemutatóinkból? Hogyan kerülhetjük el az ezzel járó kódismétlést? Mit tegyünk, ha át kell neveznünk az egyik táblát, vagy másik adatbázis-kezelőre váltunk? Mindegyik szkriptünkben egyesével át kell vezetnünk az ezekkel járó módosításokat?

A megoldás erre a problémára az, hogy az adatokkal kapcsolatos műveleteket egy helyre emeljük ki, és a különböző szkriptekben csak hivatkoznunk kell a kiemelt kódrészletre. A logikai résznek az adatokkal kapcsolatos műveletek elvégzéséért felelős részét modellnek hívjuk. Ezzel elkerüljük a szkripteken végighúzódó felesleges kódismétlést, és minden PHP kódot érintő adatbázisbeli változást csak egy helyen kell módosítani. Példánkban a modell így nézhet ki:

<?php

function bemutatok_lekerese($felhasznalo_id, $cim) {

    $mysqli = new mysqli('localhost', 'dyss', 'jelszo', 'dyss');
    $mysqli->query('set names utf8');
        
    $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();

    $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
    );

    $bemutatok = array();
    while ($stmt->fetch()) {
        $bemutatok[] = array(
            'id'                => $sor['id'],
            'indexfajl'         => $sor['indexfajl'],
            'cim'               => $sor['cim'],
            'leiras'            => $sor['leiras'],
            'megtekintes_db'    => $sor['megtekintes_db'],
            'publikus'          => $sor['publikus'] ? 'Igen' : 'Nem',
            'letrehozas_datuma' => $sor['letrehozas_datuma'],
        );
    }

    $stmt->free_result();
    $mysqli->close();

    return $bemutatok;
}
?>

Az egyszerű kiemelésen túl a bemutatók lekérését végző programlogikát függvénybe tettük, külön programozási egységként jelezve így funkcionális különbségét, egységét és újrahasznosíthatóságát.

Vissza a tartalomjegyzékhez

A megmaradó alkalmazáslogikai rész (vezérlő)

Az adatokkal kapcsolatos részt kiemelve az eredeti szkript az alábbiak szerint alakul:

<?php

include('bemutatok_4_modell.php');

$felhasznalo_id = 1;
$cim = 'új';

$bemutatok = bemutatok_lekerese($felhasznalo_id, $cim);

include('bemutatok_3_nezet.php');
?>

Látható, hogy a lényegi rész egészen rövid, és viszonylag jól olvasható lett. Ennek a résznek a feladata, hogy beolvassa az adatokat (ami most a mi esetünkben a $felhasznalo_id és a $cim változó kezdőértékadásának felel meg), azokkal lekérje a modellből a bemutatólistát, és azt a nézetnek előkészítse, továbbítsa. Ez a rész az egész oldal-kiszolgálási folyamatnak a vezérléséért felel, ezért ezt vezérlőnek hívjuk. Egy komplexebb alkalmazásban a vezérlő további feladatai közé tartozik a kérések feldolgozása ($_POST, $_GET, stb.), ezek ellenőrzése, munkamenet-kezelés, jogosultság-ellenőrzés, konfigurációs állományok beolvasása, stb.

Az eredeti szkriptünket így három, funkcionálisan jól elkülöníthető rétegre bontottuk. Az adatokkal kapcsolatos logika a modellbe kerül. Ebben nem jelenhet meg kérési paraméter ($_POST, $_GET, stb.), sem HTML. A megjelenítéssel kapcsolatos kódrészletek a nézetbe kerülnek. Ebben lehet csak HTML, PHP kód viszont minimálisan szerepelhet benne (echo, if, foreach). Az alkalmazáslogika a vezérlőbe kerül, ő végzi a klasszikus műveletsorokat, a beolvasást ($_POST, $_GET, stb), a feldolgozás (modell) és a kiírás (nézet) meghívását.

Vissza a tartalomjegyzékhez

A modell további finomítása

A modell fenti bevezetése nem oldja meg azt a problémát, amit e fejezet elején vetettünk fel, miszerint mit csináljunk akkor, ha más bővítményt kell használnunk vagy más adatbázis-kezelőre váltunk. A megoldást a modell további finomítása, több rétegre bontása jelenti. Ahhoz, hogy a modellünk mentes legyen a konkrét adatbázis-kezelőtől vagy annak egy bizonyos bővítményétől, egy absztrakt réteget, függvénykönyvtárt vezethetünk be, amely a modell magasabb szintje felé egy egységes utasításkészletet biztosít, miközben ezeket az utasításokat az egyes adatbáziskezelők nyelvén mind megvalósítja külön-külön. Innentől mindegy, hogy MySQL vagy Oracle a modell mögött álló adatbázis-kezelő, a modell egy ezektől a függvénykönyvtáraktól független utasításkészletet fog használni. Ezen funkcionalitást biztosító réteget nevezzük adatbázis-elérési absztrakciós rétegnek. Egy ilyen réteget létrehozhatunk mi magunk is (ahogy arról már az előző fejezet hibakezelésről szóló fejezetében szót ejtettünk), de célszerűbb egy kiforrott és bizonyított függvénykönyvátarat használnunk, amit a PHP az 5. verziója óta támogat, a PDO-t.

Saját adatbázis-elérési absztrakciós réteg létrehozása és használata

Az alábbiakban egy nagyon egyszerű adatbázis-elérési absztrakciós réteg felépítését mutatjuk be. Célja nem az, hogy általános vagy sokoldalú legyen, hanem az, hogy bemutassa ezen rétegnek a filozófiáját. Az itt bemutatottak mentén elindulva természetesen szofisztikált működésű elérési rétegek dolgozhatók ki.

A réteg építése annak az interfésznek a definiálásával kezdődik, amelyet magasabb szinten a modellből fogunk használni. Ehhez végig kell gondolnunk, hogy milyen funkciókat szeretnénk az adatbázisból használni. Ebben az esetben vagy egy az egyben leképezzük az egyik beépített függvénykönyvtár metódusait egy saját utasításkészletre, de lehetőségünk van magasabb szintű, több adatbázis utasítás hívásával járó műveletek is bevezetni. Példaképpen mi a legegyszerűbb dolgokat szeretnénk: kapcsolódni az adatbázishoz, eredményhalmazzal járó lekérdezéseket vagy módosító utasításokat futtatni, felszabadítani az erőforrásokat és bezárni a kapcsolatot. Ennek megfelelően a következő függvényeket tervezzük az interfészbe:

A konkrét megvalósítást objektum-orientált szemléletnek megfelelően osztályban végezzük el. A kapcsolódás pedig az osztály létrehozásakor megtörténhet. Az adatbázis-elérési absztrakciós réteg egy lehetséges megvalósítása MySQL adatbázis-kezelővel mysqli bővítményt használva a következő:

<?php

class DB {

    private $mysqli;
    private $result;
    private $stmt;
    private $row;
    private $databinding;

    function __construct() {
        $this->mysqli = new mysqli(SERVERNAME, USERNAME, PWD, DBNAME);

        if (mysqli_connect_error()) {
            die('Connect Error (' . mysqli_connect_errno() . ') '
                    . mysqli_connect_error());
        }

        $this->mysqli->query('set names utf8');
    }

    function disconnect() {
        $this->mysqli->close();
    }

    function exec( $query, $types = null, $params = null ) {
        if (is_null($types)) {
            $this->databinding = false;
            $this->result = $this->mysqli->query( $query );
            return $this->result;
        }
        else {
            $this->databinding = true;
            
            $this->stmt = $this->mysqli->prepare( $query );
            call_user_func_array(array($this->stmt, 'bind_param'), array_merge(array($types), $params));
            $this->stmt->execute();

            return $this->stmt->affected_rows;
        }
    }

    function query( $query, $types = null, $params = null) {
        if (is_null($types)) {
            $this->databinding = false;
            $this->result = $this->mysqli->query( $query );
            return $this->result;
        }
        else {
            $this->databinding = true;
            $this->stmt = $this->mysqli->prepare( $query );
            call_user_func_array(array($this->stmt, 'bind_param'), array_merge(array($types), $params));
            $this->stmt->execute();

            $meta = $this->stmt->result_metadata();

            $this->row = array();
            $params = array();
            foreach ($meta->fetch_fields() as $field) {
                $params[] = &$this->row[$field->name];
            }

            call_user_func_array(array($this->stmt, 'bind_result'), $params);

            $meta->free_result();

            return $this->stmt;
        }
    }

    function getnextrow() {
        if (!$this->databinding) {
            if ( $this->result )    {
                $row = $this->result->fetch_assoc();
            } else {
                $row = 0;
            }
            return $row;
        }
        else {
            if ( $this->stmt->fetch() ) {
                $sor = array();
                foreach ($this->row as $field => $value) {
                    $sor[$field] = $value;
                }
                return $sor;
            } else {
                return 0;
            }
        }
    }

    function freeresultset() {
        if ($this->result) {
            $this->result->free();
        }
        if ($this->stmt) {
            $this->stmt->free_result();
        }
    }
}

?>

A modellben természetesen nem ezt az osztályt szeretnénk közvetlenül használni, hiszen egy adatbáziskezelő-csere esetén minden modellemben a dbal_mysql.php-t például dbal_oracle.php-ra kellene cserélnem. Ehelyett minden modell egy db.php állományt használ, és ezen belül történik a megfelelő adatbázis-specifikus állomány betöltése.

<?php
   /*
   //Oracle
   define( "SERVERNAME", "" );
   define( "USERNAME", "felhnev" );
   define( "PWD", "jelszo" );
   define( "DBNAME", "adatbazis" );
   require "dbal_oracle.php";
   */
   
   //MySQL
   define( "SERVERNAME", "localhost" );
   define( "USERNAME", "felhnev" );
   define( "PWD", "jelszo" );
   define( "DBNAME", "adatbazis" );
   require "dbal_mysql.php";

?>

Ebben most a példa és az egyszerűség kedvéért konstansokként definiáltuk a kapcsolódási paramétereket, és ezeket használják az egyes DB osztályok konstruktorai.

Az adatbázis-elérési absztrakciós réteg bevezetésével a modellünk tovább egyszerűsödik. A lekérdezéshez most már tényleg csak a legszükségesebb paramétereket kell megadni, az eredményhalmaz feldolgozását azonban még mindig nekünk kell elvégezni. Természetesen egy megfelelő függvény segítségével ez a gyakran ismétlődő kódrészlet is megvalósítható az elérési rétegben.

<?php

include('dbal_db.php');

function bemutatok_lekerese($felhasznalo_id, $cim) {
    $db = new DB();
    
    $q = "select id, cim, leiras, indexfajl, megtekintes_db, publikus, letrehozas_datuma 
            from bemutato
            where cim like ? and
                felhasznalo_id = ?";
    $cim = "%{$cim}%";
    $db->query($q, 'si', array(
        &$cim,
        &$felhasznalo_id,
    ));
    
    $bemutatok = array();
    while ($sor = $db->getnextrow()) {
        $bemutatok[] = $sor;
    }
    $db->freeresultset();
    $db->disconnect();
    
    return $bemutatok;
}
?>

PDO – PHP Data Objects

Saját adatbázis-elérési absztrakciós réteg írása azonban rengeteg időbe és energiába kerül, mire elég általános és hibamentes megoldást kapunk. Akkor éri meg belevágni a fejszénket, ha valóban a projekt számára egyedi megoldást kell megvalósítanunk. Minden egyéb esetben érdemes más szoftverfejlesztő csapatok által elkészített megoldásokat használni. Ezeket többen fejlesztik, élesben is alkalmazták, így sok hibát már nem tartalmazhatnak, és a közösség részéről is visszajelzések, javaslatok érkezhetnek, amelyek még jobbá és sokoldalúbbá tehetik ezeket a függvénykönyvtárakat. Ezek közül mi most a PHP-val az 5.1-es verzió óta szállított PHP Data Objects (PDO) nevű adatbázis-elérési absztrakciós réteggel ismerkedünk meg. Tesszük ezt azért, mert eleve benne van a PHP-ban, nem kell külön modulokat telepítenünk, PHP-s fejlesztők fejlesztették a PHP-ba, és egy jól átgondolt, kiforrott megoldásról van szó.

Ahogy azt az előző fejezetben is írtuk, a PDO egy adatbázis-elérési absztrakciós réteget biztosít: egy olyan absztrakt programozói felületet, mely utasításkészletében nem függ a mögötte álló adatbázis-kezelőtől. A PDO önmagában csak egy egységes programozói felületet biztosít, nem lehetséges csak vele SQL utasítások futtatása; az egyes adatbázisok eléréséhez adatbázis-specifikus PDO vezérlőkre van szükség, amelyek az adott adatbázis-kezelő függvények segítségével implementálják a PDO absztrakt interfészét. A PDO-n keresztül már sokféle adatbázis-kezelő érhető el: MySQL, Oracle, MS SQL, PostrgreSQL, SQLite, stb. (teljes lista)

A MySQL adatbázisok eléréséhez a PDO_MYSQL vezérlő használata szükséges. Ez forgatja át az absztrakt utasításokat konkrét MySQL-specifikus hívásokká. A PDO_MYSQL vezérlő mögött egyaránt állhat a régebbi libmysql vagy az újabb mysqlnd könyvtár. A vezérlés így a következőképpen alakul: egy programból meghívott PDO utasítást a PDO átad a PDO_MYSQL vezérlőnek, az pedig az alatta lévő megfelelő könyvtáron keresztül kommunikál az adatbázissal.

A PDO előnye az adatbáziskezelő-független kód, az ebből fakadó hordozhatósága, valamint az egységes és egyszerű programozói felület. Hátránya az, hogy nem használhatja ki az egyes adatbázis-kezelők és azok speciális függvényei által nyújtott többletszolgáltatásokat, így például MySQL esetében a többszörös lekérdezéseket, stb.

A PDO interfész utasításai funkcionálisan az alábbiak szerint csoportosíthatók (ld. PHP kézikönyv):

A PDO ezeket a funkcionalitásokat három osztályon keresztül biztosítja: PDO, PDOStatement és PDOException. A PDO osztály egy példánya minden esetben a kiindulási pont. Ennek konstruktora végzi el a kapcsolódást az adatbázishoz, majd lekérdezések esetében ezen keresztül hívhatjuk meg az SQL utasítást (PDO::query()), vagy adatkötés esetén ezzel készíttethetjük elő az adatbázissal utasításunkat (PDO::prepare()). Mindkét függvény a PDOStatement osztály egy példányával tér vissza. Előkészített utasításunk esetén ezzel az objektummal történhetnek meg az adatkötések (PDOStatement::bindParam() vagy PDOStatement::bindValue()), és ezzel hajthatjuk végre az utasításunkat (PDOStatement::execute()). Végül – akár adatkötést használtunk, akár nem – ezen keresztül érhetőek el az eredményhalmaz adatai (PDOStatement::fetch(), PDOStatement::fetchAll(), PDOStatement::fetchColumn()) tetszőleges formátumban (tömbként, objektumként). Ha nem dolgoztuk volna fel az összes adatot, akkor a következő hívás előtt le kell zárnunk az erőforrást (PDOStatement::closeCursor()). Adatkötés nélküli DML utasítások hívása esetén a PDO::exec() függvény használható, amely az érintett sorok számával tér vissza. Az utoljára beszúrt sor azonosítója a PDO::lastInsertId() függvényen keresztül nyerhető ki. Az egész adatbázis-kapcsolat lezárását a PDO osztály példányának null érték adásával végezhetjük el. Az adatbázishibákat a PDOException osztály példányain keresztül érhetjük el. Bármelyik függvény hiba esetén hamis értékkel tér vissza.

A PDO-ban elérhetőek azok a funkcionalitások, amelyeket a MySQL függvényekkel kapcsolatban megismertünk. Így lehetőségünk van előkészített utasítások használatára, pufferelt vagy nem pufferelt lekérdezésekre, tranzakciókezelésre és szofisztikált hibakezelésre. Az alábbiakban itt is feladattípusonként nézzük meg a tipikus utasításcsoportokat. Az egyes utasítások pontos leírását a PHP kézikönyv megfelelő részében olvashatjuk, használatukra példát pedig lentebb adunk.

Lekérdezések

A lekérdezések tipikus utasításcsoportjai a következők:

Látható, hogy a programozói felület PDO esetén egyszerűbb, egységesebb, mint a MySQL függvényeknél, és kevesebb utasításból megvalósítható ugyanaz a funkcionalitás.

A PDO használatát az előző fejezet példáin mutatjuk be. A bemutató listáit előállító kód a következőképpen alakul:

<?php

$dbh = new PDO('mysql:host=localhost;dbname=dyss', 'dyss', 'jelszo');
$dbh->exec('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 = $dbh->prepare( $q );
$stmt->bindParam(1, $cim, PDO::PARAM_STR);
$stmt->bindParam(2, $felhasznalo_id, PDO::PARAM_INT);
$cim = "%{$cim}%";
$stmt->execute();

while ($sor = $stmt->fetch(PDO::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="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;
}

$dbh = null;

?>

A kódban látható, hogy a PDO osztály példányosításakor kell megadnunk a kapcsolati paraméterek mellett azt is, hogy milyen adatbázis-kezelőhöz kívánunk csatlakozni. Az első paraméter az adatforrás neve (Data Source Name, DSN), amely minden adatbázis-kezelőnél különböző formátumú. A második és harmadik paraméter a felhasználónév és a jelszó.

Paraméteres SQL utasításunkban kétféleképpen jelezhetjük a paramétereket: vagy ?-eket teszünk a paraméter helyére, vagy nevesített jelölőket használunk :par formában. Az első esetben a PDOStatement::bindParam() függvényében számokkal jelezzük, hogy hányadik paraméterhez milyen változót szeretnénk referencia szerint hozzárendelni. Nevesített jelölők esetén az első paraméter maga a jelölő neve lesz. A PDOStatement::execute() függvényét ezek után üres paraméterlistával hívjuk meg. Lehetőség van azonban arra is, hogy ennek paramétereként adjuk meg a kötendő változókat, értékeket. Ha ?-eket adtunk meg, akkor egy számokkal indexelt tömböt kell átadnunk a paramétereket a megfelelő sorrendben megadva, nevesített jelölőknél pedig egy asszociatív tömbnek kell paraméterként szerepelnie. Természetesen ebben az esetben a megfelelő PDOStatement::bind*() függvényekre nincsen szükség. Összességében a fenti kód ide tartozó része alapján a következő lehetőségeink vannak:

<?php
//1: általános jelölő, direkt adatkötéssel
$stmt = $dbh->prepare( 
    'select id, cim, leiras, indexfajl, megtekintes_db, publikus, letrehozas_datuma 
        from bemutato where cim like ? and felhasznalo_id = ?' );
$stmt->bindParam(1, $cim, PDO::PARAM_STR);
$stmt->bindParam(2, $felhasznalo_id, PDO::PARAM_INT);
$stmt->execute();

//2: általános jelölő, indirekt adatkötéssel
$stmt = $dbh->prepare( 
    'select id, cim, leiras, indexfajl, megtekintes_db, publikus, letrehozas_datuma 
        from bemutato where cim like ? and felhasznalo_id = ?' );
$stmt->execute(array($cim, $felhasznalo_id));

//3: nevesített jelölő, direkt adatkötéssel
$stmt = $dbh->prepare( 
    'select id, cim, leiras, indexfajl, megtekintes_db, publikus, letrehozas_datuma 
        from bemutato where cim like :cim and felhasznalo_id = :felhasznalo_id' );
$stmt->bindParam(':cim', $cim, PDO::PARAM_STR);
$stmt->bindParam(':felhasznalo_id', $felhasznalo_id, PDO::PARAM_INT);
$stmt->execute();

//4: nevesített jelölő, indirekt adatkötéssel
$stmt = $dbh->prepare( 
    'select id, cim, leiras, indexfajl, megtekintes_db, publikus, letrehozas_datuma 
        from bemutato where cim like :cim and felhasznalo_id = :felhasznalo_id' );
$stmt->execute(array(':cim' => $cim, ':felhasznalo_id' => $felhasznalo_id));
?>

Adatmódosító utasítások

Tipikus utasításcsoportjai a következők:

Módosító utasításokra az előző fejezetben az új bemutató beszúrása volt a példa. Ennek adatbázist érintő része PDO-val így néz ki:

<?php
$dbh = new PDO('mysql:host=localhost;dbname=dyss', 'dyss', 'jelszo');
$dbh->exec('set names utf8');

$q = "insert into bemutato 
        (cim, leiras, felhasznalo_id) values 
        (:cim, :leiras, 1)";
$stmt = $dbh->prepare( $q );
$stmt->execute(array(
    ':cim'      => $cim,
    ':leiras'   => $leiras,
));
$dbh = null;
?>

Tranzakciókezelés

Tipikus utasításcsoportok a következők:

Tárolt eljárások

Tárolt eljárásoknál érdemes a paramétereket adatkötéssel átadni. Ha a tárolt eljárásban vannak kimeneti paraméterek, akkor az adatkötés során be lehet ezt állítani a PDOStatement::bindParam() függvény paramétereként (ld. a PHP kézikönyv PDO-beli tárolt eljárásokról szóló részét).

Hibakezelés

Egy valós alkalmazás szempontjából nagyon fontos az adatbázishibák megfelelő kezelése. A PDO ilyen szempontból rugalmas, hiszen háromféle modellt is felajánl erre. Az első esetben a hibát a PDO::errorCode() vagy a PDO::errorInfo() függvényen keresztül vizsgálhatjuk. Ez a két függvény a PDOStatement példányoknál is elérhető. A második esetben a hibakódok beállításán túl a PDO egy figyelmeztetést (E_WARNING) is küld a szkriptünknek. Ez főleg fejlesztési fázisban lehet kényelmes. Végül a hibakódok beállítása mellett a PDO kivételt is dobhat adatbázishibák esetén. Ekkor a PDOException példányon keresztül kérdezhetjük le a hiba kódját.

Az ide tartozó függvények:

Már az előző fejezetben is láttuk, hogy a natív MySQL függvényekkel elég körülményes a hibavizsgálat, hiszen vagy minden adatbázisfüggvény után megvizsgáljuk annak sikerességét, ami elég energiaigényes vállalkozás és olvashatatlanná teszi a kódot, vagy egyéb módon tesszük egyszerűbbé a problémát. A PDO harmadik hibakezelési lehetősége nyújtja ilyen szempontból a legtisztább megoldást. Ha egy művelet közben hiba lép fel, kivételt dob, amit a szkriptünkben kell kezelni. Ennek több előnye van: egyrészt kódunk sokkal olvashatóbb lesz, hiszen nem kell szinte minden sorban vizsgálgatnunk az eredményt, hanem elég pár védett blokkot létrehozni, másrészt hibakeresés szempontjából is rögtön felfedi a hiba helyét, megakadályozva a szkriptet további futásában.

Ez utóbbira példát a bemutatók listázásánál az alábbi kódrészlet mutatja:

<?php
try {
    $dbh = new PDO('mysql:host=localhost;dbname=dyss', 'dyss', 'jelszo');
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $dbh->exec('set names utf8');

    //szokásos lekérdező és generáló kód
    //...
    
    $dbh = null;
}
catch (PDOException $e) { 
    echo "Hiba történt : {$e->getMessage()}";
    die();
}
catch (Exception $e) { 
    echo "Hiba történt : {$e->getMessage()}";
    die();
}
?>

A fenti kódok még akkori állapotában mutatják be a PDO alkalmazását, mikor még nem választottuk szét szkriptünket nézetre, modellre és vezérlőre. Ha ez a szétválasztás megtörténik, akkor természetesen a modellben van a helye a PDO-nak. A fejezet elején bemutatott modell PDO-val a következőképpen néz ki:

<?php

function bemutatok_lekerese($felhasznalo_id, $cim) {
    $dbh = new PDO('mysql:host=localhost;dbname=dyss', 'dyss', 'jelszo');
    $dbh->exec('set names utf8');

    $q = "select id, cim, leiras, indexfajl, megtekintes_db, publikus, letrehozas_datuma 
            from bemutato
            where cim like ? and
                felhasznalo_id = ?";
    $stmt = $dbh->prepare( $q );
    $stmt->bindParam(1, $cim, PDO::PARAM_STR);
    $stmt->bindParam(2, $felhasznalo_id, PDO::PARAM_INT);
    $cim = "%{$cim}%";
    $stmt->execute();
    
    $dbh = null;
    
    return $stmt->fetchAll();
}
?>

Vissza a tartalomjegyzékhez

Kitekintés

Az alkalmazás különböző részei, a nézet, a modell és a vezérlő mindegyike tovább finomítható, további rétegekre bontható. A vezérlő finomításával későbbiekben ismerkedünk meg. Eddigi leglátványosabb és legpraktikusabb finomításunk a modellben volt, ahol az adatbázis-elérési absztrakciós réteg bevezetésével adatbázis-független, egységes és egyszerű kódot tudtunk írni. A modell absztrahálható adatbázis-absztrakciós réteg bevezetésével, amely a programozó felé egy olyan absztrakt utasításkészletet definiál, amelyekben a táblák sorai objektumokként jelennek meg, és a metódusok az adatok manipulációjával kapcsolatos funkciókra koncentrálnak, mint pl. adott rekord módosítása, mentése. A metódusok pedig a háttérben a funkcióknak megfelelő SQL utasításokat generálják és futtatják ezeket az adatbázis-elérési absztrakciós rétegen keresztül. Így pl. egy rekord mezőjének módosítása után vagy insert, vagy update parancs generálódik attól függően, hogy a rekord újonnan kerül beszúrásra vagy létezett korábban. Tipikus adatbázis-absztrakciós rétegek az ORM (Object-Relational Mapping) technológiák, amelyek az adatbázisbeli rekordokat és táblákat üzleti objektumokra képezik le.

ORM megoldások:

Vissza a tartalomjegyzékhez

Fel a lap tetejére
Új Széchenyi terv
A projekt az Európai Unió támogatásával, az Európai Szociális Alap társfinanszirozásával valósul meg.