Lepszy var_dump czyli przyjemniejsze debugowanie PHP

bigWeb/Debug/Dumper to narzędzie funkcjonalnie odpowiadające funkcji var_dump
Jego przewagą jest sposób prezentacji danych, a także dodatkowe informacje
ułatwiające debugowanie aplikacji.

Najpoważniejszą wadą Dumpera jest to, że jest on dość obciążający dla aplikacji
gdyż uzyskanie informacji o pliku i linii, w której dump został wywołany
wymaga każdorazowo rzucenia wyjątku. Dlatego też w wersji produkcyjnej
Dumper powinien być wyłączony

UWAGA! Biblioteka zaprezentowana w przykładach wymaga min PHP 5.3
z uwagi na użycie przestrzeni nazw.
Ponieważ jednak nie na wszystkich serwerach jest już PHP w wersji obsługującej
przestrzenie nazw przygotowałem także wersję Dumpera nie wymagającą ich użycia.
w takim przypadku wywołanie bigWeb\Debug\Dumper::factory(); należy zastąpić
poprzez wywołanie bigWeb_Debug_Dumper::factory(); i analogicznie w przypadku
innych klas. Wersja Dumpera dla PHP < 5.3 zawarta jest w archiwum zip bigWeb\Debug\Dumper do ściągnięcia.

Instalacja

include_once('./Debug.php');

Można też użyć autoloadera. Wszystkie klasy potrzebne do działania Dumpera
zdefiniowane są w pliku „Debug.php”. Wyjątkiem jest FirePHP, który należy
dodać osobno jeśli chcemy wyświetlać dane w konsoli javascript

include_once('./FirePHP.php');

Podstawowe użycie

dump('some data');

w wyniku otrzymamy:

TIME: 20:46:44 FILE: /home/www/bigWeb/Debug/example.php LINE: 37 ET: 0 MU: 1.44 mb MPU: 1.67 mb

‚some data’
string(9) "some data"

Dumpa można wywołać z wieloma parametrami o różnych wartościach równocześnie.

$str = 'text';
$int = 7;
$arr = array('foo', 'bar');
$ob = new ArrayObject();
$bool = FALSE;
 
dump($str, $int, $arr, $ob, $bool);
TIME: 20:46:44 FILE: /home/www/bigWeb/Debug/example.php LINE: 47 ET: 0.000904 MU: 1.47 mb MPU: 1.67 mb

$str
string(4) "text"
$int
int(7)
$arr
array(2) {
  [0]=>
  string(3) "foo"
  [1]=>
  string(3) "bar"
}
$ob
object(ArrayObject)#3 (1) {
  ["storage":"ArrayObject":private]=>
  array(0) {
  }
}
$bool
bool(false)

Ukrywanie komunikatów

Aby zapobiec wyświetlaniu jakichkolwiek komunikatów przez dumpera należy go wyłączyć.

bigWeb\Debug\Dumper::setEnabled(FALSE);
dump('it should not show');

Aby ponownie włączyć:

bigWeb\Debug\Dumper::setEnabled(TRUE);

Definiowanie alternatywnych logerów

Domyślnie dumper do zrzutu danych używa wbudowanej funkcji var_dump jednak
klasa Dumpera jest zbudowana w oparciu o wzorzec projektowy Obserwator, gdzie
obserwatorami są wyspecjalizowane klasy do logowania i prezentacji zrzucanych danych.
Dzięki temu możemy wybierać sposób logowania informacji.

Możemy zapisywać dane w pliku – przydatne np. przy testowaniu przekierowań

$d = bigWeb\Debug\Dumper::factory();
$o = new bigWeb\Debug\Dumper\FileDump();

Koniecznym jest wskazanie katalogu w którym będą zapisywane logi.
Katalog ten musi mieć oczywiście ustawione prawa do zapisu

$o->setDir(dirname(__FILE__));
$d->attach($o);
$d->notify('foo');

wynik zostanie zapisany w pliku /home/www/bigWeb/Debug/2012-01-12.log.php

TIME: 20:46:18 FILE: /home/zh/www/Debug/example.php LINE: 71 ET: 0.001143 MU: 1.55 mb MPU: 1.67 mb
----------| 'foo' |----------
foo
.----------------------------

Można też skorzystać z dobrodziejstw FireBuga – dodatku do Firefoxa i
wyświetlać dane w konsoli javascript. W tym przypadku musimy najpierw
załadować bibliotekę FirePHP będącej „pomostem” pomiędzy PHP i FireBugiem.

include_once('./FirePHP.php');
 
$d = bigWeb\Debug\Dumper::factory();
$d->attach(new bigWeb\Debug\Dumper\FireDump());
$d->notify('bar');

wynik pokarze nam się w konsoli FireBug-a

Wynik Dumpera w konsoli FireBug

UWAGA! Ponieważ informacje do FireBuga są przesyłane za pomocą nagłówków HTTP
biblioteka FirePHP wymaga buforowania wyjścia (output bufering). Koniecznym
jest zatem użycie funkcji ob_start() w przeciwnym razie można się spotkać
z błędem „headers already sent error”

Łańcuch wywołań

Niekiedy chcemy wiedzieć jakie funkcje i metody zostały wywołane nim został
wykonany kod w danym miejscu. Aby zobaczyć cały łańcuch wywołań należy
ustawić flagę show_trace na TRUE.

function foo() {
    $d = bigWeb\Debug\Dumper::factory();
    $d->attach(new bigWeb\Debug\Dumper\VarDump());
    $d->setShowTrace(TRUE);
    $d->notify('Show chain requests');
}
 
function bar() {
    foo();
}
 
bar();
TIME: 20:46:44 FILE: /home/www/bigWeb/Debug/example.php LINE: 97 ET: 0.005149 MU: 1.57 mb MPU: 1.67 mb

$d->notify(‚Show chain requests’);
string(19) "Show chain requests"
Array
(
    [0] => Array
        (
            [file] => /home/www/bigWeb/Debug/example.php
            [line] => 97
            [function] => notify
            [class] => bigWeb\Debug\Dumper
            [type] => ->
            [args] => Array
                (
                    [0] => Show chain requests
                )

        )

    [1] => Array
        (
            [file] => /home/www/bigWeb/Debug/example.php
            [line] => 101
            [function] => foo
            [args] => Array
                (
                )

        )

    [2] => Array
        (
            [file] => /home/www/bigWeb/Debug/example.php
            [line] => 104
            [function] => bar
            [args] => Array
                (
                )

        )

)

Praca na serwerze produkcyjnym

W zasadzie Dumper powinien być wyłączony na serwerze produkcyjnym bo obciąża
aplikację, a ponadto może wyświetlać dane wrażliwe. Nie mniej w pewnych
wyjątkowych okolicznościach możemy chcieć go użyć. Musimy wcześniej przewidzieć
taką sytuację i dodatkowo skonfigurować przynajmniej jedną z instancji Dumpera
ustawiając tak zwany secret_key

$d = bigWeb\Debug\Dumper::factory();
$d->attach(new bigWeb\Debug\Dumper\FireDump());
$d->setSecretKey('verysicretkey');

Aby te dane były widoczne należy wywołać url metodą GET z parametrem
secret_key=1. W tym przypadku będzie to

example.php?verysicretkey=1
bigWeb\Debug\Dumper::setEnabled(FALSE);
$d->notify('Visible when set secret_key');

aby ponownie włączyć:

bigWeb\Debug\Dumper::setEnabled(TRUE);

Jeśli wywołamy url metodą GET z sekretnym kluczem, Dumper ustawia ciasteczko
debugcookie o wartości wywiedzionej z secret_key. Od tej pory nie trzeba już dodawać do
adresu żadnego specjalnego parametru. Jeśli jednak chcielibyśmy wyłączyć
debugowanie należy wywołać url z secret_key=0

example.php?verysicretkey=0

Tworzenie funkcji pomocniczych

Tworzenie obiektu i dodawanie obserwatorów nie jest zbyt wygodnym rozwiązaniem.
Narzędzie do debugowania powinno być extremalnie proste i szybkie w użyciu.
Dlatego warto sobie zdefiniować funkcję pomocniczą – podobną do „debug();”

function dump_all() {
    $_args = func_get_args();
    static $d = null;
    if ( $d === null )
    {
        // Proszę zwrócić uwagę na wywołanie metody factory z parametrem $level = 3
        $d = bigWeb\Debug\Dumper::factory(3);
        $o = new bigWeb\Debug\Dumper\FileDump();
        $o->setDir(dirname(__FILE__));
        $d->attach($o);
        $d->attach(new bigWeb\Debug\Dumper\FireDump());
        $d->attach(new bigWeb\Debug\Dumper\VarDump());
 
    }
    call_user_func_array(array($d, 'notify'), $_args);
}
 
dump_all('foo bar');

wynik zostanie zapisany w pliku

TIME: 20:46:44 FILE: /home/zh/Praca/bigWeb/Debug/example.php LINE: 155 ET: 0.006137 MU: 1.59 mb MPU: 1.67 mb
----------| 'foo bar' |----------
foo bar
.--------------------------------

w konsoli FireBug-a

Wynik Dumpera w konsoli FireBug

oraz wyświetlony w przeglądarce:

TIME: 20:46:44 FILE: /home/www/bigWeb/Debug/example.php LINE: 155 ET: 0.006137 MU: 1.59 mb MPU: 1.67 mb

‚foo bar’
string(7) "foo bar"

UWAGA! Od wartości level zależy prawidłowe wskazanie linii oraz pliku, w którym
wywołano dumpa. Domyślnie level = 1. Jeżeli metoda „notify” jest zagnieżdżona
w funkcji pomocniczej to wartość level powinna być inkrementowana (level = 2).
Jeśli dodatkowo metoda „notify” jest wywoływana za pośrednictwem funkcji
„call_user_func_array” to należy ustawić level = 3

$d = bigWeb\Debug\Dumper::factory(1);
 
function d1($param) {
    $d = bigWeb\Debug\Dumper::factory(2);
    // ...
    $d->notify($param);
}
 
function d2($param) {
    $_args = func_get_args();
    $d = bigWeb\Debug\Dumper::factory(3);
    // ...
    call_user_func_array(array($d, 'notify'), $_args);
}

UWAGA! Jeśli masz już zdefiniowaną funkcję „dump” w swojej aplikacji
to po dołączeniu kodu Dumpera otrzymasz wszystkomówiący wyjątek. Poinformuje
Cię on, że musisz zdefiniować sobie funkcję pomocniczą o innej nazwie.
Należy zakomentować kod wywołujący wyjątek lub też dołączyć kod Dumpera
w sposób umożliwiający przechwycenie wyjątka i utworzyć funkcję pomocniczą
np. o nazwie „d”.

try { include_once("Debug.php"); } catch (bigWeb\Debug\Exception $e) {
    function d() {
        $_args = func_get_args();
        static $d = null;
        if ( $d === null )
        {
            $d = bigWeb\Debug\Dumper::factory(3);
            $d->attach(new bigWeb\Debug\Dumper\VarDump());
 
        }
        call_user_func_array(array($d, "notify"), $_args);
    }
}

Napisana przeze mnie klasa nie zastąpi zaawansowanych i rozbudowanych narzędzi debugowania i profilowania aplikacji jednak jest prosta w użyciu nie związana stricte z żadnym frameworkiem, przez co łatwo ją zaadaptować zarówno do pracy z Zend Frameworkiem, Symfony, Kohaną czy jakimkolwiek innym – napisanym w PHP – skryptem. Biblioteka zwraca wyniki w postaci pokolorowanego kodu przy okazji pokazując czas wykonania oraz wielkość użytych zasobów. Zapomniany var_dump potrafi zmusić programistę do przeszukiwania plików projektu. W przypadku Dumpera nie ma takiego zagrożenia gdyż każdorazowo wskazuje on ścieżkę do pliku oraz nr linii, w której funkcja robiąca zrzut danych została wywołana.

Serdecznie zapraszam wszystkich do wypróbowania Dumpera oraz wszelkich uwag na temat wdrożeń i ewentualnych błędów.

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 😀