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'; ?>

Tokenizer

Pro složitější makra, abyste nemuseli ručně parsovat desítky parametrů, nabízí objekt $node nástroj tokenizer (jeho kód je uložen ve třídě MacroTokens). Ten obsahuje parametry rozparsované na jednotlivé hodnoty, které nazývá words (slova), např. hodnota, proměnná, mezera, čárka, komentář atd.

Jeden parametr získáte metodou fetchWord() přičemž jako oddělovač (tedy konec parametru) se bere mezera nebo čárka. Pokud ale za prvním slovem následuje tečka nebo dvojtečka, vrátí fetchWord() vše až k čárce:

//...
$param1 = $node->tokenizer->fetchWord();
$param2 = $node->tokeniter->fetchWord();
//...
{hello world mars} //1="world", 2="mars"
{hello world . mars} //1="world . mars", 2=""
{hello world:mars} //1="world:mars", 2=""

V případě, že za slovem následuje dvojtečka, můžete zavoláním metody fetchWords() získat všechna slova v poli:

//...
$param1 = $node->tokeniter->fetchWords();
//...
{hello world:mars} //1=["world", "mars"]

Všechny slova jsou uložena v proměnné tokens, kde každé slovo je uloženo jako pole, kde nultý index je dané slovo, první index je offset jeho začátku (počínaje 0) a index 2 je jeho typ (který odpovídá konstantám ve třídě MacroTokens). Takto můžete použít vlastní způsob pro parsování parametrů.

Ještě je třetí způsob parsování a tím je metoda joinUntil($arg), která vrátí všechna slova konče zadaným oddělovačem. Alternativně můžete použít nextUntil($arg), která vrátí slova v poli.

//...
$param1 = $node->tokeniter->joinUntil(';');
//...
{hello world, mars; pluto} //1="world, mars"

Pro získání zbytku parametrů v jednom řetězci slouží metoda joinAll() (nebo nextAll()). Pozor ale, že vrací skutečně vše včetně oddělovačů:

//...
$param1 = $node->tokeniter->joinUntil(';');
$param2 = $node->tokenizer->joinAll();
//...
{hello world; mars; pluto} //1="world",
//2="; mars; pluto"

Pokud po použití joinUntil() chce přeskočit oddělovat, musíte zavolat metodu nextValue(). Metodám *Until() můžete předat více parametrů, pokud nevíte, jaký oddělovač bude následovat (např. joinUntil(';', ',') oddělí slova čárkou nebo středníkem). Metodám *All() můžete předat jeden nebo více typů slov (viz konstanty v MacroTokens) a metoda vrátí pouze daný typ:

//...
$param1 = $node->tokeniter-joinAll(1, 9, 4);
//najde čísla (4) vč. mezer a oddělovačů
//...
{hello 1 2, 3 world 5} //1="1 2, 3 "

Další metody tokenizeru už jen v rychlosti:

  • currentValue() vrátí slovo na pozici ukazatelu
  • isCurrent($arg) ověří, jestli slovo odpovídá parametru
  • isNext() ověří, že existuje další slovo
  • expectNextValue($arg) vyhodí výjimku, pokud další slovo neodpovídá parametru
  • nextValue() vrátí další slovo (a posune ukazatel)
  • reset() vrátí ukazatel na první slovo

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 musíte použít funkci var_export() pro jeho zpracování:

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    $params = var_export($writer->formatArray(), true);
    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).

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 '";
}

Dokonce ani nemusíte řešit obě situace odděleně. Pokud dopředu víte, jak bude vypadat uzavírací kód, nebo naopak otevírací kód dokážete vygenerovat až po uzavření, můžete použít připravené proměnné $node->openingCode a $node->closingCode a nemusíte již z metody nic vracet. Pozor ale na to, že kód zadaný do těchto vlastností se automaticky neobaluje do PHP tagů, takže se do šablony zapíše přesně to, co zadáte. Pokud je to tedy PHP kód, je potřeba ho ošetřit:

public function hello(
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    if ($node->closing) {
        $node->openingCode = "<?php echo 'Hello ' ?>";
        $node->closingCode = "!"; //čistý text
        return; //metoda nic nevrací
    }
    //otevírací tag také nic nevrací
}

V tomto případě ani není potřeba makro registrovat jako otevírací a stačí zadat NULL v druhém parametru:

$me->addMacro('hello',  //jméno makra
    NULL,               //žádná otevírací metoda
    array($me, 'hello') //zavírací metoda
);

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 var_export(), což není příliš šikovné. Místo toho můžeme požádat $writer, aby nám sá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, 'Goodbye' => Everyone} vypíše „Hello World! Goodbye Everyone!“ pomocí kódu:

foreach ([
        'Hello' => 'World',
        'Goodbye' => '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) anebo 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.

Stejně jako je možno otevírací a uzavírací kód uložit do proměnných v $node, můžete kód atributu uložit do $node->attrCode:

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

Pokud u atributového makra potřebujete uvést i kód pro uzavření tagu, musíte nejprve určit, že makro je párové přes $node->empty = false. Následně můžete použít speciální hodnotu $node->htmlNode->closing, která je nastavena na true, když je makro zavoláno na konci elementu (používá se tedy stejně jako $node->closing).

public function hello(  //řídí vše najednou
    \Latte\MacroNode $node,
    \Latte\PhpWriter $writer
) {
    $node->empty = false; //zapne párové makro

    if ('n:hello' === $node->getNotation()) {
        if ($node->htmlNode->closing) {
            return "echo '!';";
    } //else otevření atributu

    return "echo ' title=\"Hello\"'";
    } //else uzavření bloku

    if ($node->closing) {
        return "echo '!';";
    } //else otevření bloku

    return "echo 'Hello ';";
}

Poznámka: správně by se pro detekci atributového makra měla používat vlastnost $node->prefix, ale nezjistil jsem (ani z kódu Latte), jak vlastně funguje a kdy se nastavuje jaká hodnota, ani co hodnoty vlastně znamenají.

Alternativně můžete použít stejné vlastnosti jako pro párové makro, tedy $node->openingCode a $node->closingCode, kde otevírací kód se vytiskne před element, který má daný atribut, a zavírací kód za jeho ukončovací tag. V tomto případě ale opět nedochází k uzavření kódu do PHP bloku a kód se přímo vytiskne:

//...
$node->empty = false;
$node->openingCode = "<?php echo 'Hello ' ?>";
$node->closingCode = "!"; //čistý text
return "echo ' title=\"Hello\"';"; //atribut

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

Pokud potřebujete, můžete v $node->htmlNode ověřit další vlastnosti elementu, který vaše makro vyvolal. Např. $htmlNode->name je jméno tagu (div, span, a atd.), $htmlNode->attrs[] obsahuje všechny atributy elementu (id, class, atd.).

Více maker jednou funkcí

Jedna funkce může zpracovávat více maker, např. pokud chcete vytvořit alias. Abyste je od sebe odlišili, můžete použít vlastnost $node->name, která obsahuje jméno makra. Pro ještě lepší rozlišení použijte funkci $node->getNotation(), která vrátí jméno makra buď obalené ve složených závorkách (normální makro, např. „{hello}„) anebo začínající n: (atributové makro, např. „n:hello„).

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.

Vnořené makro

Makro může také přistupovat k vlastnostem jiného, nadřazeného párového makra. Díky odkazu $node->parentNode se můžete podívat například na typ bloku přes $parentNode->name nebo přistupovat k jeho datům přes $parentNode->data. Samozřejmě vždy byste měli ověřit, jestli vůbec nějaký nadřazený blok existuje.

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í.

Modifikátor noescape

Pozor na to, že placeholder %modify není kompatibilní s modifikátorem noescape. Pokud noescape použijete, dostanete chybu „Filter ‚noescape‘ is not defined.

Pokud potřebujete vytvořit makro, které escapuje výstup, ale za určitých okolností nikoliv, zkuste postup z výchozího makra {include} (kód najdete v CoreMacros.php):

$noEscape = Helpers::removeFilter(
$node->modifiers, 'noescape');
return $writer->write($noEscape
    ? 'echo %node.args;'
    : 'echo %modify(%escape(%node.args));'
);

Pomocná metoda removeFilter() odstraní noescape modifikátor z makra a zároveň vrátí True, pokud byl použit. Na základě toho pak můžete vrátit různý kód, který buď escapuje nebo ne.

Makro se volá vždy

Pro někoho překvapivý bude fakt, že makro se volá vždy, a to i v případě, že je uvnitř {if} nebo {switch} makra a člověk by očekával, že makra uvnitř false podmínek se nezavolají:

{if false}
    {hello World}
{/if}

Z tohohle kódu se lze domnívat, že makro {hello} se nezavolá, protože je uvnitř nesplnitelné podmínky. Nicméně, když si rozebereme, co vlastně tenhle Latte kód dělá, tak vám dojde, že makro {hello} se zavolat musí.

Makro {if} totiž nevyhodnocuje podmínku, ale jen vkládá do šablony PHP kód pro tuto podmínku. Když se podíváme na kód, který vygenerují makra {if} a {hello}, bude to snad jasné:

<?php if (false) { ?>
    <?php echo "Hello World!"; ?>
<?php } ?>

Při generování šablony a spouštění maker dojde k tomu, že makro {if false} vloží kód <?php if (false) { ?> aniž by prováděl nějaké vyhodnocení svých parametrů. Makro {hello} pak vloží kód <?php echo "Hello World!"; ?>, jak jsme si ukázali výše. Nakonec makro {/if} vloží uzavření bloku <?php } ?>.

Teprve při spuštění šablony a generování HTML stránky dojde k tomu, že se podmínka if vyhodnotí a tím se přeskočí volání echo.

Na tohle je potřeba dát velký pozor v případě, že vaše makro již při spuštění nějak modifikuje stav aplikace, zapisuje data do databáze, nebo jinak ukládá či maže data. V takovém případě totiž dojde ke změně dat vždy (a jen jednou) a ne při každém splnění podmínky.

Obdobný problém nastane s cykly jako {for} a {foreach}, protože i ty jen generují kód cyklu do šablony a makra uvnitř se provedou jen jednou.

Pokud chcete vytvořit makro, které svůj kód volá při každém spuštění šablony, je potřeba funkčnost uzavřít do funkce a následně z makra vrátit jen volání této funkce:

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

protected static $i = 0;

public static function doHello($name) {
    echo "Hello $name! for " . ++self::$i;
}

Takto upravené makro při každém zavolání inkrementuje proměnnou $i a je tak schopno zobrazit, kolikrát jste ho zavolali v rámci jedné stránky. Díky tomu také bude schopno nezapočítat volání uvnitř {if} nebo naopak opakovaně započítat volání uvnitř {for}.

3 komentáře u „Jak správně vytvořit vlastní makra v Latte“

  1. Na presenter se dostanu přes $_presenter ($this->global->uiPresenter) nefunguje, má tedy starší Nette 2.1 a 2.2 tak jestli to je tím?

    1. Ano, máte pravdu, že vrácenou hodnotu je potřeba místo exportu (převod array na string) naopak evalovat (převod string na array) na PHP kód.
      Nicméně tento příklad je uveden jako postup, který by se NEMĚL používat, takže jeho (ne)funkčnost není až takový problém (osobně ho nepoužívám a šlo spíše o teoretickou myšlenku, jak by to šlo udělat).
      Kámen úrazu leží v tom, že tento postup evaluje pole během kompilace LATTE, ale ve většině případů potřebujete pole evalovat až při runtime, takže je lepší použít postup z kapitoly Zpracování parametrů jako pole.

Napsat komentář

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

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..