Skip to content

Commit

Permalink
Merge pull request #1770 from akrabat/php7-errors
Browse files Browse the repository at this point in the history
Support PHP7+ Errors
  • Loading branch information
silentworks committed Feb 19, 2016
2 parents 3e35f73 + 45924e2 commit 2597d9e
Show file tree
Hide file tree
Showing 3 changed files with 355 additions and 0 deletions.
30 changes: 30 additions & 0 deletions Slim/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Slim;

use Exception;
use Throwable;
use Closure;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
Expand Down Expand Up @@ -41,6 +42,7 @@
* @property-read ResponseInterface $response
* @property-read RouterInterface $router
* @property-read callable $errorHandler
* @property-read callable $phpErrorHandler
* @property-read callable $notFoundHandler function($request, $response)
* @property-read callable $notAllowedHandler function($request, $response, $allowedHttpMethods)
*/
Expand Down Expand Up @@ -333,6 +335,8 @@ public function process(ServerRequestInterface $request, ResponseInterface $resp
$response = $this->callMiddlewareStack($request, $response);
} catch (Exception $e) {
$response = $this->handleException($e, $request, $response);
} catch (Throwable $e) {
$response = $this->handlePhpError($e, $request, $response);
}

$response = $this->finalize($response);
Expand Down Expand Up @@ -600,4 +604,30 @@ protected function handleException(Exception $e, ServerRequestInterface $request
// No handlers found, so just throw the exception
throw $e;
}

/**
* Call relevant handler from the Container if needed. If it doesn't exist,
* then just re-throw.
*
* @param Throwable $e
* @param ServerRequestInterface $request
* @param ResponseInterface $response
*
* @return ResponseInterface
* @throws Exception if a handler is needed and not found
*/
protected function handlePhpError(Throwable $e, ServerRequestInterface $request, ResponseInterface $response)
{
$handler = 'phpErrorHandler';
$params = [$request, $response, $e];

if ($this->container->has($handler)) {
$callable = $this->container->get($handler);
// Call the registered handler
return call_user_func_array($callable, $params);
}

// No handlers found, so just throw the exception
throw $e;
}
}
22 changes: 22 additions & 0 deletions Slim/DefaultServicesProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Exception\ContainerValueNotFoundException;
use Slim\Handlers\PhpError;
use Slim\Handlers\Error;
use Slim\Handlers\NotFound;
use Slim\Handlers\NotAllowed;
Expand Down Expand Up @@ -101,6 +102,27 @@ public function register($container)
};
}

if (!isset($container['phpErrorHandler'])) {
/**
* This service MUST return a callable
* that accepts three arguments:
*
* 1. Instance of \Psr\Http\Message\ServerRequestInterface
* 2. Instance of \Psr\Http\Message\ResponseInterface
* 3. Instance of \Error
*
* The callable MUST return an instance of
* \Psr\Http\Message\ResponseInterface.
*
* @param Container $container
*
* @return callable
*/
$container['phpErrorHandler'] = function ($container) {
return new PhpError($container->get('settings')['displayErrorDetails']);
};
}

if (!isset($container['errorHandler'])) {
/**
* This service MUST return a callable
Expand Down
303 changes: 303 additions & 0 deletions Slim/Handlers/PhpError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
<?php
/**
* Slim Framework (http://slimframework.com)
*
* @link https://github.com/slimphp/Slim
* @copyright Copyright (c) 2011-2016 Josh Lockhart
* @license https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License)
*/
namespace Slim\Handlers;

use Throwable;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Http\Body;

/**
* Default Slim application error handler for PHP 7+ Throwables
*
* It outputs the error message and diagnostic information in either JSON, XML,
* or HTML based on the Accept header.
*/
class PhpError
{
protected $displayErrorDetails;

/**
* Known handled content types
*
* @var array
*/
protected $knownContentTypes = [
'application/json',
'application/xml',
'text/xml',
'text/html',
];

/**
* Constructor
*
* @param boolean $displayErrorDetails Set to true to display full details
*/
public function __construct($displayErrorDetails = false)
{
$this->displayErrorDetails = (bool)$displayErrorDetails;
}

/**
* Invoke error handler
*
* @param ServerRequestInterface $request The most recent Request object
* @param ResponseInterface $response The most recent Response object
* @param Throwable $error The caught Throwable object
*
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, Throwable $error)
{
$contentType = $this->determineContentType($request);
switch ($contentType) {
case 'application/json':
$output = $this->renderJsonErrorMessage($error);
break;

case 'text/xml':
case 'application/xml':
$output = $this->renderXmlErrorMessage($error);
break;

case 'text/html':
$output = $this->renderHtmlErrorMessage($error);
break;
}

$this->writeToErrorLog($error);

$body = new Body(fopen('php://temp', 'r+'));
$body->write($output);

return $response
->withStatus(500)
->withHeader('Content-type', $contentType)
->withBody($body);
}


/**
* Write to the error log if displayErrorDetails is false
*
* @param Throwable $error
*
* @return void
*/
protected function writeToErrorLog($error)
{
if ($this->displayErrorDetails) {
return;
}

$message = 'Slim Application Error:' . PHP_EOL;
$message .= $this->renderTextError($error);
while ($error = $error->getPrevious()) {
$message .= PHP_EOL . 'Previous error:' . PHP_EOL;
$message .= $this->renderTextError($error);
}

$message .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL;

error_log($message);
}

/**
* Render error as Text.
*
* @param Throwable $error
*
* @return string
*/
protected function renderTextError(Throwable $error)
{
$text = sprintf('Type: %s' . PHP_EOL, get_class($error));

if (($code = $error->getCode())) {
$text .= sprintf('Code: %s' . PHP_EOL, $code);
}

if (($message = $error->getMessage())) {
$text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message));
}

if (($file = $error->getFile())) {
$text .= sprintf('File: %s' . PHP_EOL, $file);
}

if (($line = $error->getLine())) {
$text .= sprintf('Line: %s' . PHP_EOL, $line);
}

if (($trace = $error->getTraceAsString())) {
$text .= sprintf('Trace: %s', $trace);
}

return $text;
}

/**
* Render HTML error page
*
* @param Throwable $error
*
* @return string
*/
protected function renderHtmlErrorMessage(Throwable $error)
{
$title = 'Slim Application Error';

if ($this->displayErrorDetails) {
$html = '<p>The application could not run because of the following error:</p>';
$html .= '<h2>Details</h2>';
$html .= $this->renderHtmlError($error);

while ($error = $error->getPrevious()) {
$html .= '<h2>Previous error</h2>';
$html .= $this->renderHtmlError($error);
}
} else {
$html = '<p>A website error has occurred. Sorry for the temporary inconvenience.</p>';
}

$output = sprintf(
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'>" .
"<title>%s</title><style>body{margin:0;padding:30px;font:12px/1.5 Helvetica,Arial,Verdana," .
"sans-serif;}h1{margin:0;font-size:48px;font-weight:normal;line-height:48px;}strong{" .
"display:inline-block;width:65px;}</style></head><body><h1>%s</h1>%s</body></html>",
$title,
$title,
$html
);

return $output;
}

/**
* Render error as HTML.
*
* @param Throwable $error
*
* @return string
*/
protected function renderHtmlError(Throwable $error)
{
$html = sprintf('<div><strong>Type:</strong> %s</div>', get_class($error));

if (($code = $error->getCode())) {
$html .= sprintf('<div><strong>Code:</strong> %s</div>', $code);
}

if (($message = $error->getMessage())) {
$html .= sprintf('<div><strong>Message:</strong> %s</div>', htmlentities($message));
}

if (($file = $error->getFile())) {
$html .= sprintf('<div><strong>File:</strong> %s</div>', $file);
}

if (($line = $error->getLine())) {
$html .= sprintf('<div><strong>Line:</strong> %s</div>', $line);
}

if (($trace = $error->getTraceAsString())) {
$html .= '<h2>Trace</h2>';
$html .= sprintf('<pre>%s</pre>', htmlentities($trace));
}

return $html;
}

/**
* Render JSON error
*
* @param Throwable $error
*
* @return string
*/
protected function renderJsonErrorMessage(Throwable $error)
{
$error = [
'message' => 'Slim Application Error',
];

if ($this->displayErrorDetails) {
$error['error'] = [];

do {
$error['error'][] = [
'type' => get_class($error),
'code' => $error->getCode(),
'message' => $error->getMessage(),
'file' => $error->getFile(),
'line' => $error->getLine(),
'trace' => explode("\n", $error->getTraceAsString()),
];
} while ($error = $error->getPrevious());
}

return json_encode($error, JSON_PRETTY_PRINT);
}

/**
* Render XML error
*
* @param Throwable $error
* @return string
*/
protected function renderXmlErrorMessage(Throwable $error)
{
$xml = "<error>\n <message>Slim Application Error</message>\n";
if ($this->displayErrorDetails) {
do {
$xml .= " <error>\n";
$xml .= " <type>" . get_class($error) . "</type>\n";
$xml .= " <code>" . $error->getCode() . "</code>\n";
$xml .= " <message>" . $this->createCdataSection($error->getMessage()) . "</message>\n";
$xml .= " <file>" . $error->getFile() . "</file>\n";
$xml .= " <line>" . $error->getLine() . "</line>\n";
$xml .= " <trace>" . $this->createCdataSection($error->getTraceAsString()) . "</trace>\n";
$xml .= " </error>\n";
} while ($error = $error->getPrevious());
}
$xml .= "</error>";

return $xml;
}

/**
* Returns a CDATA section with the given content.
*
* @param string $content
* @return string
*/
private function createCdataSection($content)
{
return sprintf('<![CDATA[%s]]>', str_replace(']]>', ']]]]><![CDATA[>', $content));
}

/**
* Determine which content type we know about is wanted using Accept header
*
* @param ServerRequestInterface $request
* @return string
*/
private function determineContentType(ServerRequestInterface $request)
{
$acceptHeader = $request->getHeaderLine('Accept');
$selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes);

if (count($selectedContentTypes)) {
return $selectedContentTypes[0];
}

return 'text/html';
}
}

0 comments on commit 2597d9e

Please sign in to comment.