Typická webová aplikace si načte nějaká data z databáze a zobrazí je rozmístěné na stránce. Občas do databáze i něco uloží. Tyto části jsou na sobě většinou nezávislé a jedna část by neměla ovlivnit druhou. Někdy to samozřejmě neplatí (pokud se nepodaří načíst detail produktu, nemá smysl načítat jeho obrázky), takový případ bývá ale většinou jen jeden na stránce. Z tohoto důvodu je nesmírně důležité, aby aplikace neskončila nezachycenou výjimkou (případně výjimkou zachycenou až na nejvyšší úrovni) nebo obecně fatální chybou – nezobrazení stránky je u webové aplikace většinou kritický problém. Většinu chyb je potřeba ošetřovat lokálně, blízko místa jejich vzniku.
Co když se třeba nepodaří uložit objednávku zákazníka do databáze, je to důvod pro fatální chybu? Není, protože pořád se ji může ještě podařit odeslat na e-mail obchodníka a tím o ni nepřijít. I tato operace, která je pro zpracování operace kritická, by si tedy chybu měla ošetřit lokálně.
V PHP se používají dva mechanismy ošetření chyb: návratová hodnota (spojená většinou s vygenerováním varování) nebo výjimky. Výjimky se perfektně hodí pro ošetření chyb v posloupnosti operací a pokud chceme chybu lokalizovat (omezit jen na několik operací, v krajním případě na jednu), musíme napsat kód navíc (jeden blok try–catch
na každou lokalitu). Návratová hodnota je zase výborná pro zpracování lokálních chyb a pokud je chceme rozšířit na více operací, musíme napsat kód navíc (jeden if
na každou možnou chybu). Moje zkušenost je taková, že tento kód navíc se mnoha programátorům nechce psát, čehož důsledkem je to, že buď v aplikaci může docházet k neošetřeným výjimkám nebo se z posloupnosti operací provede jen jejich nevhodná část.
To se ostatně pokouší demonstrovat David Grudl, i když podle mě trochu nešikovně. První tři příklady jsou zcela umělé: místo copy + unlink se dá použít rename, místo USE testdata
+ DELETE FROM orders
bych použil DELETE FROM testdata.orders
a místo ftp_chdir($ftp, "test")
+ ftp_delete($ftp, "database.sdb")
prosté ftp_delete($ftp, "test/database.sdb")
. Jistě by se daly najít příklady, kde druhá funkce závisí na doběhnutí první a přitom nepoužívá její výsledek, zdaleka ale nejsou běžné. Další tři příklady zase vůbec nesouvisí s tím, jakým způsobem se chyby reportují, ale s tím, že se s nimi vůbec nepočítá – při použití výjimek by byl výsledek úplně stejný. Článek se také obouvá do příkladů v dokumentaci funkce fread, tyto příklady kupodivu ale pracují správně a konzistentně – když v kterékoliv volané funkci dojde k chybě, tak bude mít proměnná $contents
hodnotu false
. Samozřejmě se vygenerují (a zalogují) nějaké chyby navíc, to ale není zásadní problém. Za zásadní problém naopak považuji nerespektování lokálnosti chyby, což způsobí nezobrazení celé stránky, které David schvaluje.
Pojďme se podívat na nějaký konkrétní příklad. Budeme chtít získat výsledek databázového dotazu a uložit ho do proměnné. Tuto proměnnou pak budeme chtít třeba v šabloně projít, neměla by ale ovlivnit ostatní operace, které na stránce provádíme. Tuto jednoduchou úlohu lze v PHP vyřešit překvapivě velkým množstvím způsobů a to i v případě, že se omezíme pouze na extenzi MySQLi. Kromě rozhodnutí, zda budeme chyby sami ošetřovat, si můžeme vybrat způsob hlášení chyb (funkcí mysqli_report) a to, zda budeme používat funkce nebo metody. Ukázky předpokládají nastavení konfigurace vhodné pro produkční prostředí, tedy error_reporting
alespoň E_ERROR | E_WARNING
, display_errors
vypnuto, log_errors
zapnuto. Chybový log je vhodné sledovat třeba pomocí jeho pravidelného posílání e-mailem.
Ukazuje se, že vcelku naivní přístup může být prakticky optimální, a naopak pokus o důkladné ošetření může mít nepříjemné následky:
<?php // dozvíme se, že došlo k chybě, ale ne příčinu mysqli_report(MYSQLI_REPORT_OFF); $result = mysqli_query($mysqli, $query); $data = mysqli_fetch_all($result); // může skončit fatální chybou bez zjištění příčiny mysqli_report(MYSQLI_REPORT_OFF); $result = $mysqli->query($query); $data = $result->fetch_all(); // nedozvíme se, že došlo k chybě mysqli_report(MYSQLI_REPORT_OFF); $result = mysqli_query($mysqli, $query); if ($result) { $data = mysqli_fetch_all($result); } // zjistíme příčinu chyby a jednu chybu navíc mysqli_report(MYSQLI_REPORT_ERROR); $result = mysqli_query($mysqli, $query); $data = mysqli_fetch_all($result); // zjistíme příčinu chyby mysqli_report(MYSQLI_REPORT_ERROR); $result = mysqli_query($mysqli, $query); if ($result) { $data = mysqli_fetch_all($result); } // může skončit nezachycenou výjimkou mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $result = $mysqli->query($query); $data = $result->fetch_all(); // nedozvíme se, že došlo k chybě mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); try { $result = $mysqli->query($query); $data = $result->fetch_all(); } catch (mysqli_sql_exception $e) { } // zjistíme příčinu chyby mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); try { $result = $mysqli->query($query); $data = $result->fetch_all(); } catch (mysqli_sql_exception $e) { error_log($e); } ?>
Ještě stručné shrnutí:
Reportování chyb | Zápis kódu | Výsledek |
---|---|---|
vypnuto | funkce | chyba bez vysvětlení |
vypnuto | metody | fatální chyba |
vypnuto | obsluha chyb | zatajení chyby |
varování | funkce | vysvětlení, chyba navíc |
varování | metody | fatální chyba |
varování | obsluha chyb | vysvětlení |
výjimky | funkce | neošetřená výjimka |
výjimky | metody | neošetřená výjimka |
výjimky | obsluha chyb | vysvětlení nebo zatajení chyby |
Za nejdůležitější považuji, aby lokální chyba nezpůsobila pád celé aplikace. V druhé řadě bychom se měli dozvědět o vzniku chyby, ideálně i o její příčině.
Jsem toho názoru, že lokálních chyb bývá ve webových aplikacích většina, opravdu globálních chyb poměrně málo (a dají se ošetřit snadno pomocí výjimek i pomocí návratové hodnoty) a celkem těžko se v reálných webových aplikacích hledají posloupnosti operací, kde by vadilo, že se zavolá B, i když se nepovedlo A. Proto jsem napsal, že výjimky nepovažuji obecně za nejlepší možný způsob ošetřování chyb a že způsob práce s chybami v PHP považuji za rozumný. Pro správné ošetření chyby je totiž nutné ji jednak ošetřit, ale také respektovat její kontext a zbytečně ji neeskalovat.
Diskuse je zrušena z důvodu spamu.