Co się powinno dziać gdy w programie zostanie napotkany błąd? W najlepszym przypadku: poinformuje nas o tym kompilator lub interpreter, co – przynajmniej w teorii – zmusi nas do jego naprawienia. Innym dobrym rozwiązaniem jest przerwanie wykonywania programu tak by nieprawidłowa operacja nie wyrządziła żadnych szkód. Opcjonalnie dobrze by było by aplikacja mogła przechwycić tego typu błąd, by później go wyświetlić w zjadliwej formie. W najgorszym wypadku, aplikacja zignoruje ten błąd i będzie kontynuowała wykonywanie.
Problem
PHP posiada dwa mechanizmy raportowania błędów, które wpisują się w całe spektrum przedstawione wyżej. Ma ono wyjątki (Exception) jak i błędy (trigger_error, ale nie możemy mylić tego ze specjalną wersją wyjątku Error, czy też inną specjalną wersją wyjątku ErrorException). Miło by było, gdyby w następnej wersji jedna z tych funkcjonalności została porzucona (i by to były błędy) na korzyść drugiej (czyli wyjątków).
Wyjątków tutaj nie chce jakoś mocno omawiać, bo są one zaimplementowane podobnie jak w każdym normalnym języku programowania, a jeśli macie doświadczenie z Javą to się szybko z nimi odnajdziecie.
Błędy są specyficznym mechanizmem który składa się z dwóch kroków: pierwszy to wyświetlenie na ekranie błędu który właśnie wystąpił, a drugi to – w zależności od tego czy interpreter potrafi poradzić sobie z błędem – przerwanie skryptu albo wykonanie go zakładając że to czego nie ma to NULL.
Tego typu błędy są wyzwalane (z ang. trigger) w zasadzie w każdej sytuacji gdy PHP nie potrafi poradzić sobie z błędem, dla przykładu:
<?php
$array = [
// this array doesn't have any key
];
// this line will emit warning
var_dump($array['missing-key']);
// this line will also emit warning
var_dump($non_existing_var);
Jeśli wykonamy taki kawałek kodu, to dostaniemy taką informacje z interpretera:
PHP Notice: Undefined index: missing-key in /home/psychob/Projekty/Blog/php-error-handling-weirdness/01-php-emitting-errors.php on line 9
PHP Stack trace:
PHP 1. {main}() /home/psychob/Projekty/Blog/php-error-handling-weirdness/01-php-emitting-errors.php:0
/home/psychob/Projekty/Blog/php-error-handling-weirdness/01-php-emitting-errors.php:9:
NULL
PHP Notice: Undefined variable: non_existing_var in /home/psychob/Projekty/Blog/php-error-handling-weirdness/01-php-emitting-errors.php on line 12
PHP Stack trace:
PHP 1. {main}() /home/psychob/Projekty/Blog/php-error-handling-weirdness/01-php-emitting-errors.php:0
/home/psychob/Projekty/Blog/php-error-handling-weirdness/01-php-emitting-errors.php:12:
NULL
Jak można zauważyć, PHP założył że w zmiennych których nie ma znajduje się wartość NULL
.
Innym miejscem gdzie interpreter nas poinformuje błędem, jest jeśli podamy do funkcji niepoprawną ilość argumentów, na przykład:
<?php
var_dump(strlen('string', 'bad-argument'));
var_dump(strlen());
Taki skrypt na wyjściu da nam:
PHP Warning: strlen() expects exactly 1 parameter, 2 given in /home/psychob/Projekty/Blog/php-error-handling-weirdness/02-incorrect-argument-count-to-system-function.php on line 5
PHP Stack trace:
PHP 1. {main}() /home/psychob/Projekty/Blog/php-error-handling-weirdness/02-incorrect-argument-count-to-system-function.php:0
PHP 2. strlen() /home/psychob/Projekty/Blog/php-error-handling-weirdness/02-incorrect-argument-count-to-system-function.php:5
/home/psychob/Projekty/Blog/php-error-handling-weirdness/02-incorrect-argument-count-to-system-function.php:5:
NULL
PHP Warning: strlen() expects exactly 1 parameter, 0 given in /home/psychob/Projekty/Blog/php-error-handling-weirdness/02-incorrect-argument-count-to-system-function.php on line 6
PHP Stack trace:
PHP 1. {main}() /home/psychob/Projekty/Blog/php-error-handling-weirdness/02-incorrect-argument-count-to-system-function.php:0
PHP 2. strlen() /home/psychob/Projekty/Blog/php-error-handling-weirdness/02-incorrect-argument-count-to-system-function.php:6
/home/psychob/Projekty/Blog/php-error-handling-weirdness/02-incorrect-argument-count-to-system-function.php:6:
NULL
Ten konkretny przypadek może być jednak zmieniony w wyjątek automatycznie jeśli skorzystamy z „nowej” funkcjonalności PHP czyli declare(strict_types=1)
, na przykład:
<?php
declare(strict_types=1);
var_dump(strlen('string', 'bad-argument'));
var_dump(strlen());
Ale nie zadziała to przy wywoływaniu funkcji które my zdefiniowaliśmy, na przykład:
<?php
declare(strict_types=1);
function my_func($abc, $def)
{
}
my_func(1, 2, 3, 4);
Jak widać, PHP wyzwoliło dwa rodzaje błędów: Notatkę (Notice)
i Ostrzeżenie (Warning). Oba typy błędów charakteryzują
się podobnym zachowaniem: Poinformuj użytkownika, i kontynuuj
wykonywanie zakładając że to czego nie ma jest równe NULL
.
Ale PHP posiada jeszcze inne typy błędów, o – bardzo intuicyjnej nazwie – Błąd (Error). W tej puli znajdują się błędy które nie pozwalają na poprawne wykonanie skryptu, na przykład nie poprawna składnia, nie istniejąca klasa, nie istniejąca funkcja, itp.:
<?php
return unknown_function('?');
Zwróci nam:
PHP Fatal error: Uncaught Error: Call to undefined function unknown_function() in /home/psychob/Projekty/Blog/php-error-handling-weirdness/05-fatal-error-unknown-function.php:2
Stack trace:
#0 {main}
thrown in /home/psychob/Projekty/Blog/php-error-handling-weirdness/05-fatal-error-unknown-function.php on line 2
<?php
interface /* Empty */ {
}
Zwróci nam:
PHP Parse error: syntax error, unexpected '{', expecting identifier (T_STRING) in /home/psychob/Projekty/Blog/php-error-handling-weirdness/06-fatal-error-bad-grammar.php on line 3
Co możemy zrobić?
Pierwszą rzeczą która przychodzi na myśl, to po prostu wyłączenie raportowania błędów
error_reporting(0)
error_reporting pozwala na wyłączenie wyświetlania błędów przez PHP. Jest to na pewno przydatna funkcjonalność którą wypada skonfigurować na środowisku produkcyjnym, a jej działanie wygląda tak:
<?php
/** @noinspection PhpUndefinedVariableInspection */
error_reporting(0);
$array = [
// this array doesn't have any key
];
// this line will emit warning
var_dump($array['missing-key']);
// this line will also emit warning
var_dump($non_existing_var);
Na wyjściu powinniśmy dostać:
/home/psychob/Projekty/Blog/php-error-handling-weirdness/07-error-reporting.php:11:
NULL
/home/psychob/Projekty/Blog/php-error-handling-weirdness/07-error-reporting.php:14:
NULL
Tylko nie jest to idealne rozwiązanie, dlatego że błąd w kodzie nadal jest, my tylko spowodowaliśmy że nie można go zobaczyć. Dlatego idealnym rozwiązaniem byłoby gdybyśmy mogli taki błąd zmienić w wyjątek. I da się to zrobić przy pomocy paru narzędzi które twórcy nam przygotowali, a mówiąc konkretnie set_error_handler
i ErrorException
.
set_error_handler
set_error_handler jest funkcją która pozwala nam na zastąpienie domyślnego error handlera naszym własnym. Funkcja ta przyjmuje dwa argumenty: funkcje która ma być wywołana w przypadku błędu jak i dla których typów błędu ma być wykonana.
Przykładowa funkcja która zacznie rzucać wyjątki w przypadku wystąpienia błędu wygląda tak:
set_error_handler(function (int $level, string $message, string $file, int $line) {
if (error_reporting() === 0) {
return;
}
throw new \ErrorException($message, 0, $level, $file, $line);
});
Taka implementacja zapewni nam że wszystkie błędy które można przechwycić, rzucą wyjątek. Warto tutaj też zwrócić uwagę na warunek który zwróci nam wartość null z funkcji, jest to obsługa specjalnego operatora który istnieje w PHP: ‘@’ tak zwany operator STFU. O ile nie jest zalecane korzystanie z niego, to nigdy nie wiemy która z naszych zależności będzie z niego korzystać. Co do zwracanej wartości, to jeśli nasz handler zwróci wartość false, to kontrole przejmie domyślny handler (ten co wyświetla błąd na standardowym wyjściu).
Ale co z błędami których przechwycić się nie da? Bo jak to przystało na PHP, istnieją błędy których się nie da przechwycić przy pomocy set_error_handler, dla przykładu:
<?php
set_error_handler(function (int $level, string $message, string $file, int $line) {
if (error_reporting() === 0) {
return;
}
throw new ErrorException($message, 0, $level, $file, $line);
});
interface foo
{
public function bar();
}
class baz implements foo
{
}
Co można w takim wypadku zrobić?
register_shutdown_function
Należy zaznaczyć tutaj, że tego typu błędów nie da się przechwycić w taki sposób by można było kontynuować normalną egzekucje programu, dlatego że w większości przypadków jest to brak zaimplementowanej metody która jest w interfejsie, abstrakcyjna metoda itp. Nie zmienia to faktu, że możemy zareagować na tego typu błąd i możemy wyświetlić swoją informacje o błędzie.
By to zrobić, należy podpiąć się pod „shutdown function”, czyli funkcje która będzie wykonywana po zakończeniu skryptu, służy do tego funkcja register_shutdown_function. I nie ma tu rozróżnienia czy wykonywanie zakończy się sukcesem czy porażką. W naszym przypadku taka funkcja mogłaby wyglądać tak:
register_shutdown_function(function () {
$lastError = error_get_last();
if ($lastError !== NULL) {
throw new ErrorException($lastError['message'], 0, $lastError['type'], $lastError['file'],
$lastError['line']);
}
});
Jak można zauważyć z implementacji, sprawdzamy jaki był ostatni błąd i jeśli nie został obsłużony to rzucamy wyjątek. Tutaj pojawia się pytanie: Skoro ten kawałek kodu wykona się już po zakończeniu skryptu, to jak taki wyjątek złapać?
Nie da się, nawet przy pomocy set_exception_handler
. By przejąć tego typu wyjątek należy go samemu przekazać do naszego exception handlera, albo obsłużyć go w tej funkcji.
set_exception_handler
set_exception_handler działa tak samo jak set_error_handler, z tym wyjątkiem że ta funkcja jest wykonywana gdy zostanie rzucony wyjątek i nie zostanie on złapany przy pomocy bloku try...catch
. Po wykonaniu naszego exception handlera, wykonywanie skryptu się zakończy.
<?php
set_error_handler(function (int $level, string $message, string $file, int $line) {
if (error_reporting() === 0) {
return;
}
throw new ErrorException($message, 0, $level, $file, $line);
});
set_exception_handler(function (Throwable $e) {
var_dump($e->getMessage());
var_dump($e->getTraceAsString());
});
var_dump($array); // not existing variable
set_error/exception_handler a register_shutdown_function
Warto wspomnieć o różnym zachowaniu między tymi funkcjami. set_error/exception_handler
przy wywołaniu podmienią aktualnego handlera, a starego wrzucą na stos którego można przywrócić przy pomocy funkcji restore_error/exception_handler
. Takie zachowanie może się przydać, jeśli chcemy błędy obsługiwać „blokowo”, czyli w części aplikacji będziemy chcieli zamieniać błędy na wyjątki (bo ta część je obsługuje), a w innej chcemy na przykład je ignorować, bo jest to część legacy naszej aplikacji.
register_shutdown_function
, rejestruje kolejną funkcje która zostanie uruchomiona po zakończeniu wykonywania skryptu. Czyli jeśli odpalimy ją dwa razy, to nasza funkcja zostanie wykonana dwa razy.
Podsumowanie
PHP posiada dwa konkurujące ze sobą mechanizmy raportowania błędów. Na szczęście jeden jest w odwrocie i – miejmy nadzieje – zostanie wkrótce zastąpiony w całości przez wyjątki.