venerdì, agosto 30, 2013

PHP - Gestione delle eccezioni


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.

Introduzione

Non sempre possiamo dare per scontato che le cose funzionino come dovrebbero.
Supponiamo di dover leggere un file e elaborarlo in qualche modo.
L'istruzione di base è

$lines=file('foo.txt');

Siamo sicuri che il file esista? E che sia leggibile?

Se il file non esistesse, otterremmo

Warning
: file(foo.txt) [function.file]: failed to open stream: No such file or directory in... 

Se il file non fosse leggibile, otterremmo

Warning: file(foo.txt) [function.file]: failed to open stream: Permission denied in...

L'approccio del pessimista

L'approccio del pessimista si basa sullo scrivere del codice di test prima di effettuare l'operazione:

$filename='foo.txt';
if (file_exists($filename) && is_readable($filename))
{
  $lines=file('foo.txt');
}
else
{
  // some code here...
}

I problemi di questo approccio sono:
  1. le cose potrebbero andare male anche per altri motivi oltre a quelli presi in considerazione nel test (il file esiste ed è leggibile, ma ci sono blocchi danneggiati nel disco...);
  2. vengono fatte tre cose anziché una;
  3. nella frazione di secondo tra l'esecuzione dei test e l'esecuzione delle operazioni lo stato potrebbe cambiare (un altro processo cambia i permessi sui file);
  4. in alcuni casi (ad esempio la connessione ad un database con determinate credenziali) non c'è modo di verificare prima se le credenziali sono corrette.
Un vero pessimista quindi non potrebbe usarlo... :-)

L'approccio dell'ottimista

Un ottimista potrebbe dare per scontato che le cose vadano bene, ma tenere in considerazione l'ipotesi (da lui considerata remota) che invece vadano storte. Scriverà quindi un codice di questo genere, mettendo il silenziatore (simbolo @) all'istruzione che potrebbe fallire e controllandone l'esito a posteriori:

$lines=@file('foo.txt');
if ($lines)
{
  print_r($lines);
}
else
{
  // some code here...
}

Anche questo approccio ha dei problemi (vedi al riguardo Five reasons why the shut-op operator (@) should be avoided):
  1. vengono nascosti messaggi di errore insospettabili, rendendo più difficile il debug;
  2. rende l'esecuzione più lenta, perché tutto il meccanismo delle impostazioni (file php.ini) è invocato per cambiare il valore della variabile error_reporting) e il codice non viene ottimizzato (TODO: benchmark?)

L'approccio try... catch


L'approccio serio è di includere il codice che potrebbe fallire in un blocco try e gestire separatamente il possibile errore:

try
{
  $filename='foo.txt';
  if (!$lines=file($filename))
  {
    throw new Exception(sprintf('Could not read file "%s"', $filename));
  };
  print_r($lines);
}
catch (Exception $e)
{
  // do something here...
}


Se si vogliono evitare gli spiacevoli messaggi di warning, si dovrà lavorare sul file di configurazione php.ini (in produzione non li si vuole, nell'ambiente di sviluppo sì). In ambiente di produzione potrà essere utile impostare error_log a true, in modo da avere i messaggi di errore scritti nel log del server web.

Cosa fare nella gestione dell'eccezione dipende dai singoli casi, ma è bene sapere che un oggetto di tipo Exception mette a disposizione delle funzioni membro per accedere a informazioni utili per il debug:

catch (Exception $e)
{
  echo "Something went wrong\n";
  echo sprintf("message: %s\n", $e->getMessage());
  echo sprintf("file: %s\n", $e->getFile());
  echo sprintf("line: %s\n", $e->getLine());
}

con un risultato simile al seguente:

Something went wrong
message: Could not read file "foo.txt"
file:    /var/www/...mycode.php
line:    61

Subclassing delle eccezioni e catene di eccezioni

Può essere utile definire delle eccezioni personalizzate in modo da gestire in maniera specifica i vari problemi che si possono presentare. Inoltre, esiste un meccanismo a catena di blocchi try... catch che consente di recuperare informazioni su cosa è andato storto. Si veda questo esempio completo:

class FileNotReadableException extends Exception
{
}

function readFooFile($filename)
{
  try
  {
    if (!$lines=file($filename))
    {
      throw new FileNotReadableException(sprintf('Could not read file "%s"', $filename)); 
    };
    return $lines;
  }
  catch (Exception $e)
  {
    // there was another kind of error...
    throw $e;
  }
}


try
{
  print_r(readFooFile('foo.txt'));
}
catch (Exception $e)
{
  if ($e instanceof FileNotReadableException)
  {
    echo "I couldn't read the file\n";
    echo sprintf("message: %s\n", $e->getMessage());
    echo sprintf("file:    %s\n", $e->getFile());
    echo sprintf("line:    %s\n", $e->getLine());
    echo "trace:\n";
    foreach($e->getTrace() as $number=>$error)
    {
      echo sprintf("  error %d:\n", $number);
      foreach($error as $key=>$value)
      {
        echo sprintf("    %s: %s\n", $key, $value);
      }
    }
  }
  else
  {
    echo "Something went wrong for an unknown reason...\n";
  }
}

in cui:
  1. viene definita una classe personalizzata FileNotReadableException
  2. viene definita una funzione che lancia un'eccezione presa in carico nel codice principale
  3. viene controllato il tipo di eccezione lanciata (con l'operatore instance_of)

Il risultato potrebbe essere simile al seguente:

I couldn't read the file
message: Could not read file "foo.txt"
file: /var/www/corsophp/loris/lezioni/lezione_eccezioni.php
line: 62
trace:
  error 0:
    file: /var/www/corsophp/loris/lezioni/lezione_eccezioni.php
    line: 76
    function: readFooFile
    args: Array

In alternativa, è possibile impostare una serie di catch in cui si specificano i tipi di eccezione:

...
catch (FileNotReadableException $e)
{
  echo "I couldn't read the file\n";
  echo sprintf("message: %s\n", $e->getMessage());
  echo sprintf("file:    %s\n", $e->getFile());
  echo sprintf("line:    %s\n", $e->getLine());
  echo "trace:\n";
  foreach($e->getTrace() as $number=>$error)
  {
    echo sprintf("  error %d:\n", $number);
    foreach($error as $key=>$value)
    {
      echo sprintf("    %s: %s\n", $key, $value);
    }
  }
}
catch (Exception $e)
{
  echo "Something went wrong for an unknown reason...\n";
}

Il meccanismo è delle catene di eccezioni è alla base degli strumenti di debug di Symfony:


Classi predefinite di eccezioni

La Standard PHP Library mette a disposizione alcuni tipi di classi derivare di eccezioni, che potrebbero essere utilmente utilizzate.
Nell'esempio qui sopra, avremmo potuto scrivere:

class FileNotReadableException extends RunTimeException
{
}

Nessun commento:

Posta un commento