Symfony 4.3
Lancement de Symfony Mailer
Symfony 5.0
Introduction du Notifier en tant que composant expérimental
Symfony Notifier
est annoncé au SymfonyLive London
Symfony 5.3
Le composant Notifier n'est plus expérimental
The Notifier Component
Symfony 1.3
SwiftMailer par défaut
interface MessageInterface
{
public function getRecipientId(): ?string;
public function getSubject(): string;
public function getOptions(): ?MessageOptionsInterface;
public function getTransport(): ?string;
}
class SmsMessage implements MessageInterface, FromNotificationInterface
{
private ?string $transport = null;
private string $subject;
private string $phone;
private string $from;
private ?MessageOptionsInterface $options;
private ?Notification $notification = null;
...
}
class EmailMessage implements MessageInterface, FromNotificationInterface
{
private RawMessage $message;
private ?Envelope $envelope;
private ?Notification $notification = null;
...
}
class Notification
{
private array $channels = [];
private string $subject = '';
...
private string $importance = self::IMPORTANCE_HIGH;
...
}
class InvoiceNotification extends Notification implements EmailNotificationInterface, SmsNotificationInterface
{
private const int MAX_EMAIL_PRICE_AMOUNT = 10_000;
public function __construct(
private readonly int $price,
) {
}
public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): EmailMessage
{
$message = new RawMessage('You\'re invoiced '.strval($this->price).' EUR.');
return new EmailMessage($message);
}
public function asSmsMessage(RecipientInterface $recipient, ?string $transport = null): SmsMessage
{
return new SmsMessage($recipient->getPhone(), 'You\'re invoiced '.strval($this->price).' EUR.');
}
public function getChannels(RecipientInterface $recipient): array
{
if($this->price > self::MAX_EMAIL_PRICE_AMOUNT){
return ['sms'];
}
return ['email'];
}
}
interface NotifierInterface
{
public function send(
Notification $notification,
RecipientInterface ...$recipients
): void;
}
interface RecipientInterface
{
}
interface EmailRecipientInterface extends RecipientInterface
{
public function getEmail(): string;
}
framework:
notifier:
channel_policy:
urgent: ['sms', 'chat/slack', 'email']
high: ['chat/slack']
medium: ['browser']
low: ['browser']
EmailChannel
SmsChannel
ChatChannel
PushChannel
BrowserChannel
interface ChannelInterface
{
public function notify(
Notification $notification,
RecipientInterface $recipient,
?string $transportName = null
): void;
public function supports(
Notification $notification,
RecipientInterface $recipient
): bool;
}
|| Transport::send
interface TransportInterface extends \Stringable
{
/**
* @throws TransportExceptionInterface
*/
public function send(MessageInterface $message): ?SentMessage;
public function supports(MessageInterface $message): bool;
}
interface TransportFactoryInterface
{
/**
* @throws UnsupportedSchemeException
* @throws IncompleteDsnException
* @throws MissingRequiredOptionException
*/
public function create(Dsn $dsn): TransportInterface;
public function supports(Dsn $dsn): bool;
}
Ex: Brevo, Esendex, Mailjet
framework:
notifier:
texter_transports:
twilio: '%env(TWILIO_DSN)%'
chatter_transports:
slack: '%env(SLACK_DSN)%'
framework:
notifier:
chatter_transports:
dummychat: 'dummychat://Us3r:p4ssW0rd@default'
texter_transports:
failover: '%env(SLACK_DSN)% || %env(TELEGRAM_DSN)%'
roundrobin: '%env(SLACK_DSN)% && %env(TELEGRAM_DSN)%'
channel_policy:
urgent: ['sms']
high: ['sms']
medium: ['chat']
low: ['chat']
final class Transports implements TransportInterface
{
/**
* @var array<string, TransportInterface>
*/
private array $transports = [];
...
public function send(MessageInterface $message): SentMessage
{
if (!$transport = $message->getTransport()) {
foreach ($this->transports as $transport) {
if ($transport->supports($message)) {
return $transport->send($message);
}
}
throw new LogicException(...);
}
if (!isset($this->transports[$transport])) {
throw new InvalidArgumentException(...);
}
if (!$this->transports[$transport]->supports($message)) {
throw new LogicException(...);
}
return $this->transports[$transport]->send($message);
}
}
class PostalMailMessage implements MessageInterface, FromNotificationInterface
{
private ?string $transport = null;
private ?Notification $notification = null;
public function __construct(
private RawLetter $letter,
private PostalAddress $recipientPostalAddress,
private ?MessageOptionsInterface $options = null,
) {
}
...
}
interface PostalMailNotificationInterface
{
public function asPostalMailMessage(
PostalMailRecipientInterface $recipient,
?string $transport = null
): ?PostalMailMessage;
}
Symfony/Component/Notifier/{Notification|Message}/
interface PostalMailRecipientInterface extends RecipientInterface
{
public function getPostalAddress(): PostalAddress;
}
readonly class PostalAddress
{
public function __construct(
public string $name,
public string $streetAddress,
public string $postalCode,
public string $city,
public string $country
){
}
}
Symfony/Component/Notifier/Recipient/
class PostalMailChannel extends AbstractChannel
{
public function notify(
Notification $notification,
RecipientInterface $recipient,
?string $transportName = null
): void {
$message = null;
if ($notification instanceof PostalMailNotificationInterface) {
$message = $notification->asPostalMailMessage($recipient, $transportName);
}
$message ??= PostalMailMessage::fromNotification($notification);
if (null !== $transportName) {
$message->transport($transportName);
}
if (null === $this->bus) {
$this->transport->send($message);
} else {
$this->bus->dispatch($message);
}
}
...
}
Symfony/Component/Notifier/Channel/
// notifier.php
...
->set('notifier.channel.postal_mail', PostalMailChannel::class)
->args([
service('poster.transports'),
abstract_arg('message bus'),
])
->tag('notifier.channel', ['channel' => 'postal_mail'])
...
->set('poster.transports', Transports::class)
->factory([service('poster.transport_factory'), 'fromStrings'])
->args([[]])
->set('poster.transport_factory', Transport::class)
->args([tagged_iterator('poster.transport_factory')])
->set('poster.messenger.postal_mail_handler', MessageHandler::class)
->args([service('poster.transports')])
->tag('messenger.message_handler', ['handles' => PostalMailMessage::class])
...
Symfony/Component/Bundle/FrameworkBundle/Resources/config/
// Configuration.php
...
->fixXmlConfig('poster_transport')
->children()
->arrayNode('poster_transports')
->useAttributeAsKey('name')
->prototype('scalar')->end()
->end()
->end()
...
// FrameworkExtension.php
...
if ($config['poster_transports']) {
$container->getDefinition('poster.transports')
->setArgument(0, $config['poster_transports']);
}
...
Symfony/Component/Bundle/FrameworkBundle/DependencyInjection
TransportFactory
class MySendingBoxTransportFactory extends AbstractTransportFactory
{
public final const string SCHEME = 'mysendingbox';
public function create(Dsn $dsn): MySendingBoxTransport
{
$scheme = $dsn->getScheme();
if($scheme !== self::SCHEME){
throw new UnsupportedSchemeException(...);
}
return new MySendingBoxTransport(
$this->getUser($dsn).':',
$this->dispatcher,
$this->client
);
}
protected function getSupportedSchemes(): array
{
return [self::SCHEME];
}
}
Symfony/Component/Component/Notifier/Bridge/MySendingBox
Transport
class MySendingBoxTransport extends AbstractTransport
{
...
protected function doSend(MessageInterface $message): SentMessage
{
...
$response = $this->client->request(
'POST',
'https://'.$this->getEndpoint().'/letters',
[
'json' => [
'to' => $to,
'color' => 'bw',
'postage_type' => 'ecopli',
'source_file' => $message->getSubject(),
'source_file_type' => 'html'
] + $from,
'auth_basic' => $this->apiKey,
]
);
...
}
...
}
Symfony/Component/Component/Notifier/Bridge/MySendingBox