eRIZ’s weblog

PHP, webdesign, Linux, Windows i inne, bo nie samym chlebem człowiek żyje
Serdecznie zapraszam do udziału w ANKIECIE

propozycja: niech PHP będzie wyjątkiem

Od momentu wprowadzenia namiastki obiektów do naszego kochanego języka, coraz bardziej zaczęły nasilać się narzekania, że PHP jest sto lat za konkurencją pod wieloma względami. Pewnie lista narzekań ciągnęłaby się aż do wyczerpania zapasów papieru toaletowego w WC, ale na pewne rzeczy progamista nie ma po prostu wpływu.

Jeśli nie da się czegoś rozwiązać wprost, zawsze można spróbować to… obejść. Wbrew pozorom, w programowaniu zdarza się to nierzadko. ;)

A o czym chcę napisać? O wyrzucaniu błędów przez PHP zamiast wyjątków.

chore pomysły?

Na Blipie, na tagu #php zawrzała dyskusja: Python vs. PHP. Przytaczanie całej dyskusji byłoby raczej bezsensowne i bym się zajeździł z kopiowaniem wypowiedzi wszystkich użytkowników. W pewnym momencie na tapecie znalazł się temat totalnego non-sensu PHP w postaci wyrzucania błędów zamiast wyjątków. I tu się w zupełności zgodzę - przecież funkcje np. otwierania pliku muszą najpierw sprawdzić, czy ten plik istnieje, a i tak wyrzuci błąd.

Aby uniknąć błędów, które nieraz ciężko wyłapać (bo przecież nie wpływają na dalsze działanie kodu), trzeba albo “wyciszyć” wypluwanie warningów korzystając z małpiszona przed funkcją, albo sprawdzić poprzez file_exists istnienie obiektu źródłowego oraz jego uprawnienia przez is_readable. Po kiego, skoro jest to ponowne wykonanie czegoś, co zostało już sprawdzone.

Ok, skoro są błędy, to PHP pozwala też coś z nimi zrobić - albo schować, albo zignorować, albo wyłapać. Zajmiemy się tym ostatnim przypadkiem. set_error_handler chyba większość Czytelników już zna (albo właśnie poznaje). Pozwala na przechwycenie wypluwanego przez wszystkie funkcje błędu i ew. zalogowanie, etc. Oczywiście, z np. PARSE_ERROR już nie jest tak różowo (bo błędu parsowania kodu się nie da przechwycić - co by było w sytuacji, gdyby ów znajdował się w algorytmie przechwytywania? ;)).

PHP daje popalić

“Najprostsze rozwiązania są zwykle najlepsze”. Przeanalizujmy poniższy kawałek kodu.

  1. $f = fopen('asdasdasdasd', 'r');

Otrzymamy warning: … failed to open stream. No ok, ale to przy aktywnym wyświetlaniu błędów. Zwykle podobny kod jest wykorzystywany z innymi przydatnymi funkcjami (celowo tutaj nie uwzględniam file_get_contents, etc):

  1. $f = fopen('asdasdasd', 'r');
  2. flock($f, LOCK_SH);
  3.  
  4. while(!feof($f)){
  5.     echo fread($f, 512);
  6. }

Otrzymamy już więcej niż jeden błąd. Na serwerze produkcyjnym nie wyświetli się żaden (przynajmniej nie powinien), jeśli administrator nie pilnuje serwisu, nikt może nawet nie zauważyć, że coś jest nie tak.

Ale nasz admin, czy programista jest jednak trochę bardziej łebski i zastosował dodatkowo poniższy kawałek kodu:

  1. function catchError($no, $str, $file, $line){
  2.     logger::instance()->write($str.' -> '.$file.':'.$line);
  3.     return true;
  4. }
  5.  
  6. set_error_handler('catchError');

Fajnie, cacy, logujemy sobie to do pliku, czasem admin zajrzy i znajdzie błędy. Ok, tylko że idealny kod wyglądałby tak:

  1. if(file_exists($fname) && is_readable($fname) && $f = fopen($fname, 'r')){
  2.     if(flock($f, LOCK_SH)){
  3.         while(!feof($f)){
  4.             echo fread($f, 512);
  5.         }
  6.     }else{
  7.         echo 'coś nie tak';
  8.     }
  9. }else{
  10.     echo 'coś nie tak';
  11. }

Aż się prosi o wyjątki. Zamiast tego mamy w chorobę sprawdzania i tak naprawdę - jest to spora część tego, co chcemy zrobić. Oczywiście, jeśli są to błędy, to można pójść na lenia i zrezygnować ze sprawdzania oraz to wyciszyć.

Idealnie by było jednak to wpakować w blok try..catch i obsłużyć wyjątkiem, ale standardowe funkcje PHP ich nie wyrzucają. Dopiero biblioteki, które powstały w późnych latach programowania PHP5 zaczęły ich używać. W innych językach, to standard, coś w stylu IOException, czy pochodne. Dlaczego PHP tego nie ma?!

prawa Murphy’ego: “prowizorka jest zawsze najlepsza”

Miałem okazję rozmawiać z gościem na roku, który już nieco programował, ale wątpił w filozofię wyjątków. No ok, jego sprawa, ale rozważmy prostą sytuację:

  1. $f = @fopen('zuo', 'r');

No i nasza zmienna $f zawiera albo zasób, albo false. A skąd mamy wiedzieć, czy to był błąd timeoutu, czy brak uprawnień, czy po prostu nie istniejącego pliku? Użyjmy bardzo prostego przykładu (fakt, trochę naginany, ale zobrazuje moją myśl bez zbędnego owijania):

  1. try{
  2.     $f = fopen('asd', 'a');
  3.     flock($f, LOCK_EX);
  4.     //...
  5. }catch (IOException $ex){
  6.     switch ($ex->type){
  7.         case 'noExists':
  8.             //
  9.         break;
  10.         case 'noPermissions':
  11.             //
  12.         break;
  13.     }
  14. }

Aby zrealizować ten hipotetyczny kawałek kodu, należałoby użyć mniej więcej:

  1. class IOException extends Exception {}
  2.  
  3. try {
  4.     if (!file_exists('asd')) {
  5.         throw new IOException('noExists');
  6.     }
  7.     if (!is_writable('asd')) {
  8.         throw new IOException('noPermissions');
  9.     }
  10.     $f = fopen(...);
  11.  
  12.     //...
  13. }
  14. catch (IOException $ex) {
  15.     // reszta jak wyżej
  16. }

Noż w mordę! Wymyślanie koła na nowo. A jeśli jest, do tego, takich miejsc w kodzie kilkanaście albo i więcej? Koszmar. Niektórzy pewnie napisali już swojego obiektowego wrappera, który załatwia takie rzeczy. Jednak nadal kuszą te wyjątki. Ciekawe, ile ticketów w tej sprawie wisi na bugtracku developerów PHP;)

Wpadł mi jednak do głowy pewien pomysł.

jak nie do przodu, to tyłem naprzód

Najlepiej niech kod przemówi:

  1. function catchError(...) {
  2.     throw new Exception();
  3. }
  4.  
  5. set_error_handler('catchError');

Myślę sobie, za cholerę nie będzie działać. Jednak przyzwyczajony do niektórych dziwactw tego języka postanowiłem odpalić podobny do powyższego kawałek kodu:

  1. function catchError($no, $str, $file, $line) {
  2.     throw new Exception($str);
  3. }
  4.  
  5. set_error_handler('catchError');
  6.  
  7. try {
  8.     echo 'raz';
  9.     fopen('asdasd', 'r');
  10.    
  11.     echo 'dwa';
  12.     $p = file_get_contents('asfjhsdfkd');
  13.    
  14.     echo 'trzy';
  15. }catch (exception $ex) {
  16.     echo $ex;
  17. }

Jakież było moje zdziwienie, gdy przeglądarka wyświetliła:

  1. razexception 'Exception' with message 'fopen(asdasd) [function.fopen]: failed to open stream: No such file or directory' in E:\www\poligon\exception\test.php:6
  2. Stack trace:
  3. #0 [internal function]: catchError(2, 'fopen(asdasd) [...', 'E:\www\poligon\...', 13, Array)
  4. #1 E:\www\poligon\exception\test.php(13): fopen('asdasd', 'r')
  5. #2 {main}

Wyświetliło się tylko raz i zrzutowany na stringa wyjątek. Przecież to ustrojstwo… Działa?! A stąd już krótka droga do rozróżnienia, który błąd wystąpił.

że co?!

Tak, do tej pory jakoś do mnie nie trafia, że:

  • w ogóle coś takiego zadziałało
  • że tak prostego obejścia jeszcze się nie wykorzystuje na szerszą skalę

Owszem, na pewno jest to rozwiązanie wolniejsze niż prawdopodobna implementacja w kodzie interpretera. Jednak zyskujemy coś, co pozwala nam na lepszą i wygodniejszą kontrolę kodu. Już sam fakt, że został wypluty wyjątek - tak naprawdę - ma dość duże znaczenie.

Pozostaje jeszcze dostosowanie funkcji wyłapującej błędy tak, aby wypluwać poszczególne rodzaje wyjątków.

  1. class IOException extends Exception {
  2.     public $type = null;
  3.    
  4.     function __construct($message = null, $code = 0) {
  5.         parent::__construct($message, $code);
  6.        
  7.         if(strpos($message, 'no such') !== null){
  8.             $this->type = 'noExists';
  9.         }else if (strpos($message, 'permission') !== null) {
  10.             $this->type = 'noPermissions';
  11.         }
  12.     }
  13. }
  14.  
  15. function catchError($no, $str, $file, $line) {
  16.     if ($no == E_WARNING) {
  17.         $func = substr($str, 0, strpos($str, '('));
  18.         switch ($func) {
  19.             case 'fopen':
  20.             case 'fwrite':
  21.             case 'file_get_contents':
  22.             case 'file_put_contents':
  23.             case 'flock':
  24.                 //...
  25.                 throw new IOException($str);
  26.         }
  27.     }
  28. }
  29.  
  30. set_error_handler('catchError');
  31.  
  32. try {
  33.     echo 'raz';
  34.     fopen('asdasd', 'r');
  35.    
  36.     echo 'dwa';
  37.     $p = file_get_contents('asfjhsdfkd');
  38.    
  39.     echo 'trzy';
  40. }
  41. catch (IOException $ex) {
  42.     echo $ex->type;
  43. }

Nie jest to może idealne sprawdzanie w wyjątku, ale… działa.

koniec?

Przyznam szczerze, że do tej pory nie bardzo mieści się to wszystko w moim małym rozumku, że dość prowizoryczne rozwiązanie po prostu działa. Pewnie znalazłyby się lepsze metody wykrywania rodzaju błędu w wyjątku, ale chciałem zaprezentować tylko ogólną ideę.

Zapraszam do dyskusji. :)

23 komentarzy

dopisz swój :: trackback :: RSS z komentarzami

RSS z komentarzami :: trackback

Skomentuj

Możesz używać znaczników XHTML. Dozwolone są następujące tagi: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>

Wszystkie komentarze przechodzą przez moderację oraz filtry antyspamowe. Nie zostanie opublikowany komentarz, jeśli:

  • Jego treść obraża kogokolwiek.
  • W treści znajdują się wulgaryzmy i słownictwo ogólnie uznane za nieprzyzwoite.
  • Mam wątpliwości co do autora wpisu (Wszelkie anonimy są kasowane - niezależnie od zawartości - wpisz prawdziwy e-mail. Jeśli usunąłem, Twoim zdaniem, komentarz niesłusznie - daj znać). Zdarza się, iż sprawdzam kim jest komentujący.
  • Zawiera jakąkolwiek formę reklamy.

Szufladka