Programista: PHP5, MySQL, PostgreSQL, JavaScript, xhtml/css
JID: hwao@chrome.pl
PHP5 udostępnia nam bardzo wygodny sposób obsługi zdarzeń powodujących różnego rodzaju komplikacje. Pomysł został zaczerpnięty z innych języków, także jego działanie jest niemal identyczne z tym, jakie spotykany w Javie. Oczywiście mowa tutaj o Exception (z ang. Wyjątki).
Dzięki nim możemy oznajmić aplikacji, że wystąpił błąd z którym dalej ona nie możne poprawnie funkcjonować. W przeciwieństwie do funkcji die() czy też exit(), które kończą wykonywanie programu, wyjątki umożliwiają nam podjąć odpowiednich kroków mających na celu eliminacji problemu. Oczywiście moglibyśmy stosować wszędzie funkcje/metody, które zwracają true lub false, w zależności od tego czy jej działanie zakończyło się sukcesem. Pomysł często stosowany, lecz przy większych systemach można bardzo szybko stracić rachubę, co powoduje błąd (gdyż mamy tylko wiedzę, o tym że działanie funkcji/metody się nie powiodło, nie znamy przyczyn). Kod staje się także dużo bardziej nie czytelny, ogromne ilości instrukcji warunkowych w znacznym stopniu wpływając negatywnie na czytelność kodu. Można także probować korzystać z funkcji trigger_error(), która to "wytwarza" błąd. Stosuje się ją powszechnie w bibliotekach pisanych dla PHP4. Moim zdaniem jednak, to narzędzie jest bardzo nie wygodne, w dodatku nie oferuje nam możliwości obsłużenia błędu tylko nas o nim informuje.
Zatem jak działają wyjątki? W ciele try{} dajemy kod w którym spodziewamy się wystąpienia wyjątku, nie należy zawierać za dużych fragmentów kodu. Zawieramy tylko tyle kodu, ile jest niezbędne do działania jakiegoś fragmentu aplikacji. Zależy nam na tym, że jeżeli zostanie zwrócony wyjątek, to dzięki catch(){} będziemy w stanie obsłużyć wyjątek tak, aby dalsza część aplikacji działała już poprawnie.
Tak jak już wspomniałem po bloku try{} umieszczamy blok/i catch(){} w którym wypisujemy jakiego wyjątku oczekujemy, a następnie w ciele bloku piszemy kod odpowiedzialny za "zreperowanie". Bloków catch(){} umieszczamy tyle, ile wyjątków możemy się spodziewać i podołamy ich obsłudze.
Zademonstruje przykładowy szkic:
Try { // Blok w którym spodziewamy sie wyjątku } catch (exception $ePierwszy ) { // obsługa pieszego spodziewanego wyjątku } catch (exception $eNty ) { // obsługa kolejnego wyjątku }
Na początku uruchomiony jest blok try{}, jeżeli podczas jego działania zostanie zwrócony jakikolwiek wyjątek to następuje uruchomcie kolejnych bloków catch(){}, tj. Jeżeli wyjątek nie pasuje do pierwszego bloku sprawdzany jest kolejny (całość przypomina w pewnym sensie działanie switch()'a). Wyjątki dopasowywane są do bloków catch(){} po typie obiektu(nazwach klasy Exception). W wypadku braku wystąpienia wyjątku w bloku try{}, bloki catch(){} w ogóle nie zostają uruchomione.
Już mniej więcej wiemy jak obsłużyć wyjątki, teraz nauczymy się jak je zwracać. Do zwrócenia wyjątku używamy throw. Działa ona podobnie do return, z tą różnica że nie zwraca danych tylko wyrzuca wyjątek który łapiemy blokiem try{} i obsługujemy w pasującym ciele catch(){}.
Przykład:
$sPath = './router.php'; try { if( !file_exists( $sPath ) ) { throw new Exception( 'plik nie istnieje!' ); } include $sPath; if( !class_exists( 'Router' ) ) { throw new Exception( 'Klasa Router nie istnieje!' ); } $Router = new Router(); } catch( Exception $Ex ) { echo 'Wystąpiły problemy!'."\n"; echo '',$Ex,''; }
W bloku catch(){} powinny zostać wykonane operacje odpowiedzialne za ?odratowanie kodu? my jednak wyświetlimy tylko że pojawiły się problemy, obsłużeniem ich zajmiemy się trochę potem.
Kod pokazuje proste zastosowanie wyjątków, ale nic specjalnego on sobą nie reprezentuje. ?eby można było zacząć korzystać z pełnych dobrodziejstw wyjątków musimy przyjrzeć sie dokładniej ich budowie.
W PHP5 wyjątku są obiektami, co pozwala nam na dopasowywanie ich do swoich potrzeb. Głównym obiektem jest klasa Exception(). Przyjrzyjmy się dokładniej jej budowie, aby wiedzieć z czym dokładnie mamy do czynienia.
class Exception { protected $message = 'Unknown exception'; // exception message protected $code = 0; // user defined exception code protected $file; // source filename of exception protected $line; // source line of exception function __construct(string $message=NULL, int code=0); final function getMessage(); // message of exception final function getCode(); // code of exception final function getFile(); // source filename final function getTrace(); // an array of the backtrace() final function getTraceAsString(); // formated string of trace /* Overrideable */ function _toString(); // formated string for display }
Opisze mniej więcej o co chodzi w danych właściwościach i metodach:
Tak wygląda budowa głównej klasy wyjątków. Warto zapoznać się z jej budową, ponieważ to na jej postawie będziemy tworzyć własne wyjątki, które będą nas informować o problemach w naszych programach.
Ponieważ wyjątki są obiektami, możemy tworzyć własne obiekty poprzez dziedziczenie po klasie Exception. Nasuwa się tutaj pytanie, poco tworzyć własne wyjątku? Bardzo ważna jest nazwa klasy, ponieważ to po niej będziemy wyłapywać odpowiednie wyjątki w blokach catch(){}. Nazwa powinna także odzwierciedlać naturę problemu lub gdzie wystąpił.
Stwórzmy sobie własny wyjątek odpowiedzialny za poinformowanie nas, iż wystąpił problem w jądrze naszego systemy. Odpowiednia nazwa dla tego wyjątku będzie SystemException. Uczynimy go odpowiedzialnym za wyrzucanie wszelkich problemów napotkanych w naszej klasie System.
class SystemException extends Exception {} class System { protected $Router = null; public $sPath = './router.php'; public function runRouter() { if( !file_exists( $this->sPath ) ) { throw new SystemException( 'Plik z routerem nie istnieje! Ścieżka “'.$this->sPath.'”' ); } include $sPath; $this->Router = new Router(); } } $System = new System(); try { $System->runRouter(); } catch( SystemException $Ex ) { // Nie udało sie włączyć routera, ponieważ plik nie istnieje $System->sPath = './Istniejacy/Router.php'; $System->runRouter(); } catch( Exception $Ex ) { echo 'Standardowy wyjątek'; }
Należy zwrócić uwaga na kolejność bloków catch(){}, gdyż jak zostanie już znaleziony wyjątek pasujący do danego bloku catch(){}, dalsze boki nie będą już przeszukiwane (podobnie jak to ma miejsce switch(), a catch( Excpetion $Ex ) można traktować jak odpowiednik default).
Czemu jakbym dał na początku blok catch( Exception $Ex ){}to wyjątek został by nie poprawnie obsłużony? Ponieważ każdy wyjątek powinien dziedziczyć po Exception. Ustawiając na samym początku bloki catch(){} poszukujący właśnie tego wyjątku( Exception), będzie zawsze on uznany za ten najbardziej pasujący, co oczywiście nie jest prawda. Dlatego należy pamiętać aby bloki catch(){} określających najmniejszy błąd umieszczać na samej gorze i stopniowo przechodzić w dół, aż w końcu dotrzemy do wyjątku Exception.
Wracając do samego kodu, działa on następująco. Wewnątrz bloku try{} uruchamiamy metodę klasy, która może zwrócić wyjątek. U nas metoda sprawdza czy istnieje plik, tak się akurat dzieje że plik nie istnieje, zostaje zwrócony wyjątek (i zatrzymane wykonywanie metody, tak jak by tam było return). Ponieważ kod był objęty blokiem try{} poszukiwany jest blok catch(){} pasujący do zwróconego wyjątku, tam następuje obsługa wyjątku. W klasie system zostaje zmieniona ścieżka na istniejącą i ponownie zostaje uruchomiona metoda System::runRouter(). Tym razem operacja jest wykonana poprawnie i nie zostaje zwrócony wyjątek (musisz pamiętać tylko aby plik istniał, także nie zdziw się jeżeli zobaczysz komunikat wyjątku jak odpalisz przykład - to nie błąd )
Oczywiście możemy robić także bardziej skomplikowane konstrukcje. Przykładowo w klasie Router może zostać także zostać zgłoszony wyjątek gdy coś będzie powodować problemy.
Napisze prosty przykład.
class RouterException extends Exception{} class Router { public function __construct() { // Prowadzimy jakąś działalność, po czym okazuje się // że coś nie działa throw new RouterException( 'Ojojoj...' ); } } class NewSystem extends System { public function runRounter() { try { parent::runRouter(); } catch( SystemException $SystemEx ) { // Wywalany wyjątek, ponieważ juz mamy jego obsługę niżej throw $SystemEx; } catch( RouterException $RouterEx ) { // Próbujemy rozwiązać problem naszego Router'a // Jeżeli sie nie uda to możemy zdecydować sie na wyrzucenie wyjątku , np: throw new $RouterEx; } } } $System = new System(); try { $System->runRouter(); } catch( SystemException $Ex ) { // Nie udało sie włączyć routera, ponieważ plik nie istnieje $System->sPath = './Istniejacy/Router.php'; $System->runRouter(); } catch( Exception $Ex ) { // Tutaj zostanie obsłużony RouterException, ponieważ u góry nie ma nic pasującego do niego echo 'Wystąpił problem: ',$Ex; }
Ponieważ wyjątek jest obiektem możemy sobie go dopasować do naszych potrzeb. Dodanie nowych metod czy właściwości nie sprawi problemu, a może znacznie ułatwić nam prace z naszym systemem.
class LogEx { static protected $aLog = array(); static public function add( ProException $Ex ) { self::$aLog[] = $Ex; } static public function get() { return self::$aLog; } } class ProException extends Exception { protected $bHandle = false; public function __construct( $sMessage, $iCode = 0 ) { parent::__construct( $sMessage, $iCode ); $this->bHandle = false; LogEx::add( $this ); } public function handled() { $this->bHandle = true; } public function inHandled() { return $this->bHandle; } public function __toString() { return __CLASS__ . ': ['.$this->code.']: '.$this->message."\n"; } }
Powyższy kod obrazuje nam jak bardzo prosto możemy zrobić z wyjątkami to, co nam jest potrzebne. Rozbudowałem wyjątek tak, że będzie on raportował o swoim stanie, to znaczy jest dodawany do listy wyjątków i jeżeli programista go obsłuży, np tak:
try { throw new ProException( 'Test' ); } catch( ProException $Ex ) { // obsłużony $Ex->handled(); } $aExList = LogEx::get(); foreach( $aExList As $Ex ) { echo ( $Ex->inHandled() ? 'Złapany' : 'Nie złapany' ).' : ', $Ex; }
Dzięki temu, mamy dostęp do listy wszystkich wyjątków jakie pojawiły się nam podczas pracy systemu. Zostajemy także poinformowani czy systemowi udało się rozwiązać problem. W bardzo prosty sposób możemy także oddać opis jak z danym problemem system sobie poradził.
Oczywiście to co zrobicie z własnymi obiektami zależy tylko od waszej wyobraźni i potrzeb.
Jeżeli wyjątek nie zostaje pochwycony, zostaje wyświetlona informacja niesiona z wyjątkiem, wraz z jednoczesnym zatrzymywaniem wykonywania skryptu w miejscu jego wystąpienia. Powinieneś już wiedzieć bardzo dużo na temat wyjątków, przedstawię jeszcze jak to działa w praktyce.
class MyException extends Exception {} echo 'Test 1'."\n"; try { throw new MyException(); } catch( MyException $e ) { // Ten kod zostanie wykonany echo '1. MyException'."\n"; } catch( Exception $e ) { // pominięty ponieważ blok powyżej został wykonany echo '1. Exception'."\n"; } // Dalszy kod wykonany echo 'Test 2'."\n"; try { throw new Exception(); } catch( MyException $e ) { // Ten kod zostanie pominięty, nie pasuje do wyjątku echo '2. MyException'."\n"; } catch( Exception $e ) { // Ten kod zostanie wykonany echo '2. Exception'."\n"; } // Dalszy kod wykonany echo 'Test 3'."\n"; try { throw new MyException(); } catch( Exception $e ) { // Ten kod zostanie wykonany, ponieważ MyException dziedziczy Exception echo '3. Exception'."\n"; } // Dalszy kod wykonany echo 'Test 4'."\n"; try { // tu nie będzie wyjątku $sNull = null; } catch( Exception $e ) { // pominięty, ponieważ nie ma wyjątku echo '4. Exception'."\n"; } echo 'Koniec'."\n";
Proponuje abyś sam uruchomił ten kod i wyciągną wnioski patrząc na źródło i efekt wyświetlony w przeglądarce. Chyba ten przykład rozwiał wszelkie Twoje wątpliwości, dotyczące działania wyjątków.
Czasem zdarza się, że jakiś wyjątek pozostaje nie obsłużony, albo to z braku bloku try{}, bądź też braku pasujących blok catch(){}. Wtedy taki wyjątek zostaje surowo wyświetlony na ekranie. My jednak tego nie chcemy bądź dla zwiększenia bezpieczeństwa(może znajdować się tam jakieś hasło, ect), lub nie chcemy zasypywać użytkownika jakimiś nie zrozumiałymi dla niego błędami(lepiej wyświetlić jakąś prostą stronę z błędem), albo po prostu dla ułatwienia pracy deweloperowi. PHP5 udostępnia nam mechanizm umożliwiający zaopiekowanie się nie chcianymi wyjątkami. Funkcja stworzona do tego nazywa się set_exception_handler(), zademonstruje prosty przykład jak jej użyć.
define( 'DEV', true ); function ExceptionHandler( $Ex ) { if( DEV == true ) { // Można zrobić coś dużo bardziej rozbudowanego echo 'Nie kochany wyjątek: '.$Ex->getMessage()."\n"; } else { // Zapis wyjątku do pliku, albo cos die( 'Przepraszamy za usterki!' ); } } set_exception_handler( 'ExceptionHandler' ); throw new Exception( 'UFO ' ); echo 'Nie wykonany kod, ponieważ trafił nam się nie obłożony wyjątek';
Jeżeli ustawimy stałej DEV wartość true, będą pojawiać się nie złapane wyjątki - deweloper poprawi i po płaczu.
Jeżeli oddajemy już kod od działania ustawiamy false, wtedy użytkownik nie zobaczy wyjątku (w którym mogły by być jakieś hasła, lubo cokolwiek) tylko zostanie pokazana mu informacja jakaś informacja(przeproszenia, ect), można dopisać logowanie nie obsłużonych wyjątków dzięki temu deweloper będzie mógł obsłużyć wszystkie wyjątki czytając log.
Niestety nie mogę nic poradzić na formatowanie kodu Pozostaje Wam ściągnąć wersje PDF, tam jest one poprawne, chyba że ktoś będzie chciał w komentarzach to przygotuje zipa z kodem.
Ja zaproponowałbym:
http://www.yarpo.pl/2010/11/29/hierarchia-klas-wyjatkow/
oraz:
http://www.yarpo.pl/2010/12/02/walidacja-danych-w-klasie-wyjatku/
witam! fajny artykul..:)
Mam problem... a raczej pytanie:) ogoleni watki ok... ale czy jest mozliwosc powortu z watku w miejscu ktorym zostal stworzony??
np:
if(prawda/falsz)
{
throw new SystemException('cos', 'nicosc');
jakis kod dalej dalej dalej...
}
cache (SystemException $Ex )
{
$Ex->cos();
}
jesli w if stworzy wyjatek, cache go zlapie... ale teraz mam rpzypadek ze wyjatek skutecznie obsloz i naprawil blad :).. i teraz pasuje mi wrocic do miejsca w ktorym wystapilo throw new SystemException('cos', 'nicosc'); ... i zeby kod wykonoywal sie dalej :)
Dopiero ucze sie piątki, przyda sie napewno :)