Zpracování fatálních chyb
Školení, která pořádám
Měl jsem za to, že fatální chyby PHP nelze při běhu ošetřit a nebyl jsem jediný. Objevil jsem ale způsob, jak je zpracovat přeci jen lze.
Jde o to, že funkce zaregistrovaná přes register_shutdown_function se provede i v případě fatální chyby. No a zbytek je hračka – stačí uvnitř této funkce detekovat, jestli došlo k fatální chybě a pokud ano, tak ji zpracovat. Od PHP 5.2 se k tomu dá použít funkce error_get_last, ve starších verzích by se dala detekovat neprázdnost $php_errormsg, kterou bychom si před legitimním koncem skriptu vyčistili. Alternativně by se dala pro zpracování běžných chyb použít funkce set_error_handler.
<?php
function shutdown_error() {
$error = error_get_last();
if ($error['type'] & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_PARSE)) {
// zpracování fatálních chyb
}
}
register_shutdown_function('shutdown_error');
?>
Funkce zjistí, jestli došlo k fatální chybě a pokud ano, tak ji zpracuje. Ostatní stavy nijak neošetřuje.
Diskuse
Jan R.:
Nedaří se mi toto zprovoznit. Minimalistický příklad
http://paste.org/index.php?id=7034 - když odstraním onu chybnou redeklaraci funkce abcde, touch() se provede (mimo jiné Apache v tu chvíli má aktuální adresář jinde, takže se soubor vytvoři tam, kde mám httpd.exe - není to dokonce bezpečnostní chyba?)
Pokud tam ale chybu (redeklaraci funkce abcde) nechám, žádný soubor se nevytvoří. Stejně tak nelze nic vypsat na výstup. PHP 5.2.9-1, Apache 2.2, Windows.
Mohl by někdo uvést nejmenší možný příklad, kdy tento trik opravdu nějak viditelně zachytí fatální chybu? Předem díky!
Fatální chyby nesmí být v souboru, kde se definuje jejich ošetření. Příčinou je to, že k většině fatálních chyb dojde už při kompilaci, takže se kód ani nespustí.
Minimálním možným příkladem je rozdělení kódu do dvou souborů – jeden nastaví ošetření chyb a druhý vložený přes include fatální chybu vyvolá.
Jan R.:
Funguje, díky! Původně jsem to měl rozdělené do dvou souborů, ale opačným, chybně-chybným způsobem :-)
Bednee:
Půjde takto ošetřit fatal errory když dojde pamět na memory limitu? Často se mi to stává při resizovaní velkých obrázků pomocí GD knihovny. Bylo by dobré, kdyby to nepoložilo celou aplikaci, ale prostě by se jenom ten obrázek nezresizoval.
Ano, funguje to i v tomto případě, ověřil jsem to. U obrázků se dá zabraná paměť předem spočítat podle jeho rozměrů.
Bednee:
Zajímavé. Pokud se dá předem i zjistit, kolik jí ještě zbývá, tak to je řešení.
Můžu vědět jak? Trochu jsem poslední rok z PHPka vypadl :)
Díky
Zabraná paměť se dá zjistit pomocí memory_get_usage(), povolená pomocí ini_get('memory_limit'). Obrázek by měl teoreticky zabírat 4*šířka*výška bajtů, ale v praxi zabírá o něco víc. Rozměry se dají levně zjistit pomocí getimagesize().
Nejen funkce registrovaná přes register_shutdown_function() se po fatální chybě provede, takových možností jsem našel několik. Docela kuriozní je třeba registrování vlastního stream wrapperu pomocí stream_wrapper_register() - po fatální chybě dojde ještě k volání stream_close. Dále se uzavírají session nebo output buffering handlery.
Problém byl tedy v tom _detekovat_, že fatální chyba nastala. Že nejde o korektní ukončení skriptu.
Myslím, že $php_errormsg k tomu použít nejde, to vrací i po fatální chybě NULL. PHP 5.2 přineslo funkci error_get_last(). Bohužel, když jsem ji testoval, taky mi nedokázala vrátit správné info, takže jsem ji zařadil po bok $php_errormsg.
Ještě že jsi to vyvrátil, zachytávání fatálních chyb Laděnce velmi chybělo!
$php_errormsg s některými fatálními chybami funguje, např. "parse error", s jinými ale ne, např. "memory limit".
Pro detekci fatálních chyb by se dalo na legitimním konci skriptu nastavit nějakou konstantu nebo proměnnou a tu v shutdown funkci detekovat (mělo by to jít udělat přes auto_append_file). To ale není moc elegantní a navíc to vyžaduje zapnutí ignore_user_abort().
Celé je to takové divné, že? Kéž bych si pamatoval, na co konkrétně jsem tehdy zkoušel ten error_get_last()... V PHP 5.2.9 jsem každopádně nepřišel na nic, co by nezachytil.
Místo ignore_user_abort() se dá testovat connection_status(), málem bych na tuto funkci opět zapomněl, i když jsem její název nedávno docela pracně hledal.
Místo append file IMHO lze použít destruktor. Pro uchování instance až do konce lze použít nejen trik s vlastností rovnou this (do 5.3), ale i statická vlastnost třídy. Jen nesmí nastat fatal error po destruktoru. Pořadí volání teď nemám chuť zkoušet. Navíc, pokud by to nebylo zdokumentované, nemá smysl se na to spoléhat (kompatibilita).
Někdy kolem 2002-2003 jsem použil vlastní output buffer handler a parsování výsledné stránky, pokud skript nedoběhl, jak měl (resp. pokud se ručně nezavolala funkce, která nastavila OK flag):
<?php
function _my_ob_handler($buffer)
{
// clean exit flag set means no fatal errors
if ($this->enabled != FALSE && $this->catch_parse_fatal != FALSE && connection_aborted() == FALSE && $this->_clean_exit == FALSE) {
if (ini_get('html_errors') == '')
// |1-type |2-msg |3-file |4-line
$re = '{\r?\n((?:fatal|parse)[^:]+): +(.+) +in (.+) on line ([0-9]+)}i'; // for html_errors = Off
else
// |1-type |2-msg |3-file |4-line
$re = '{<br(?: /)?>\r?\n<b>((?:fatal|parse)[^<]+)</b>: +(.+) +in <b>([^<]+)</b> on line <b>([0-9]+)</b><br(?: /)?>}i'; // for html_errors = On
// ...
?>
ale error_get_last() je mnohem muckovatější muck.
Pro cesky-hosting.cz je tenhle skriptik must-have :-) Diky!
Diskuse je zrušena z důvodu spamu.