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 $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.

před 10 lety v rubrice PHP | blog píše David Grudl | nahoru

Mohlo by vás zajímat

Komentáře

  1. pp #1

    avatar

    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 😁

    před 10 lety
  2. Vojta B #2

    A co prostě registrovat error handler na celý běh kódu?

    před 10 lety | reagoval [5] David Grudl
  3. Tomáš #3

    avatar

    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?

    před 10 lety | reagoval [5] David Grudl
  4. Tomáš #4

    avatar

    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ř.

    před 10 lety
  5. David Grudl #5

    avatar

    #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.

    před 10 lety
  6. Timy #6

    avatar

    $line = __LINE__ + 9

    Nemají tam být tři rovnítka?

    před 10 lety | reagoval [7] David Grudl
  7. David Grudl #7

    avatar

    #6 Timy, jasně, opraveno

    před 10 lety
  8. smrdící epidemie #8

    avatar

    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.

    $old_reporting = error_reporting(0);
    ob_start();
    $file = fopen($path, 'r');  // o tuhle funkci nám jde
    $ob_contents = ob_get_contents();
    ob_end_clean();
    error_reporting($old_reporting);
    if ($ob_contents) {
        // došlo k chybě
    }
    před 4 lety
  9. blicí punčocháče #9

    avatar

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

    Možná by šlo doplnit, že od PHP verze 7 existuje funkce:

    error_clear_last()
    před 4 lety
  10. čůrací šuplík #10

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

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

    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.

    před 4 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.


phpFashion © 2004, 2024 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.