yiisoft / request-model

https://www.yiiframework.com/
BSD 3-Clause "New" or "Revised" License
15 stars 9 forks source link

Runtime formatter with translator #52

Open razonyang opened 2 years ago

razonyang commented 2 years ago

Hi, I would like to format the error message with runtime Yii translator, a middleware that store the translator instance into request attributes, according to the Accept-Language header or URL query parameters.

I extend the RequestModel class like this:

<?php
// Custom RequestModel
abstract class RequestModel extends BaseModel implements RulesProviderInterface
{
    private ?TranslatorInterface $translator = null;

    public function __construct(
        private TranslatorServiceInterface $translatorService,
    )
    {
    }

    public function getTranslator(): TranslatorInterface
    {
        if (!$this->translator) {
            $this->translator = $this->translatorService->fromAttributes($this->getRequestData()['attributes']);
        }

        return $this->translator;
    }
}
<?php
// Custom Formatter
class Formatter implements FormatterInterface
{
    public function __construct(
        private TranslatorInterface $translator
    )
    {
    }

    public function format(string $message, array $parameters = []): string
    {
        return $this->translator->translate($message, $parameters);
    }
}

Is there any way to hack the runtime formatter into rules? The formatter is belongs to RuleHandler, not Rule, so I could not do it in the getRules function.

public function getRules(): array
{
    return [
        'body.login' => [
            new Required(),
        ],
        'body.password' => [
            new Required(),
            new HasLength(6, 16),
        ],
    ];
}
xepozz commented 2 years ago

Why wouldn't you use setLocale()?

razonyang commented 2 years ago

Why wouldn't you use setLocale()?

Sorry, I didn't notice that, is this method can be used in RoadRunner and Swoole safely?

xepozz commented 2 years ago

Why wouldn't you use setLocale()?

Sorry, I didn't notice that, is this method can be used in RoadRunner and Swoole safely?

I haven't checked it, but seems it may occur problems. @yiisoft/yii3 has anyone checked this case?

darkdef commented 2 years ago

In RR better use withLocale https://github.com/yiisoft/translator#get-a-new-translator-instance-with-a-locale-to-be-used-by-default-in-case-locale-isnt-specified-explicitly

razonyang commented 2 years ago

Hi @xepozz, I just tested it in RR and Swoole, the RR seems fine with the setLocale, but Swoole does not.

I guess requests are blocking in RR single worker, the next request won't be handled until the current request was done. And Swoole Coroutine won't blocking requests, if the current request is suspend, other requests will be handled. The DI resetter isn't useful in this case.

I wrote a simple test case as the following.

image

The response will be affected by other requests.

<?php
// Middleware
class TranslatorMiddleware implements MiddlewareInterface
{
    public function __construct(
        private TranslatorInterface $translator,
    ) {
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $this->translator->setLocale($request->getQueryParams()['locale']);
        sleep(5);
        return $handler->handle($request);
    }
}
<?php
// Controller
class InfoController
{
    public const TITLE = 'New Tab API';

    public const VERSION = '1.0.0';

    public function index(DataResponseFactoryInterface $responseFactory, TranslatorInterface $translator): ResponseInterface
    {
        return $responseFactory->createResponse([
            'title' => self::TITLE,
            'version' => self::VERSION,
            't' => $translator->translate('home'),
        ]);
    }
}

As the image shown, the Application Runner does invoke the StateResetter::reset method to reset the locale per request.

<?php
    private function afterRespond(
        Application $application,
        ContainerInterface $container,
        ?ResponseInterface $response,
    ): void {
        $application->afterEmit($response);
        /** @psalm-suppress MixedMethodCall */
        $container
            ->get(StateResetter::class)
            ->reset(); // We should reset the state of such services every request.
        var_dump('reset service state');
        gc_collect_cycles();
    }

I'm not sure am I test it in a right way.

The HTTP server is running with Swoole 5 Coroutine

razonyang commented 2 years ago

Hi, I forked the yiisoft/app-api and created the swoole branch for checking this case.

Changes can be found at https://github.com/razonyang/yii-app-demo/commit/13300e05cafe016785e439c92ef81f36308c7e76 and https://github.com/razonyang/yii-app-demo/commit/d1e9ccb58e56b18dfef27cb4b7e1a53533429b7e Swoole runnner adapter https://github.com/razonyang/yii-runner-swoole

Steps for testing:

$ git clone -b swoole git@github.com:razonyang/yii-app-demo.git

$ cd yii-app-demo

$ docker-compose up --build -d php

$ docker-compose exec php bash
root@***:/app# composer install
root@***:/app# exit

$ docker-compose up --build -d swoole

And then make some concurrent requests.

$ curl "http://localhost:9501?locale=zh-CN"

$ curl "http://localhost:9501?locale=en-US"

I think it would be better if we are able to use withLocale instead of setLocale, and then inject the runtime translator into Rule Handlers.