PHP: Gestione degli errori con logging e rotazione automatica dei file

by theArchitect
SHARE
PHP: Gestione degli errori con logging e rotazione automatica dei file
© Guida-HTML5.it

Introduzione

Quando un’applicazione PHP cresce, salvare gli errori su un semplice file di log non basta più. Un file unico può diventare troppo grande, difficile da consultare e persino rischioso da gestire in produzione. Un approccio molto utile e pratico è la gestione degli errori con logging e rotazione automatica dei file: gli errori vengono scritti in file separati per giorno, dimensione o contesto, così da mantenere i log ordinati, leggibili e facili da analizzare.

Questo tutorial ti mostra come costruire un sistema semplice ma robusto per:

  • intercettare errori e eccezioni in PHP;
  • registrare messaggi di log in modo strutturato;
  • ruotare automaticamente i file quando cambiano giorno o superano una dimensione massima;
  • mantenere il codice pulito e adatto a un progetto reale.

L’obiettivo non è creare un framework completo, ma una soluzione concreta che puoi integrare facilmente in un progetto PHP intermedio.

Codice completo

<?php
declare(strict_types=1);

/**
 * Logger con rotazione automatica:
 * - crea un file di log al giorno
 * - se il file supera una certa dimensione, lo rinomina con suffisso
 * - intercetta errori PHP, eccezioni e fatal error
 */
class RotatingLogger
{
    private string $logDir;
    private string $baseFileName;
    private int $maxFileSizeBytes;

    public function __construct(string $logDir, string $baseFileName = ´app.log´, int $maxFileSizeBytes = 1048576)
    {
        $this->logDir = rtrim($logDir, DIRECTORY_SEPARATOR);
        $this->baseFileName = $baseFileName;
        $this->maxFileSizeBytes = $maxFileSizeBytes;

        if (!is_dir($this->logDir)) {
            mkdir($this->logDir, 0775, true);
        }
    }

    public function log(string $level, string $message, array $context = []): void
    {
        $filePath = $this->getLogFilePath();

        $entry = sprintf(
            "[%s] [%s] %s %s%s",
            date(´Y-m-d H:i:s´),
            strtoupper($level),
            $message,
            $this->formatContext($context),
            PHP_EOL
        );

        $this->rotateIfNeeded($filePath);

        file_put_contents($filePath, $entry, FILE_APPEND | LOCK_EX);
    }

    public function registerHandlers(): void
    {
        set_error_handler([$this, ´handlePhpError´]);
        set_exception_handler([$this, ´handleException´]);
        register_shutdown_function([$this, ´handleShutdown´]);
    }

    public function handlePhpError(int $severity, string $message, string $file, int $line): bool
    {
        // Trasforma gli errori PHP in log strutturato
        $this->log(´error´, $message, [
            ´severity´ => $severity,
            ´file´ => $file,
            ´line´ => $line
        ]);

        // Restituiamo false per permettere a PHP di gestire anche il suo flusso standard
        return false;
    }

    public function handleException(Throwable $exception): void
    {
        $this->log(´critical´, $exception->getMessage(), [
            ´exception´ => get_class($exception),
            ´file´ => $exception->getFile(),
            ´line´ => $exception->getLine(),
            ´trace´ => $exception->getTraceAsString()
        ]);

        http_response_code(500);
        echo "Si è verificato un errore interno.";
    }

    public function handleShutdown(): void
    {
        $error = error_get_last();

        if ($error !== null) {
            $types = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
            if (in_array($error[´type´], $types, true)) {
                $this->log(´fatal´, $error[´message´], [
                    ´file´ => $error[´file´],
                    ´line´ => $error[´line´]
                ]);
            }
        }
    }

    private function getLogFilePath(): string
    {
        // File giornaliero: app-2026-06-20.log
        $datePart = date(´Y-m-d´);
        return $this->logDir . DIRECTORY_SEPARATOR . pathinfo($this->baseFileName, PATHINFO_FILENAME) . ´-´ . $datePart . ´.log´;
    }

    private function rotateIfNeeded(string $filePath): void
    {
        if (!file_exists($filePath)) {
            return;
        }

        clearstatcache(true, $filePath);

        if (filesize($filePath) < $this->maxFileSizeBytes) {
            return;
        }

        $timestamp = date(´His´);
        $rotatedFile = preg_replace(´/.log$/´, ´´, $filePath) . ´-´ . $timestamp . ´.log´;

        rename($filePath, $rotatedFile);
    }

    private function formatContext(array $context): string
    {
        if (empty($context)) {
            return ´´;
        }

        return json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
}

// =========================
// ESEMPIO DI UTILIZZO
// =========================

$logger = new RotatingLogger(__DIR__ . ´/logs´, ´app.log´, 1024 * 1024);
$logger->registerHandlers();

// Log manuale
$logger->log(´info´, ´Applicazione avviata correttamente´, [
    ´user_id´ => 42,
    ´module´ => ´dashboard´
]);

// Esempio di eccezione gestita
try {
    throw new RuntimeException(´Connessione al database non disponibile´);
} catch (Throwable $e) {
    $logger->handleException($e);
}

// Esempio di errore PHP (warning) intercettato
echo $undefinedVariable;

// Esempio di fatal error simulato: chiamata a funzione inesistente
// nonExistingFunction();

Spiegazione

La classe RotatingLogger centralizza tre compiti fondamentali: scrittura del log, gestione degli handler di errore e rotazione dei file. Vediamola in modo ordinato.

1. Costruttore e directory dei log

Nel costruttore definiamo:

  • la cartella dove salvare i file;
  • il nome base del file;
  • la dimensione massima consentita.

Se la directory non esiste, viene creata automaticamente. In un progetto reale questo evita errori banali di configurazione e rende il logger più autonomo.

2. Metodo log()

Il metodo log() costruisce una riga leggibile con:

  • data e ora;
  • livello di gravità;
  • messaggio;
  • contesto opzionale in formato JSON.

Prima di scrivere, il logger chiama rotateIfNeeded() per controllare se il file ha superato la soglia. Questo è importante: così eviti che il file cresca senza controllo.

3. Rotazione automatica

La rotazione qui è semplice ma efficace. Se il file supera la dimensione massima, viene rinominato con un suffisso orario. Per esempio:

  • app-2026-06-20.log
  • app-2026-06-20-153012.log

In questo modo il file corrente resta pulito, mentre quello vecchio viene conservato come archivio. La strategia è utile quando vuoi evitare file enormi e mantenere una cronologia consultabile.

4. Gestione degli errori PHP

Con set_error_handler() intercetti errori come warning, notice e altri problemi non fatali. Il metodo handlePhpError() registra anche informazioni utili come severità, file e linea.

Questa informazione contestuale è preziosa perché ti permette di capire subito dove è nato il problema, senza dover riprodurre manualmente il bug.

5. Gestione delle eccezioni

Con set_exception_handler() possiamo intercettare eccezioni non gestite. Nel nostro esempio, il logger salva:

  • classe dell’eccezione;
  • messaggio;
  • file e linea;
  • stack trace completo.

In produzione, dopo il log, mostriamo un messaggio generico all’utente e restituiamo HTTP 500. È una buona pratica: il dettaglio tecnico resta nel log, mentre l’utente vede un errore pulito e sicuro.

6. Errori fatali allo shutdown

Alcuni errori, come i fatal error, non possono essere gestiti normalmente. Per questo usiamo register_shutdown_function() e leggiamo error_get_last(). Se l’errore appartiene alle categorie critiche, viene scritto nel log prima della chiusura dello script.

Best practice

  • Usa livelli di log coerenti: info, warning, error, critical, fatal. Ti aiuterà a filtrare i problemi più velocemente.
  • Non salvare dati sensibili: evita password, token, dati personali o query complete con informazioni riservate.
  • Proteggi la cartella dei log: in produzione la directory non dovrebbe essere accessibile direttamente via web.
  • Scrivi log strutturati: il contesto in JSON rende più semplice l’analisi manuale e automatica.
  • Non mostrare dettagli tecnici all’utente: il messaggio pubblico deve essere generico, mentre il log conserva il dettaglio.
  • Usa rotazione e retention: oltre alla rotazione, valuta una pulizia automatica dei file più vecchi di un certo numero di giorni.
  • Evita logging eccessivo: troppe informazioni inutili rendono i log rumorosi e difficili da analizzare.

Riepilogo

La gestione degli errori con logging e rotazione automatica è una soluzione molto utile per applicazioni PHP in crescita. Ti permette di:

  • tenere sotto controllo gli errori senza perdere informazioni;
  • evitare file di log troppo grandi;
  • separare i problemi per data o dimensione;
  • migliorare la manutenzione e il debug del progetto.

Con poche decine di righe di codice puoi costruire un sistema già valido per ambienti di sviluppo e piccoli progetti di produzione. Se poi il progetto cresce, potrai evolverlo verso soluzioni più avanzate, come librerie dedicate o integrazione con sistemi di osservabilità.

Approfondisci con risorse ufficiali

  • Manuale PHP - Error Handling: documentazione ufficiale su gestione di errori, eccezioni e handler personalizzati.
  • Manuale PHP - set_error_handler(): dettagli su come intercettare gli errori PHP.
  • Manuale PHP - set_exception_handler(): guida alla gestione centralizzata delle eccezioni.
  • Manuale PHP - register_shutdown_function(): utile per intercettare errori fatali in fase di terminazione.
  • Manuale PHP - file_put_contents(): riferimento per la scrittura sicura su file.

Se vuoi, nel prossimo passo posso anche scrivere una versione di questo tutorial con PSR-3 e Monolog, così da mostrare come gestire logging e rotazione in modo più professionale in un progetto moderno.

SHARE