Il problema degli stack trace nei framework
Quando un framework lancia un'eccezione, lo stack trace punta al codice interno del framework — non al codice dell'applicazione che ha causato l'errore. Se il query builder fallisce perche hai passato un nome di colonna inesistente, lo stack trace mostra la riga dentro ActiveQuery::where(), non la riga nel tuo controller dove hai scritto la query sbagliata. Per chi sviluppa con il framework, il file e la riga piu utili sono quelli del proprio codice, non quelli del core.
Soft PHP MVC risolve questo problema con un trucco elegante nella classe base CoreException: il costruttore usa debug_backtrace() per risalire al vero punto di origine dell'errore e sovrascrive le proprieta $this->file e $this->line dell'eccezione.
CoreException: il cuore della gerarchia
CoreException e una classe astratta che estende Exception di PHP. Il suo costruttore accetta tre parametri: il messaggio di errore, il codice HTTP, e un $traceId che indica quanti livelli risalire nello stack per trovare il vero chiamante. Il default e 1, cioe il frame immediatamente sopra il punto dove l'eccezione viene lanciata.
Il meccanismo funziona cosi: debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $traceId + 1) cattura lo stack fino al livello richiesto, senza includere gli argomenti delle funzioni (per ragioni di performance e sicurezza — non vuoi che password o token finiscano nello stack trace). Dal frame individuato, estrae file e line e li usa per sovrascrivere le proprietà dell'eccezione.
Il messaggio viene anche arricchito con (Originated in file.php on line 42), rendendo il debug immediato anche nei log dove lo stack trace completo potrebbe non essere presente.
Il parametro $traceId e fondamentale per i casi in cui l'eccezione non viene lanciata direttamente nel codice utente, ma in un metodo helper intermedio. Se un helper del framework chiama un metodo che lancia l'eccezione, con $traceId = 2 risali al codice dell'applicazione saltando il frame dell'helper.
La gerarchia: 16 eccezioni specializzate
Da CoreException derivano eccezioni specifiche per ogni tipo di errore che il framework puo incontrare:
- NotFoundException (404) — risorsa non trovata: articolo, progetto, route
- ValidationException (422) — validazione input fallita, porta con se l'array degli errori per campo
- UnauthorizedException (401) — accesso non autorizzato, utente non autenticato
- DecryptionException — payload cifrato corrotto o chiave sbagliata
- ModelException — errori nella configurazione o nell'uso dei modelli ORM
- QueryBuilderException — errori nella costruzione delle query (join invalidi, colonne inesistenti)
- ViewException — template non trovato, errori nel rendering
- FileNotFoundException, StorageException, FileSystemException — operazioni su file fallite
Ogni eccezione ha un codice HTTP di default appropriato. Questo permette all'ExceptionHandler di mappare automaticamente l'eccezione alla risposta corretta senza logica condizionale nel controller.
ExceptionHandler: il catch universale
L'ExceptionHandler e il punto dove tutte le eccezioni non gestite convergono. In Mvc::run(), il blocco try/catch attorno a $router->resolve() cattura qualsiasi Throwable e lo passa a ExceptionHandler::handle().
Il metodo handle() fa tre cose in sequenza:
1. Determina il codice HTTP. Un'espressione match mappa le eccezioni note ai loro codici: NotFoundException a 404, ValidationException a 422, UnauthorizedException a 401. Per le eccezioni generiche, se il codice dell'eccezione e gia un codice HTTP valido (tra 400 e 599), lo usa; altrimenti fallback a 500.
2. Log degli errori server. Solo gli errori 500+ vengono loggati tramite Log::exception(). Un 404 non e un errore del server — e un comportamento normale. Un 500 invece indica un bug o un problema infrastrutturale e deve essere tracciato.
3. Content negotiation. Se la Response rileva che il client vuole JSON (wantsJson() — basato sull'header Accept o sul prefisso della route), l'errore viene restituito come JSON con error, code e, per le ValidationException, l'array errors con i dettagli per campo. Altrimenti, viene renderizzata la pagina di errore HTML corrispondente al codice.
ValidationException: errori strutturati
ValidationException e un caso speciale perche non porta solo un messaggio, ma un array associativo di errori per campo: ['email' => ['Il campo email e obbligatorio'], 'password' => ['Minimo 8 caratteri']]. Il metodo getErrors() restituisce questo array, che l'ExceptionHandler include nella risposta JSON per le API.
Per le richieste web, gli errori vengono flashati in sessione tramite SessionStorage::flashErrors() e resi disponibili nella view successiva per mostrare i messaggi di validazione accanto ai campi del form. Il flusso e: validazione fallisce → ValidationException → redirect back → errori letti dalla sessione flash → mostrati nel form.
I due layer di error handling
Il framework ha due livelli distinti di gestione errori, attivi contemporaneamente:
NativeErrorProvider registra un handler con set_error_handler() che cattura errori e warning PHP nativi (notice, deprecation, strict). Questi vengono loggati in app.log ma non interrompono l'esecuzione (a meno che non siano fatal). E il livello di guardia per problemi sottili che altrimenti passerebbero inosservati.
WhoopsProvider registra Whoops come exception handler. In sviluppo, Whoops intercetta le eccezioni non gestite e mostra una pagina di debug interattiva con stack trace, variabili d'ambiente, e il codice sorgente attorno alla riga dell'errore. In produzione, Whoops viene disattivato e le eccezioni passano all'ExceptionHandler che mostra pagine di errore pulite senza dettagli tecnici.
Questi due layer coesistono: NativeErrorProvider per i warning e gli errori PHP che non lanciano eccezioni, Whoops/ExceptionHandler per le eccezioni vere e proprie. Il risultato e che nessun errore passa inosservato — tutto viene loggato o mostrato, a seconda dell'ambiente.
Error pages personalizzate
Le pagine di errore vivono in /resources/error/ e sono template PHP puri. Response::setErrorHandle() cerca un template corrispondente al codice HTTP (ad esempio 404.php, 500.php) e lo renderizza. Se non esiste un template specifico, viene usato un fallback generico.
Separare le pagine di errore dal codice di gestione permette di personalizzare l'aspetto degli errori senza toccare la logica dell'ExceptionHandler. Vuoi una pagina 404 con un'illustrazione? Modifica il template. Vuoi mostrare informazioni diverse in base alla route? Il template ha accesso al messaggio e al codice dell'errore.