feature #33409 [Mailer] Add support for multiple mailers (fabpot)

This PR was merged into the 4.4 branch.

Discussion
----------

[Mailer] Add support for multiple mailers

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | closes #32535
| License       | MIT
| Doc PR        | -

This adds the possibility to define several email transports.

If you only have one email transport,  nothing changes:

```yaml
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'
```

But if you need more than one, use the `transports` entry instead:

```yaml
framework:
    mailer:
        transports:
            main: '%env(MAILER_DSN)%'
            important: '%env(MAILER_DSN_IMPORTANT)%'
```

Then, when sending an email via the `Mailer`, pass the mailer name explicitly (by default, the first one is used):

```php
// use the first "main" transport
$mailer->send($email);

// or use the "important" one (`null` is the envelope)
$mailer->send($email, null, 'important');
```

The web profiler now displays the name and the shorten DSN.

Commits
-------

de5fae4dd8 [Mailer] Add support for multiple mailers
This commit is contained in:
Fabien Potencier 2019-09-02 16:12:44 +02:00
commit 2ad7f97568
9 changed files with 115 additions and 7 deletions

View File

@ -1555,8 +1555,17 @@ class Configuration implements ConfigurationInterface
->arrayNode('mailer')
->info('Mailer configuration')
->{!class_exists(FullStack::class) && class_exists(Mailer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->validate()
->ifTrue(function ($v) { return isset($v['dsn']) && \count($v['transports']); })
->thenInvalid('"dsn" and "transports" cannot be used together.')
->end()
->fixXmlConfig('transport')
->children()
->scalarNode('dsn')->defaultValue('smtp://null')->end()
->scalarNode('dsn')->defaultNull()->end()
->arrayNode('transports')
->useAttributeAsKey('name')
->prototype('scalar')->end()
->end()
->arrayNode('envelope')
->info('Mailer Envelope configuration')
->children()

View File

@ -1977,7 +1977,12 @@ class FrameworkExtension extends Extension
$loader->load('mailer.xml');
$loader->load('mailer_transports.xml');
$container->getDefinition('mailer.default_transport')->setArgument(0, $config['dsn']);
if (!\count($config['transports']) && null === $config['dsn']) {
$config['dsn'] = 'smtp://null';
}
$transports = $config['dsn'] ? ['main' => $config['dsn']] : $config['transports'];
$container->getDefinition('mailer.transports')->setArgument(0, $transports);
$container->getDefinition('mailer.default_transport')->setArgument(0, current($transports));
$classToServices = [
SesTransportFactory::class => 'mailer.transport_factory.amazon',

View File

@ -6,13 +6,18 @@
<services>
<service id="mailer.mailer" class="Symfony\Component\Mailer\Mailer">
<argument type="service" id="mailer.default_transport" />
<argument type="service" id="mailer.transports" />
<argument type="service" id="messenger.default_bus" on-invalid="ignore" />
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
</service>
<service id="mailer" alias="mailer.mailer" />
<service id="Symfony\Component\Mailer\MailerInterface" alias="mailer.mailer" />
<service id="mailer.transports" class="Symfony\Component\Mailer\Transports">
<factory service="mailer.transport_factory" method="fromStrings" />
<argument type="collection" /> <!-- transports -->
</service>
<service id="mailer.transport_factory" class="Symfony\Component\Mailer\Transport">
<argument type="tagged_iterator" tag="mailer.transport_factory" />
</service>
@ -24,7 +29,7 @@
<service id="Symfony\Component\Mailer\Transport\TransportInterface" alias="mailer.default_transport" />
<service id="mailer.messenger.message_handler" class="Symfony\Component\Mailer\Messenger\MessageHandler">
<argument type="service" id="mailer.default_transport" />
<argument type="service" id="mailer.transports" />
<tag name="messenger.message_handler" />
</service>

View File

@ -369,7 +369,8 @@ class ConfigurationTest extends TestCase
'scoped_clients' => [],
],
'mailer' => [
'dsn' => 'smtp://null',
'dsn' => null,
'transports' => [],
'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class),
],
];

View File

@ -55,7 +55,7 @@ class MailerTest extends AbstractWebTestCase
}
};
$mailer = new Mailer($testTransport, null);
$mailer = new Mailer($testTransport);
$message = (new Email())
->subject('Test subject')

View File

@ -4,6 +4,7 @@ CHANGELOG
4.4.0
-----
* 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)
* [BC BREAK] Removed the `encryption` DSN option (use `smtps` instead)

View File

@ -22,7 +22,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Mailer implements MailerInterface
final class Mailer implements MailerInterface
{
private $transport;
private $bus;

View File

@ -27,6 +27,7 @@ use Symfony\Component\Mailer\Transport\SendmailTransportFactory;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory;
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mailer\Transport\Transports;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -54,6 +55,13 @@ class Transport
return $factory->fromString($dsn);
}
public static function fromDsns(array $dsns, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface
{
$factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger)));
return $factory->fromStrings($dsns);
}
/**
* @param TransportFactoryInterface[] $factories
*/
@ -62,6 +70,16 @@ class Transport
$this->factories = $factories;
}
public function fromStrings(array $dsns): Transports
{
$transports = [];
foreach ($dsns as $name => $dsn) {
$transports[$name] = $this->fromString($dsn);
}
return new Transports($transports);
}
public function fromString(string $dsn): TransportInterface
{
$dsns = preg_split('/\s++\|\|\s++/', $dsn);

View File

@ -0,0 +1,69 @@
<?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\Mailer\Transport;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\SmtpEnvelope;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Transports implements TransportInterface
{
private $transports;
private $default;
/**
* @param TransportInterface[] $transports
*/
public function __construct(iterable $transports)
{
$this->transports = [];
foreach ($transports as $name => $transport) {
if (null === $this->default) {
$this->default = $transport;
}
$this->transports[$name] = $transport;
}
if (!$this->transports) {
throw new LogicException(sprintf('"%s" must have at least one transport configured.', __CLASS__));
}
}
public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage
{
/** @var Message $message */
if (RawMessage::class === \get_class($message) || !$message->getHeaders()->has('X-Transport')) {
return $this->default->send($message, $envelope);
}
$headers = $message->getHeaders();
$transport = $headers->get('X-Transport');
$headers->remove('X-Transport');
if (!isset($this->transports[$transport])) {
throw new InvalidArgumentException(sprintf('The "%s" transport does not exist.', $transport));
}
return $this->transports[$transport]->send($message, $envelope);
}
public function __toString(): string
{
return 'all';
}
}