Fehér halál, avagy a PHP fatal error nyomában
Nem szeretnék egy újabb cikket írni a PHP-ben való hibakezelés fontosságáról. Azt azért mégis megemlítem, hogy egy komoly(nak szánt) rendszer esetén nincs nagyobb lúzerség a világon, mint amikor a felhasználó egy linkünkre kattintva az alábbi üzenettel találkozik:
Parse error: syntax error, unexpected T_ECHO, expecting ',' or ';' in /home/kisbogaram/public_html/administrator/templates/khepri/index.php on line 119
Ennél már az is lényegesen jobb, ha a egy üres képernyővel találja magát szemben, tehát éles környezetben szigorúan:
ini_set('display_errors', '0');
Mi azonban hiba esetén inkább valami ilyet szeretnénk látni:
A Blizzard hibaoldala
Triviális megoldás (Apache esetén) az egyedi hibaoldal használata 500-as (és egyéb) hibákra is, nem csak 404-re.
ErrorDocument 500 /errordocs/500.html
Ezzel már majdnem meg tudunk felelni az önmagunk felé támasztott magas elvárásoknak. Ha azonban azt szeretnénk, hogy hiba esetén rendszerünk végezzen el valamiféle fontos műveletet (logoljon, mentsen el egy stack trace-t, értesítsen minket stb.), akkor ez még önmagában nem elég.
Ekkor jön a PHP set_error_handler()
eljárása. Ennek szépségeire most nem térek ki, de ki-ki nézze át, ha még nem használta. Ezzel el tudjuk kapni a legtöbb felmerülő hibát, azonban még mindig ott van a fatal error. Az ellen nem véd (sőt, még a vízbűl sem veszi ki a zoxigént).
Van azonban egy kedves duó, akik használatával el tudunk marni szinte minden felmerülő hibát.
Egyik jó barátunk a register_shutdown_function()
. Ennek megadhatunk egy callbacket, amely a szkript futása után végrehajtódik. Fontos tudni, hogy 4.1-es PHP verzió óta ez a szkript részeként fut le, tehát küldhetünk belőle kimenetet, illetve elkaphatjuk a puffer tartalmát pl. az ob_get_contents()
-el. A 4.1-es PHP-ban tehát használhatunk kimeneti puffert, és a kimenetünket egy preg_match()
-csel átvizsgálva kiszűrhetjük a végzetes hibákat. Ez a praktika sok egyébre is használható, pl. egy keretrendszernél vizsgálhatjuk, hogy a rendszer kimente csak és kizárólag a sablon által előállított kód legyen, ezzel elkerülve a véletlenül bent egyéb (pl. var_dump()
) kikerülését a felhasználókhoz. Erre vannak persze más módszerek is (pl. SVN pre-commit hurok), de ha valamiért szeretnénk tisztán PHP-ból megoldani, hát hajrá.
Az 5.0-s verzió felett azonban hibakezelésben sokkal könnyebb a dolgunk ennél, ugyanis ott van nekünk az error_get_last()
. Ez a remek függvény visszaadja nekünk a legutóbbi el nem kapott hibát egy ilyen tömbben:
Array(
[type] => 8
[message] => Undefined variable: a
[file] => C:\WWW\index.php
[line] => 2
)
Ezzel már nyitva áll előttünk a lehetőség, hogy kulturált hibakezelőt írjunk a végzetes hibák elkapására, anélkül, hogy megbocsáthatatlan bűnt követnénk el a hatékonyság könyörtelen istenének templomában.
Íme egy működő példa:
register_shutdown_function('shutdown');
function shutdown()
{
if (($error = error_get_last()) && isset($error['type'])
&& ($error['type'] & (E_ERROR | E_PARSE | E_COMPILE_ERROR)))
{
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
header('Content-Type: text/html; charset=utf-8');
}
echo '<h1>Baj van</h1>';
echo '<p>„Baj van, baj, baj van, baj.<br />';
echo 'Gyenge a fű, hülye a csaj<br />';
echo 'Nedves a dohány, kevés a lé<br />';
echo 'Baj van, baj, ez nem oké.”</p>';
echo '<cite>Lukács László – Tankcsapda</cite>';
echo '<code>' . print_r($error, true) . '</code>';
}
}
Az ezek után következők már bármiféle csúnyaságot elkövethetnek. Íme egy kód, ami el is követi mindazt, amit csak el lehet követni:
set_time_limit(1);
ini_set('memory_limit', '5000');
echo 'fdsgfds'
gonoszFuggyvenz();
function gonoszFuggyveny()
{
echo str_repeat('tucc tucc tucc ', 50000);
for ($i = 1; $i < 10000000; $i++) {
$x += tan($i);
}
}
Szintaktikailag hibás a negyedik sorban. Nem létező függvényt hív az ötödikben. Túllépi a memória és az időkorlátot.
Miért írtam akkor mégis, hogy „a legtöbb” felmerülő hibát? Ennek a módszernek is vannak bizonyos limitációi:
- Ha a
register_shutdown_function()
-t tartalmazó állomány szintaktikailag hibás, az ellen nem véd sem isten, sem ördög. E_CORE_ERROR
a PHP inicializálása alatt keletkezhet. Ezt sem tudjuk sehogyan kivédeni, de elvileg mi ilyen hibát nem is tudunk előállítani PHP kódból.
Jó vadászatot!
■
Köszi
Rendszergazdai oldalról annyit, hogy ahhoz, hogy ez működjön, be kell kapcsolni az error trackinget.
marmint melyik feature-re
az hogy a register_shutdown_function -ben definialt fuggveny vegrehajtasra kerul a hiba utan, vagy hogy innen el lehet erni az utolso hibat?
track_errors csak ahhoz kell hogy bekapcsolva legyen, hogy a $php_errormsg valtozobol elerhesd az utolso hibat, az error_get_last() enelkul is mukodik.
Tyrael
registrer_shutdown_function
kosz hogy leirtad megegyszer
http://bugs.php.net/bug.php?id=48969
masodik comment:
I am reopening this. Because even with fatal errors, the shutdown handler should run. It always has so if that changes it is a BC break. Assigning to Dmitry.
ez alapjan szerintem a kovetkezo major verzio kiadasaig nem fog valtozni ez a viselkedes.
Tyrael
üdv. örülök hogy valaki
örülök hogy valaki vette a fáradtságot, hogy megirta a cikket.
viszont szerintem a php4-es hivatkozásokat nyugodtan ki lehetett volna hagyni, nem supportált már évek óta.
Tyrael
recoverable errors
Kiegészíteném még egy kis PHP5-ös adalékkal. Én jobb szeretem, ha kivételeket dobál a kódom. Van egy szelete a fatal error családnak, ami mégsem annyira fatális, hogy térdre kényszerítse a kódodat. Ezek az ún. "recoverable" hibák. Mivel nekem a kivételek a fétisem, ezt szoktam alkalmazni:
"Van egy szelete a fatal
a recoverable error az nem fatal error, pont ezert is van lehetoseg az elkapasara.
a peldadhoz annyit hozzatennek, hogy hasznos, viszont oda kell figyelni ra, hogy minden fejleszto tisztaban legyen vele, hogy , illetve nehogy feluldefinialjak az error handleredet (Zend Framework SOAP osztalya pl. ezt csinalja, szopott is a debugolassal az egyik kollegam...).
a masik dolog, hogy a te kodod ebben az esetben az aktualis error reporting beallitastol fuggetlenul mindig dob exception-t, ami eleg problemas tud lenni.
tehat hogy kicsit erzekletesebben mutassam be, hiaba allitod be az error_reporting -ban, hogy te nem akarsz notice-okat kapni, akkor is meg fog hivodni az error handler, amiben mivel nem vizsgalod az aktualis error_reporting erteket, mindig el fogod dobni a kivetelt.
ami a meginkabb nem trivialis problema, hogy ugyanigy hiaba nyomnal el egy hibat @ segitsegevel, valojaban az is csak annyit csinal, hogy elmenti az aktualis error_reporting erteket, majd atallitja 0-ra, majd vegrehajtja az utasitast, vegul visszaallitja az error_reportingot.
emiatt a te kodod az ilyen esetekben is eldobna az exceptiont, ami megint nem tul logikus, raadasul elegge csunyan fog nezni rad, akinek ezt debugolni kell.
az utolso dolog, amit mondani szerettem volna, hogyha exception-okkel oldod meg a hibakezelest, akkor ne felejts el set_exception_handler-rel gondoskodni az esetlegesen el nem kapott kivetelek kezeleserol, amely handler tartalmazzon egy jo nagy try/catch blokkot, kulonben az ott fellepo hibak egy szinten elegge nehezen ertelmezheto "Exception thrown without a stack frame" uzenetet fognak dobni egy segfault tarsasagaban.
Tyrael
de fatal
> a recoverable error az nem fatal error, pont ezert is van lehetoseg az elkapasara.
Idéznék:
[http://php.net/manual/en/errorfunc.constants.php]
De ezen nem veszünk össze. Lényeg, hogy ennél a hibatípusnál van lehetőség azelőtt elkapni a hibát, mielőtt valódi ékszíjledobós végzetes hibára fut a kód.
> a masik dolog, hogy a te kodod ebben az esetben az aktualis error reporting beallitastol fuggetlenul mindig dob exception-t, ami eleg problemas tud lenni.
Problémás lehet abban az esetben, ha nem kezeled a kivételeket normálisan. Én speciel minden hibát kivételre dobok, aztán a kivételkezelésben döntök róla, hogy mit kezdjek a hibával. Az idézett kód célja, hogy a hibás működésből eredő, de még helyrehozható hibákat kezelje és nem az, hogy a hibát "eltusolja". Attól, hogy az error_reporting-gal elnémítod a kódod, még a hibák létezni fognak ugyebár :) Igen, ahogy mondod pont emiatt fogja eldobni a kivételt, amivel majd szépen a megfelelő catch-ben foglalkozol.
> emiatt a te kodod az ilyen esetekben is eldobna az exceptiont, ami megint nem tul logikus, raadasul elegge csunyan fog nezni rad, akinek ezt debugolni kell.
Ez a kód csak abban az esetben dobja el a kivételt, ha recoverable a hiba. Mi a probléma ezzel? És a debuggolással? A kivétel stacktrace-ében benne van az elkapott hiba debug_backtrace()-es stack-je is.
Végülis holtáig tanul az ember, ezért kíváncsi lennék az ellenvetésekre :)
De ezen nem veszünk össze.
egyetertek, bar szerencsetlennek tartom, hogy az angol dokumentacio osszemossa a recoverable error-t a fatal-okkal, ugyanis teljesen maskepp mukodik a fatalis hibak kezelese a zend engine-ben, mint a nem fatalis hibake.
na most ket eset lehet: vagy minden hibabol kivetelt csinalsz, ahogy irod is az elso mondatban, ebben az esetben ott vannak a problemak amiket emlitettem. minden kivaltott hibara rafut az error handler, es ha nem vizsgalod benne az error reportingot, akkor erhetnek meglepetesek (E_STRICT/E_DEPRECATED/E_NOTICE-ra nem valoszinu hogy eles kornyezetben meg akarjuk szakitani a vegrehajtast, vagy esetleg egy utasitas nem veletlenul van elnyomva @-cal(pl. egy fopen-nel nem lehet garantalni, hogy ott van-e a keresett fajl.))
a masik eset, hogy csak a E_RECOVERABLE_ERROR-t kezeled a az error handleredben, ami meg jo is lehet, bar azt erdemes hozzatenni, hogy nagyon keves helyen van hasznalva a php-n belul, sokkal tobb helyen lehetne:
lasd itt
szoval ha csak erre hasznalod, akkor az ugy okes.
egyetertek, elsosorban azert szeretem az exceptionokkel torteno hibakezelest, mert lokalisan van lehetoseged kezelni a hibat (elerheto minden valtozo a donteshez, egyszeruen meg lehet ismetelni a muveletet, etc.).
Tyrael
Elgépelés?
Ez itt gépelési hiba lenne, vagy csak egy olyan elnevezés, amiről még nem hallottam? (feltételezhetően a google sem)
kimeneti akart szerintem
furcsa, hogy senkinek nem szurta ki a szemet...
Tyrael
Javítva
osszedobtam egy egyszeru kis
https://github.com/Tyrael/php-error-handler
aki kivancsi ra, hogy hogyan lehet E_CORE_ERROR -t eloidezni php-bol, pl igy:
<?php
$foo = new ReflectionClass('StdClass');
clone $foo;
Tyrael
Örülök, hogy megtaláltam,
nincs mit, orulok, hogy
Tyrael