Possible to dynamically change the ErrorHandler's logger? (Slim 4)

Here’s the setup (and maybe someone can offer an alternative to my current system):

Application Middleware (added in this order, so executed in reverse):

  1. ErrorMiddleware (w/ custom ErrorHandler)
  2. RoutingMiddleware
  3. CustomMiddleware (determines api version and therefore which directory/files to include)

Reasoning:
I’m adding RoutingMW after the ErrorMW so when they run in reverse I have access to any routing info during my error middleware/handler. Also, I don’t know why I can’t put my CustomMW first and therefore run it last (it throws a status 500, and I can’t seem to debug it).

When I set my ErrorMW, I also create my custom ErrorHandler and pass it to the ErrorMW as the default handler. I want the ErrorHandler to have a logger, so I set the DI container’s logger to write into my “lastest api version” directory (say, /logs/v2.0/api-log), and inject that into the ErrorHandler. When the app runs and finally determines the right API version, I overwrite the DI container’s logger with the chosen API version for the log file directory, and then inject that logger in my various controllers as needed. If the user is running an old app and needs an older API like 1.0, the application will log non-errors to the correct /logs/v1.0/api-log, but the ErrorHandler will still reference the old Logger I gave it during the application Middleware setup (/logs/v2.0/api-log), so any errors are written there instead.

Is there a way, during the application, to change the custom ErrorHandler’s logger and set it to the correct 1.0 path?

Hope that made sense,
Thanks!

Note that in Slim the middleware stack is LIFO (last in, first out). The middleware you add last is executed first. So the order is “reversed”.

I think the ErrorMiddleware should be the last middleware in the stack to catch all exceptions of your application. So the Routing middleware should be added before the ErrorMiddleware. And the CustomMiddleware should be added before the Routing middleware.

Example:

$app->add(CustomMiddlware::class);
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);

When you want to configure a custom Logger for the ErrorMiddleware you may add a DI container definition instead and pass your custom logger there.

Add a new container defintion:

use Psr\Container\ContainerInterface;
use Slim\App;
use Slim\Middleware\ErrorMiddleware;
// ...

return [
    App::class => function (ContainerInterface $container) {
        AppFactory::setContainer($container);
    
        return AppFactory::create();
    },

    ErrorMiddleware::class => function (ContainerInterface $container) {
        $settings = $container->get('settings')['error'];
        $app = $container->get(App::class);

        // Create a custom logger here
        $logger = ...;

        $errorMiddleware = new ErrorMiddleware(
            $app->getCallableResolver(),
            $app->getResponseFactory(),
            (bool)$settings['display_error_details'],
            (bool)$settings['log_errors'],
            (bool)$settings['log_error_details'],
            $logger
        );

        return $errorMiddleware;
    },
];

Then replace addErrorMiddleware with the ErrorMiddleware class to let the DI container inject the ErrorMiddleware:

use Slim\Middleware\ErrorMiddleware;

// ...
$app->add(ErrorMiddleware::class);

PS: You also need to fetch the App instance from the DI container as well then.

// Create App instance
$app = $container->get(App::class);

// ...

$app->run();

Hi there, thanks for the reply.
I’m not understanding your first block of code. I usually do:

container->set(MyClass::class, function($container){
    //...
});

Also, I don’t understand… why do you add the App to the container? Also, I am aware of the order of execution of the middleware, and that’s why I specifically stated the reasoning behind the error middleware not being last: I want the routing info if an exception is thrown, and that data would not be available yet. Finally, I already said that I tried putting my custom middleware first, but I can’t figure out what the problem is yet.

Your solution doesn’t work for me because the settings needed to create the logger (namely, the api version, which is used to build the path where the logs are written to) are determined after all middleware get executed.

Anyway, I already figured out a solution that works for me.

$myErrorHandler = new MyErrorHandler($app->getCallableResolver(), $app->getResponseFactory(), null, $container);

$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler($myErrorHandler);

I basically inject, in MyErrorHandler, the $container rather than my “old logger”. This way when MyErrorHandler’s __invoke() is called, it can dynamically retrieve the correct logger by calling $container->get('my_correct_api_version_logger')

I don’t know, this might be a silly approach (I’m cringing at the thought of passing the $container to the ErrorHandler), but it’s working at least. However, this is making me rethink my need for a dynamic logger. Maybe logging separate api versions into one file isn’t such a bad idea. Thoughts?

Yes, passing the container should be avoided when possible.
Another approach would be a routing group for each API version and then add a custom LoggerVersion1Middleware and LoggerVersion2Middleware to each of this routing group.
The LoggerVersionXMiddleware then would configure the Slim ErrorMiddleware with a custom ErrorHandler. $errorMiddleware->setDefaultErrorHandler($myErrorHandler);.

The error middleware, once instantiated, will handle errors for the entire lifecycle of the app. It’s best if it’s up and running as soon as possible. Assuming the request object is available where an exception occurs you can access a RouteContext to provide routing info.

@odan How do I get access to the $errorMiddleware /its errorHandler from a route group? That was, in essence, the root of my original issue – I couldn’t change the errorHandler once I set it up in the application middleware.

Thanks again for the suggestions!!

By the way: this is the error I get when I do your suggested order of application middleware (custom, routing, error - added in that order)

404 Not Found
The application could not run because of the following error:

Type: Slim\Exception\HttpNotFoundException
Code: 404
Message: Not found.
File: [...]/composer/vendor/slim/slim/Slim/Middleware/RoutingMiddleware.php
Line: 91

Commenting out the middle one (Routing, aka $app->addRoutingMiddleware()), or swapping it with my custom middleware, makes my request go through properly.

I just read this in the docs:

If you were using determineRouteBeforeAppMiddleware, you need to add the Middleware\RoutingMiddleware middleware to your application just before your call run() to maintain the previous behaviour.

But I don’t even know what determineRouteBeforeAppMiddleware is, I’ve never seen such a setting, so I’m confused.

@dafriend Moving the error middleware last, I get the following error when manually throwing an exception in my first GET route: Cannot create RouteContext before routing has been completed. My custom error handler simply tries to to access \Slim\Routing\RouteContext::fromRequest($request), but it fails with that error. That’s why I ended up placing the Routing middleware last.

P.S. I just realized that I had a weird setup: my actual routes were technically inside my custom application middleware. This middleware would create a $api_version based on user input, and then I would require_once "path/to/{$api_version}/api/routes.php". To try something, I moved out the require_once "[...]/routes.php" and put it after the three application middleware and before the $app->run(), and then I finally stopped getting the 404 error above when placing my custom application middleware before the routing.
So, I think I’m doing my versioning routing wrong. How can I not hardcode a specific api version on my client app, say v1, yet dynamically choose which routes.php file to include? I tried using the DI container to store the chosen $api_version (when inside my custom application middleware), and then read it like so $this->getContainer()->get('app_version') inside a route group, but that doesn’t work inside a group.
Any suggestions on the best way to version or simply to include the right version of my routes?

I feel like this post went a bit off-topic, but I think it’s revealing the deeper source of my issue. Thanks! :smiley: