Saját sablonrendszer II.: a kezelő és a sablonok

2005. nov. 22. (kedd), 23:36, D. keze nyomán
Open Source, Saját kód, Webfejlesztés, Ismeretterjesztő
A cikk első részében áttekintettük a működési rétegek elkülönítésének előnyeit, a sablonok alapjait, a továbbiakban pedig a gyakorlati megvalósításra térünk rá. Ha a PHP kódok valamelyik része nem lenne világos, a PHP kézikönyv remek útmutatást jelenthet.
Az egyszerűbb követhetőség kedvéért a teljes kód, valamint egy példasablon már most letölthető, megtekinthető.

Első rész:
  1. A háttér: különálló működési rétegek
  2. Előnyök
  3. Hátrányok
  4. Mi az a sablon?
  5. Mit is szeretnénk elérni?
Második rész:
  1. A sablonkezelő részei
  2. A sablonkezelő osztály, mint megvalósítás
  3. Végszó


A sablonkezelő részei

Sablonkezelőnkben az elvi felépítést tekintve az alábbiakra lesz szükségünk:
  1. a teljes sablonállomány betöltése (tpl_file_betolt())
  2. a sablonállomány egy részének betöltése, így tetszőleges számú sablont tárolhatunk egyetlen sablonállományban (tpl_sablon_betolt())
  3. a sablonban lévő elemek cseréje (tpl_elem_csere())
  4. a sablon feltételes részeinek kezelése (tpl_feltetelkezelo())
  5. a sablon ismétlődő részeinek kezelése (tpl_metszet_betolt(), tpl_csere_metszetben())
  6. a kész sablon kiadása (tpl_kimenet())
  7. hibakezelés (tpl_hiba())

A sablonkezelő osztály, mint megvalósítás

A sablonkezelőt objektum orientált megközelítésben fogjuk létrehozni, a PHP5 el(nem)terjedtségére való tekintettel még a PHP 4 szerint.

class sablonkezelo
{
function tpl_file_betolt($sablon_file) {}
function tpl_sablon_betolt($melyik) {}
function tpl_metszet_betolt($melyik) {}
function tpl_csere_metszetben($tmb_adat, $melyik) {}
function tpl_feltetelkezelo($tmb_adat, $str) {}
function tpl_elem_csere($tmb_adat, $str) {}
function tpl_hiba($kod) {}
function tpl_kimenet() {}
}

1. A sablonállomány betöltése

function tpl_file_betolt($sablon_file)
{
if (@file_exists($sablon_file)) {
$this->tpl_file = $sablon_file;
$this->tpl_tartalom = file_get_contents($this->tpl_file);
} else {
$this->tpl_hiba('nincs_tpl_file:'.$sablon_file);
}
}

A sablonállomány betöltése rém egyszerű: ha megvan a szükséges állomány, tartalmát beolvassuk, ha nincs, jöhet a hibakezelés.

2. A sablonállomány egy részének betöltése
function tpl_sablon_betolt($melyik)
{
if ( !empty($this->tpl_tartalom) ) {
$start_pos = strpos($this->tpl_tartalom, "[SABLON:$melyik]")
+ strlen("[SABLON:$melyik]"); // start tag hossza
$stop_pos = strpos($this->tpl_tartalom, "[/SABLON:$melyik]");
$this->sablon = trim(substr($this->tpl_tartalom, $start_pos, $stop_pos-$start_pos));
} else {
$this->tpl_hiba('ures_tpl_file');
}
}

A teljes állománytartalomból ki kell választanunk azt a sablont, amelyikre szükségünk van. Ezt a [SABLON:valami][/SABLON:valami] elemek határolják, így az állomány tartalmából (amely egy string, vagyis karakterláncolat) kivágjuk azt a részt, amelyik [SABLON:valami] kezdetű, és [/SABLON:valami] végű.

3. A sablonban lévő elemek cseréje
function tpl_elem_csere($tmb_adat, $str)
{
foreach ($tmb_adat as $mezo => $ertek) {
$str = str_replace('{'.$mezo.'}', $ertek, $str);
}
return $str;
}

A sablonban az elemeket kapcsos zárójelek fogják közre (számtalan más sablonkezelő rendszerhez igazodva ezzel), ezek képviselik a "geometriai alakzatokat". A csere során a kapott adattömb kulcsainak megfelelő elemeket a tömbkulcshoz tartozó értékre cseréljük a célstringben (amely nem feltétlenül az egész sablont jelenti!). Például ha a tömbben az 'ertek1' => 'Érték 1' kulcs-érték pár szerepel, akkor a szövegben az {ertek1} elemet Érték 1-re cseréljük.
Megjegyzések: nyilvánvaló, hogy
az összes {ertek1} a célstringben Érték 1-re cserélődik,
a célszöveg célszerű megválasztásával befolyásolható, hogy ne az egész pl. sablonban cserélődjenek az ugyanolyan elemek,
ne adjuk két eltérő elemnek ugyanazt a nevet egyazon sablonban, sablonrészben, hisz ilyenkor az utóbb definiált értékre cserélődik mind
a szövegben normálisan is kapcsos zárójelek közt lévő, nem elem jellegű részek nem fognak cserélődni, mivel a tömbben nincs megfelelő kulcsuk,
a 4-es pont fordítva is igaz, a tömbben nem, a sablonban szereplő elemek kapcsos zárójelben, eredeti alakjukban megjelennek a kimeneten, így azonnal látható, ha valamit elbaltáztunk (a feltételes szerkezetekről lásd alább)

4. a sablon feltételes részeinek kezelése

Sablonunkban könnyen lehetnek olyan elemek, melyekről nem tudjuk előre, vajon meg kell-e jelenítenünk őket: elég az adminisztrációs linkekre gondolnunk, melyek csak jogosult felhasználóknak tűnnek elő. function tpl_feltetelkezelo($tmb_adat, $str)
{
while ( preg_match('/^.*[IF:(w+)](.*)[/IF:1].*$/sm', $str) ) {
$feltetel = preg_replace('/^.*[IF:(.+)](.*)[/IF:1].*$/sm', '$1', $str);
$str = preg_replace(
"/[IF:$feltetel](.*)[/IF:$feltetel]/sm",
(array_key_exists($feltetel, $tmb_adat) ? '$1' : ''), $str);
// van ilyen adat -> vezerlo if-et kivesszuk, sablon marad
// nincs ilyen adat -> kivesszuk az egesz felteteles reszt
}
return $str;
}

A célstringben megkeressük az összes feltételes szerkezetet, megnézzük, hogy az adattömbben van-e hozzájuk tartozó érték, ha van, akkor megjelenítjük őket, ha nincs, akkor kivesszük az egész feltételes részt.
A sablonban a feltételesen megjelenítendő részeket [IF:ertek]{valami} és {ertek}[/IF:ertek] határolja, ahol az IF-ben lévő 'ertek' a megjelenítendő rész bármelyik eleme lehet. Egy-egy IF felismerésére reguláris kifejezést használunk, az összes begyűjtéséhez a while vezérlési szerkezettel lépkedünk végig a célstringen, melynek minden ciklusában a következő történik:
- ha van megfelelő tartalom, a sablonhoz tartozó [IF:ertek] és [/IF:ertek] stringeket kiszedjük, a közöttük lévő, feltételes részt azonban benn hagyjuk a sablonban, így a továbbiakban a hagyományos módon érvényesül
- ha nincs tartalom, az egész IF-es részt IF-estől, közbezárt részestől kiszedjük, így nem érvényesül a továbbiakban.
Ezt a
(array_key_exists($feltetel, $tmb_adat) ? '$1' : '')
feltétel valósítja meg.

5. A sablon ismétlődő részeinek kezelése

Képzeljünk el egy listát, melynek minden eleme HTML-t tekintve ugyanolyan, csak a tartalmuk változik elemenként. A lista hosszát dinamikus lapoknál jellemzően nem tudjuk, így a sablonállományban nem tudjuk, hány elemet írjunk. A megoldás egy olyan, egyszer definiált "al-sablon", amelyet a lista minden eleméhez felhasználunk - ezt neveztem el metszetnek.
A metszeteket egyrészt meg kell találni a sablonban, másrészt ki kell "vágni" őket a teljes sablonból, harmadrészt a bennük lévő elemeket cserélni kell.
function tpl_metszet_betolt($melyik)
{
if ( !empty($this->sablon) ) {
$start_pos = strpos($this->sablon, "[METSZET:$melyik]")
+ strlen("[METSZET:$melyik]"); // start tag hossza
$stop_pos = strrpos($this->sablon, "[/METSZET:$melyik]");
$this->metszetek[$melyik] = substr($this->sablon, $start_pos, $stop_pos-$start_pos);
} else {
$this->tpl_hiba('nincs_sablon');
}
}

A metszet betöltése elvét tekintve megegyezik a sablonok betöltésével, pusztán a tisztább, átláthatóbb kód érdekében tartottam meg őket külön.
function tpl_csere_metszetben($tmb_adat, $melyik)
{
// metszetben felteteles reszek feldolgozasa
$this->metszetek[$melyik] = $this->tpl_feltetelkezelo($tmb_adat, $this->metszetek[$melyik]);

// elemek csereje (nincs tartalom -> metszet sablonjat adjuk meg)
if ( !isset($this->metszet_tartalmak[$melyik]) ) {
$this->metszet_tartalmak[$melyik] = $this->metszetek[$melyik];
$this->metszet_tartalmak[$melyik] = $this->tpl_elem_csere($tmb_adat, $this->metszet_tartalmak[$melyik]);
} else {
$this->metszet_tartalmak[$melyik] .= "n".$this->tpl_elem_csere($tmb_adat, $this>metszetek[$melyik]);
}
}

A metszetek kezelését két részre bontottam: a $this->metszetek tömb a metszetek sablonrészeit gyűjti, míg a $this->metszet_tartalmak a már kész, feldolgozott, teljes listákat.
Metszeten belül is lehetségesek feltételek, ezeket előre feldolgozzuk, ezáltal is kímélve az erőforrásokat.
Lehetséges, hogy még nincs metszettartalom (hisz első ízben még nincs), ekkor a sablonrészt adjuk meg metszetnek, és lecseréljük benne az elemeket.
Minden ezt követő alkalommal azonban már csak megragadjuk a metszetsablont, annak alapján lecseréljük a benne lévő elemeket, és hozzáadjuk az addigi metszettartalomhoz (ezáltal lesz végül a metszettartalom adott metszet sokszorozása, adatokkal feltöltve).
A sablon minden egyes metszetén ez eljátszható.

A preg_match meglehetősen erőforrás-igényes függvény, így meg kell kerülni, ha nagy számú ismétlődő elemünk van - mint pl. több, hosszú select űrlapmező.
A tpl_select metódus ezt hivatott szolgálni:
function tpl_select($tmb_adat, $selected, $prefix = "ntt")
{
$str = '';
foreach ($tmb_adat as $ertek => $cimke) {
$str .= ($str=='' ? '' : $prefix)."<option value="$ertek""
.($ertek==$selected ? ' selected="selected"' : '').">$cimke</option>";
}
return $str;
}

Működése értelemszerű: a választási lehetőségeket adja vissza stringként.

6. A kész sablon kiadása

Itt már örülünk, a sablonkezelés oroszlánrészén már túl vagyunk.
function tpl_kimenet()
{
if ( !empty($this->metszetek) ) {
foreach($this->metszetek as $m_nev => $m_tartalom) {
$this->sablon = preg_replace(
"/[METSZET:$m_nev].*[/METSZET:$m_nev]/smU",
(!empty($this->metszet_tartalmak[$m_nev]) ? $this->metszet_tartalmak[$m_nev] : ''),
$this->sablon);
}
}
return $this->sablon;
}

A kimenetben megnézzük, hogy volt(ak)-e a sablonunkban metszet(ek), és ha volt(ak), elvégezzük a tartalomra cserélést, majd az egész sablont - amely most már adatokkal feltöltött (X)HTML kódot jelent - kimenetre küldjük.

7. Hibakezelés

function tpl_hiba($kod) { die($kod); }
Ennél egyszerűbb nem is lehetne: ha a futást veszélyeztető hibát észlelünk, agyonütjük a scriptet, ne szaladjon tovább.
Nyilván felmerül a kérdés, miért nem rakok be egyszerűen egy die()-t oda, ahol rendellenes működés történt. Tervezési hiba volna, hisz lehetséges, hogy a későbbiekben komolyabb hibakezelés kellhet (a kódban turkálásról ugye beszéltünk a cikk első részében, mégha nem is ilyen kontextusban), másrészt logikailag is szerencsésebb elkülöníteni a hibakezelést.
Kérdésként vetődhet még fel, hogy miért szüntetjük meg a script futását, ha pl. hibaüzenet mellett folytathatnánk a generálást. A sablonkezelő rendszer részeihez külső felhasználó normális esetben nem fér hozzá, így csak a fejlesztés során követhetünk el hibát - akkor meg jobb, ha rögvest megkapjuk a méltó jutalmat.

Végszó

A sablonkezelő a fenti példákban egy XHTML-"sablonnyelv" hibridet dolgoz fel, de értelemszerűen bármilyen leírónyelvhez, tulajdonképp bármilyen szöveghez használható általános cserélőként.
Figyeljünk rá, hogy az str_replace() lényegesen gyorsabban fut le, mint a preg_replace(), így előbbit érdemes minden lehetséges helyen használni. Aki az elején elmulasztotta volna, itt is leszedheti a teljes kódot, illetve a példasablont is. A sablonkezelőt a nyílt forráskód szellemében bátran felhasználhatjátok, cserébe jelöljetek meg forrásként.

A cikk-kettősben tehát egy röpke pillanat erejéig betekintést nyerhettünk a tervezési mintákba, megismerkedtünk a sablonokkal, előnyeikkel-hátrányaikkal, majd egy primitív, de rugalmas sablonkezelőt építettünk.
A fentiekben olvasható első kódpublikációm, így örömmel fogadom a kiegészítéseket, tanácsokat, javításokat - és persze kérdéseitek, tapasztalaitok más/saját sablonkezelővel kapcsolatban is!