phpFashion

Na navigaci | Klávesové zkratky

Jak správně načítat webové fonty

Stránka by měla být čitelná ihned. Je velmi protivné, když si například v metru nemůžete přečíst článek jen kvůli tomu, že se nestihl načíst webový font.

Prohlížeče se totiž chovají tak, že text zobrazí až poté, co se font stáhne, aby zabránili tzv. Flash of Unstyled Text (FOUT), tedy nepříjemnému probliknutí jiného fontu. Problém je, že nemají žádný timeout, po jehož uplynutí by se použil alternativní font.

O tomhle tématu jsem psal už dříve a doporučoval pro mobilní zařízení webové fonty vůbec nepoužívat. Stejně krom autora grafiky to nikdy nepozná :-)

Mobilům (nebo lépe řečeno zařízením do šířky 500px) můžeme ulevit tímto způsobem:

/* font stáhneme jen na větších zařízeních */
@import "http://fonts.googleapis.com/css?family=PT+Serif" screen and (min-width: 500px);

body {
    font: 18px/1.7 Georgia, serif;
}

@media (min-width: 500px) {
    body { /* a font použijeme jen na větších zařízeních */
        font-family: 'PT Serif', Georgia, serif;
    }
}

Emulace timeoutu

Další možností je timeout implementovat pomocí JavaScriptu. Jenže zjistit, že se font načetl, není nic triviálního, dělá se to pomocí triků, jako je třeba detekce změny šířky předpřipraveného text atd. Font Loading API zatím podporuje jen Chrome. Takže lepší bude použít hotové řešení, jako je například Web Font Loader.

Web Font Loader

Knihovnu Web Font Loader vyvíjí Google společně s Typekit. Nedávno o ní psal Aleš Roubíček, takže na něj navážu a pokusím se upravit řešení tak, aby se skript načítal asynchronně a neblokoval stránku.

Do hlavičky vložte tento kód, kterým asynchronně načtete Web Font Loader a také příslušný font:

<script>
    WebFontConfig = { google: { families: ['PT+Serif:400:latin,latin-ext'] } };
</script>
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js" async defer></script>

Web Font Loader umí detekovat stavy, kdy se font načítá a kdy už je načtený, a to pomocí událostí nebo nastavováním tříd elementu <html>. Jakmile je font načtený, nastaví třídu wf-active. Zároveň řeší timeout.

Upravíme styl tak, aby se font použil teprve ve správný okamžik:

body {
    font: 18px/1.7 Georgia, serif;
}

html.wf-active body {
    font-family: 'PT Serif', Georgia, serif;
}

Tohle řešení má ale potíž. Bude docházet k FOUT, tedy k probliknutí alternativního fontu. A to vypadá velmi ošklivě.

Proto během načítání písmo skryjeme, tak jak to dělají prohlížeče standardně, bez loaderu.

html.wf-loading * {
    color: transparent !important;
}

Třídu wf-loading musíme nastavit ihned, nelze čekat, až se Web Font Loader načte. Ale zároveň ji musíme odstranit, když načtení loaderu selže. Výsledný kód vypadá takto:

<script>
    WebFontConfig = { google: { families: ['PT+Serif:400:latin,latin-ext'] } };

    var el = document.documentElement;
    el.className += ' wf-loading';
    setTimeout(function() {
        el.className = el.className.replace(/(^|\s)wf-loading(\s|$)/g, ' ');
    }, 1000);
</script>
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js" async defer></script>

Doplnění: tohle vypadá jako způsob, jak obejít jakékoliv probliknutí


Rychlejší stránky s Google Universal Analytics

Ukáži vám, jak zrychlit načítání Google Analytics nebo Web Loaderu a ještě zjednodušit měřící kód.

Tímto kódem vložíme do stránky měřící bod Google Analytics:

<script>
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

    ga('create', 'UA-XXXXX-XX', 'auto');
    ga('send', 'pageview');
</script>

Když si ho odtemníme, vypadá takto:

<script>
    (function() {
        window['GoogleAnalyticsObject'] = 'ga';
        window['ga'] = window['ga'] || function() {
            (window['ga'].q = window['ga'].q || []).push(arguments)
        }, window['ga'].l = 1 * new Date();
        var script = document.createElement('script'),
            firstScript = document.getElementsByTagName('script')[0];
        script.async = 1;
        script.src = '//www.google-analytics.com/analytics.js';
        firstScript.parentNode.insertBefore(script, firstScript)
    })();

    ga('create', 'UA-XXXXX-XX', 'auto');
    ga('send', 'pageview');
</script>

Řádek window['GoogleAnalyticsObject'] = 'ga' říká, že objekt Google Analytics bude uložen v globální proměnné ga, nicméně je zbytečný, protože ga je výchozí hodnota.

Dále následuje vytvoření objektu v proměnné ga, který představuje jen dočasný zásobník, kam se ukládá sekvence volání jednotlivých příkazů, a po načtení skriptu bude nahrazen skutečným objektem Google Analytics. Uvedený kód se dá výrazně zjednodušit vypuštěním window a pokud na stránce není víc měřících kódu, můžeme zrušit i podmínky ga = ga || ... a dostáváme se k jednoduchému:

ga = function() { ga.q.push(arguments) };
ga.q = [];
ga.l = +new Date;

Dále následuje vytvoření elementu <script> a jeho vložení do stránky. Nastavení script.async = 1 je zbytečné, protože podle HTML specifikace je každý skriptem vkládaný <script> asynchronní.

Nicméně – neexistuje žádný důvod, proč element <script> vytvářet JavaScriptem. Je mnohem výhodnější použít normální HTML. Stránka se díky tomu načte rychleji, nedochází k blokování a může se využít preload scanner v moderních prohlížečích. Tím se dostáváme k této podobě:

<script>
    ga = function() { ga.q.push(arguments) };
    ga.q = [];
    ga.l = +new Date;
    ga('create', 'UA-XXXXX-XX','auto');
    ga('send','pageview');
</script>
<script src="//www.google-analytics.com/analytics.js" async></script>

Aby se kód načítal asynchronně i v prohlížečích IE 5.5 – 9, je nutné kromě atributu async přidat ještě defer.

V dnešní době se relativní URL //www.google-analytics.com dá považovat za antipattern, vhodnější je vždy používat https.

Finální podoba

Konečná podoba včetně minifikace vypadá takto:

<script>
    ga=function(){ga.q.push(arguments)};ga.q=[];ga.l=+new Date;
    ga('create','UA-XXXXX-XX','auto');ga('send','pageview');
</script>
<script src="https://www.google-analytics.com/analytics.js" async defer></script>

Rychlejší, kratší a ještě navíc hezčí. Nechť slouží :-)

Web Font Loader

Podobným způsobem je vhodné načítat i Web Font Loader, tj. místo v dokumentaci uvedeného

<script>
   WebFontConfig = {
      typekit: { id: 'xxxxxx' }
   };

   (function(d) {
      var wf = d.createElement('script'), s = d.scripts[0];
      wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js';
      s.parentNode.insertBefore(wf, s);
   })(document);
</script>

používejte

<script>
   WebFontConfig = {
      typekit: { id: 'xxxxxx' }
   };
</script>
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js" async defer></script>

Google Tag Manager

Protože adresa skriptu se v kódu Tag Manageru skládá, uvedenou optimalizaci nelze bohužel použít.


Chrome a nekonečné přesměrování

Chrome 44 (beta) odesílá nově hlavičku HTTPS: 1, která může způsobovat problémy.

Na některých hostinzích (z těch co používám třeba WebSupport už to opravili) si pak PHP myslí, že požadavek je pod šifrovaným spojením HTTPS. Tj. proměnná $_SERVER['HTTPS'] === 'on'.

U aplikací v Nette, které neběží pod https, to pak způsobí nekonečný redirect. Aplikace si prostě myslí, že k ní přistupujete přes URL https://example.com a přesměrovává na http://example.com.

Můžete to vyzkoušet z příkazové řádky pomocí:

curl -I --header "HTTPS: 1" http://example.com`

Že je hlavička HTTPS: 1 problematická se už ví, takže je možné, že se změní a do Chrome nedostane. Každopádně jako rychlý workaround, aby nedocházelo ke smyčce přesměrování v betaverzi Chrome, je přidat na začátek bootstrap.php:

unset($_SERVER['HTTPS']);

Zároveň je dobré si uvědomit, že na některých hostinzích lze detekci šifrovaného spojení velmi snadno ošálit.


Jak se opouští PHP 5.3

PHP 5.3 je stále nejpoužívanější verzí PHP. Nicméně jde o verzi nepodporovanou, nevycházejí už ani záplaty na závažné bezpečnostní díry. Podle trendů se zdá, že verze 5.4 by ji mohla vystřídat po prázdninách. Ironií je, že v ten moment i jí skončí podpora.

Jisté je, že v době vydání další verze Nette bude PHP 5.3 dávno passé, tudíž jsem v masteru změnil minimální požadovanou verzi z PHP 5.3.1 na PHP 5.4.4.

Právě tohle setinkové číslo jsem zvolil s ohledem na Debian 7.0 Wheezy.

Opuštění větve 5.3 znamenalo řadu spíš kosmetických změn (DI, Tracy), odstranění drobného počtu workaroundů (magic quotes u Http, Reflection, Utils) a výjimečně i užití nových tříd (Finder nebo Component-Model). Což potvrzuje, že verze PHP 5.3 byla skutečně hodně dobrá.

PHP 5.4 zároveň skýtá nové možnosti, první implementovanou byla žádaná serializace objektu DateTime do JSON. Dále se nabízí možnost nahradit Nette\Object za trait atd.

Na co jsem se ale hodně těšil byla normální syntax pro zápis polí pomocí [] namísto šíleného array(). A také možnost používat v šablonách <?= místo dlouhého <?php echo.

S jejich nasazením v masteru jsem ale váhal, protože by to udělalo z cherry-pickování do starších větví peklo. Nicméně verze Nette 2.1 a od května i 2.2 jsou ve stádiu, kdy se opravují jen závažné chyby, které se v Nette objevují jen zřídkakdy, a v aktuální verzi 2.3 se nové komity objevují jen svátečně, tudíž jsem si řekl, že netřeba to odkládat a všechny repozitáře převedl na hezčí syntax.

(Rebasování větví byl docela oříšek, rozepsal jsem se o tom včera. Takový převod tabulátorů na mezery by musel být hotové peklo.)

Nástroj Code Checker nyní kontroluje, aby se v repozitářích používala výhradně nová syntax a nedocházelo k míchání obou zápisů polí.

Přechod na PHP 5.5 by přinesl taky jednu byť drobnou syntaktickou změnu: konstantu ::class. A mohly by se použít nové funkce v Nette\Utils\Image a případně Nette\Security\Password, typ DateTime by se v typehintech nahradil za DateTimeInterface.

Přechod na PHP 5.6 by přinesl zejména nahrazení většiny func_get_args() & call_user_func_array() za trojtečky (včetně operátoru (expand) v Latte). V pár případech by se dalo využít i konstant obsahujících pole.

A přechod na PHP 7 by byla revoluce.


Jak na převod array() na [] v Gitu

Jak jsem v projektech, které používají PHP 5.4 a vyšší, převáděl starou syntaxi pro zápis polí array() na novou [].

Samotný převod PHP souborů je úplně jednoduchý. Stačí použít PHP 5.4 Short Arrays Converter a v repozitáři zavolat:

php /php54-arrays/convert.php .

Nástroj zamění syntax ve všech souborech *.php a *.phpt v aktuálním adresáři a také ve všech podadresářích. Změněné soubory jsem pak komitnul (příklad).

Oříšek je ale rebasování dalších větví na takto změněný master.

Nakonec jsem na to šel přes filtry. Ale plně zautomatizovat se mi to nepovedlo.

Nejprve je vhodné všechny větve rebasovat na master těsně před samotnou změnou syntaxe.

Poté jsem si vytvořil filtr nazvaný phparray, který bude on-the-fly překládat v PHP souborech [] na array() při checkoutu a obráceně při komitování. Tedy aby slučování probíhalo při použití staré syntaxe, ale komitnulo se s novou.

Filtr se vytvoří v souboru .git/config, v mém případě to vypadalo takto:

[filter "phparray"]
    clean = c:/php/php.exe w:/php54-arrays/convert.php
    smudge = c:/php/php.exe w:/php54-arrays/convert.php -r
    required

Aby se filtr při slučování používal (ale i při každém checkoutu, cherry-picku atd), je nutné doplnit do .git/config ještě následující:

[merge]
    renormalize = true

Filtr se bude aplikovat na soubory *.php a *.phpt, což se definuje v souboru .git/info/attributes (nepoužívejte .gitattributes, protože jde o dočasnou záležitost a nechceme ji komitovat):

*.php filter=phparray
*.phpt filter=phparray

Teď by ve větvi mělo fungovat git rebase master. Bez konfliktů, které by bylo nutné ručně řešit. Jenže ouha, konflikty se mi vytvářely (na Windows; je možné, že na Linuxu to půjde) a když jsem je chtěl řešit v TortoiseGit, objevovala se hláška, že na souboru .git/index.lock je zámek atd. Zkoušel jsem experimentovat s dalšími nastaveními pro merge, ale bez výsledku.

Zkusil jsem dělat rebase ručně: což znamená nejprve větev resetnout na master (git reset master --hard) a pak jednotlivé komity přidávat pomocí git cherry-pick <hash>. Nedělal jsem to z příkazové řádky, ale pomocí TortoiseGit. I nadále mi hlásil, že některé soubory neumí automaticky sloučit a je vyžadován ruční zásah, ale vždy stačilo soubor rozkliknout do TortoiseGitMerge a rovnou stisknout Mark as resolved.

Bylo to otravné, ale fungovalo to a postupně jsem „rebasoval“ všechny větve. Poté jsem filtr z .git/config a .git/info/attributes smazal.

Proč to nešlo úplně automaticky, nemám páru, nicméně bylo snazší hodinu klikat, než dva dny studovat Git.


PHP 5.4 Short Arrays Converter

Command-line script to convert between array() and PHP 5.4's short syntax []. It uses native PHP tokenizer, so conversion is safe. The script was successfully tested against thousands of PHP files.

Download from GitHub

To convert all *.php and *.phpt files in whole directory recursively or to convert a single file use:

php convert.php <directory | file>

To convert source code from STDIN and print the output to STDOUT use:

php convert.php < input.php > output.php

To convert short syntax [] to older long syntax array() use option --reverse:

php convert.php --reverse [<directory | file>]

Framework je přežitek

Způsob, jak se vyvíjejí aplikace v PHP, se v posledních 5 letech dramaticky proměnil. Nejprve jsme opouštěli čisté PHP a učili se používat frameworky, později přišel Composer a s ním instalace knihoven z příkazové řádky a nyní nastává konec frameworků, jak je známe.

Monolitické frameworky se postupně rozpadají do samostatných (decoupled) komponent. A to přináší řadu výhod. Zatímco dříve bylo použití jen jedné části frameworku obtížné až nemožné, dnes si prostě nainstalujete jeho komponentu. Vývojový cyklus jednotlivých komponent může mít různé tempo. Mají vlastní repozitáře, issue trackery, mohou mít vlastní vývojářské týmy.

Komponenty můžete aktualizovat na nové verze průběžně, bez čekání, než vyjde další verze celého frameworku. Nebo naopak se můžete rozhodnout určitou komponentu neaktualizovat, třeba kvůli BC breaku.

Význam slova framework se tak posouvá, o verzích už takřka nelze hovořit. Místo frameworku XYZ ve verzi 2.3.1 používáte sadu komponent v různých verzích, které spolu fungují.

Rozdělení frameworku na komponenty je dost složité. Nette to trvalo 2 roky a hotovo bylo loni. Naprostou nutností bylo prosazení se Composeru a také důsledné používání dependency injection. Nette dnes tvoří přes 20 samostatných repozitářů a v tom původním zbyla jen jediná třída.

Všechny významné frameworky, jako Symfony, Zend, Laravel nebo CakePHP, jsou rozčleněné do komponent, byť k dotažení chybí ještě jeden krok: rozdělení do samostatných repozitářů (namísto náhražky v podobě Git subtree split). Zend slibuje, že s tím přijde ve verzi 2.5, uvidíme, co Symfony.

Komponování Nette

Dlouhým úvodem jsem se vás snažil přivést na myšlenku, že dívat se na Nette jako na framework v nějaké konkrétní verzi je překonané. Že šikovnější je k němu přistupovat jako k sadě komponent.

Tj. namísto uvádění závislosti na nette/nette uvádět závislosti na konkrétních komponentách. Tak to nyní dělá i Sandbox. Jako základ budoucí aplikace může posloužit i Nette Web Project, což je úplně minimalistická obdoba Sandboxu. Stáhněte jej pomocí

composer create-project nette/web-project

a vyhoďte z composer.json komponenty, které nepotřebujete. Zrychlíte tak operace Composeru.

Rychleji se k vám dostanou i bugfixy. Po opravení chyby lze hned u příslušné komponenty tagnout novou verzi, zatímco cyklus uvolňování verzí celého frameworku je mnohem pomalejší.

Pokud tvoříte doplňky pro Nette, tak vůbec neváhejte a hned nahraďte závislost na nette/nette výčtem skutečně požadovaných komponent.

Samozřejmě i nadále budou vycházet nové verze frameworku jako doposud, bude fungovat require nette/nette a pro verzi 2.3 budou vycházet i distribuce v archivech ZIP. Ale jejich význam bude pomalu upadat.


Nette je 3. nejpopulárnější framework!

Nette se v anketě Best PHP Framework for 2015 pořádané magazínem SitePoint umístilo na úžasném 3. místě. Moc děkuji všem za hlasy, tak skvělý výsledek jsem opravdu nečekal.

Těší mě na tom, že uživatelé jsou s Nette nejspíš spokojení, jinak by asi hlasy neposílali. A také samozřejmě fakt, že Nette tím na sebe upozornilo ve světě, kde se o něm kvůli jazykové bariéře tolik neví.

Na výsledcích je zajímavé i to, že si člověk uvědomí, jak velké množství PHP frameworků se reálně používá, že existují i další populární „lokální frameworky“ a že je tu stále dost těch, kteří žádný framework nepoužívají.

před 4 měsíci v rubrice Nette | shlédnuto 4963×


Nabušené DI srdce pro vaše aplikace

Jednou z nejzajímavějších částí Nette, kterou vychvalují i uživatelé jiných frameworků, je Dependency Injection Container (dále Nette DI). Podívejte se, jak snadno jej můžete použít kdekoliv, i mimo Nette.

Mějme aplikaci pro rozesílání newsletterů. Kód jednotlivých tříd jsem zjednodušil na dřeň. Máme tu objekt představující email:

class Mail
{
    public $subject;
    public $message;
}

Někoho, kdo ho umí odeslat:

interface Mailer
{
    function send(Mail $mail, $to);
}

Přidáme podporu pro logování:

interface Logger
{
    function log($message);
}

A nakonec třídu, která rozesílání newsletterů zajišťuje:

class NewsletterManager
{
    private $mailer;
    private $logger;

    function __construct(Mailer $mailer, Logger $logger)
    {
        $this->mailer = $mailer;
        $this->logger = $logger;
    }

    function distribute(array $recipients)
    {
        $mail = new Mail;
        ...
        foreach ($recipients as $recipient) {
            $this->mailer->send($mail, $recipient);
        }
        $this->logger->log(...);
    }
}

Kód respektuje Dependency Injection, tj. že každá třída pracuje pouze s proměnnými, které jsme jí předali. Také máme možnost si Mailer i Logger implementovat po svém, třeba takto:

class SendMailMailer implements Mailer
{
    function send(Mail $mail, $to)
    {
        mail($to, $mail->subject, $mail->message);
    }
}

class FileLogger implements Logger
{
    private $file;

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

    function log($message)
    {
        file_put_contents($this->file, $message . "\n", FILE_APPEND);
    }
}

DI kontejner je nejvyšší architekt, který umí stvořit jednotlivé objekty (v terminologii DI označované jako služby) a poskládat a nakonfigurovat je přesně podle naší potřeby.

Kontejner pro naši aplikaci by mohl vypadat třeba takto:

class Container
{
    private $logger;
    private $mailer;

    function getLogger()
    {
        if (!$this->logger) {
            $this->logger = new FileLogger('log.txt');
        }
        return $this->logger;
    }

    function getMailer()
    {
        if (!$this->mailer) {
            $this->mailer = new SendMailMailer;
        }
        return $this->mailer;
    }

    function createNewsletterManager()
    {
        return new NewsletterManager($this->getMailer(), $this->getLogger());
    }
}

Implementace vypadá takto, aby:

  • se jednotlivé služby vytvářely, až když je potřeba (lazy)
  • dvojí volání createNewsletterManager využívalo stále stejný objekt loggeru a maileru

Vytvoříme instanci Container, necháme ji vyrobit managera a můžeme se pustit do spamování uživatelů newslettery:

$container = new Container;
$manager = $container->createNewsletterManager();
$manager->distribute(...);

Podstatné na Dependency Injection je, že žádná třída nemá závislost na kontejneru. Tudíž jej můžeme klidně nahradit za jiný. Třeba za kontejner, který nám vygeneruje Nette DI.

Nette DI

Nette DI je totiž generátor kontejnerů. Instruujeme ho (zpravidla) pomocí konfiguračních souborů a třeba tato konfigurace vygeneruje cca totéž, jako byla třída Container:

services:
    - FileLogger( log.txt )
    - SendMailMailer
    - NewsletterManager

Zásadní výhodou je stručnost zápisu. Navíc jednotlivým třídám můžeme přidávat další a další závislosti často bez nutnosti do konfigurace zasahovat.

Nette DI vygeneruje skutečně PHP kód kontejneru. Ten je proto extrémně rychlý, programátor přesně ví, co dělá, a může ho třeba i krokovat.

Kontejner může mít v případě velkých aplikací desetitisíce řádků a udržovat něco takového ručně by už nejspíš ani nebylo možné.

Nasazení Nette DI do naší aplikace je velmi snadné. Nejprve jej nainstalujeme Composerem (protože stahování zipů je tááák zastaralé):

composer require nette/di

Výše uvedenou konfiguraci uložíme do souboru config.neon a pomocí třídy Nette\DI\ContainerLoader vytvoříme kontejner:

$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp');
$class = $loader->load('', function($compiler) {
    $compiler->loadConfig(__DIR__ . '/config.neon');
});
$container = new $class;

a pak jej opět necháme vytvořit objekt NewsletterManager a můžeme rozesílat emaily:

$manager = $container->getByType('NewsletterManager');
$manager->distribute(['john@example.com', ...]);

Ale ještě na chvíli zpět ke ContainerLoader. Uvedený zápis je podřízen jediné věci: rychlosti. Kontejner se vygeneruje jednou, jeho kód se zapíše do cache (adresář __DIR__ . '/temp') a při dalších požadavcích se už jen odsud načítá. Proto je načítání konfigurace umístěno do closure v metodě $loader->load().

Během vývoje je užitečné aktivovat auto-refresh mód, kdy se kontejner automaticky přegeneruje, pokud dojde ke změně jakékoliv třídy nebo konfiguračního souboru. Stačí v konstruktoru ContainerLoader uvést jako druhý argument TRUE.

Jak vidíte, použití Nette DI rozhodně není limitované na aplikace psané v Nette, můžete jej pomocí pouhých 3 řádků kódu nasadit kdekoliv. Zkuste si s ním pohrát, celý příklad je dostupný na GitHubu.


Generování odkazů kupříkladu v emailech a Nette 2.3

Od Nette 2.3 je k dispozici nástroj LinkGenerator pro vytváření odkazů bez nutnosti použití presenterů a přitom stejně pohodlně. Jak ho použít?

Ukážeme si to na příkladu třídy, která odesílá emaily. Kód emailu může vypadat nějak takto:

<title>Subject of message</title>

<p>Hello {$name} <a n:href="Homepage:">click here</a></p>

Třídě předám LinkGenerator a ta si jej uloží do proměnné $this->linkGenerator.

class MailSender
{
    /** @var Nette\Application\LinkGenerator */
    private $linkGenerator;

    function construct(Nette\Application\LinkGenerator $generator)
    {
        $this->linkGenerator = $generator;
    }

Samotné odesílání emailů bude provádět metoda sendEmail, která si vytvoří objekt Latte:

function sendEmail()
{
    $latte = new Latte\Engine;
    $latte->setTempDirectory(...);
    ...
}

U Latte je vhodné nastavit temp directory , aby se šablona s každým emailem nemusela znovu kompilovat, nicméně pokud jich posíláte jen několik denně, nebo třeba hodně, ale v jednom requestu (tj. pomocí jednoho objektu $latte), není to nutné, kompilace je blesková. Kde cestu k temp složce vzít? Můžeme si ji opět předat, ale chytřejší řešení je místo toho si předat objekt (tzv. továrničku), který dovede nakonfigurované Latte vyrobit:

/** @var Nette\Bridges\ApplicationLatte\ILatteFactory */
private $latteFactory;

// $latteFactory opět předáme přes konstruktor

function sendEmail()
{
    $latte = $this->latteFactory->create();

    // nainstalujme do $latte makra {link} a n:href
    Nette\Bridges\ApplicationLatte\UIMacros::install($latte->getCompiler());

    // a vygenerujeme HTML email
    $html = $latte->renderToString(__DIR__ . '/email.latte', [
        'name' => $order->getName(), // proměnné do šablony
        ....
    ]);

    // a odešleme jej, viz dále
}

Tady ještě jednou přeruším, protože musím zmínit ještě jednu alternativu, a to nechat si vygenerovat přímo známý objekt $template, který bude obsahovat například proměnné $basePath apod, už s nakonfigurovaným Latte. Vyměním tedy latteFactory za templateFactory:

/** @var Nette\Application\UI\ITemplateFactory */
private $templateFactory;

// $templateFactory si opět předáme přes konstruktor

function sendEmail()
{
    $template = $this->templateFactory->createTemplate();
    $template->name = $order->getName();
    $template->setFile(__DIR__ . '/email.latte');
}

Jde o obdobu použití $this->createTemplate() uvnitř presenteru či komponenty.

Zbytek metody sendEmail bude vypadat takto:

    ...
    $mail = new Nette\Mail\Message;
    $mail->addTo($order->email);
    $mail->setHtmlBody($html); // nebo setHtmlBody($template)

    $mailer->send($mail);   // $mailer si opět předáme konstruktorem
}

A teď zbývá poslední krok! Zapojit do toho LinkGenerator, ke kterému se konečně dostávám. Je to snadné, i když zatím ne intuitivní (intuitivnější API přijde s Latte 2.4). Jednoduše generátor vložte do šablony do proměnné _control.

Tedy buď:

$params = array(
    'name' => $order->getName(),
    '_control' => $this->linkGenerator,
    ...
);
$html = $latte->renderToString(__DIR__ . '/email.latte', $params);

nebo

$template->_control = $this->linkGenerator;

podle toho, zda používáte latteFactory nebo templateFactory.

O té chvíle můžete používat makro {link} nebo n:href.

Všechny odkazy se budou generovat absolutní, tedy včetně http://example.com. Pokud router používá relativní cesty, což je velmi časné, generátor bere doménu z aktuálního HTTP requestu. Ale v CLI žádný HTTP request pochopitelně neexistuje. Lze jej však podstrčit třeba v bootstrapu:

$configurator->addServices([
    'http.request' => new Nette\Http\Request(new Nette\Http\UrlScript('http://example.com')),
]);