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í:
Presenter i potomek uvedou závislosti v konstruktoru– to by bylo peklopro každou závislost se použije samostatný setter– zbytečně moc režijního kódu- použije se jedna metoda pro závislosti Presenteru a jedna pro potomka
- Presenter využije konstruktor, potomek metodu inject()
- potomek využije konstruktor, Presenter metodu inject()
- nebudeme do toho tahat konstruktor, použijí se jiné metody
- použije se třída PresenterDependencies sdružující závislosti do jedné proměnné
- 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ší.
Jak vypsat funkci, argumenty, proměnné?
Ladíte v prohlížeči PHP aplikaci a potřebujete ji v jednom místě snadno zastavit a přehledně zobrazit všechny důležité informace jako:
- právě volanou funkci
- argumenty, které ji byly předané
- lokální proměnné
- call stack
A občas by se hodily i globální proměnné $_SERVER, $_COOKIES, HTTP hlavičky, apod.
Jak na to?
Tohle umí Laděnka neboli Nette\Diagnostics\Debugger. Pokud používáte Nette Framework, Laděnku už máte, protože je jeho nedílnou součástí. Ostatní si mohou stáhnout přímo framework (disponuje i jednosouborovou verzí) nebo samostatnou verzi NDebugger.zip.
Laděnku nejprve aktivujeme. Pro uživatele samostatné verze stačí zavolat:
ndebug();
(Uživatelé Nette Framework, aktivujte Debugger::$strictMode =
TRUE; a zavolejte Debugger::enable().)
Nyní v místě, kde chceme program přerušit a zobrazit všechny
požadované informace, stačí napsat $stop():
public function authenticate(array $credentials)
{
list($username, $password) = $credentials;
$row = $this->users->where('username', $username)->fetch();
$stop(); // tady se program přeruší
...
Debugger vám přehledně ukáže místo, kde k přerušení došlo:

Všimněte si, že hodnotu lokální proměnné lze zobrazit tím, že na ni najedete myší. Cool!
Můžete si nastavit systém tak, aby při kliknutí na jméno souboru jej otevřel ve vašem oblíbeném editoru:

K dispozici je přehled všech proměnných, které byly funkci předány (pod záložkou Callstack):

A přehledně vypsané všechny lokální proměnné, složitější struktury můžete kliknutím rozbalit:

Samozřejmostí je celý call stack, opět s rozklikávacím pohledem do zdrojového kódu nebo předávaných proměnných. A také lze klikem na jméno souboru jej otevřít v editoru:

Níže najdete všechny informace o HTTP požadavku, počínaje hlavičkami přes cookies k GET/POST proměnným:

A nechybí ani pole $_SERVER, konfigurační direktivy,
konstanty atd:

Dokáže tohle váš framework?
Pokud ne, seznamte ho s Laděnkou.
TOHLE. KURVA. ANI. OMYLEM.
Ne každému a ne na všechno se vždy a univerzálně hodí velké frameworky!
Nadpis jsem si vypůjčil z článku Manifest miniaturního PHP, pod nějž bych se klidně elektronicky podepsal, mít elektronický podpis. Sice argument s počítáním řádků je nefér a diskutabilní, ale rozumím, co se autor snažil říct. Na Zdrojáku jsem pod něj napsal komentář, který jsem se nakonec rozhodl zvěčnit i tu na blogu:
Docela často dělám jednoduché weby, které celé napíšu v „notepadu“, a chci, aby kód neměl víc řádků, než je naprosto nezbytně nutné a nahrávat kvůli 20kB webu i se styly na hosting hafo-megabajtový framework nepřichází v úvahu už vůbec.
Ale přitom i v těch jednoduchých webech chci použít věci, co jsou vyřešené v Nette, a nechci se vzdát navyklého pohodlí. Jsem líný programátor. Z toho důvodu se dá Nette Framework používat jako micro framework.
Chtělo by to příklad. Zrovna včera jsem si redesignoval http://davidgrudl.com a dal k dispozici zdrojové kódy (odkryjte rožek vlevo nahoře), čistě pro inspiraci ostatním, jak takový microsite řeším. Celý PHP kód webu je tvořen jedním souborem index.php, který je, věřím, srozumitelný, byť pro neznalého asi méně. Zbytek jsou šablony. A framework nahraný v minifikované podobě jediného souboru, což, společně s faktem, že má cca dvojnásobnou velikost než jQuery, řeší psychologický blok „nechci nahrávat celej framework“.
Nebo příklad blogu, který najdete přímo v distribuci. Jeho zdroják tvoří taktéž pouze index.php, a to ještě s méně řádky, než v předchozím případě. Vše ostatní jsou šablony, viz https://github.com/…ta/templates.
Asi bych měl napsat, proč vlastně na pidiwebech framework používám. Tak
především si dnes neumím představit, že bych něco programoval bez Laděnky, ta mi pak na ostrém
serveru bude logovat chyby (ačkoliv u statického webu asi nebudou).
Především ale využiju šablonovací systém Latte,
protože už od 2 stránek chci mít oddělený layout a obsah, vyhovuje mi
stručná syntaxe Latte a spoléhám na automatické escapování. Využiji
i routování, protože prostý
požadavek mít URL adresy bez koncovek .php dokáže
v mod_rewrite nastavit (správně!) jen
bůh.
V prvním zmíněném webu se ještě používá kešování pro twitter feedy, na
blogu je zase využit databázový
layer. A taky SEO vychytávka Nette, která automaticky předchází
známé chybě, kdy se při stránkování pohybem vpřed a
vzad dostaneme na tutéž stránku, jen jí v URL bude navíc strašit
page=1.
A taky mi Nette zajistí, že se při chybě nikdy neobrazí programátorské chybové hlášky PHP, ale uživateli srozumitelná stránka. A ještě autoloading – už jej vnímám jako takovou samozřejmost, že bych na něj úplně zapomněl.
Samozřejmě někdy ještě přidám kontaktní formulář a nechám ho odesílat emailem. Teď si teprve uvědomuji, že vlastně využívám 90 % frameworku.
Takhle tvořím quick'n'dirty weby a takhle mě to baví ![]()
Poznámka: napsat konečně článek o tom, proč místo „osvědčeného“ Phing (2,3 MB) používám své Make (71× menší) a je to po všech stránkách lepší udělátko.
„Mám nejhoršího klienta, stále mění zadání“
Znáte ty nářky vývojářů, že jejich klient nemá jasnou představu a neustále mění zadání zakázky? To pláčou nad vlastní neschopností. Když je slyším, nejraději bych popřál nebohému klientovi lepšího dodavatele.
Klient nemá jasné zadání, protože není odborník na webdesign. Zajímalo by mě, kolik webdesignérů se vyzná v podnikání svého klienta tak dobře, že by dokázalo vytvořit precizní zadání, kdyby se karty obrátily.
Pokud klient zadání průběžně mění, znamená to, že ho projekt zajímá a baví, že nad ním neustále přemýšlí. Je pak větší pravděpodobnost, že vznikne něco skutečně užitečného. A především: bude poptávat další a další práci.
Pokud si vývojář tyto věci uvědomí, pak pochopí, že je to právě on, kdo musí přizpůsobit svůj styl práce. Třeba zjednodušit přidání kolonky PSČ na web, ač to v původním zadání nebylo.
Nové jmenné prostory v Nette
Společně s vydáním Nette Framework 2 beta dochází k úpravě jmenných prostorů a názvů tříd. Je jasné, že přejmenovávání nepatří mezi populární úpravy, nicméně snažil jsem se udělat vše pro to, aby přechod byl bezbolestný.
Jaké jsou vlastně důvody?
Nette Framework je dost možná prvním PHP frameworkem na světě, který používání jmenných prostorů zavedl. Kvůli prodlevám s vydáním PHP 5.3 se začaly v praxi používat docela pozdě a tudíž mají stále svou první a dnes již historickou podobu. Ta neodpovídá současnému stavu frameworku, navíc pojmenování tříd bylo často ovlivněno kompatibilitou s verzí PHP 5.2. Před vydáním nové verze frameworku je záhodno stav napravit.
Zpětná kompatibilita
Úpravy se týkají především verze frameworku pro PHP 5.3. Ve verzích pro 5.2 dochází jen k drobným změnám, a to proto, aby se zbytečně nerozevíraly nůžky mezi 5.3 a 5.2 verzemi.
Nejspíš by k tak radikálním krokům ani nedošlo, kdyby nebylo možné
přechod zcela zautomatizovat. K dispozici proto máte skript, který všechny
změny ve vašich zdrojových souborech udělá za vás! Najdete jej
v distribučním balíku v
tools/Code-Migration/class-updater.php.
Parametry pro spuštění:
php class-updater.php -d /cesta/k/app [-f]
kde /cesta/k/app nahradíte za skutečnou cestu ke složce s vašimi
zdrojovými soubory. Pokud neuvedete nepovinný parameter -f,
skript běží v read-only režimu a jen vypíše, které soubory by
změnil.
DŮLEŽITÉ UPOZORNĚNÍ: nezapomeňte si před použitím zazálohovat zdrojové kódy a po konverzi ověřit porovnáním, zda záměny proběhly v pořádku.
Skript vyžaduje pro spuštění PHP 5.3 a najdete jej jen v 5.3 balíku. Lze pomocí něj konvertovat všechny verze Nette Framework (tj. pro 5.3 i pro 5.2).
Pokud z nějakého důvodu nemůžete soubory aktualizovat, můžete
využít aliasování tříd. Stačí ve své aplikaci hned po načtení
frameworku načíst skript compatibility-aliases.php, který
najdete opět v tools/Code-Migration. Příklad v
bootstrap.php:
// načteme framework
require LIBS_DIR . '/Nette/loader.php';
// načteme aliasy
require LIBS_DIR . '/compatibility-aliases.php';
Nette\Debug::enable(); // fungují původní názvy tříd
Aliasování funguje pouze pro Nette Framework ve verzi pro PHP 5.3.
Pravidla pojmenování
Novou podobu názvů tříd jsem chtěl podřídit, jako v podstatě vše v Nette, pohodlí programátora. Jak ukázala řada diskusí, subjektivní požadavky mohou být rozporuplné. Pro někoho znamená pohodlí mít co nejkratší názvy tříd, pro jiného jsou to názvy co nejvíce popisné. Přehled myšlenek, ze kterých se vycházelo, najdete v článku Best practices pro jmenné prostory v PHP.
Nyní by všechny třídy měly mít výstižný název i bez uvedení jmenného prostoru, preferovány jsou kratší názvy a lze pěkně využít parciální namespaces:
use Nette\Http; // alias pro namespace
// dostupné jsou všechny třídy via Http:
$request = new Http\Request;
$response = new Http\Response;
Nová struktura pro 5.3
Nejlépe si novou strukturu můžete prohlédnout přímo v API dokumentaci. Důležité změny jsou tyto:
- výjimky, které byly bez namespace, jsou nyní
v prostoru
Nette Nette\Debugje teďNette\Diagnostics\Debugger(což je dlouhé, zvažuje se alias např. NDebug)- Presenter & Control jsou ve jmenném prostoru
Nette\Application\UI, routery vNette\Application\Routers - z AppForm se stal
Nette\Application\UI\Form String→Nette\Utils\StringsaArrayTools→Nette\Utils\ArraysFinder,Json,Neon,PaginatoraHtmljsou nyní taky vNette\Utils(třídaToolsuž není)Nette\Mail\Mail→Nette\Mail\Message- prostor
Nette\Templatespřejmenován naNette\Templating ITranslatorje nyní v prostoruNette\Localization- úložiště pro cache jsou v
Nette\Caching\Storages - low level komponenty jsou v
Nette\ComponentModel
Jak bylo vysvětleno výše, nemusíte se přechodu obávat, konverzi za vás udělá utilitka.
Nová struktura pro 5.2
Zde jsou důležité vlastně jen tyto změny:
Debug→Debugger,String→Strings,ArrayTools→Arrays,MultiRouter→RouteListaDownloadResponse→FileResponse
A pak ještě několik tříd, konverzi opět udělá utilitka.
Přechod na verzi 5.3 se jmennými prostory
PHP ve verzi 5.2 je zatím stále podporováno, ale rozhodně doporučujeme
aktualizovat na verzi pro PHP 5.3 se jmennými prostory. S migrací vám
pomůže nový nástroj tools/Code-Migration/migrate-53.php.
Příkazová řádka i všechny upozornění jsou stejné, jako v případě
nástroje class-updater.php. Enjoy!

