Hello,
When requesting a route that does not exist, I get the Slim 404 error page. How can I setup my own action/view ? I do not find it in the documentation of Slim 4
Thx, Th.
Hello,
When requesting a route that does not exist, I get the Slim 404 error page. How can I setup my own action/view ? I do not find it in the documentation of Slim 4
Thx, Th.
You have at least 3 options (there are more).
In Slim 4 you can register an error render per content type, e.g. html/text or application/json:
You can also register a custom default error handler with full access to the response object:
The third option is to implement a custom error handler middleware (without adding the Slim error middleware to the stack).
Thanks. So I’d like to write something like this:
class HttpErrorHandler extends SlimErrorHandler {
protected $responder;
public function __construct(
Responder $responder,
CallableResolverInterface $callableResolver,
ResponseFactoryInterface $responseFactory,
?LoggerInterface $logger = null) {
parent::__construct($callableResolver, $responseFactory, $logger);
$this->responder = $responder;
}
protected function respond(): Response {
$exception = $this->exception;
$errorCode = $exception->getCode();
$response = $this->responder->createResponse();
return $this->responder->render($response, 'myerror.twig', ['errorCode' => $errorCode]);
But I can not inject Responder this way. I get an error: Fatal error: Uncaught TypeError: Argument 1 passed to App\Handlers\HttpErrorHandler::__construct() must be an instance of App\Utils\Responder, instance of Slim\CallableResolver given,
I guest there is something I still not understand about injection.
According to your example (with Twig) you should implement your own default error handler for the Slim error middleware.
Example:
$container = $app->getContainer();
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler($container->get(DefaultErrorHandler::class));
Here is an example with Twig: DefaultErrorHandler
Thank for the help. It works now.
For some reasons, in case of 404 error, it does not go throught the middlewares, even if there are not route specified.
I wrote that code:
namespace App\Handlers;
use App\Utils\ExceptionDetail;
use App\Utils\Responder;
use DomainException;
use InvalidArgumentException;
use Selective\Validation\Exception\ValidationException;
use Slim\Exception\HttpException;
use Throwable;
use Psr\Log\LoggerInterface as Logger;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
/**
* Default Error Renderer.
* Inspired by https://github.com/odan/slim4-skeleton/blob/master/src/Handler/DefaultErrorHandler.php
*/
class DefaultErrorHandler {
const TEMPLATE = '/http_error.twig';
/**
* @var Responder
*/
private $responder;
/**
* @var Logger
*/
private $logger;
/**
* The constructor.
*
* @param Responder
* @param Logger
*/
public function __construct(Responder $responder, Logger $logger) {
$this->responder = $responder;
$this->logger = $logger;
}
/**
* Invoke.
*
* @param Request $request The request
* @param Throwable $exception The exception
* @param bool $displayErrorDetails Show error details
* @param bool $logErrors Log errors
*
* @return ResponseInterface The response
*/
public function __invoke(Request $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors): Response {
// Log error
if ($logErrors) {
$this->logger->error(sprintf(
'Error: [%s] %s, Method: %s, Path: %s',
$exception->getCode(),
ExceptionDetail::getExceptionText($exception),
$request->getMethod(),
$request->getUri()->getPath()
));
}
// Detect status code
$statusCode = $this->getHttpStatusCode($exception);
// Error message
$genericErrorMessage = $this->getErrorMessage($exception, $statusCode, $displayErrorDetails);
// Render twig template
$response = $this->responder->createResponse();
return $this->responder->render(
$response->withStatus($statusCode),
Self::TEMPLATE,
['statusCode' => $statusCode, 'genericErrorMessage' => $genericErrorMessage]);
}
/**
* Get http status code.
*
* @param Throwable $exception The exception
*
* @return int The http code
*/
private function getHttpStatusCode(Throwable $exception): int {
// Detect status code
$statusCode = 500;
if ($exception instanceof HttpException) {
$statusCode = (int)$exception->getCode();
}
if ($exception instanceof DomainException || $exception instanceof InvalidArgumentException) {
// Bad request
$statusCode = 400;
}
if ($exception instanceof ValidationException) {
// Unprocessable Entity
$statusCode = 422;
}
$file = basename($exception->getFile());
if ($file === 'CallableResolver.php') {
$statusCode = 404;
}
return $statusCode;
}
/**
* Get error message.
*
* @param Throwable $exception The error
* @param int $statusCode The http status code
* @param bool $displayErrorDetails Display details
*
* @return string The message
*/
private function getErrorMessage(Throwable $exception, int $statusCode, bool $displayErrorDetails): string {
// Default error message
$errorMessage = '500 Internal Server Error';
if ($statusCode === 403) {
$errorMessage = '403 Access denied. The user does not have access to this section.';
} elseif ($statusCode === 404) {
$errorMessage = '404 Not Found';
} elseif ($statusCode === 405) {
$errorMessage = '405 Method Not Allowed';
} elseif ($statusCode >= 400 && $statusCode <= 499) {
$errorMessage = sprintf('%s Error', $statusCode);
}
if ($displayErrorDetails === true) {
$errorMessage .= ' - Error details: ' . ExceptionDetail::getExceptionText($exception);
}
return $errorMessage;
}
}
Thx, Th.
Hi thierryler,
I’m facing the same issue now. Can you pls help me to resolve this?
I just copied your code and replaced DefaultErrorHandler.php. Now getting Undefined class ‘ExceptionDetail’. What is this Class?
its a custom class with a static method that returns the text message from the exception.
probably a class with something like
public static function getExceptionText(Exception $exception): string
{
return $exception->getMessage();
}
but it probably includes some extra details, because he could have used $exception->getMessage()
in the logger statement instead of ExceptionDetail::getExceptionText($exception)
.
TLDR; Just use $exception->getMessage()
where you are getting the error.
when I try to change return return $this->responder->render(
to return $this->view->render(
by adding Twig into this Middleware, getting errors like Uncaught Twig\Error\RuntimeError: Unable to load the "Slim\Views\TwigRuntimeExtension" runtime.
The reason why I change this is, Twig Template not rendering twig variables and blocks instead of printing it.
How to solve this?
Here is the code
<?php
declare(strict_types=1);
namespace App\Utils;
use Throwable;
/**
* Class ExceptionDetail.
* copied from https://github.com/odan/slim4-skeleton/blob/master/src/Utility/ExceptionDetail.php
*/
final class ExceptionDetail {
/**
* Get exception text.
*
* @param Throwable $exception Error
* @param int $maxLength The max length of the error message
*
* @return string The full error message
*/
public static function getExceptionText(Throwable $exception, int $maxLength = 0): string {
$code = $exception->getCode();
$file = $exception->getFile();
$line = $exception->getLine();
$message = $exception->getMessage();
$trace = $exception->getTraceAsString();
$error = sprintf('[%s] %s in %s on line %s.', $code, $message, $file, $line);
$error .= sprintf("\nBacktrace:\n%s", $trace);
if ($maxLength > 0) {
$error = substr($error, 0, $maxLength);
}
return $error;
}
}
?>
Can you help me to render the twig template using Twig View to use extends and blocks?
Error details mentioned above
Another issue while changing a few things getting Uncaught Twig\Error\LoaderError: Looks like you try to load a template outside configured directories
But I’m loading template from correct folder const TEMPLATE = '../resources/views/pages/404.twig';
Have you done something like that?
$settings = $container->get('settings');
$displayErrorDetails = $settings['displayErrorDetails'];
$logErrors = $settings['logErrors'];
$logErrorDetails = $settings['logErrorDetails'];
$errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, $logErrors, $logErrorDetails);
$errorMiddleware->setDefaultErrorHandler($container->get(DefaultErrorHandler::class));
and
<?php
declare(strict_types=1);
namespace App\Handlers;
use App\Beans\Usual;
use App\Constantes;
use App\Utils\ExceptionDetail;
use App\Utils\Responder;
use DomainException;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface as Logger;
use Selective\Validation\Exception\ValidationException;
use Slim\Exception\HttpException;
use Throwable;
/**
* Default Error Renderer.
* Inspired by https://github.com/odan/slim4-skeleton/blob/master/src/Handler/DefaultErrorHandler.php
*/
class DefaultErrorHandler {
const TEMPLATE = '/http_error.twig';
/**
* @var Responder
*/
private $responder;
/**
* @var Logger
*/
private $logger;
/**
* @var Usual
*/
private $usual;
/**
* The constructor.
*
* @param Responder
* @param Logger
*/
public function __construct(Responder $responder, Logger $logger, Usual $usual) {
$this->responder = $responder;
$this->logger = $logger;
$this->usual = $usual;
}
/**
* Invoke.
*
* @param Request $request The request
* @param Throwable $exception The exception
* @param bool $displayErrorDetails Show error details
* @param bool $logErrors Log errors
*
* @return ResponseInterface The response
*/
public function __invoke(Request $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors): Response {
$path = $request->getUri()->getPath();
// Log error
if ($logErrors) {
$this->logger->error(sprintf(
'Error: [%s] %s, Method: %s, Path: %s',
$exception->getCode(),
ExceptionDetail::getExceptionText($exception),
$request->getMethod(),
$path
));
}
$loggerUid = $this->getLoggerUid();
$lang = 'fr';
// Pour quand on aura d'autres langues
// if(strpos($path, '/fr') === 0) {
// $lang = 'fr';
// } else {
// // ...
// }
$this->usual->lang = $lang;
// Detect status code
$httpStatusCode = $this->getHttpStatusCode($exception);
// $errorCode = $this->getErrorCodeFromStatus($httpStatusCode);
// $this->usual->addErrorCode($errorCode);
// Error message
$genericErrorMessage = $this->getErrorMessage($exception, $httpStatusCode, $displayErrorDetails);
$genericErrorCode = $httpStatusCode;
// Render twig template
$response = $this->responder->createResponse();
return $this->responder->render(
$response,
Self::TEMPLATE,
[
'httpStatusCode' => $httpStatusCode,
'genericErrorMessage' => $genericErrorMessage,
'genericErrorCode' => $genericErrorCode,
'loggerUid' => $loggerUid,
],
$httpStatusCode);
}
// private function getErrorCodeFromStatus(int $statusCode): string {
// if(400 <= $statusCode && $statusCode <= 404) {
// return 'ERROR_' . $statusCode;
// } else if ($statusCode == 500) {
// return 'ERROR_500';
// } else {
// return 'ERROR_XXX';
// }
// }
/**
* Get http status code.
*
* @param Throwable $exception The exception
*
* @return int The http code
*/
private function getHttpStatusCode(Throwable $exception): int {
// Detect status code
$statusCode = 500;
if ($exception instanceof HttpException) {
$statusCode = (int)$exception->getCode();
}
if ($exception instanceof DomainException || $exception instanceof InvalidArgumentException) {
// Bad request
$statusCode = 400;
}
if ($exception instanceof ValidationException) {
// Unprocessable Entity
$statusCode = 422;
}
$file = basename($exception->getFile());
if ($file === 'CallableResolver.php') {
$statusCode = 404;
}
return $statusCode;
}
/**
* Get error message.
*
* @param Throwable $exception The error
* @param int $statusCode The http status code
* @param bool $displayErrorDetails Display details
*
* @return string The message
*/
private function getErrorMessage(Throwable $exception, int $statusCode, bool $displayErrorDetails): string {
// Default error message
$errorMessage = '500 Internal Server Error';
if ($statusCode === 403) {
$errorMessage = '403 Access denied. The user does not have access to this section.';
} elseif ($statusCode === 404) {
$errorMessage = '404 Not Found';
} elseif ($statusCode === 405) {
$errorMessage = '405 Method Not Allowed';
} elseif ($statusCode >= 400 && $statusCode <= 499) {
$errorMessage = sprintf('%s Error', $statusCode);
}
if ($displayErrorDetails === true) {
$errorMessage .= ' - Error details: ' . ExceptionDetail::getExceptionText($exception);
}
return $errorMessage;
}
protected function getLoggerUid(): string {
$loggerUid = 'unkonw';
try {
$loggerUid = $this->logger->getProcessors()[0]->getUid();
} catch(Exception $e) {
// rien
}
return $loggerUid;
}
}
and http_error.twig is a custom page:
{% extends 'fr/template.twig' %}
{% block content %}
<div class="container">
<style>
.http-error .status-code {
font-size: 7em;
color: red;
}
.http-error .buttons {
margin-top: 30px;
}
</style>
<h1>Error...</h1>
<div class="http-error">
<div class="status-code">{{ httpStatusCode }}</div>
<div class="message">
Un erreur s'est produite !
{% if genericErrorCode == 'CSRF' %}
Erreur technique HTTP #400-01
{% else %}
{{ genericErrorMessage }}
{% endif %}
</div>
<div class="buttons">
<a href="{{ url_for('home', {'lang': 'fr'}) }}" class="btn btn-primary">Retour à l'accueil</a>
</div>
</div>
</div>
{% endblock %}
Thank you so much for your reply.
What code is inside this App\Utils\Responder
?
Mine is App\Responder\Responder
and this doesn’t have a render function,
but this has php render instead. That’s why the Twig template not rendering as the above image.
<?php
namespace App\Responder;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Slim\Interfaces\RouteParserInterface;
use Slim\Views\PhpRenderer;
use function http_build_query;
/**
* A generic responder.
*/
final class Responder
{
private PhpRenderer $phpRenderer;
private RouteParserInterface $routeParser;
private ResponseFactoryInterface $responseFactory;
/**
* The constructor.
*
* @param PhpRenderer $phpRenderer The template engine
* @param RouteParserInterface $routeParser The route parser
* @param ResponseFactoryInterface $responseFactory The response factory
*/
public function __construct(
PhpRenderer $phpRenderer,
RouteParserInterface $routeParser,
ResponseFactoryInterface $responseFactory
) {
$this->phpRenderer = $phpRenderer;
$this->responseFactory = $responseFactory;
$this->routeParser = $routeParser;
}
/**
* Create a new response.
*
* @return ResponseInterface The response
*/
public function createResponse(): ResponseInterface
{
return $this->responseFactory->createResponse()->withHeader('Content-Type', 'text/html; charset=utf-8');
}
/**
* Output rendered template.
*
* @param ResponseInterface $response The response
* @param string $template Template pathname relative to templates directory
* @param array $data Associative array of template variables
*
* @return ResponseInterface The response
*/
public function withTemplate(ResponseInterface $response, string $template, array $data = []): ResponseInterface
{
return $this->phpRenderer->render($response, $template, $data);
}
/**
* Creates a redirect for the given url / route name.
*
* This method prepares the response object to return an HTTP Redirect
* response to the client.
*
* @param ResponseInterface $response The response
* @param string $destination The redirect destination (url or route name)
* @param array $queryParams Optional query string parameters
*
* @return ResponseInterface The response
*/
public function withRedirect(
ResponseInterface $response,
string $destination,
array $queryParams = []
): ResponseInterface {
if ($queryParams) {
$destination = sprintf('%s?%s', $destination, http_build_query($queryParams));
}
return $response->withStatus(302)->withHeader('Location', $destination);
}
/**
* Creates a redirect for the given url / route name.
*
* This method prepares the response object to return an HTTP Redirect
* response to the client.
*
* @param ResponseInterface $response The response
* @param string $routeName The redirect route name
* @param array $data Named argument replacement data
* @param array $queryParams Optional query string parameters
*
* @return ResponseInterface The response
*/
public function withRedirectFor(
ResponseInterface $response,
string $routeName,
array $data = [],
array $queryParams = []
): ResponseInterface {
return $this->withRedirect($response, $this->routeParser->urlFor($routeName, $data, $queryParams));
}
/**
* Write JSON to the response body.
*
* This method prepares the response object to return an HTTP JSON
* response to the client.
*
* @param ResponseInterface $response The response
* @param mixed $data The data
* @param int $options Json encoding options
*
* @return ResponseInterface The response
*/
public function withJson(
ResponseInterface $response,
$data = null,
int $options = 0
): ResponseInterface {
$response = $response->withHeader('Content-Type', 'application/json');
$response->getBody()->write((string)json_encode($data, $options));
return $response;
}
}
If you can share the Responder dependency details. That may enough to solve this issue.
Can you share this dependency details?
Actually, I will need to send an entire project to explain. I might send you to an article we wrote (in french).