phpFashion

Na navigaci | Klávesové zkratky

DI a předávání závislostí presenterům

V Nette jsme narazili na zajímavý problém, který úzce souvisí s článkem Dependency Injection a předávání závislostí a komplikacemi způsobenými dědičností.

Stěžejním bodem tvorby aplikací v Nette je vytváření vlastních presenterů, tedy potomků třídy Nette\Application\UI\Presenter (dále Presenter s velkým P). Ta má své závislosti. Byl bych rád, aby uživatelé stavěli aplikace v souladu s DI a tedy se i jejich presentery hlásily ke svým závislostem.

Jenže jak to realizovat? Možná řešení:

  1. Presenter i potomek uvedou závislosti v konstruktoru – to by bylo peklo
  2. pro každou závislost se použije samostatný setter – zbytečně moc režijního kódu
  3. použije se jedna metoda pro závislosti Presenteru a jedna pro potomka
    1. Presenter využije konstruktor, potomek metodu inject()
    2. potomek využije konstruktor, Presenter metodu inject()
    3. nebudeme do toho tahat konstruktor, použijí se jiné metody
  4. použije se třída PresenterDependencies sdružující závislosti do jedné proměnné
  5. injektování přímo do proměnných – nevím, jak pojmout

ad 1) Jak už jsem psal v odkazovaném článku, pokud by Presenter i jeho potomek pro předání závislostí použili konstruktor, musel by se ten překrývající postarat o předání všech rodičovských závislostí. Ale jakmile se rodičovské závislosti změní (refactoring Presenteru), všechny třídy se rozbijí. Neakceptovatelné.

ad 2) Zda předávat všechny závislosti jednou metodou nebo zda vytvořit metodu/injektor pro každou závislost zvlášť, je v principu jedno. Protože nemám rád, když je moc režijního kódu a zbytečně bobtná API, volím raději jednu metodu.

ad 3) Šikovně vypadají přístupy 3a a 3b, tedy použít konstruktor pro závislosti jedné strany a metodu inject() pro závislosti druhé. O tom, který způsob použít, jsme se přeli s Honzou Tichým. On preferoval způsob 3b s tím, že když už nemáme žádné ideální řešení, zvolme z možných to, které bude funkční, pokud uživatel začne věc řešit intuitivně, aniž by věděl o nějaké metodě inject(). A intuitivně použije konstruktor.

Namítal jsem, že ho tak utvrdíme v mylném dojmu, že předek, tedy třída Presenter, vlastně žádné své závislosti nemá, jelikož si programátor v konstruktoru předává jen ty své. Ale jde víceméně o banalitu.

Horší je, že konstruktor potomka je volán nad objektem, který ještě není plně inicializován, tj. nejsou mu předány závislosti rodiče. Zkrátka se volá ještě před voláním inject(). Pokud by uživatel v konstruktoru použil funkci, která nějakou takovou závislost vyžaduje (například getUser()), výsledkem bude zavádějící chybová hláška Fatal Error: Call to a member function xyz on a non-object nebo nějaká podobná. Což není dobře.

Kdyby se uživatel zeptal, jak má tedy situaci vyřešit a v konstruktoru plné inicializace docílit, odpovědí by bylo: nejde to, přesuň kód do metody startup(). (Pokud nejsem srozumitelný, podívejte se na příklad níže.) Tímto problémem řešení 3a netrpí.

ad 5) injektování přímo do proměnných s anotací @inject by bylo vůbec nejšikovnější. Naprosté minimum režijního kódu, konec problémů s dědičností. Má to ale svá úskalí: obsah public proměnných nelze kontrolovat, private proměnné nejsou součástí veřejného API (jak je plnit?) nicméně k tomuto tématu se ještě v nějakém dalším článku vrátím.

Nakonec jsme se s Vaškem Purchartem domluvili na šalamounském řešení 4, kdy se všechny závislosti budou předávat konstruktorem s tím, že závislosti třídy Presenter sbalíme do jediného objektu PresenterDependencies, ten už se bude předávat snadno a při změně závislostí Presenteru se nic nerozbije.

Pokud budu chtít do svého presenteru předat objekt ArticleFacade, bude to vypadat takto:

class ArticlePresenter extends Presenter
{
    function __construct(Nette\Application\UI\PresenterDepedencies $pd, ArticleFacade $af)
    {
        parent::__construct($pd);
        $this->articleFacade = $af;

        // nyní je objekt plně inicializován a lze volat třeba:
        if (!$this->getUser()->isLoggedIn()) { ... }
    }
}

Podstatné je, že všechny závislosti budou předány přímo v konstruktoru, objekt bude ihned inicializován a nebudou se zavádět žádné novoty v podobě metod inject. Jak Vašek psal, jde o ústupek pohodlnosti v zájmu přehlednosti: „Většinou v Nette vyhrávala vždy pohodlnost … mám za to, že v rámci plánovaných změn a refaktoringů by Nette mělo tuhle houpačku mírně zhoupnout směrem k přehlednosti za cenu absolutní pohodlnosti.“

Poznámka: aby bylo evidentní, že třída PresenterDependencies je jen workaround nedostatku v návrhu jazyka a že vlastně nemá sémantický význam v objektovém modelu, nevadilo by mi ji implementovat takto triviálně:

class PresenterDependencies
{
    private $services;

    function __construct(Application\Application $application, ..., Security\User $user)
    {
        $this->services = func_get_args();
    }

    function get()
    {
        return $this->services;
    }
}


abstract class Presenter
{
    function __construct(PresenterDependencies $pd)
    {
        list($this->application, ..., $this->user) = $pd->get();
    }

}

Konec dobrý, všechno dobré. Vašek připravil pull request a já si uvědomil…

Slepá ulička

…že tohle řešení je vlastně nejhorší ze všech. Proč?

Udělal jsem si výčet faktorů, které chci u jednotlivých řešení sledovat (pořadí nesouvisí s důležitostí):

  • pohodlnost
  • intuitivnost
  • blbuvzdornost
  • moment inicializace
  • rozšiřitelnost (dědičnost)

ad pohodlnost: Povinné předání objektu PresenterDependencies je zdánlivá drobnost, ale troufám si tvrdit, že tohle by byl důvod, proč mnozí DI nepoužijí a raději si ArticleFacade vytáhnou ze service locatoru. Uvedený type hint je nejdelší název třídy z celého frameworku a psát ho je za trest.

Představil jsem si sám sebe, jak tohle ukazuju na školení a připadal bych si jako kokot. Jako bych školil nějaký Zend. Musel bych se ptát sám sebe, proč to neděláme nějak lépe.

ad intuitivnost: Honza položil otázkou, co se stane, pokud člověk začne věc řešit intuitivně. Dopadne to špatně. Nenapadne ho, že existuje nějaké PresenterDepedencies a že je ho potřeba předat. Nepředá ho, Presenter nezíská závislosti a bude v poloinicializovaném stavu. V nečekané chvíli vyskočí nějaký divoký nicneříkající Fatal Error.

S tím velmi úzce souvisí otázka blbuvzdornosti. I když budu vědět, že musím volat parent::__construct() (vždycky volejte!), mohu na to snadno zapomenout a bude problém. V případě presenteru by bylo možné (a vhodné) implementovat kontrolu, zda byl volán, stejně jako je tomu u metody startup(), ale problém je, že řešení 4 jde této chybě naproti.

Programátor také může udělat chybu v type hintu (všimli jste si, že jsem ji v příkladu udělal?) nebo jej neuvede vůbec. Sice jsem se snažil poladit chybové hlášky DI kontejneru, ale ty ve své obecnosti nikdy nemohou být dokonalé a programátor bude stejně zmaten.

Pod momentem inicializace myslím okamžik, kdy je objekt plně inicializován. PresenterDependencies ho dokázal dostat do konstruktoru, jiné řešení tohle neumí. Nicméně výhodu v případě presenterů relativizuje fakt, že uživatelé jsou stejně zvyklí úvodní kód dávat do metody startup(), kde už objekt plně inicializován je.

Pátý a dosud opomíjený faktor hodnotí, jak se řešení vypořádá s existencí více úrovní dědičnosti. Což je v případě Nette obvyklé, tj. vytvořit si nějaký abstraktní BasePresenter a teprve od něj dědit. Co když bude mít BasePresenter také nějaké závislosti? Řešení 4 předpokládá, že bychom je také uváděli v konstruktoru třídy BasePresenter a znovu opakovali v jeho potomcích. Otravnost by se tím zvyšovala.

Ven ze slepé uličky

Udělal jsem si tabulku, která u každého řešení uvádí, jak vyhovuje jednotlivým faktorům. Jasným vítězem se ukázalo být 3c. To konstruktor vůbec nepoužívá a každá třída si své závislosti předá metodou (nejlépe finální) s názvem začínajícím na inject. (Viz implementace.)

PresenterFactory, která má za úkol vytvářet instance presenterů, postupuje tak, že po vytvoření objektu zavolá všechny jeho metody inject v pořadí od třídy Presenter k potomkům. Tedy programátor si v každé třídě vytvoří metodu nazvanou inject() nebo podobně, a v ní uvede jednotlivé závislosti. Nemusí předávat nic navíc, minimalizuje se možnost chyby. Z hlediska pohodlnosti, rozšiřitelnosti a blbuvzdornosti je tak učiněno za dost.

Typický příklad předávání závislostí do presenterů bude v Nette vypadat takto:

class BasePresenter extend Presenter
{
    function injectBase(User $user, ...)
    {
        $this->user = $user;
        ...
    }
}

class ArticlePresenter extends BasePresenter
{
    function inject(ArticleFacade $af)
    {
        $this->articleFacade = $af;
    }
}

A co z hlediska intuitivnosti? Co když uživatel použije pro předání závislostí konstruktor, protože o žádných metodách inject() neví? Nevadí, fungovat to bude. Pochopitelně v konstruktoru nebude stále možné volat např. metodu getUser(), ale tuto daň rád zaplatím.

Celá věc by šla zjednodušit injektováním přímo do proměnných, třeba časem k tomu najedeme klíč. Do té doby se mi 3c jeví nejschůdnější.


DI a předávání závislostí

Víte, že Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída otevřeně hlásí ke svým závislostem, místo toho, aby je někde pokoutně získávala. Otázka zní, jak se k nim hlásit a jak je předávat.

K předávání závislostí můžeme využít konstruktor:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
    {
        $this->httpRequest = $httpRequest;
        $this->router = $router;
        $this->session = $session;
    }

}

$foobar = new Foobar($hr, $router, $session);

Nebo metody:

class Foobar
{
    private $httpRequest, $router, $session;

    function setHttpRequest(HttpRequest $httpRequest)
    {
        $this->httpRequest = $httpRequest;
    }

    function setRouter(Router $router)
    {
        $this->router = $router;
    }

    function setSession(Session $session)
    {
        $this->session = $session;
    }
}

$foobar = new Foobar;
$foobar->setSession($session);
$foobar->setHttpRequest($hr);
$foobar->setRouter($router);

Nebo přímo naplnit jednotlivé proměnné:

class Foobar
{
    /** @var HttpRequest */
    public $httpRequest;

    /** @var Router */
    public $router;

    /** @var Session */
    public $session;
}

$foobar = new Foobar;
$foobar->session = $session;
$foobar->httpRequest = $hr;
$foobar->router = $router;

Které řešení je nejlepší? Aby článek nebyl neúměrně dlouhý, odkážu se na Předávání závislostí od Vaška Purcharta, tak si jej přečtěte, protože budu navazovat tam, kde končí.

Takže znovu: které řešení je nejlepší? Kdyby byly ekvivalentní, bylo by nejspíš to poslední, protože kód třídy je nejkratší a kratší kód minimalizuje možnost vzniku chyby a šetří čas při psaní i čtení. Nicméně řešení ekvivalentní nejsou. Jsou spíše diametrálně odlišené.

Immutability tedy neměnnost

Immutable object je objekt, který nemění svůj stav od chvíle, co byl vytvořen. Nevěřili byste, kolik problémů objektového návrhu se dá vyřešit jen tím, že se objekty stanou neměnné. Ale to je téma na jiný článek.

Prakticky vždy budeme chtít, aby závislosti objektu byly neměnné. A v tomto směru se jednotlivé varianty předávání liší. Veřejné (public) proměnné můžeme změnit kdykoliv a změnu nelze detekovat, což je zcela diskvalifikuje ze hry a dále už s touto variantou nebudu vůbec počítat. A to ani nemluvím o chybějící typové kontrole. (Viz také úvaha nad tím, jak by se dalo property injection řešit.)

Mohlo by vás napadnout nahradit public za private a vložit do nich závislosti některým z nízkoúrovňových triků (třeba pomocí reflexe), ale takové obcházení vlastností jazyka do obecných úvah o DI nepatří. Privátní proměnné nejsou součástí veřejného API třídy a nelze se jimi hlásit k závislostem. A také nehackujme jazyk, dokud to není nutné.

Neměnnost bychom si u metod mohli zajistit sami:

function setRouter(Router $router)
{
    if ($this->router) {
        throw new InvalidStateException('Router has already been set.');
    }
    $this->router = $router;
}

A protože metoda není klasický obecný setter, tj. lze ji volat jen jednou, nelze očekávat existenci getteru a můžeme její volání považovat za povinné, mohla by používat jiné názvosloví. Například prefix inject, v tomto případě injectRouter().

Vytvořili bychom tedy pro větší srozumitelnost konvenci, že závislosti předáváme metodami inject.

(Musím zdůraznit, že se bavíme o konvenci užitečné pro programátora, o žádných DI kontejnerech v článku nepadlo ani slovo. Pochopitelně by se jí dalo s úspěchem využít i v kontejnerech, nicméně je naprosto zásadní uvědomit si, co je příčinou a co důsledkem.)

Používání metod pro injektáž má svá úskalí:

  • musíme sami zajistit neměnnost
  • špatně se odhaluje okamžik, kdy jsou nastaveny všechny závislosti, abychom provedli inicializaci objektu
  • měli bychom také ověřovat, že se některé závislosti neopomněly nastavit
  • režijní kód bude poměrně ukecaný a dlouhý

Všechny tyto nedostatky řeší už z principu injektáž přes konstruktor, proto vychází jako nejvhodnější.

(…Tedy, ehm, neřeší… Ale k tomu se hnedle dostaneme.)

Konstruktor hell

Nenápadný problém předávání závislostí přes konstruktor tkví v tom, že nemáme žádné vodítko, v jakém pořadí jsou parametry uvedeny. Napadá mě snad leda řadit je abecedně (divné, co?). Pokud by dvě závislosti byly stejného typu, potom v pořadí source, destination apod.

Byť nám s tímto problémem může pomoci napovídání v IDE nebo automaticky generované kontejnery, nic to nemění na tom, že metoda s nejasnými parametry snižuje srozumitelnost kódu.

Jakožto líny člověk neoblibuji ani ty strojově se opakující přiřazování v těle konstruktoru. Jako zkratku lze použít:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
    {
        list($this->httpRequest, $this->router, $this->session) = func_get_args();
    }

}

Ale pokud by byla poslední závislost nepovinná, mohlo by to skončit u Notice: Undefined offset.

Uvažuji nad sepsáním RFC pro PHP, aby bylo možné používat zápis:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $this->httpRequest, Router $this->router, Session $this->session)
    {
    }

}

Nicméně tohle jsou jen syntaktické libůstky oproti kruciálnímu problému s dědičností.

Co se stane, když vytvoříme potomka:

class Barbar extends Foobar
{
    private $logger;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session, Logger $logger)
    {
        parent::__construct($httpRequest, $router, $session);
        $this->logger = $logger;
    }

}

Jak vidno, konstruktor potomka musí:

  • vyjmenovat závislosti rodiče
  • zavolat rodičovský konstruktor

To je v pořádku, závislosti rodiče jsou i jeho dědictvím. Jenže neexistuje mechanismus, kterým by se dalo volání rodičovského konstruktoru vynutit. Jednou z nejprotivnějších chyb se tak stane opomenutí volání parent::__construct. Takže předpoklad, že konstruktor už z principu vynucuje předání závislostí, je vlastně chybný. Konstruktor se dá snadno obejít.

Bez zajímavosti není, že zdáním je i neměnnost, protože nic nebrání zavolat na hotovém objektu $barbar->__construct(...) a protlačit mu jiné závislosti. Měl by tedy konstruktor testovat, zda není volán podruhé? Kašlete na to, konstruktor se prostě znovu volat nesmí. Otázka konvence.

Největší průšvih nastane ale ve chvíli, kdy provedu refactoring třídy Foobar, jehož důsledkem bude změna závislostí. Bude nutné přepsat konstruktory všech potomků. Jistě, je to logické, ale v praxi může jít o fatální zádrhel. Pokud například rodičem bude třída z frameworku (např. Presenter), jejíž potomky píše každý uživatel frameworku, fakticky se tak znemožní její vývoj, protože zásah do závislostí by byl kolosálním BC breakem.

Řada z výhod konstruktorové injektáže se rozplynula jak pára nad hrncem. Pokud se zdálo, že volání konstruktoru je vynuceno jazykem (silné a bezpečné), zatímco volání metod inject jen konvencí (opomenutelné), tak najednou se ukazuje, že to není zcela pravda.

Další možnosti

Možností, která částečně obchází problém konstruktoru a dědičnosti, je použití třídy FooDependencies zmíněné v článku Dependency Injection versus Service Locator:

class FoobarDependencies
{
    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
}

class Foobar
{
    function __construct(FoobarDependencies $deps)
}

class Barbar extends Foobar
{
    function __construct(FoobarDependencies $deps, Logger $logger)
    {
        parent::__construct($deps);
        $this->logger = $logger;
    }
}

Když se změní závislosti rodičovské třídy Foobar, nemusí to nutně rozbít všechny potomky, protože se předávají v jedné proměnné. Běda ale, pokud ji předat zapomenou… Navíc tento způsob vyžaduje největší množství režijního kódu (dokonce celou režijní třídu).

Nebo lze závislosti rodičovské třídy Foobar předávat metodami a konstruktor uvolnit pro potomky. Rodič by se pak fakticky inicializoval až po volání těchto metod, takže konstruktor potomka by se volal nad neinicializovaným objektem. To není dobré.

A co obráceně, závislosti rodičovské třídy Foobar předávat konstruktorem a potomka metodami? To eliminuje všechny problémy, krom toho, že se těžko odhalí okamžik, kdy jsou nastaveny všechny závislosti (kvůli inicializaci objektu) a zda jsou vůbec nastaveny.

A co kdyby se všechny závislosti potomka nastavily jedinou metodou inject()? To by nejspíš vyřešilo všechny komplikace.

Nicméně stále jde jen o dvojstupňový případ rodič – potomek. Pro každého dalšího potomka by bylo třeba přijít s novou injektovací metodou a byl by problém zajistit, aby byly volány ve správném pořadí.

Dovedu si proto představit, že by vzniklo nové čisté řešení využívající nějaké PHP magie uvnitř třídy, která by ušetřila psaní režijního kódu, elegantně exponovala závislosti a předávala je do proměnných. Ty by mohly být označené třeba anotací @inject, nicméně šlo by o anotaci určenu pro tuto vnitřní implementaci, nikoliv o hint pro DI kontejner. Efekt by to mělo ve chvíli, kdyby se z toho stala obecněji uznávaná konvence, jinak to bude jen magie.

tl;dr

Předávání závislostí různými cestami má svá úskalí. Použití metod vyžaduje velké množství režijního kódu. Není od věci tyto metody pojmenovávat jiným prefixem, než obecné settery, kupříkladu lze použít inject. Poskytne to totiž důležitou informaci pro programátora, sekundárně ji může využít i DI kontejner.

Pokud nepoužíváte dědičnost, je zpravidla nejšikovnější závislosti předat skrze konstruktor a PHP by mohlo v příštích verzích syntaxi ještě o něco zjednodušit. Pokud ale do hry vstoupí dědičnost, je najednou všechno jinak. Ukazuje se, že dokonalý obecný mechanismus asi ani neexistuje. Možná by nebylo od věci zkusit nějaký, byť za využití PHP magie, vymyslet.


Všechny části:


SASS, LESS, Stylus nebo čisté CSS? (2)

Vyplatí se používat CSS preprocesory? A pokud ano, který zvolit? Pokračuji v rozboru tří nejznámějších konkurentů.

CSS preprocesor je nástroj, který vám ze zdrojového kódu zapsaného ve vlastní syntaxi vygeneruje CSS pro prohlížeč. Mezi nejznámější patří SASS, LESS a Stylus. Mají řešit praktické nedostatky samotného CSS.

předchozí části jsme si ukázali, jak preprocesory nainstalovat. Dnes se jim podívám na zoubek.

Syntaxe

Ač jednotlivé nástroje používají odlišnou syntax, všechny rozumí klasickému CSS. To je nesmírně důležité! Můžete kopírovat existující CSS fragmenty a budou fungovat. Ačkoliv…

SASS používá dvě různé syntaxe, jedna se nazývá SASS (stejně jako preprocesor), nepoužívá středníky ani složené závorky {} a místo toho odsazuje mezerami či tabulátory:

// SASS
#main
    color: blue
    font-size: 0.3em

Druhou syntaxí je SCSS, která vypadá jako klasické CSS a měla být i plně kompatibilní (budu ji pro SASS používat v dalších příkladech). Stejnou syntax má i LESS:

// SCSS, LESS a CSS
#main {
    color: blue;
    font-size: 0.3em;
}

Stylus také rozumí CSS syntaxi, nicméně upozorňuje, že nejde o 100% kompatibilitu, především proto, že hodně bazíruje na odsazování. V jeho syntaxi lze vynechat složené závorky, středníky a dokonce i dvojtečky:

// Stylus
#main
    color blue
    font-size 0.3em

Ale pozor! Důležité je, že tyto znaky můžeme vynechat. Pokud vám zápis připadá příliš hutný, klidně si dvojtečky doplňte. Nechcete odsazovat? Vražte tam složené závorky. Velmi podobně funguje NEON. V tomto se zásadně liší od úsporné SASS syntaxe, kde vynechání závorek a středníků je povinnost. Což ji činí z praktického hlediska nepoužitelnou a chápu, proč tvůrci o kompatibilitu s CSS usilovali, ale udělali to nešťastně zavedením další, nekompatibilní, ukecané syntaxe SCSS.

Když už se totiž rozhodnu preprocesor používat, ocením, když mi zjednoduší i syntaxi (alespoň nepovinné středníky), což bohužel umí jen Stylus. Jeho syntax je nejpružnější a nejúspornější.

V kontrastu přísnosti LESS a SASS je zajímavé, že všechny tři preprocesory podporují // řádkové komentáře.

Když udělám chybu

Pokud uděláte v dokumentu chibu, preprocesory vás na ni upozorní chybovou hláškou. A v tomto směru se jednotlivé nástroje zásadně liší. Pokud třeba zapomenu ve výše uvedeném kódu uzavírací }, oznámí SASS:

Syntax error: Invalid CSS after "font-size: 0.3em;": expected "}", was ""
        on line 4 of test.scss

Stylus:

if (err) throw err;
               ^
ParseError: stdin:4
1| #main {
2|   color: blue;
3|   font-size: 0.3em;
> 4|

unexpected "eos"

a LESS:

ParseError: missing closing `}` in test.less:9:17
8
9 unexpected "eos"undefined

Nutno dodat, že LESS měl šťastnější chvilku. Pokud bych zapomněl středník, skončí to u obvyklejšího

ParseError: Syntax Error on line 2 in test.less:2:1
1 #main {
2       color: blue
3       font-size: 0.3em

Výmluvné chybové hlášky jsou pro mě důležité a v tomto směru s přehledem vede SASS. Ačkoliv se to z ukázky tak nejeví, hodně se snaží i Stylus. Běžně však špatně spočítá číslo řádku a hlásí chybu o pár řádků výš, než skutečně je. LESS se nejčastěji zmůže na nicneříkající Syntax Error on line XY.

Občas jsem narážel na chyby v samotném preprocesoru. Pokud se mi je podařilo izolovat, snažil jsem se je nahlásit (LESS, Stylus), ale ne vždy to šlo. Zde se nejvíc projevila vyspělost jednotlivých nástrojů. Zatímco nejstarší SASS se mi z rovnováhy nepodařilo vyvést nikdy, druhé dva nástroje obsahují chyb bohužel požehnaně.

Co všechno umí?

Drtivou většinou užitečných vychytávek najdeme u všech tří nástrojů, byť třeba nemusí být uvedeny v dokumentaci. V množství asi vede SASS, ale takto se to hodnotit nedá. Třeba nested properties nepotřebuji, naopak property lookup a generátor data URI ve Stylusu vypadá užitečně. Tedy do chvíle, než zjistíte, že převádí buď všechny obrázky, nebo žádný.

Nechci sepisovat výčet jednotlivých vlastností ani srovnávat jejich zápis, to najdete v dokumentaci nebo ve srovnávacím článku na NetTuts. Zaměřím se místo toho na fundamentální rozdíly a největší z nich se týkají tzv. mixinů.

Mixiny

Mixiny jsou z mého pohledu nejdůležitější vychytávkou preprocesorů, protože mění způsob, jak píšeme HTML kód. Vysvětlím. Asi nejznámější „mixin“ je tzv. clearfix, která se stará o to, aby z bloku nevytékaly plovoucí prvky. Obvykle vypadá nějak takto:

.clearfix:after {
    clear: both;
    content: ".";
    display: block;
    height: 0;
    visibility: hidden;
    font-size: 0;
}

(Poznámka: existují jednodušší i složitější varianty clearfixu. Kupodivu mně vždycky fungovalo nastavit bloku overflow: auto a clearfix jsem nepotřeboval. Ale v tuto chvíli jde jen o příklad.).

Clearfix pak aktivujeme v HTML:

<div id="content" class="clearfix">
    <div style="float: left;">....</div>
    ...
</div>

Ale to neděláš dobře, Jaromíre. Clearfix by se měl aplikovat na straně stylů, přímo pro #content, nikoliv v HTML. Jenže komu by se chtělo kazit si krásný stylopis kopírováním ošklivého hacku? Mnoho kodérů raději sáhne do HTML.

S preprocesory však netřeba nic kopírovat a vystačíme si s jedinou instrukcí. Nejprve si clearfix uložíme jako tzv. mixin a poté ho v definici #content zavoláme:

SASS:

// definice mixinu (zkráceno)
@mixin clearfix {
    &:after { clear: both; ... }
}

#content {
    @include clearfix; // vložení mixinu
}

LESS:

.clearfix() {
    &:after { clear: both; ... }
}

#content {
    .clearfix;
}

Stylus:

clearfix()
    &:after { clear: both; ... }

#content
    clearfix()

Zmizí tak nutkání zasahovat do zdrojového kódu. A to je velmi důležité.

Jak vidno, syntaxe SASS je opět nejukecanější. Za zmínku stojí, že LESS umožňuje volat jako mixin i jakoukoliv třídu nebo ID.

Parametrické mixiny

Ještě zajímavější je to ve chvíli, kdy mixinům předáme parametry:

SASS:

@mixin border-radius($arg) {
    -webkit-border-radius: $arg;
    -moz-border-radius: $arg;
    border-radius: $arg;
}

#content {
    @include border-radius(2px);
}

LESS:

.border-radius(@arg) {
    -webkit-border-radius: @arg;
    -moz-border-radius: @arg;
    border-radius: @arg;
}

#content {
    .border-radius(2px);
}

Stylus:

border-radius(arg)
    -webkit-border-radius: arg
    -moz-border-radius: arg
    border-radius: arg


#content
    border-radius(2px)

Ve Stylusu můžeme použít i zapis s dvojtečkou:

#content
    border-radius: 2px

Což je velmi zajímavé! Když totiž později vendor prefix odstraníme, nebo když naopak zjistíme, že určité vlastnosti potřebujeme vendorovou variantu dodat, nemusíme změnit ani čárku v samotném stylu. Wow.

Jenže… co když mixin zavoláme s více parametry? Například uvedeme border-radius: 2px 3px. Překvapivě Stylus bude 3px v tichosti ignorovat. Proč tomu tak je?

V CSS existují dva způsoby, jak uvést více argumentů, lišící se v oddělovači:

  • oddělené mezerama: border: 1px solid black
  • oddělené čárkama: rgb(10, 20, 30)

Totéž se rozlišuje i v preprocesorech. Ve Stylusu platí, že volání mixinu border-radius: 2px 3px je ekvivalentní k border-radius(2px, 3px) (a dává mi to smysl), obojí představuje předávní dvou argumentů. Naopak border-radius(2px 3px) bez čárky předává argument jeden. Náš mixin očekává pouze jeden parametr a druhý tiše bez jakéhokoliv varování zahodil. Oprava spočívá v použití užitečného klíčového slova arguments namísto předaného argumentu arg.

SASS a LESS naopak předání více argumentů, než je mixinem očekáváno, vnímají jako fatální chybu. V případě SASS to v důsledku vede k podivným blokům $shadow-1, …, $shadow-10 patrným v galerii Compass, zatímco LESS se s problémem snaží vypořádat konstrukcí when(). Nic z toho mi nepřipadá ideální.

Pokračování zase příště.


SASS, LESS, Stylus nebo čisté CSS? (1)

Už pár let jsem si pořádně nezakódoval a začalo mi to chybět. Zachtělo se mi udělat stránky podle nejnovějších trendů. Responsivní design okořeněný CSS preprocesory. Ale váhal jsem: jsou preprocesory víc, než jen chvilková móda?

CSS preprocesor je nástroj, který vám ze zdrojového kódu zapsaného ve vlastní syntaxi vygeneruje CSS pro prohlížeč. Mezi nejznámější patří SASS, LESS a Stylus.

Faktem je, že jakmile začne stylopis kynout do větších rozměrů, řada věcí se řeší dosti nepohodlně. Je třeba vynaložit úsilí, aby zůstal čitelný a srozumitelný. Aby se z něj nestal write-only soubor plný magický konstant a hacků. Spoustu těchto nešvarů preprocesory řeší, nejvíc se těším na vnořené definice, matematické výrazy, mixiny a proměnné.

Vlastně je smutné, že preprocesory musely vzniknout. Ukazuje to na zanedbanost vývoje CSS. Na druhou stranu, může z nich i těžit. Preprocesory jsou mladé projekty procházející bouřlivým vývojem, reagují na potřeby uživatelů a lze u nich, na rozdíl od standardu, tolerovat i případné nekompatibilní změny. Ve finále tak mohou ukázat směr, kterým se má vydat příští CSS.

Pokud se kódováním webů bavíte řadu let, máte vybudované konvence pomáhající nedostatky obcházet. Preprocesory pak nemusí být úplně samozřejmou volbou. Nicméně dnes je běžné používat různé CSS generátory a preprocesor nabízí čistější cestu, než copy&pastovat vygenerovaný kód.

Rád zkouším nové věci, proto jsem dal preprocesorům šanci. Který ale zvolit? Nejlepší je si všechny osahat.

Instalace

Začneme tedy rovnou instalací. Na webu SASS i LESS rychle najdete vyčerpávající postup, jak knihovny získat. SASS je napsaný v Ruby, LESS v Node.js, takže prvním krokem bude instalace překladače, což by neměl být v žádném operačním systému problém. Preprocesor pak nainstalujete příkazem:

gem install sass

resp.

npm install less

Velmi snadné, PHP může závidět.

Naopak web Stylusu selhává, snaží se mást odkazy na Github, zatímco informace, jak ho instalovat, je důmyslně skrytá kdesi vespod úvodní stránky. Vězte tedy, že Stylus je také napsán v Node.js a nainstaluje se obdobně příkazem npm install stylus.

Pozor na jednu věc, npm instaluje balíček do aktuálního adresáře, takže abyste mohli preprocesory pohodlně spouštět z příkazové řádky, musíte si cestu k lessc.cmd a stylus.cmd přidat do proměnné PATH. Ve Windows se to dělá sparťansky, takže spíš oceníte možnost nainstalovat balíčky do globálního adresáře (pomocí přepínače -g, tj. npm install -g stylus), ke kterému už cestu zaregistroval při instalaci překladač.

Příkazová řádka pro SASS a Stylus nabízí spoustu voleb, LESS umí jen konvertovat vstupní soubor do výstupního CSS. Zmátlo mě, že SASS i Stylus zavolané bez parametrů se nijak neohlásí a očekávají vstup z klávesnice. Zavolejte je tedy s parametrem -h a vypíše se nápověda všech voleb.

Vývoj a deployment

Preprocesory vyžadují, aby se mezi úpravou stylopisu a zobrazením v prohlížeči udělat jeden krok navíc: kompilace do CSS.

Tento krůček může mnohé odradit. Pokud jste zvyklí rovnou editovat CSS soubory na FTP serveru, nad preprocesorem vůbec neuvažujte. Existují sice možnosti, jak třeba z LESS generovat CSS za běhu přímo v prohlížeči, ale rozhodně nejsou určeny pro produkční nasazení.

Pokud máte deployment automatizovaný, stačí do procesu zařadit kompilaci voláním příkazové řádky a je vystaráno.

Jak řešit onen krok navíc během vývoje? Kodéra rozhodně blbnutí s příkazovou řádkou nezajímá a chce rovnou psát stylopis.

Jak jsem zmínil, LESS umí překládat stylopisy v prohlížeči, stačí tedy zalinkovat soubor less.js a můžete rovnou připojovat soubory ve formátu LESS (povšimněte hodnoty atributu rel):

<link rel="stylesheet/less" type="text/css" href="styles.less">
<script src="less.js" type="text/javascript"></script>

SASS a Stylus zase nabízejí sledovací režim, ve kterém monitorují adresář se styly ve svém formátu a při každé změně souboru je ihned překládají do CSS.

// překlad souborů z adresáře /css.sass do /css (včetně podadresářů)
sass --watch css.sass:css

// překlad souborů z adresáře /css.stylus do /css
stylus --watch css.stylus --out css

Do vygenerovaného CSS lze pomocí přepínače --line-numbers přidat pro lepší orientaci komentáře s číslem řádku zdrojového souboru. Pokud vyvíjíte ve Firefoxu, ještě užitečnější je nainstalovat FireStylus a kompilovat s přepínačem --firebug. V záložce HTML by se pak měly objevovat odkazy přímo na zdrojový soubor. Píšu měly, protože mi to nefunguje.

Všechny tři preprocesory jsou seřazeny na startovní čáře. Který z nich běží nejlépe? Pokračování příště.

(Doporučuji aktuálnější průvodce CSS preprocesory od Martina Michálka, první a druhý díl.)


Bug in IE9: twice sent cookie is removed

I found a strange bug in Internet Explorer 9, which appears in the last version 9.0.8112.16421. If the same cookie is sent twice, once with the expiration at the end of the session and once with any other expiration, IE9 will remove cookie. Webpage must have a strict doctype.

Example:

<?php

// expire at the end of the session
setcookie('test', 'value', 0);
// expire in 1 hour
setcookie('test', 'value', 3600 + time());

// switch browser and document mode to IE9
?>
<!doctype html>

Is cookie set? <?php echo isset($_COOKIE['test']) ? 'yes' : 'no' ?>

Try to open this example in IE9 and refresh: cookie will not be set.

Solution is to remove older cookies after sending new one:

$headers = array();
foreach (headers_list() as $header) {
    if (preg_match('#^Set-Cookie: .+?=#', $header, $m)) {
        $headers[$m[0]] = $header;
        header_remove('Set-Cookie'); // requires PHP 5.3 or newer
    }
}
foreach ($headers as $header) {
    header($header, FALSE);
}

Bug is fixed in Nette Framework.