ugrás a tartalomhoz

PHP alapú sablonmegoldás

szabo.b.gabor · 2010. Aug. 14. (Szo), 18.24

Komoly alkalmazások fejlesztése elképzelhetetlen sablonok használata nélkül. Számtalan megoldás létezik, az egyik legismertebb talán a Smarty. Én most egy sokkal egyszerűbb, PHP alapú megoldást szeretnék bemutatni, amivel könnyű elindulni a sablonok használatának rögös útján, és ha megismeri az ember, lehet, hogy nem is érzi szükségét a továbblépésnek.

A megoldás nem tőlem származik, egyetemi éveim alatt találtam sablonozó rendszer után kutatva, a Weblaboron körülnézve is megtalálható Nagy Gusztáv webes jegyzetének végén. Ő Massasit jelöli meg forrásként, én annakidején máshol bukkantam rá (amit most természetesen nem sikerült megtalálnom).

Miért is használunk sablonokat? A választ már mindenki kívülről fújja: hogy különválasszuk a logikát a megjelenéstől (MVC). A program egy része előállítja a megjelenítéshez szükséges adatokat, majd átadja egy másik programrésznek (ez volna a sablon), amely a megfelelő formában megjeleníti azokat. Ezzel elkerülhető, hogy az adatbázisból kiolvasó ciklusunk teli legyen kiiratás parancsokkal, könnyebben megtaláljuk a hibákat mindkét oldalon.

A legtöbb sablonrendszer (páldául a Smarty is) saját szintaktikát alkalmaz a sablonokban. Ez a megoldás itt is a PHP nyelvet használja, hiszen az teljes körűen alkalmas erre. Ez magában rejti a veszélyt, hogy bármilyen más, ide nem tartozó kódot is írhatunk a sablonunkba. Ennek elkerülése a sablon szerzőjének (aki általában maga a programozó) felelőssége. Nem mindenki dolgozik hatalmas projekteken, ahol különválnak a folyamatok.

Jöjjön hát az osztály bemutatása, először egy egyszerű példán kereztül.

require 'Template.php';

// A tömb akár adatbázisból is jöhetne
$videos = array(
    array(
        'artist' => 'Al Di Meola',
        'url'    => 'http://youtube.com/watch?v=Q1F4etHibVo',
    ),
    
    array(
        'artist' => 'John Mclaughlin',
        'url'    => 'http://youtube.com/watch?v=pSybXLofaDM',
    ),
    
    array(
        'artist' => 'Paco De Lucia',
        'url'    => 'http://youtube.com/watch?v=2oyhlad64-s',
    ),
);

// A sablon osztály példányosítása
$template = new Template('index.tpl');

// A tömb elérhetővé tétele a sablon számára
$template->set('videos', $videos);

// A kimenet legenerálása, kiíratása
echo $template->fetch();
<h1>Híres, tehetséges gitárbűvészek</h1>
<div class="videos">
	<? foreach($videos as $video): ?>
		<div class="video">
			<h2><?= $video['artist'] ?></h2>
			<object
				type="application/x-shockwave-flash"
				data="<?= $video['url'] ?>"
				width="480"
				height="385"
			>
				<param name="movie" value="<?= $video['url'] ?>" />
			</object>
		</div>
	<? endforeach ?>
</div>

Mit is csináltunk mindezzel? Egy tömböt, mely tartalmazta három gitárművész nevét és egy hivatkozást egy róla szóló Youtube videóra, átadtunk egy sablonfájlnak, ahol ezeket az előadókat videóikkal együtt megjelenítettük. Az első állomány nem szorul magyarázatra, az index.tpl-ben lehetnek érdekes elemek, de ezek mind a PHP nyelv részei.

A <?= egy <?php echo kifejezés rövidítése. Van, aki ezt nem szereti, az ne használja, én vállalom a hátrányait.

Vezérlési szerkezetek esetében használható a <? foreach ($collection as $item): ?> és <? endforeach ?> páros és megfelelői a feltételvizsgálatra, ciklusokra is.

Nézzük hát a mágikus Template osztály forrását.

class Template
{   
    private $variables; // A sablon változóit tárolja
    private $file;      // A sablon elérési útja
    
    function __construct($file = null)
    {
        $this->setFile($file);
    }
        
    function setFile($file)
    {
        $this->file = $file;
    }

    // Változó hozzáadása
    function set($name, &$value)
    {
        $this->variables[$name] = get_class($value) == 'Template' ? $value->fetch() : $value;
    }

    // Megnyitja a sablont, lefuttatja és visszaadja a kimenetét
    function fetch($file = null) {
        if ($file) {
            $this->setFile($file);
        }

        // A változók elérhetővé tétele a helyi hatókörben
        if (!empty($this->variables)) {
            extract($this->variables);
        }
        
        // Kimeneti puffer indítása
        ob_start();
        
        
        // Ha létezik a fájl, beillesztjük
        if (file_exists($this->file)) {
            include $this->file;
        } else {
            // Hibakezelés
        }
        
        // A puffer tartalmának mentése
        $contents = ob_get_contents();
        
        // Majd ürítése
        ob_end_clean();

        return $content;
    }
}

Haladjunk lépésről lépésre, és vizsgáljuk meg jobban osztályunkat.

function __construct($file = null)
{
    $this->setFile($file);
}

A konstruktorban meghatározhatjuk, hogy milyen sablon fájlt használjunk. Ha ezt itt nem tesszük meg, megadhatjuk a fetch() hívásakor is.

function set($name, &$value)
{
    $this->variables[$name] = get_class($value) == 'Template' ? $value->fetch() : $value;
}

A set() függvény segítségével változókat rendelhetünk hozzá a sablonhoz, amiket majd a kimenet generálása során használhat. A hozzárendelt változók $name argumentumban megadott néven érhetők majd el a sablonfájlban. Más sablon is hozzáadható, ezesetben a hozzáadandó változónak automatikusan meghívjuk a kimenetet generáló függvényét, és a kimenet lesz elérhető a változóban. A sablonok tehát egymásba ilymódon ágyazhatók.

function fetch($file = null) {
    if ($file) {
        $this->setFile($file);
    }

    // A változók elérhetővé tétele a helyi hatókörben
    if (!empty($this->variables)) {
        extract($this->variables);
    }
    
    // Kimeneti puffer indítása
    ob_start();
    
    
    // Ha létezik a fájl, beillesztjük
    if (file_exists($this->file)) {
        include $this->file;
    } else {
        // Hibakezelés
    }
    
    // A puffer tartalmának mentése
    $contents = ob_get_contents();
    
    // Majd ürítése
    ob_end_clean();

    return $content;
}

A fetch() függvény generálja le a kimenetet, amit egy karakterláncban ad vissza. Ezt bátran felhasználhatjuk bármire.

Mit nyerhetünk mindezzel? Egészen programunk végéig nem kell, hogy bármit is küldjünk a kimenetre, bármikor hívhatunk header() függvényeket, a kimeneten bármilyen módosítást végrehajthatunk, akár programunk legvégén is, és legfőképpen szebb, átláthatóbb kóddal fogunk dolgozni.

Remélem sikerült ezen egyszerű példán át megismertetnem az olvasókkal a sablonkezelés alapjait, akik addig nem használtak ilyet, ezek után remélhetőleg bátrabban vágnak bele.

 
1

Majdnem ugyanezt ...

Max Logan · 2010. Aug. 14. (Szo), 20.42
... a megoldást sikerült nekem is kb. két éve összehoznom. Kicsivel bővebb, de az alapja dettó ugyanez.

Annyival több az enyém, hogy van display() és generate() metódus (előbbi echo, utóbbi return), helper támogatás és egy autoClean flag, amivel a render utáni automatikus változóhozzárendelés nullázást lehet szabályozni.
$contents = ob_get_contents();  
ob_end_clean();  
return $content;
helyett nálam
return ob_get_clean;
van.

A működése nálam annyiban más, hogy egy TemplateEngine-t kell példányosítani és a display(), generate() kapja paraméterként a template fájl nevét. De ki fogom egészíteni a te megoldásoddal is, kinek mi a szimpatikusabb. Jelenleg van nekem egy clearAssign() és egy clearAllAssign() metódusom (előbbi egy vagy több, utóbbi az összes változóhozzárendelést törli -- autoClean az utóbbit hívja meg).
2

kis optimalizáció

oker1 · 2010. Aug. 15. (V), 05.31
az extractnak érdemes EXTR_REFS | EXTR_OVERWRITE második paramétert adni, nagyobb adatmennyiségnél jó ha nem másolja le a nem-objektum változókat a templatehez.
3

smarty

oker1 · 2010. Aug. 15. (V), 05.34
ha valaki megkérdezi mi a bajom a smartyval, mindig azt szoktam mondani hogy száz sor alatt jobb template enginet lehet írni, csak az extract függvényt kell ismerni :)
4

Objektumok

janoszen · 2010. Aug. 15. (V), 10.21
Én erre azt szoktam válaszolni, hogy próbálj meg objektumokkal dolgozni vele. A sebességéről nem is beszélve.
6

Én belenéztem egyszer a

inf · 2010. Aug. 15. (V), 14.24
Én belenéztem egyszer a forrásába. Borzasztó volt, azóta is rémálmaim vannak... :-P

Egyébként én még mindig az xslt mellett vagyok sablonozásnál, ki lehet tenni kliens oldalra, de ha muszáj szerver oldalon gányolni vele, akkor szerintem nem annyira bonyolult egy egyszerűbb xslt->php konvertálót írni.
5

Azért

szaky · 2010. Aug. 15. (V), 11.04
Azért a Smarty nem kicsit többet tud annál, amit 200 sorban az extract-al csinálsz...
7

Mégpedig?

Joó Ádám · 2010. Aug. 15. (V), 16.51
Mi kell ennél több egy sablonozórendszerbe?
8

Csekkit

deejayy · 2010. Aug. 16. (H), 06.47
Csekkit:

<?php

    function get_var($r, $v) {
        return array_key_exists($v, $r) ? $r[$v] : $v;
    }

    function subst($r, $template) {
        return preg_replace("/\{([0-9a-z_]+)\}/e", "get_var(\$r, '$1')", $template);
    }

    $text = "helo {name}!";

    print($text);
    print(subst(array(), $text));
    print(subst(array('name' => 'Béla'), $text));

?>
Persze ciklusokat kezelni, mint a smarty, nem tud, de lehet, hogy jobb is így :)

Illetve meg lehet fűszerezni ezt az egészet még olyannal is, hogy ha a behelyettesítendő _nf-re végződik {szamertek_nf}, akkor ráküldeni egy number_format függvényt is, vagy bármi más.

Vagy, ha az átadott $r tömbben nem található valamilyen változó, akkor egy globális sablontömbből vegyen értéket, tipikusan pl. a site url-je {site_url}, és akkor azt nem kell minden sablonnál átadni.
9

Kérdés

janoszen · 2010. Aug. 16. (H), 07.28
Kérdés, megéri-e ilyesmivel szivatni magad, amikor a PHP önmagában is pontosan megfelelő sablonozó nyelvnek. Ráadásul messze gyorsabb és flexibilisebb is.
10

Egyelőre nem tűnik szívásnak,

deejayy · 2010. Aug. 16. (H), 09.28
Egyelőre nem tűnik szívásnak, de persze puding próbája.

Szerintem ez:
hello {name}
jobb, mint ez:
hello <?=$r['name']?>
Vagy ez:
Összeg {sum_nf}
jobb, mint ez:
Összeg <?=number_format($r['sum'], 2, ' ', ',')?>
11

jobb vagy rosszabb

tiku I tikaszvince · 2010. Aug. 16. (H), 09.49
Szerintem egyik formára sem lehet azt mondani, hogy jobb lenne mint a másik. A Smarty által használt formára max annyit mondanék, hogy egyszerűbb, de semmiképp nem jobb.

Számomra az a legnagyobb ellenérv a saját formátumot használó sablonozó rendszerekkel szemben, hogy ezzel plusz egy hiba hordozót építünk be az alkalmazásunkba. Egy rosszul megírt sablonozóból fakadó teljesítmény problémákról már nem is beszélve.
12

Semmi értelme szerintem az

virág · 2010. Aug. 16. (H), 10.24
Semmi értelme szerintem az ilyen sablonozásnak, nekem a PHP alapú sbalon pont elég - amit pl. a Symfony is használ, teljesen átlátható, gyors és a site builderek is pikk-pakk megértik, minden egyéb megoldást erőltetettnek tartok, de aki szereti őket az felőlem használja nyugodtan :)
13

Egyetértek

fchris82 · 2010. Aug. 17. (K), 14.04
Jó néhány évvel ezelőtt, amikor olvasgattam az MVC-ről, akkor én is nagy lelkesen elkezdtem Smarty-t használni az egyik projekthez, és csak a mélytorkos szívás van vele a mai napig (szerencsére ma már csak igazán ritkán kell hozzányúlni) Tényleg reménytelen objektumokkal dolgozni benne. Rengeteg plusz munkát okozott, hogy az objektumok által visszaadott eredményt mindig tömbbé alakítsam, hogy azt átadhassam a Smarty-nak. Sőt, elég erőforrás pazarló, mert mindent elő kell készítenem tömbbe a Smarty-nak, akkor is, ha nem is lesz rá szükség mondjuk. Ezt egy objektum esetében megoldhatom ezzel:

class User
{
  protected $msgs = null;

  // Ha ezt egy if után hívom, ami mondjuk nem is teljesül, akkor tök feleslegesen kérdeztem le Smarty esetén
  public function getMessages()
  {
    if(is_null($this->msgs))
    {
      $this->msgs = $this->readMessagesFromDb();
    }

    return $this->msgs;
  }
}
A PHP meg önmagában megálló sablonnyelv is, amit minden editor megfelelően tud színezni. A Symfony-ban is jól látszik, megfelelően használható a dolog, semmi szükség az ilyen template megoldásokra. Még csak nem is kell többet írnom, mert a gyakori műveletek bele vannak helyezve kódsablonokba (Netbeans alatt):
phpe[TAB] --> <?php echo ${msg} ?>${cursor}
phpt[TAB] --> <?php echo __('${msg}'${params}) ?>${cursor}
phpif[TAB] --> ...
14

hihi jó ez az n+1-edik kell-e

Crystal · 2010. Aug. 18. (Sze), 00.20
hihi jó ez az n+1-edik kell-e template nyelv vagy nem téma.

Na de nem is szólnék hozzá túl bőven, 1 gondolat: a forráskódban a get_class($value) == 'Template' helyett én inkább azt írnám, hogy $value instanceof Template, mert hogy az akkor is működni fog ha származtatunk a Template osztályból.
15

érdemes megnézni ezt is

Crystal · 2010. Aug. 18. (Sze), 15.20
érdemes megnézni ezt is (Kohana View osztály) :
http://github.com/kohana/core/blob/master/classes/kohana/view.php
16

Anno még néztem olyan

inf · 2010. Aug. 18. (Sze), 20.16
Anno még néztem olyan sablonmotort, ahol a php volt a nyelv, és csak ellenőrizte az engine, hogy van e jogosultság a használt php függvények hívására. Szerintem még ez a legértelmesebb módszer, ha az ember mindenképp valami php-s template engine-t akar használni.
Valamelyik nagyobb framework-höz volt, ahogy nézem kohana is ilyesmit tol, de én úgy emlékszem másik fw-hez volt először.