Jak správně vytvořit vlastní makra v Latte

Kromě sdílení šablon můžete v Latte často používaný kód zapouzdřit do makra. Rozdíl je v tom, že zatímco kód v šabloně se provádí při každém načtení stránky, makro může část kódu připravit v době kompilace šablony a do šablony vložit již připravený kód.

Instalace makra

Abyste mohli makra používat, je potřeba je nainstalovat do Latte frameworku. Pokud používáte celé Nette, stačí zapsat třídu do konfiguračního souboru:

latte:
    macros:
        - App\Components\Macros\MyMacros

V případě samostatného Latte je potřeba přidat je do frameworku při inicializaci:

$template->registerHelperLoader('Nette\Templating\Helpers::loader');
$template->onPrepareFilters[] =
    function($template) {
        $latte = new Nette\Latte\Engine();
        App\Components\Macros\MyMacros::install(
            $latte->getCompiler()
        );
        $template->registerFilter($latte);
    }
;

Samotná makra se pak přidávají vytvořením třídy odděděné z MacroSet v metodě install:

abstract class MyMacros 
    extends \Latte\Macros\MacroSet {

    public static function install(
        \Latte\Compiler $compiler
    ) {
        $set = new static($compiler);

        $set->addMacro(
            'jmeno_makra',
            array($me, 'metoda_s_kodem')
        );
    }

    public function metoda_s_kodem(
        \Latte\MacroNode $node, 
        \Latte\PhpWriter $writer
    ) { ... }
}

Metoda pro generování kódu makra dostává dva parametry: $node je třída reprezentující samotné makro (a dá se z ní např. přečíst jméno a parametry makra) a $writer je generátor PHP kódu, který můžete (ale nemusíte) použít pro usnadnění práce s makrem. Jak je používat si povíme dále.

Testování makra

Při psaní a testování makra pravděpodobně narazíte na jeden problém – změny provedené v makru se nezobrazují na stránce.

Je to tím, že Latte generuje šablony, jen pokud se změní samotný soubor se šablonou (tedy *.latte). Pokud ale změníte makro bez změny šablony, kde ho používáte, bude Latte stále používat již vygenerovanou šablonu, která obsahuje staré makro.

Po změně makra tedy musíte buď změnit i šablonu, kde ho používáte (např. připsáním mezery) nebo vymazat cache, kam se vygenerované šablony ukládají (např. /temp/cache/latte/*.latte).

Také si uvědomte, že v metodě, která makro generuje nemůžete používat debugovací nástroje jako var_dump() nebo barDump(), protože výstup funkce se buď zahodí (během generování šablony) nebo se nebude vůbec provádět (při opakovaném zobrazení stránky).

Z makra ale můžete vyhodit výjimku, např. pokud byl předán nevhodný parametr. V takovém případě se šablona nevygeneruje a místo ní se zobrazí hláška „Thrown exception '...' in ...“ a v laděnce (pokud ji používáte) se zobrazí řádek, kde makro voláte.

Základní makro

Nejjednodušší makro může jen vygenerovat kód a vrátit ho:

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    $param = $node->args;
    return "echo 'Hello $param!';";
}

Makro pak zavoláme jako {hello World} a na stránce se vypíše „Hello World!„.

Všimněte si, že metoda nevrací samotné „Hello $param!„, ale celý PHP kód včetně echo, uvozovek a středníku. To proto, že to, co vrátí makro, se následně zapíše do vygenerované šablony, což je obyčejný PHP kód (obalený třídou).

Do šablony se tedy již zapíše celý kód včetně zpracovaného parametru, tedy:

<?php echo 'Hello World!'; ?>

Během zobrazení stránky již tedy nemusí framework přemýšlet nad parametrem a rovnou zobrazí připravený text.

Parametry

Z vlastnosti $node->args získáte přesně to, co je zapsáno v závorkách za makrem. Pokud tedy zadáte do šablony {test my macro!} bude v args uložen řetězec „my macro!“ a {test my => array} vrátí řetězec „my => array„.

To znamená, že pokud do parametru makra zadáte pole, proměnnou nebo volání funkce, bude v $node->args uložen text definující pole, jméno proměnné nebo volání funkce. Pak nemůžete použít výše uvedený postup, ale musíte hodnotu vložit do kódu pomocí $writer, jak je uvedeno dále:

{hello $this->getWorldName()}

Takovéhle volání ve výsledku vypíše „Hello $this->getWorldName()„, protože nedojde k zavolání metody, ale jen přenosu kódu, který to měl provést.

Makro s více parametry

Makro může mít i více parametrů a může zpracovávat i složitější kódy jako jsou podmínky, cykly, apod. Jelikož ale z $node->args získáte řetězec, budete si muset parametry sami rozparsovat:

public function include(
    $param = $node->args;
    $params = explode(' ', $param);

    if ('css' === $params[0]) {
        return "echo '/styles/$params[1].css';";
    }
    elseif ('js' === $params[0]) {
        return "echo '/js/$params[1].js';";
    }
}

Následně můžete použít makra {include css layout} (vypíše „/styles/layout.css„) nebo {include js main} (vypíše "/js/main.js").

I v tomto případě budou parametry vyhodnoceny pouze jednou a do šablony je zapíše už jen:

<?php echo '/styles/layout.css'; ?>

Makro s parametrem typu pole

Pokud chcete do makra předat pole parametrů jako {test my => macro}, musíte použít $writer pro získání daného pole. Writer ale vrací pole jako PHP řetězec, takže byste museli použít eval pro jeho zpracování:

 public function hello(
 \Latte\MacroNode $node,
 \Latte\PhpWriter $writer
) {
   eval('$params = ' 
       . $writer->formatArray() . ';');

    return "echo 'Hello {$params['my']}';";
} 

Tento postup není příliš vhodný a je lepší rovnou použít $writer pro záměnu parametrů (viz dále).

Jak správně zapsat pole do makra je uvedeno níže.

Párové makro

Kromě jednoduchého makra {hello World} můžete použít i párové makro {hello}World{/hello}. Při registraci makra je v takovém případě potřeba zaregistrovat dvě metody – jednu pro počáteční a druhou pro ukončovací makro:

$me->addMacro('hello',     //jméno makra
    array($me, 'hello'),   //otevírací metoda
    array($me, 'helloEnd') //zavírací metoda
);
public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    return "echo 'Hello '";
}
public function helloEnd(
    \Latte\MacroNode $node, 
    \Latte\PhpWriter $writer
) {
    return "echo '!';";
}

Vygenerovaný PHP kód pak bude vypadat:

<?php echo 'Hello ' ?>world<?php echo '!'; ?>

Jelikož vlastnost $node->closing obsahuje True nebo False podle toho, které makro se zpracovává, můžete dvě metody nahradit jednou:

$me->addMacro('hello',     //jméno makra
    array($me, 'hello'),   //otevírací metoda
    array($me, 'hello')    //zavírací metoda
);
public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    if ($node->closing) {
        return "echo '!';";
    } //else:
    return "echo 'Hello '";
}

Parametry

Párové makro také může mít i parametry: {hello cruel}World{/hello}. Jejich zpracování je stejné jako u jednoduchého makra a parametry jsou dostupné v obou voláních (tedy otevírací i zavírací metoda):

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    if ($node->closing) {
        return "echo '!';";
    }

    $param = $node->args;
    return "echo 'Hello $param '";
}

Makro {hello Happy}World{/hello} vypíše „Hello Happy World„.

Vlastní data

Pokud si chcete z otevírací metody předat data do uzavírací, můžete k tomu použít připravenou vlastnost $node->data, která je typu objekt:

public function hello(
    \Latte\MacroNode $node, 
    \Latte\PhpWriter $writer
) {
    if ($node->closing) {
        return "echo ' and ',
           '{$node->data->params}!';";
    }

    $node->data->params = $node->args;
    return "echo 'Hello '";
}

V tomto případě vypíše makro {hello Sun}World{/hello} řetězec „Hello World and Sun„.

Párové makro s obsahem

Pokud chcete, aby makro měnilo obsah, který je uveden mezi jeho tagy, budete muset použít output buffer:

public function test(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    if ($node->closing) {
        return "\$content = ob_get_clean();"
. "echo 'HELLO ', strtoupper(\$content), '!';";
    }
    return "ob_start();";
}

V otevírací metodě jen spustíme bufferování výstupu a v uzavírací metodě si ho uložíme do proměnné. S proměnnou pak můžeme dále pracovat před tím, než ji vypíšeme. Makro {hello}world{/hello} tak nyní vrátí „HELLO WORLD!„.

Všimněte si, že jelikož používáme dvojité uvozovky, musíme znak $ escapovat, aby se proměnná zapsala do výsledného PHP kódu místo toho, aby se ihned zpracovala!

Pozor na to, že výstup musíte nejprve uložit do proměnné a pak teprve vypisovat další obsah, který chcete skutečně zobrazit. Použitím „echo 'HELLO ', strtoupper(ob_get_clean()), '!';„; by makro vypsalo „WORLDHELLO !„, protože HELLO by se ještě zapsalo do bufferu!

Párové makro bez obsahu

I když vytvoříte makro jako párové, stále ho můžete použít jako jednoduché, nepárové makro (tedy bez vnitřního obsahu). K tomu slouží proměnná $node->empty (ve starších verzích $node->isEmpty). Pokud ji nastavíte v úvodní metodě na True, nebude již Latte hledat koncové makro (resp. pokud ho najde, vyhodí chybu).

Pozor na to, že jméno empty může být trochu zavádějící vzhledem k tomu, jak ho obvykle kontrolujete. Většinou totiž je makro prázdné, pokud jeho parametry prázdné nejsou:

$node->empty = !empty($node->args);

Upravme naše {hello} makro tak, aby šlo volat oběma výše uvedenými způsoby, tedy {hello world} a {hello}world{/hello}:

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    $params = $node->args;
    //volání s parametrem -> jednoduché makro
    if (!empty($params)) {
        $node->empty = true;
        return "echo 'Hello $params!'";
    }

    //volání jako párové s obsahem
    if ($node->closing) {
        return 'echo \'!\'';
    }

    return 'echo \'Hello \'';
}

V obou případech bude makro vracet text „Hello world!„, ale lišit se bude vygenerované PHP:

//{hello world}
<?php echo 'Hello world!' ?>

//{hello}world{/hello}
<?php echo 'Hello ' ?>world<?php echo '!' ?>

Toto je samozřejmě jen příklad, jak budete rozlišovat, jestli je makro jednoduché nebo párové záleží na situaci (např. zda uvedený parametr vyžaduje obsah nebo ne). Pozor ale na to, že uživatel vašeho makra (tedy programátor, kodér, apod.) musí být seznámen s těmito podmínkami, aby zbytečně nehledal, proč mu skáče chyba, že koncové makro je neočekávané:

{* tohle je ŠPATNÝ příklad použití makra: *}
{include jquery}           {* jquery.min.js *}
{include}main.js{/include}       {* main.js *}
{include theme}        {* jquery-ui.min.css *}
{include css}layout{/include} {* layout.css *}

Zpracování parametrů pomocí writeru

Ve výše uvedených makrech jsme parametry zpracovávali ‚ručně‘ z $node->args. Pomocí $writer si ale můžeme nechat parametry zapsat do výsledného PHP kódu bez potřeby jejich ručního zpracování:

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    return $writer->write(
        'echo \'Hello \', %node.args, \'!\';'
    );
}

Šablona pak bude obsahovat:

echo 'Hello ', 'World', '!';

Všimněte si, že $writer při použití %node.args automaticky vložil hodnotu do uvozovek tak, aby ji šlo použít přímo v PHP kódu. Proto je potřeba pro výpis do stránky přes echo řetězce rozdělit. Poznámka: tuhle větu berte s rezervou – v některých případech se mi stalo, že writer hodnotu do uvozovek nedal a musel jsem je doplnit ručně. Doporučuji zkontrolovat, jaký kód makro generuje, a podle potřeby ho upravit.

Pozor na to, že pokud makro nebude mít parametry, vygeneruje se nevalidní PHP kód! Lepší je tedy nejprve v kódu makra zkontrolovat empty($node->args) a podle toho rozhodnout, jaký kód vygenerujete!

Také pozor na to, že každé %node může být v kódu jen jednou! Pokud tedy chcete parametry použít vícekrát, musíte je uložit do proměnné:

 
public function hello( 
    \Latte\MacroNode $node, 
    \Latte\PhpWriter $writer 
) { 
    return $writer->write(
         '$p = %node.args;'
       . 'echo $p, \' Hello \', $p;' 
    ); 
} 

Zpracování parametrů jako pole

Výše jsme používali pole přes eval(), což není příliš šikovné (i když u šablon nehrozí injection nebo podobné útoky). Místo toho můžeme požádat $writer, aby nám parametry převedl na pole:

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    return $writer->write('echo \'Hello \','
        . '%node.array[\'my\'], \'!\';');
}

Díky tomu, že jsme použili %node.array budou parametry vloženy jako pole, takže makro {hello my => World} vytvoří PHP kód:

echo 'Hello ', ['my' => 'World']['my'], '!';

V tomto příkladu to vypadá podivně, ale až pole uložíte do proměnné nebo předáte do cyklu, bude to dávat větší smysl:

public function greetings(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    return $writer->write(
        'foreach (%node.array as $key => $value)'
        . '{ echo $key, \' \', $value, \'! \';};'
    );
}

Pak makro {greetings Hello => World, 'Good Bye' => Everyone} vypíše „Hello World! Good Bye Everyone!“ pomocí kódu:

foreach ([
    'Hello' => 'World', 
    'Good Bye' => 'Everyone'
] as $key => $value){
   echo $key, ' ', $value, '! ';
};

Jak správně zapsat pole do makra je uvedeno níže.

Zpracování proměnné nebo funkce v parametru

Výše jsem zmiňoval, že v $node->args a %node.args je uloženo přesně to, co zadáte do makra. To znamená, že pokud zadáte {hello $world} nebo {hello $this->getWorld()}, bude v args uloženo „$world“ nebo „$this->getWorld()“ místo výsledné hodnoty.

Pro získání hodnoty byste museli použít eval() (což nedoporučuji) a nebo správně vložit text do výsledného kódu tak, aby se vyhodnotil při zpracování šablony:

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    return $writer->write(
        'echo \'Hello \', %node.args, \'!\';'
    );
}

Šablona pak bude obsahovat:

echo 'Hello ', $this->getWorld(), '!';

První parametr jako modifikátor

Jelikož se často vytváří makra, která se mají chovat různě pro různé situace, nabízí $writer možnost použít první parametr odděleně od zbytku. Pokud v kódu makra použijete placeholder %node.word, nahradí se prvním parametrem. Následné použití %node.args nebo %node.array pak již bude bez tohoto parametru.

Např. makro {hello big world} může parametr big použít k tomu, aby zvětšil první písmeno:

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    return $writer->write(
        '$p = %node.args;'
      . 'echo \'Hello \', ' 
      . '(\'big\' === %node.word ' 
      . '? ucFirst($p) ' 
      . ': $p), \'!\';' 
    ); 
} 

Vypsaný text pak bude "Hello World!" nebo "Hello world!" podle toho, jestli zadáte big nebo něco jiného jako první slovo.

První slovo a pak pole

Pokud chcete do makra předat slovo (%node.word) a pole (%node.array), může být trochu problém správně trefit, jak parametry zapsat. Tohle jsou ty správné formáty:

{include js file => 'main.js', folder => js}
{include css, layout, 'themes/dark'}
{include 'minified js' 'jquery'}

Všimněte si, že první slovo od zbytku parametrů může oddělovat buď mezera nebo čárka. Naproti tomu položky pole musí být odděleny čárkami. Samozřejmě pokud má pole jen jednu položku (jako zde ve 3. řádku), žádná čárka v parametrech nebude.

Pokud pole obsahuje klíče, jsou odděleny šipkou (stejně jako v PHP), pokud jde o indexované (číselné) pole, jsou jen hodnoty odděleny čárkami (bez indexů). Samotné hodnoty pole mohou být uvedeny bez uvozovek, pokud neobsahují speciální znaky. Pokud ano (mezeru, tečku, lomítko, apod.), je potřeba je uvést do uvozovek. Totéž platí i pro první slovo; tedy pokud má obsahovat více slov nebo speciální znaky, musí být v uvozovkách.

Makra pro HTML atributy

Stejně, jako můžete vytvářet jednoduchá a párová makra, můžete také vytvořit makro pro HTML atributy. Například makro <p n:hello="world"> může vytvořit <p title="Hello world!">.

Makro musíte vytvořit společně s párovým, kdy metodu pro atribut zadáte jako třetí metodu (tedy 4. parametr):

$me->addMacro('hello', //jméno makra 
    array($me, 'hello'), //otevírací metoda
    array($me, 'helloEnd') //zavírací metoda 
    array($me, 'helloAttr') //atribut metoda
); 

Samotné makro pak vypadá obdobně:

public function helloAttr(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    $param = $node->args;
    return "echo ' title=\"Hello $param!\"'";
}

Vygenerovaný PHP kód pak bude vypadat:

<p<?php echo ' title="Hello world!"'; ?>>

Všimněte si, že makro vkládá svůj výstup na konec HTML tagu v kódu. Musíte tedy zajistit, že makro vygeneruje validní HTML atribut (tedy jméno="hodnota") včetně mezery na začátku a počítat s tím, že nemusí být přesně v místě, kde makro zadáte.

Pokud chcete vytvořit makro jen pro atribut bez vlastního párového makra, stačí jako druhý a třetí parametr zadat NULL.

Makra bez funkcí

Pokud máte hodně jednoduché (jedno-řádkové) makro, nemusíte pro něj vytvářet funkce, ale vystačíte si se zadáním řetězce do funkce addMacro():

$me->addMacro('title', //jméno makra 
    'echo \'<span title="\', %node.args, \'">\'', 
                        //otevírací metoda
    'echo \'</span>\''  //zavírací metoda 
    'echo \' title="\', %node.args, \'"\''
                        //atribut metoda
); 

I v tomto případě musíte zadat PHP kód (tedy s echo), můžete používat placeholdery pro %node (hodnota je zpracována pomocí $writer) a musíte správně vyřešit escapování a doplňování uvozovek podle toho, jak $writer nahradí placeholdery. U atributového makra pak nezapomeňte na úvodní mezeru.

V novějších verzích PHP (tedy 5.3+) pak můžete používat i anonymní funkce (closure), kdy místo pole nebo řetězce do funkce addMacro() zadáte přímo funkce. Tento postup ale nedoporučuji, protože při více makrech by se install metoda stala nepřehlednou.

Scope makra

I když makro vytváříte ve třídě MacroSet, vlastní makro se bude spouštět v metodě šablony (Template). To znamená, že uvnitř makra můžete používat proměnné a metody, které byste normálně používali v šabloně.

Chcete v šabloně zobrazit obsah proměnné, kterou jste vytvořili v presenteru přes $this->template nebo v šabloně přes {var}? Žádný problém, v makru bude normálně dostupná:

public function include(
    $param = $node->args;
    $params = explode(' ', $param);

    if ('css' === $params[0]) {
        return "echo \$baseUrl,"
           . "'/styles/$params[1].css';";
    }
    elseif ('js' === $params[0]) {
        return "echo \$baseUrl,"
           . "'/js/$params[1].js';";
    }
}

Všimněte si, že proměnnou $baseUrl používáme bez jakýchkoliv dalších obalů, stačí escapovat znak $, aby se proměnná vypsala do kódu makra.

Stejně tak můžete přistupovat k dalším prvkům, které jsou dostupné v šabloně přes $this; např. v Nette 2.4 můžete z makra volat metodu Presenter::link():

public function include(
    $param = $node->args;
    $params = explode(' ', $param);

    if ('css' === $params[0]) {
        return "echo \$this->global"
          . "->uiPresenter->link('Compile:css'),"
          . "'?file=params[1].css';";
    }
    elseif ('js' === $params[0]) {
        return "echo \$this->global"
           . "->uiPresenter->link('Compile:js'),"
           . "'?file=$params[1].js';";
    }
}

Proměnná $this->global->uiPresenter odkazuje na aktuální presenter.

Filtry v makru

V makru můžete použít i filtry, které jsou dostupné pod $this->filters (jejich seznam najdete ve třídě Latte\Runtime\FilterExecutor):

public function hello(
    \Latte\MacroNode $node, 
    \Latte\PhpWriter $writer 
) { 
    return "echo call_user_func("
    . "$this->filters->escapehtml, 'Hello $param!')"; 
} 

Trochu záludné je to, že filtry musíte volat přes call_user_func(), protože Nette je vrací pomocí magické metody __get() a pro přímé volání by byla potřeba __call(). Alternativně můžete filtr uložit do proměnné a pak volat proměnnou jako funkci.

Další filtry jsou dostupné ve třídě Latte\Runtime\Filters (zkráceně LR\Filters),  odkud je můžete volat přímo jako statické metody:

public function hello(
    \Latte\MacroNode $node, 
    \Latte\PhpWriter $writer 
) { 
    return "echo LR\Filters::escapeHtml("
          . ", 'Hello $param!')"; 
} 

Automatický escape filtr

Pro jednoduché escapování výstupu můžete využít placeholder z $writer:

public function hello(
    \Latte\MacroNode $node, 
    \Latte\PhpWriter $writer 
) { 
    return $writer->write("echo %escape("
          . ", 'Hello $param!')"); 
} 

Placeholder %escape je content-aware a vloží správnou funkci z LR\Filters (např. escapeHtmlText, escapeCss, atd.). Přesto doporučuji zkontrolovat, jaký filtr se vkládá, aby vám omylem do inline JS nevložil HTML escapování, protože to špatně rozpoznal.

Externí modifikátor

V makru můžete použít ještě jeden placeholder, který vloží filtr podle toho, jaký filtr byl zadán při volání makra. Například pokud naše makro {hello} zavoláte jako {hello World|escape}, můžete pak v kódu použít:

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    return $writer->write(
        'echo \'Hello \', '
      . '%modify(%node.args), \'!\';'
    );
}

Vygenerovaný kód pak bude:

echo 'Hello ',
    LR\Filters::escapeHtmlText('World'), '!';

Jak vidíte, $writer automaticky rozpoznal, jaký escape je potřeba použít a nahradil %modify() za odpovídající filtr.

Jako filtr můžete použít libovolný filtr, např. {hello $world|trim} pro odstranění mezer z proměnné, nebo {hello '<b>World</b>'|stripTags} pro odstranění HTML tagů. Placeholder %modify se již postará o to, aby se použil správný filtr na správném místě – filtr se pak neaplikuje na celý text, ale jen na vámi udanou část. Pokud při volání makra žádný modifikátor nezadáte, placeholder %modify se z kódu úplně odstraní.

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *