Abstrakce nad abstrakcí

Školení, která pořádám

Zapojil jsem se do vývoje jednoho webu postaveného na Nette. Nad některými změnami, které by mi obvykle zabraly minutu, jsem strávil třeba hodinu. Aplikace je podle mě udělaná dobře, logické celky jsou oddělené, kód se neopakuje, takže v tom to není. Problém je, že kvůli několika úrovním abstrakce musím ty abstrakce nejdřív pochopit a naučit se, jak se v nich dělá to, co potřebuji. Přesně vím, co chci udělat, i jak se to ve finále udělá, ale oříšek je to přes ty abstrakce protlačit.

Příkaz po připojení k databázi

Kupříkladu jsem chtěl po připojení k databázi provést nějaký SQL příkaz. Ve starých dobrých časech jsem si ze skriptů includoval connect.inc.php, ve kterém jsem zavolal mysql_connect, za který bych si přidal to mysql_query, které jsem potřeboval. V této aplikaci je do prezenteru injectován repozitář – o to se asi nějak stará samo Nette, fajn. Repozitáře mají rodičovskou třídu, do jejího konstruktoru jsem tedy zkusil doplnit volání toho příkazu, což ale samozřejmě vedlo k tomu, že jednotlivé repozitáře příkaz volaly nezávisle na sobě a ten se tak provedl opakovaně. Konstruktor repozitářů přijímá instanci Dibi. Kde ta se bere? Mohl bych příkaz doplnit tam. Ale v kódu se přímo nikde nevytváří, asi ji zase injectuje Nette. Přímo do existujícího kódu si tedy příkaz nikam nedoplním, musím nějak přesvědčit Nette nebo Dibi, aby to udělaly za mě. Říkám si, že asi nejsem první, kdo něco takového v Dibi potřebuje. Koukám se do dokumentace, ale tam o tom nic nenacházím. V API dokumentaci projíždím seznam metod a taky tam nic nevidím. Nakonec se koukám do zdrojáku a zjišťuji, že to Dibi skutečně podporuje. Ono to je nakonec i v té API dokumentaci, ale ne moc přehledně:

Ukázka dokumentace

Pak už na první dobrou hádám, že stačí tenhle parametr přidat do sekce dibi v config.neon (syntaxe NEON je další abstrakce, ale naštěstí jednoduchá) a kód skutečně funguje.

Co když by Dibi něco takového nepodporovalo? Jsem jeden z těch, kdo by poslal pull request, ale co když by ho autor nepřijal? Tuším, že i to by se dalo vyřešit. V Nette bych si jistě mohl udělat nějakou DibiFactory, která by ten příkaz zavolala tam. Ale je to další abstrakce, kterou bych si musel nejdřív nastudovat.

Doplňování jména

V jednom formuláři je políčko name, které značí název místa, a políčko email, do kterého se zadává e-mail člověka, který název zadává. Chrome (a asi i další prohlížeče) při vyplňování name nabízí jméno člověka, což samo o sobě moc nevadí. Co je horší, že při automatickém doplnění e-mailu se přepíše i to, co je zadané v názvu. Ten je navíc o stránku výš, takže si toho člověk ani nevšimne. Můj první nápad bylo políčko prostě přejmenovat. Ve starých dobrých časech by to bylo triviální. Políčko bych změnil na <input name="place" value="<?php echo htmlspecialchars($row["name"]); ?>"> a kód, který ho zpracovává, na $row["name"] = $_POST["place"]. Přejmenovávat sloupec v databázi by bylo velmi složité, protože se v aplikaci používá na mnoha různých místech. Jak tohle udělat v aplikaci postavené nad Nette? Nepřišel jsem na to. Aplikace (nebo Nette?) automaticky provazuje formulářová políčka s daty v databázi a formulářová data zase posílá podle názvu do databáze. Asi by někam šlo doplnit tohle přejmenování, ale minimálně by to působilo jako hack.

Napadla mě jednodušší věc – políčku přidat atribut <input autocomplete>, což by v běžném formuláři byla práce na 10 sekund. Ve fóru jsem našel, jak se to dělá pro celý formulář s tím, že „pro jednotlivé inputy to není problém“. Pro mě to problém je. V dokumentaci jsem nic o nastavování vlastních atributů elementům nenašel. V API dokumentaci jsem našel metodu getControl, která vrací HTML element pro políčko. To mi přišlo dost podobné jako getElementPrototype doporučované pro nastavení autocomplete celému formuláři, tak jsem ji zkusil použít. Bohužel to ale nezabralo kvůli tomu, že metoda generuje kód pokaždé znovu a změny v dříve vygenerovaném HTML přijdou vniveč. Naštěstí jsem našel metodu setAttribute (později přejmenovanou na setHtmlAttribute), která vedla ke kýženému cíli. Povedlo by se mi to i pomocí metody getControlPrototype, která ale bohužel není v dokumentaci TextBase, kde jsem našel getControl. Je to proto, že pochází z rodičovské třídy a TextBase ji na rozdíl od getControl nepřepisuje. Navíc rozdíl mezi těmito metodami není nijak zevrubně popsán – čekal bych odkaz z jedné na druhou s vysvětlením rozdílu.

Proměnná na devu

Na vývojovém serveru jsem si chtěl nastavit nějakou proměnnou a použít ji v šabloně. Ve starých dobrých časech bych do config_local.php přidal define("MEDIA_ROOT", "https://...") a kdekoliv v kódu ji potom prostě vypsal. Jak to udělat v Nette? V config.local.neon si můžu vlastní proměnnou přidat do sekce parameters, jak ji ale pak použít v šabloně? Nepřišel jsem na to, tak jsem zvolil velmi krkolomné řešení poslání parametru do App\Model\Configuration (který se injectuje do presenterů), tam jeho uložení a následné ruční předání z prezenteru do šablony. Tuším, že tohle je úplně špatné řešení a že existuje mnohem jednodušší, ale v dokumentaci se o sekci parameters vůbec nepíše, natož aby tam bylo vysvětlené, jak ty parametry použít v jiných částech aplikace.

Závěr

Tímhle článkem rozhodně nechci říct, že abstrakce jsou špatné nebo že použité části (aplikace samotná, Nette, Dibi) nejsou dobré. Chci říct to, že každá abstrakce může něco zjednodušit, ale nejdřív se ji člověk musí naučit. Je jistě velmi pohodlné nestarat se o vytváření všech objektů použitých v aplikaci, ale pokud to nějaká abstrakce dělá za mě, tak je najednou mnohem pracnější to přizpůsobit. Stejně tak je pohodlné nemuset každé formulářové políčko vytvářet ručně, ale když ho potřebuji změnit, tak je to s abstrakcí opět složitější. Nebo vlastně ani ne, ale nejdřív tu abstrakci musím podrobně znát.

Dokumentace je v takovém případě klíčová. V uživatelské dokumentaci Dibi a Nette jsem třikrát nenašel to, co jsem potřeboval (onConnect, setAttribute, parameters). V API dokumentaci dvakrát ano, ale jednou v nepřehledné formě (onConnect) a jednou jsem sešel na scestí (getControl místo getControlPrototype). Jednu věc jsem nenašel vůbec (správné použití parameters), ale možná jen nevím, kde hledat. Na to, abych dokumentaci sám vylepšil, se necítím dost erudovaný – např. BaseControl má ještě nedokumentovanou metodu getControlPart, jaký je její smysl?

Kolem Nette je velmi aktivní komunita, takže když bych se zeptal, asi bych se dobral často k lepšímu řešení, než jaké jsem našel já. Ale už jen dobře zformulovat dotaz je těžká práce a čekat na odpověď se mi nikdy nechce – mám nějaký problém a chci ho vyřešit hned. Radši budu hodinu hledat v dokumentaci (a u toho třeba dozvím i něco dalšího) než deset minut formulovat dotaz a pak hodinu nebo den čekat na odpověď.

Jakub Vrána, Řešení problému, 9.12.2019, diskuse: 9 (nové: 0)

Diskuse

Kit:

Díky za skvělý článek, který odpovídá mým pocitům, které mívám u projektů v Nette. Pro své potřeby jsem si napsal poměrně krátkou třídu, kterou používám takto:
    $db = new MyPDO($config['MySQL']);
    $model = new Model($db);
... a mám DIC připraven. Stačí ho jen vložit do parametrů nějakého konstruktoru.

lenoch:

Díky, jsem rád, že existuje ještě někdo, kdo má podobné pocity. Není to problém Nette, které je na tom podle mě ještě dobře, ale většiny frameworků a nejen v php.

Často jsem strávil spoustu času přicházením na to, jak něco "ohnout" a "podsunout" abych donutil framework, který se urputně bránil, dělat to, co potřebuju.

Ale říkal jsem si, že možná nejsem dost intelektuálně na výši, že framework ví co dělá, když v něm něco nejde - atd.

Tím nechci říct, že frameworky jsou špatné - když děláte něco většího, tak buď nějaký použijete, nebo náhle zjistíte, že si programujete vlastní...

Svaťa:

Mám za to, že problém je filozofie Nette - hodně se toho dělá ''samo''. A když nemáte nastudováno/odprogramováno, jak toto ''samo sa to'' funguje, tak narážíte.

Další komplikací je konvence nad konfigurací. Když konvenci neznáte, nemáte se kde dočíst, jak je co nakonfigurováno... Teda krom zdrojáků.

Proto mám raději nástroje, které nejsou tak chytré, a dělají toho málo.

Taco:

Nebude to tím, že prostě neznáš tu filozofii? Když třeba porovnám Symfony, Nette, a Adminer - Symfony mi přijde takové už moc akademické. Nette je tak akorát, všechno mi jde na ruku, když něco nechci, nepoužiju a nepřekáží. Adminer jsem v rámci jednoho projektu chtěl vykuchat, a vzdal jsem to.

Není překvapující, že když neznáš styl a nebo dokonce máš jinej, tak to prostě drhne. IMHO ty máš hodně jinej styl než mnoho.

ikona Jakub Vrána OpenID:

Tím to podle mě není. Když chci něco udělat, tak se to v té abstrakci nejdřív musím naučit. Je celkem jedno, jaký ta abstrakce má styl, naučit se to musím tak jako tak.

David Grudl:

když technologie ujde dlouhý vývoj v tom, jak vytvořit lepší abstrakci, a následně dlouhý vývoj v tom, jak výslednou abstrakci maximálně zjednodušit, tak se stává pro člověka, který tyto dva body přeskočil, hodně nesrozumitelnou.

Zásadní koncepce, na které stojí dnešní vývoj, spočívá v tom, že žádný objekt nebo funkce nesahá na data mimo sebe. Protože jinak vzniknou skryté vazby, které nelze zpětně dohledat a které udělají z vývoje v určité fázi peklo. Takovými daty mohou být parametry, spojení s databází, ale vlastně i konstanty. Nesahat na tyto data znamená obrovský kotrmelec v tom, jak navrhovat aplikace. Pak přijde framework, který se snaží ruční předávání co nejvíce zjednodušit a automatizovat, a člověk který tyhle abstrakce nezná, je prostě ztracený.

Dá se to samozřejmě obcházet nebo kombinovat se „starým dobrým“ přístupem, ale lepší je fakt do té abstrakce proniknout.

Jen bodově k tomu na co jsi narazil:
- chtěl jsi po připojení k databázi provést nějaký SQL příkaz: na to je onConnect asi ideál, nicméně jsem netušil, že tam něco takového je, takže sám bych buď přidal setup do služby https://doc.nette.org/cs/3.0/configuring#toc-low-level-upravy nebo si vyrobil továrničku

- na přidání HTML atributů slouží metoda setHtmlAttribute(). Je to v dokumentaci věnující se vykreslování, což možná nemusí být zřejmé https://doc.nette.org/cs/3.0/form-rendering#toc-html-atributy. Ty ostatní metody jsou spíš interní.

- změnit name u políčka: to člověče nikdo za 10 let nepotřeboval :-) Ale že jsi to ty, poslal jsem commit a půjde to udělat přes setHtmlAttribute('name', '...')

- sekce parameters (v dokumentaci viz https://doc.nette.org/cs/3.0/di-configuration) funguje jednosměrně, tj. parametry můžeš posílat do jednotlivých služeb, ale nikdo jiný si na ně sáhnout nemůže. Viz koncepce výše. Takže myslím, že řešení přes App\Model\Configuration zní dobře.

ikona Jakub Vrána OpenID:

- V tom https://doc.nette.org/cs/3.0/configuring#toc-low-level-upravy, co je @resource? Jak to mám zjistit?

- Interní metody by měly být jako interní označené a mohlo by v jejich dokumentaci být uvedeno, co použít místo nich.

- Všiml jsem si anotace @inject a hledal jsem něco podobného pro pojmenované parametry (obdoba https://github.com/google/guice/wiki/BindingAnnotations#named). To jsem ale nenašel, přitom se mi to zdá šikovné.

David Grudl:

Co jsou to pojmenované parametry? Co je to za jméno v tom @Named?

ikona Jakub Vrána OpenID:

Cokoliv si tam vymyslíš. Na jednom místě svážeš String anotovaný třeba @Named("MediaRoot") s nějakou hodnotou a na druhém místě to pak injectuješ.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2020 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.