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 $prev(...func_get_args());
}
return false; // volej systémový handler
});
try {
$file = fopen($path, 'r'); // o tuhle funkci nám jde
} finally {
restore_error_handler();
}
Co je magická konstanta 9
už víte.
No a tak my v PHP žijem, no.
Komentáře
pp #1
Nejde o zrůdnost jazyka ale historické dědictví platformy. Do ranného PHP se daly snadno dolepit funkce z nativních knihoven a tak to každý dělal. Když se rozhodlo o jejich pevném začlenění, nikdo se ani nenamáhal je nějak jednotně pojmenovat a prostě se ponechalo v stávající podobě.
Takže dnes je (navíc v globálním prostoru) hromada bordelu. Teoreticky lze postupně nahradit něčím jiným, co již bude pracovat správně, vyhazovat výjimky, půjde ladit. Nebo přejít na jiný jazyk 😁
Vojta B #2
A co prostě registrovat error handler na celý běh kódu?
Tomáš #3
PHPčkáři jsou divní, místo, aby byli rádi, že mají aplikaci odolnou vůči chybám ( = neví o nich), snaží se nad tím postavit nějaký chrám.
Teď ale vázně, tohle řešení nepočítá se situaci, kdy funkce vyhodí nějaký warning nebo notice a přitom provedla řádně svůj úkol a my to bereme jako stopku a počítáme s tím jako neúspěšnou operací. Jediné řešení, které mě v tomhle napadá, je prostě kontrolovat očekávaný stav po zavolání funkce, jak to třeba dělá mysql při ukládání do binlogu. Tím ale ztratíme univerzálnost.
Z hlavy vím, že nějaké notice vyhazovaly funkce pro zápis souborů na disk v případě hodně nevhodného jména podle platformy. Dalši notice mi vyhazovala práce s tcp, kdy docházelo ke ztrátě úvodních paketů ačkoliv se to zotavilo samo. Nemáte někdo lepší přehled a neznáte takovéhle případy?
Tomáš #4
Vojto, to by to právě vůbec neřešilo, jednak je tady cíl programově ošetřit konkrétní operaci a jednak musíme rozlišovat kontext chyby, část chyb se prostě stane aniž by nám to nějak vadilo a můžeme je opravit dodatečně podle logů, undefined index např.
David Grudl #5
#3 Tomáš, to je dobrá poznámka, tady by se pravděpodobně dalo rozlišovat E_NOTICE a E_WARNING. Alespoň co jsem tak procházel zdrojáky PHP, tak E_WARNING v podstatě odpovídá situaci, kdy lze vyhodit výjimku.
#2 Vojta B, tam bohužel pak vzniká problém s přenositelností takového kódu.
Timy #6
$line = __LINE__ + 9
Nemají tam být tři rovnítka?
David Grudl #7
#6 Timy, jasně, opraveno
smrdící epidemie #8
Dovolím si nadhodit ještě jeden způsob. Lze použít jen, pokud daná funkce při bezchybném provedení nic nevypisuje na výstup. A uznávám i další nevýhodu: na rozdíl od error handleru se musí tímto kódem „obalovat“ každá funkce, o kterou nám jde.
blicí punčocháče #9
Možná by šlo doplnit, že od PHP verze 7 existuje funkce:
čůrací šuplík #10
…
error_get_last() vrací NULL, pokud ještě k chybě nedošlo. A tak, trochu paradoxně, takový kód může opět generovat chyby.
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.