Autoload, call_user_func_array i parent::__construct

Pracuje właśnie nad mechanizmem automatycznie generującym klasy potomne dziedziczące po istniejącej już w systemie klasie bazowej.

class A
{
	public function __construct()
	{}
 
	public function index()
	{
		// do something
	}
}

Mając powyższą klasę bazową wystarczy wygenerować klasę potomną, która w konstruktorze wywoła konstruktor parenta.

class B extends A
{
	public function __construct()
	{
		parent::__construct();
	}
}

Tyle, że konstruktor klasy bazowej może mieć jakieś parametry.

class A
{
	private $param;
 
	public function __construct($param)
	{
		$this->param = $param;
	}
 
	public function index()
	{
		// do something
	}
}

Ponieważ chcę napisać generator w miarę uniwersalny, a nie wiem ile parametrów i jakie może mieć, każda z klas bazowych, które będę chciał rozszerzyć dobrym rozwiązaniem wydawało by się wywoływanie klasy parenta przy pomocy funkcji „call_user_func_array”.

class B extends A
{
	public function __construct()
	{
		$args = func_get_args();
		call_user_func_array(array('parent', '__construct'), $args);
	}
}

Po utworzeniu obiektu:

$obj = new B('foo');

Wszystko działało zgodnie z oczekiwaniami, było fajnie tylko jak włączyłem raportowanie błędów ze sprawdzaniem składni

error_reporting(E_ALL^E_STRICT);
ini_set("display_errors", 1);

To zobaczyłem taki nieprzyjemny komunikat „**Strict standards: Non-static method A::__construct() cannot be called statically, assuming $this from compatible context B**”.

Powiem szczerze, że tak nie za bardzo wiedziałem co mam począć w tej sytuacji bo najbardziej intuicyjne rozwiązanie okazało się niezgodne ze standardami. Zajrzałem oczywiście do manuala gdzie znalazłem rozwiązanie. Przy okazji dowiedziałem się jeszcze, że inaczej powinno to wyglądać dla PHP w wersji 5.0 – 5.2, a już inaczej w PHP 5.3 i prawdopodobnie 6.

class D extends A
{
	public function __construct()
	{
		$args = func_get_args();
		if (version_compare(PHP_VERSION, '5.3.0', '>')) {
			call_user_func_array('parent::__construct', $args);
		} else {
			call_user_func_array(array($this, 'parent::__construct'), $args);
		}
	}
}

Gdyby to zamykało temat zapewne nie stworzyłbym tego wpisu, tyle, że pojawił się kolejny problem.

W moim eksperymentalnym frameworku używam loadera klas, który w razie nieznalezienia wywoływanej klasy skanuje zadane foldery w poszukiwaniu plików php, a następnie sprawdza czy znajdują się w nich definicje klas lub interfejsów. Następnie tworzy mapę w postaci tablicy, w której kluczem jest nazwa klasy, a wartością ścieżka do pliku zawierającego definicję owej klasy.

Otóż zauważyłem, że przy każdym wywoływaniu „call_user_func_array(array($this, ‚parent::__construct’), $args);” następuje uruchomienie autoloadera i w moim przypadku skanowanie plików w poszukiwaniu klasy „parent”. Gdyby nie związane z tym wydłużenie obsługi requesta zapewne nie zauważyłbym nawet wywołanie autoloadera gdyż pomimo, że funkcja autoloadera zwraca false kod wykonuje się prawidłowo nie generując żadnych błędów lub ostrzeżeń.

Nie mniej koniecznym okazało się spatchowanie autoloadera

function autoload($class) {
	if ($class == 'parent') {
		return false;
	}
	// ...
}
spl_autoload_register('autoload');

FirePhp

[Firebug](http://getfirebug.com/) jest już obowiązkowym dodatkiem do Firefoxa dla każdego webdevelopera. Jest on wręcz niezbędny i osobiście nie wyobrażam sobie pracy z css-em bez niego. Jako programista produkujący skrypty wykonywane raczej po stronie serwera oczywiście doceniam jego użyteczność, ale dopiero w połączeniu z [FirePHP](http://www.firephp.org/) popadam w euforię ;).

Przy okazji tylko wspomnę, że istnieje odpowiednik FirePhp dla Pythona [FirePy](http://code.google.com/p/firepy/wiki/Documentation), Ruby, Perla i paru jeszcze innych języków o czym można się przekonać wchodząc na stronę [wiki projektu](http://www.firephp.org/Wiki/). W tym samym miejscu przekonamy się, że FirePHP lub jego odpowiedniki został już zintegrowany z całą masą co bardziej popularnych frameworków i aplikacji jak np. Zend Framework, Symfony, Kohana, Typo3, Prado, Seagull, WordPress, eZ itd. Nie dziwi to gdyż jak się sam przekonałem użycie FirePhp jest wręcz banalne.

Wziąwszy pod uwagę wielkość [dokumentacji użycia](http://www.firephp.org/HQ/Use.htm) ograniczającą się do jednej, niewielkiej strony, można by pomyśleć, że FirePHP nie jest zbyt imponującą biblioteką albo jest kiepsko udokumentowana. Okazuje się, że funkcjonalność FirePHP jest rzeczywiście ograniczona, ale jest to ograniczenie w sensie pozytywnym. Znajdziemy tu tylko to, co jest naprawdę przydatne. I tak mamy:

* możliwość wielopoziomowego (log, info, warn, error) logowania danych do konsoli Firebuga – możemy też dane przedstawić w formie tabeli.
* możliwość zrzutu danych prezentowanego w zakładce „sieć” Firebuga
* możliwość włączenia errorhandlera i exceptionhandlera przechwytujących błędy i wyjątki php i logujących je do konsoli Firrebuga
* możliwość wyświetlenia backtrace-a

FirePHP nie jest pozbawiony błędów lub raczej niedoskonałości, które ujawniają się w trakcie próby przesłania zbyt wielkiej liczby danych lub zbyt długo trwającej rekurencji dlatego też boję się oprzeć swojego debuggera jedynie o ten dodatek. Z drugiej strony podoba mi się np. [firebug_profiler](http://github.com/nickdunn/firebug_profiler) do CMS-a Symphony napisany przez Nicka Dunna, co skłania mnie do eksperymentowania z tym dodatkiem i badania granic jego przydatności.

Na razie ograniczam użycie FirePHP do bieżącego debugowania aplikacji. Dotychczas chcąc szybko wyświetlić zawartość jakieś zmiennej używałem var_dump-a. Ponieważ użycie var_dump-a w wielu miejscach rodziło pewne problemy z jego późniejszym odszukaniu zdefiniowałem sobie w Eclipsie taki mały snippet, który oprócz właściwej zmiennej wyświetlał też nazwę pliku oraz linię w której var_dump został wywołany. Wyglądało to mniej więcej tak:

var_dump('FILE:'.__FILE__. ' LINE:'.__LINE__, $var); //DEBUG

Zważywszy, że snippet wywoływany był za pomocą skrótu klawiaturowego dodawanie ‚FILE:’.__FILE__. ‚ LINE:’.__LINE__ nie było zbyt upierdliwe, jednak postanowiłem tę kwestię rozwiązać w bardziej elegancki sposób.

class Logger
{
	private static $_firePhp;
 
	public static function log($object, $label = null)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->log($object, $label);
		$_fb->groupEnd();
	}
 
	public static function warn($object, $label = null)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->warn($object, $label);
		$_fb->groupEnd();
	}
 
	public static function info($object, $label = null)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->info($object, $label);
		$_fb->groupEnd();
	}
 
	public static function error($object, $label = null)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->error($object, $label);
		$_fb->groupEnd();
	}
 
	public static function table($label, $table)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->table($label, $table);
		$_fb->groupEnd();
	}
 
	public static function dump($key, $variable)
	{
		$_fb = self::_getFirePhp();
		$_fb->dump($key, $variable);
	}
 
	private static function _getFirePhp()
	{
		if (is_null(self::$_firePhp)) {
			require_once('FirePHPCore/FirePHP.class.php');
			$_firephp = FirePHP::getInstance(true);
			$_options = array('maxObjectDepth' => 10,
				'maxArrayDepth' => 20,
				'useNativeJsonEncode' => true,
				'includeLineNumbers' => true);
			$_firephp->setOptions($_options);
			self::$_firePhp = $_firephp;
		}
		return self::$_firePhp;
	}
 
	private static function _prepareMetaData()
	{
		$_level = 1;
		try {
			throw new Exception();
		} catch (Exception $_e) {
			$_trace = $_e->getTrace();
			return sprintf('FILE: %s LINE %s TIME %s'
				, $_trace[$_level]['file']
				, $_trace[$_level]['line']
				, date("h:i:s", time()));
		}
	}
}

Jak widać do wyłuskania meta danych użyłem wyjątku. Szybka analiza stacktracea i w miarę prosty sposób uzyskałem informację o miejscu wywołania loggera. Teraz wyświetlając jakąś zmienną w konsoli FireBuga wiem nie tylko o tym co ta zmienna zawiera, ale też w sytuacji, w której zapomniałbym o usunięciu tymczasowego wpisu, mogę łatwo go odnaleźć i naprawić swoje niedopatrzenie 😀