Záměna proměnných v řetězci

Školení, která pořádám

Pro vygenerování HTML kódu ve webové aplikaci obvykle používám šablony, konkrétně Latte. Někdy je to ale zbytečně těžký kalibr a úplně by mi stačilo v textu nahradit pár proměnných. Něco jako sprintf, ale s tím, že by proměnné byly pojmenované.

K tomu se dá použít funkce preg_replace_callback, pomocí které vyhledáme proměnné v určitém formátu a nahradíme je na základě pole s definovanými hodnotami.

Řešení v PHP 5.3

PHP 5.3 nám díky anonymním funkcím nabízí elegantní řešení. Funkci pro záměnu můžeme definovat přímo při volání a můžeme jí určit, že může používat pole s hodnotami.

<?php
/** Náhrada proměnných v řetězci
* @param string řetězec $abc se nahradí za proměnnou, $$ za dolar
* @param array ('abc' => 'hodnota')
* @return string
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function replaceVariables($template, $variables) {
    return preg_replace_callback(
        '~\$([a-z_][a-z0-9_]*|\$)~i',
        function ($match) use ($variables) {
            if ($match[1] == '$') {
                return '$';
            }
            return $variables[$match[1]];
        },
        $template
    );
}

echo replaceVariables('Hello $name!', array('name' => 'World'));
?>

Řešení v PHP 5

V PHP < 5.3 si s funkcí nevystačíme a musíme definovat třídu, která si hodnoty uloží do pomocné vlastnosti:

<?php
class VariablesReplacer {
    protected $variables;
    private $string;
    
    function __construct($template, $variables) {
        $this->variables = $variables;
        $this->string = preg_replace_callback(
            '~\$([a-z_][a-z0-9_]*|\$)~i',
            array($this, 'replace'),
            $template
        );
    }
    
    function __toString() {
        return $this->string;
    }
    
    protected function replace($match) {
        if ($match[1] == '$') {
            return '$';
        }
        return $this->variables[$match[1]];
    }
    
}

echo new VariablesReplacer('Hello $name!', array('name' => 'World'));
?>

PHP 4

V PHP 4 neexistovala metoda __toString, proměnné navíc mohly být jen veřejné. Ke zpracování výsledku bylo tedy nutné použít dočasnou proměnnou:

<?php
class VariablesReplacer4 {
    var $string;
    var $variables;
    
    function VariablesReplacer4($template, $variables) {
        $this->variables = $variables;
        $this->string = preg_replace_callback(
            '~\$([a-z_][a-z0-9_]*|\$)~i',
            array($this, '_replace'),
            $template
        );
    }
    
    function _replace($match) {
        if ($match[1] == '$') {
            return '$';
        }
        return $this->variables[$match[1]];
    }
    
}

$variablesReplacer = new VariablesReplacer4('Hello $name!', array('name' => 'World'));
echo "$variablesReplacer->string\n";
?>

Hack

Samozřejmě by se dala použít i funkce eval (lišící se ve zpracování zpětného lomítka):

<?php
function replaceVariablesEval($template, $variables) {
    extract($variables);
    return eval('return "' . addcslashes($template, '\\"') . '";');
}

echo replaceVariablesEval('Hello $name!', array('name' => 'World'));
?>

Tento přístup je ale pomalý (protože eval nedokáží zpracovat akcelerátory) a potenciálně nebezpečný (protože se dá přistoupit i k superglobálním proměnným).

Závěr

Na jednoduché ukázce je vidět, jak se novější verze PHP dají využít k psaní kratšího a elegantnějšího kódu.

Přijďte si o tomto tématu popovídat na školení Programování v PHP 5.

Jakub Vrána, Řešení problému, 26.8.2011, diskuse: 22 (nové: 0)

Diskuse

zdenek:

byl bych nerad, kdyby si ctenari odnesli do sveta programovani podobny ne-lazy pristup, kdy trida zpracovava vse jiz v konstruktoru. paklize ma clanek slouzit jako vychovny, radeji bych v konstruktoru data jen ulozil, a az v __toString() vse zpracoval (tedy v okamziku, kdy to skutecne potrebuji).

ale to je jen muj ciste neodborny nazor na vec (a samozrejme jsem rad, ze se nekdo vychove venuje)

ikona Jakub Vrána OpenID:

Původně jsem to tak zamýšlel, ale pak jsem si uvědomil, že by se to muselo zpracovat při každém převodu na řetězec (kterých může být víc). Samozřejmě by šel výsledek kešovat, ale to by kód zbytečně zkomplikovalo.

V tomhle případě se výhody lazy přístupu podle mě prakticky nikdy nevyužijí.

TF:

Mám pro někoho možná absolutně trapný amatérský dotaz, ale programuji už dobré jedno desetiletí ale pořád jsem ještě nepochopil výhodu šablon. Proč psát {$var} místo <?php echo $var; ?> ? Jenom kvůli těm pár ušetřeným znakům ? Navíc se mi stává, že s tou proměnnou třeba potřebuju ještě udělat nějakou složitější formátovací operaci a to je skoro vždycky na syntaxi a obratnost jednodušší použít opět PHP než pseudosyntaxi šablonovacího jazyka.
Pokud někdo dokáže bez emocí v pěti větách napsat jasný důvod proč jsou pseudošablonovací jazyky lepší než samotné PHP - které bych nazval nativním šablonovacím jazykem - budu rád.

ikona David Grudl OpenID:

Už jsem to zmiňoval jednou na Zdrojáku, tak se trošku opakuju. Máme běžný úkol:

- vypisuji nebufferované data z databáze (tj. neznám dopředu počet položek) jako elementy <li>
- každý lichý bude mít class="lichy"
- poslední bude mít třídu class="posledni" (tedy class="posledni lichy", bude-li poslední zároveň lichý)
- protože jsme puntičkáři, nechceme v kódu žádné prázdné <li class=" "> apod.
- a samozřejmě veškerá data vypisuje escapovaná

Pokud porovnáme řešení v šablonovacím systému (např. Latte https://gist.github.com/1172704) s řešením v PHP, rozdíl nebude několik ušetřených znaků, ale hodně moc znaků, sníží se riziko bezpečnostní díry kvůli opomenutému escapování a hlavně to člověk bude mít rychleji napsané a v budoucnu i rychleji upravené.

Ale aby to bylo skutečně zřejmé, zkus si uvedené zadání doopravdy vyřešit v PHP.

ra.ri.ta:

Zdá se mi, že příčina zrodu nádstavbových udělátek všeho druhu je naprosto někde úplně, ale úplně jinde.

Bude potřeba udělat pár školení, jinak jsme finančně v háji.
Ale na co.
A tu, ejhle, spásná myšlenka
Uděláme šablonovací systém. A zase máme pár školení.

Tomáš Kavalek:

Vy to asi nemáte moc v hlavě v pořádku - soudě dle této Vaší představy.

ikona Josef Čech:

Samozřejmě ignorujete-li argumenty, tak příčinou mohou být i svobodní zednáři, kteří se snaží šablonovacími systémy zotročit nebohý programátorský lid. .)

Jiří Knesl:

Taky by mohli vývojáři PHP udělat složitější, aby vyvařili peníze ze školení.

Třeba by mohlo být vyechování proměnné:

<?php SystemIOConsole::out(SecurityHelpersEscapeString::escapeString($a)); ?>

To by bolo prachů!

Jiří Knesl:

A teď vážně.

Někde jsou drátky a tranďáky.

Když se dobře poskládají, máme z nich AND, OR, NAND...

Když se dobře poskládají, máme procák.

Když se chvíli snažíme, uděláme operační systém.

Když se chvíli snažíme, máme z jedniček a nul assembler, po čase možná i C a po čase C++ a nebo garbage collector. Wow! Od pájení drátků jsme udělali hodně nadstaveb (to je materiálu na školení!)

A teď máme Apache a na něm web (třeba FastCGI).

Data taky sypeme do SQL a ne do souborů. (A když do souborů, pomocí filesystému, ne lineárně přímo do bloků na disku).

Do toho vleze PHP a už jsme od drátků opravdu daleko. Všichni na dráze se topí v prachách (fakt?).

A teď si přijde nějakej Grudl a pájkou slepí frejmwork, aby si mohl koupit nový Mercedes.

Jak to je? Proč každý programátor radši nevezme pájku a nepostaví si svůj počítač a nenapíše si svůj OS?

ikona Daniel Tlach:

Protože šablony píše člověk, který nemá znalosti PHP a je tak daleko snazší, když píše {$var} než <?php echo $var ?>. Kromě toho, že si to spíš zapamatuje, je i nižší pravděpodobnost, že způsobí nějaký průšvih, třeba špatným zápisem.

A jak zmínil David - data jsou už escapovaná a většinou jsou k dispozici vychytávky, které automaticky řeší takové věci, jako sudé a liché řádky, speciální třídy pro první nebo poslední záznam atd.

V neposlední řadě má snad každý lepší šablonovací systém nějakou cache, takže odpadá nutnost se o cache starat.

ikona Senky:

Ja som bol od svojich začiatkov učený na template systém (konkrétne ten, ktorý má phpBB) a nikdy som od toho neodbočil. Keď robím malé stránky, že majú 5 podstránok, tak kašlem na template systém a dám tam trochu php, avšak oddelené súbory využívam takmer stále. Keď však ide do niečoho väčšieho, nedám na template systém dopustiť. Treba to vyskúšať a prídeš na to, že hoci by si si spísal aj vlastný jednoduchý parser, je to vždy lepšie ako písať php do template. Stačí potom fungovať hoci aj na jednej premennej (napr $template) a pridávať tam nové template premenné takto:

<?php
$template
= array_merge($template, array(
    'NOVA_TEMPLATE_PREMENNA' => prva_funkcia( druha_funkcia($input)),
));
?>

ja som takémuto systému učený vďaka phpBB a myslím si, že je to veľmi dobrý spôsob, ako nemať bordel v kóde.

Hacafrakus:

Na phpBB jsem také zvyklý, velice se mi ten způsob šablonování líbí (mimochodem zdravím - známý z c&c.cz;)), ovšem v rámci zrychlení se v normálním nastavení nekontroluje změna šablon (které jsou následně kompilovány do PHP a ukládány do cache). Umožňuje také používání proměnných v CSS stylech, které jsou, pokud se nepletu, ukládány do databáze. Takhle se dají různé věci, jako šířka layoutů nebo sada ikon, měnit přímo z administrace.

ikona Jakub Vrána OpenID:

Ekvivalentem {$var} z Latte není <?php echo $var; ?>, ale <?php echo htmlspecialchars($var, ENT_QUOTES); ?> (zhruba). A to zároveň považuji za nejdůležitější výhodu šablon – nedá se zapomenout na ošetření proměnných, takže nedochází k fatálním bezpečnostním problémům.

ikona v6ak:

Ano, zhruba. Zatímco varianta htmlspecialchars  by mohla způsobit XSS například v CSS, Latte varianta jen tak ne, snad s výjimkou URL (javascript:...).

(Vím, že to víš, jen doplňuji...)

O:

Jedno desetileti... to uz si asi pise do CV senior :-)

manakmichal:

Díky šablon. jazykům je nejen zajištěna bezpečnost výstupu, protože se nezapomene výstup patřičně ošetřit. Tvorba šablon je také mnohem efektivnější díky zkráceným a jednodušším zápisům a je možno používat šablonových filtrů. Kód je pak mnohem přehlednější a nepopsatelně lépe se udržuje. Má to velké výhody oproti čistému zapisování v php.

Buďme rádi, že lidé šablonovací jazyky vymysleli. Ano, chce to sice naučit nějaké konstrukce, ale vyplatí se to - je to celkem snadné. A není tu přece jenom Latte, ale i dost podobné Smarty a další.

Opravdový odborník :-):

K tomu "a je možno používat šablonových filtrů"

Tady jen pozor na to, že takový filtr se stává součástí programu, obsahuje určitou jeho logiku. A tady nastává problém, pokud chceme udržovat několik šablon současně (např. různé vzhledy) nebo umožnit psaní šablon externím "matlalům"*, musíme (když chceme zmenit tu logiku) zasáhnout do všech šablon.

Píšu to proto, že nacpat do šablonovacího systému kde co je velké lákadlo - ono to pak vypadá úžasně, co všechno ten systém umí a jak je to jednoduché, ale dopady na architekturu programu jsou pak fatální. Proto je potřeba v těch filtrech řešit jen věci, které souvisí čistě s prezentační vrstvou - a nejen to: jen ty, které jsou specifické pro danou šablonu. Pokud máme šablon více, je potřeba ty společné části přesunou o úroveň níž (stále však v rámci prezentační vrstvy).

*) nemyslím to nijak pejorativně - častým argumentem pro šablony je to, že je může psát i ne-programátor, jehož čas je levný, a není tam moc co zkazit.

Opravdový odborník :-):

Můj komentář se týká ukázek kódu pro PHP 5:

Na jednu stranu chápu, že účel světí prostředky... ale tohle mi přijde jako naprosté nepochopení OOP nebo pohrdání jím.

Jak bych úlohu řešil já:

- pojmout to čistě procedurálně (na tom není nic špatného), tedy budeme mít nějakou funkci (či veřejnou statickou metodu) a nikde žádné objekty (resp. nebudou z volajícího kódu vidět, uvnitř klidně být mohou, to je implementační detail). Funkce přijme jako parametr šablonu a tabulku náhrad a vrátí text s doplněnými hodnotami

- pracovat objektově, napíšeme třídu, která bude zodpovědná za to nahrazování. Při vytváření instance (v konstruktoru) ji naparametrizujeme (předáme jí tabulku náhrad) a na této instanci pak budeme volat metodu, která přijme šablonu a vrátí vyplněný text.

- pracovat objektově, ale oproti předchozími případu to pojmeme obráceně - instance bude spojena se šablonou a při volání metody budeme předávat tabulku náhrad. Toto řešení dává větší smysl, protože obvykle máme jednu šablonu a několikrát ji vyplníme. Nicméně může to být i naopak - jednu sadu hodnot naplníme do více šablon (např. více druhů výstupu).

ikona v6ak:

V zásadě souhlas.  Jen dodám:
* U čistě procedurálního pojetí můýeme použít třídu a statickou metodu, dostaneme autoloading.
* Šablona jako objekt dává smysl. Moýná to v PHP není až tak patrné, protože se obvykle jednou inicializuje vše pro každé spuštění a po použití se vše ihned zahodí, ale v případě opakovaného použití máme architekturu, na které můžeme optimalizovat - šablony můžeme kompilovat do něčeho (to závisí na platformě) nebo to můýeme ponechat na chztřejší JIT (opět podle platformy). A vůbec, v konstruktoru (nebo v továrně) můžeme parseovat a upozornit na chyby

ikona Jakub Vrána OpenID:

Čistě procedurálně to v PHP < 5.3 elegantně udělat nejde. Statická metoda by klidně použít šla, to je dobrý postřeh.

Při psaní článku jsem přemýšlel, jestli v konstruktoru nenastavit jen jednu proměnnou (klonil jsem se k šabloně), ale nakonec jsem dospěl k tomu, že to v tomto případě žádné praktické výhody nemá. Takže jsem nastavil oboje, což má tu výhodu, že lze použít metodu __toString (která se pro tento případ podle mě hezky hodí).

ikona v6ak:

Proč by to tam nešlo čistě procedurálně a elegantně?

Jinak přístup 'dám mu vše v konstruktoru a pak si to jednou metodou vyzvednu' mi moc objektový nepřijde. I to sice může mít smysl, ale hlavně pokud jde o nějaký lazy přístup. Ten zde asi moc potřeba nebude. A kdyby, pak by to šlo udělat trošku univerzálnějším způsobem, zvlášť od 5.3.

ikona Jakub Vrána OpenID:

Tak ukaž kód.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.