diff --git a/.travis.yml b/.travis.yml index 2ee616260..9f5de8386 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,3 +24,5 @@ after_script: notifications: slack: slimphp:0RNzx2JuhkAqIf0MXcUZ0asT + +dist: trusty diff --git a/Slim/App.php b/Slim/App.php index 187f9f990..147a9d2ed 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -8,22 +8,21 @@ */ namespace Slim; -use Exception; use FastRoute\Dispatcher; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Slim\Exception\InvalidMethodException; -use Slim\Exception\MethodNotAllowedException; -use Slim\Exception\NotFoundException; -use Slim\Handlers\Error; -use Slim\Handlers\NotAllowed; -use Slim\Handlers\NotFound; -use Slim\Handlers\PhpError; +use Slim\Exception\HttpException; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpNotAllowedException; +use Slim\Handlers\ErrorHandler; use Slim\Interfaces\CallableResolverInterface; +use Slim\Interfaces\ErrorHandlerInterface; use Slim\Interfaces\RouteGroupInterface; use Slim\Interfaces\RouteInterface; use Slim\Interfaces\RouterInterface; +use BadMethodCallException; +use Exception; use Throwable; /** @@ -54,26 +53,6 @@ class App */ protected $router; - /** - * @var callable - */ - protected $notFoundHandler; - - /** - * @var callable - */ - protected $notAllowedHandler; - - /** - * @var callable - */ - protected $errorHandler; - - /** - * @var callable - */ - protected $phpErrorHandler; - /** * @var array */ @@ -84,6 +63,8 @@ class App 'displayErrorDetails' => false, 'addContentLengthHeader' => true, 'routerCacheFile' => false, + 'defaultErrorHandler' => null, + 'errorHandlers' => [] ]; /******************************************************************************** @@ -97,6 +78,7 @@ class App */ public function __construct(array $settings = []) { + $this->addSettings($settings); $this->container = new Container(); } @@ -156,7 +138,7 @@ public function __call($method, $args) } } - throw new \BadMethodCallException("Method $method is not a valid method"); + throw new BadMethodCallException("Method $method is not a valid method"); } /******************************************************************************** @@ -278,144 +260,181 @@ public function getRouter() } /** - * Set callable to handle scenarios where a suitable - * route does not match the current request. + * Set callable to handle scenarios where an error + * occurs when processing the current request. * * This service MUST return a callable that accepts - * two arguments: + * three arguments optionally four arguments. * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface + * 3. Instance of \Exception + * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * - * @param callable $handler + * @param string $type + * @param callable $callable + * + * @throws \RuntimeException */ - public function setNotFoundHandler(callable $handler) + public function setErrorHandler($type, $callable) { - $this->notFoundHandler = $handler; + $resolver = $this->getCallableResolver(); + $handler = $resolver->resolve($callable); + $handlers = $this->getErrorHandlers(); + $handlers[$type] = $handler; + $this->addSetting('errorHandlers', $handlers); } /** - * Get callable to handle scenarios where a suitable - * route does not match the current request. + * Get callable to handle scenarios where an error + * occurs when processing the current request. + * + * @param string $type + * @return callable * - * @return callable|Error + * @throws \RuntimeException */ - public function getNotFoundHandler() + public function getErrorHandler($type) { - if (!$this->notFoundHandler) { - $this->notFoundHandler = new NotFound(); + $handlers = $this->getErrorHandlers(); + + if (isset($handlers[$type])) { + $handler = $handlers[$type]; + $resolver = $this->getCallableResolver(); + return $resolver->resolve($handler); } - return $this->notFoundHandler; + return $this->getDefaultErrorHandler(); } /** - * Set callable to handle scenarios where a suitable - * route matches the request URI but not the request method. + * Retrieve error handler array from settings + * + * @returns array + * + * @throws \RuntimeException + */ + protected function getErrorHandlers() + { + $handlers = $this->getSetting('errorHandlers', []); + + if (!is_array($handlers)) { + throw new \RuntimeException('Slim application setting "errorHandlers" should be an array.'); + } + + return $handlers; + } + + + /** + * Set callable as the default Slim application error handler. * * This service MUST return a callable that accepts - * three arguments: + * three arguments optionally four arguments. * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Array of allowed HTTP methods + * 3. Instance of \Exception + * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * - * @param callable $handler + * @param callable $callable + * + * @throws \RuntimeException */ - public function setNotAllowedHandler(callable $handler) + public function setDefaultErrorHandler($callable) { - $this->notAllowedHandler = $handler; + $resolver = $this->getCallableResolver(); + $handler = $resolver->resolve($callable); + $this->addSetting('defaultErrorHandler', $handler); } /** - * Get callable to handle scenarios where a suitable - * route matches the request URI but not the request method. + * Get the default error handler from settings. * - * @return callable|Error + * @return callable|ErrorHandler */ - public function getNotAllowedHandler() + public function getDefaultErrorHandler() { - if (!$this->notAllowedHandler) { - $this->notAllowedHandler = new NotAllowed(); + $handler = $this->getSetting('defaultErrorHandler', null); + + if (!is_null($handler)) { + $resolver = $this->getCallableResolver(); + return $resolver->resolve($handler); } - return $this->notAllowedHandler; + return new ErrorHandler(); } /** - * Set callable to handle scenarios where an error - * occurs when processing the current request. + * Set callable to handle scenarios where a suitable + * route does not match the current request. * - * This service MUST return a callable that accepts three arguments: + * This service MUST return a callable that accepts + * three arguments optionally four arguments. * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of \Error + * 3. Instance of \Exception + * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * * @param callable $handler */ - public function setErrorHandler(callable $handler) + public function setNotFoundHandler($handler) { - $this->errorHandler = $handler; + $this->setErrorHandler(HttpNotFoundException::class, $handler); } /** - * Get callable to handle scenarios where an error - * occurs when processing the current request. + * Get callable to handle scenarios where a suitable + * route does not match the current request. * - * @return callable|Error + * @return callable|ErrorHandlerInterface */ - public function getErrorHandler() + public function getNotFoundHandler() { - if (!$this->errorHandler) { - $this->errorHandler = new Error($this->getSetting('displayErrorDetails')); - } - - return $this->errorHandler; + return $this->getErrorHandler(HttpNotFoundException::class); } /** - * Set callable to handle scenarios where a PHP error - * occurs when processing the current request. + * Set callable to handle scenarios where a suitable + * route matches the request URI but not the request method. * - * This service MUST return a callable that accepts three arguments: + * This service MUST return a callable that accepts + * three arguments optionally four arguments. * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of \Error + * 3. Instance of \Exception + * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * - * @param callable $handler + * @param string|callable $handler */ - public function setPhpErrorHandler(callable $handler) + public function setNotAllowedHandler($handler) { - $this->phpErrorHandler = $handler; + $this->setErrorHandler(HttpNotAllowedException::class, $handler); } /** - * Get callable to handle scenarios where a PHP error - * occurs when processing the current request. + * Get callable to handle scenarios where a suitable + * route matches the request URI but not the request method. * - * @return callable|Error + * @return callable */ - public function getPhpErrorHandler() + public function getNotAllowedHandler() { - if (!$this->phpErrorHandler) { - $this->phpErrorHandler = new PhpError($this->getSetting('displayErrorDetails')); - } - - return $this->phpErrorHandler; + return $this->getErrorHandler(HttpNotAllowedException::class); } /******************************************************************************** @@ -575,17 +594,18 @@ public function group($pattern, $callable) * @return ResponseInterface * * @throws Exception - * @throws MethodNotAllowedException - * @throws NotFoundException */ public function run($silent = false) { $response = $this->container->get('response'); + $request = $this->container->get('request'); try { - $response = $this->process($this->container->get('request'), $response); - } catch (InvalidMethodException $e) { - $response = $this->processInvalidMethod($e->getRequest(), $response); + $response = $this->process($request, $response); + } catch (Exception $e) { + $response = $this->handleException($e, $request, $response); + } catch (Throwable $e) { + $response = $this->handleException($e, $request, $response); } if (!$silent) { @@ -595,35 +615,6 @@ public function run($silent = false) return $response; } - /** - * Pull route info for a request with a bad method to decide whether to - * return a not-found error (default) or a bad-method error, then run - * the handler for that error, returning the resulting response. - * - * Used for cases where an incoming request has an unrecognized method, - * rather than throwing an exception and not catching it all the way up. - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @return ResponseInterface - */ - protected function processInvalidMethod(ServerRequestInterface $request, ResponseInterface $response) - { - $router = $this->getRouter(); - $request = $this->dispatchRouterAndPrepareRoute($request, $router); - $routeInfo = $request->getAttribute('routeInfo', [RouterInterface::DISPATCH_STATUS => Dispatcher::NOT_FOUND]); - - if ($routeInfo[RouterInterface::DISPATCH_STATUS] === Dispatcher::METHOD_NOT_ALLOWED) { - return $this->handleException( - new MethodNotAllowedException($request, $response, $routeInfo[RouterInterface::ALLOWED_METHODS]), - $request, - $response - ); - } - - return $this->handleException(new NotFoundException($request, $response), $request, $response); - } - /** * Process a request * @@ -635,8 +626,6 @@ protected function processInvalidMethod(ServerRequestInterface $request, Respons * @return ResponseInterface * * @throws Exception - * @throws MethodNotAllowedException - * @throws NotFoundException */ public function process(ServerRequestInterface $request, ResponseInterface $response) { @@ -649,14 +638,7 @@ public function process(ServerRequestInterface $request, ResponseInterface $resp } // Traverse middleware stack - try { - $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->callMiddlewareStack($request, $response); $response = $this->finalize($response); return $response; @@ -750,16 +732,25 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res $routeInfo = $request->getAttribute('routeInfo'); } - if ($routeInfo[0] === Dispatcher::FOUND) { - $route = $router->lookupRoute($routeInfo[1]); - return $route->run($request, $response); - } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { - $notAllowedHandler = $this->getNotAllowedHandler(); - return $notAllowedHandler($request, $response, $routeInfo[1]); + $exception = null; + switch ($routeInfo[0]) { + case Dispatcher::FOUND: + $route = $router->lookupRoute($routeInfo[1]); + return $route->run($request, $response); + + case Dispatcher::METHOD_NOT_ALLOWED: + $exception = new HttpNotAllowedException; + $exception->setAllowedMethods($routeInfo[1]); + $exception->setRequest($request); + break; + + case Dispatcher::NOT_FOUND: + $exception = new HttpNotFoundException; + $exception->setRequest($request); + break; } - $notFoundHandler = $this->getNotFoundHandler(); - return $notFoundHandler($request, $response); + return $this->handleException($exception, $request, $response); } /** @@ -791,13 +782,59 @@ protected function dispatchRouterAndPrepareRoute(ServerRequestInterface $request return $request->withAttribute('routeInfo', $routeInfo); } + /** + * Resolve custom error handler from container or use default ErrorHandler + * @param Exception|Throwable $exception + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return mixed + */ + public function handleException($exception, ServerRequestInterface $request, ResponseInterface $response) + { + $exceptionType = get_class($exception); + $handler = $this->getErrorHandler($exceptionType); + $displayErrorDetails = $this->getSetting('displayErrorDetails'); + + /** + * Retrieve request object from exception + * and replace current request object if not null + */ + if (method_exists($exception, 'getRequest')) { + $r = $exception->getRequest(); + if (!is_null($r)) { + $request = $r; + } + } + + /** + * Check if exception is instance of HttpException + * If so, check if it is not recoverable + * End request immediately in case it is not recoverable + */ + $recoverable = true; + if ($exception instanceof HttpException) { + $recoverable = $exception->isRecoverable(); + } + + $params = [ + $request, + $response, + $exception, + $displayErrorDetails + ]; + + return $recoverable + ? call_user_func_array($handler, $params) + : $response; + } + /** * Finalize response * * @param ResponseInterface $response * @return ResponseInterface * - * @throws RuntimeException + * @throws \RuntimeException */ protected function finalize(ResponseInterface $response) { @@ -840,50 +877,4 @@ protected function isEmptyResponse(ResponseInterface $response) return in_array($response->getStatusCode(), [204, 205, 304]); } - - /** - * Call relevant handler from the Container if needed. If it doesn't exist, - * then just re-throw. - * - * @param Exception $e - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * - * @return ResponseInterface - * @throws Exception if a handler is needed and not found - */ - protected function handleException(Exception $e, ServerRequestInterface $request, ResponseInterface $response) - { - if ($e instanceof MethodNotAllowedException) { - $handler = $this->getNotAllowedHandler(); - $params = [$e->getRequest(), $e->getResponse(), $e->getAllowedMethods()]; - } elseif ($e instanceof NotFoundException) { - $handler = $this->getNotFoundHandler(); - $params = [$e->getRequest(), $e->getResponse()]; - } else { - // Other exception, use $request and $response params - $handler = $this->getErrorHandler(); - $params = [$request, $response, $e]; - } - - return call_user_func_array($handler, $params); - } - - /** - * 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 Throwable - */ - protected function handlePhpError(Throwable $e, ServerRequestInterface $request, ResponseInterface $response) - { - $handler = $this->getPhpErrorHandler(); - $params = [$request, $response, $e]; - - return call_user_func_array($handler, $params); - } } diff --git a/Slim/DefaultServicesProvider.php b/Slim/DefaultServicesProvider.php index 36475de87..f973cf613 100644 --- a/Slim/DefaultServicesProvider.php +++ b/Slim/DefaultServicesProvider.php @@ -104,85 +104,6 @@ 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 - * that accepts three arguments: - * - * 1. Instance of \Psr\Http\Message\ServerRequestInterface - * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of \Exception - * - * The callable MUST return an instance of - * \Psr\Http\Message\ResponseInterface. - * - * @param Container $container - * - * @return callable - */ - $container['errorHandler'] = function ($container) { - return new Error($container->get('settings')['displayErrorDetails']); - }; - } - - if (!isset($container['notFoundHandler'])) { - /** - * This service MUST return a callable - * that accepts two arguments: - * - * 1. Instance of \Psr\Http\Message\ServerRequestInterface - * 2. Instance of \Psr\Http\Message\ResponseInterface - * - * The callable MUST return an instance of - * \Psr\Http\Message\ResponseInterface. - * - * @return callable - */ - $container['notFoundHandler'] = function () { - return new NotFound; - }; - } - - if (!isset($container['notAllowedHandler'])) { - /** - * 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. Array of allowed HTTP methods - * - * The callable MUST return an instance of - * \Psr\Http\Message\ResponseInterface. - * - * @return callable - */ - $container['notAllowedHandler'] = function () { - return new NotAllowed; - }; - } - if (!isset($container['callableResolver'])) { /** * Instance of \Slim\Interfaces\CallableResolverInterface diff --git a/Slim/Exception/HttpBadGatewayException.php b/Slim/Exception/HttpBadGatewayException.php new file mode 100644 index 000000000..30563c2c3 --- /dev/null +++ b/Slim/Exception/HttpBadGatewayException.php @@ -0,0 +1,10 @@ +details = $details; + } + } + + /** + * @param ServerRequestInterface $request + */ + public function setRequest(ServerRequestInterface $request) + { + $this->request = $request; + } + + /** + * @param ResponseInterface $response + */ + public function setResponse(ResponseInterface $response) + { + $this->response = $response; + } + + /** + * @param array $details + */ + public function setDetails(array $details) + { + $this->details = $details; + } + + /** + * @param string $title + */ + public function setTitle($title) + { + $this->title = $title; + } + + /** + * @param string $description + */ + public function setDescription($description) + { + $this->description = $description; + } + + public function notRecoverable() + { + $this->recoverable = false; + } + + /** + * @return null|ServerRequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * @return null|ResponseInterface + */ + public function getResponse() + { + return $this->response; + } + + /** + * @return array|null|string + */ + public function getDetails() + { + return $this->details; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return bool + */ + public function isRecoverable() + { + return $this->recoverable; + } +} diff --git a/Slim/Exception/HttpForbiddenException.php b/Slim/Exception/HttpForbiddenException.php new file mode 100644 index 000000000..a9f64173a --- /dev/null +++ b/Slim/Exception/HttpForbiddenException.php @@ -0,0 +1,10 @@ +getDetails(); + + if (isset($details['allowedMethods'])) { + $allowedMethods = $details['allowedMethods']; + + if (is_array($allowedMethods)) { + return implode(', ', $allowedMethods); + } elseif (is_string($allowedMethods)) { + return $allowedMethods; + } + + return ''; + } + } + + /** + * @param string|array $methods + */ + public function setAllowedMethods($methods) + { + if (is_null($this->details)) { + $this->details = []; + } + + $this->details['allowedMethods'] = $methods; + } +} diff --git a/Slim/Exception/HttpNotFoundException.php b/Slim/Exception/HttpNotFoundException.php new file mode 100644 index 000000000..614c001ce --- /dev/null +++ b/Slim/Exception/HttpNotFoundException.php @@ -0,0 +1,10 @@ +request = $request; - parent::__construct(sprintf('Unsupported HTTP method "%s" provided', $method)); - } - - public function getRequest() - { - return $this->request; - } -} diff --git a/Slim/Exception/MethodNotAllowedException.php b/Slim/Exception/MethodNotAllowedException.php deleted file mode 100644 index a737ab941..000000000 --- a/Slim/Exception/MethodNotAllowedException.php +++ /dev/null @@ -1,81 +0,0 @@ -request = $request; - $this->response = $response; - $this->allowedMethods = $allowedMethods; - } - - /** - * Get request - * - * @return ServerRequestInterface - */ - public function getRequest() - { - return $this->request; - } - - /** - * Get response - * - * @return ResponseInterface - */ - public function getResponse() - { - return $this->response; - } - - /** - * Get allowed methods - * - * @return string[] - */ - public function getAllowedMethods() - { - return $this->allowedMethods; - } -} diff --git a/Slim/Exception/NotFoundException.php b/Slim/Exception/NotFoundException.php deleted file mode 100644 index 527ba6629..000000000 --- a/Slim/Exception/NotFoundException.php +++ /dev/null @@ -1,62 +0,0 @@ -request = $request; - $this->response = $response; - } - - /** - * Get request - * - * @return ServerRequestInterface - */ - public function getRequest() - { - return $this->request; - } - - /** - * Get response - * - * @return ResponseInterface - */ - public function getResponse() - { - return $this->response; - } -} diff --git a/Slim/Handlers/AbstractError.php b/Slim/Handlers/AbstractError.php deleted file mode 100644 index 42f8dde3d..000000000 --- a/Slim/Handlers/AbstractError.php +++ /dev/null @@ -1,99 +0,0 @@ -displayErrorDetails = (bool) $displayErrorDetails; - } - - /** - * Write to the error log if displayErrorDetails is false - * - * @param \Exception|\Throwable $throwable - * - * @return void - */ - protected function writeToErrorLog($throwable) - { - if ($this->displayErrorDetails) { - return; - } - - $message = 'Slim Application Error:' . PHP_EOL; - $message .= $this->renderThrowableAsText($throwable); - while ($throwable = $throwable->getPrevious()) { - $message .= PHP_EOL . 'Previous error:' . PHP_EOL; - $message .= $this->renderThrowableAsText($throwable); - } - - $message .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; - - $this->logError($message); - } - - /** - * Render error as Text. - * - * @param \Exception|\Throwable $throwable - * - * @return string - */ - protected function renderThrowableAsText($throwable) - { - $text = sprintf('Type: %s' . PHP_EOL, get_class($throwable)); - - if ($code = $throwable->getCode()) { - $text .= sprintf('Code: %s' . PHP_EOL, $code); - } - - if ($message = $throwable->getMessage()) { - $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); - } - - if ($file = $throwable->getFile()) { - $text .= sprintf('File: %s' . PHP_EOL, $file); - } - - if ($line = $throwable->getLine()) { - $text .= sprintf('Line: %s' . PHP_EOL, $line); - } - - if ($trace = $throwable->getTraceAsString()) { - $text .= sprintf('Trace: %s', $trace); - } - - return $text; - } - - /** - * Wraps the error_log function so that this can be easily tested - * - * @param $message - */ - protected function logError($message) - { - error_log($message); - } -} diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php new file mode 100644 index 000000000..73fa9764c --- /dev/null +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -0,0 +1,249 @@ +displayErrorDetails = $displayErrorDetails; + $this->request = $request; + $this->response = $response; + $this->exception = $exception; + $this->method = $request->getMethod(); + $this->statusCode = $this->determineStatusCode(); + $this->contentType = $this->determineContentType($request); + $this->renderer = $this->determineRenderer(); + + if (!$this->displayErrorDetails) { + $this->writeToErrorLog(); + } + + return $this->formatResponse(); + } + + /** + * Determine which content type we know about is wanted using Accept header + * + * Note: This method is a bare-bones implementation designed specifically for + * Slim's error handling requirements. Consider a fully-feature solution such + * as willdurand/negotiation for any other situation. + * + * @param ServerRequestInterface $request + * @return string + */ + protected function determineContentType(ServerRequestInterface $request) + { + $acceptHeader = $request->getHeaderLine('Accept'); + $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + $count = count($selectedContentTypes); + + if ($count) { + $current = current($selectedContentTypes); + + /** + * Ensure other supported content types take precedence over text/plain + * when multiple content types are provided via Accept header. + */ + if ($current === 'text/plain' && $count > 1) { + return next($selectedContentTypes); + } + + return $current; + } + + if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) { + $mediaType = 'application/' . $matches[1]; + if (in_array($mediaType, $this->knownContentTypes)) { + return $mediaType; + } + } + + return 'text/html'; + } + + /** + * Determine which renderer to use based on content type + * Overloaded $renderer from calling class takes precedence over all + * + * @return ErrorRendererInterface + * + * @throws \RuntimeException + */ + protected function determineRenderer() + { + $renderer = $this->renderer; + + if ((!is_null($renderer) && !class_exists($renderer)) + || (!is_null($renderer) && !in_array('Slim\Interfaces\ErrorRendererInterface', class_implements($renderer))) + ) { + throw new \RuntimeException(sprintf( + 'Non compliant error renderer provided (%s). ' . + 'Renderer must implement the ErrorRendererInterface', + $renderer + )); + } + + if (is_null($renderer)) { + switch ($this->contentType) { + case 'application/json': + $renderer = JsonErrorRenderer::class; + break; + + case 'text/xml': + case 'application/xml': + $renderer = XmlErrorRenderer::class; + break; + + case 'text/plain': + $renderer = PlainTextErrorRenderer::class; + break; + + default: + case 'text/html': + $renderer = HtmlErrorRenderer::class; + break; + } + } + + return new $renderer($this->exception, $this->displayErrorDetails); + } + + /** + * @return int + */ + protected function determineStatusCode() + { + if ($this->method === 'OPTIONS') { + return 200; + } elseif ($this->exception instanceof HttpException) { + return $this->exception->getCode(); + } + return 500; + } + + /** + * @return ResponseInterface + */ + protected function formatResponse() + { + $e = $this->exception; + $response = $this->response; + $body = $this->renderer->renderWithBody(); + + if ($e instanceof HttpNotAllowedException) { + $response = $response->withHeader('Allow', $e->getAllowedMethods()); + } + + return $response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->withBody($body); + } + + /** + * Write to the error log if displayErrorDetails is false + * + * @return void + */ + protected function writeToErrorLog() + { + $renderer = new PlainTextErrorRenderer($this->exception, true); + $error = $renderer->render(); + $error .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; + $this->logError($error); + } + + /** + * Wraps the error_log function so that this can be easily tested + * + * @param string $error + */ + protected function logError($error) + { + error_log($error); + } +} diff --git a/Slim/Handlers/AbstractErrorRenderer.php b/Slim/Handlers/AbstractErrorRenderer.php new file mode 100644 index 000000000..bcdf48bb2 --- /dev/null +++ b/Slim/Handlers/AbstractErrorRenderer.php @@ -0,0 +1,51 @@ +exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + } + + /** + * @return Body + */ + public function renderWithBody() + { + $body = new Body(fopen('php://temp', 'r+')); + $body->write($this->render()); + return $body; + } +} diff --git a/Slim/Handlers/AbstractHandler.php b/Slim/Handlers/AbstractHandler.php deleted file mode 100644 index b166a1564..000000000 --- a/Slim/Handlers/AbstractHandler.php +++ /dev/null @@ -1,59 +0,0 @@ -getHeaderLine('Accept'); - $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); - - if (count($selectedContentTypes)) { - return current($selectedContentTypes); - } - - // handle +json and +xml specially - if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) { - $mediaType = 'application/' . $matches[1]; - if (in_array($mediaType, $this->knownContentTypes)) { - return $mediaType; - } - } - - return 'text/html'; - } -} diff --git a/Slim/Handlers/Error.php b/Slim/Handlers/Error.php deleted file mode 100644 index dd0bc8d4e..000000000 --- a/Slim/Handlers/Error.php +++ /dev/null @@ -1,224 +0,0 @@ -determineContentType($request); - switch ($contentType) { - case 'application/json': - $output = $this->renderJsonErrorMessage($exception); - break; - - case 'text/xml': - case 'application/xml': - $output = $this->renderXmlErrorMessage($exception); - break; - - case 'text/html': - $output = $this->renderHtmlErrorMessage($exception); - break; - - default: - throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); - } - - $this->writeToErrorLog($exception); - - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - - return $response - ->withStatus(500) - ->withHeader('Content-type', $contentType) - ->withBody($body); - } - - /** - * Render HTML error page - * - * @param \Exception $exception - * - * @return string - */ - protected function renderHtmlErrorMessage(\Exception $exception) - { - $title = 'Slim Application Error'; - - if ($this->displayErrorDetails) { - $html = '

The application could not run because of the following error:

'; - $html .= '

Details

'; - $html .= $this->renderHtmlException($exception); - - while ($exception = $exception->getPrevious()) { - $html .= '

Previous exception

'; - $html .= $this->renderHtmlExceptionOrError($exception); - } - } else { - $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; - } - - $output = sprintf( - "" . - "%s

%s

%s", - $title, - $title, - $html - ); - - return $output; - } - - /** - * Render exception as HTML. - * - * Provided for backwards compatibility; use renderHtmlExceptionOrError(). - * - * @param \Exception $exception - * - * @return string - */ - protected function renderHtmlException(\Exception $exception) - { - return $this->renderHtmlExceptionOrError($exception); - } - - /** - * Render exception or error as HTML. - * - * @param \Exception|\Error $exception - * - * @return string - */ - protected function renderHtmlExceptionOrError($exception) - { - if (!$exception instanceof \Exception && !$exception instanceof \Error) { - throw new \RuntimeException("Unexpected type. Expected Exception or Error."); - } - - $html = sprintf('
Type: %s
', get_class($exception)); - - if (($code = $exception->getCode())) { - $html .= sprintf('
Code: %s
', $code); - } - - if (($message = $exception->getMessage())) { - $html .= sprintf('
Message: %s
', htmlentities($message)); - } - - if (($file = $exception->getFile())) { - $html .= sprintf('
File: %s
', $file); - } - - if (($line = $exception->getLine())) { - $html .= sprintf('
Line: %s
', $line); - } - - if (($trace = $exception->getTraceAsString())) { - $html .= '

Trace

'; - $html .= sprintf('
%s
', htmlentities($trace)); - } - - return $html; - } - - /** - * Render JSON error - * - * @param \Exception $exception - * - * @return string - */ - protected function renderJsonErrorMessage(\Exception $exception) - { - $error = [ - 'message' => 'Slim Application Error', - ]; - - if ($this->displayErrorDetails) { - $error['exception'] = []; - - do { - $error['exception'][] = [ - 'type' => get_class($exception), - 'code' => $exception->getCode(), - 'message' => $exception->getMessage(), - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - 'trace' => explode("\n", $exception->getTraceAsString()), - ]; - } while ($exception = $exception->getPrevious()); - } - - return json_encode($error, JSON_PRETTY_PRINT); - } - - /** - * Render XML error - * - * @param \Exception $exception - * - * @return string - */ - protected function renderXmlErrorMessage(\Exception $exception) - { - $xml = "\n Slim Application Error\n"; - if ($this->displayErrorDetails) { - do { - $xml .= " \n"; - $xml .= " " . get_class($exception) . "\n"; - $xml .= " " . $exception->getCode() . "\n"; - $xml .= " " . $this->createCdataSection($exception->getMessage()) . "\n"; - $xml .= " " . $exception->getFile() . "\n"; - $xml .= " " . $exception->getLine() . "\n"; - $xml .= " " . $this->createCdataSection($exception->getTraceAsString()) . "\n"; - $xml .= " \n"; - } while ($exception = $exception->getPrevious()); - } - $xml .= ""; - - return $xml; - } - - /** - * Returns a CDATA section with the given content. - * - * @param string $content - * @return string - */ - private function createCdataSection($content) - { - return sprintf('', str_replace(']]>', ']]]]>', $content)); - } -} diff --git a/Slim/Handlers/ErrorHandler.php b/Slim/Handlers/ErrorHandler.php new file mode 100644 index 000000000..47836b216 --- /dev/null +++ b/Slim/Handlers/ErrorHandler.php @@ -0,0 +1,19 @@ +exception; + $title = 'Slim Application Error'; + + if ($this->displayErrorDetails) { + $html = '

The application could not run because of the following error:

'; + $html .= '

Details

'; + $html .= $this->renderExceptionFragment($e); + } else { + $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; + } + + return $this->renderHtmlBody($title, $html); + } + + /** + * @param string $title + * @param string $html + * @return string + */ + public function renderHtmlBody($title = '', $html = '') + { + return sprintf( + "" . + " " . + " " . + " %s" . + " " . + " " . + " " . + "

%s

" . + "
%s
" . + " Go Back" . + " " . + "", + $title, + $title, + $html + ); + } + + /** + * @param Exception $exception + * @return string + */ + private function renderExceptionFragment($exception) + { + $html = sprintf('
Type: %s
', get_class($exception)); + + if (($code = $exception->getCode())) { + $html .= sprintf('
Code: %s
', $code); + } + + if (($message = $exception->getMessage())) { + $html .= sprintf('
Message: %s
', htmlentities($message)); + } + + if (($file = $exception->getFile())) { + $html .= sprintf('
File: %s
', $file); + } + + if (($line = $exception->getLine())) { + $html .= sprintf('
Line: %s
', $line); + } + + if (($trace = $exception->getTraceAsString())) { + $html .= '

Trace

'; + $html .= sprintf('
%s
', htmlentities($trace)); + } + + return $html; + } +} diff --git a/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php new file mode 100644 index 000000000..6e3f715e4 --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php @@ -0,0 +1,50 @@ +exception; + $error = ['message' => $e->getMessage()]; + + if ($this->displayErrorDetails) { + $error['exception'] = []; + do { + $error['exception'][] = $this->formatExceptionFragment($e); + } while ($e = $e->getPrevious()); + } + + return json_encode($error, JSON_PRETTY_PRINT); + } + + /** + * @param \Exception|\Throwable $e + * @return array + */ + private function formatExceptionFragment($e) + { + return [ + 'type' => get_class($e), + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]; + } +} diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php new file mode 100644 index 000000000..fa876d75a --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -0,0 +1,62 @@ +exception; + + $text = 'Slim Application Error:' . PHP_EOL; + $text .= $this->formatExceptionFragment($e); + + while ($e = $e->getPrevious()) { + $text .= PHP_EOL . 'Previous Error:' . PHP_EOL; + $text .= $this->formatExceptionFragment($e); + } + + return $text; + } + + /** + * @param \Exception|\Throwable $e + * @return string + */ + private function formatExceptionFragment($e) + { + $text = sprintf('Type: %s' . PHP_EOL, get_class($e)); + + if ($code = $e->getCode()) { + $text .= sprintf('Code: %s' . PHP_EOL, $code); + } + if ($message = $e->getMessage()) { + $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); + } + if ($file = $e->getFile()) { + $text .= sprintf('File: %s' . PHP_EOL, $file); + } + if ($line = $e->getLine()) { + $text .= sprintf('Line: %s' . PHP_EOL, $line); + } + if ($trace = $e->getTraceAsString()) { + $text .= sprintf('Trace: %s', $trace); + } + + return $text; + } +} diff --git a/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php b/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php new file mode 100644 index 000000000..ed83742e2 --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php @@ -0,0 +1,51 @@ +exception; + $xml = "\n {$e->getMessage()}\n"; + if ($this->displayErrorDetails) { + do { + $xml .= " \n"; + $xml .= " " . get_class($e) . "\n"; + $xml .= " " . $e->getCode() . "\n"; + $xml .= " " . $this->createCdataSection($e->getMessage()) . "\n"; + $xml .= " " . $e->getFile() . "\n"; + $xml .= " " . $e->getLine() . "\n"; + $xml .= " \n"; + } while ($e = $e->getPrevious()); + } + $xml .= ""; + + return $xml; + } + + /** + * Returns a CDATA section with the given content. + * + * @param string $content + * @return string + */ + private function createCdataSection($content) + { + return sprintf('', str_replace(']]>', ']]]]>', $content)); + } +} diff --git a/Slim/Handlers/NotAllowed.php b/Slim/Handlers/NotAllowed.php deleted file mode 100644 index 9f382c45c..000000000 --- a/Slim/Handlers/NotAllowed.php +++ /dev/null @@ -1,147 +0,0 @@ -getMethod() === 'OPTIONS') { - $status = 200; - $contentType = 'text/plain'; - $output = $this->renderPlainNotAllowedMessage($methods); - } else { - $status = 405; - $contentType = $this->determineContentType($request); - switch ($contentType) { - case 'application/json': - $output = $this->renderJsonNotAllowedMessage($methods); - break; - - case 'text/xml': - case 'application/xml': - $output = $this->renderXmlNotAllowedMessage($methods); - break; - - case 'text/html': - $output = $this->renderHtmlNotAllowedMessage($methods); - break; - default: - throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); - } - } - - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - $allow = implode(', ', $methods); - - return $response - ->withStatus($status) - ->withHeader('Content-type', $contentType) - ->withHeader('Allow', $allow) - ->withBody($body); - } - - /** - * Render PLAIN not allowed message - * - * @param array $methods - * @return string - */ - protected function renderPlainNotAllowedMessage($methods) - { - $allow = implode(', ', $methods); - - return 'Allowed methods: ' . $allow; - } - - /** - * Render JSON not allowed message - * - * @param array $methods - * @return string - */ - protected function renderJsonNotAllowedMessage($methods) - { - $allow = implode(', ', $methods); - - return '{"message":"Method not allowed. Must be one of: ' . $allow . '"}'; - } - - /** - * Render XML not allowed message - * - * @param array $methods - * @return string - */ - protected function renderXmlNotAllowedMessage($methods) - { - $allow = implode(', ', $methods); - - return "Method not allowed. Must be one of: $allow"; - } - - /** - * Render HTML not allowed message - * - * @param array $methods - * @return string - */ - protected function renderHtmlNotAllowedMessage($methods) - { - $allow = implode(', ', $methods); - $output = << - - Method not allowed - - - -

Method not allowed

-

Method not allowed. Must be one of: $allow

- - -END; - - return $output; - } -} diff --git a/Slim/Handlers/NotFound.php b/Slim/Handlers/NotFound.php deleted file mode 100644 index d4a9dec4e..000000000 --- a/Slim/Handlers/NotFound.php +++ /dev/null @@ -1,126 +0,0 @@ -determineContentType($request); - switch ($contentType) { - case 'application/json': - $output = $this->renderJsonNotFoundOutput(); - break; - - case 'text/xml': - case 'application/xml': - $output = $this->renderXmlNotFoundOutput(); - break; - - case 'text/html': - $output = $this->renderHtmlNotFoundOutput($request); - break; - - default: - throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); - } - - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - - return $response->withStatus(404) - ->withHeader('Content-Type', $contentType) - ->withBody($body); - } - - /** - * Return a response for application/json content not found - * - * @return ResponseInterface - */ - protected function renderJsonNotFoundOutput() - { - return '{"message":"Not found"}'; - } - - /** - * Return a response for xml content not found - * - * @return ResponseInterface - */ - protected function renderXmlNotFoundOutput() - { - return 'Not found'; - } - - /** - * Return a response for text/html content not found - * - * @param ServerRequestInterface $request The most recent Request object - * - * @return ResponseInterface - */ - protected function renderHtmlNotFoundOutput(ServerRequestInterface $request) - { - $homeUrl = (string)($request->getUri()->withPath('')->withQuery('')->withFragment('')); - return << - - Page Not Found - - - -

Page Not Found

-

- The page you are looking for could not be found. Check the address bar - to ensure your URL is spelled correctly. If all else fails, you can - visit our home page at the link below. -

- Visit the Home Page - - -END; - } -} diff --git a/Slim/Handlers/PhpError.php b/Slim/Handlers/PhpError.php deleted file mode 100644 index 3ecce30cf..000000000 --- a/Slim/Handlers/PhpError.php +++ /dev/null @@ -1,205 +0,0 @@ -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; - default: - throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); - } - - $this->writeToErrorLog($error); - - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - - return $response - ->withStatus(500) - ->withHeader('Content-type', $contentType) - ->withBody($body); - } - - /** - * Render HTML error page - * - * @param \Throwable $error - * - * @return string - */ - protected function renderHtmlErrorMessage(\Throwable $error) - { - $title = 'Slim Application Error'; - - if ($this->displayErrorDetails) { - $html = '

The application could not run because of the following error:

'; - $html .= '

Details

'; - $html .= $this->renderHtmlError($error); - - while ($error = $error->getPrevious()) { - $html .= '

Previous error

'; - $html .= $this->renderHtmlError($error); - } - } else { - $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; - } - - $output = sprintf( - "" . - "%s

%s

%s", - $title, - $title, - $html - ); - - return $output; - } - - /** - * Render error as HTML. - * - * @param \Throwable $error - * - * @return string - */ - protected function renderHtmlError(\Throwable $error) - { - $html = sprintf('
Type: %s
', get_class($error)); - - if (($code = $error->getCode())) { - $html .= sprintf('
Code: %s
', $code); - } - - if (($message = $error->getMessage())) { - $html .= sprintf('
Message: %s
', htmlentities($message)); - } - - if (($file = $error->getFile())) { - $html .= sprintf('
File: %s
', $file); - } - - if (($line = $error->getLine())) { - $html .= sprintf('
Line: %s
', $line); - } - - if (($trace = $error->getTraceAsString())) { - $html .= '

Trace

'; - $html .= sprintf('
%s
', htmlentities($trace)); - } - - return $html; - } - - /** - * Render JSON error - * - * @param \Throwable $error - * - * @return string - */ - protected function renderJsonErrorMessage(\Throwable $error) - { - $json = [ - 'message' => 'Slim Application Error', - ]; - - if ($this->displayErrorDetails) { - $json['error'] = []; - - do { - $json['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($json, JSON_PRETTY_PRINT); - } - - /** - * Render XML error - * - * @param \Throwable $error - * - * @return string - */ - protected function renderXmlErrorMessage(\Throwable $error) - { - $xml = "\n Slim Application Error\n"; - if ($this->displayErrorDetails) { - do { - $xml .= " \n"; - $xml .= " " . get_class($error) . "\n"; - $xml .= " " . $error->getCode() . "\n"; - $xml .= " " . $this->createCdataSection($error->getMessage()) . "\n"; - $xml .= " " . $error->getFile() . "\n"; - $xml .= " " . $error->getLine() . "\n"; - $xml .= " " . $this->createCdataSection($error->getTraceAsString()) . "\n"; - $xml .= " \n"; - } while ($error = $error->getPrevious()); - } - $xml .= ""; - - return $xml; - } - - /** - * Returns a CDATA section with the given content. - * - * @param string $content - * @return string - */ - private function createCdataSection($content) - { - return sprintf('', str_replace(']]>', ']]]]>', $content)); - } -} diff --git a/Slim/Interfaces/ErrorHandlerInterface.php b/Slim/Interfaces/ErrorHandlerInterface.php new file mode 100644 index 000000000..f3db465c5 --- /dev/null +++ b/Slim/Interfaces/ErrorHandlerInterface.php @@ -0,0 +1,37 @@ +assertAttributeContains('foo', 'settings', $app); } + public function testGetDefaultErrorHandler() + { + $app = new App(); + $this->assertInstanceOf('\Slim\Handlers\ErrorHandler', $app->getDefaultErrorHandler()); + } + /******************************************************************************** * Router proxy methods *******************************************************************************/ - public function testGetRoute() { $path = '/foo'; @@ -783,7 +791,6 @@ public function testEmptyGroupWithEmptyNestedGroupAndSegmentRouteWithoutLeadingS /******************************************************************************** * Middleware *******************************************************************************/ - public function testBottomMiddlewareIsApp() { $app = new App(); @@ -989,11 +996,122 @@ public function testAddMiddlewareOnRouteAndOnTwoRouteGroup() $this->assertEquals('In1In2In3CenterOut3Out2Out1', (string)$res->getBody()); } + /******************************************************************************** + * Error Handlers + *******************************************************************************/ + public function testSetErrorHandler() + { + $app = $this->appFactory(); + $app->get('/foo', function ($req, $res, $args) { + return $res; + }); + $app->add(function () { + throw new HttpNotFoundException(); + }); + $exception = HttpNotFoundException::class; + $handler = function ($req, $res) { + return $res->withJson(['Oops..']); + }; + $app->setErrorHandler($exception, $handler); + $res = $app->run(true); + $expectedOutput = json_encode(['Oops..']); + + $this->assertEquals($res->getBody(), $expectedOutput); + } + + /** + * @expectedException \RuntimeException + */ + public function testSetErrorHandlerThrowsExceptionWhenInvalidCallableIsPassed() + { + $app = new App(); + $app->setErrorHandler(HttpNotFoundException::class, 'UnresolvableCallable'); + } + + public function testSetDefaultErrorHandler() + { + $app = $this->appFactory(); + $app->get('/foo', function ($req, $res, $args) { + return $res; + }); + $app->add(function () { + throw new HttpNotFoundException(); + }); + $handler = function ($req, $res) { + return $res->withJson(['Oops..']); + }; + $app->setDefaultErrorHandler($handler); + $res = $app->run(true); + $expectedOutput = json_encode(['Oops..']); + + $this->assertEquals($res->getBody(), $expectedOutput); + } + + /** + * @expectedException \RuntimeException + */ + public function testSetDefaultErrorHandlerThrowsExceptionWhenInvalidCallableIsPassed() + { + $app = new App(); + $app->setDefaultErrorHandler(HttpNotFoundException::class, 'UnresolvableCallable'); + } + + public function testErrorHandlerShortcuts() + { + $app = new App(); + $handler = new MockErrorHandler(); + $app->setNotAllowedHandler($handler); + $app->setNotFoundHandler($handler); + + $this->assertInstanceOf(MockErrorHandler::class, $app->getErrorHandler(HttpNotAllowedException::class)); + $this->assertInstanceOf(MockErrorHandler::class, $app->getErrorHandler(HttpNotFoundException::class)); + } + + public function testGetErrorHandlerWillReturnDefaultErrorHandlerForUnhandledExceptions() + { + $app = new App(); + $exception = MockCustomException::class; + $handler = $app->getErrorHandler($exception); + + $this->assertInstanceOf(ErrorHandler::class, $handler); + } + + public function testGetErrorHandlerResolvesContainerCallableWhenHandlerPassedIntoSettings() + { + $app = new App(); + $container = new Container(); + $app->setContainer($container); + $app->setNotAllowedHandler(MockErrorHandler::class); + $handler = $app->getErrorHandler(HttpNotAllowedException::class); + + $this->assertEquals([new MockErrorHandler(), '__invoke'], $handler); + } + + public function testGetDefaultHandlerResolvesContainerCallableWhenHandlerPassedIntoSettings() + { + $app = new App(); + $container = new Container(); + $app->setContainer($container); + $app->setDefaultErrorHandler(MockErrorHandler::class); + $handler = $app->getDefaultErrorHandler(); + + $this->assertEquals([new MockErrorHandler(), '__invoke'], $handler); + } + + /** + * @expectedException \RuntimeException + */ + public function testGetErrorHandlersThrowsExceptionWhenErrorHandlersArgumentSettingIsNotArray() + { + $settings = ['errorHandlers' => 'ShouldBeArray']; + $app = new App($settings); + $handler = new MockErrorHandler(); + $app->setErrorHandler(HttpNotFoundException::class, $handler); + } /******************************************************************************** * Runner *******************************************************************************/ - public function testInvokeReturnMethodNotAllowed() { $app = new App(); @@ -1017,6 +1135,11 @@ public function testInvokeReturnMethodNotAllowed() $req = new Request('POST', $uri, $headers, $cookies, $serverParams, $body); $res = new Response(); + // Create Html Renderer and Assert Output + $exception = new HttpNotAllowedException; + $exception->setAllowedMethods(['GET']); + $renderer = new HtmlErrorRenderer($exception, false); + // Invoke app $resOut = $app($req, $res); @@ -1024,7 +1147,7 @@ public function testInvokeReturnMethodNotAllowed() $this->assertEquals(405, (string)$resOut->getStatusCode()); $this->assertEquals(['GET'], $resOut->getHeader('Allow')); $this->assertContains( - '

Method not allowed. Must be one of: GET

', + $renderer->render(), (string)$resOut->getBody() ); @@ -1471,7 +1594,6 @@ public function testRun() $this->assertEquals('bar', (string)$resOut); } - public function testRespond() { $app = new App(); @@ -1787,28 +1909,6 @@ public function appFactory() return $app; } - /** - * throws \Exception - * throws \Slim\Exception\MethodNotAllowedException - * throws \Slim\Exception\NotFoundException - * expectedException \Exception - */ -// public function testRunExceptionNoHandler() -// { -// $app = $this->appFactory(); -// -// $container = $app->getContainer(); -// unset($container['errorHandler']); -// -// $app->get('/foo', function ($req, $res, $args) { -// return $res; -// }); -// $app->add(function ($req, $res, $args) { -// throw new \Exception(); -// }); -// $res = $app->run(true); -// } - /** * @requires PHP 7.0 */ @@ -1837,64 +1937,28 @@ public function testRunNotFound() $app->get('/foo', function ($req, $res, $args) { return $res; }); - $app->add(function ($req, $res, $args) { - throw new NotFoundException($req, $res); + $app->add(function () { + throw new HttpNotFoundException; }); $res = $app->run(true); $this->assertEquals(404, $res->getStatusCode()); } - /** - * expectedException \Slim\Exception\NotFoundException - */ -// public function testRunNotFoundWithoutHandler() -// { -// $app = $this->appFactory(); -// $container = $app->getContainer(); -// unset($container['notFoundHandler']); -// -// $app->get('/foo', function ($req, $res, $args) { -// return $res; -// }); -// $app->add(function ($req, $res, $args) { -// throw new NotFoundException($req, $res); -// }); -// $res = $app->run(true); -// } - public function testRunNotAllowed() { $app = $this->appFactory(); $app->get('/foo', function ($req, $res, $args) { return $res; }); - $app->add(function ($req, $res, $args) { - throw new MethodNotAllowedException($req, $res, ['POST']); + $app->add(function () { + throw new HttpNotAllowedException; }); $res = $app->run(true); $this->assertEquals(405, $res->getStatusCode()); } - /** - * expectedException \Slim\Exception\MethodNotAllowedException - */ -// public function testRunNotAllowedWithoutHandler() -// { -// $app = $this->appFactory(); -// $container = $app->getContainer(); -// unset($container['notAllowedHandler']); -// -// $app->get('/foo', function ($req, $res, $args) { -// return $res; -// }); -// $app->add(function ($req, $res, $args) { -// throw new MethodNotAllowedException($req, $res, ['POST']); -// }); -// $res = $app->run(true); -// } - public function testAppRunWithdetermineRouteBeforeAppMiddleware() { $app = $this->appFactory(); @@ -1988,8 +2052,10 @@ public function testCallingAContainerCallable() $request = new Request('GET', Uri::createFromString(''), $headers, [], [], $body); $response = new Response(); + $exception = new HttpNotFoundException; $notFoundHandler = $app->getNotFoundHandler(); - $response = $notFoundHandler($request, $response); + $displayErrorDetails = $app->getSetting('displayErrorDetails'); + $response = $notFoundHandler($request, $response, $exception, $displayErrorDetails); $this->assertSame(404, $response->getStatusCode()); } @@ -2150,25 +2216,6 @@ public function testIsEmptyResponseWithoutEmptyMethod() $this->assertTrue($result); } - public function testHandlePhpError() - { - $this->skipIfPhp70(); - $method = new \ReflectionMethod('Slim\App', 'handlePhpError'); - $method->setAccessible(true); - - $throwable = $this->getMockBuilder('\Throwable') - ->setMethods(['getCode', 'getMessage', 'getFile', 'getLine', 'getTraceAsString', 'getPrevious'])->getMock(); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - $res = new Response(); - - $res = $method->invoke(new App(), $throwable, $req, $res); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame('text/html', $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), '')); - } - protected function skipIfPhp70() { if (version_compare(PHP_VERSION, '7.0', '>=')) { diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 26593979c..534a968b8 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\TestCase; use Slim\Container; use Psr\Container\ContainerInterface; +use Slim\Tests\Mocks\MockErrorRenderer; class ContainerTest extends TestCase { @@ -29,11 +30,10 @@ public function setUp() */ public function testGet() { - $this->assertInstanceOf('\Slim\Handlers\NotFound', $this->container->get('notFoundHandler')); + $this->container['MockErrorRenderer'] = new MockErrorRenderer(new \Exception('oops'), false); + $this->assertInstanceOf(MockErrorRenderer::class, $this->container->get('MockErrorRenderer')); } - - /** * Test `get()` throws error if item does not exist * @@ -124,22 +124,6 @@ public function testGetRouter() $this->assertInstanceOf('\Slim\Router', $this->container['router']); } - /** - * Test container has error handler - */ - public function testGetErrorHandler() - { - $this->assertInstanceOf('\Slim\Handlers\Error', $this->container['errorHandler']); - } - - /** - * Test container has error handler - */ - public function testGetNotAllowedHandler() - { - $this->assertInstanceOf('\Slim\Handlers\NotAllowed', $this->container['notAllowedHandler']); - } - /** * Test settings can be edited */ diff --git a/tests/Exception/HttpExceptionTest.php b/tests/Exception/HttpExceptionTest.php new file mode 100644 index 000000000..df6e897e0 --- /dev/null +++ b/tests/Exception/HttpExceptionTest.php @@ -0,0 +1,79 @@ +assertEquals($exceptionWithMessage->getMessage(), 'Oops..'); + + $details = ['allowedMethods' => 'POST']; + $exceptionWithDetails = new HttpNotAllowedException($details); + $this->assertEquals($exceptionWithDetails->getDetails(), $details); + } + + public function testHttpExceptionRequestReponseGetterSetters() + { + // Prepare request and response objects + $uri = Uri::createFromGlobals($_SERVER); + $headers = Headers::createFromGlobals($_SERVER); + $cookies = []; + $serverParams = $_SERVER; + $body = new RequestBody(); + $request = new Request('GET', $uri, $headers, $cookies, $serverParams, $body); + $response = new Response(); + $exception = new HttpNotFoundException; + $exception->setRequest($request); + $exception->setResponse($response); + + $this->assertInstanceOf(Request::class, $exception->getRequest()); + $this->assertInstanceOf(Response::class, $exception->getResponse()); + } + + public function testHttpExceptionAttributeGettersSetters() + { + $exception = new HttpNotFoundException; + $exception->setTitle('Title'); + $exception->setDescription('Description'); + $exception->setDetails(['Details']); + + $this->assertEquals('Title', $exception->getTitle()); + $this->assertEquals('Description', $exception->getDescription()); + $this->assertEquals(['Details'], $exception->getDetails()); + } + + public function testHttpExceptionRecoverableGetterSetter() + { + $exception = new HttpNotFoundException; + $exception->notRecoverable(); + + $this->assertEquals(false, $exception->isRecoverable()); + } + + public function testHttpNotAllowedExceptionGetAllowedMethods() + { + $exception = new HttpNotAllowedException; + $exception->setAllowedMethods('GET'); + $this->assertEquals('GET', $exception->getAllowedMethods()); + + $exception = new HttpNotAllowedException; + $this->assertEquals('', $exception->getAllowedMethods()); + } +} diff --git a/tests/Handlers/AbstractErrorHandlerTest.php b/tests/Handlers/AbstractErrorHandlerTest.php new file mode 100644 index 000000000..d03277036 --- /dev/null +++ b/tests/Handlers/AbstractErrorHandlerTest.php @@ -0,0 +1,133 @@ +getMockForAbstractClass(AbstractErrorHandler::class); + $class = new ReflectionClass(AbstractErrorHandler::class); + + $reflectionProperty = $class->getProperty('renderer'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($abstractHandler, MockErrorRenderer::class); + + $method = $class->getMethod('determineRenderer'); + $method->setAccessible(true); + $method->invoke($abstractHandler); + + $this->addToAssertionCount(1); + } + + /** + * @expectedException \RuntimeException + */ + public function testDetermineContentTypeMethodThrowsExceptionWhenPassedAnInvalidRenderer() + { + $abstractHandler = $this->getMockForAbstractClass(AbstractErrorHandler::class); + $class = new ReflectionClass(AbstractErrorHandler::class); + + $reflectionProperty = $class->getProperty('renderer'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($abstractHandler, 'NonExistentRenderer::class'); + + $method = $class->getMethod('determineRenderer'); + $method->setAccessible(true); + $method->invoke($abstractHandler); + } + + public function testHalfValidContentType() + { + $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); + + $req->expects($this->any())->method('getHeaderLine')->will($this->returnValue('unknown/+json')); + + $abstractHandler = $this->getMockForAbstractClass(AbstractErrorHandler::class); + + $newTypes = [ + 'application/xml', + 'text/xml', + 'text/html', + ]; + + $class = new ReflectionClass(AbstractErrorHandler::class); + + $reflectionProperty = $class->getProperty('knownContentTypes'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($abstractHandler, $newTypes); + + $method = $class->getMethod('determineContentType'); + $method->setAccessible(true); + + $return = $method->invoke($abstractHandler, $req); + + $this->assertEquals('text/html', $return); + } + + /** + * Ensure that an acceptable media-type is found in the Accept header even + * if it's not the first in the list. + */ + public function testAcceptableMediaTypeIsNotFirstInList() + { + $request = $this->getMockBuilder('Slim\Http\Request') + ->disableOriginalConstructor() + ->getMock(); + + $request->expects($this->any()) + ->method('getHeaderLine') + ->willReturn('text/plain,text/html'); + + // provide access to the determineContentType() as it's a protected method + $class = new ReflectionClass(AbstractErrorHandler::class); + $method = $class->getMethod('determineContentType'); + $method->setAccessible(true); + + // use a mock object here as AbstractErrorHandler cannot be directly instantiated + $abstractHandler = $this->getMockForAbstractClass(AbstractErrorHandler::class); + + // call determineContentType() + $return = $method->invoke($abstractHandler, $request); + + $this->assertEquals('text/html', $return); + } + + public function testOptions() + { + $handler = new ErrorHandler(); + $exception = new HttpNotAllowedException(); + $exception->setAllowedMethods(['POST', 'PUT']); + /** @var Response $res */ + $res = $handler->__invoke($this->getRequest('OPTIONS'), new Response(), $exception, false); + $this->assertSame(200, $res->getStatusCode()); + $this->assertTrue($res->hasHeader('Allow')); + $this->assertEquals('POST, PUT', $res->getHeaderLine('Allow')); + } + + /** + * @param string $method + * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Request + */ + protected function getRequest($method, $contentType = 'text/html') + { + $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); + $req->expects($this->once())->method('getMethod')->will($this->returnValue($method)); + $req->expects($this->any())->method('getHeaderLine')->will($this->returnValue($contentType)); + return $req; + } +} diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php new file mode 100644 index 000000000..3c27e0519 --- /dev/null +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -0,0 +1,99 @@ +render(); + + $this->assertRegExp('/.*The application could not run because of the following error:.*/', $output); + } + + public function testHTMLErrorRendererRenderFragmentMethod() + { + $exception = new Exception('Oops..', 500); + $renderer = new HtmlErrorRenderer($exception, true); + $reflectionRenderer = new ReflectionClass(HtmlErrorRenderer::class); + $method = $reflectionRenderer->getMethod('renderExceptionFragment'); + $method->setAccessible(true); + $output = $method->invoke($renderer, $exception); + + $this->assertRegExp('/.*Type:*/', $output); + $this->assertRegExp('/.*Code:*/', $output); + $this->assertRegExp('/.*Message*/', $output); + $this->assertRegExp('/.*File*/', $output); + $this->assertRegExp('/.*Line*/', $output); + } + + public function testJSONErrorRendererDisplaysErrorDetails() + { + $exception = new Exception('Oops..'); + $renderer = new JsonErrorRenderer($exception, true); + $reflectionRenderer = new ReflectionClass(JsonErrorRenderer::class); + $method = $reflectionRenderer->getMethod('formatExceptionFragment'); + $method->setAccessible(true); + $fragment = $method->invoke($renderer, $exception); + $output = json_encode(json_decode($renderer->render())); + $expectedString = json_encode(['message' => 'Oops..', 'exception' => [$fragment]]); + + $this->assertEquals($output, $expectedString); + } + + public function testJSONErrorRendererDoesNotDisplayErrorDetails() + { + $exception = new Exception('Oops..'); + $renderer = new JsonErrorRenderer($exception, false); + $output = json_encode(json_decode($renderer->render())); + $this->assertEquals($output, json_encode(['message' => 'Oops..'])); + } + + public function testJSONErrorRendererDisplaysPreviousError() + { + $previousException = new Exception('Oh no!'); + $exception = new Exception('Oops..', 0, $previousException); + $renderer = new JsonErrorRenderer($exception, true); + $reflectionRenderer = new ReflectionClass(JsonErrorRenderer::class); + $method = $reflectionRenderer->getMethod('formatExceptionFragment'); + $method->setAccessible(true); + $output = json_encode(json_decode($renderer->render())); + + $fragments = [ + $method->invoke($renderer, $exception), + $method->invoke($renderer, $previousException), + ]; + + $expectedString = json_encode(['message' => 'Oops..', 'exception' => $fragments]); + + $this->assertEquals($output, $expectedString); + } + + public function testXMLErrorRendererDisplaysErrorDetails() + { + $previousException = new RuntimeException('Oops..'); + $exception = new Exception('Ooops...', 0, $previousException); + $renderer = new XmlErrorRenderer($exception, true); + $output = simplexml_load_string($renderer->render()); + + $this->assertEquals($output->message[0], 'Ooops...'); + $this->assertEquals((string)$output->exception[0]->type, 'Exception'); + $this->assertEquals((string)$output->exception[1]->type, 'RuntimeException'); + } +} diff --git a/tests/Handlers/AbstractHandlerTest.php b/tests/Handlers/AbstractHandlerTest.php deleted file mode 100644 index afd27afdf..000000000 --- a/tests/Handlers/AbstractHandlerTest.php +++ /dev/null @@ -1,71 +0,0 @@ -getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - - $req->expects($this->any())->method('getHeaderLine')->will($this->returnValue('unknown/+json')); - - $abstractHandler = $this->getMockForAbstractClass(AbstractHandler::class); - - $newTypes = [ - 'application/xml', - 'text/xml', - 'text/html', - ]; - - $class = new \ReflectionClass(AbstractHandler::class); - - $reflectionProperty = $class->getProperty('knownContentTypes'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($abstractHandler, $newTypes); - - $method = $class->getMethod('determineContentType'); - $method->setAccessible(true); - - $return = $method->invoke($abstractHandler, $req); - - $this->assertEquals('text/html', $return); - } - - /** - * Ensure that an acceptable media-type is found in the Accept header even - * if it's not the first in the list. - */ - public function testAcceptableMediaTypeIsNotFirstInList() - { - $request = $this->getMockBuilder('Slim\Http\Request') - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->any()) - ->method('getHeaderLine') - ->willReturn('text/plain,text/html'); - - // provide access to the determineContentType() as it's a protected method - $class = new \ReflectionClass(AbstractHandler::class); - $method = $class->getMethod('determineContentType'); - $method->setAccessible(true); - - // use a mock object here as AbstractHandler cannot be directly instantiated - $abstractHandler = $this->getMockForAbstractClass(AbstractHandler::class); - - // call determineContentType() - $return = $method->invoke($abstractHandler, $request); - - $this->assertEquals('text/html', $return); - } -} diff --git a/tests/Handlers/ErrorTest.php b/tests/Handlers/ErrorHandlerTest.php similarity index 56% rename from tests/Handlers/ErrorTest.php rename to tests/Handlers/ErrorHandlerTest.php index a513acc42..cb1751030 100644 --- a/tests/Handlers/ErrorTest.php +++ b/tests/Handlers/ErrorHandlerTest.php @@ -9,8 +9,9 @@ namespace Slim\Tests\Handlers; use PHPUnit\Framework\TestCase; -use Slim\Handlers\Error; +use Slim\Handlers\ErrorHandler; use Slim\Http\Response; +use Exception; class ErrorTest extends TestCase { @@ -31,13 +32,13 @@ public function errorProvider() * * @dataProvider errorProvider */ - public function testError($acceptHeader, $contentType, $startOfBody) + public function testErrorHandler($acceptHeader, $contentType, $startOfBody) { - $error = new Error(); - $e = new \Exception("Oops", 1, new \Exception('Previous oops')); + $errorHandler = new ErrorHandler(); + $e = new Exception("Oops", 1, new Exception('Previous oops')); /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); + $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e, false); $this->assertSame(500, $res->getStatusCode()); $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); @@ -49,42 +50,26 @@ public function testError($acceptHeader, $contentType, $startOfBody) * * @dataProvider errorProvider */ - public function testErrorDisplayDetails($acceptHeader, $contentType, $startOfBody) + public function testErrorHandlerDisplayDetails($acceptHeader, $contentType, $startOfBody) { - $error = new Error(true); - $e = new \Exception('Oops', 1, new \Exception('Opps before')); + $errorHandler = new ErrorHandler(true); + $e = new Exception('Oops', 1, new Exception('Oops before')); /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); + $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e, true); $this->assertSame(500, $res->getStatusCode()); $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); } - /** - * @expectedException \UnexpectedValueException - */ - public function testNotFoundContentType() - { - $errorMock = $this->getMockBuilder(Error::class)->setMethods(['determineContentType'])->getMock(); - $errorMock->method('determineContentType') - ->will($this->returnValue('unknown/type')); - - $e = new \Exception("Oops"); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - - $errorMock->__invoke($req, new Response(), $e); - } - /** * Test that an exception with a previous exception provides correct output * to the error log */ public function testPreviousException() { - $error = $this->getMockBuilder('\Slim\Handlers\Error')->setMethods(['logError'])->getMock(); + $error = $this->getMockBuilder('\Slim\Handlers\ErrorHandler')->setMethods(['logError'])->getMock(); $error->expects($this->once())->method('logError')->with( $this->logicalAnd( $this->stringContains("Type: Exception" . PHP_EOL . "Message: Second Oops"), @@ -92,26 +77,10 @@ public function testPreviousException() ) ); - $first = new \Exception("First Oops"); - $second = new \Exception("Second Oops", 0, $first); - - $error->__invoke($this->getRequest('GET', 'application/json'), new Response(), $second); - } - - /** - * If someone extends the Error handler and calls renderHtmlExceptionOrError with - * a parameter that isn't an Exception or Error, then we thrown an Exception. - * - * @expectedException \RuntimeException - */ - public function testRenderHtmlExceptionorErrorTypeChecksParameter() - { - $class = new \ReflectionClass(Error::class); - $renderHtmlExceptionorError = $class->getMethod('renderHtmlExceptionOrError'); - $renderHtmlExceptionorError->setAccessible(true); + $first = new Exception("First Oops"); + $second = new Exception("Second Oops", 0, $first); - $error = new Error(); - $renderHtmlExceptionorError->invokeArgs($error, ['foo']); + $error->__invoke($this->getRequest('GET', 'application/json'), new Response(), $second, false); } /** diff --git a/tests/Handlers/NotAllowedTest.php b/tests/Handlers/NotAllowedTest.php deleted file mode 100644 index fa26d5523..000000000 --- a/tests/Handlers/NotAllowedTest.php +++ /dev/null @@ -1,84 +0,0 @@ -'], - ['application/hal+xml', 'application/xml', ''], - ['text/xml', 'text/xml', ''], - ['text/html', 'text/html', ''], - ]; - } - - /** - * Test invalid method returns the correct code and content type - * - * @dataProvider invalidMethodProvider - */ - public function testInvalidMethod($acceptHeader, $contentType, $startOfBody) - { - $notAllowed = new NotAllowed(); - - /** @var Response $res */ - $res = $notAllowed->__invoke($this->getRequest('GET', $acceptHeader), new Response(), ['POST', 'PUT']); - - $this->assertSame(405, $res->getStatusCode()); - $this->assertTrue($res->hasHeader('Allow')); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals('POST, PUT', $res->getHeaderLine('Allow')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - public function testOptions() - { - $notAllowed = new NotAllowed(); - - /** @var Response $res */ - $res = $notAllowed->__invoke($this->getRequest('OPTIONS'), new Response(), ['POST', 'PUT']); - - $this->assertSame(200, $res->getStatusCode()); - $this->assertTrue($res->hasHeader('Allow')); - $this->assertEquals('POST, PUT', $res->getHeaderLine('Allow')); - } - - /** - * @expectedException \UnexpectedValueException - */ - public function testNotFoundContentType() - { - $errorMock = $this->getMockBuilder(NotAllowed::class)->setMethods(['determineContentType'])->getMock(); - $errorMock->method('determineContentType') - ->will($this->returnValue('unknown/type')); - - $errorMock->__invoke($this->getRequest('GET', 'unknown/type'), new Response(), ['POST']); - } - - /** - * @param string $method - * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Request - */ - protected function getRequest($method, $contentType = 'text/html') - { - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - $req->expects($this->once())->method('getMethod')->will($this->returnValue($method)); - $req->expects($this->any())->method('getHeaderLine')->will($this->returnValue($contentType)); - - return $req; - } -} diff --git a/tests/Handlers/NotFoundTest.php b/tests/Handlers/NotFoundTest.php deleted file mode 100644 index 5500c8b5c..000000000 --- a/tests/Handlers/NotFoundTest.php +++ /dev/null @@ -1,75 +0,0 @@ -'], - ['application/hal+xml', 'application/xml', ''], - ['text/xml', 'text/xml', ''], - ['text/html', 'text/html', ''], - ]; - } - - /** - * Test invalid method returns the correct code and content type - * - * @dataProvider notFoundProvider - */ - public function testNotFound($acceptHeader, $contentType, $startOfBody) - { - $notAllowed = new NotFound(); - - /** @var Response $res */ - $res = $notAllowed->__invoke($this->getRequest('GET', $acceptHeader), new Response(), ['POST', 'PUT']); - - $this->assertSame(404, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - /** - * @expectedException \UnexpectedValueException - */ - public function testNotFoundContentType() - { - $errorMock = $this->getMockBuilder(NotFound::class)->setMethods(['determineContentType'])->getMock(); - $errorMock->method('determineContentType') - ->will($this->returnValue('unknown/type')); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - - $errorMock->__invoke($req, new Response(), ['POST']); - } - - /** - * @param string $method - * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Request - */ - protected function getRequest($method, $contentType = 'text/html') - { - $uri = new Uri('http', 'example.com', 80, '/notfound'); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - $req->expects($this->once())->method('getHeaderLine')->will($this->returnValue($contentType)); - $req->expects($this->any())->method('getUri')->will($this->returnValue($uri)); - - return $req; - } -} diff --git a/tests/Handlers/PhpErrorTest.php b/tests/Handlers/PhpErrorTest.php deleted file mode 100644 index 5e5e3188d..000000000 --- a/tests/Handlers/PhpErrorTest.php +++ /dev/null @@ -1,183 +0,0 @@ -'], - ['application/hal+xml', 'application/xml', ''], - ['text/xml', 'text/xml', ''], - ['text/html', 'text/html', ''], - ]; - } - - /** - * Test invalid method returns the correct code and content type - * - * @requires PHP 7.0 - * @dataProvider phpErrorProvider - */ - public function testPhpError($acceptHeader, $contentType, $startOfBody) - { - $error = new PhpError(); - - /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), new \Exception()); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - /** - * Test invalid method returns the correct code and content type - * - * @requires PHP 7.0 - * @dataProvider phpErrorProvider - */ - public function testPhpErrorDisplayDetails($acceptHeader, $contentType, $startOfBody) - { - $error = new PhpError(true); - - $exception = new \Exception('Oops', 1, new \Exception('Opps before')); - - /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $exception); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - /** - * @requires PHP 7.0 - * @expectedException \UnexpectedValueException - */ - public function testNotFoundContentType() - { - $errorMock = $this->getMockBuilder(PhpError::class)->setMethods(['determineContentType'])->getMock(); - $errorMock->method('determineContentType') - ->will($this->returnValue('unknown/type')); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - - $errorMock->__invoke($req, new Response(), new \Exception()); - } - - /** - * Test invalid method returns the correct code and content type - * - * @requires PHP 5.0 - * @dataProvider phpErrorProvider - */ - public function testPhpError5($acceptHeader, $contentType, $startOfBody) - { - $this->skipIfPhp70(); - $error = new PhpError(); - - $throwable = $this->getMockBuilder('\Throwable') - ->setMethods(['getCode', 'getMessage', 'getFile', 'getLine', 'getTraceAsString', 'getPrevious'])->getMock(); - - /** @var \Throwable $throwable */ - - /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $throwable); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - - - /** - * Test invalid method returns the correct code and content type - * - * @dataProvider phpErrorProvider - */ - public function testPhpErrorDisplayDetails5($acceptHeader, $contentType, $startOfBody) - { - $this->skipIfPhp70(); - - $error = new PhpError(true); - - $throwable = $this->getMockBuilder('\Throwable') - ->setMethods(['getCode', 'getMessage', 'getFile', 'getLine', 'getTraceAsString', 'getPrevious'])->getMock(); - - $throwablePrev = clone $throwable; - - $throwable->method('getCode')->will($this->returnValue(1)); - $throwable->method('getMessage')->will($this->returnValue('Oops')); - $throwable->method('getFile')->will($this->returnValue('test.php')); - $throwable->method('getLine')->will($this->returnValue('1')); - $throwable->method('getTraceAsString')->will($this->returnValue('This is error')); - $throwable->method('getPrevious')->will($this->returnValue($throwablePrev)); - - /** @var \Throwable $throwable */ - - /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $throwable); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - /** - * @requires PHP 5.0 - * @expectedException \UnexpectedValueException - */ - public function testNotFoundContentType5() - { - $this->skipIfPhp70(); - $errorMock = $this->getMockBuilder(PhpError::class)->setMethods(['determineContentType'])->getMock(); - - $errorMock->method('determineContentType') - ->will($this->returnValue('unknown/type')); - - $throwable = $this->getMockBuilder('\Throwable')->getMock(); - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - - $errorMock->__invoke($req, new Response(), $throwable); - } - - /** - * @param string $method - * - * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Request - */ - protected function getRequest($method, $acceptHeader) - { - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - $req->expects($this->once())->method('getHeaderLine')->will($this->returnValue($acceptHeader)); - - return $req; - } - - /** - * @return mixed - */ - protected function skipIfPhp70() - { - if (version_compare(PHP_VERSION, '7.0', '>=')) { - $this->markTestSkipped(); - } - } -} diff --git a/tests/Mocks/MockCustomException.php b/tests/Mocks/MockCustomException.php new file mode 100644 index 000000000..307215093 --- /dev/null +++ b/tests/Mocks/MockCustomException.php @@ -0,0 +1,18 @@ +