Envoi de courriers postaux avec Symfony Notifier

Raphaël Geffroy

  • Développeur (Back-end)
  • PHP / Symfony & friends
  • Dr Data - Consent Store

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;
}
  1. Notification -> Message
  2. 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

  • 61