From f574b1eb0d8733a8f5f9672698fc18bbac89dc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 13 Jan 2024 12:49:15 +0100 Subject: [PATCH 01/10] allow use of TranslatorInterface as Translator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- src/ValidatorPluginManager.php | 7 +++-- test/ValidatorPluginManagerTest.php | 44 +++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/ValidatorPluginManager.php b/src/ValidatorPluginManager.php index f4db1d80..d4500d08 100644 --- a/src/ValidatorPluginManager.php +++ b/src/ValidatorPluginManager.php @@ -2,6 +2,7 @@ namespace Laminas\Validator; +use Laminas\I18n\Translator\TranslatorInterface; use Laminas\I18n\Validator as I18nValidator; use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Exception\InvalidServiceException; @@ -589,9 +590,11 @@ public function injectTranslator($first, $second) $container = $container->getServiceLocator(); } - if ($validator instanceof Translator\TranslatorAwareInterface) { - if ($container && $container->has('MvcTranslator')) { + if ($validator instanceof Translator\TranslatorAwareInterface && $container) { + if ($container->has('MvcTranslator')) { $validator->setTranslator($container->get('MvcTranslator')); + } elseif ($container->has(TranslatorInterface::class)) { + $validator->setTranslator($container->get(TranslatorInterface::class)); } } } diff --git a/test/ValidatorPluginManagerTest.php b/test/ValidatorPluginManagerTest.php index 10843fb9..5553e56c 100644 --- a/test/ValidatorPluginManagerTest.php +++ b/test/ValidatorPluginManagerTest.php @@ -5,6 +5,7 @@ namespace LaminasTest\Validator; use Exception; +use Laminas\I18n\Translator\TranslatorInterface; use Laminas\ServiceManager\Exception\InvalidServiceException; use Laminas\ServiceManager\ServiceManager; use Laminas\Validator\AbstractValidator; @@ -58,20 +59,53 @@ public function testAllowsInjectingTranslator(): void self::assertEquals($translator, $validator->getTranslator()); } - public function testNoTranslatorInjectedWhenTranslatorIsNotPresent(): void + public function testAllowsInjectingTranslatorInterface(): void { + $translator = $this->createMock(Translator::class); + $container = $this->createMock(ContainerInterface::class); + $container + ->expects(self::exactly(2)) + ->method('has') + ->willReturnMap( + [ + ['MvcTranslator', false], + [TranslatorInterface::class, true], + ] + ); + $container ->expects(self::once()) + ->method('get') + ->with(TranslatorInterface::class) + ->willReturn($translator); + + $validators = new ValidatorPluginManager($container); + + $validator = $validators->get(NotEmpty::class); + + self::assertInstanceOf(AbstractValidator::class, $validator); + self::assertEquals($translator, $validator->getTranslator()); + } + + public function testNoTranslatorInjectedWhenTranslatorIsNotPresent(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container + ->expects(self::exactly(2)) ->method('has') - ->with('MvcTranslator') - ->willReturn(false); + ->willReturnMap( + [ + ['MvcTranslator', false], + [TranslatorInterface::class, false], + ] + ); $container ->expects(self::never()) - ->method('get') - ->with('MvcTranslator'); + ->method('get'); $validators = new ValidatorPluginManager($container); From 2f6af59c35c76033139b5bdb7914423fa927ec58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 13 Jan 2024 13:34:01 +0100 Subject: [PATCH 02/10] fix issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- src/ValidatorPluginManager.php | 7 +++---- test/ValidatorPluginManagerTest.php | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ValidatorPluginManager.php b/src/ValidatorPluginManager.php index d4500d08..e7e35ae7 100644 --- a/src/ValidatorPluginManager.php +++ b/src/ValidatorPluginManager.php @@ -2,7 +2,6 @@ namespace Laminas\Validator; -use Laminas\I18n\Translator\TranslatorInterface; use Laminas\I18n\Validator as I18nValidator; use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Exception\InvalidServiceException; @@ -590,11 +589,11 @@ public function injectTranslator($first, $second) $container = $container->getServiceLocator(); } - if ($validator instanceof Translator\TranslatorAwareInterface && $container) { + if ($validator instanceof Translator\TranslatorAwareInterface && $container instanceof ContainerInterface) { if ($container->has('MvcTranslator')) { $validator->setTranslator($container->get('MvcTranslator')); - } elseif ($container->has(TranslatorInterface::class)) { - $validator->setTranslator($container->get(TranslatorInterface::class)); + } elseif ($container->has(Translator\TranslatorInterface::class)) { + $validator->setTranslator($container->get(Translator\TranslatorInterface::class)); } } } diff --git a/test/ValidatorPluginManagerTest.php b/test/ValidatorPluginManagerTest.php index 5553e56c..100204b5 100644 --- a/test/ValidatorPluginManagerTest.php +++ b/test/ValidatorPluginManagerTest.php @@ -5,13 +5,13 @@ namespace LaminasTest\Validator; use Exception; -use Laminas\I18n\Translator\TranslatorInterface; use Laminas\ServiceManager\Exception\InvalidServiceException; use Laminas\ServiceManager\ServiceManager; use Laminas\Validator\AbstractValidator; use Laminas\Validator\Exception\RuntimeException; use Laminas\Validator\Explode; use Laminas\Validator\NotEmpty; +use Laminas\Validator\Translator\TranslatorInterface; use Laminas\Validator\ValidatorInterface; use Laminas\Validator\ValidatorPluginManager; use LaminasTest\Validator\TestAsset\InMemoryContainer; From 77851ea14f3cd85184ba9f6229b50db7e0a8bad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 13 Jan 2024 18:05:42 +0100 Subject: [PATCH 03/10] add Translator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- src/ConfigProvider.php | 4 +- src/Translator/DummyTranslator.php | 23 ++ src/Translator/Translator.php | 53 ++++ src/Translator/TranslatorFactory.php | 132 ++++++++ src/ValidatorPluginManager.php | 3 +- test/Translator/TranslatorFactoryTest.php | 348 ++++++++++++++++++++++ test/Translator/TranslatorTest.php | 41 +++ test/ValidatorPluginManagerTest.php | 2 +- 8 files changed, 603 insertions(+), 3 deletions(-) create mode 100644 src/Translator/DummyTranslator.php create mode 100644 src/Translator/Translator.php create mode 100644 src/Translator/TranslatorFactory.php create mode 100644 test/Translator/TranslatorFactoryTest.php create mode 100644 test/Translator/TranslatorTest.php diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 188ebc39..ae6babd6 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -25,12 +25,14 @@ public function getDependencyConfig() { return [ 'aliases' => [ - 'ValidatorManager' => ValidatorPluginManager::class, + Translator\TranslatorInterface::class => Translator\Translator::class, + 'ValidatorManager' => ValidatorPluginManager::class, // Legacy Zend Framework aliases 'Zend\Validator\ValidatorPluginManager' => ValidatorPluginManager::class, ], 'factories' => [ + Translator\Translator::class => Translator\TranslatorFactory::class, ValidatorPluginManager::class => ValidatorPluginManagerFactory::class, ], ]; diff --git a/src/Translator/DummyTranslator.php b/src/Translator/DummyTranslator.php new file mode 100644 index 00000000..87faf9cd --- /dev/null +++ b/src/Translator/DummyTranslator.php @@ -0,0 +1,23 @@ +translator; + } + + /** + * Translate a message using the given text domain and locale + * + * @param string $message + * @param string $textDomain + * @param string $locale + * @return string + */ + public function translate($message, $textDomain = 'default', $locale = null) + { + return $this->translator->translate($message, $textDomain, $locale); + } + + /** + * Provide a pluralized translation of the given string using the given text domain and locale + * + * @param string $singular + * @param string $plural + * @param int $number + * @param string $textDomain + * @param string $locale + * @return string + */ + public function translatePlural($singular, $plural, $number, $textDomain = 'default', $locale = null) + { + return $this->translator->translatePlural($singular, $plural, $number, $textDomain, $locale); + } +} diff --git a/src/Translator/TranslatorFactory.php b/src/Translator/TranslatorFactory.php new file mode 100644 index 00000000..c0b3f57b --- /dev/null +++ b/src/Translator/TranslatorFactory.php @@ -0,0 +1,132 @@ +has(TranslatorInterface::class)) { + return new Translator($container->get(TranslatorInterface::class)); + } + + return $this->marshalTranslator($container); + } + + /** + * Marshal an Translator. + * + * If configuration exists, will pass it to the I18nTranslator::factory, + * decorating the returned instance in an MvcTranslator. + * + * Otherwise: + * + * - returns an Translator decorating a DummyTranslator instance if + * ext/intl is not loaded. + * - returns an Translator decorating an empty I18nTranslator instance. + * + * @return Translator + */ + private function marshalTranslator(ContainerInterface $container) + { + // Load a translator from configuration, if possible + $translator = $this->marshalTranslatorFromConfig($container); + if ($translator) { + return $translator; + } + + // If ext/intl is not loaded, return a dummy translator + if (! extension_loaded('intl')) { + return new Translator(new DummyTranslator()); + } + + return new Translator(new I18nTranslator()); + } + + /** + * Attempt to marshal a translator from configuration. + * + * Returns: + * - an Translator seeded with a DummyTranslator if "translator" + * configuration is available, and evaluates to boolean false. + * - an Translator seed with an I18nTranslator if "translator" + * configuration is available, and is a non-empty array or a Traversable + * instance. + * - null in all other cases, including absence of a configuration service. + * + * @return void|Translator + */ + private function marshalTranslatorFromConfig(ContainerInterface $container) + { + if (! $container->has('config')) { + return; + } + + $config = $container->get('config'); + + if (! is_array($config) || ! array_key_exists('translator', $config)) { + return; + } + + // 'translator' => false + if ($config['translator'] === false) { + return new Translator(new DummyTranslator()); + } + + // Empty translator configuration + if (is_array($config['translator']) && empty($config['translator'])) { + return; + } + + // Unusable translator configuration + if (! is_array($config['translator']) && ! $config['translator'] instanceof Traversable) { + return; + } + + // Create translator from configuration + $i18nTranslator = I18nTranslator::factory($config['translator']); + + // Inject plugins, if present + if ($container->has('TranslatorPluginManager')) { + $loaderManager = $container->get('TranslatorPluginManager'); + + assert($loaderManager instanceof LoaderPluginManager); + + $i18nTranslator->setPluginManager($loaderManager); + } + + // Inject into service manager instances + if ($container instanceof ServiceManager) { + $container->setService(TranslatorInterface::class, $i18nTranslator); + } + + return new Translator($i18nTranslator); + } +} diff --git a/src/ValidatorPluginManager.php b/src/ValidatorPluginManager.php index e7e35ae7..af7234d0 100644 --- a/src/ValidatorPluginManager.php +++ b/src/ValidatorPluginManager.php @@ -2,6 +2,7 @@ namespace Laminas\Validator; +use Laminas\I18n\Translator\TranslatorInterface; use Laminas\I18n\Validator as I18nValidator; use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Exception\InvalidServiceException; @@ -592,7 +593,7 @@ public function injectTranslator($first, $second) if ($validator instanceof Translator\TranslatorAwareInterface && $container instanceof ContainerInterface) { if ($container->has('MvcTranslator')) { $validator->setTranslator($container->get('MvcTranslator')); - } elseif ($container->has(Translator\TranslatorInterface::class)) { + } elseif ($container->has(TranslatorInterface::class)) { $validator->setTranslator($container->get(Translator\TranslatorInterface::class)); } } diff --git a/test/Translator/TranslatorFactoryTest.php b/test/Translator/TranslatorFactoryTest.php new file mode 100644 index 00000000..d4f1fdc3 --- /dev/null +++ b/test/Translator/TranslatorFactoryTest.php @@ -0,0 +1,348 @@ +createMock(TranslatorInterface::class); + + $container = $this->createMock(ServiceManager::class); + $container + ->expects(self::once()) + ->method('has') + ->with(TranslatorInterface::class) + ->willReturn(true); + $container + ->expects(self::once()) + ->method('get') + ->with(TranslatorInterface::class) + ->willReturn($translator); + + self::assertInstanceOf(ContainerInterface::class, $container); + + $factory = new TranslatorFactory(); + $test = $factory($container, TranslatorInterface::class); + + $this->assertInstanceOf(Translator::class, $test); + $this->assertSame($translator, $test->getTranslator()); + } + + /** @psalm-return array */ + public static function expectedTranslatorProvider(): array + { + return extension_loaded('intl') + ? ['intl-loaded' => [I18nTranslator::class]] + : ['no-intl-loaded' => [DummyTranslator::class]]; + } + + /** + * @dataProvider expectedTranslatorProvider + * @psalm-param class-string $expected + */ + public function testFactoryReturnsTranslatorDecoratingDefaultTranslatorWhenNoConfigPresent( + string $expected + ): void { + $container = $this->createMock(ServiceManager::class); + $container + ->expects(self::exactly(2)) + ->method('has') + ->willReturnMap( + [ + [TranslatorInterface::class, false], + ['config', false], + ] + ); + $container + ->expects(self::never()) + ->method('get'); + + self::assertInstanceOf(ContainerInterface::class, $container); + + $factory = new TranslatorFactory(); + $test = $factory($container, TranslatorInterface::class); + + $this->assertInstanceOf(Translator::class, $test); + $this->assertInstanceOf($expected, $test->getTranslator()); + } + + /** + * @dataProvider expectedTranslatorProvider + * @psalm-param class-string $expected + */ + public function testFactoryReturnsMvcDecoratorDecoratingDefaultTranslatorWhenNoTranslatorConfigPresent( + string $expected + ): void { + $container = $this->createMock(ServiceManager::class); + $container + ->expects(self::exactly(2)) + ->method('has') + ->willReturnMap( + [ + [TranslatorInterface::class, false], + ['Zend\I18n\Translator\TranslatorInterface', false], + ['config', true], + ] + ); + $container + ->expects(self::once()) + ->method('get') + ->with('config') + ->willReturn([]); + + self::assertInstanceOf(ContainerInterface::class, $container); + + $factory = new TranslatorFactory(); + $test = $factory($container, TranslatorInterface::class); + + $this->assertInstanceOf(Translator::class, $test); + $this->assertInstanceOf($expected, $test->getTranslator()); + } + + public function testFactoryReturnsMvcDecoratorDecoratingDummyTranslatorWhenTranslatorConfigIsFalse(): void + { + $container = $this->createMock(ServiceManager::class); + $container + ->expects(self::exactly(2)) + ->method('has') + ->willReturnMap( + [ + [TranslatorInterface::class, false], + ['Zend\I18n\Translator\TranslatorInterface', false], + ['config', true], + ] + ); + $container + ->expects(self::once()) + ->method('get') + ->with('config') + ->willReturn(['translator' => false]); + + self::assertInstanceOf(ContainerInterface::class, $container); + + $factory = new TranslatorFactory(); + $test = $factory($container, TranslatorInterface::class); + + $this->assertInstanceOf(Translator::class, $test); + $this->assertInstanceOf(DummyTranslator::class, $test->getTranslator()); + } + + /** + * @dataProvider expectedTranslatorProvider + * @psalm-param class-string $expected + */ + public function testFactoryReturnsMvcDecoratorDecoratingDefaultTranslatorWhenEmptyTranslatorConfigPresent( + string $expected + ): void { + $container = $this->createMock(ServiceManager::class); + $container + ->expects(self::exactly(2)) + ->method('has') + ->willReturnMap( + [ + [TranslatorInterface::class, false], + ['Zend\I18n\Translator\TranslatorInterface', false], + ['config', true], + ] + ); + $container + ->expects(self::once()) + ->method('get') + ->with('config') + ->willReturn(['translator' => []]); + + self::assertInstanceOf(ContainerInterface::class, $container); + + $factory = new TranslatorFactory(); + $test = $factory($container, TranslatorInterface::class); + + $this->assertInstanceOf(Translator::class, $test); + $this->assertInstanceOf($expected, $test->getTranslator()); + } + + /** @psalm-return array, 1: class-string}> */ + public static function invalidTranslatorConfig(): array + { + $expectedTranslator = extension_loaded('intl') + ? I18nTranslator::class + : DummyTranslator::class; + + return [ + 'null' => [['translator' => null], $expectedTranslator], + 'true' => [['translator' => true], $expectedTranslator], + 'zero' => [['translator' => 0], $expectedTranslator], + 'int' => [['translator' => 1], $expectedTranslator], + 'float-0' => [['translator' => 0.0], $expectedTranslator], + 'float' => [['translator' => 1.1], $expectedTranslator], + 'string' => [['translator' => 'invalid'], $expectedTranslator], + 'object' => [['translator' => (object) ['translator' => 'invalid']], $expectedTranslator], + ]; + } + + /** + * @param array $config + * @psalm-param class-string $expected + * @dataProvider invalidTranslatorConfig + */ + public function testFactoryReturnsDecoratorDecoratingDefaultTranslatorWithInvalidTranslatorConfig( + $config, + $expected + ): void { + $container = $this->createMock(ServiceManager::class); + $container + ->expects(self::exactly(2)) + ->method('has') + ->willReturnMap( + [ + [TranslatorInterface::class, false], + ['Zend\I18n\Translator\TranslatorInterface', false], + ['config', true], + ] + ); + $container + ->expects(self::once()) + ->method('get') + ->with('config') + ->willReturn($config); + + self::assertInstanceOf(ContainerInterface::class, $container); + + $factory = new TranslatorFactory(); + $test = $factory($container, TranslatorInterface::class); + + $this->assertInstanceOf(Translator::class, $test); + $this->assertInstanceOf($expected, $test->getTranslator()); + } + + /** + * @psalm-return array|ArrayAccess}> + */ + public static function validTranslatorConfig(): array + { + $locale = Locale::getDefault() === 'en-US' ? 'de-DE' : Locale::getDefault(); + $config = [ + 'locale' => $locale, + 'event_manager_enabled' => true, + ]; + + return [ + 'array' => [$config], + 'traversable' => [new ArrayObject($config)], + ]; + } + + /** + * @requires extension intl + * @dataProvider validTranslatorConfig + * @param array|ArrayAccess $config + */ + public function testFactoryReturnsConfiguredTranslatorWhenValidConfigIsPresent($config): void + { + $container = $this->createMock(ServiceManager::class); + $container + ->expects(self::exactly(3)) + ->method('has') + ->willReturnMap( + [ + [TranslatorInterface::class, false], + ['Zend\I18n\Translator\TranslatorInterface', false], + ['config', true], + ['TranslatorPluginManager', false], + ] + ); + $container + ->expects(self::once()) + ->method('get') + ->with('config') + ->willReturn(['translator' => $config]); + $container + ->expects(self::once()) + ->method('setService') + ->with(TranslatorInterface::class, new IsInstanceOf(I18nTranslator::class)); + + self::assertInstanceOf(ContainerInterface::class, $container); + + $factory = new TranslatorFactory(); + $test = $factory($container, TranslatorInterface::class); + + $this->assertInstanceOf(Translator::class, $test); + + $decorated = $test->getTranslator(); + $this->assertInstanceOf(I18nTranslator::class, $decorated); + $locale = $config['locale'] ?? null; + self::assertIsString($locale); + $this->assertEquals($locale, $decorated->getLocale()); + $this->assertTrue($decorated->isEventManagerEnabled()); + } + + /** + * @param array|ArrayAccess $config + * @requires extension intl + * @dataProvider validTranslatorConfig + */ + public function testFactoryReturnsConfiguredTranslatorInjectedWithTranslatorPluginManagerWhenValidConfigIsPresent( + $config + ): void { + $loaders = $this->createMock(LoaderPluginManager::class); + + $container = $this->createMock(ServiceManager::class); + $container + ->expects(self::exactly(3)) + ->method('has') + ->willReturnMap( + [ + [TranslatorInterface::class, false], + ['Zend\I18n\Translator\TranslatorInterface', false], + ['config', true], + ['TranslatorPluginManager', true], + ] + ); + $container + ->expects(self::exactly(2)) + ->method('get') + ->willReturnMap( + [ + ['config', ['translator' => $config]], + ['TranslatorPluginManager', $loaders], + ] + ); + $container + ->expects(self::once()) + ->method('setService') + ->with(TranslatorInterface::class, new IsInstanceOf(I18nTranslator::class)); + + self::assertInstanceOf(ContainerInterface::class, $container); + + $factory = new TranslatorFactory(); + $test = $factory($container, TranslatorInterface::class); + + $this->assertInstanceOf(Translator::class, $test); + + $decorated = $test->getTranslator(); + $this->assertInstanceOf(I18nTranslator::class, $decorated); + $this->assertEquals($config['locale'], $decorated->getLocale()); + $this->assertTrue($decorated->isEventManagerEnabled()); + $this->assertSame($loaders, $decorated->getPluginManager()); + } +} diff --git a/test/Translator/TranslatorTest.php b/test/Translator/TranslatorTest.php new file mode 100644 index 00000000..340dc227 --- /dev/null +++ b/test/Translator/TranslatorTest.php @@ -0,0 +1,41 @@ +i18nTranslator = $this->createMock(I18nTranslator::class); + $this->translator = new Translator($this->i18nTranslator); + } + + public function testIsAnI18nTranslator(): void + { + $this->assertInstanceOf(TranslatorInterface::class, $this->translator); + } + + public function testIsAValidatorTranslator(): void + { + $this->assertInstanceOf(TranslatorInterface::class, $this->translator); + } + + public function testCanRetrieveComposedTranslator(): void + { + $this->assertSame($this->i18nTranslator, $this->translator->getTranslator()); + } +} diff --git a/test/ValidatorPluginManagerTest.php b/test/ValidatorPluginManagerTest.php index 100204b5..01435b1b 100644 --- a/test/ValidatorPluginManagerTest.php +++ b/test/ValidatorPluginManagerTest.php @@ -71,7 +71,7 @@ public function testAllowsInjectingTranslatorInterface(): void ->willReturnMap( [ ['MvcTranslator', false], - [TranslatorInterface::class, true], + [\Laminas\I18n\Translator\TranslatorInterface::class, true], ] ); From 24b7dde7639dc7a56339af3108d5ea20eabaa891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 18 May 2024 19:37:10 +0200 Subject: [PATCH 04/10] mark classes as final, remove getter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- src/Translator/DummyTranslator.php | 2 +- src/Translator/Translator.php | 10 +------ test/Translator/TranslatorFactoryTest.php | 33 +++++++++++++++++------ test/Translator/TranslatorTest.php | 9 ++++++- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/Translator/DummyTranslator.php b/src/Translator/DummyTranslator.php index 87faf9cd..f5818455 100644 --- a/src/Translator/DummyTranslator.php +++ b/src/Translator/DummyTranslator.php @@ -6,7 +6,7 @@ use Laminas\I18n\Translator\TranslatorInterface as I18nTranslatorInterface; -class DummyTranslator implements I18nTranslatorInterface +final class DummyTranslator implements I18nTranslatorInterface { /** @inheritDoc */ public function translate($message, $textDomain = 'default', $locale = null) diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 863bb4b6..b4fcfaab 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -7,7 +7,7 @@ use Laminas\I18n\Translator\TranslatorInterface as I18nTranslatorInterface; use Laminas\Validator\Translator\TranslatorInterface as ValidatorTranslatorInterface; -class Translator implements +final class Translator implements I18nTranslatorInterface, ValidatorTranslatorInterface { @@ -15,14 +15,6 @@ public function __construct(protected I18nTranslatorInterface $translator) { } - /** - * @return I18nTranslatorInterface - */ - public function getTranslator() - { - return $this->translator; - } - /** * Translate a message using the given text domain and locale * diff --git a/test/Translator/TranslatorFactoryTest.php b/test/Translator/TranslatorFactoryTest.php index d4f1fdc3..68673b33 100644 --- a/test/Translator/TranslatorFactoryTest.php +++ b/test/Translator/TranslatorFactoryTest.php @@ -19,6 +19,7 @@ use Locale; use PHPUnit\Framework\Constraint\IsInstanceOf; use PHPUnit\Framework\TestCase; +use ReflectionProperty; use function extension_loaded; @@ -46,7 +47,9 @@ public function testFactoryReturnsTranslatorDecoratingTranslatorInterfaceService $test = $factory($container, TranslatorInterface::class); $this->assertInstanceOf(Translator::class, $test); - $this->assertSame($translator, $test->getTranslator()); + + $prop = new ReflectionProperty($test, 'translator'); + $this->assertSame($translator, $prop->getValue($test)); } /** @psalm-return array */ @@ -84,7 +87,9 @@ public function testFactoryReturnsTranslatorDecoratingDefaultTranslatorWhenNoCon $test = $factory($container, TranslatorInterface::class); $this->assertInstanceOf(Translator::class, $test); - $this->assertInstanceOf($expected, $test->getTranslator()); + + $prop = new ReflectionProperty($test, 'translator'); + $this->assertInstanceOf($expected, $prop->getValue($test)); } /** @@ -117,7 +122,9 @@ public function testFactoryReturnsMvcDecoratorDecoratingDefaultTranslatorWhenNoT $test = $factory($container, TranslatorInterface::class); $this->assertInstanceOf(Translator::class, $test); - $this->assertInstanceOf($expected, $test->getTranslator()); + + $prop = new ReflectionProperty($test, 'translator'); + $this->assertInstanceOf($expected, $prop->getValue($test)); } public function testFactoryReturnsMvcDecoratorDecoratingDummyTranslatorWhenTranslatorConfigIsFalse(): void @@ -145,7 +152,9 @@ public function testFactoryReturnsMvcDecoratorDecoratingDummyTranslatorWhenTrans $test = $factory($container, TranslatorInterface::class); $this->assertInstanceOf(Translator::class, $test); - $this->assertInstanceOf(DummyTranslator::class, $test->getTranslator()); + + $prop = new ReflectionProperty($test, 'translator'); + $this->assertInstanceOf(DummyTranslator::class, $prop->getValue($test)); } /** @@ -178,7 +187,9 @@ public function testFactoryReturnsMvcDecoratorDecoratingDefaultTranslatorWhenEmp $test = $factory($container, TranslatorInterface::class); $this->assertInstanceOf(Translator::class, $test); - $this->assertInstanceOf($expected, $test->getTranslator()); + + $prop = new ReflectionProperty($test, 'translator'); + $this->assertInstanceOf($expected, $prop->getValue($test)); } /** @psalm-return array, 1: class-string}> */ @@ -232,7 +243,9 @@ public function testFactoryReturnsDecoratorDecoratingDefaultTranslatorWithInvali $test = $factory($container, TranslatorInterface::class); $this->assertInstanceOf(Translator::class, $test); - $this->assertInstanceOf($expected, $test->getTranslator()); + + $prop = new ReflectionProperty($test, 'translator'); + $this->assertInstanceOf($expected, $prop->getValue($test)); } /** @@ -288,7 +301,9 @@ public function testFactoryReturnsConfiguredTranslatorWhenValidConfigIsPresent($ $this->assertInstanceOf(Translator::class, $test); - $decorated = $test->getTranslator(); + $prop = new ReflectionProperty($test, 'translator'); + $decorated = $prop->getValue($test); + $this->assertInstanceOf(I18nTranslator::class, $decorated); $locale = $config['locale'] ?? null; self::assertIsString($locale); @@ -339,7 +354,9 @@ public function testFactoryReturnsConfiguredTranslatorInjectedWithTranslatorPlug $this->assertInstanceOf(Translator::class, $test); - $decorated = $test->getTranslator(); + $prop = new ReflectionProperty($test, 'translator'); + $decorated = $prop->getValue($test); + $this->assertInstanceOf(I18nTranslator::class, $decorated); $this->assertEquals($config['locale'], $decorated->getLocale()); $this->assertTrue($decorated->isEventManagerEnabled()); diff --git a/test/Translator/TranslatorTest.php b/test/Translator/TranslatorTest.php index 340dc227..567cce93 100644 --- a/test/Translator/TranslatorTest.php +++ b/test/Translator/TranslatorTest.php @@ -9,6 +9,8 @@ use Laminas\Validator\Translator\Translator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use ReflectionException; +use ReflectionProperty; class TranslatorTest extends TestCase { @@ -34,8 +36,13 @@ public function testIsAValidatorTranslator(): void $this->assertInstanceOf(TranslatorInterface::class, $this->translator); } + /** + * @throws ReflectionException + */ public function testCanRetrieveComposedTranslator(): void { - $this->assertSame($this->i18nTranslator, $this->translator->getTranslator()); + $prop = new ReflectionProperty($this->translator, 'translator'); + + $this->assertSame($this->i18nTranslator, $prop->getValue($this->translator)); } } From f0f78b98c7cc3ec9645f1f7c5fee3584cf390dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 18 May 2024 20:10:29 +0200 Subject: [PATCH 05/10] remove 2 phpcs:disable marks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- psalm-baseline.xml | 10 +++++----- src/Translator/DummyTranslator.php | 3 +-- src/Translator/TranslatorFactory.php | 2 -- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index d27e225a..1920b9aa 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1907,6 +1907,11 @@ + + + + + @@ -2004,10 +2009,6 @@ get('MvcTranslator')]]> - - - - @@ -2018,7 +2019,6 @@ - getServiceLocator()]]> diff --git a/src/Translator/DummyTranslator.php b/src/Translator/DummyTranslator.php index f5818455..317a7e3b 100644 --- a/src/Translator/DummyTranslator.php +++ b/src/Translator/DummyTranslator.php @@ -17,7 +17,6 @@ public function translate($message, $textDomain = 'default', $locale = null) /** @inheritDoc */ public function translatePlural($singular, $plural, $number, $textDomain = 'default', $locale = null) { - // phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators - return $number == 1 ? $singular : $plural; + return (int) $number === 1 ? $singular : $plural; } } diff --git a/src/Translator/TranslatorFactory.php b/src/Translator/TranslatorFactory.php index c0b3f57b..0a456c37 100644 --- a/src/Translator/TranslatorFactory.php +++ b/src/Translator/TranslatorFactory.php @@ -4,8 +4,6 @@ namespace Laminas\Validator\Translator; -// phpcs:disable WebimpressCodingStandard.PHP.CorrectClassNameCase - use Laminas\I18n\Translator\LoaderPluginManager; use Laminas\I18n\Translator\Translator as I18nTranslator; use Laminas\I18n\Translator\TranslatorInterface; From 13374aa6fcac190f16307d6d5db4aabbe6dabf23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 18 May 2024 20:39:57 +0200 Subject: [PATCH 06/10] test updated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- test/Translator/TranslatorTest.php | 60 ++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/test/Translator/TranslatorTest.php b/test/Translator/TranslatorTest.php index 567cce93..af613f1e 100644 --- a/test/Translator/TranslatorTest.php +++ b/test/Translator/TranslatorTest.php @@ -9,15 +9,13 @@ use Laminas\Validator\Translator\Translator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use ReflectionException; -use ReflectionProperty; class TranslatorTest extends TestCase { /** @var Translator */ protected $translator; - /** @var TranslatorInterface|MockObject */ + /** @var TranslatorInterface&MockObject */ protected $i18nTranslator; public function setUp(): void @@ -26,23 +24,55 @@ public function setUp(): void $this->translator = new Translator($this->i18nTranslator); } - public function testIsAnI18nTranslator(): void + public function testTranslate(): void { - $this->assertInstanceOf(TranslatorInterface::class, $this->translator); - } + $message = 'This is the message'; + $textDomain = 'default'; + $locale = 'en_US'; - public function testIsAValidatorTranslator(): void - { - $this->assertInstanceOf(TranslatorInterface::class, $this->translator); + $this->i18nTranslator->expects($this->once()) + ->method('translate') + ->with($message, $textDomain, $locale) + ->willReturn($message); + + $this->assertEquals( + $message, + $this->translator->translate( + $message, + $textDomain, + $locale + ) + ); } - /** - * @throws ReflectionException - */ - public function testCanRetrieveComposedTranslator(): void + public function testTranslatePlural(): void { - $prop = new ReflectionProperty($this->translator, 'translator'); + $singular = 'singular'; + $plural = 'plural'; + $number = 2; + $textDomain = 'default'; + $locale = 'en_US'; + + $this->i18nTranslator->expects($this->once()) + ->method('translatePlural') + ->with( + $singular, + $plural, + $number, + $textDomain, + $locale + ) + ->willReturn($singular); - $this->assertSame($this->i18nTranslator, $prop->getValue($this->translator)); + $this->assertEquals( + $singular, + $this->translator->translatePlural( + $singular, + $plural, + $number, + $textDomain, + $locale + ) + ); } } From 25f1c5fdafb19c1c30d2cfeef6f7a10ace446c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 21 May 2024 21:43:37 +0200 Subject: [PATCH 07/10] mark DummyTranslator as internal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- src/Translator/DummyTranslator.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Translator/DummyTranslator.php b/src/Translator/DummyTranslator.php index 317a7e3b..fb153abd 100644 --- a/src/Translator/DummyTranslator.php +++ b/src/Translator/DummyTranslator.php @@ -6,6 +6,9 @@ use Laminas\I18n\Translator\TranslatorInterface as I18nTranslatorInterface; +/** + * @internal + */ final class DummyTranslator implements I18nTranslatorInterface { /** @inheritDoc */ From b516a5b72a25a19c0ef9fe7c9f5df5c40d92be7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 21 May 2024 21:44:27 +0200 Subject: [PATCH 08/10] set internal Translator to private and readonly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- src/Translator/Translator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index b4fcfaab..fbd7bd4e 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -11,7 +11,7 @@ final class Translator implements I18nTranslatorInterface, ValidatorTranslatorInterface { - public function __construct(protected I18nTranslatorInterface $translator) + public function __construct(private readonly I18nTranslatorInterface $translator) { } From 1701f70e0eecdf625098f8b26a39771f8f4763eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 21 May 2024 21:45:45 +0200 Subject: [PATCH 09/10] mark TranslatorFactory as final and remove FactoryInterface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- src/Translator/TranslatorFactory.php | 28 ++++++++--------------- test/Translator/TranslatorFactoryTest.php | 16 ++++++------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/Translator/TranslatorFactory.php b/src/Translator/TranslatorFactory.php index 0a456c37..35086ab7 100644 --- a/src/Translator/TranslatorFactory.php +++ b/src/Translator/TranslatorFactory.php @@ -7,7 +7,6 @@ use Laminas\I18n\Translator\LoaderPluginManager; use Laminas\I18n\Translator\Translator as I18nTranslator; use Laminas\I18n\Translator\TranslatorInterface; -use Laminas\ServiceManager\Factory\FactoryInterface; use Laminas\ServiceManager\ServiceManager; use Psr\Container\ContainerInterface; use Traversable; @@ -21,13 +20,9 @@ * Overrides the translator factory from the i18n component in order to * replace it with the bridge class from this namespace. */ -class TranslatorFactory implements FactoryInterface +final class TranslatorFactory { - /** - * @param string $requestedName - * @return Translator - */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container): Translator { // Assume that if a user has registered a service for the // TranslatorInterface, it must be valid @@ -49,14 +44,13 @@ public function __invoke(ContainerInterface $container, $requestedName, ?array $ * - returns an Translator decorating a DummyTranslator instance if * ext/intl is not loaded. * - returns an Translator decorating an empty I18nTranslator instance. - * - * @return Translator */ - private function marshalTranslator(ContainerInterface $container) + private function marshalTranslator(ContainerInterface $container): Translator { // Load a translator from configuration, if possible $translator = $this->marshalTranslatorFromConfig($container); - if ($translator) { + + if ($translator instanceof Translator) { return $translator; } @@ -78,19 +72,17 @@ private function marshalTranslator(ContainerInterface $container) * configuration is available, and is a non-empty array or a Traversable * instance. * - null in all other cases, including absence of a configuration service. - * - * @return void|Translator */ - private function marshalTranslatorFromConfig(ContainerInterface $container) + private function marshalTranslatorFromConfig(ContainerInterface $container): ?Translator { if (! $container->has('config')) { - return; + return null; } $config = $container->get('config'); if (! is_array($config) || ! array_key_exists('translator', $config)) { - return; + return null; } // 'translator' => false @@ -100,12 +92,12 @@ private function marshalTranslatorFromConfig(ContainerInterface $container) // Empty translator configuration if (is_array($config['translator']) && empty($config['translator'])) { - return; + return null; } // Unusable translator configuration if (! is_array($config['translator']) && ! $config['translator'] instanceof Traversable) { - return; + return null; } // Create translator from configuration diff --git a/test/Translator/TranslatorFactoryTest.php b/test/Translator/TranslatorFactoryTest.php index 68673b33..426b57d7 100644 --- a/test/Translator/TranslatorFactoryTest.php +++ b/test/Translator/TranslatorFactoryTest.php @@ -44,7 +44,7 @@ public function testFactoryReturnsTranslatorDecoratingTranslatorInterfaceService self::assertInstanceOf(ContainerInterface::class, $container); $factory = new TranslatorFactory(); - $test = $factory($container, TranslatorInterface::class); + $test = $factory($container); $this->assertInstanceOf(Translator::class, $test); @@ -84,7 +84,7 @@ public function testFactoryReturnsTranslatorDecoratingDefaultTranslatorWhenNoCon self::assertInstanceOf(ContainerInterface::class, $container); $factory = new TranslatorFactory(); - $test = $factory($container, TranslatorInterface::class); + $test = $factory($container); $this->assertInstanceOf(Translator::class, $test); @@ -119,7 +119,7 @@ public function testFactoryReturnsMvcDecoratorDecoratingDefaultTranslatorWhenNoT self::assertInstanceOf(ContainerInterface::class, $container); $factory = new TranslatorFactory(); - $test = $factory($container, TranslatorInterface::class); + $test = $factory($container); $this->assertInstanceOf(Translator::class, $test); @@ -149,7 +149,7 @@ public function testFactoryReturnsMvcDecoratorDecoratingDummyTranslatorWhenTrans self::assertInstanceOf(ContainerInterface::class, $container); $factory = new TranslatorFactory(); - $test = $factory($container, TranslatorInterface::class); + $test = $factory($container); $this->assertInstanceOf(Translator::class, $test); @@ -184,7 +184,7 @@ public function testFactoryReturnsMvcDecoratorDecoratingDefaultTranslatorWhenEmp self::assertInstanceOf(ContainerInterface::class, $container); $factory = new TranslatorFactory(); - $test = $factory($container, TranslatorInterface::class); + $test = $factory($container); $this->assertInstanceOf(Translator::class, $test); @@ -240,7 +240,7 @@ public function testFactoryReturnsDecoratorDecoratingDefaultTranslatorWithInvali self::assertInstanceOf(ContainerInterface::class, $container); $factory = new TranslatorFactory(); - $test = $factory($container, TranslatorInterface::class); + $test = $factory($container); $this->assertInstanceOf(Translator::class, $test); @@ -297,7 +297,7 @@ public function testFactoryReturnsConfiguredTranslatorWhenValidConfigIsPresent($ self::assertInstanceOf(ContainerInterface::class, $container); $factory = new TranslatorFactory(); - $test = $factory($container, TranslatorInterface::class); + $test = $factory($container); $this->assertInstanceOf(Translator::class, $test); @@ -350,7 +350,7 @@ public function testFactoryReturnsConfiguredTranslatorInjectedWithTranslatorPlug self::assertInstanceOf(ContainerInterface::class, $container); $factory = new TranslatorFactory(); - $test = $factory($container, TranslatorInterface::class); + $test = $factory($container); $this->assertInstanceOf(Translator::class, $test); From 95a9f2b9bafc4279ee20835d8df73498a368a0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 21 May 2024 21:46:25 +0200 Subject: [PATCH 10/10] use early exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller --- src/ValidatorPluginManager.php | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/ValidatorPluginManager.php b/src/ValidatorPluginManager.php index af7234d0..6d040862 100644 --- a/src/ValidatorPluginManager.php +++ b/src/ValidatorPluginManager.php @@ -585,17 +585,27 @@ public function injectTranslator($first, $second) $validator = $first; } + if (! $validator instanceof Translator\TranslatorAwareInterface) { + return; + } + // V2 means we pull it from the parent container if ($container === $this && method_exists($container, 'getServiceLocator') && $container->getServiceLocator()) { $container = $container->getServiceLocator(); } - if ($validator instanceof Translator\TranslatorAwareInterface && $container instanceof ContainerInterface) { - if ($container->has('MvcTranslator')) { - $validator->setTranslator($container->get('MvcTranslator')); - } elseif ($container->has(TranslatorInterface::class)) { - $validator->setTranslator($container->get(Translator\TranslatorInterface::class)); - } + if (! $container instanceof ContainerInterface) { + return; + } + + if ($container->has('MvcTranslator')) { + $validator->setTranslator($container->get('MvcTranslator')); + + return; + } + + if ($container->has(TranslatorInterface::class)) { + $validator->setTranslator($container->get(Translator\TranslatorInterface::class)); } }