* @author Nicolas Grekas
*/
class ExceptionHandler
{
private $debug;
private $charset;
private $handler;
private $caughtBuffer;
private $caughtLength;
private $fileLinkFormat;
public function __construct($debug = true, $charset = null, $fileLinkFormat = null)
{
$this->debug = $debug;
$this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8';
$this->fileLinkFormat = $fileLinkFormat;
}
/**
* Registers the exception handler.
*
* @param bool $debug Enable/disable debug mode, where the stack trace is displayed
* @param string|null $charset The charset used by exception messages
* @param string|null $fileLinkFormat The IDE link template
*
* @return static
*/
public static function register($debug = true, $charset = null, $fileLinkFormat = null)
{
$handler = new static($debug, $charset, $fileLinkFormat);
$prev = set_exception_handler([$handler, 'handle']);
if (\is_array($prev) && $prev[0] instanceof ErrorHandler) {
restore_exception_handler();
$prev[0]->setExceptionHandler([$handler, 'handle']);
}
return $handler;
}
/**
* Sets a user exception handler.
*
* @param callable $handler An handler that will be called on Exception
*
* @return callable|null The previous exception handler if any
*/
public function setHandler(callable $handler = null)
{
$old = $this->handler;
$this->handler = $handler;
return $old;
}
/**
* Sets the format for links to source files.
*
* @param string|FileLinkFormatter $fileLinkFormat The format for links to source files
*
* @return string The previous file link format
*/
public function setFileLinkFormat($fileLinkFormat)
{
$old = $this->fileLinkFormat;
$this->fileLinkFormat = $fileLinkFormat;
return $old;
}
/**
* Sends a response for the given Exception.
*
* To be as fail-safe as possible, the exception is first handled
* by our simple exception handler, then by the user exception handler.
* The latter takes precedence and any output from the former is cancelled,
* if and only if nothing bad happens in this handling path.
*/
public function handle(\Exception $exception)
{
if (null === $this->handler || $exception instanceof OutOfMemoryException) {
$this->sendPhpResponse($exception);
return;
}
$caughtLength = $this->caughtLength = 0;
ob_start(function ($buffer) {
$this->caughtBuffer = $buffer;
return '';
});
$this->sendPhpResponse($exception);
while (null === $this->caughtBuffer && ob_end_flush()) {
// Empty loop, everything is in the condition
}
if (isset($this->caughtBuffer[0])) {
ob_start(function ($buffer) {
if ($this->caughtLength) {
// use substr_replace() instead of substr() for mbstring overloading resistance
$cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength);
if (isset($cleanBuffer[0])) {
$buffer = $cleanBuffer;
}
}
return $buffer;
});
echo $this->caughtBuffer;
$caughtLength = ob_get_length();
}
$this->caughtBuffer = null;
try {
\call_user_func($this->handler, $exception);
$this->caughtLength = $caughtLength;
} catch (\Exception $e) {
if (!$caughtLength) {
// All handlers failed. Let PHP handle that now.
throw $exception;
}
}
}
/**
* Sends the error associated with the given Exception as a plain PHP response.
*
* This method uses plain PHP functions like header() and echo to output
* the response.
*
* @param \Exception|FlattenException $exception An \Exception or FlattenException instance
*/
public function sendPhpResponse($exception)
{
if (!$exception instanceof FlattenException) {
$exception = FlattenException::create($exception);
}
if (!headers_sent()) {
header(sprintf('HTTP/1.0 %s', $exception->getStatusCode()));
foreach ($exception->getHeaders() as $name => $value) {
header($name.': '.$value, false);
}
header('Content-Type: text/html; charset='.$this->charset);
}
echo $this->decorate($this->getContent($exception), $this->getStylesheet($exception));
}
/**
* 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));
}
/**
* Gets the HTML content associated with the given exception.
*
* @return string The content as a string
*/
public function getContent(FlattenException $exception)
{
switch ($exception->getStatusCode()) {
case 404:
$title = 'Sorry, the page you are looking for could not be found.';
break;
default:
$title = 'Whoops, looks like something went wrong.';
}
if (!$this->debug) {
return <<
$title