PHP/Wyjątki

Z Wikibooks, biblioteki wolnych podręczników.
< PHP
Poprzedni rozdział: Interfejsy
Następny rozdział: Elementy statyczne

Wyjątki[edytuj]

Błędy nie muszą wynikać wyłącznie z nieuwagi programisty. Może je powodować użytkownik poprzez dziwne działania, niewłaściwą konfigurację lub nawet problemy systemowe. Obsługa błędów jest to jeden z istotniejszych elementów współczesnych aplikacji. Początkujący programiści często ją bagatelizują, kompletnie nie przejmując się tym, że przy złych ustawieniach na ekranie przeglądarki pojawia się 500 ostrzeżeń PHP, albo załatwiając sprawę najprostszą komendą die(). Wykorzystują to hakerzy, dla których ścieżki dostępu, nazwy plików i numery linii wyświetlane przy komunikatach PHP to znakomite źródło informacji o serwerze, skrypcie oraz umiejętnościach programisty.

Ignorowanie zagrożenia wynika także z tego, że PHP przez długi czas nie miał zadowalających mechanizmów obsługi błędów, a opracowane wtedy prymitywne rozwiązania funkcjonują do dnia dzisiejszego. Zastanówmy się zatem, czego będziemy wymagać od systemu obsługi błędów:

  1. Priorytet - nie wszystkie błędy są krytyczne dla pracy skryptu. Gdy nie uda nam się połączyć z bazą danych, najprawdopodobniej nie będziemy w stanie nic wyświetlić, dlatego informujemy internautę, że tym razem musi obejść się smakiem. Jednak brak pliku językowego nie jest już aż tak krytyczny. Prawdopodobnie tłumacze nie zakończyli jeszcze swej pracy, dlatego tymczasowo możemy obejść problem, wczytując komunikaty w domyślnym języku, które już są gotowe.
  2. Miejsce wystąpienia - sposób obsługi błędu często zależy też od miejsca jego wystąpienia. Brak pliku konfiguracyjnego to poważna sprawa, lecz inne funkcje brak potrzebnych im plików mogą interpretować inaczej. Jako programiści musimy mieć możliwość decydowania, co zrobić z błędnymi sytuacjami w konkretnym miejscu skryptu.
  3. Przerwanie pracy - gdy wystąpi błąd, dalsze wykonywanie funkcji zazwyczaj przestaje mieć sens, jednak nie zawsze chcemy przy tym przerwać cały skrypt. System obsługi błędów musi mieć możliwość przerwania tych partii wykonywanego kodu, dla których wykryty problem jest krytyczny, nie zakłócając przy tym reszty skryptu.

Te trzy właściwości posiadają wyjątki. W PHP każdy wyjątek jest specjalnym obiektem klasy Exception lub jej pochodnych. Wyjątki można rzucać oraz obsługiwać. Skrypt rzuca wyjątek podczas wystąpienia sytuacji, którą uznajemy za błędną. Powoduje on przerwanie wykonywania bieżącego fragmentu kodu. Kod zawarty jest w specjalnych blokach informujących, jak wyjątki danego rodzaju obsłużyć. Interpreter przerywa wszystkie aktualnie wykonywane funkcje, dopóki nie trafi na blok, który wie, jak obsłużyć rzucony wyjątek i jemu przekazuje sterowanie. W kodzie obsługi wyjątku możemy sprawdzić, co to jest za błąd oraz np. wyświetlić internaucie informacje o problemie.

Wyjątki w praktyce[edytuj]

Przyjrzyjmy się teraz, jak suchy opis prezentuje się w praktyce. Poniżej prezentujemy zmodyfikowaną wersję klasy FileConfigLoader z naszego systemu konfiguracji omówionego w poprzednich rozdziałach. Dotychczas brak pliku obsługiwaliśmy, po prostu zwracając pustą tablicę, jednak logiczniejszym jest rzucenie wtedy wyjątku z informacją o problemie. W przykładzie zakładamy, że wykonałeś ćwiczenie z rozdziału Dziedziczenie.

<?php
 
class FileConfigLoader extends FileLoader
{
   private $_fileName = '';
 
   public function setFilename($filename)
   {
      $this->_fileName = $filename;
   } // end setFilename();
 
   public function load()
   {
      if(file_exists($this->_filename))
      {
         return parse_ini_file($this->_filename);
      }
 
      // jeśli pliku nie ma, rzuć wyjątek
      throw new Exception('Cannot read the configuration file: '.$this->_filename);
   } // end load();
} // end FileConfigLoader;

Wyjątki rzucamy przy pomocy słowa kluczowego throw, którego argumentem jest wyrażenie dające obiekt klasy Exception lub pochodnych. Może on być wczytany ze zmiennej lub zwrócony przez inną metodę, lecz najczęściej spotykaną konstrukcją jest podana w przykładzie throw new, która w locie tworzy potrzebny obiekt. Konstruktor klasy Exception domyślnie pobiera w argumencie komunikat błędu, który możemy dobrać według upodobań.

Na razie tak rzucony wyjątek spowoduje co najwyżej wyświetlenie komunikatu Fatal error z informacją, że PHP nie znalazł żadnego bloku potrafiącego go przechwycić i obsłużyć. Przechwytywaniem wyjątków zajmuje się blok try... catch:

<?php
require('./Config.php');
require('./ConfigLoader.php');
 
$config = new Config;

try
{
   $config->addLoader(new FileConfigLoader('./config/basic.ini.php'));
   // nieistniejący plik
   $config->addLoader(new FileConfigLoader('./config/unexisting.ini.php'));

   // spróbujmy odczytać nieistniejącą opcję. Jak pamiętamy, spowoduje to
   // próbę wczytania wszystkich plików oraz odkrycie, że jeden z nich nie
   // istnieje. Rzucony zostanie wyjątek.
   echo $config->get('unexisting_option');

   // to się już nie wykona
   echo $config->get('website_name');
}
catch(Exception $exception)
{
   echo 'Wystąpił błąd w linii '.$exception->getLine().': '.$exception->getMessage();
}

Jak pamiętamy, nasz system konfiguracji obsługuje leniwe ładowanie, czyli pliki odczytywane są w momencie odwołania do nieznanej opcji. Przy tej okazji obiekt stworzony dla pliku unexisting.ini.php odkryje, że taki plik nie istnieje i rzuci wyjątek, przerywając kolejne poziomy wykonywania skryptu, dopóki nie natrafi na blok try, który przechwytuje wszystkie wyjątki, jakie wystąpiły w podanym kodzie. Następnie porównuje je z blokiem catch, gdzie precyzujemy, jakie klasy wyjątków chcemy obsługiwać oraz do jakiej zmiennej należy zapisać przechwycony wyjątek. Jeśli dany blok nie może odnaleźć dopasowania, interpreter wraca do przerywania pracy kolejnych funkcji i metod, dopóki nie natrafi na następny try...catch. Kiedy PHP zlokalizuje już kod wiedzący, jak obsłużyć dany wyjątek, przeskakuje do odpowiadającego mu bloku catch i wykonuje go.

W naszym przypadku przepływ sterowania będzie wyglądać następująco:

  1. Próbujemy odczytać opcję unexisting_option.
  2. Opcja jest niedostępna. Próbujemy załadować pliki.
  3. Ładujemy plik basic.ini.php.
  4. Opcja się nie pojawiła, więc przechodzimy dalej.
  5. Ładujemy plik unexisting.ini.php
  6. Taki plik nie istnieje. Rzucamy wyjątek.
  7. PHP przerywa wykonywanie metody load() w klasie FileConfigLoader.
  8. PHP przerywa wykonywanie metody get() w klasie Config, która odpowiada za pobranie wartości nieistniejącej opcji.
  9. PHP przerywa wykonywanie dalszego kodu w bloku try.
  10. PHP znajduje blok catch i odkrywa, że potrafi on obsługiwać wyjątki klasy Exception.
  11. PHP zapisuje obiekt wyjątku do zmiennej $exception i rozpoczyna wykonywanie kodu należącego do bloku catch.
  12. Wyświetlamy komunikat i linię, w której wystąpił błąd.
  13. Kontynuujemy pracę skryptu od pierwszej linijki po bloku try ... catch.

Zagnieżdżanie bloków try[edytuj]

Bloki try... catch można zagnieżdżać. PHP próbuje dopasować rzucony wyjątek do najniższego z bloków, który potrafi go obsłużyć i tam przekierowuje działanie skryptu. Zazwyczaj aplikacja posiada jeden główny blok, którego zadaniem jest przechwycenie wszystkich wyjątków i potraktowanie ich jako błędy krytyczne. Wszędzie, gdzie chcemy potraktować je łagodniej, stosujemy dodatkowe bloki, w których określamy inny sposób obsługi.

Poniżej pokazany jest przykład zagnieżdżania. Za pomocą argumentu URL where można ustawić, w którym miejscu skrypt ma rzucić wyjątek:

<?php

if(!isset($_GET['where']))
{
   $_GET['where'] = 0;
}

try
{
   if($_GET['where'] == 0)
   {
     throw new Exception('Błąd 0');
   }
   echo 'Dalsza część bloku...<br/>';
   try
   {
      if($_GET['where'] == 1)
      {
        throw new Exception('Błąd 1');
      }
      echo 'Dalsza część bloku podrzędnego...<br/>';
   }
   catch(Exception $exception)
   {
      echo 'Problem: '.$exception->getMessage().'<br/>';
   }
  
   echo 'Dalsza część skryptu...<br/>';
}
catch(Exception $exception)
{
   echo 'Błąd krytyczny: '.$exception->getMessage().'<br/>';
}

Wywołaj skrypt z argumentem ?where=0 oraz ?where=1. Pierwsze wywołanie rzuci wyjątek Błąd 0, który zostanie przechwycony przez główny blok, który leży najbliżej, i potraktowane jak błąd krytyczny. Wyjątek z drugiego wywołania znajduje się w podrzędnym bloku, dlatego to właśnie on zostanie użyty do jego obsługi. Zauważmy, że po wykonaniu klauzuli catch skrypt kontynuuje działanie od końca bloku - w drugim przypadku wciąż wyświetli nam się napis "Dalsza część skryptu". Instrukcje echo umieszczone tuż po throw nie wykonają się nigdy.

Pokażemy teraz przykład praktycznego zastosowania, w którym wykorzystamy klasę SplFileObject z biblioteki Standard PHP Library. Udostępnia ona obiektowy interfejs do operacji na plikach, a ewentualne błędy raportuje jako wyjątki RuntimeException. Naszym zadaniem jest wczytanie konfiguracji strony WWW oraz tekstu powitalnego:

  1. Brak konfiguracji jest krytyczny - bez niej nie jesteśmy w stanie wyświetlić strony.
  2. Brak tekstu powitalnego możemy potraktować łagodniej - być może osoba odpowiedzialna za treść merytoryczną jeszcze go nie dostarczyła, dlatego zamiast niego wystarczy wyświetlić tekst domyślny.
<?php
try
{
   $config = new Config;

   // Ładowanie konfiguracji strony
   $manualLoader = new ManualConfigLoader();
   $configFile = new SplFileObject('./config/website.conf');
   foreach($configFile as $line)
   {
      $option = explode('=', $line);
      $manualLoader->addOption(trim($option[0]), trim($option[1]));
   }
   $config->addLoader($manualLoader);

   // Ładowanie tekstu powitalnego.
   $text = '';
   try
   {
      $introFile = new SplFileObject($config->get('intro_file'));
      foreach($introFile as $line)
      {
         $text .= $line;
      }
   }
   catch(RuntimeException $exception)
   {
      $text = 'Brak zdefiniowanego tekstu powitalnego.';
   }

   // Wyświetl tekst powitalny
   echo $text;
}
catch(RuntimeException $exception)
{
   echo 'Błąd krytyczny: '.$exception->getMessage();
}

Aby przykład zadziałał, należy dołączyć do niego stworzony w poprzednich rozdziałach system konfiguracji oraz napisać klasę ManualConfigLoader. Jest to kolejny system ładowania, który umożliwia ręczne definiowanie opcji konfiguracyjnych z poziomu skryptu poprzez metodę addOption($nazwa, $wartosc). Pozostawiamy to jako ćwiczenie.

Obiekty klasy SplFileObject reprezentują pojedynczy plik tekstowy, który jest otwierany w konstruktorze. Wszystkie błędy sygnalizowane są rzuceniem wyjątku. Najprostszy odczyt z pliku polega na umieszczeniu obiektu w pętli foreach, dzięki czemu w zmiennej $line pojawi się treść kolejnych linijek. Przykład pokazuje także, jak wykorzystać try ... catch jako instrukcję sterowania przepływem wykonania skryptu. Przyjrzyjmy się, jak ładowany jest tekst powitalny. Przed wejściem do sekcji krytycznej tworzymy pustą zmienną $text. W bloku try próbujemy ją wypełnić na podstawie treści z pliku. Jeśli proces ten zostanie z jakiegoś powodu przerwany, PHP rzuci wyjątek, który sprawi, że do zmiennej zostanie zapisana wartość domyślna. W ten sposób tuż po wykonaniu bloku mamy pewność, że w zmiennej $text zawsze coś się znajdzie - albo wczytane z pliku, albo uzupełnione domyślną treścią. Wyjątek potrzebny jest nam jedynie do przerwania wykonywania aktualnego kodu i przeskoczenia do catch. Nie zajmujemy się jego zawartością oraz informacjami.

Klasa Exception[edytuj]

Klasa Exception jest podstawą wszystkich wyjątków, jakie można rzucać w PHP. Udostępnia ona zbiór podstawowych metod do zarządzania informacjami o wyjątku i często jest rozszerzana poprzez dziedziczenie. Programiści często nie dodają żadnej nowej funkcjonalności w klasach pochodnych. Chodzi o to, aby móc przechwytywać tylko pewien rodzaj wyjątków pochodzących z określonego źródła:

<?php

class CustomException extends Exception { }

try
{
   // kod ...

}
catch(CustomException $exception)
{
  // Przechwytujemy jedynie wyjątki CustomException, a
  // innymi się nie zajmujemy.
}

Konstruktor klasy Exception przyjmuje do trzech argumentów:

  1. Komunikat $message
  2. Kod błędu $code
  3. Poprzedni wyjątek $previous

Ponadto mamy do dyspozycji szereg metod służących do pobierania informacji o wyjątku, gdy już go przechwycimy:

  • getMessage() - zwraca komunikat błędu.
  • getCode() - zwraca kod błędu.
  • getPrevious() - zwraca poprzedni wyjątek.
  • getFile() - zwraca nazwę pliku, w którym wyjątek został rzucony.
  • getLine() - zwraca numer linii, w której wyjątek został rzucony.
  • getTrace() - zwraca tablicę zawierającą ślad stosu, czyli informacje o wszystkich funkcjach i metodach wywołanych w momencie rzucenia wyjątku.
  • getTraceAsString() - zwraca ślad stosu, ale jako tekst.

Biblioteka SPL dostarcza zbiór gotowych klas wyjątków dla najczęstszych problemów. Programista powinien je stosować wszędzie tam, gdzie to jest potrzebne, zachowując ich znaczenie.

  • RuntimeException - reprezentuje błędy, które mogą być wykryte jedynie w trakcie wykonywania (brak pliku, błąd połączenia itd.).
  • OutOfRangeException - reprezentuje błędy przekroczenia przez wartość ustalonego zakresu.
  • RangeException - podanie niewłaściwego zakresu wartości.
  • OutOfBoundsException - brak wartości o podanym kluczu.
  • DomainException - próba użycia niewłaściwych danych (np. spodziewamy się listy opcji konfiguracyjnych, a dostaliśmy zawartość menu).
  • LengthException - wartość ma niewłaściwą długość.
  • InvalidArgumentException - argument ma niewłaściwą wartość (np. spodziewaliśmy się liczby, a dostaliśmy tablicę).
  • OverflowException - próba dodania nowej wartości do przepełnionego pojemnika na dane (np. gdy ustawiliśmy odgórny limit na 100 elementów).
  • UnderflowException - przeciwieństwo poprzedniego, czyli próba pobrania wartości z pustego.

Dobrą praktyką jest pisanie tzw. kodu idiotoodpornego, który nie sypie się przy podaniu niewłaściwych danych, lecz rozsądnie to sygnalizuje, dzięki czemu reszta aplikacji ma szansę odpowiednio zareagować. Nietrudno zauważyć, że powyższe klasy wyjątków dotyczą najbardziej podstawowych problemów, jakie mogą wystąpić w naszym skrypcie, dlatego powinny być wykorzystane przy sprawdzaniu warunków początkowych i brzegowych.

Problem sprzątania[edytuj]

Przerywanie pracy w połowie skomplikowanego procesu nie zawsze jest dobrym pomysłem. Metoda mogła przydzielić sobie sporo zasobów (np. otwarte pliki), a rzucając wyjątek uniemożliwiamy jej ich zwolnienie. Może to doprowadzić do kolejnych problemów w dalszej części skryptu lub nawet uszkodzenia danych. Problem ten znany jest pod nazwą problemu sprzątania, czyli jak zwolnić przydzielone zasoby po rzuceniu wyjątku. Niektóre języki oferują dla niego wbudowane wsparcie poprzez dodatkową klauzulę finally w bloku try. Wykonuje się ona zawsze, niezależnie od tego czy pojawi się wyjątek czy nie, a jej zadaniem jest posprzątanie po całym procesie. Brakuje jej jednak w PHP, dlatego programiści muszą radzić sobie inaczej. Często spotykanym rozwiązaniem jest dodanie "fikcyjnego" bloku try ... catch, który przechwytuje wyjątek tylko po to, by posprzątać, po czym rzuca go ponownie:

public function complexMethod()
{
   try
   {
      echo 'Skomplikowane obliczenia';

      // domyślne sprzątanie.
      $this->_cleanObject();
   }
   catch(Exception $exception)
   {
      $this->_cleanObject();
      throw $exception;
   }
} // end complexMethod();

Podświetlona linijka ukazuje dopiero co przechwycony wyjątek, który rzucamy ponownie, by zajął się nim nadrzędny blok try, ponieważ my chcieliśmy tylko posprzątać.

Wyjątki w PHP[edytuj]

W PHP wyjątki pojawiły się stosunkowo późno. O ile we własnym kodzie nie ma problemu z ich stosowaniem, to większość dostępnych wbudowanych rozszerzeń nie potrafi raportować błędów przy ich pomocy. Obsługę wyjątków posiada jedynie kilka najnowszych, obiektowych modułów:

  • Standard PHP Library
  • Biblioteka PHAR
  • Biblioteka PDO

Niestety i to nie jest regułą. Twórcy PHP nie są konsekwentni i wciąż zdarza im się wprowadzać nowe rozszerzenia z autorskimi mechanizmami obsługi błędów (np. biblioteka Intl wprowadzona w PHP 5.3.0). Aby pisać w PHP, trzeba do tego po prostu przywyknąć.

Język posiada jednak możliwość tłumaczenia wielu standardowych błędów na wyjątki. Sztuczka polega na napisaniu specjalnej funkcji obsługi błędów i zainstalowaniu jej w skrypcie:

<?php
function ExceptionErrorHandler($errno, $errstr, $errfile, $errline)
{
   if(in_array($errno, array(E_WARNING, E_RECOVERABLE_ERROR)))
   {
      throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
   }
   return false;
}

set_error_handler('ExceptionErrorHandler');

Powyższy kod sprawi, że ostrzeżenia (Warning) oraz przechwytywalne błędy krytyczne (Catchable fatal error) zostaną przekonwertowane na wyjątki.

Zakończenie[edytuj]

Wyjątki to elegancki mechanizm obsługi błędów, który świetnie współpracuje z programowaniem obiektowym. W następnym rozdziale powrócimy do niego z powrotem, aby omówić elementy statyczne klas, z których można korzystać bez konieczności posiadania obiektów.