Víte, jak se pozná, že se Disassembler nenudí? Články vycházejí jednou za tři měsíce. Zato jsem ale konečně překopal ksicht i útroby svého webu, což bylo už hezky dlouho potřeba. Prošel jsem si u toho několika různými fázemi vývoje webových aplikací (popírání, hněv, smlouvání, deprese, smíření) a zjistil, že mě takové věci po těch všech letech už opravdu nebaví. Nakonec jsem skončil tam, kde jsem začal, akorát o trochu lépe, čistěji a štíhleji.
Udělej si sám
Budu se hluboce stydět, ale prozradím vám, že jsem s úpravou webu začal někdy na podzim roku 2015. Už tehdy jsem si říkal, že vychytávky jako HTML5shiv už moc lidí neohromí. Málokdo v tu dobu chodil prohlížet web s Firefoxem 3 nebo IE8 a nativní HTML5 bylo všudypřítomné stejně jako voňavé stromečky v autech, takže rovnák na vohejbák už nebyl potřeba. Navíc mi Google v nástrojích pro webmastery tu a tam pofňukával, že můj blog není mobile-friendly a že bych s tím měl něco dělat. I dal jsem milému Googlovi za pravdu a začal vymýšlet, jak redakční systém upravit nebo nahradit, aby zase chvilku držel laťku s nejnovějšími hipstandardy. Jelikož už jsem s vývojem webů seknul před notnou řádkou let a raději se oddávám hrátkám v příkazové řádce, moje první myšlenky směřovaly k hotovým redakčním systémům. Na webových serverech, které spravuji, tu a tam běží nějaký WordPress nebo Joomla a zdá se, že jejich majitelé jsou s nimi spokojeni, tak proč to také nezkusit.
SlovTisk
Nasypal jsem tedy na svoji testovací virtuálku WordPress, který mi přišel lépe zdokumentovaný a lépe promyšlený, co se týče API a možností přizpůsobení, a jal se replikovat svůj web. Šablonový systém WordPressu funguje jako jakákoliv jiná old-schoolová PHP šablona. Je to prosté HTML, protkané různými <?php if ($bla): ?>
, <?php else: ?>
, <?php endif; ?>
, <?php foreach ($bla as $ble): ?>
, <?php endforeach; ?>
, které je nahákované na nějaký view ze kterého tahá data a metody. K dalšímu zpracování je pak celý output buffer se šablonou požírán pomocí ob_start()
a ob_get_contents()
. Tohle mi v zásadě vyhovuje, protože se mi systém nesnaží podstrkávat nějaký vlastní samoúčelný a obskurní šablonový systém, ke kterému bych byl nucen znát jeho vlastní syntaxi a jeho vlastní specifika. Neříkám, že takové systémy jsou a priori špatné, ale PHP samo o sobě je tak příšerné, že jej nezachrání ani svěcená voda. Ale zpět k WordPressu. Potřeboval jsem do něj nějak vecpat mé stávající články a ostatní data. Moje databáze měla toho času celkem jednoduchou strukturu, takže jsem nakonec skončil u celkem rozumně použitelného WP all import, který umí skrze průvodce importovat data z CSV nebo XML. Vytáhnout data ze staré databáze a vyrobit z nich XML už byla celkem hračka. Jediné, co mě zarazilo, bylo opět samotné PHP, protože má 5 různých modulů pro práci s XML (DOM, DOM XML, libxml, SimpleXML a XMLReader / XMLWriter).
Když jsem si přelil data, omaloval šablonu a napojil všechny kousky na příslušné funkce, jal jsem se kód optimalizovat. Milý WordPress totiž ve snaze ulehčit majitelům blogísků integraci všelijakých featur, přidává hromadu vlastních kousků. Tak například pitomý <head>
ve výchozím stavu obsahuje odkazy na RSS feed, RSD feed, Windows Live Writer manifest (důvod jeho existence doposud nechápu), skript pro detekci a správné zobrazení emoji, oEmbed odkazy, odkaz na REST API a hromádku nějakých dalších skriptů a meta tagů. Nejen že většinu z nich nepotřebuju, ale třeba takové REST API umí být i vyloženě škodlivé. Většina toho balastu se dá odstranit, ale vaše functions.php pak začíná takto
<?php
remove_action('wp_head', 'feed_links', 2);
remove_action('wp_head', 'feed_links_extra', 3);
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_head', 'wp_generator');
remove_action('wp_head', 'wp_oembed_add_discovery_links');
remove_action('wp_head', 'wp_oembed_add_host_js');
remove_action('wp_head', 'rest_output_link_wp_head');
remove_action('wp_print_styles', 'print_emoji_styles');
Nemusím asi zdůrazňovat, že každá funkce, která takovouto výzvu k vystěhování neobdržela, bude generování výsledné stránky zbytečně zdržovat. I tohle jsem ale na chvíli překousl a vesele pokračoval v transformaci. Redakční systém WordPressu je velmi uživatelský přívětivý, ale pořád při každém kliku cítím ty hromady hooků a eventů. Když jsem si tedy konečně změřil, jak dlouho milý WordPress stránku generuje, okamžitě jsem s transformací přestal. Medián 600 ms je na takovýhle srandovní malý webík zatraceně hodně. A to používám PHP 7 a mám zapnutý Zend OPcache modul pro PHP. WordPress žádnou pořádnou server-side cache nemá a ohýbat to přes koleno nějakými dalšími pluginy je mi poněkud proti srsti. Nejvíce bezpečnostních problémů s WordPressem totiž přináší právě pluginy třetích stran (čímž tedy nechci říct, že jádro by na tom bylo zrovna dvakrát dobře, jak je ostatně možno vidět ze seznamu CVE). Stále mám v živé paměti security hardening jednoho cizího WordPressového blogísku, kdy se vinou prasácky napsaného pluginu na server dostal krásný web shell a server pak pálil do světa deset tisíc psaníček za hodinu. Hardening mimochodem mimo patřičných aktualizací a důkladného prořezání pluginů spočíval i v přidání následujícího kousku do .htacess. Zejména zakázání již zmíněného REST API se v retrospektivě zdá jako dobrý tah.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^wp-admin/includes/ - [F,L]
RewriteRule !^wp-includes/ - [S=3]
RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]
RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L]
RewriteRule ^wp-includes/theme-compat/ - [F,L]
</IfModule>
<Files wp-login.php>
Require from 12.34.56.78 # IP administratora
</Files>
<Files wp-config.php>
Require all denied
</Files>
<Files xmlrpc.php>
Require all denied
</Files>
Z českých luhů a hájů
Myšlenku použití volně dostupného systému pro správu obsahu jsem tedy zavrhl a raději začal uvažovat směrem k vylepšování a modernizaci stávajícího kódu. Původní kód pro svůj blog jsem psal někdy v roce 2011 na základě předchozích projektů z let 2007 – 2009 vylepšených o poměrně laicky sebrané best practices a několik dalších věcí, jejichž použití mi postupem času v PHP začaly dávat smysl. Řekl jsem si, že bych třeba mohl použít nějaký opravdický framework. V úvahu přicházely molochy jako Zend, Symfony nebo Laravel, ale vzhledem k tomu, pro jak velké projekty jsou takové frameworky primárně určeny, bylo by to jako jít s kanónem na vrabce. Navíc chci psát kód, který je malý a rychlý a to nejen v PHP, ale obecně v jakémkoliv jazyce a pro jakýkoliv projekt. Téměř všechny mé kódy jsou vlastně zároveň i minimum viable product, protože prostě napíšu jen to, co je potřeba a napíšu to tak, abych se v tom ještě za dva roky vyznal. V tomto směru se plně ztotožňuji s Edem Finklerem a jeho MicroPHP manifestem (do češtiny přeložil Martin Malý jako Manifest miniaturního PHP). A pak jsem objevil Nette. Malý český framework. Z dokumentace se tvářil, že by teoreticky mohl být přesně to, co hledám. Tak jsem si tedy stáhl vzorový projekt a nestačil se divit. Pominu-li všelijaké vyfikundace na ladění (které jsou mimochodem pro samotný vývoj nesmírně užitečné) a autoloading, samotná idea frameworku, způsob programování i životní cyklus Model-View-Presenteru a záležitosti kolem dependency injection byly téměř totožné s tím, co už jsem ve svém projektu 4 roky měl. Náhodou jsem prostě za ta léta plácání webíků dokonvergoval ke stejnému výsledku jako někdo úplně jiný. Doufám, že je to známka toho, že mé plácání bylo odváděno svědomitě.
Tak jsem tedy webík překlopil do Nette. Samozřejmě to nebylo jen tak, protože některé ideje byly značně odlišné od mých a dokumentace obsahovala víceméně jen takové ty často používané postupy, takže nejlepším zdrojem informací se nakonec ukázala samotná API reference. Dodělávání komentářů skrze AJAX pak připomínalo spíše masturbaci se struhadlem – trochu zábavné, ale většinou bolestivé – protože ačkoliv framework poskytuje vývojáři dostatek volnosti, není úplně jasné kde přesně a jakým způsobem se má patřičná AJAXová odpověď odesílat. Navíc k tomu vyžaduje vlastní jQuery plugin, což u PHP frameworku nepovažuju za úplně standardní součást. Bohužel i Nette se snaží umět všechno a nabízet spoustu berliček a ulehčení a když se k tomu pak přidá ještě vlastní šablonovací systém Latte, ORM-like chytré databázové objekty a další udělátka, výkon tím trpí. Ostatně se sami můžete přesvědčit u webů jako rohlik.cz nebo slevomat.cz a kouknout, jaká obrovitánská díra na vás zeje, než se dočkáte prvního bajtu odpovědi. Samozřejmě chápu, že špatné použití nástroje nutně neimplikuje, že nástroj samotný je špatný a také očekávám, že velkou roli budou hrát i ostatní faktory, jako latence sítě a vytížení samotného webového a databázového serveru, ale opět se dostáváme zpátky k rozdílu mezi aplikací, která je hotová ve chvíli, kdy funguje a aplikací, se kterou se vývojář mazlí a optimalizuje každý kousek skládačky tak, až to hraničí s posedlostí.
Odtučňovací kúra
Takže zpět ke kreslícímu prknu a udělat si to znovu po svém. Dle podobnosti s Nette jsem usoudil, že základ mého mikroframeworku je dobrý, a že na něm můžu dál stavět. V prvé řadě jsem si tedy ujasnil, co vlastně chci přesně na webu změnit. Google na mě pořvával, že nejsem dostatečně moderní, takže první věc, která je paradoxně nejvíce vzdálená nějakému programování, byla výroba responsivního vzhledu, který by se dobře četl i na mobilních zařízeních. Responsivní design mi z technického hlediska v dnešní době přijde jako strašně vtipná disciplína, protože můj telefon s 5,2" displejem má úplně stejné rozlišení jako můj 27" monitor. Do hry tedy musí vstoupit různé faktory škálování a další věci, díky kterým naprosto ztrácím představu o rozměrech a poměrech, ale i přesto jsem nucen honit každý pixel. Součástí redesignu bylo přesunutí menu z levé strany nahoru, aby se do něj vlezlo víc položek. Tím jsem zároveň získal více místa pro samotný obsah a protože tělo stránky již nemá pevně danou velikost (má pouze maximální), můžete si prohlížeč připíchnout na stranu a na druhé půlce monitoru předstírat práci. Porod ovšem byl, vyladit na dotykových zařízeních menu tak, aby bylo použitelné i s „plnotučným“ širokým designem. Samotné názvy kategorií totiž nejen že skrývají další podkategorie, které se zobrazí při najetí myši, ale jsou zároveň odkazy samotnými. Idea byla taková, že první dotek menu pouze otevře a až druhý návštěvníka zavede na příslušnou kategorii. Jen na tomhle jsem strávil nějaké tři hodinky a napíšu o tom samostatný článek, protože ladění kombinace JavaScriptových myšových a dotykových eventů vydá na pěknou procházku očistcem.
Modré odkazy jsem přebarvil na žabičkově hackersky zelenou, která lépe pasuje do celého vzezření webu. Taky jsem zahodil všechny sociální javascripty a sdílítka si udělal „postaru“, jak jsem kdysi zmiňoval ve svém úplně prvním článku. A když už jsem se vrtal v layoutu, tak jsem jej pěkně celý zeštíhlil, pozvyhazoval nějaké zbytečné obalující <div>
y a další balast, protože když už minimalizuju, tak ať to stojí za to. Další záležitostí, která se dočkala kompletního přepracování, jsou komentáře. Diskuse je nyní větvená (tj. reakce se zobrazují přímo pod komentářem, na který je reagováno) a navíc, jelikož provozuji poněkud technický blog, jsem přidal možnost značkování pomocí markdown syntaxe, takže návštěvníci s jeho pomocí mohou vesele vkládat snippety s kódem, které se pak nerozsypou se po celém komentáři. Naopak jsem odebral možnost registrace a přihlašování, protože z nějakých dvaceti tisíc vracejících se návštěvníků tuto možnost využilo asi třicet jednotlivců, takže v rámci zeštíhlování se ode dneška komentuje pouze bez přihlášení. Vracející se a opakovaně komentující návštěvníci budou mít vyplňování komentářového formuláře ulehčeno, protože se jim v sušence uloží jméno, se kterým naposledy komentovali.
Poslední zásadní změnou byla integrace cache přímo do samotných útrob kódu, čímž se z mého frameworku efektivně stal static content generator. Při první návštěvě se stránka vygeneruje a uloží, při každé další leze rovnou z cache. A když mluvím o zrychlení, tak tím skutečně myslím zrychlení. 90% požadavků má execution time pod 300 μs. To jako 0,3 ms. To jako 0,0003 sekund. To je jako dobrý, ne? Nejdéle pak trvá vyhledávání v článcích, protože to necachuju. Tam se to občas vyšplhá až na 70 ms, protože databáze nehorázně zdržuje. Když k tomu přihodím veškerou režii ze strany web serveru (na kterém jsem si konečně po měsících experimentování zapnul HTTP/2), operačního systému a běžně dostupné internetové přípojky, TTFB mi spadně někam k 25 ms a jsem spokojen. Nakonec jsem akorát updatoval všechny knihovny třetích stran, tedy jQuery, fancyBox 3 a highlight.js.
Pěkně hloupý preprocesor
Samozřejmě mě v průběhu programování PHP obšťastňovalo svou blbostí, takže jsem přišel na další špek, u kterého jsem se chytal za hlavu, podobě jako kdysi, když jsem se pokoušel zaokrouhlovat. V PHP i pythonu existuje funkce range($min, $max, $step)
. Jak asi tušíte, do funkce se vrazí dvě čísla omezující rozsah a třetí číslo, které říká, kolik se má přičítat. Funkce potom vrátí všechna čísla v rozsahu s daným inkrementem. A pokud používáte PHP, tak možná i s exkrementem. V pythonu to vypadá takto
>>> range(5, 10, 1)
[5, 6, 7, 8, 9]
>>> range(10, 14, 10)
[10]
Chápete, jo? Rozsah od 5 do 10, skákej po jedné = [5, 6, 7, 8, 9]
. Rozsah od 10 do 14, skákej po 10 = [10]
. A protože 10 + 10 je víc než 14, tak další číslo už v řadě prostě nebude. PHP to vzalo za opačný konec.
php > print_r(range(5, 10, 1));
Array
(
[0] => 5
[1] => 6
[2] => 7
[3] => 8
[4] => 9
[5] => 10
)
php > print_r(range(10, 14, 10));
PHP Warning: range(): step exceeds the specified range in php shell code on line 1
K čemu je prosím vás takováhle funkce dobrá? Já bych vám to řekl, ale nechci být sprostý. Takže na to, abych mohl v PHP mít range()
, která korektně počítá od nuly bez off-by-one chyby a kterou můžu dál použít jinde, musím si ji napsat po svém.
function _xrange($min, $max, $step) {
for ($i = $min; $i <= $max; $i = $i + $step)
yield $i;
}
No, a pokud chci skutečně pole hodnot, musím si generátor / iterátor pomoci funkce iterator_to_array()
proběhnout.
function xrange($min, $max, $step = 1) {
return iterator_to_array($this->_xrange($min, $max, $step));
}
Což je funkce, která je svou implementací postavená na hlavu a dál podporuje špatné zvyky, protože požírá libovolné objekty implementující Traversable
interface (který mimochodem vůbec nic neimplementuje, protože všechno kolem iterátorů implementuje až Iterator
), včetně asociativních polí a object
ů. Něco takového ve slušných jazycích (JavaScript, python) není z principu možné, pokud objekt přímo neimplementuje konkrétní iterátor, který říká jak se má iterovat. Ne že by jich PHP mělo málo. A vlastní si radši nepište, protože byste mohli potkat 8 let straý a stále neopravený bug 49369. Takže je lepší se na celý milý generátor vyprdnout a vracet pole rovnou, jak se to dělalo v počítačovém pravěku.
function xrange($min, $max, $step = 1) {
$result = [];
for ($i = $min; $i <= $max; $i = $i + $step) {
$result[] = $i;
}
return $result;
}
A to je samozřejmě jen špička ledovce. Pro další PHP hejt můžete pokračovat na provařený článek PHP: a fractal of bad design.
Doufám, že TIOBE index bude ještě nějakou dobu vykazovat stejné trendy a příští upgrade webu bude spočívat v přepsání do pythonu, Node.js nebo něčeho použitelného. Proč se ještě pořád tak hojně využívá PHP, mi je záhadou, ale jelikož můj web běží společně se 150 dalšími na mém virtuálním privátním serveru, je mi poněkud žinantní být special snowflake a ohýbat si infrastrukturu jen pro sebe. Vy se naštěstí s PHP (snad) drbat nemusíte, takže si užijte omlazený vzhled, a kdybyste přišli na nějakou chybku, pochlubte se mi.