Trying to integrate Symfony/Translation into Slim 4

I am trying to integrate the use of the Symfony Translate component into Slim 4 to create a multi-lingual application. I am facing a problem with setting the locale in the container and then retrieving it with the Twig View with the proper translation resource.

I have created a custom routing middleware that is supposed to capture the locale and store it in the container.

        $path = $request->getUri()->getPath();
        $params = $path === '/' ? [] : explode('/', substr($path, 1));

        // check route for allowed locales
        if (count($params) > 0 && in_array(strtolower($params[0]), $this->container->get('allowed_locales'))) {
            $route->locale = strtolower(array_shift($params));
            $this->container->set('locale', $route->locale);
        }

I have created a definition in container to initialize the Twig View with Translation component:

            'view' => function (ContainerInterface $container) {
                return TwigFactory::createWith($container);
            }

The TwigFactory:

class TwigFactory
{
    public static function createWith(ContainerInterface $container): Twig
    {
        $locale = $container->get('locale');
        $twig = Twig::create($twig_path, $twig_options);

        $resourcePath = APPROOT . "/translations";
        $translator = new Translator($locale, new MessageFormatter(new IdentityTranslator()));
        $translator->setFallbackLocales(['en']);
        $translator->addLoader('yaml', new YamlFileLoader());
        if ( $locale !== 'en') {
            $translator->addResource('yaml', "{$resourcePath}/messages.{$locale}.yaml", $locale, 'messages');
        }
        $translator->addResource('yaml', "{$resourcePath}/messages.en.yaml", 'en', 'messages');
        $twig->addExtension(new TranslationExtension($translator));

        if ($twig_options['debug']) {
            $twig->addExtension(new DebugExtension());
        }

        return $twig;
    }
}

In my controller, when the template is to be rendered:

        $view = $this->container->get('view');
        return $view->render($response, 'page.index.twig', $data);

The translation loaded is always English (en). It seems like the middleware is not setting the different locale in the container, or the definition is being processed by the container before the middleware has set the locale.

What am I doing wrong here?

PS: any input on how to use the TwigExtractor in Symfony Translation component in a Slim 4 implementation will be appreciated as well.

Note that a DI container definition is not request specific, whereas a middleware (handler) is request specific. The language context depends on the request and should therefore be changed within a middleware and not within the DI container.

Your point make sense to me conceptually.

But, the general approach documented for Twig Templating is that it is a dependency and should be acquired through the container.

The translation extension is a Twig Extension, i.e. a component of a dependency.

Are you suggesting that even though the Translation component is loaded as a Twig Extension within the dependency in the container, the locale related resources should be injected into dependency within the request context?

Since not all requests will generate view, using a middleware does not seem suitable.

But then this implies that locale related resource loading to the view needs to happen in the Controller context. This seems to me like a standardized and repetitive task being handled separately in every container. The does not seem like a very component-oriented approach to me.

On my initial study of this requirement, it seemed to me that the “lazy loading” concept within the DI container was well suited to this requirement. But for some reason in my code it is not lazy loading Twig from the container. The Twig object is being created before the middleware runs. This does not seem to match the PHP-DI Documentation. Based on this documentation, this definition

'view' => function (ContainerInterface $container) {
                return TwigFactory::createWith($container);
          }

should lazy-load Twig when I execute the $this->container->get('view') in the Controller. But in my tests, the twig object is being loaded first before the middleware and controller are executed.

Do I need to specifically enable lazy loading?

From the documentation

From the lowest priority to the highest:

  • autowiring if enabled
  • attributes if enabled
  • PHP definitions (file or array) in the order they were added
  • definitions added straight in the container with $container->set()

You could add twig view like this

...
$container->set('view', function () {
    return Twig::create(__DIR__ . '/path/to/views/', [
        'cache' => false
    ]) ;
});

Then call it in your base controller

<?php

use Psr\Container\ContainerInterface;

class Controller
{
    public function __construct(protected ContainerInterface $container)
    {}

    public function __get($name)
    {
        return $this->container->get($name);
    }
}

Finally you can extends that base controller

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

class UserController extends Controller
{
    public function show(Request $request, Response $response): Response
    {
        return $this->view->render($response, 'user.twig');
    }
}

Thank you for the suggestion of having a base controller that all controllers can extend from.

I have also learned that my impression that the documentation recommends using Twig as primarily a container dependency was wrong (apologies for that response).

https://www.slimframework.com/docs/v4/features/templates.html

In fact, by using the TwigMiddleware, the view object is added as an attribute to the Request object, and the container is only used by the Middleware to get the Twig component.

@odan would you recommend that I extend the TwigMiddleware to include the locale aware Translation component into the View object in the Request? I would want the TwigMiddleware to execute before the addition of the Translation component to the view.

You don’t have to “lazy load” these dependencies. You can configure the dependencies within the DI container, but to change the language (local) of the “Translator” instance you need to call the the setLocal method of the Translator instance within a middleware that is aware of the HTTP request specific context (language of the user). You don’t need to implement this within a controller or base-controller class, because a middleware is a good approach for this use-case.

To get access to the Translator instance within a middleware via dependency injection, a dedicated DI container is needed. Example:

use Psr\Container\ContainerInterface;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Formatter\MessageFormatter;
use Symfony\Component\Translation\IdentityTranslator;
use Symfony\Component\Translation\Loader\MoFileLoader;
use Slim\Views\Twig;
// ...
return [
  // ...

    Translator::class => function (ContainerInterface $container) {
        $translator = new Translator(
            'en_US',
            new MessageFormatter(new IdentityTranslator())
        );

        $translator->addLoader('mo', new MoFileLoader());

        return $translator;
    },
];

Then, you can declare the Translator as dependency within your custom “TranslatorMiddleware” constructor and change the language using the “setLocal” method. Also make sure, to use the same “Translator” instance within your “Twig::class” DI container definition.

Example middleware:

<?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 Symfony\Component\Translation\Translator;

final class TranslatorMiddleware implements MiddlewareInterface
{
    private Translator $translator;

    public function __construct(Translator $translator)
    {
        $this->translator = $translator;
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Detect user language from request or session etc.
        // ...
        $locale = 'en_US';

        // Set language
        $this->translator->setLocale($locale);

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

Thank you… putting the Translator instance into the container is an excellent approach.

This then allows me to use the TwigMiddleware with a locale aware Middleware as follows:

Application Boot (order is important so that Twig is available in the Request)

 $app->add(new TranslatorMiddleware($app));
 $app->add(TwigMiddleware::create($app, $container->get('twig')));

TranslatorMiddleware

$twig = Twig::fromRequest($request);
$this->translator->setLocale($request->getLocale());
$twig->addExtension(new TranslationExtension($this->translator));

$request = $request->withAttribute('view', $twig);
return $handler->handle($request);

Then in any Controller

$view = Twig::fromRequest($request);

This remains consistent with the approach in the Documentation.
I appreciate everyone’s help here in getting this structured… thank you!