LoggerFactory used outside route controller

Hi All,
I just started my adventure with Slim and I feel I get lost a bit.
My page controller looks like this:

<?php
namespace App\Action;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
use Slim\Routing\RouteContext;
use App\Factory\LoggerFactory;
use App\Factory\Translations;


final class Dashboard
{
  private $lang;
  private $twig;
  private $i18n;

  public function __construct(Twig $twig, Translations $i18n, LoggerFactory $logger) {

    $this->twig = $twig;
    $this->i18n = $i18n;
    $this->lang = $this->i18n->get_locale();

    $this->logger = $logger
      ->addFileHandler('application.log')
      ->createLogger();

  }

  public function __invoke(Request $request, Response $response) {

    $routeContext = RouteContext::fromRequest($request);
    $route = $routeContext->getRoute();

    $this->i18n->getTranslations($route->getName(), $this->lang);

    $this->logger->info('This information is going to be stored in the log file');

    $view_data = [ ... ];

    return $this->twig->render($response, '/page/dashboard.twig', $view_data);
  }
}
?>

I have a few questions around that code:

  1. Is there a more elegant way to get the route name? My code works but I feel there is a better way.
  2. Logger → ideally it should also log request data, like: method, route, http code. Is there a way that the logger will take that information automatically so I do not have to pass it on each controller? I would like to use Logger in the other classes (for example in Translations, that is auto-wired) that is why I’m after gathering request data by the Logger automatically.

Can someone point me out the direction? Any tips/hints are more than appreciated.

Thanks.

Welcome @qpon :slight_smile:

I will directly address your questions:

  1. This is the correct way to fetch the route from the request.
 $routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
$this->i18n->getTranslations($route->getName(), $this->lang);

BUT I see what you are trying to implement here and in this case, yes there is a “better” way to initialize the translation for each request. I would move this 3 lines into a “TranslationMiddleware” and add it to your global middleware stack.

  1. Also this logging functionality should be moved into its own e.g. “LoggerMiddleware”.
$this->logger->info('This information is going to be stored in the log file');

Then your single action controller only needs to implement what it needs to do and becomes very lean.

1 Like

Hi @odan,
many thanks for this tip around translations and middleware. I have added a new middleware that does the trick for me.
Now, from the action controller I’m able to get translations I want with a single line, i.e:
var_dump($request->getAttribute('i18n'));
what is just awesome.

For others, as they may be interested in similiar solution:

Container:

...

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

    return new Translations($settings['default_locale']);
  },

  TranslationMiddleware::class => function (ContainerInterface $container) {

    return TranslationMiddleware::create(
      $container->get(App::class),
      Translations::class
    );
  },

A single line to a Middleware.php stack was added:
$app->add(TranslationMiddleware::class);

MiddlewareTranslation.php

namespace App\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Interfaces\RouteParserInterface;
use Slim\Routing\RouteContext;
use RuntimeException;
use App\Factory\Translations;
use Slim\App;

class TranslationMiddleware implements MiddlewareInterface {

  protected $i18n;

  public static function create(App $app, string $containerKey = 'i18n'): self
  {
      $container = $app->getContainer();
      if ($container === null) {
          throw new RuntimeException('The app does not have a container.');
      }
      if (!$container->has($containerKey)) {
          throw new RuntimeException(
              "The specified container key does not exist: $containerKey"
          );
      }

      $tr = $container->get($containerKey);
      if (!($tr instanceof Translations)) {
          throw new RuntimeException(
              "Translations instance could not be resolved via container key: " . $containerKey
          );
      }

      return new self(
        $tr
      );
  }

  public function __construct(Translations $tr) {
      $this->i18n = $tr;
  }

  public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
  {
      $routeContext = RouteContext::fromRequest($request);
      $route = $routeContext->getRoute();

      $phrases = $this->i18n->getTranslations($route->getName(), $this->i18n->get_locale());

      $request = $request->withAttribute('i18n', $phrases);

      return $handler->handle($request);
  }
}

However, I think I need to better describe my need with the logger in order to get a further piece of an advice from you. The objective I have against the logger is simple: write all important information about the application state throughout its entire lifecycle. So I would like to use info(), warning() and other functions in many different places, logging contextaul details as well as the same information regardless where logger is used: (request method, http code, route etc.).

Would you still also create a middleware for the logger and create an instance of the logger in the process() method, where you also define filehandler and push processors? So something similar to this:

public function __construct(LoggerFactory $l) {
      $this->factory = $l;
  }


  public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
  {
      $routeContext = RouteContext::fromRequest($request);
      $route = $routeContext->getRoute();

      $this->logger = $this->factory
        ->addExtras($route) // to extract all pieces I want and then use pushProcessor()
        ->addFileHandler('application.log')
        ->createLogger();

      $request = $request->withAttribute('log', $this->logger);

      return $handler->handle($request);
  }

Is this the way forward? Your 5 cents are more than welcome :slight_smile:

It depends on what you want to log.

If you only want to log all HTTP-specific requests, then a simple Apache/Nginx logfile or a custom PHP middleware would be good for those specific tasks. Note that the logfiles of the web server are of course much more performant.

However, if you want to log the details of your business logic, you’d better create a context-specific logger instance within your specific domain classes (services).

I’m more after business logic logging. The use case I have on mind is:
In my twig template I may refer to a phrase that does not exist in the translation file (is not defined). I would like to report that fact in the log. For this specific case, just to name a few valuable parts of information:

  • channel (i can cover that with logger easily)
  • phrase which was not found (easy to cover)
  • route (crucial bit, as this indicates the file in which translation was missed)

Sticking to the route bit, I can provide this information manually (like in the first post), by adding extra 3 lines. If I think about this in a way that each service / action I will create (hundreds) will have those 3 extra lines then I feel it is going to be a waste. I’m fine with context-specific logger but still the million dollar question is: how to avoid 3 extra lines per each service / action to get some of the information valueable from the trail perspective?

In all examples I saw on git the logger is instantialize in the controller constructor (makes sense because of the auto-wiring). As that happens in the constructor, I have no access to the request variable. Maybe I dig to far while being afraid of the 3 extra lines. If there is a way to avoid repeats, please let me know if you see such.

Many thanks for the great support showed so far. Much appreciated!

I do not know the specific requirements of your application. In general, you could extract that general code into a new class and declare it as dependency (constructor dependency injection) where you need it. This (translation?) class could also create (or declare) a logger instance, that logs all invalid or missing translation keys. So you have moved all this “translation” logic into this specific class.

2 Likes