Currently I’m upgrading an application from Slim3 to Slim4.
With Slim3 I injected the container into all of my controllers, where with Slim4 I redesigned everything to dependency injection. The app runs now partly with Slim4 and I noticed, that the same site has significant longer loading time in Slim4 than in Slim3. For example 3.5s instead of less than 1s (according to chrome dev tools).
After some research I realized, that the instantiation of Monolog caused the long loading time. I use Monolog to write to a log file and also to send an email with the error to the admin. In Slim3 I registered the logger in the container.
//Slim Error abfangen
$container['errorHandler'] = function ($c) {
return function ($request, $response, $error) use ($c) {
$c->logger->error($error->getMessage(), ['Seite' => $request->getUri()->getScheme() . '://' . $request->getUri()->getHost() . $request->getUri()->getPath()]);
return $c->view->render($response->withStatus(500), 'error.twig');
};
};
//PHP Error abfangen
$container['phpErrorHandler'] = function ($c) {
return $c->errorHandler;
};
//404 notFoundHandler überschreiben mit eigener Seite
$container['notFoundHandler'] = function ($c) {
return function ($request, $response) use ($c) {
return $c->view->render($response->withStatus(404), '404.twig');
};
};
//405 notAllowedHandler überschreiben mit eigener Seite
$container['notAllowedHandler'] = function ($c) {
return function ($request, $response) use ($c) {
return $response->withRedirect($c->router->pathFor('home'));
};
};
In Slim3 the error handler and so also Monolog were only instantiated in case of an error.
if ($this->container->has($handler)) {
$callable = $this->container->get($handler);
// Call the registered handler
return call_user_func_array($callable, $params);
}
The Problem in Slim4 is, that if I inject the logger into the error handler, Monolog is instantiated on every request.
So I had a look at slim4-skeleton and at Daniels Blog Post about monolog.
In slim4-skeleton I just called the index page and had in my test environment a loading time of 1.3s. If I didn’t load the ErrorMiddleware, I had a loading time of about 0.5s. I had the same result if I just didn’t load the logger (commented out in DefaultErrorHandler and in container.php).
So I searched for a solution to only instantiate Monolog if an error occurs. I made a LoggerFactory as suggested by Daniel (@odan) . Then I inject this LoggerFactory in my ErrorHandler, but don’t call the createInstance() function in the constructor. So Monolog isn’t loaded. I call $this->loggerFactory->createInstance() in the logError() function, so it’s only called in case if really something has to be logged.
<?php
namespace App\Handlers;
use Psr\Http\Message\ResponseInterface;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpException;
use Slim\Exception\HttpForbiddenException;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpNotImplementedException;
use Slim\Exception\HttpUnauthorizedException;
use Slim\Handlers\ErrorHandler;
use Exception;
use Psr\Http\Message\ResponseFactoryInterface;
use Slim\Interfaces\CallableResolverInterface;
use Slim\Views\Twig;
use Throwable;
use App\Factory\LoggerFactory;
class HttpErrorHandler extends ErrorHandler
{
public const BAD_REQUEST = 'BAD_REQUEST';
public const INSUFFICIENT_PRIVILEGES = 'INSUFFICIENT_PRIVILEGES';
public const NOT_ALLOWED = 'NOT_ALLOWED';
public const NOT_IMPLEMENTED = 'NOT_IMPLEMENTED';
public const RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND';
public const SERVER_ERROR = 'SERVER_ERROR';
public const UNAUTHENTICATED = 'UNAUTHENTICATED';
protected $view;
protected $loggerFactory;
public function __construct(Twig $view,
CallableResolverInterface $callableResolver,
ResponseFactoryInterface $responseFactory,
LoggerFactory $loggerFactory)
{
parent::__construct($callableResolver, $responseFactory);
$this->view = $view;
$this->loggerFactory = $loggerFactory;
}
protected function respond(): ResponseInterface
{
$exception = $this->exception;
$statusCode = 500;
$type = self::SERVER_ERROR;
$description = 'An internal error has occurred while processing your request.';
if ($exception instanceof HttpException) {
$statusCode = $exception->getCode();
$description = $exception->getMessage();
if ($exception instanceof HttpNotFoundException) {
$type = self::RESOURCE_NOT_FOUND;
} elseif ($exception instanceof HttpMethodNotAllowedException) {
$type = self::NOT_ALLOWED;
} elseif ($exception instanceof HttpUnauthorizedException) {
$type = self::UNAUTHENTICATED;
} elseif ($exception instanceof HttpForbiddenException) {
$type = self::UNAUTHENTICATED;
} elseif ($exception instanceof HttpBadRequestException) {
$type = self::BAD_REQUEST;
} elseif ($exception instanceof HttpNotImplementedException) {
$type = self::NOT_IMPLEMENTED;
}
}
if (
!($exception instanceof HttpException)
&& ($exception instanceof Exception || $exception instanceof Throwable)
&& $this->displayErrorDetails
) {
$description = $exception->getMessage();
}
$error = [
'statusCode' => $statusCode,
'error' => [
'type' => $type,
'description' => $description,
],
];
if($statusCode < 500){
$payload = $this->view->fetch('404.twig');
} else {
$payload = $this->view->fetch('error.twig');
}
$response = $this->responseFactory->createResponse($statusCode);
$response->getBody()->write($payload);
return $response;
}
protected function writeToErrorLog(): void
{
$renderer = $this->callableResolver->resolve($this->logErrorRenderer);
$error = $renderer($this->exception, $this->logErrorDetails);
if (!$this->displayErrorDetails) {
$error .= "\nTips: To display error details in HTTP response ";
$error .= 'set "displayErrorDetails" to true in the ErrorHandler constructor.';
}
$this->logError($error);
}
protected function logError(string $error): void
{
$this->logger = $this->loggerFactory->createInstance();
$this->logger->error($error, ['Seite' => $this->request->getUri()->getScheme() . '://' . $this->request->getUri()->getHost() . $this->request->getUri()->getPath()]);
}
}
This seems to work and I have loading times comparable to Slim3 in my Application.
What do you think about this? Have you better or nicer ideas how to handle this?