How can i translate OptionalInputFilter validator messages in Mezzio?

When i use OptinalInputFilter class, the translate object does not translate the validations errors.

i run the below the codes.

<?php

namespace App\Filter;

use App\Validator\Db\RecordExists;
use App\Validator\Db\NoRecordExists;
use Laminas\Filter\StringTrim;
use Laminas\Validator\Uuid;
use Laminas\InputFilter\OptionalInputFilter;
use Laminas\Validator\StringLength;
use Laminas\Db\Adapter\AdapterInterface;
use Laminas\InputFilter\InputFilterPluginManager;

class WorkplaceSaveFilter extends InputFilter
{
    public function __construct(
        AdapterInterface $adapter,
        InputFilterPluginManager $filter
    )
    {
        $this->filter = $filter;
        $this->adapter = $adapter;
    }

    public function setInputData(array $data)
    {
        $this->add([
            'name' => 'id',
            'required' => true,
            'validators' => [
                ['name' => Uuid::class],
                [
                    'name' => HTTP_METHOD == 'POST' ? NoRecordExists::class : RecordExists::class,
                    'options' => [
                        'table'   => 'workplaces',
                        'field'   => 'workplaceId',
                        'adapter' => $this->adapter,
                    ]
                ]
            ],
        ]);
        $companyFilter = new OptionalInputFilter();
        $companyFilter->add([
            'name' => 'id',
            'required' => true,
            'validators' => [
                ['name' => Uuid::class],
                [
                    'name' => RecordExists::class,
                    'options' => [
                        'table'   => 'companies',
                        'field'   => 'companyId',
                        'adapter' => $this->adapter,
                    ]
                ]
            ],
        ]);
        $this->add($companyFilter, 'companyId');
        $this->add([
            'name' => 'workplaceName',
            'required' => true,
            'filters' => [
                ['name' => StringTrim::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'encoding' => 'UTF-8',
                        'min' => 3,
                        'max' => 255,
                    ],
                ],
            ],
        ]);
        $this->add([
            'name' => 'registrationNumber',
            'required' => false,
            'filters' => [
                ['name' => StringTrim::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'encoding' => 'UTF-8',
                        'min' => 3,
                        'max' => 100,
                    ],
                ],
            ],
        ]);
        $this->add([
            'name' => 'address',
            'required' => false,
            'filters' => [
                ['name' => StringTrim::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'encoding' => 'UTF-8',
                        'min' => 3,
                        'max' => 255,
                    ],
                ],
            ],
        ]);
        // Set input data
        //
        $this->setData($data);
    }
}

Expected output:

{
    "data": {
        "error": {
            "companyId": [
                {
                    "id": [
                        "Şirket: Değer gereklidir ve boş bırakılamaz"
                    ]
                }
            ]
        }
    }
}

Active Result:

{
    "data": {
        "error": {
            "companyId": [
                {
                    "id": [
                        "Şirket: Value is required and can't be empty"
                    ]
                }
            ]
        }
    }
}

Here is my config provider

<?php
declare(strict_types=1);

use Laminas\ConfigAggregator\ArrayProvider;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\ConfigAggregator\PhpFileProvider;

// To enable or disable caching, set the `ConfigAggregator::ENABLE_CACHE` boolean in
// `config/autoload/local.php`.
$cacheConfig = [
    'config_cache_path' => 'data/cache/config-cache.php',
];

$aggregator = new ConfigAggregator([
    \Laminas\Cache\Storage\Adapter\Redis\ConfigProvider::class,
    \Mezzio\Tooling\ConfigProvider::class,
    \Mezzio\Cors\ConfigProvider::class,
    \Laminas\Mail\ConfigProvider::class,
    \Laminas\Paginator\ConfigProvider::class,
    \Laminas\Mvc\I18n\ConfigProvider::class,
    \Mezzio\Authorization\ConfigProvider::class,
    \Laminas\Serializer\ConfigProvider::class,
    \Laminas\Cache\ConfigProvider::class,
    // \Mezzio\Authentication\LaminasAuthentication\ConfigProvider::class,
    // \Mezzio\Authentication\OAuth2\ConfigProvider::class,
    // \Mezzio\Authentication\ConfigProvider::class,
    \Laminas\Db\ConfigProvider::class,
    \Laminas\InputFilter\ConfigProvider::class,
    \Laminas\Filter\ConfigProvider::class,
    \Laminas\I18n\ConfigProvider::class,
    \Laminas\HttpHandlerRunner\ConfigProvider::class,
    // \Mezzio\LaminasView\ConfigProvider::class,
    \Mezzio\Router\LaminasRouter\ConfigProvider::class,
    \Laminas\Router\ConfigProvider::class,
    \Laminas\Validator\ConfigProvider::class,
    // Include cache configuration
    new ArrayProvider($cacheConfig),
    \Mezzio\Helper\ConfigProvider::class,
    \Mezzio\ConfigProvider::class,
    \Mezzio\Router\ConfigProvider::class,
    \Laminas\Diactoros\ConfigProvider::class,
    // Swoole config to overwrite some services (if installed)
    class_exists(\Mezzio\Swoole\ConfigProvider::class)
        ? \Mezzio\Swoole\ConfigProvider::class
        : function(){ return[]; },
    // Default App module config
    App\ConfigProvider::class,
    // Load application config in a pre-defined order in such a way that local settings
    // overwrite global settings. (Loaded as first to last):
    //   - `global.php`
    //   - `*.global.php`
    //   - `local.php`
    //   - `*.local.php`

    // new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'),
    new PhpFileProvider(realpath(__DIR__) . sprintf('/autoload/{,*.}{global,%s}.php', getenv('APP_ENV') ?: 'local')),
    
    // Load development config if it exists
    new PhpFileProvider(realpath(__DIR__) . '/development.config.php'),
], $cacheConfig['config_cache_path']);

return $aggregator->getMergedConfig();

Here is my TranslatorDelagatorFactory class

<?php

declare(strict_types=1);

namespace App\Container;

use Interop\Container\ContainerInterface;
use Laminas\I18n\Translator\Resources;
use Laminas\ServiceManager\Factory\DelegatorFactoryInterface;

class TranslatorDelegatorFactory implements DelegatorFactoryInterface
{
    public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null)
    {
        $translator = $callback();
        
        $translator->addTranslationFilePattern(
            'phpArray',
            Resources::getBasePath(),
            Resources::getPatternForValidator()
        );
        $translator->addTranslationFilePattern(
            'phpArray',
            Resources::getBasePath(),
            Resources::getPatternForCaptcha()
        );

        return $translator;
    }
}

Help please thanks.

I have not tested it, but the optional input filter is created independently, so the translator might not work.

Try the following:

$this->add(
    [
        'name' => 'companyId',
        'type' => Laminas\InputFilter\OptionalInputFilter::class,
        [
            'name'     => 'id',
            'required' => true,
            // …
        ],
    ]
);

Thanks for your response,
I tried like this,

$this->add(
    [
        'name' => 'companyId',
        'type' => OptionalInputFilter::class,
        [
            'name'     => 'id',
            'required' => true,
            'validators' => [
                ['name' => Uuid::class],
                [
                    'name' => RecordExists::class,
                    'options' => [
                        'table'   => 'companies',
                        'field'   => 'companyId',
                        'adapter' => $this->adapter,
                    ]
                ]
            ],
        ],
    ]
);
$this->add($companyFilter, 'companyId');

but i get below the error

{
    "title": "Laminas\\InputFilter\\Exception\\InvalidArgumentException",
    "type": "https://httpstatus.es/400",
    "status": 400,
    "file": "/vendor/laminas/laminas-inputfilter/src/Factory.php",
    "line": 156,
    "error": "Laminas\\InputFilter\\Factory::createInput expects an array or Traversable; received \"string\"",
    "trace": [
        {

Also, I tried it in another way,

$companyFilter->add([
    'name' => 'id',
    'required' => true,
    'type' => OptionalInputFilter::class,
    'validators' => [
        ['name' => Uuid::class],
        [
            'name' => RecordExists::class,
            'options' => [
                'table'   => 'companies',
                'field'   => 'companyId',
                'adapter' => $this->adapter,
            ]
        ]
    ],
]);
$this->add($companyFilter, 'companyId');

But i am getting same error…

My mistake, the method add does not work directly, therefore:

$this->add(
    $this->getFactory()->createInputFilter(
        [
            'type' => Laminas\InputFilter\OptionalInputFilter::class,
            [
                'name'     => 'id',
                'required' => true,
                // …
            ],
        ]
    ),
    'companyId'
);

Background: if an array is used with the add method, then only a single input is created, so that no input filter via array can be used here. But the method add can use an object of the type input filter.

I hope my memory is correct.

Do you set the default translator for the validators somewhere?

Laminas\Validator\AbstractValidator::setDefaultTranslator($translator);

Or how do you use the translator?

That looks strange! MVC in Mezzio?!

I removed \Laminas\Mvc\I18n\ConfigProvider::class, class from config provider. And i tried below the code.

$this->add(
    $this->getFactory()->createInputFilter(
        [
            'type' => OptionalInputFilter::class,
            [
                'name'     => 'id',
                'required' => true,
                'validators' => [
                    ['name' => Uuid::class],
                    [
                        'name' => RecordExists::class,
                        'options' => [
                            'table'   => 'companies',
                            'field'   => 'companyId',
                            'adapter' => $this->adapter,
                        ]
                    ]
                ],
            ],
        ]
    ),
    'companyId'
);

But result is same translator does not work.

In my config provider class I use TranslatorDelegatorFactory to init translation object.

return [
    'invokables' => [
        // Handler\PingHandler::class => Handler\PingHandler::class,
    ],
    'delegators' => [
        TranslatorInterface::class => [
            'App\Container\TranslatorDelegatorFactory',
        ],
    ],

Do i need to really use this code ?

Laminas\Validator\AbstractValidator::setDefaultTranslator($translator);

If yes how i can get the validator translator instance ?

Laminas\\Validator\\AbstractValidator::setDefaultTranslator(): Argument #1 ($translator) must be of type ?Laminas\\Validator\\Translator\\TranslatorInterface, Laminas\\I18n\\Translator\\Translator given

Give me some and I will test the entire process with my Mezzio test application.

A configuration for the translator should be enough, a delegator is not needed. The rest of the work is done by factory method of the translator.

Hi, have you had a chance to test?

Sorry, I’m very busy with client projects.

The problem here is laminas-validator:

This means that your first idea of using laminas-mvc-i18n seems to be correct here - strange, but correct.

I hope I can test it in my local test application…

1 Like