ugrás a tartalomhoz

Egy érdekes PHP autoload bug

Hodicska Gergely · 2008. Júl. 10. (Cs), 05.45
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.
<?php
throw new Exception();
class foo {}
?>
<?php
class bar extends foo {}
?>
<?php
function __autoload($className)
{
    include $className.'.php';
}
new bar();
?>
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:
<?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();
?>
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.
 
1

Szerintem ez nem bug

tolmi · 2008. Júl. 10. (Cs), 11.21
Bár nem vagyok internals, azért én is oknyomoztam egy kicsit és 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:
It is important to remember that the standard PHP error handler is completely bypassed.

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:
If the error-handler function returns, script execution will continue with the next statement after the one that caused an error.

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:
When an exception is thrown, code following the statement will not be executed, and PHP will attempt to find the first matching catch block. If an exception is not caught, a PHP Fatal Error will be issued with an "Uncaught Exception ..." message, unless a handler has been defined with set_exception_handler().

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 ;)
2

autoload elnyeli az exceptiont

Hodicska Gergely · 2008. Júl. 10. (Cs), 13.08
A fenti érvelés pár helyen nem pontos, de nem is a lényeg itt, hanem hogy az autoload-ból nem jön ki az exception. Meg amúgy sem magyarázná az első esetet, ahol nincs is hibakezelő. A hibakezelős verziót csak azért írtam, hogy látszódjon egy legitim felhasználás,

Először a fenitre reagálva:
Feltételezem, hogy a legtöbb default install PHP-ban (így a Felhőjében is) az error reporting E_ALL & ~E_NOTICE.
Ezt sértésnek veszem. ;) De csak félig, mert van még pár régi kódunk, ahol nem lehetett kikapcsolni a NOTICE-t, de az E_ALL a vesszőparipám, elég sok olyan NOTICE van, ami konkrét program hiba.

Tehát ha definiálunk saját error handlert a set_error_handlerrel, akkor bizony a PHP sajátja már nem fut le.
Ezért is illik egy ilyesmi a hiba kezelő elejére: 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.

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:
Azt meg nem mondom Neked, hogy van-e különbség aközött, hogy explicit visszatérsz NULL-lal vagy sem, de a saját hibakezelőből FALSE-szal kell visszatérni, hogy a PHP belső hibakezelője mégis lefusson.

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.
Itt értem, hogy mire gondolsz, de jelen esetben ez nem gond, és kipróbálod, beteheted az exception dobást a foo definició után is, akkor is ez a jelenség.

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.
Ez nem így van, lásd:
<?php
function foo()
{
	echo BAR;
}

foo();

define('BAR', 1);
?>
A fenti kód NOTICE-ol, de amúgy pont ezért is ajánlott (ha teheti az ember) osztály szintű konstansokat használni, mert azok complie time veszik el az időt, valamint az elérésük is gyorsabb, mert egy kisebb lookup table-ben kell keresni őket.

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:
<?php
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.
3

Bug-nembug

janoszen · 2008. Júl. 11. (P), 13.41
Hali,

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

Próba

Gixx · 2008. Júl. 14. (H), 14.16
Bár ez tudom, hogy nem az autoload-os problémára ad közvetlen megoldást, sőt lehet, hogy csak amtőrködök, és ezer sebből vérzik az a példa, amit mindjárt mutatok, de én egy helyen az alábbi módon konvertáltam error-ból exception-t:

<?php

class CoreException extends Exception
{
	public function __construct($message = null, $code = 0)
	{
		parent::__construct($message, $code);
	}
	
	/**
	 * ez állítja be a trigger_error-os hibáknál a kivételkezelőben a sort
	 * @param int $line - a sort, ahol a hiba kiváltódott
	 */
	public function setLine($line) 
	{  
		$this->line = $line; 
	} 
  
  /**
	 * ez állítja be a trigger_error-os hibáknál a kivételkezelőben a fájlt
	 * @param string $file - a file, amiben a hiba kiváltódott
	 */   
	public function setFile($file) 
	{ 
		$this->file = $file; 
	} 
	
	/**
	 * összeállítja a hibaüzenetet, mind az eldobott kivételek, mind a trigger_error kezelő számára
	 * @return $string - teljes hibaüzenet
	 */
	public function myString()
	{
	  $codenames = array(1 => 'Error', 2 => 'Warning', 8 => 'Notice');
	  $code = $this->getCode();
	  
	  if(isset($codenames[$code]))
	  {
	  	$code = $codenames[$code];
	  }
	  
	  $str = 'Uncaught Exception:<br />'."\r\n";
		$str .= 'Code: '.$code.'<br />'."\r\n";
		$str .= 'Message: '.$this->getMessage().'<br />'."\r\n";
		$str .= 'File: '.$this->getFile().'<br />'."\r\n";
		$str .= 'Line: '.$this->getLine().'<br />'."\r\n";
		$str .= 'Trace: <pre>'.print_r($this->getTraceAsString(),true).'</pre>'."\r\n";
		
		return $str;
	}
	
	/**
	 * kivétel eldobásakor automatikusan meghívódik
	 */
	public function __toString()
	{
		return self::evalError($this);
	}

	/**
	 * ez kezeli le a nem kivétel jellegű felhasználói hibákat és rendszer üzeneteket 
	 * @param int $code - hibakód
	 * @param string $message - hibaüzenet
	 * @param string $file - a file, amiben a hiba kiváltódott
	 * @param int $line - a sor, ahol a hiba kiváltódott
	 * @param mixed $context
	 */
	public static function errorHandler($code, $message, $file, $line, $context)
	{
		$exception = new CoreException($message, $code); 
		$exception->setLine($line); 
		$exception->setFile($file);
		print CoreException::evalError($exception); 
	}
	
	/**
	 * ez kezeli le kimenetet
	 * @param object $exception - CoreException objektum
	 */
	public static function evalError(CoreException $exception)
	{
		// itt lehet akár log fájlba iratást is csinálni
		return $exception->myString();
	}
}

set_error_handler(array('CoreException','errorHandler'), E_ALL);

throw new CoreException('hiba van');
trigger_error('hiba van');

?>

5

ez miért jó?

Hodicska Gergely · 2008. Júl. 14. (H), 19.51
Nem igazán látom a fenti kód értelmét. Még ha print helyett throw lenne, de így most jön egy hiba, átalakítod exceptionné, majd kiprinteled, ennek így nincs sok teteje.


Üdv,
Felhő
6

a throw nem volt jó

Gixx · 2008. Júl. 16. (Sze), 11.57
Már régen írtam ezt, de arra emlékszem, hogy végigkísérleteztem a lehetőségeket, és azt hiszem azért lett print a throw helyett, mert throw esetén elveszett volna a file, line és a code valós értéke. De ez nem biztos, de ez rémlik döntő okként.

Bér lehet, hogy a print után egy exit; nem ártott volna :)
7

oké, de így mi értelme van?

Hodicska Gergely · 2008. Júl. 16. (Sze), 15.46
Üdv,
Felhő
8

éppen ki mit lát benne :)

Gixx · 2008. Júl. 16. (Sze), 18.02
Tény, hogy profi rendszerekbe nem használnám én se. ZF-be meg egyenesen fölösleges.

É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.
9

de mire jó ott az exception?

Hodicska Gergely · 2008. Júl. 16. (Sze), 21.59
Most komolyan nem értem. Mit használsz ki az exception-ből? Van egy függvényed, ami kap paramétereket. Ezeket a paramétereket átpasszintod egy exceptionnek, aminek van egy metódusa, ami formázva megjeleníti ezeket az adatokat. Miért nem maga az error handler függvény teszi ezt meg?


Üdv,
Felhő

u.i.: Jobbulást, hallottam mi történt. :(
10

csak a trace

Gixx · 2008. Júl. 17. (Cs), 12.40
Lényegében erre volt nekem szükségem:
getTraceAsString();
Ha ezt ki lehet nyerni a sima error-ból, végülis az is jó, de én nem tudom, hogy lehet másképp.

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

Óóó van ennél cifrább is :D

inf3rno · 2010. Júl. 25. (V), 23.48

function throwException()
{
	throw new Exception();
}

spl_autoload_register('throwException',true);

class MyClass implements NonExistingInterface
{}
Ez nekem kinyírja apache szervert php 5.3.0-nál. Elvileg már javították. Ha nem létező interface-nél dobsz exception-t autoload közben, akkor a php5ts.dll valamit elgányol és szétcsúszik minden. Konkrétan annyira, hogy windowst újra kell indítani, mert nem lehet helyreállítani máshogy a szervert.