phpFashion

Na navigaci | Klávesové zkratky

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')),
]);

Nefunguje ti flexbox na iPadu, co?

Jak zprovoznit CSS Flexbox na iOS zařízeních aneb pár poznámek pro sebe, až zase příště budu zoufat, proč to neflexí, a tápat v paměti, jak jsem to minule vyřešil.

Prefixy

Safari stále ještě, i ve verzi 8.1, vyžaduje pro Flexible Box prefixy. Jsi opět překvapený, že? Takže tam hezky doplň display: -webkit-flex nebo -webkit-flex-wrap: wrap atd.

Pořadí

Záleží na pořadí deklarací! Tohle funguje:

-webkit-flex-wrap: wrap;
-webkit-justify-content: space-between;
flex-warp: wrap;
justify-content: space-between;
display: -webkit-flex;
display: flex;

Zatímco tohle pořadí, které se ti líbí více, na iPhone a iPadu vůbec neflexí:

display: -webkit-flex;
-webkit-flex-wrap: wrap;
-webkit-justify-content: space-between;

display: flex;
flex-wrap: wrap;
justify-content: space-between;

Jak udělat mezeru mezi prvky?

Řešíš, jak zajistit minimální mezeru mezi prvky v natahovacím kontejneru s justify-content: space-between? Vždycky nad tím dlouze dumáš, googlíš to a nikdy jsi nic nevygooglil.

Mezera má být natahovací, ale nesmí jít pod určité minimum. A krajní prvky musí přiléhat ke kraji.

Můžeš prvkům nastavit margin-right: x a kontejneru margin-right: -x, ale to trošku rozhodí layout a na mobilu půjde stránku horizontálně posouvat. Třeba ti někdo poradí něco lepšího… Řešením je obalit kontejner do prvku s overflow: hidden.


Nette 2.3 bude trošku citlivka

Nette ve verzi 2.3 bude case sensitive. Proč a co to konkrétně znamená?

Case sensitivity, anglicky „citlivost na velikost písmen“, znamená, že například Homepage a homepage jsou dva naprosto rozdílné řetězce. PHP je citlivé na velikost písmen u proměnných, ignoruje ji u tříd a metod. JavaScript, C#, Ruby nebo XML jsou citlivé u všech identifikátorů, naopak HTML je case insensitive. Souborový systém na Linuxu je case sensitive, zatímco na Windows a Macu nikoliv. Atd.

Začínal jsem na jazycích, které nebyly na velikost písmen citlivé, a připadalo mi šílené, že některé jiné jsou. „To si jako u každého identifikátoru musím pamatovat, které písmenko je malé a které velké?“. Tohle byla lichá obava, funguje to stejně jako v přirozeném jazyce, prostě stačí znát, že vlastní jména se píší s velkým počátečním písmenem, nikoliv to, že jeden konkrétní člověk se jmenuje Franta a ne franta.

Nicméně mám rád pravidlo „buď velkorysý v tom, co přijímáš, a striktní v tom, co odesíláš“ a protože striktní lpění na citlivosti může být až komické (Nabla je automat), Nette bylo vůči velikosti písmen tolerantní.

Konkrétně šlo o

  • jména presenterů
  • některé parametry v cestě URL
  • názvy tříd u autoloadingu a DI kontejneru

Být tolerantní v prostředí, které je na velikost písmen citlivé, ba co hůř, které je citlivé v závislosti na operačním systému apod, přináší řadu úskalí. Například URL lišící se jen velikostí písmen mohou mít zcela odlišný obsah, atd. A přitom vlastně jen tolerujete, že někdo píše jak motejlek.

Proto od verze 2.3 bude Nette na velikost písmen citlivé.

A protože se Nette vždy snaží o co nejbezproblémovější přechod, bude chyby ve velikosti písmen detekovat a upozorní na ně.


Jak je to s release managementem Nette

Občas dostávám otázky, proč Nette nemá přesný harmonogram vydávání verzí, kde by bylo uvedeno, kdy přesně vyjde příští verze a jak dlouho bude podporovaná. Pokusím se vysvětlit, jak to v Nette funguje.

Podle mého je ideální vydávat ročně jednu až dvě nové větší verze. Rychlejší a zejména pomalejší vydávání způsobuje různé komplikace, a ony 1–2 verze ročně se ukázaly jako ideál.

Zároveň přibližně každých 6 týdnů vydávám opravné setinkové verze. Někdy je interval kratší, protože se objeví chyba, kterou je třeba opravit co nejdřív, jindy je delší, protože k vydání verze není důvod.

Podporu pro každou verzi se snažím držet dlouho:

  • Nette 0.9 vyšlo 8/2009, poslední 0.9.7 vyšla 1/2012 (2,5 roku)
  • Nette 2.0 vyšlo 2/2012, poslední 2.0.18 vyšla 12/2014 (2,5 roku)
  • Nette 2.1 vyšlo 12/2013, aktuální 2.1.9 uvádí, že podpora bude trvat do 1/2016 (2,1 roku)

Podpora tedy vždy trvá minimálně 2 roky. Pravdou je, že tato informace nebyla explicitně na webu zmíněna, proto jsem ji doplnil na stránku download.

Související otázkou je i podpora konkrétní verze PHP. Před nějakou dobou jsem sliboval, že PHP 5.3 bude podporované ještě pár let, a skutečně nadcházející Nette 2.3 (vyjde asi na přelomu února a března) je stále s PHP 5.3 kompatibilní, tedy minimálně dva roky ještě bude 5.3 podporované. (Nicméně téměř jistě příští verze Nette už 5.3 opustí.)

Zpátky k harmonogramu vydávání nových verzí. Předvídatelné a pravidelné vydání nových verzí je z mého pohledu ideální, na rozdíl od kalendáře s přesnými daty, kdy která verze v budoucnu vyjde. Koneckonců ho téměř žádný jiný podobný open source projekt nemá.

Samozřejmě chápu, že je prima pocit vědět, kdy která verze vyjde, také bych rád věděl, kdy třeba vyjde PHP 7.0, ale zároveň rozumím, že nikdo z PHP mi přesné datum nedá.

Vydávání nové verze je poměrně náročný proces, do kterého zasahuje hodně faktorů, a tím hlavním je být s výsledkem spokojený a stát si za ním. Kdyby měl do toho ještě zasahovat faktor pevného data, tak to bude jen na úkor něčeho jiného. A to nechci.

Dovedu si představit typy projektů, kde naopak přesný harmonogram je výhodou, ale co funguje jinde, není nutně vhodné i pro Nette.

Doplnění: aby nedošlo k nedorozumění, je samozřejmě potřebné, aby se tým vývojářů domluvil na harmonogramu vydání příští verze, buď mezi sebou interně, nebo veřejně jako v případě PHP timeline, což je při koordinaci velkého počtu vývojářů nutnost. Článek se týkal něčeho jiného, tedy veřejných příslibů, kdy přesně v následujících letech vyjde která verze.)