Benvenuto al Laravel Cafè! Ogni settimana proporremo un nuovo argomento sul mondo Laravel, quindi trova un posto libero, prendi un caffè e condividi le tue opinioni con la comunità! L’idea è di creare un punto di discussione. Se hai qualche perplessità sull’argomento trattato, leggi fino alla fine e fai una domanda usando il forum! Cercheremo di risponderci a vicenda e di aiutarci, ed il confronto ci farà crescere tutti un po’ di più.
Di mercoledì in mercoledì parleremo di qualcosa di diverso, quindi tornaci a trovare! Potresti dare una mano a qualcuno in difficoltà, o ricevere tu un aiuto in caso di problemi! Dai, siediti, il primo caffè lo offriamo noi.
L’Argomento
Quante volte abbiamo letto sul nostro monitor “Whoops, looks like something went wrong”, magari lanciando qualche imprecazione, o magari sapendo già il motivo di quell’errore e correggendo al volo il codice? Le eccezioni fanno parte della struttura di un software da sempre e, come tutte le altre “parti” del nostro software, vanno gestite a dovere. Come avrete intuito oggi dedicheremo la nostra pausa caffè alle Exception di Laravel, a come gestirle e come customizzarle.
Il Codice
A differenza degli ultimi articoli, durante questo caffè ci soffermeremo più sulla teoria che sulla pratica, dato che la personalizzazione delle exception può variare a seconda delle esigenze, o può semplicemente rimanere invariata. Come prima cosa vediamo nel dettaglio il workflow della gestione delle exception di Laravel. La documentazione ufficiale come al solito risulta un po’ scarna sotto questo aspetto dato che spiega nel dettaglio gli strumenti, ma il workflow dell’Exception Handler rimane tutto racchiuso nel suo codice sorgente (anche questo molto intuitivo, ma molto meno commentato rispetto ad altre classi). Mi focalizzerò esclusivamente sul render di una exception, ma in realtà il framework gestisce anche il richiamo del logger della stessa.
Come ho anticipato, i sorgenti sono abbastanza intuitivi quindi vi consiglio di sbirciare i link ai sorgenti di Github che ho incluso nei vari punti.
Viene lanciata una exception e attraverso la classe IlluminateFoundationBootstrapHandleExceptions
viene richiamato il metodo handleException($e)
. Come si può vedere il framework gestisce in maniera differente se è stato lanciato un comando da console o si è verificato un errore in una qualche request, in questo articolo ci focalizzeremo comunque solo sul secondo caso:
/**
* Handle an uncaught exception from the application.
*
* Note: Most exceptions can be handled via the try / catch block in
* the HTTP and Console kernels. But, fatal error exceptions must
* be handled differently since they are not normal exceptions.
*
* @param Throwable $e
* @return void
*/
public function handleException($e)
{
if (! $e instanceof Exception) {
$e = new FatalThrowableError($e);
}
// Il metodo report controlla se l'exception va loggata o meno
$this->getExceptionHandler()->report($e);
if ($this->app->runningInConsole()) {
// Render dell'exception da linea di comando
$this->renderForConsole($e);
} else {
// Render dell'exception come view
$this->renderHttpResponse($e);
}
}
// Secondo step
/**
* Render an exception as an HTTP response and send it.
*
* @param Exception $e
* @return void
*/
protected function renderHttpResponse(Exception $e)
{
$this->getExceptionHandler()->render($this->app['request'], $e)->send();
}
Il metodo renderHttpResponse
richiama la classe AppExceptionsHandler
che eredita il metodo render
dalla classe IlluminateFoundationExceptionsHandler
e, nel caso in cui l’utente non modifichi nulla, lo richiama. Ricordiamoci che da questo momento in poi la gestione dell’exception viene standardizzata dal framework, e questo comporta una certa “rigidità” del flusso dal momento in cui il metodo `parent::render($request, $exception)’ viene richiamato, al momento in cui riceviamo il response finale. Al tempo stesso, viene lasciata al developer la più totale libertà di modificarlo, dato che al momento l’unica cosa che ha fatto il framework è intercettare una exception e richiamare la classe addetta alla sua gestione.
Vediamo quindi come si comporta il framework da questo momento in poi
/**
* Render an exception into an HTTP response.
*
* @param IlluminateHttpRequest $request
* @param Exception $exception
* @return IlluminateHttpResponse
*/
public function render($request, Exception $exception)
{
/*
* Qui si possono aggiungere le istruzioni custom per la gestione delle exception
*/
// Caso base
return parent::render($request, $exception);
}
Richiamato il metodo parent::render($request, $exception)
, il framework inizia a preparare un normale response (come avviene per qualsiasi altro controller dell’applicazione) che contiene sia i dati della request che l’exception lanciata. Prima di arrivare al response finale però effettua alcuni controlli sul tipo di exception lanciata, convertendola dove necessario per facilitarne la gestione.
Per farvi un esempio, il metodo $this->validate($request, $rules)
nel caso in cui riscontra la violazione di una regola, lancia una ValidationException
che segue lo stesso iter delle normali exception, ma il response finale è sarà un redirect:
// Validation exception
redirect()->back()->withInput($request->input())->withErrors($errors)
Nel caso in cui l’exception non rientri in uno dei casi particolari, allora verrà si procederà con la creazione del response:
/**
* Render an exception into a response.
*
* @param IlluminateHttpRequest $request
* @param Exception $e
* @return SymfonyComponentHttpFoundationResponse
*/
public function render($request, Exception $e)
{
$e = $this->prepareException($e);
if ($e instanceof HttpResponseException) {
return $e->getResponse();
} elseif ($e instanceof AuthenticationException) {
return $this->unauthenticated($request, $e);
} elseif ($e instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($e, $request);
}
// Caso base
return $this->prepareResponse($request, $e);
}
/**
* Prepare response containing exception render.
*
* @param IlluminateHttpRequest $request
* @param Exception $e
* @return SymfonyComponentHttpFoundationResponse
*/
protected function prepareResponse($request, Exception $e)
{
if ($this->isHttpException($e)) {
return $this->toIlluminateResponse($this->renderHttpException($e), $e);
} else {
return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
}
}
Si passa quindi al render vero e proprio, che rispecchia la “rigidità” precedentemente citata e che, a mio avviso, può essere il motivo principale di modifica e personalizzazione della classe AppExceptionHandler
. Nel caso in cui l’exception sia un’istanza della classe SymfonyComponentHttpKernelExceptionHttpException
o una sua estensione, verrà controllato lo statusCode
(4xx, 5xx) e ricercata la corrispondente view nella cartella resources/views/errors/
, altrimenti l’ExceptionHandler ritornerà l’html con lo stacktrace dell’eccezione. Mi soffermo solo sui metodo getHtml($exception)
e decorate($content, $css)
, richiamati nel caso in cui l’exception non sia un’istanza/estensione della classe SymfonyComponentHttpKernelExceptionHTTPException
, poiché è il responsabile della generazione della pagina che tutti noi conosciamo bene, e che potrebbe essere spunto per una propria pagina custom.
/**
* Gets the full HTML content associated with the given exception.
*
* @param Exception|FlattenException $exception An Exception or FlattenException instance
*
* @return string The HTML content as a string
*/
public function getHtml($exception)
{
if (!$exception instanceof FlattenException) {
$exception = FlattenException::create($exception);
}
return $this->decorate($this->getContent($exception), $this->getStylesheet($exception));
}
private function decorate($content, $css)
{
return <<<eof <!doctype="" html="">
<meta charset=""{$this-">charset}" />
<meta name=""robots"" content=""noindex,nofollow""></eof>
<style>
/* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html */<br />
html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}</p>
<p> html { background: #eee; padding: 10px }<br />
img { border: 0; }<br />
#sf-resetcontent { width:970px; margin:0 auto; }<br />
$css<br />
</style>
$content
EOF;
}
Per quanto questo workflow sia completo in ogni sua forma, possono nascere esigenze diverse per eccezioni particolari per le quali preferiamo che l’applicazione abbia un comportamento diverso rispetto allo standard. Vorrei ricordare inoltre che i metodi del “dietro le quinte”, ovvero della classe IlluminateFoundationExceptionsHandler
, possono essere a loro volta utilizzati, evitando quindi codice ridondante. Vi faccio un piccolo esempio:
/**
* Render an exception into an HTTP response.
*
* @param IlluminateHttpRequest $request
* @param Exception $exception
* @return IlluminateHttpResponse
*/
public function render($request, Exception $exception)
{
if ($exception instanceof AppExceptionsMyCustomException && $request->ajax()) {
// Ritorno l'eccezione in formato JSON e log
Log::error($exception->getMessage());
return response()->json(['whoops' => $exception->getMessage()], $exception->getStatusCode());
} else if($exception instanceof AppExceptionsMyCustomException && env('APP_ENV', 'local') == 'production'){
// Ritorno una view apposita
return response()->view('errors.custom_error.blade.php', ['whoops' => $exception->getMessage()]);
} else if($exception instanceof AppExceptionsMyCustomException) {
// Ritorno la view di debug
return $this->convertExceptionToResponse($exception);
}
return parent::render($request, $exception);
}
Da qui in poi dipende tutto dalle necessità che ciascun developer ha per la propria applicazione. Il consiglio che do è comunque di sfruttare il più possibile l’handler soprattutto per evitare spiacevoli schermate bianche in produzione o per crearsi un piccolo “monitor” che notifichi in tempo reale uno sviluppatore di quanto è accaduto (quindi dati della request, eccezione lanciata ecc…) senza dover scartabellare nella cartella dei log in cerca di risposta.
… ed ora?
Come gestite gli errori e i log nelle vostre applicazioni?
Se volete avere maggiori informazioni riguardo errori ed gestione degli stessi, piuttosto che gestire viste customizzate o response in JSON per, ad esempio, le rotte API, vi invito a continuare la discussione in tutti i canali di Laravel Italia così da riuscire rendere più completo ed esaustivo l’articolo di oggi.
Ah, e ci trovate anche sul nostro Slack!