Come gestire gli errori con email di report in PHP

php_logoUno dei passi finali legati alla pubblicazione di un sito PHP consiste nel disattivare la visualizzazione degli errori.
Seppure sia vero che sarebbe meglio che un sito pubblicato non contenga errori, è altrettanto vero che non esiste un’applicazione priva di bug.
Quello che possiamo fare quindi è evitare che la visualizzazione degli errori riveli informazioni che dovrebbero vedere solo gli sviluppatori e che rischiano di compromettere la sicurezza della nostra applicazione, del server stesso o la privacy dei nostri utenti.

Disattivare la visualizzazione degli errori è molto semplice, ci basta un’istruzione:

ini_set('display_errors',0);

Ma così facendo come facciamo ad accorgerci se si verifica un errore?
Il nostro obiettivo è di venire avvisati tramite una email dell’errore avvenuto e di mostrare all’utente finale un semplice messaggio che consigli di riprovare più tardi.

Error Handler Personalizzato

PHP ci fornisce la possibilità di specificare una funzione personalizzata per gestire gli errori: set_error_handler.
E’ importante notare che la nostra funzione verrà chiamata per qualsiasi errore si verifichi, compresi i NOTICE e gli errori soppressi con l’operatore @.

Nell’esempio che andrò ad illustrare faccio uso di Zend_Mail e Zend_Config per l’invio delle email di notifica e la gestione della configurazione, ma è tutto facilmente adattabile ad altri framework o all’uso della semplice funzione mail.

Lo scheletro della nostra funzione di gestione degli errori si presenta così:

function mail_error_report($errno, $errstr, $errfile, $errline, $errcontext) {
    if (!(error_reporting() & $errno)) return;

    $terminate = false;
    switch ($errno) {
        case E_NOTICE:
        case E_USER_NOTICE:
            $messaggio = "Notice";
        break;
        case E_WARNING:
        case E_USER_WARNING:
            $messaggio = "Warning";
            $terminate = true;
        break;
        case E_ERROR:
        case E_USER_ERROR:
        case E_RECOVERABLE_ERROR:
            $messaggio = "Fatal error";
            $terminate = true;
        break;
        case E_STRICT:
            $messaggio = "Strict error";
        break;
        case E_DEPRECATED:
        case E_USER_DEPRECATED:
            $messaggio = "Deprecated";
        break;
        default:
            $messaggio = "Unknown";
        break;
    }

    if ($terminate)
        die();
    return true;
}

La prima riga all’interno della nostra funzione è forse quella più interessante e serve a onorare la configurazione error_reporting specificata nel php.ini o a runtime e a ignorare gli errori soppressi con @, in questo modo evitiamo di interrompere l’applicazione o mandare email per errori che sono stati esplicitamente ignorati.

Lo switch che segue serve a tradurre il nr di errore $errno, in una stringa di testo leggibile e a decidere in che caso interrompere lo script. In questo caso si è deciso che i “Fatal Error” e i “Warning”, interrompono l’esecuzione. Questi errori infatti sono quelli che quasi sicuramente, se lasciati proseguire, produrrebbero risultati inaspettati e potenzialmente dannosi.
Per gli altri errori invieremo solo la mail di avviso e l’errore verrà soppresso restituendo “true”, così da notificare all’interprete PHP che l’errore è da considerarsi gestito e che l’error_handler predefinito non deve essere interpellato.

Ora completiamo la funzione con l’invio effettivo della email e con la presentazione del messaggio al visitatore che è incappato nell’errore.

Il Report

function getvarinfo(&$var) {
    if (is_null($var)) return 'null';
    if (is_bool($var)) return $var ? 'true' : 'false';

    ob_start();
    print_r($var);
    $ret = ob_get_contents();
    ob_end_clean();
    return $ret;
}

function mail_error_report($errno, $errstr, $errfile, $errline, $errcontext) {
    if (!(error_reporting() & $errno)) return;

    global $config;

    $mail = new Zend_Mail('UTF-8');
    foreach ($config->error_reporting->recipients as $recipient)
        $mail->addTo($recipient);
    $mail->setSubject( "PHP script error on {$_SERVER['SERVER_NAME']}" );

    $terminate = false;
    switch ($errno) {
        case E_NOTICE:
        case E_USER_NOTICE:
            $messaggio = "Notice";
        break;
        case E_WARNING:
        case E_USER_WARNING:
            $messaggio = "Warning";
            $terminate = true;
        break;
        case E_ERROR:
        case E_USER_ERROR:
        case E_RECOVERABLE_ERROR:
            $messaggio = "Fatal error";
            $terminate = true;
        break;
        case E_STRICT:
            $messaggio = "Strict error";
        break;
        case E_DEPRECATED:
        case E_USER_DEPRECATED:
            $messaggio = "Deprecated";
        break;
        default:
            $messaggio = "Unknown";
        break;
    }

    $messaggio .= ": $errstr in $errfile on line $errline\\n";

    // Alcune informazioni utili sulla richiesta
    if (array_key_exists('SCRIPT_NAME', $_SERVER))
        $messaggio .= "SCRIPT NAME: {$_SERVER['SCRIPT_NAME']}\\n";
    if (array_key_exists('REQUEST_URI', $_SERVER))
        $messaggio .= "REQUEST URI: {$_SERVER['REQUEST_URI']}\\n";
    if (array_key_exists('QUERY_STRING', $_SERVER))
        $messaggio .= "QUERY STRING: {$_SERVER['QUERY_STRING']}\\n";
    $messaggio .= "\\nContext:\\n\\n";

    // Includo le variabili del contesto in cui  avvenuto l'error, saltando $GLOBALS per evitare inutili duplicati
    foreach ($errcontext as $name => $value)
        if ($name != 'GLOBALS')
            $messaggio .= "\\$$name = ".getvarinfo($value)."\\n";

    $mail->setBodyText( trim($messaggio)."\\n\\n" );
    $mail->send();

    if ($terminate) {
        header("HTTP/1.1 500 Internal Server Error");
        die("<p>Si  verificato un errore imprevisto. Il nostro team di tecnici  stato avvisato dell'accaduto e provveder a breve a risolvere l'inconveniente.</p><p>La preghiamo di riprovare pi tardi.</p><p>Grazie.</p>");
    }
    return true;
}

// Uso una variabile di configurazione per abilitare/disabilitare la reportistica via email
if ($config->error_reporting->enabled) {
    ini_set('display_errors', 0);

    if (count($config->error_reporting->recipients))
        set_error_handler('mail_error_report');
}

Il nostro report è così completo di tutte le informazioni che possono servirci e all’utente è presentato un cordiale messaggio col corretto codice di errore HTTP 500, fondamentale per evitare spiacevoli indicizzazioni dei motori di ricerca e inutili salvataggi in cache nei browser.

Catturare i Compile Errors

Tuttavia questo codice non copre tutti i tipi di errore che possono verificarsi, ce ne sono infatti alcuni che non vengono gestiti dal nostro error_handler, come ad esempio gli errori di sintassi o altre tipologie di errori che vengono rilevati in una fase troppo prematura all’interno dell’interpretazione dello script perchè l’handler possa essere chiamato.

Qui entra in gioco un piccolo trucchetto che ci permette di catturare anche queste ultime eccezioni: Impostando una funzione che venga eseguita in fase di shutdown (del PHP, non del server), possiamo controllare tramite error_get_last, se si sono verificati errori che non sono stati gestiti, e in tal caso, passarli alla nostra funzione.
Ecco come fare:

// [...]

// Uso una variabile di configurazione per abilitare/disabilitare la reportistica via email
if ($config->error_reporting->enabled) {
    ini_set('display_errors', 0);

    if (count($config->error_reporting->recipients)) {
        set_error_handler('mail_error_report');

        function shutdown() {
            $error = error_get_last();
            if ($error && in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR)))
                mail_error_report($error['type'], $error['message'], $error['file'], $error['line'], $GLOBALS);
        }
        register_shutdown_function('shutdown');
    }
}