PHP/Każdy popełnia błędy

Poprzedni rozdział: Inne elementy składni
Następny rozdział: Korzystanie z dokumentacji

Każdy popełnia błędy

edytuj

Błąd w programowaniu jest wynikiem pomyłki/literówki w trakcie pisania aplikacji albo niedostrzeżeniem jakiejś cechy projektowanego algorytmu. Wielu początkujących programistów jest przerażonych, gdy na ekranie pojawią im się tajemnicze komunikaty, a w parze z tym idzie niezaradność. Dlatego w tym rozdziale pragniemy skupić się na zagadnieniu błędu. Omówimy komunikaty zgłaszane przez PHP, techniki pomagające zlokalizować i usunąć błędy, a także powiemy nieco o pakietach testowych od strony teoretycznej, ponieważ do praktyki pozostało nam wciąż parę zagadnień związanych ze składnią.

Komunikaty błędów PHP

edytuj

Przetworzenie skryptu PHP składa się w istocie z dwóch faz:

  • Interpretacji - kod skryptu tłumaczony jest na wewnętrzny zestaw instrukcji interpretera. Faza ta często jest także zwana parsowaniem.
  • Wykonywania - właściwe wykonanie skryptu.

Każda z nich generuje nieco inne komunikaty błędów, dzięki czemu wiemy, gdzie szukać przyczyny problemów. Napiszmy sobie taki skrypt:

<?php
if(isset($zmienna)
{
   echo $zmienna;
}

Po jego uruchomieniu powinniśmy ujrzeć następujący komunikat:

Parse error: syntax error, unexpected '{' in /home/uzytkownik/www/kurs/aplikacja.php on line 3

Jest to błąd składni powiązany z fazą kompilacji. PHP nie może skompilować skryptu, ponieważ w podanym pliku w linii 3 natrafił na nieprawidłową konstrukcję składniową. Nie oznacza to jednak, że błąd jest akurat w tej linii. Zazwyczaj powoduje go jakaś pomyłka nieco wyżej. Przyjrzyjmy się komunikatowi. Mówi on, że PHP natknął się na niespodziewany nawias klamrowy w linii 3. Rzeczywiście, znajduje się on na swym miejscu tak, jak być powinien, ale skoro według interpretera nie powinno go tu być, a nic więcej w tej linii nie mamy, zerknijmy wyżej: if(isset($zmienna) - okazuje się, że nie zamknęliśmy jednego z nawiasów. PHP oczekiwał znaku ), lecz zamiast tego przeszedł do kolejnej linijki, dostał { i zgłosił błąd. Po dodaniu brakującego nawiasu skrypt zaczyna poprawnie działać.

Po usunięciu pierwszego błędu postanowiliśmy rozbudować skrypt i wywołaliśmy funkcję inicjującą naszą aplikację WWW.

<?php 
if(isset($zmienna))
{
   echo $zmienna
   inicjujAplikacje();
}

Po uruchomieniu znów pojawił nam się komunikat błędu!

Parse error: syntax error, unexpected T_STRING, expecting ',' or ';' in /home/uzytkownik/www/kurs/aplikacja.php on line 5

Owo tajemnicze T_STRING jest tzw. tokenem. Kompilatory mają tendencję do ułatwiania pracy zarówno sobie, jak i programiście je tworzącemu. Wszystkie elementy pełniące identyczną funkcję, np. zmienne, grupowane są pod jedną wspólną nazwą zwaną tokenem. Tworzenie składni jest teraz bardzo proste. Jeżeli chcemy, aby w jakimś miejscu można było podać dowolną zmienną, używamy tokenu i kompilator już wie, co można tam umieszczać. Informacje o błędnym użyciu tokenu PHP wyświetla w sposób jawny, jak widać na powyższym przykładzie. Komunikat informuje nas teraz, że PHP natknął się na ciąg tekstowy, oczekując przecinka albo średnika. Spójrzmy linijkę wyżej - rzeczywiście, brakuje nam średnika. Mógłbyś zapytać - czemu ciąg tekstowy, skoro to jest funkcja? Kompilator po prostu nie dotarł jeszcze do występujących dalej nawiasów, więc wstępnie zaklasyfikował nazwę funkcji jako tekst. Gdyby udało mu się tam dotrzeć, połączyłby z nią nawiasy i powstał nowy token: T_FUNCTION.

<?php 
$zmienna = 'wartosc';
if(isset($zmienna))
{
   echo $zmienna;
   inicjujAplikacje();
}

Poprawiliśmy usterkę i zainicjowaliśmy zmienną $zmienna jakąś wartością, jednak nasz skrypt nadal nie działa. Tym razem mamy do czynienia z problemem innego kalibru:

Fatal error: Call to undefined function inicjujAplikacje() in /home/uzytkownik/www/kurs/aplikacja.php on line 5

Jest to błąd wykonywania skryptu. PHP poinformował nas w ten sposób, że dotarł do wywołania funkcji inicjujAplikacje(), lecz taka nie istnieje. Pozostało nam jedynie odkryć przyczynę tego stanu - może nie dołączyliśmy jakiejś ważnej biblioteki? Zwróć uwagę, że PHP sprawdza istnienie funkcji w momencie wykonywania skryptu, a nie kompilacji. Gdybyśmy nie zainicjowali zmiennej $zmienna, kod wewnątrz instrukcji if nie zostałby wykonany i problem pozornie by nie wystąpił. To oczywiście tylko złudzenie. On tam jest, lecz PHP nie wykonał tego kawałka kodu i nie wykrył problemu. Spróbuj wykonać taki eksperyment. Praktyka ta ma swoje uzasadnienie - przypomnijmy sobie instrukcję eval z końca poprzedniego rozdziału. Tam nazwa funkcji była ustalana w momencie wykonywania, gdyż tylko wtedy mają prawo istnieć jakiekolwiek zmienne.

Komunikaty Fatal error pojawiają się zawsze wtedy, gdy w trakcie wykonywania wystąpi problem uniemożliwiający dalszą pracę interpreterowi. Przykładem jest podanie do instrukcji require nazwy nieistniejącego pliku. PHP go nie odnajdzie i zatrzyma wykonywanie. Warto nadmienić, że użycie include spowoduje "tylko" pokazanie się ostrzeżenia, a skrypt będzie dalej wykonywany.

Innym rodzajem komunikatu o błędzie jest Warning, czyli ostrzeżenie. W odróżnieniu od Fatal error lub Parse error, Warning nie przerywa działania naszego skryptu. Przykładem pojawiania się tego komunikatu może być podanie nieprawidłowych parametrów w jakiejś funkcji:

<?php
$tekst = 'to jest jakiś tekst';
sort($tekst);

Widzimy, że zmienna $tekst nie jest tablicą, więc użycie tej zmiennej w funkcji sort(), która służy do sortowania tablic, wywołuje pojawienie się komunikatu o błędzie typu Warning:

Warning: sort() expects parameter 1 to be array, string given in /home/uzytkownik/www/kurs/aplikacja.php on line 3

Mówi on, że funkcja sort() jako parametr pierwszy może przyjąć tylko tablice, a dostała co innego.

Ostatnim z omówionych komunikatów błędów będą tzw. notices, czyli powiadomienia. Mają one niski priorytet i służą do informowania o miejscach, które potencjalnie mogą stanowić źródło problemu. Oto jedna z takich sytuacji:

<?php
echo $zmienna;

Skrypt próbuje tutaj wyświetlić zawartość zmiennej $zmienna, lecz nie jest ona zdefiniowana. Może to być zarówno świadome działanie (wiemy, że zmienna może w tym miejscu nie istnieć, ale dopuszczamy to) lub też wynik literówki w jej nazwie, co oczywiście niosłoby dla nas poważne konsekwencje. Dlatego interpreter na wszelki wypadek wyświetli w tym miejscu komunikat:

Notice: Undefined variable: zmienna in /home/uzytkownik/www/kurs/aplikacja.php on line 2

Podobny komunikat pojawi się, gdy spróbujemy odczytać wartość nieistniejącego elementu tablicy. Do dobrych zwyczajów należy takie pisanie skryptów, aby nie generowały one tego typu komunikatów z kilku powodów:

  1. Jeśli nasz skrypt będą też rozwijać inni programiści, mogą mieć oni ustawiony wysoki poziom raportowania błędów, przez co właściwa treść strony będzie im ginąć w setkach powiadomień.
  2. Wiele serwerów, pomimo ryzyka związanego z bezpieczeństwem, pozostawia włączony wysoki poziom raportowania błędów. Z identycznego powodu nie będzie można używać tam naszego skryptu.
  3. Brak komunikatów jest wyrazem dbałości o sytuacje wyjątkowe oraz możliwość ich obsługi.

Używaj komendy isset() do sprawdzania, czy wymagane w danym fragmencie zmienne istnieją, a także inicjuj je przed pierwszym użyciem domyślną wartością.

Operator @

edytuj

Jeżeli dowolne wyrażenie PHP poprzedzimy znakiem @, interpreter nie wyświetli żadnego komunikatu, jeżeli wykryje w nim błąd. Nie oznacza to naturalnie, że błąd ten nagle znika. On tam jest, lecz my nie jesteśmy o tym powiadamiani. Dlatego należy bardzo rozważnie postępować z jego użyciem. @ przydaje się głównie przy funkcjach do komunikacji z zewnętrznymi źródłami danych, np. plikami. Jeżeli PHP wykryje np. brak żądanego pliku, nie tylko generuje określony wynik, ale także pokazuje komunikat Warning, co nie zawsze jest zjawiskiem pożądanym. Operatorem tym możemy także "zakazać" wyświetlania komunikatów Notice o niezdefiniowaniu zmiennej, jeżeli tego naprawdę potrzebujemy. Poniżej pokazany przykład ma za zadanie pokazać datę modyfikacji pliku lub nasz własny komunikat, jeżeli on nie istnieje. Na początek wariant najprostszy:

<?php
echo filemtime('plik.txt');

Kiedy plik.txt nie będzie istnieć, interpreter wygeneruje stosowny komunikat, którego my w takiej formie oglądać nie chcemy. Dlatego wcześniej sprawdzimy drugą funkcją istnienie pliku.

<?php
if(file_exists('plik.txt'))
{
	echo filemtime('plik.txt');
}
else
{
	echo 'Podany plik nie istnieje.';
}

Co dwie funkcje to nie jedna, lecz pojawia się nam tu problem wydajności. Odczyt czegokolwiek z twardego dysku stanowi duży narzut czasowy dla oprogramowania, dlatego powinniśmy się starać wykonywać jak najmniej takich operacji. Wiemy z dokumentacji, że samo filemtime() zwraca nam false, jeśli plik nie istnieje, przeszkadza nam tu jedynie "firmowe" ostrzeżenie. Dlatego właśnie skorzystamy z @, aby się go pozbyć:

<?php
$wynik = @filemtime('plik.txt');
if($wynik !== FALSE)
{
	echo $wynik;
}
else
{
	echo 'Podany plik nie istnieje.';
}

Wynik funkcji zapisujemy w zmiennej, ponieważ jest on nam potrzebny w paru miejscach. Aby nie dostawać komunikatu Warning, poprzedziliśmy wywołanie funkcji operatorem @.

Wszystko można zapisać inaczej.

<?php
@echo filemtime('plik.txt') or die('Podany plik nie istnieje.');

Poziom raportowania błędów

edytuj

Poziom raportowania błędów określa, na które zdarzenia PHP ma reagować wyświetleniem stosownego komunikatu. Docelowo ustawia się go w pliku konfiguracyjnym php.ini, w dyrektywie error_reporting. Podczas instalacji według tego podręcznika, zaleciliśmy użycie najwyższego możliwego poziomu E_ALL | E_STRICT, który zgłasza dosłownie wszystko. Dzięki temu możemy w porę reagować na każdy problem i mieć pewność, że po zainstalowaniu aplikacji na innym serwerze nie zostaniemy zasypani toną komunikatów. Jednak takie ustawienie należy stosować tylko w fazie tworzenia/testowania naszych skryptów. Gdy już umieścimy je na stronie internetowej, należy wyłączyć wyświetlanie wszystkich błędów, ponieważ takie komunikaty często ujawniają wiele informacji o skrypcie i używanym oprogramowaniu, które mogą ułatwić atak na nasz serwis.

Poziom raportowania można też tymczasowo zmieniać z poziomu skryptu funkcją error_reporting():

<?php
error_reporting(E_ALL ^ E_NOTICE);
echo 'Teraz komunikaty Notice nie działają: '.$nieistniejacaZmienna;

Funkcja ta ma jeszcze jedną użyteczną właściwość, mianowicie zwraca jako rezultat dotychczasowy poziom raportowania, dzięki czemu możemy go potem przywrócić:

<?php
$poprzedni = error_reporting(E_ALL ^ E_NOTICE);
echo 'Teraz komunikaty Notice nie działają: '.$nieistniejacaZmienna;

error_reporting($poprzedni);
echo 'A teraz już tak: '.$nieistniejacaZmienna;

Techniki debugowania

edytuj

Inny rodzaj błędów związany jest z algorytmami produkującymi niewłaściwe rezultaty. Odnaleźć przyczynę problemu jest tu znacznie trudniej, ponieważ nie jesteśmy raczeni żadnymi komunikatami i musimy sami dochodzić, co się właściwie stało. Jedynym sposobem jest poumieszczanie w kodzie na chwilę własnych informacji, które dokładnie powiadomią nas, które fragmenty są wykonywane w jakiej kolejności i z jakimi danymi. Metoda wywodzi się w prostej linii z języka C/C++, gdzie w kodzie umieszcza się tzw. asercje. Jeżeli kompilujemy kod programu w trybie debug (wykrywanie błędów), w oznaczanych przez nas miejscach dodawane są odpowiednie instrukcje sprawdzające poprawność danych i raportujące ewentualne problemy. Kiedy kompilujemy program w wersji "oficjalnej", preprocesor nie umieszcza już tych instrukcji w kodzie. Oprócz tego do wielu języków istnieją specjalne debugery, które pozwalają śledzić wykonywanie krok po kroku lub zatrzymać na wybranej linii, co przydaje się przy analizie złożonych algorytmów.

PHP żadnego preprocesora nie ma (można nawet powiedzieć, że sam nim jest dla języka HTML), dlatego sami musimy poumieszczać w kodzie odpowiednie wstawki. Najprostszą z nich jest echo 'Test';. Komendę taką umieszczamy, chcąc sprawdzić, czy algorytm dociera do danego miejsca. Po uruchomieniu, jeśli na ekranie pokaże nam się rzeczony napis, oznacza to, iż akurat to miejsce działa dobrze. Instrukcją echo możemy także wyświetlać dane lub sprawdzać kolejność wykonywania operacji, wstawiając różne komunikaty. Komenda die() nie tylko wyświetla komunikat, ale także zatrzymuje skrypt. Do wyświetlania zawartości tablic lub obiektów możemy zastosować funkcje var_dump() albo print_r(). Po usunięciu usterki wszystkie poczynione przez nas wstawki usuwamy z kodu.

PHP posiada specjalną funkcję obsługi asercji: assert(). Generuje ona komunikat błędu, jeżeli podane jej wyrażenie ma wartość false:

<?php
$zmienna = 3; 
assert($zmienna == 6);

Jednak to jeszcze nie wszystko. Musimy funkcją assert_options() włączyć to sprawdzanie:

<?php
assert_options(ASSERT_ACTIVE, 1);
assert_options(ASSERT_WARNING, 1);
 
$zmienna = 3;
 
assert($zmienna == 6);

Po skończeniu debugowania wcale nie musimy usuwać wywołań funkcji assert(). Wystarczy ustawić opcję ASSERT_ACTIVE na 0 i przestaje być ona aktywna.

Do debugowania skryptów PHP mogą być także przydatne zewnętrzne moduły w stylu Xdebug (dostępny w repozytorium PECL tj. http://pecl.php.net/package/Xdebug a nawet jako paczka w niektórych dystrybucjach Linuksa). Rozwiązania takie są dosyć ciekawe, ponieważ pokazują znacznie więcej użytecznych dla programisty informacji. Przykładowo, Xdebug w momencie wystąpienia błędu pokazuje całą listę wywołań funkcji nadrzędnych. Rozważmy sytuację, gdy pewnej funkcji używamy w kilku modułach, lecz co jakiś czas generuje ona błąd. Dzięki obecności listy możemy natychmiast określić, który moduł wywołuje ją nieprawidłowo. Xdebug można również skonfigurować do pracy z zewnętrznymi środowiskami IDE do tworzenia skryptów (np. PDT wchodzący w skład pakietu Eclipse).

Pakiety testowe

edytuj

W praktyce okazuje się, że ręczne sprawdzanie aplikacji po każdej wprowadzonej zmianie bywa uciążliwe i jest narażone na ryzyko przeoczenia jakiegoś istotnego elementu. Dlatego w większych projektach używa się na co dzień systemów automatycznego testowania. Zagadnieniom testowania poświęcono wiele książek. Sprowadzają się one najczęściej do napisania dodatkowego zestawu testów, który wywołuje różne funkcje w określony sposób i sprawdza czy wynik ich działania zgadza się ze spodziewanym. Po wprowadzeniu większych zmian wystarczy jedynie uruchomić system testujący, którego wynikiem będzie raport, ile testów zostało pomyślnie zaliczonych i gdzie pojawiły się błędy.

Możemy wyróżnić dwa rodzaje testów:

  1. Testy funkcjonalne - sprawdzają funkcjonalność gotowej aplikacji, np. wypełniając formularz określonymi danymi i weryfikując odpowiedź programu.
  2. Testy jednostkowe - koncentrują się na pojedynczych elementach kodu aplikacji (np. funkcjach w kodzie) sprawdzając, jak reagują one na określone argumenty. Założenie polega na tym, że jeśli funkcje działają poprawnie i gdzieś wystąpił błąd, najprawdopodobniej leży on w sposobie poskładania programu z funkcji, a nie w samych funkcjach.

Do każdego z nich istnieją odpowiednie systemy testowania. Testy funkcjonalne mogą być realizowane przy pomocy Selenium, zaś do testów jednostkowych w PHP powszechnie stosowany jest skrypt PHPUnit. Nie będziemy ich teraz poznawać, ponieważ wciąż brakuje nam sporo wiedzy o samym PHP, aby zrozumieć, jak są one napisane i jak takie testy układać. Wrócimy do nich w dalszej części podręcznika.