Własna obsługa błędów i zamiana na wyjątki

Błędy zdarzają się wszędzie i każdemu, natomiast raczej nie jest wskazane „chwalenie się” nimi przed użytkownikami aplikacji.

Konfiguracja

Podstawowyą kwestią jest ich ukrycie na środowisku produkcyjnym przed oczami użytkownika, tym bardziej że w stosie wywołań mogą się pojawić wrażliwe dane użytkownika/aplikacji/systemu/inne. Do ukrycia „wpadki” używa się właściwości display_errors (php.ini). Właściwość pozwala włączyć lub wyłączyć pokazywanie błędów oraz skierować je na odpowiednie wyjście:

  • Off – wyłączenie pokazywania błędów (do użycia na „produkcji”)
  • stderr – przekierowanie błędów na STDERR (działa tylko z CGI/CLI)
  • On/stdout – pokazanie błędów/przekierownie na standardowe wyjście, czyli pokazanie komunikatów w treści strony wyświetlonej użytkownikowi (przydatne w trakcie programowania)

Jednak ukrycie komunikatów błędów w żaden sposób nie rozwiązuje faktycznego problemu w aplikacji. Do tego potrzebne są komunikaty błędów i to tych znaczących. Do filtrowania komunikatów służy dyrektywa error_reporting. Jest to poziom raportowania błędów, które będą wyświetlane/pokazywane – możliwych wartości jest cała masa, a dodatkowo można je łączyć – lista stałych.

Obie dyrektywy (w zależności od uprawnień czy konfiguracji środowiska) mogą zostać ustawione na poziomie pliku php.ini, definicji vhosta, pliku .htaccess, czy w samym kodzie PHP. Ostatni wariant wygląda następująco:

<?php
ini_set('display_errors', 1);
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE);

var_dump(ini_get('display_errors')); // pokaże: string '1' (length=1)
var_dump(ini_get('error_reporting')); // pokaże: string '22519' (length=5)

W PHP wartości dyrektyw można zmieniać i odczytywać funkcjami ini_set() / ini_get(). Dla dyrektywy error_reporting jest dedykowana funkcja – wywołana z argumentem zmienia wartość, natomiast bez argumentu zwraca obecną wartość, nie zmienia to jednak faktu, że można używać standardowych funkcji. Oczywiście nie wszystkie dyrektywy można zmieniać w powyższy sposób – tutaj znajduje się lista dyrektyw z oznaczeniem sposobu zmiany wartości i opis poziomów.

Wywołanie błędu

<?php
echo 'punkt 1'. PHP_EOL;
trigger_error('Test wywolania bledu', E_USER_NOTICE);
echo 'punkt 2'. PHP_EOL;
trigger_error('Test wywolania bledu', E_USER_ERROR);
echo 'punkt 3'. PHP_EOL;

Do wywołania błędu z poziomu kodu służy funkcja trigger_error() – pierwszy argument to własny komunikat, natomiast drugi to typ błędu. W zależności od tego jak bardzo błąd jest poważny wykonywanie skryptu jest kontynuowane lub przerywane, co widać poniżej w wyniku wykonania powyższego kodu:

punkt 1

Notice: Test wywolania bledu in /home/cim/public_html/Testy/error.php on line 12
Call Stack
#	Time	Memory	Function	Location
1	0.0001	230360	{main}( )	../error.php:0
2	0.0002	231520	trigger_error ( )	../error.php:12

punkt 2

Fatal error: Test wywolania bledu in /home/cim/public_html/Testy/error.php on line 14
Call Stack
#	Time	Memory	Function	Location
1	0.0001	230360	{main}( )	../error.php:0
2	0.0003	231520	trigger_error ( )	../error.php:14

Po wystąpieniu błedu typu „Notice” wykonywanie przebiega dalej (np. pokazanie „punkt 2”), natomiast błąd typu „Fatal error” przerywa wykonanie (brak wyświetlenia „punkt 3”).

Przechwytywanie

Błąd został zgłoszony i przy odpowiedniej konfiguracji został wyświetlony na ekranie. Docelowo jednak efekt ma być zupełnie inny, błędy powinny być zgłaszane do odpowiednich osób. PHP posiada przydatną funkcję – error_log – która wspomaga wysyłanie e-maili, dodawnie komuniaktów błędów do systemowego logu, tworzenie własnego logu. Jednak można również samemu wysyłać e-maile (poprzez serwer SMTP) czy zapisywać do pliku.

W obu przypadkach należy w jakiś sposób przechwycić informację o wystąpieniu błędu. Realizuje się to poprzez utworzenie własnej funkcji i zarejestrowanie jej za pomocą set_error_handler().

<?php
define('LOG_SEPARATOR', ';');
ini_set('display_errors', 1);
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE);

function obsluga($kodBledu, $komunikat, $plik, $linia, $zmienne)  {
    $elementy = array(
        date('H:i:s'),
        $kodBledu,
        $komunikat,
        $plik,
        $linia,
        //print_r($zmienne, true)
    );
    error_log(implode(LOG_SEPARATOR, $elementy) . PHP_EOL, 3, 'logi_'. date('Ymd') .'.log');
}

set_error_handler('obsluga');

echo 'punkt 1'. PHP_EOL;
trigger_error('Test wywolania notice', E_USER_NOTICE);
echo 'punkt 2'. PHP_EOL;
trigger_error('Test wywolania bledu', E_USER_ERROR);
echo 'punkt 3'. PHP_EOL;

Powyższy kod spowoduje, że na ekranie zostanie wyświetlone tylko: „punkt 1, punkt 2, punkt 3”. Natomiast zarejestrowana funkcja przechwytująca informację o błędzie obsłuży w sposób „cichy” testowe wywołania błędów. Do funkcji przekazywane są takie informacje jak: poziom błędu, komunikat oraz plik i linia, w których błąd został znaleziony/zgłoszony i zmienne towarzyszące kontekstowi wywołania.

W tym przypadku dodatkowo dane zostały zapisane do pliku (generowanego osobno dla każdego dnia) z logami, gdzie poszczególne dane (oddzielone średnikami) zawarte są w jednym wierszu razem z godziną wystąpienia problemu. W 13 wierszu znajduje się przekazanie do pliku z logami zrzutu zmiennych, które w tej chwili jest nie aktywne, ponieważ zaburzałoby jednowierszowy podział dla zdarzeń. Jednak warto zapisywać również te dane, ponieważ pomagają w odnalezieniu źródła problemu.

Przy rozwiązywaniu problemu lub diagnozie błędu, który występuje w bardzo specyficznych okolicznościach przydatna jest funkcja debug_backtrace(). Zwraca ona tablicę ze stosem wywołań od uruchomienia skryptu do wywołania jej samej – dla każdego wywołania podane są szczegółowe informacje pozwalające prześledzić kolejne kroki wykonania skryptu.

Zamiana na wyjątki

Ostatnią kwestią, którą poruszę jest zamiana wystąpień klasycznych ostrzeżeń/błędów na wyjątki. W PHP istnieje specjalnie zdefiniowany w tym celu typ wyjątku – ErrorException. Sposób użycia został pokazany poniżej poprzez modyfikację poprzedniego przykładu.

<?php
define('LOG_SEPARATOR', ';');
ini_set('display_errors', 1);
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE);

function obsluga($kodBledu, $komunikat, $plik, $linia)  {
    throw new ErrorException($komunikat, $kodBledu, 0, $plik, $linia);
}

set_error_handler('obsluga');

echo 'punkt 1'. PHP_EOL;
trigger_error('Test wywolania notice', E_USER_NOTICE);
echo 'punkt 2'. PHP_EOL;
trigger_error('Test wywolania bledu', E_USER_ERROR);
echo 'punkt 3'. PHP_EOL;

Natomiast efekt uruchomienia będzie się prezentował następująco (tylko początek):

punkt 1
Fatal error: Uncaught exception 'ErrorException' with message 'Test wywolania notice' in ...

Funkcja przechwytująca błędy przekazała swoje parametry do wywołania wyjątku ErrorException, wyjątek został rzucony… i niestety zakończyło to się błędem, ponieważ wyjątek nie został złapany. Żeby temu zaradzić należałoby każde miejsce, w którym błąd jest zgłaszany ręcznie zamknąć w bloku try/catch i/lub zdefiniować globalną funkcję obsługującą nieprzechwycone wyjątki. To jest jednak temat na jeden z następnych wpisów.

Klasyczne błędy generowane przez interpreter lub zgłąszane ręcznie pozwalają na zakomunikowanie programiście, że coś jest nie tak i pomagają w rozwiązaniu problemu. Jednak spełniają rolę tylko informacyjną – zapisanie do logu, wysłanie powiadomienia na e-mail czy pokazanie użytkownikowi strony z informacją o błędzie aplikacji. Natomiast w oparciu o mechanizm obsługi błędów nie ma możliwości reagowania na konkretne i przewidywalne problemy (jeżeli pojawia się wywołanie trigger_error() to programista przewidział ewentualny problem). Rozwiązaniem jest wykorzystywanie wyjątków, które mogą być przechwytywane globalnie (tak jak błędy) oraz lokalnie dając programiście szanę na obsługę konkretnego zaistniałego problemu (a nie tylko w oparciu o poziom błędu).

Linki:

Dodaj komentarz