Egy érdekes PHP autoload bug
Sikerült ma egy érdekes kis PHP bugot fogni. Épp nekiálltunk átállni az új AmfPHP-re, minden szépen és jól ment, de kollégámnak (]-[appy) lett egy fura hibája. Egy addig teljesen jól működő osztály használatakor a PHP azt mondta, hogy nem található az osztály. Miközben ő debuggolta az autoloadunkat én is belefutottam egy hibába: kis debuggolás után kiderült, hogy ugyanarról az esetről van szó. A pontos felállást most nem vázolnám itt, de sikerült szépen lecsupaszítani egy egyszerű verzióra.Erre a következő hibaüzenetet kapjuk: "Fatal error: Class 'bar' not found".
A fenti kód nem tűnik túl realisztikusnak, de például a következő már egy teljesen legitim helyzet:Sajnos ez esetben a PHP (szerintem hibásan, de kíváncsi vagyok az internals lista véleményére) félrevezető hibaüzenetet ad.
■ <?php
throw new Exception();
class foo {}
?>
<?php
class bar extends foo {}
?>
<?php
function __autoload($className)
{
include $className.'.php';
}
new bar();
?>
A fenti kód nem tűnik túl realisztikusnak, de például a következő már egy teljesen legitim helyzet:
<?php
define('error', oops_i_left_the_quotes);
class foo {}
?>
<?php
class bar extends foo {}
?>
<?php
function __autoload($className)
{
include $className.'.php';
}
function error_handler()
{
throw new Exception();
}
set_error_handler("error_handler");
new bar();
?>
Szerintem ez nem bug
Előszöris a probléma tömöreb megfogalmazása álljon itt, ugyanis ismerősömnek megmutatva sajna nem szúrta ki hogy hol is itt a bibi. Tehát a probléma forrása, hogy a foo.php-ban (a második kódcsoport példánál) van egy define, aminél a második paramétert, azaz magának az error konstansnak az értékét nem quote-oltuk, tehát először a PHP interpreter ellenőrzi hogy már létező konstans-e. Ha nem, akkor dob egy E_NOTICE-t (gondolom ez E_STRICT-é változna, ha be lenne kapcsolva), hogy nem definiált konstans és ezért úgy veszi hogy ez egy string. Ez dokumentált viselkedés de nagyon ajánlott ezt elkerülni!
Ezek után a kód folytatódik és az error konstans is értelmezve lesz.
Viszont ha a fenti példához hasonlóan van saját error handlerünk és az exception-t dob, akkor jelentkezik a Felhő által is tapasztalt probléma, azaz nincs meg a Bar class.
Akkor íme az én érvelésem hogy miért nem bug:
1) Feltételezem, hogy a legtöbb default install PHP-ban (így a Felhőjében is) az error reporting E_ALL & ~E_NOTICE. Tehát a Notice-okat az interpreter lenyeli, nyomát error hnadler nélkül nem látjuk.
2) A PHP.net-en a következők olvashatóak a set_error_handler doksijában:
Tehát ha definiálunk saját error handlert a set_error_handlerrel, akkor bizony a PHP sajátja már nem fut le.
3) AFAIK a php belső error handlerében van implementálva a hibajelentés beállításainak értelmezése. Tehát AFAIK a php.ini vonatkozó beállításai itt érvényesülnek, nem pedig C-ben minden egyes hibaesetnél külön-külön.
4) a set_error_handler doksijában van még egy releváns kijelentés:
Azaz, ha a mi kis error handlerünk, hibakezelőnk visszatér, akkor a script végrehajtás folytatódik a hibát okozó utasítást követő utasítással. Ebből explicit következik (mivel vagy visszatér, vagy nem), hogyha nem tér vissza, akkor nem folytatódik a végrehajtás. Azt is vegyük figyelembe, hogyha nem is adunk meg return-t egy függvényben, akkor is visszatér, AFAIK NULL-lal. Tehát hogyan lehetne elérni hogy ne térjen vissza az error handler: die(), exit(), explicit processzgyilok ééééés:
5) Exception dobása. Hogyan viselkedik az Exception a PHP-ban:
Tehát ha dobunk egy Exception-t az error handlerünkben, akkor bizony a kód normál végrehajtási szála megszakad az első egyező catch-ig, vagy ha ilyen nincs, akkor az interpreter dob egy fatal error-t.
6) Mi történik tehát. Az error handlerünk, annak ellenére hogy a php.ini-ben az van, hogy nem kell nekünk a notice, megkapja azt is. A mi feladatunk hogy ellenőrizzük ezt a beállítást az ini_get-ekkel, persze ha akarjuk. Amúgy illene is!
Szóval megkajuka notice-t hogy undefined constant, amire ellenőrzés nélkül exception-t dobunk. Tehát megszakítjuk a normal code flow-t. Természetesen azt még figyelembe kell vennünk, hogy a define ugye nem függvény, kiértékelése nem RUNTIME történik, hanem PARSE vagy COMPILE time. Ergo hamarabb jön meg a notice-unk, minthogy az interpreter betöltötte volna a foo class-t.
Ebből következőleg a bar extends-e nem elégíthető ki, hiszen még a class listában nem létezik a foo, hiszen az őt tartalmazó file-t nem tudtuk végigparse-olni, megszakított minket az error handler exception-je.
Így a stack-ben két hibánk is lesz: Az exception, amit dobtunk, meg ennek eredményeként az is, hogy nem tudtuk kielégíteni a bar extend-ét sem.
Remélem sikerült világosan leírnom a gondolataimat.
Még két dolog:
Az hogy az hiba-e hogy az interpreter szól azért is, mert nem tudta kielégíteni a class függőséget azon túl hogy az exception-t is látjuk, döntse el mindenki maga. Szerintem nem hiba, mivel teljesen a valóságot közli. Maximum a túl sok info kategóriába tartozik, ami becsaphat minket.
Ha pedig az érvelésem rossz, mondjátok, vagy jöhetnek a balták ;)
autoload elnyeli az exceptiont
Először a fenitre reagálva:
if (!($errno & error_reporting())) {return;}
, és akkor még a @ esete is le van kezelve, mert a saját hibakezelő ilyenkor is meghívódik.Ami pedig a gondot okozta (nekem :)): a manuál pontatlan. Azt írja, hogy az autoload-ban dobott hibát nem lehet elkapni, és ezért fatal errort okoz. De valójában az történik, hogy az autoload elnyeli az exceptiont ( És ami miatt ez nekünk szívás volt az az, hogy az AMFPHP egy ilyen exceptionös cuccal vágja felül a mi hibakezelőnket, így nem értesültünk a hibáról). És ezen infó birtokában már kb. áll ami a manuálban van, bár egy kérdésem még volt, kíváncsi vagyok mit válaszolnak:
define('error', oops_i_left_the_quotes);
class foo {
}
?>
In this case I got bar not found.
<?php
class foo {
const error = oops_i_left_the_quotes;
}
?>
In this case I got the exception.
So the difference is that class level constants are creating after the autoload?
Üdv,
Felhő
u.i. Belinkelnék egy hibakezelésről szóló doksit, amit még belső oktatási anyagnak csináltam. Szerettem volna belőle cikket, de mostanában úgyis esélytelen, ezért ez a téma kitűnő alkalom a megosztására.
Bug-nembug
belekontárkodom kicsit: ha jól sejtem, akkor a PHP az autoloadból kilépve azonnal megnézi, hogy a kívánt osztály létezik-e és ha nem, akkor el is halálozik. Azt a tudást, hogy exception hatására lépett ki, nem kezelték le. Ugyanezért nem lehet magában az autoloadban exceptiont dobni (pedig szerettem volna).
A probléma megkerülésére én azt csináltam, hogy az exception handlerben trigger_errorral dobtam hibát (ami nyilván nálatok ebben a konstrukcióban nem működne). Az error handler nem lép ki az autoloadból, utána ugyanott folytatja a futását, tehát tudja loggolni a hibát.
[off]
Próbálkoztunk mi is hibából exception konvertálni, de miután ezen és még N random helyen erre nincs fölkészítve a PHP, a hangos tiltakozás ellenére kivágtam a kukába a hibakezelőt.
[/off]
[off 2]
Föladtam azon próbálkozásomat hogy a PHP-ba más, jobban végiggondolt OOP-s nyelvekhez hasonló funkcionalitást gyógyítsak, mert a végén úgyis mindig a PHP fejlesztőcsapat nyer, amikor egy huszárvágással beletesz valamit, amitől az előző hackem nem működik. :]
[/off 2]
Disclaimer: lehet hogy hülyeséget írtam, javítsatok ki ha így lenne, nem volt időm végiggondolni a problémát.
Próba
ez miért jó?
Üdv,
Felhő
a throw nem volt jó
Bér lehet, hogy a print után egy exit; nem ártott volna :)
oké, de így mi értelme van?
Felhő
éppen ki mit lát benne :)
Én kísérlet képpen csináltam a saját oldalamon. A célom csupán annyi volt, hogy a trigger_error sokszor szegényes és visszakövethetetlen hibaüzeneteit kicsit feljavítsam. Ezt a kitűzést teljesíti is. A trace az pont jó erre.
A kivétel kivétel maradt (csak szebb kimenettel, mint alapból), a trigger error meg hellyel-közzel kivétel lett, de legalább ugyanolyan formában adta hírül a bajait, mint a rendes kivétel.
A third-party cuccok meg sokszor csak trigger_error-oznak, szóval az én szememben még hasznosnak is bizonyul. Plusz, ahol a kommenttel jeleztem, ott nálam valóban van logolás is, sokszor vettem ennek is hasznát.
de mire jó ott az exception?
Üdv,
Felhő
u.i.: Jobbulást, hallottam mi történt. :(
csak a trace
Amúgy ez is egy tipikus példája az én nyakatekert megoldásaimnak :)
u.i.: köszi, már kezd funkcionálni a vállam, a jövő héten remélhetőleg már mehetek dolgozni :)
Óóó van ennél cifrább is :D