[Translation] Use proven DSN class from Notifier

This class is already in use, no need to introduce it as experimental
This commit is contained in:
Oskar Stark 2021-04-25 09:14:57 +02:00
parent 88abb39b92
commit a7979c44de
14 changed files with 269 additions and 90 deletions

View File

@ -6,6 +6,7 @@
"symfony/http-kernel": "source", "symfony/http-kernel": "source",
"symfony/messenger": "source", "symfony/messenger": "source",
"symfony/notifier": "source", "symfony/notifier": "source",
"symfony/translation": "source",
"symfony/validator": "source", "symfony/validator": "source",
"*": "dist" "*": "dist"
} }

View File

@ -1360,7 +1360,7 @@ class FrameworkExtension extends Extension
$parentPackages = ['symfony/framework-bundle', 'symfony/translation', 'symfony/http-client']; $parentPackages = ['symfony/framework-bundle', 'symfony/translation', 'symfony/http-client'];
foreach ($classToServices as $class => $service) { foreach ($classToServices as $class => $service) {
$package = sprintf('symfony/%s-translation', substr($service, \strlen('translation.provider_factory.'))); $package = sprintf('symfony/%s-translation-provider', substr($service, \strlen('translation.provider_factory.')));
if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable($package, $class, $parentPackages)) { if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable($package, $class, $parentPackages)) {
$container->removeDefinition($service); $container->removeDefinition($service);

View File

@ -1,7 +1,7 @@
{ {
"name": "symfony/loco-translation", "name": "symfony/loco-translation-provider",
"type": "symfony-bridge", "type": "symfony-bridge",
"description": "Symfony Loco Translation Bridge", "description": "Symfony Loco Translation Provider Bridge",
"keywords": ["loco", "translation", "provider"], "keywords": ["loco", "translation", "provider"],
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"license": "MIT", "license": "MIT",

View File

@ -13,7 +13,7 @@
</php> </php>
<testsuites> <testsuites>
<testsuite name="Symfony Loco Translation Bridge Test Suite"> <testsuite name="Symfony Loco Translation Provider Bridge Test Suite">
<directory>./Tests/</directory> <directory>./Tests/</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Exception;
/**
* @author Oskar Stark <oskarstark@googlemail.com>
*/
class MissingRequiredOptionException extends IncompleteDsnException
{
public function __construct(string $option, string $dsn = null, ?\Throwable $previous = null)
{
$message = sprintf('The option "%s" is required but missing.', $option);
parent::__construct($message, $dsn, $previous);
}
}

View File

@ -19,7 +19,7 @@ class UnsupportedSchemeException extends LogicException
private const SCHEME_TO_PACKAGE_MAP = [ private const SCHEME_TO_PACKAGE_MAP = [
'loco' => [ 'loco' => [
'class' => Bridge\Loco\Provider\LocoProviderFactory::class, 'class' => Bridge\Loco\Provider\LocoProviderFactory::class,
'package' => 'symfony/loco-translation', 'package' => 'symfony/loco-translation-provider',
], ],
]; ];
@ -31,14 +31,14 @@ class UnsupportedSchemeException extends LogicException
} }
$package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null;
if ($package && !class_exists($package['class'])) { if ($package && !class_exists($package['class'])) {
parent::__construct(sprintf('Unable to synchronize translations via "%s" as the providers is not installed; try running "composer require %s".', $provider, $package['package'])); parent::__construct(sprintf('Unable to synchronize translations via "%s" as the provider is not installed; try running "composer require %s".', $provider, $package['package']));
return; return;
} }
$message = sprintf('The "%s" scheme is not supported', $dsn->getScheme()); $message = sprintf('The "%s" scheme is not supported', $dsn->getScheme());
if ($name && $supported) { if ($name && $supported) {
$message .= sprintf('; supported schemes for translation providers "%s" are: "%s"', $name, implode('", "', $supported)); $message .= sprintf('; supported schemes for translation provider "%s" are: "%s"', $name, implode('", "', $supported));
} }
parent::__construct($message.'.'); parent::__construct($message.'.');

View File

@ -12,11 +12,11 @@
namespace Symfony\Component\Translation\Provider; namespace Symfony\Component\Translation\Provider;
use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Exception\MissingRequiredOptionException;
/** /**
* @author Mathieu Santostefano <msantostefano@protonmail.com> * @author Fabien Potencier <fabien@symfony.com>
* * @author Oskar Stark <oskarstark@googlemail.com>
* @experimental in 5.3
*/ */
final class Dsn final class Dsn
{ {
@ -25,23 +25,14 @@ final class Dsn
private $user; private $user;
private $password; private $password;
private $port; private $port;
private $options;
private $path; private $path;
private $dsn; private $options;
private $originalDsn;
public function __construct(string $scheme, string $host, ?string $user = null, ?string $password = null, ?int $port = null, array $options = [], ?string $path = null) public function __construct(string $dsn)
{ {
$this->scheme = $scheme; $this->originalDsn = $dsn;
$this->host = $host;
$this->user = $user;
$this->password = $password;
$this->port = $port;
$this->options = $options;
$this->path = $path;
}
public static function fromString(string $dsn): self
{
if (false === $parsedDsn = parse_url($dsn)) { if (false === $parsedDsn = parse_url($dsn)) {
throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN is invalid.', $dsn)); throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN is invalid.', $dsn));
} }
@ -49,21 +40,18 @@ final class Dsn
if (!isset($parsedDsn['scheme'])) { if (!isset($parsedDsn['scheme'])) {
throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a scheme.', $dsn)); throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a scheme.', $dsn));
} }
$this->scheme = $parsedDsn['scheme'];
if (!isset($parsedDsn['host'])) { if (!isset($parsedDsn['host'])) {
throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a host (use "default" by default).', $dsn)); throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a host (use "default" by default).', $dsn));
} }
$this->host = $parsedDsn['host'];
$user = isset($parsedDsn['user']) ? urldecode($parsedDsn['user']) : null; $this->user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null;
$password = isset($parsedDsn['pass']) ? urldecode($parsedDsn['pass']) : null; $this->password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null;
$port = $parsedDsn['port'] ?? null; $this->port = $parsedDsn['port'] ?? null;
$path = $parsedDsn['path'] ?? null; $this->path = $parsedDsn['path'] ?? null;
parse_str($parsedDsn['query'] ?? '', $query); parse_str($parsedDsn['query'] ?? '', $this->options);
$dsnObject = new self($parsedDsn['scheme'], $parsedDsn['host'], $user, $password, $port, $query, $path);
$dsnObject->dsn = $dsn;
return $dsnObject;
} }
public function getScheme(): string public function getScheme(): string
@ -96,6 +84,20 @@ final class Dsn
return $this->options[$key] ?? $default; return $this->options[$key] ?? $default;
} }
public function getRequiredOption(string $key)
{
if (!\array_key_exists($key, $this->options) || '' === trim($this->options[$key])) {
throw new MissingRequiredOptionException($key);
}
return $this->options[$key];
}
public function getOptions(): array
{
return $this->options;
}
public function getPath(): ?string public function getPath(): ?string
{ {
return $this->path; return $this->path;
@ -103,6 +105,6 @@ final class Dsn
public function getOriginalDsn(): string public function getOriginalDsn(): string
{ {
return $this->dsn; return $this->originalDsn;
} }
} }

View File

@ -23,7 +23,7 @@ class NullProvider implements ProviderInterface
{ {
public function __toString(): string public function __toString(): string
{ {
return NullProviderFactory::SCHEME.'://default'; return 'null';
} }
public function write(TranslatorBagInterface $translatorBag, bool $override = false): void public function write(TranslatorBagInterface $translatorBag, bool $override = false): void

View File

@ -20,19 +20,17 @@ use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
*/ */
final class NullProviderFactory extends AbstractProviderFactory final class NullProviderFactory extends AbstractProviderFactory
{ {
const SCHEME = 'null';
public function create(Dsn $dsn): ProviderInterface public function create(Dsn $dsn): ProviderInterface
{ {
if (self::SCHEME === $dsn->getScheme()) { if ('null' === $dsn->getScheme()) {
return new NullProvider(); return new NullProvider();
} }
throw new UnsupportedSchemeException($dsn, self::SCHEME, $this->getSupportedSchemes()); throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes());
} }
protected function getSupportedSchemes(): array protected function getSupportedSchemes(): array
{ {
return [self::SCHEME]; return ['null'];
} }
} }

View File

@ -37,7 +37,7 @@ class TranslationProviderCollectionFactory
$providers = []; $providers = [];
foreach ($config as $name => $currentConfig) { foreach ($config as $name => $currentConfig) {
$providers[$name] = $this->fromDsnObject( $providers[$name] = $this->fromDsnObject(
Dsn::fromString($currentConfig['dsn']), new Dsn($currentConfig['dsn']),
!$currentConfig['locales'] ? $this->enabledLocales : $currentConfig['locales'], !$currentConfig['locales'] ? $this->enabledLocales : $currentConfig['locales'],
!$currentConfig['domains'] ? [] : $currentConfig['domains'] !$currentConfig['domains'] ? [] : $currentConfig['domains']
); );

View File

@ -72,7 +72,18 @@ abstract class ProviderFactoryTestCase extends TestCase
{ {
$factory = $this->createFactory(); $factory = $this->createFactory();
$this->assertSame($expected, $factory->supports(Dsn::fromString($dsn))); $this->assertSame($expected, $factory->supports(new Dsn($dsn)));
}
/**
* @dataProvider createProvider
*/
public function testCreate(string $expected, string $dsn)
{
$factory = $this->createFactory();
$provider = $factory->create(new Dsn($dsn));
$this->assertSame($expected, (string) $provider);
} }
/** /**
@ -82,7 +93,7 @@ abstract class ProviderFactoryTestCase extends TestCase
{ {
$factory = $this->createFactory(); $factory = $this->createFactory();
$dsn = Dsn::fromString($dsn); $dsn = new Dsn($dsn);
$this->expectException(UnsupportedSchemeException::class); $this->expectException(UnsupportedSchemeException::class);
if (null !== $message) { if (null !== $message) {
@ -92,17 +103,6 @@ abstract class ProviderFactoryTestCase extends TestCase
$factory->create($dsn); $factory->create($dsn);
} }
/**
* @dataProvider createProvider
*/
public function testCreate(string $expected, string $dsn)
{
$factory = $this->createFactory();
$provider = $factory->create(Dsn::fromString($dsn));
$this->assertSame($expected, (string) $provider);
}
/** /**
* @dataProvider incompleteDsnProvider * @dataProvider incompleteDsnProvider
*/ */
@ -110,7 +110,7 @@ abstract class ProviderFactoryTestCase extends TestCase
{ {
$factory = $this->createFactory(); $factory = $this->createFactory();
$dsn = Dsn::fromString($dsn); $dsn = new Dsn($dsn);
$this->expectException(IncompleteDsnException::class); $this->expectException(IncompleteDsnException::class);
if (null !== $message) { if (null !== $message) {

View File

@ -14,7 +14,6 @@ namespace Symfony\Component\Translation\Test;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\Translation\Bridge\Loco\Provider\LocoProvider;
use Symfony\Component\Translation\Dumper\XliffFileDumper; use Symfony\Component\Translation\Dumper\XliffFileDumper;
use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Provider\ProviderInterface; use Symfony\Component\Translation\Provider\ProviderInterface;
@ -45,7 +44,7 @@ abstract class ProviderTestCase extends TestCase
/** /**
* @dataProvider toStringProvider * @dataProvider toStringProvider
*/ */
public function testToString(LocoProvider $provider, string $expected) public function testToString(ProviderInterface $provider, string $expected)
{ {
$this->assertSame($expected, (string) $provider); $this->assertSame($expected, (string) $provider);
} }

View File

@ -13,69 +13,142 @@ namespace Symfony\Component\Translation\Tests\Provider;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Exception\MissingRequiredOptionException;
use Symfony\Component\Translation\Provider\Dsn; use Symfony\Component\Translation\Provider\Dsn;
class DsnTest extends TestCase final class DsnTest extends TestCase
{ {
/** /**
* @dataProvider fromStringProvider * @dataProvider constructProvider
*/ */
public function testFromString(string $string, Dsn $expectedDsn): void public function testConstruct(string $dsnString, string $scheme, string $host, ?string $user = null, ?string $password = null, ?int $port = null, array $options = [], ?string $path = null)
{ {
$actualDsn = Dsn::fromString($string); $dsn = new Dsn($dsnString);
$this->assertSame($dsnString, $dsn->getOriginalDsn());
$this->assertSame($expectedDsn->getScheme(), $actualDsn->getScheme()); $this->assertSame($scheme, $dsn->getScheme());
$this->assertSame($expectedDsn->getHost(), $actualDsn->getHost()); $this->assertSame($host, $dsn->getHost());
$this->assertSame($expectedDsn->getPort(), $actualDsn->getPort()); $this->assertSame($user, $dsn->getUser());
$this->assertSame($expectedDsn->getUser(), $actualDsn->getUser()); $this->assertSame($password, $dsn->getPassword());
$this->assertSame($expectedDsn->getPassword(), $actualDsn->getPassword()); $this->assertSame($port, $dsn->getPort());
$this->assertSame($expectedDsn->getPath(), $actualDsn->getPath()); $this->assertSame($path, $dsn->getPath());
$this->assertSame($expectedDsn->getOption('from'), $actualDsn->getOption('from')); $this->assertSame($options, $dsn->getOptions());
$this->assertSame($string, $actualDsn->getOriginalDsn());
} }
public function fromStringProvider(): iterable public function constructProvider(): iterable
{ {
yield 'simple dsn' => [ yield 'simple dsn' => [
'scheme://localhost', 'scheme://localhost',
new Dsn('scheme', 'localhost', null, null, null, [], null), 'scheme',
'localhost',
];
yield 'simple dsn including @ sign, but no user/password/token' => [
'scheme://@localhost',
'scheme',
'localhost',
];
yield 'simple dsn including : sign and @ sign, but no user/password/token' => [
'scheme://:@localhost',
'scheme',
'localhost',
];
yield 'simple dsn including user, : sign and @ sign, but no password' => [
'scheme://user1:@localhost',
'scheme',
'localhost',
'user1',
];
yield 'simple dsn including : sign, password, and @ sign, but no user' => [
'scheme://:pass@localhost',
'scheme',
'localhost',
null,
'pass',
]; ];
yield 'dsn with user and pass' => [ yield 'dsn with user and pass' => [
'scheme://u$er:pa$s@localhost', 'scheme://u$er:pa$s@localhost',
new Dsn('scheme', 'localhost', 'u$er', 'pa$s', null, [], null), 'scheme',
'localhost',
'u$er',
'pa$s',
]; ];
yield 'dsn with user and pass and custom port' => [ yield 'dsn with user and pass and custom port' => [
'scheme://u$er:pa$s@localhost:8000', 'scheme://u$er:pa$s@localhost:8000',
new Dsn('scheme', 'localhost', 'u$er', 'pa$s', '8000', [], null), 'scheme',
'localhost',
'u$er',
'pa$s',
8000,
]; ];
yield 'dsn with user and pass, custom port and custom path' => [ yield 'dsn with user and pass, custom port and custom path' => [
'scheme://u$er:pa$s@localhost:8000/channel', 'scheme://u$er:pa$s@localhost:8000/channel',
new Dsn('scheme', 'localhost', 'u$er', 'pa$s', '8000', [], '/channel'), 'scheme',
'localhost',
'u$er',
'pa$s',
8000,
[],
'/channel',
];
yield 'dsn with user and pass, custom port, custom path and custom option' => [
'scheme://u$er:pa$s@localhost:8000/channel?from=FROM',
'scheme',
'localhost',
'u$er',
'pa$s',
8000,
[
'from' => 'FROM',
],
'/channel',
]; ];
yield 'dsn with user and pass, custom port, custom path and custom options' => [ yield 'dsn with user and pass, custom port, custom path and custom options' => [
'scheme://u$er:pa$s@localhost:8000/channel?from=FROM', 'scheme://u$er:pa$s@localhost:8000/channel?from=FROM&to=TO',
new Dsn('scheme', 'localhost', 'u$er', 'pa$s', '8000', ['from' => 'FROM'], '/channel'), 'scheme',
'localhost',
'u$er',
'pa$s',
8000,
[
'from' => 'FROM',
'to' => 'TO',
],
'/channel',
]; ];
yield 'dsn with user and pass that contains an urlencoded character' => [ yield 'dsn with user and pass, custom port, custom path and custom options and custom options keep the same order' => [
'scheme://u$er:p%2Fa$s@localhost', 'scheme://u$er:pa$s@localhost:8000/channel?to=TO&from=FROM',
new Dsn('scheme', 'localhost', 'u$er', 'p/a$s'), 'scheme',
'localhost',
'u$er',
'pa$s',
8000,
[
'to' => 'TO',
'from' => 'FROM',
],
'/channel',
]; ];
} }
/** /**
* @dataProvider invalidDsnProvider * @dataProvider invalidDsnProvider
*/ */
public function testInvalidDsn(string $dsn, string $exceptionMessage): void public function testInvalidDsn(string $dsnString, string $exceptionMessage)
{ {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage($exceptionMessage); $this->expectExceptionMessage($exceptionMessage);
Dsn::fromString($dsn);
new Dsn($dsnString);
} }
public function invalidDsnProvider(): iterable public function invalidDsnProvider(): iterable
@ -96,13 +169,94 @@ class DsnTest extends TestCase
]; ];
} }
public function testGetOption(): void /**
* @dataProvider getOptionProvider
*/
public function testGetOption($expected, string $dsnString, string $option, ?string $default = null)
{ {
$options = ['with_value' => 'some value', 'nullable' => null]; $dsn = new Dsn($dsnString);
$dsn = new Dsn('scheme', 'localhost', 'u$er', 'pa$s', '8000', $options, '/channel');
$this->assertSame('some value', $dsn->getOption('with_value')); $this->assertSame($expected, $dsn->getOption($option, $default));
$this->assertSame('default', $dsn->getOption('nullable', 'default')); }
$this->assertSame('default', $dsn->getOption('not_existent_property', 'default'));
public function getOptionProvider(): iterable
{
yield [
'foo',
'scheme://localhost?with_value=foo',
'with_value',
];
yield [
'',
'scheme://localhost?empty=',
'empty',
];
yield [
'0',
'scheme://localhost?zero=0',
'zero',
];
yield [
'default-value',
'scheme://localhost?option=value',
'non_existent_property',
'default-value',
];
}
/**
* @dataProvider getRequiredOptionProvider
*/
public function testGetRequiredOption(string $expectedValue, string $options, string $option)
{
$dsn = new Dsn(sprintf('scheme://localhost?%s', $options));
$this->assertSame($expectedValue, $dsn->getRequiredOption($option));
}
public function getRequiredOptionProvider(): iterable
{
yield [
'value',
'with_value=value',
'with_value',
];
yield [
'0',
'timeout=0',
'timeout',
];
}
/**
* @dataProvider getRequiredOptionThrowsMissingRequiredOptionExceptionProvider
*/
public function testGetRequiredOptionThrowsMissingRequiredOptionException(string $expectedExceptionMessage, string $options, string $option)
{
$dsn = new Dsn(sprintf('scheme://localhost?%s', $options));
$this->expectException(MissingRequiredOptionException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
$dsn->getRequiredOption($option);
}
public function getRequiredOptionThrowsMissingRequiredOptionExceptionProvider(): iterable
{
yield [
'The option "foo_bar" is required but missing.',
'with_value=value',
'foo_bar',
];
yield [
'The option "with_empty_string" is required but missing.',
'with_empty_string=',
'with_empty_string',
];
} }
} }

View File

@ -28,11 +28,11 @@ class NullProviderFactoryTest extends TestCase
{ {
$this->expectException(UnsupportedSchemeException::class); $this->expectException(UnsupportedSchemeException::class);
(new NullProviderFactory())->create(new Dsn('foo', '')); (new NullProviderFactory())->create(new Dsn('foo://localhost'));
} }
public function testCreate() public function testCreate()
{ {
$this->assertInstanceOf(NullProvider::class, (new NullProviderFactory())->create(new Dsn('null', ''))); $this->assertInstanceOf(NullProvider::class, (new NullProviderFactory())->create(new Dsn('null://null')));
} }
} }