Jak jsme na Megapixelu přecházeli na Latte 3

Development

Ne vždy stačí k přechodu na novou verzi knihovny pouze stažení a drobné úpravy kódu. Někdy jde o změnu tak zásadní, že je nutné podstatnou část kódu přepsat – právě to je náš případ, kdy jsme přešli z šablonovacího systému Latte 2 na Latte 3. Pojďte se s námi podívat, jak probíhal přechod u tak velkého projektu jako je Megapixel.

Když na konci května 2022 vyšla nová generace tohoto šablonovacího systému, autoři ji představili jako „impozantní skok vpřed“ a nový kompilátor nazvali „raketovou vědou“. A já s nimi musím souhlasit. S příchodem Latte 3 se kompletně mění způsob průchodu šablon. Nově se používá při průchodu kódu AST strom (známá věc především z oblasti překladačů programovacích jazyků). Díky tomu může šabloně ještě více porozumět a transformovat ji způsoby, které předtím byly nemožné nebo hodně složité na implementaci. Právě tento nový způsob průchodu se odráží především ve zcela novém přístupu při vytváření rozšíření. Ale o tom až dále v textu.

E-shop Megapixel (kterému se převážně věnuji) je stejně jako většina našich projektů postavený na frameworku Nette s využitím Latte šablon. Vždy se snažíme používat ty nejmodernější technologie, a protože nechceme aby nám „ujel vlak“ – ať už z hlediska bezpečnosti nebo kvality kódu (například další velká verze Nette 4 bude vyžadovat Latte 3), rozhodli jsme se udělat tento skok vpřed co nejdříve.

Přecházíme dvakrát

Protože jde o opravdu velký skok, současně s novou verzí vyšla i takzvaná „přechodová verze“ (Latte 2.11). Ta přináší zcela nový linter (nástroj pro odhalení problémů statickou analýzou kódu), který po spuštění projde všechny šablony a upozorní na zastaralé konstrukce v kódu, které v nové verzi již nebudou fungovat – v případě velkého projektu se může jednat i o stovky drobných chyb. Nalezené problémy se v šablonách opraví, znovu se spustí linter a tímto způsobem se iteruje až do chvíle, kdy se žádné nové chyby neobjeví. S trochou nadsázky jde vlastně o takovou „whac-a-mole“ hru pro programátory – opravit všechny chyby, které z linteru vyskočí.

Nejoblíbenější programátorská hra? Plácni krtka!

Linter se nám osvědčil a opravdu značně nám pomohl objevit velkou část problémů, které by mohly s přechodem nastat. Dobrou vlastností linteru je i jeho jeho snadná rozšiřitelnost podle potřeb projektu. Může tak lépe porozumět vlastním makrům a filtrům. Po upgradu na Latte 3 jeho užitečnost nekončí – stále lze použít na odhalení chyb, které mohou vzniknout například z nepozornosti (špatný název filtru, chybějící ukončovací značka, …) a tak jsme se ho rozhodli přidat k ostatním nástrojům kontroly kódu v našem repozitáři, kde bez problému funguje a dál hlídá náš kód.

Když přepsat, tak pořádně

Přechodovou verzi jsme nasadili, potenciální chyby opravili a mysleli jsme, že máme těžkou část za sebou. To nejsložitější (a to, co nakonec zabralo poměrově nejvíce času) nás ale teprve čekalo. Kromě standardních šablon rozšiřujeme Latte v projektu o další komponenty, jako jsou filtry a makra. Díky tomu máme sjednocenou logiku pro zobrazování například vstupních prvků formuláře, obrázků nebo správné formátování cen a telefonních čísel.

Jak jsem v úvodu nastínil, kvůli úplně novému způsobu interní reprezentace šablony se i kompletně změnil způsob rozšiřování Latte o další funkce. Nejvíc se změny dotkly způsobu definice a vykreslování nových tagů, které nemají s původním přístupem skoro nic společného. Většina populárních knihoven už je v době psaní článku plně kompatibilní s Latte 3 a v těchto případech stačí pouze změnit způsob registrace rozšíření v projektu. V případě vlastních rozšíření specifických pro projekt to ale znamená, že je potřeba všechny od základu přepsat.

Sbohem makra, přicházejí extensions

Když jsem výše psal, že Latte 2 a Latte 3 nemají ve vytváření rozšíření skoro nic společného, myslel jsem to opravdu doslova. Začneme od základu – v Latte 3 už neexistuje koncept maker tak jako v Latte 2. Místo toho existují rozšíření („extensions“), které na jednom místě mohou definovat kromě vlastních tagů také filtry, funkce a providery. Tento přístup je mnohem čistší. Můžeme tak například definovat vlastní tagy a související providery na jednom místě – třeba přímo v modulu, ke kterému se rozšíření vztahuje.

Další podstatnou změnou (a vlastnosti plynoucí z použití AST) je dvojí průchod šablon. V praxi to znamená, že se nejdříve vytvoří objekt reprezentující tag (se všemi argumenty a obsahem) a teprve v druhém průchodu se generuje kód. S tím také souvisí elegantněji řešená definice nových tagů. Pryč jsou doby dlouhých souborů s mnoha definicemi tagů. Nyní je každý tag definován třídou, která si v sobě nese argumenty, obsah, filtry a obecně jeho kontext. Tento přístup má také obrovskou výhodu v úpravě existujících tagů. Je možné jen upravit část chování bez nutnosti reimplementace celého tagu. Toho jsme v praxi na projektu využili při upravení chování Nette tagu {control}. Díky novému způsobu definice tagů jsme tak mohli využít existující zpracování argumentů a změnit pouze část logiky vykreslování tagu.

Za sebe mohu říct, že nový koncept rozšíření je určitě skok správným směrem. Bohužel zatím jako velký nedostatek je absence podrobné dokumentace celého systému. Oceňuji lidsky popsané návody, které jsou na webu Latte dostupné. Ty se ale problému věnují spíše povrchně a některé věci v nich chybí – například ukázkové výstupy generovaného kódu nebo jak správně aplikovat filtry na obsah.

Protip pro ostatní, kteří začínají s vlastními Latte 3 tagy: pro pochopení chování context formatteru (funkce pro generování kódu z tagu) mi pomohly jednotkové testy Latte, kde je vidět chování při různých vstupech včetně aplikace filtru na obsah.

Zde je funkční příklad implementace Latte tagu pro zobrazení Nette flash messages. Samotné rozšíření pak stačí už jen zaregistrovat v konfiguraci projektu. ​​

Problém přepisu maker „na slepo“

Kvůli výše popsané změně koncepce rozšíření šablonovacího systému jsou všechna Latte 2 makra po povýšení na Latte 3 nefunkční (a naopak Latte 3 rozšíření nefungují na Latte 2). To představuje problém, kdy je nejdříve potřeba všechna existující makra přepsat do nového stylu rozšíření, aby bylo vůbec možné zobrazit webovou stránku. Makra se registrují z celého projektu, i když se v právě zobrazené stránce nemusí používat, a proto, pokud bychom chtěli postupně přepisovat makro po makru, bychom museli nejdříve odregistrovat všechna makra a postupně je přidávat. To není moc proveditelné, protože jsou v projektu makra, která se používají prakticky na každé stránce a celé se to začíná komplikovat. ​​

Zvolil jsem proto alternativní a trochu nestandardní cestu – využil jsem při vývoji nových rozšíření jednotkové testy. Díky jednotkovým testům bylo možné vyvíjet a ověřit chování každého vlastního tagu zcela odděleně od zbytku monolitu. Samotná workflow vypadala zhruba následovně: ​​

  1. Vzít existující makro
  2. Podívat se na jeho implementaci a analyzovat kde a jak se používá
  3. Vytvořit nový tag a kostru jednotkových testů k tomuto tagu
  4. Spustit test nad tímto tagem a zkontrolovat výstup šablony
  5. V případě nesrovnalostí upravit definici tagu a takto iterovat, dokud vše není v pořádku.
  6. Finální výstup tagu uložit k testu a smazat staré makro

…a takto postupně pro všechna vlastní makra v projektu (v našem případě jde o 23 maker). Nejde přímo o TDD (test driven development) přístup, kdy se v testech definuje chování a následně se tvoří implementace. Jde jen o jakési využití jednotkových testů jako prostředek pro rychlé zobrazení výstupu tagu. Tento přístup má samozřejmě pozitivní vedlejší efekt, kdy jsou nyní všechny tagy pokryty testy a lze tak v budoucnu detekovat nechtěnou změnu chování.​​

Blížíme se do finále

Po přepsání všech maker, registraci nových rozšíření a aktualizace externích závislostí bylo konečně možné načíst lokální verzi webu a pokračovat v hledání chyb už v prohlížeči. Díky použití nového linteru, pokrytím jednotkovými testy a existujícím Cypress testům, které hlídají nejdůležitější akce, se moc chyb neobjevilo. Šlo především o pár edge-case případů, které hlavně souvisely s fungováním nového Latte 3 kompilátoru než s refaktoringem našeho kódu. Narazil jsem i na některé (drobné) chyby přímo v Latte knihovně, které jsem nahlásil autorovi.

Zde je výpis odlišného chování od Latte 2, které se během testování a provozu objevilo (platí pro verzi 3.0.4):

  • Nelze použít unset() volání funkce v {php} bloku. Lze opravit použitím RawPhpExtension.
  • atribut n:if se v Latte 3 vykoná před atributem n:ifset, což dle mého názoru nedává smysl a jedná se tak o chybu (nezáleží ani na pořadí atributů v tagu). Lze opravit sjednocením těchto podmínek.
  • Negace u kontroly existence prvku v poli ({if ! $foo in [“hello”, “world”]}) je chybně převedena na kód – in_array(!foo, [...]) místo správného !in_array($foo, [...]). Lze opravit obalením do závorek.
  • Pokud v odkazu na signál následuje jako argument konstanta, použije se vykřičník označující Nette signál jako negace tohoto argumentu. n:href=”click! \Pd\Foo::CONSTANT” se vyhodnotí jako cesta „click“ s argumentem „!\Pd\Foo::CONSTANT“. Lze opravit přidáním čárky za vykřičník (n:href=”click!, \Pd\Foo::CONSTANT”).

1. prosince 2022, 9:06

První prosincový den, 9 hodin ráno – čas, kdy se nová verze s podporou Latte 3 nasadila na ostrý web. Než ale k tomu mohlo dojít, bylo důležité zařídit několik věcí. Tou hlavní byla nutnost interně otestovat úplně vše, co využívá Latte šablony – tedy e-shop, administrace a generátory feedů (došlo přecejen ke značnému předělání). Po testování následovalo naplánování data nasazení. Celá situace byla trochu ztížená začínající vánoční sezónou, proto jsme nasazení věnovali daleko více opatrnosti než je běžné. I když jsme nové implementaci věřili, je vždy lepší počítat s nejhorším a proto jsme byli připraveni na vrácení provedených změn v případě závažného problému. Ráno tedy proběhlo nasazení a …

Nikdo si ničeho nevšimnul…

…nic se nestalo, všechno funguje stejně jako před tím. Požadavky se vyřizují, žádné závažné chyby se neobjevily a nikdo nic nepoznal. Pro uživatele nedošlo k vůbec žádné změně. Protože jsem hned po nasazení kontroloval stav webu, zapomněl jsem informaci o proběhlém nasazení předat našemu projekťákovi, který se o hodinu později ptal, kdy bude úprava nasazena. Přechod tedy proběhl tak hladce, že si nikdo jiný této změny nevšiml. Časem se objevilo pár drobných chyb, které ale neměly na hlavní funkcionalitu vliv a které jsme co nejdříve vyřešili. Celkově tedy bylo nasazení úspěch.

Závěrem

Provádění technických změn, jako je právě tato, má jednu nevýhodu. Pokud se provede opravdu dobře, nikdo si ničeho nevšimne, ale pokud nastane problém, hned ho všichni pocítí. V našem případě se díky opatrnému přístupu a důkladnému testování jednalo o první případ a my tak můžeme při vývoji těžit z nových technologií, které se nám díky tomu zpřístupnily. I když se z venku nic nezměnilo, myslím že je dobré neopomíjet kvalitu kódu a zkrocení technického dluhu projektu, které do budoucna ulehčí implementaci dalších zajímavých věcí. Zároveň jde taky o důkaz toho, že je Latte 3 plně připraveno pro použití na velkých projektech a není potřeba se přechodu obávat. Jen je potřeba se připravit na vyšší časovou náročnost, především v případě velkého množství vlastních maker.

Sdílet na facebooku anebo twitteru

Zpět nahoru