[Mailer] Change the syntax for DSNs using failover or roundrobin

This commit is contained in:
Fabien Potencier 2019-09-06 16:21:30 +02:00
parent b7371ea5c6
commit 39dd213960
7 changed files with 100 additions and 31 deletions

View File

@ -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)

View File

@ -36,7 +36,7 @@ class FailoverTransportTest extends TestCase
$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()

View File

@ -35,7 +35,7 @@ class RoundRobinTransportTest extends TestCase
$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()

View File

@ -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 @@ class TransportTest extends TestCase
];
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")'];
}
}

View File

@ -18,6 +18,7 @@ use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory
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 @@ class Transport
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;
}
return $this->fromDsnObject(Dsn::fromString($dsn));
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(substr($dsn, $offset))), \strlen($dsn)];
}
}
public function fromDsnObject(Dsn $dsn): TransportInterface
@ -106,21 +149,6 @@ class Transport
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) {

View File

@ -31,6 +31,6 @@ class FailoverTransport extends RoundRobinTransport
protected function getNameSymbol(): string
{
return '||';
return 'failover';
}
}

View File

@ -58,9 +58,9 @@ class RoundRobinTransport implements TransportInterface
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 @@ class RoundRobinTransport implements TransportInterface
protected function getNameSymbol(): string
{
return '&&';
return 'roundrobin';
}
private function moveCursor(int $cursor): int