diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 509e1b657c9ae..3abe095c0621c 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -4,6 +4,18 @@ CHANGELOG 4.4.0 ----- + * [BC BREAK] changed the syntax for failover and roundrobin DSNs + + Before: + + dummy://a || dummy://b (for failover) + dummy://a && dummy://b (for roundrobin) + + After: + + failover(dummy://a dummy://b) + roundrobin(dummy://a dummy://b) + * added support for multiple transports on a `Mailer` instance * [BC BREAK] removed the `auth_mode` DSN option (it is now always determined automatically) * STARTTLS cannot be enabled anymore (it is used automatically if TLS is disabled and the server supports STARTTLS) diff --git a/src/Symfony/Component/Mailer/Tests/Transport/FailoverTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/FailoverTransportTest.php index 2fb1b422f31d5..7e66309cabb41 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/FailoverTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/FailoverTransportTest.php @@ -36,7 +36,7 @@ public function testToString() $t2 = $this->createMock(TransportInterface::class); $t2->expects($this->once())->method('__toString')->willReturn('t2://local'); $t = new FailoverTransport([$t1, $t2]); - $this->assertEquals('t1://local || t2://local', (string) $t); + $this->assertEquals('failover(t1://local t2://local)', (string) $t); } public function testSendFirstWork() diff --git a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php index a703eb36bf15d..add578b233744 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php @@ -35,7 +35,7 @@ public function testToString() $t2 = $this->createMock(TransportInterface::class); $t2->expects($this->once())->method('__toString')->willReturn('t2://local'); $t = new RoundRobinTransport([$t1, $t2]); - $this->assertEquals('t1://local && t2://local', (string) $t); + $this->assertEquals('roundrobin(t1://local t2://local)', (string) $t); } public function testSendAlternate() diff --git a/src/Symfony/Component/Mailer/Tests/TransportTest.php b/src/Symfony/Component/Mailer/Tests/TransportTest.php index 47b352c7dd143..c82c83c6f9b38 100644 --- a/src/Symfony/Component/Mailer/Tests/TransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/TransportTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Mailer\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\SmtpEnvelope; use Symfony\Component\Mailer\Transport; @@ -44,14 +45,42 @@ public function fromStringProvider(): iterable ]; yield 'failover transport' => [ - 'dummy://a || dummy://b', + 'failover(dummy://a dummy://b)', new FailoverTransport([$transportA, $transportB]), ]; yield 'round robin transport' => [ - 'dummy://a && dummy://b', + 'roundrobin(dummy://a dummy://b)', new RoundRobinTransport([$transportA, $transportB]), ]; + + yield 'mixed transport' => [ + 'roundrobin(dummy://a failover(dummy://b dummy://a) dummy://b)', + new RoundRobinTransport([$transportA, new FailoverTransport([$transportB, $transportA]), $transportB]), + ]; + } + + /** + * @dataProvider fromWrongStringProvider + */ + public function testFromWrongString(string $dsn, string $error): void + { + $transportFactory = new Transport([new DummyTransportFactory()]); + + $this->expectExceptionMessage($error); + $this->expectException(InvalidArgumentException::class); + $transportFactory->fromString($dsn); + } + + public function fromWrongStringProvider(): iterable + { + yield 'garbage at the end' => ['dummy://a some garbage here', 'The DSN has some garbage at the end: some garbage here.']; + + yield 'not a valid DSN' => ['something not a dsn', 'The "something" mailer DSN must contain a scheme.']; + + yield 'failover not closed' => ['failover(dummy://a', 'The "(dummy://a" mailer DSN must contain a scheme.']; + + yield 'not a valid keyword' => ['foobar(dummy://a)', 'The "foobar" keyword is not valid (valid ones are "failover", "roundrobin")']; } } diff --git a/src/Symfony/Component/Mailer/Transport.php b/src/Symfony/Component/Mailer/Transport.php index 3949fa78c508e..d3c4e58f6a3ff 100644 --- a/src/Symfony/Component/Mailer/Transport.php +++ b/src/Symfony/Component/Mailer/Transport.php @@ -18,6 +18,7 @@ use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\UnsupportedHostException; use Symfony\Component\Mailer\Transport\Dsn; use Symfony\Component\Mailer\Transport\FailoverTransport; @@ -82,17 +83,59 @@ public function fromStrings(array $dsns): Transports public function fromString(string $dsn): TransportInterface { - $dsns = preg_split('/\s++\|\|\s++/', $dsn); - if (\count($dsns) > 1) { - return new FailoverTransport($this->createFromDsns($dsns)); + list($transport, $offset) = $this->parseDsn($dsn); + if ($offset !== \strlen($dsn)) { + throw new InvalidArgumentException(sprintf('The DSN has some garbage at the end: %s.', substr($dsn, $offset))); } - $dsns = preg_split('/\s++&&\s++/', $dsn); - if (\count($dsns) > 1) { - return new RoundRobinTransport($this->createFromDsns($dsns)); - } + return $transport; + } + + private function parseDsn(string $dsn, int $offset = 0): array + { + static $keywords = [ + 'failover' => FailoverTransport::class, + 'roundrobin' => RoundRobinTransport::class, + ]; + + while (true) { + foreach ($keywords as $name => $class) { + $name .= '('; + if ($name === substr($dsn, $offset, \strlen($name))) { + $offset += \strlen($name) - 1; + preg_match('{\(([^()]|(?R))*\)}A', $dsn, $matches, 0, $offset); + if (!isset($matches[0])) { + continue; + } + + ++$offset; + $args = []; + while (true) { + list($arg, $offset) = $this->parseDsn($dsn, $offset); + $args[] = $arg; + if (\strlen($dsn) === $offset) { + break; + } + ++$offset; + if (')' === $dsn[$offset - 1]) { + break; + } + } + + return [new $class($args), $offset]; + } + } + + if (preg_match('{(\w+)\(}A', $dsn, $matches, 0, $offset)) { + throw new InvalidArgumentException(sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords)))); + } + + if ($pos = strcspn($dsn, ' )', $offset)) { + return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset, $pos))), $offset + $pos]; + } - return $this->fromDsnObject(Dsn::fromString($dsn)); + return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset))), \strlen($dsn)]; + } } public function fromDsnObject(Dsn $dsn): TransportInterface @@ -106,21 +149,6 @@ public function fromDsnObject(Dsn $dsn): TransportInterface throw new UnsupportedHostException($dsn); } - /** - * @param string[] $dsns - * - * @return TransportInterface[] - */ - private function createFromDsns(array $dsns): array - { - $transports = []; - foreach ($dsns as $dsn) { - $transports[] = $this->fromDsnObject(Dsn::fromString($dsn)); - } - - return $transports; - } - private static function getDefaultFactories(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): iterable { foreach (self::FACTORY_CLASSES as $factoryClass) { diff --git a/src/Symfony/Component/Mailer/Transport/FailoverTransport.php b/src/Symfony/Component/Mailer/Transport/FailoverTransport.php index 8722aa4be0a26..7d8f54c011f9c 100644 --- a/src/Symfony/Component/Mailer/Transport/FailoverTransport.php +++ b/src/Symfony/Component/Mailer/Transport/FailoverTransport.php @@ -31,6 +31,6 @@ protected function getNextTransport(): ?TransportInterface protected function getNameSymbol(): string { - return '||'; + return 'failover'; } } diff --git a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php index f2029fb4338b0..9180817d0e28e 100644 --- a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php +++ b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php @@ -58,9 +58,9 @@ public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentM public function __toString(): string { - return implode(' '.$this->getNameSymbol().' ', array_map(function (TransportInterface $transport) { + return $this->getNameSymbol().'('.implode(' ', array_map(function (TransportInterface $transport) { return (string) $transport; - }, $this->transports)); + }, $this->transports)).')'; } /** @@ -99,7 +99,7 @@ protected function isTransportDead(TransportInterface $transport): bool protected function getNameSymbol(): string { - return '&&'; + return 'roundrobin'; } private function moveCursor(int $cursor): int