How to setup a 404 action/view in Slim 4?

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 :frowning:

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.

1 Like

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.

1 Like

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;
  }
}

?>
1 Like

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 %}
1 Like

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).