Envoi de courriers postaux avec Symfony Notifier
![](https://s3.amazonaws.com/media-p.slid.es/uploads/2720296/images/11343920/php-symfony.png)
Raphaël Geffroy
- Développeur (Back-end)
- PHP / Symfony & friends
- Dr Data - Consent Store
![](https://s3.amazonaws.com/media-p.slid.es/uploads/2720296/images/11343915/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/2720296/images/11343919/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/2720296/images/11343930/pasted-from-clipboard.png)
Notifier / Mailer
05-2019
Symfony 4.3
Lancement de Symfony Mailer
11-2019
Symfony 5.0
Introduction du Notifier en tant que composant expérimental
09-2019
Symfony Notifier
est annoncé au SymfonyLive London
05-2021
Symfony 5.3
Le composant Notifier n'est plus expérimental
The Notifier Component
11-2009
Symfony 1.3
SwiftMailer par défaut
L'envers du vendor
Notification
Message
Qu'est ce qu'un Message
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;
...
}
Qu'est ce qu'une Notification
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'];
}
}
Le Notifier
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']
Les channels
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;
}
- Notification -> Message
- MessageBus::dispatch
|| Transport::send
Les Bridges
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;
}
- TransportFactory
- Transport
- Webhook
Ex: Brevo, Esendex, Mailjet
framework:
notifier:
texter_transports:
twilio: '%env(TWILIO_DSN)%'
chatter_transports:
slack: '%env(SLACK_DSN)%'
Configuration du composant
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']
L'agrégat Transports
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);
}
}
Texter
Chatter
Et nos courriers postaux dans tout ça ?
Le Message & La Notification
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}/
Le Recipient
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/
Le Channel
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/
La Configuration
// 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/
La Configuration (suite)
// 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
Le Bridge
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
Le Bridge
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
Live Coding
Postal mails with Symfony Notifier
By Raphael Geffroy
Postal mails with Symfony Notifier
- 74