Obsługa błędów w PHP

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.

Przykład takiej funkcji:

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

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s

Ta witryna wykorzystuje usługę Akismet aby zredukować ilość spamu. Dowiedz się w jaki sposób dane w twoich komentarzach są przetwarzane.