phpFashion

Na navigaci | Klávesové zkratky

Jak v PHP detekovat chybu? No těžko…

Do žebříčku 5 největších zrůdností jazyka PHP rozhodně patří nemožnost zjistit, zda volání nativní funkce skončilo úspěchem, nebo chybou. Ano, čtete správně. Zavoláte funkci a nevíte, zda došlo k chybě a k jaké.

Teď si možná klepete na čelo a říkáte: selhání přece poznám podle návratové hodnoty, ne? Hmmm…

Návratová hodnota

Nativní (nebo interní) funkce obvykle vracejí v případě neúspěchu FALSE. Jsou tu výjimky, například json_decode, která vrací NULL, pokud je vstup nevalidní nebo překročí limit zanoření. Což najdeme v dokumentaci, potud ok.

Tato funkce slouží k dekódování JSONu i jeho hodnot, tedy volání json_decode('null') také vrátí NULL, tentokrát ale jako korektní výsledek. Musíme tedy rozlišovat NULL jakožto správný výsledek a NULL jakožto chybu:

$res = json_decode($s);
if ($res === NULL && $s !== 'null') {
    // došlo k chybě
}

Je to hloupé, ale pámbů zaplať, že to vůbec lze. Existují totiž funkce, u kterých nelze z návratové hodnoty poznat, že k chybě došlo. Např. preg_grep nebo preg_split vrací částečný výsledek, tedy pole, a nepoznáte vůbec nic (více v Zrádné regulární výrazy).

json_last_error & spol.

Funkce informující o poslední chybě v určitém rozšíření PHP. Bohužel bývají mnohdy nespolehlivé a je obtížné zjistit, co to vlastně ta poslední chyba je.

Například json_decode('') neresetuje příznak poslední chyby, takže json_last_error vrací výsledek nikoliv pro poslední, ale pro nějaké předchozí volání json_decode (viz How to encode and decode JSON in PHP?). Obdobně ani preg_match('neplatývýraz', $s) neresetuje preg_last_error. Pro některé chyby nemají tyto funkce kód, takže je vůbec nevrací, atd.

error_get_last

Obecná funkce vracející poslední chybu. Bohužel je nesmírně komplikované zjistit, zda se chyba týkala vámi volané funkce. Onu poslední chybu totiž mohla vygenerovat úplně jiná funkce.

První možností je přihlížet ke error_get_last() jen ve chvíli, kdy návratová hodnota značí chybu. Bohužel třeba funkce mail() umí vygenerovat chybu, i když vrátí TRUE. Nebo naopak preg_replace v případě neúspěchu nemusí chybu generovat vůbec.

Druhou možností je před voláním naší funkce „poslední chybu“ vyresetovat:

@trigger_error('', E_USER_NOTICE); // reset

$file = fopen($path, 'r');

if (error_get_last()['message']) {
    // došlo k chybě
}

Kód je zdánlivě jasný, chyba může vzniknout pouze při volání funkce fopen(). Ale není tomu tak. Pokud je $path objekt, bude převeden na řetězec metodou __toString. Pokud je to jeho poslední výskyt, bude volán i destruktor. Mohou se volat funkce URL wrapperu. Atd.

Tedy i zdánlivě nevinný řádek může vykonat spoustu PHP kódu, který může generovat jiné chyby, z nichž poslední pak vrátí error_get_last().

Musíme se proto ujistit, že k chybě došlo skutečně při volání fopen:

@trigger_error('', E_USER_NOTICE); // reset

$file = fopen($path, 'r');

$error = error_get_last();
if ($error['message'] && $error['file'] === __FILE__ && $error['line'] === __LINE__ - 3) {
    // došlo k chybě
}

Ona magická konstanta 3 je počet řádků mezi __LINE__ a voláním fopen. Prosím bez komentáře.

Tímto způsobem už chybu odhalíme (tedy pokud ji funkce emituje, což třeba zmíněné funkce pro práci s regulárními výrazy zpravidla nedělají), ale nejsme schopni ji potlačit, tedy zabránit tomu, aby se zalogovala apod. Použití například shut-up operátoru @ je problematické v tom, že zatají vše, tedy veškerý další PHP kód, který se v souvislosti s naší funkcí volá (viz zmíněné destruktory, wrappery atd.).

Vlastní error handler

Šíleným, ale zřejmě jediným možným způsobem, jak zjistit, zda určitá funkce vyhodila chybu s možností ji potlačit, je instalace vlastního chybového handleru pomocí set_error_handler. Jenže není sranda to udělat správně:

  • vlastní handler musíme také odstranit
  • musíme jej odstranit i v případě, že se vyhodí výjimka
  • musíme zachytávat skutečně jen chyby vzniklé v inkriminované funkci
  • a všechny ostatní předat původnímu handleru

Výsledek vypadá takto:

$prev = set_error_handler(function($severity, $message, $file, $line) use (& $prev) {
    if ($file === __FILE__ && $line === __LINE__ + 9) { // magická konstanta
        throw new Exception($message);
    } elseif ($prev) { // volej předchozí uživatelský handler
        return call_user_func_array($prev, func_get_args());
    }
    return FALSE; // volej systémový handler
});

try {
    $file = fopen($path, 'r');  // o tuhle funkci nám jde
    restore_error_handler();
} catch (Exception $e) {
    restore_error_handler();
    throw $e;
}

Co je magická konstanta 9 už víte.

No a tak my v PHP žijem, no.


Jak komitovat se záruční smlouvou?

Už jsem odpověděl na spoustu pull requestů „Can you add tests?“ Ale ne proto, že bych byl testofil, nebo abych dotyčného buze prudil.

Pokud posíláte pull request, který opravuje nějakou chybu, tak pochopitelně musíte před odesláním vyzkoušet, jestli skutečně funguje. Kolikrát si člověk myslí, že něco snadno fixne a ejhle, rozbije to ještě víc. Nechci se opakovat, ale tím, že to vyzkoušíte, jste vyrobili test, tak ho jen přiložte.

(Bohužel někteří lidé svůj kód doopravdy nevyzkouší. Kdyby to šlo, dával bych měsíční bany za pull requesty vytvořené přímo ve webovém editoru Githubu.)

Ale to stále není ten nejhlavnější důvod: Test je jediná záruka, že vaše oprava bude fungovat v budoucnu.

Už mnohokrát se stalo, že někdo poslal pull request, který mi nebyl užitečný, ale upravoval funkcionalitu důležitou pro něj. Zejména pokud to byl někdo, koho znám, a vím, že je dobrý programátor, tak jsem to mergnul. Pochopil jsem, k čemu to chce, nevadilo to ničemu jinému, tak jsem PR přijal a v tu chvíli vypustil z hlavy.

Pokud svůj pull request doplnil testem, tak jeho kód dodnes funguje a bude fungovat i nadále.

Pokud ho testem nedoplnil, tak se klidně může stát, že mu to nějaká další úprava rozbije. Ne schválně, prostě se to stane. Nebo už se to stalo. A nemá smysl láteřit, jaký jsem vůl, že jsem mu už potřetí rozbil jeho kód, ačkoliv jsem před 3 lety přijal jeho pull request, si to snad musím pamatovat né, takže mu to snad dělám naschvál… Nedělám. Nikdo si nepamatujeme, co jsme měli před třemi lety na svačinu.

Pokud vám na nějaké funkcionalitě záleží, přiložte k ní test. Pokud vám na ni nezáleží, vůbec ji neposílejte.


Alpha-beta-RC is so 20th century

Přestanu používat při vývoji označení testovacích fází alpha/beta/RC a nahradím ho jedním slovem.

Existuje řada způsobů, jak software verzovat. U knihoven se často hovoří o sémantickém verzování, kdy verzi reprezentuje trojice čísel major.minor.patch, a hlavní číslo major musí být zvýšeno pokaždé, když se v kódu objeví zpětně nekompatibilní změna. Což zní na první pohled rozumně, ale z mnoha důvodů to takřka nikdo 100% nedodržuje.

Taktéž existují různé způsoby, jak označovat jednotlivé fáze vývoje, z nichž nejznámější je asi slovní označení alpha/beta/RC. V beta fázi by měl být produkt kompletní co se vlastností týče a dále by se měly opravovat chyby, ladit kompatibilita atd. K čemuž se opět přistupuje různě, asi největším extrémem jsou věčné bety, které zpopularizoval Google. Opačným případem je prohlížeč Chrome, který přišel se zrychleným vydáváním verzí a přelévá mezi kanály dev/beta/stable.

Jak vidno, přístupy se u jednotlivých projektů značně liší. Záleží na mnoha faktorech, velikostí týmu počínaje, marketingem konče. Pro mě je prioritou, aby systém vedl k:

  • vydávání odladěných major & minor verzí
  • při maximální snaze zachovat zpětnou kompatibilitu
  • a pokud možno v pravidelných intervalech, včetně přísunu novinek

Což se lehko řekne, mnohem těžší je se k tomu dobrat, navíc aby to bylo v lidských silách.

Flow, které mi celkem vyhovuje a uvedené priority vesměs plní, vypadá následovně:

  1. vývoj probíhá neprve ve větvích (buď veřejně v podobě pull requestů nebo lokálně)
  2. mergnutím do masteru (což je vlastně permanentní alpha-verze) začíná testování. Přičemž žádný commit nesmí rozbít testy, ale může narušit zpětnou kompatibilitu (viz dále)
  3. jednou za půl roku až rok bych rád vydal větší verzi:
    • vytvořím release-větev s názvem jako v3.2
    • musím rozhodnout, které věci vynechat (revert) a které přidat (merge pull requestů a lokálních větví)
    • především však udělat co nejvíc pro zachování kompatibility rozbité v bodě 2)
    • v krátkých intervalech vydávat testovací verze reagující na chyby a připomínky (což bývá zápřah)
  4. a pak po dlouhou dobu udržovat verzi čerstvou
    • cherry-pickováním oprav z masteru (nebo naopak, ale výjimečně někdo připraví pull request oproti release-větvi)
    • vyhýbat se BC breakům (byť je někdy značně ošidné rozlišit, co BC break je)
    • vydávat patch verze (tj. setinkové) v krátkých intervalech

Někomu to připadá jako úplně normální postup, jiní zase brblají, takže musím zmínit, na jaká v reálu narážím úskalí.

S každými novinkami přichází (byť třeba jen hypotetické) BC breaky. To je prostě realita. Přičemž zachovávání kompatibility beru jako velmi důležitou věc a stojí mě tak 70 % času investovaného do vývoje. Je taky nejčastějším důvodem, proč otálím s přidáním nových věcí. V případě Texy to vedlo v podstatě až k ukončení vývoje.

Existuje dost frameworků, které přežily tlustou čáru mezi dvěma major verzemi. Já ji ale dělat nechci. Nechci ani utopit vývoj. Kloním se proto k postupným zdokumentovaným BC breakům u větších verzí. Navádím uživatele co změnit pomocí E_USER_DEPRECATED hlášek. Nejvýhodnější situaci tak mají ti, kteří průběžně updatují.

Nicméně dle sémantického verzování bych proto měl každého půl roku až rok vydat novou major verzi, což se mi nejeví z řady důvodu praktické. Zejména to vyvolává dojem, že přechod bude velmi náročný a uživatelé začnou setrvávat u starých verzí. Což nechci. Proto raději zvedám jen minor verzi (tj. desetinkovou).

Podle sémantického verzování se novinky mohou objevovat jen v major & minor verzích. Vydávání těchto verzí v intervalech kratších než půl roku vytváří dojem, že vývoj uhání příliš rychle, a uživatelé opět přestávají aktualizovat. Ovšem přijít s užitečnou novinkou a odkládat její vydání i půl roku a více, než bude čas na další větší verzi, nepřináší užitek nikomu, a uživatele toužícího po novince tlačí k používání masteru. Což opět nechci. Takže občas zařadím novinky i do bodu 4). Nepříjemné je, že si kvůli tomu vyslechnete dost kritiky, ještě nepříjemnější je, že po letech jejího permanentního poslouchání už nemáte jakoukoliv schopnost rozlišovat kritiku oprávněnou od kritiky české.

Pokud vydám alpha verzi a poprosím o testování, zhostí se toho naprosté minimum lidí. O moc lépe na tom nejsou ani první beta-verze. Teprve až začnu vydávat RC, na fóru to ožije. Což je mnohokrát ověřený fakt. Nepříjemné je, že i když následně vydáte sebestabilnější verzi, bude vám neustále někdo kazit náladu vysvětlováním, jak to babráte a nerozumíte dělení na alpha/beta/RC. Z jeho pohledu jakoby oprávněně, tudíž jsem se rozhodl, že nebudu nadále rozlišovat tyto fáze, protože to nikomu nic nepřináší, cíli udělat co nejlepší stable podřizuji vše nehledě na fázi, a bude stačit vydávat jen číslované -testing verze (v podstatě beta-verze).

Dává smysl mi ukončit tuto testovací fázi a vydat stable ve chvíli, kdy sám jsem spokojený a reportování chyb poklesne na minimum. Samozřejmě mohl bych pár měsíců čekat, ale realita je taková, že aktivita se nastartuje teprve až vydáním stable.

Ještě jednou to shrnu:

  • master je alpha-verze, do které se dostanou jen nadějné novinky + bugfixy, a bez záruky zpětné kompatibility se tu testují
  • jednou za čas vytvořím release-větev, která po sérii -testing verzí řešících mj. kompatibilitu bude završena rychlým vydáním stable
  • mezi masterem a release-větví cherry-pickuji opravy, občas menší novinky, a vydávám setinkové verze, ideálně s jedním RC

Docela jsem zvědavý, co v případě Nette udělá s uvedeným flow rozdělení na malé projekty. Teoreticky bych mohl najet na rychlé verzování u částí, které se budou vyvíjet, a uživatel by si pomocí Composeru poskládal takové Nette, jaké by chtěl. Zatím ale spíš narážím na nejrůznější překážky a vydávání nových verzí je teď mnohem větší dřina.


Jak předávat závislosti v Nette

Jaké jsou best practice pro předávání závislostí presenterům, komponentám a jiným službám v Nette?

Nejprve: nezávisle na frameworku nebo typu tříd platí vždy tohle:

  • povinné závislosti předávat konstruktorem
  • volitelné buď samostatnou metodou, nebo též konstruktorem

Tečka.


Nicméně existuje případ, kdy je předávání závislostí konstruktorem problematické, tzv. constructor hell, tedy situace, kdy předáváme závislosti konstruktorem určité třídě a zároveň jejímu potomkovi. Tohle nastává často u presenterů, kde je právě zvykem mít nějaký BasePresenter. Jako workaround dává DI kontejner v Nette možnost použít v BasePresenteru místo konstruktoru metodu nazvanou injectBase() (případně jakkoliv jinak, jen musí začínat na inject).

Jde o workaround pro base presentery, takže v jiných situacích metody inject nepoužívejte.

Dále předávání závislostí přes konstruktory nebo jiné metody je otravné v tom, že musíte napsat nějaký rutinní kód. Nutnost psát rutinní kód vždy ukazuje na slabé místo samotného jazyka, jedna ze šancí na zjednodušení byla zamítnuta, nicméně některé IDE ho umí vygenerovat a taktéž DI kontejner ve frameworku nabízí zkratku v podobě @inject anotací. Public (!) proměnnou doplníte anotací např. /** @var Model\UserFacade @inject */ a framework sám do ní vloží službu a vyhnete se psaní konstruktoru nebo inject metody.

Protože to není čisté řešení, funguje opět jen pro presentery. Používání anotací rozhodně není best practice, ale je to sakra pohodlné.


Doporučuji podívat se na přednášku Filipa Procházky a přečíst článek Vojty Dobeše Tvorba komponent s využitím autowiringu.


Nette Revolution 2.2

Asi největší revoluce v dějinách Nette, která se vás nijak nedotkne.

Poměrně krátce po vydání velké verze 2.1, která přinesla řadu vylepšení nejen ve formulářích, je tu další velká verze 2.2, která přichází s úplně novou infrastrukturou. Totiž původní repozitář Nette byl smazán rozdělen do 19 nových samostatných komponent: Application, Bootstrap, Caching, ComponentModel, Nette Database, DI, Finder, Forms, Http, Latte, Mail, Neon, PhpGenerator, Reflection, RobotLoader, SafeStream, Security, Tokenizer, Tracy a Utils.

(Podívejte se na video Kvadratura Nette.)

Co se tím mění pro vás, uživatele Nette Frameworku?

Takřka nic. Stále si můžete stáhnout celý framework nebo jej nainstalovat příkazem composer require nette/nette.

Ale zároveň máte možnost používat zcela samostatně šablonovací systém Latte, Laděnku (neboli Tracy), databázovou vrstvu a vlastně cokoliv.

Rozdělení frameworku do menších částí je trend, který už započalo například Symfony nebo Zend Framework 2. Nutnou podmínkou byl vznik respektovaného nástroje pro správu závislostí v PHP, kterým se stal Composer. Pokud si s ním zatím netykáte, neváhejte a seznamte se, není daleko doba, kdy se v PHP knihovny nebudou distribuovat jinak.

Nette se na rozdíl od Symfony nebo Zendu rozdělilo i fyzicky, což znamená, že každá komponenta má vlastní repozitář, issue tracker a číslování verzí. Zároveň každý repozitář obsahuje testy a všechny související soubory. Podívejte se například na Forms, kde najdete příklady použití v examples, v adresáři src/assets JavaScripty a v adresáři src/Bridges kód, který propojuje různé Nette komponenty. V případě formulářů jde o makra pro Latte.

Stejným způsobem jsou pak strukturovány všechny repozitáře.

Rozdělení frameworku je završením dvouleté práce, s cílem zachovat maximální kompatibilitu. Vlastně i proto zůstáváme u čísla verze 2. Jediným výrazným zásekem bylo oddělení Latte, což si vyžádalo přepracovat systém šablon, který v Nette existuje v takřka původní podobně od verze 0.8. Ale postupně:

Tracy

Laděnka byla přejmenována na Tracy, nemusíte se tak trápit s psaním šíleného Nette\Diagnostics\Debugger, nyní stačí Tracy\Debugger. Původní třída je z důvodu zpětné kompatibility stále funkční.

Pokud píšete doplňky, prefix názvů CSS tříd se změnil z nette- na tracy- a třída nette-toggle-collapsed na dvojici tracy-toggle tracy-collapsed. Původní třídy jsou u starých doplňku změněny na nové automaticky.

Latte

Šablonovací systém byl vždy poměrně úzce provázán s dalšími částmi frameworku, zejména třídami z jmenného prostoru Nette\Templating. Aby bylo Latte samostatně použitelné, bylo potřeba mu vymyslet nové snadno použitelné API, které se obejde bez těchto pomocných tříd. A vypadá takto:

$latte = new Latte\Engine; // nikoliv Nette\Latte\Engine
$latte->setTempDirectory('/path/to/cache');

$latte->addFilter('money', function($val) { return ...; }); // dříve registerHelper()

$latte->onCompile[] = function($latte) {
    $latte->addMacro(...); // when you want add some own macros, see http://goo.gl/d5A1u2
};

$latte->render('template.latte', $parameters);
// or $html = $latte->renderToString('template.latte', $parameters);

Jak vidíte, Latte si řeší samo načítání šablon a jejich kešování, čímž pádem původní FileTemplate a vlastně celý Nette\Templating z velké míry pozbývá na smyslu existence. Tyto třídy i nadále fungují a snaží se zajistit kompatibilitu s novým Latte, nicméně jsou zavržené. Ze stejného důvodu jsou zavržené i třídy Nette\Utils\LimitedScope, Nette\Caching\Storages\PhpFileStorage a služba templateCacheStorage.

Naopak Application přináší vlastní třídu Template (nahrazující FileTemplate a zajišťují kompatibilitu) a továrnu TemplateFactory, která rozšiřuje možnosti, jak v presenterech a komponentách pracovat se šablonami.

Ostatní

Třídy Nette\ArrayHash, ArrayList, DateTime, Image a ObjectMixin jsou nyní součástí balíčku Utils, proto i jejich namespace byl změněn z Nette na Nette\Utils. Obdobně Nette\Utils\Neon se stalo součástí balíčku Neon a bude mít namespace Nette\Neon\Neon. Aby změna byla transparentní, vytváří se pro tyto a některé další třídy tiše aliasy. Aliasy ostatních tříd vytváří Nette Loader spuštěný v loader.php a vypisuje přitom varování (abyste mohli svůj kód upravit). Pokud loader.php nepoužíváte (tj. instalujete Nette přes Composer), můžete Nette Loader načíst ručně třeba v bootstrap.php. Od RC4 se Nette Loader spouští vždy.

Alternativně je možné vytvořit v bootstrap.php rovnou všechny aliasy a obejít tak varování. Ale jelikož PHP při vytváření aliasů načítá zdrojový kód tříd, může to vést k mírnému snížení výkonu:

@array_walk(Nette\Loaders\NetteLoader::getInstance()->renamed, 'class_alias');

Zavržena (tj. stále funguje, jen vyhodí E_USER_DEPRECATED) je třída Nette\Utils\MimeTypeDetector, která od PHP 5.3 není potřeba, neboť ji plně nahrazuje rozšíření Fileinfo (pod Windows jej nezapomeňte zapnout v php.ini).

Byla zrušena podpora anotace @serializationVersion a dohledávání tříd pro vlastní anotace – tyto věci nebyly známé ani používané, ale měly negativní vliv na výkon.

A nakonec, chybné odkazy v šabloně nyní začínají hashem, tj. místo error:... se vypisuje #error:, aby když na takový odkaz omylem kliknete, browser nevypsal strašidelnou hlášku. Upravte si proto CSS.

Novinky

Verze 2.2.0 přichází i s řadou novinek.

Formuláře nyní přenášejí parametr do v POST datech, takže nebude strašit v URL. Přibyly nové validátory Form::MIN a Form::MAX. A do funkcí obsluhující událost onSuccess se nyní jako druhý parametr předávají hodnoty formuláře, takže ušetříte psaní $values = $form->getValues().

V databázi přibyla nová funkce fetchAssoc(). Můžete se podívat na pár příkladů použití v testech.

Anotace @inject nyní respektují aliasy definované pomocí use.

Pokud do data- atributů třídy Html vložíte pole, bude se serializovat do JSONu.

V souboru config.neon můžete jednotlivým uživatelů definovat také jejich role.

A přibyla nová třída Nette\Security\Passwords, která řeší hashování hesel.