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.

Dropbox – podejście drugie

12 maja (czyli tydzień temu) na Dropboxie była organizowana zabawa nazwana przez twórców platformy: Dropquest II. Zabawa polegała na rozwiązaniu serii 20 różnych łamigłówek… Można powiedzieć, że od najłatwiejszej do najtrudniejszej. Zagadki się nie powtarzały i były – co ważne – ciekawe. Nie polegały też na jakiejś specjalistycznej wiedzy, tylko na umiejętności logiki i myślenia.

Nadal można je rozwiązać, jeśli ktoś chce to tutaj może zacząć (potrzebne jest konto na dropboxie). Moim zdaniem jeszcze warto, bo jeśli rozwiążemy wszystkie łamigłówki to nasza przestrzeń dyskowa zwiększy się o 1 GiB. Sam będę brutalnie szczery i powiem że sam tego nie rozwiązywałem i korzystałem z podpowiedzi /g/ (z którego dowidziałem się o tej zabawie), jak i ich małego poradnika. Załapałem się na dopiero na 3621 miejsce:

Przeprowadzony przez nich dropquest, jest doskonałą okazją by wrócić do dropboxa i jeszcze raz się jemu przyjrzeć. Jako że wcześniejsza wersja posta o tej usłudze nie jest zbyt dobra i została odpowiednio skomentowana przez kolegę Mateusza… Więc zacznijmy od początku:

Czytaj dalej „Dropbox – podejście drugie”

Dropbox – czyli chmura dla każdego

Nowa zaktualizowana wersja wpisu znajduje się tutaj: https://psychobpl.wordpress.com/2012/05/20/dropbox-podejscie-drugie/

Dropbox, jest to usługa świadczona przez firmę o tej samej nazwie pozwalająca na przechowywanie swoich danych w chmurze. Do czego może się takie coś przydać? Po pierwsze do przechowywania kopii zapasowych dokumentów, albo do synchronizowania dokumentów/plików między paroma komputerami, gdzie przynoszenie pendrive’a jest uciążliwe/nie możliwe, o nagrywaniu płyt CD/DVD – lub nawet o dyskietkach – nie wspominając.

Wersja darmowa Dropboxa oferuje od 2 GiB do 10 GiB – jeśli dobrze pamiętam, co prawda zdobycie tych 10 GiB jest trochę uciążliwe ale to możliwe. Standardowo przy instalacji dostajemy 2 GiB przestrzeń na serwerach firmy. By z nich skorzystać należy zainstalować oprogramowanie udostępnione na stronie usługi, można co prawda skorzystać także z interfejsu internetowego usługi, ale nie jest to aż tak wygodne jak program.

Po zainstalowaniu programu – który działa na Windowsie, Linuksie i MacOsie – zostaniemy poproszeni o zalogowanie się lub zarejestrowanie. Nie jest to jakieś skomplikowane więc tego opisywać nie trzeba. Domyślny folder który będzie synchronizowany z naszą prywatną chmurą znajduje się w Moje Dokumenty/Dropbox. Jeśli do niego wejdziemy, to zauważymy że obok ikonek wszystkich plików i folderów w nim, znajdują się dodatkowe symbole. Zielony ptaszek oznacza że wersja pliku/folderu jest u nas identyczna jak w chmurze, zaś 2-strzałki układające się w koło na niebieskim tle oznacza że dany plik/folder jest różny od tego co jest w chmurze i należy go synchronizować.

Teraz zawsze gdy uruchomimy program i coś w naszym dropboxowym folderze zmienimy, to wszelkie zmiany zostaną zsynchronizowane z serwerem, a gdy zalogujemy się z innego komputera także z tym innym komputerem.

Do czego może nam się ów dropbox przydać? Do tego do czego został wymyślony, czyli do synchronizacji plików między naszymi komputerami. Oprócz tego mamy także możliwość udostępniania tych plików dla wszystkich – albo raczej dla tych którzy mają odpowiedni link. Możemy także dzielić nasze pliki z innym kontem przez shared folders, ale jako że mi to nie było potrzebne, nie korzystałem z tego, więc nie mogę się na ten temat wypowiedzieć.

Jak pisałem, najpierw mamy tylko 2 GiB miejsca w naszej prywatnej chmurze. Ale jak to zwiększyć, do tych 10 GB? Jest na to jeden sposób, musimy przekonać paru naszych kolegów (~32 kolegów), by zarejestrowali się z naszego ref-linku – ja też was o to proszę ;). Jeśli ktoś zarejestruje się z naszego linku to dostaniemy dodatkowe 250 MiB, i my i nasz kolega. Jeśli zaś jesteśmy studentami, albo mamy e-maila w domenie *.edu, możemy podwoić ilość miejsca dostawanego za jednego ref-linka, czyli będziemy dostawać 500 MiB i będziemy mogli zwiększyć zapas naszego miejsca o 16 GiB ( czyli do 18 GiB na darmowy koncie ). By to zrobić należy się zweryfikować na stronie: https://www.dropbox.com/edu – należy mieć już utworzone konto. Niestety Politechniki Opolskiej z automatu nie zatwierdza 😦 – choć myślę że przez domenę edu.pl. By dostać jeszcze 250 MiB należy wykonać 5 zadań ze strony: https://www.dropbox.com/gs.