ugrás a tartalomhoz

Fehér halál, avagy a PHP fatal error nyomában

Eddie · 2010. Dec. 13. (H), 11.56

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:

Grats. You broke it.
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!

 
1

Köszi

janoszen · 2010. Dec. 14. (K), 08.38
Köszönjük a cikket, érdekes megoldás. Mint azt szóban is megbeszéltük, azt érdemes figyelembe venni, hogy ez amolyan felemás módon van dokumentálva, ergó könnyen lehet, hogy ez a fícsör mindenféle changelog bejegyzés nélkül eltűnik a PHP-ból.

Rendszergazdai oldalról annyit, hogy ahhoz, hogy ez működjön, be kell kapcsolni az error trackinget.
3

marmint melyik feature-re

Tyrael · 2010. Dec. 14. (K), 12.09
marmint melyik feature-re gondolsz hogy eltunik?
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
4

registrer_shutdown_function

Eddie · 2010. Dec. 14. (K), 14.50
A doksi szerint a fenti function-ban definiált callback függvény akkor fut le, ha a script futtatása befejeződött, vagy exit() került meghívásra. Nem írják le konkrétan, hogy fatal error esetén ez mindig le fog futni. Ha lefut, akkor az error_get_last() biztosan működni fog, mivel ez a function is a request része.
5

kosz hogy leirtad megegyszer

Tyrael · 2010. Dec. 14. (K), 16.15
kosz hogy leirtad megegyszer ugyanazt, amire rakerdeztem. :)
http://bugs.php.net/bug.php?id=48969
masodik comment:
[2009-07-20 09:12 UTC] derick##kukac##php.net
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
2

üdv. örülök hogy valaki

Tyrael · 2010. Dec. 14. (K), 12.03
üdv.

ö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
6

recoverable errors

joed · 2010. Dec. 20. (H), 13.42
Hello!

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:

/**
 * Objektum környezetben.
 */
public function __construc(){
	set_error_handler(array(&$this, "recoverableErrorHandler"));
}

/**
 * Recoverable error handler.
 *
 * @param int $code Error number.
 * @param string $message Error message.
 * @param string_type $file File where error raised.
 * @param int $line Line where error raised.
 * @return bool
 */
public function recoverableErrorHandler($code, $message, $file, $line) {
	if ($code === E_RECOVERABLE_ERROR) {
		throw new ErrorException($message, 0, $code, $file, $line);
		return true;
	}
	return false;
}
Persze nem helyettesíti a a cikkben leírt körültekintő módszereket, de talán hasznos lehet mások számára is.
7

"Van egy szelete a fatal

Tyrael · 2010. Dec. 20. (H), 13.59
"Van egy szelete a fatal error családnak"
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
8

de fatal

joed · 2010. Dec. 20. (H), 15.29
> "Van egy szelete a fatal error családnak"
> a recoverable error az nem fatal error, pont ezert is van lehetoseg az elkapasara.
Idéznék:
E_RECOVERABLE_ERROR (integer): Catchable fatal error. It indicates that a probably dangerous error occured, but did not leave the Engine in an unstable state. If the error is not caught by a user defined handle (see also set_error_handler()), the application aborts as it was an E_ERROR.
[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 :)
9

De ezen nem veszünk össze.

Tyrael · 2010. Dec. 20. (H), 16.17
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.

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.

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.

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.


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 :)


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
10

Elgépelés?

Arnold Layne · 2011. Jan. 2. (V), 23.10
A 4.1-es PHP-ban tehát használhatunk okimenti puffert...

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)
11

kimeneti akart szerintem

Tyrael · 2011. Jan. 3. (H), 02.14
kimeneti akart szerintem lenni.
furcsa, hogy senkinek nem szurta ki a szemet...

Tyrael
12

Javítva

Török Gábor · 2011. Jan. 3. (H), 13.04
Kösz a jelzést.
13

osszedobtam egy egyszeru kis

Tyrael · 2011. Feb. 18. (P), 15.14
osszedobtam egy egyszeru kis hibakezelo fajlt en is, itt elerheto:
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
14

Örülök, hogy megtaláltam,

prom3theus · 2011. Már. 9. (Sze), 14.58
Örülök, hogy megtaláltam, nagyon hasznos. Korábban volt róla blogmark is vagy tweet, meg is volt nyitva egy lapon nálam hogy majd elteszem könyvjelzőbe, de mire oda jutottam, valami rejtélyes "nem találni ilyen oldal" típusú hibaüzenetet adott a GH. Szerencse, hogy itt is linkelted, mert guglival is hiába kerestem 1 órán át, erre a cikkre fejből emlékeztem.
15

nincs mit, orulok, hogy

Tyrael · 2011. Már. 9. (Sze), 20.21
nincs mit, orulok, hogy megtalaltad. :)

Tyrael