$30 off During Our Annual Pro Sale. View Details »

Symfony Notifier

Symfony Notifier

Fabien Potencier

September 13, 2019
Tweet

More Decks by Fabien Potencier

Other Decks in Technology

Transcript

  1. Back to the basics...
    Fabien Potencier
    @fabpot

    View Slide

  2. Building flexible high-level abstractions

    on top of low-level ones
    Embrace the Linux philosophy
    Web: HttpFoundation, HttpKernel, Routing, ...
    Infrastructure: Dotenv, Console, DependencyInjection, Cache, Lock, Messenger, ...
    Language: VarDumper, VarExporter, HttpClient, Polyfill, Intl, Process, Ldap, ...
    Features: ExpressionLanguage, Workflow, Mailer,...

    View Slide

  3. Symfony Mailer > Swiftmailer
    HttpClient < Easy third-party provider support (HTTP APIs)
    Messenger < Easy asynchronous emails
    Twig < Easy email composition
    Dotenv < Easy transport configuration via DSNs
    ...

    View Slide

  4. Symfony Mailer > Swiftmailer
    PHPUnit test assertions
    Profiler support
    ...
    use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
    class SomeTest extends WebTestCase
    {
    public function testMailerAssertions()
    {
    $client = $this->createClient();
    $client->request('GET', '/send_email');
    $this->assertEmailCount(1);
    $this->assertQueuedEmailCount(1);
    $this->assertEmailIsQueued($this->getMailerEvent(0));
    $this->assertEmailIsNotQueued($this->getMailerEvent(1));
    $email = $this->getMailerMessage(0);
    $this->assertEmailHasHeader($email, 'To');
    $this->assertEmailHeaderSame($email, 'To', 'fabien@symfony
    $this->assertEmailTextBodyContains($email, 'Bar');
    $this->assertEmailHtmlBodyContains($email, 'Foo');
    $this->assertEmailAttachementCount($email, 1);
    }
    }
    Consistent
    Well integrated

    View Slide

  5. Building flexible high-level abstractions

    on top of low-level ones
    What's next?

    View Slide

  6. Building flexible high-level abstractions

    on top of low-level ones
    System Emails?

    View Slide

  7. Symfony Mailer
    + Twig with TwigExtraBundle - for auto-configuration, Twig 1.12+

    + Twig inky-extra package - Twig 1.12+

    + Zurb Foundation for Emails CSS stylesheet

    + Twig cssinliner-extra package - Twig 1.12+

    + Optimized default Twig templates
    = Built-in responsive, flexible, and generic system emails

    View Slide

  8. use Symfony\Bridge\Twig\Mime\SystemEmail;
    $email = (new SystemEmail())
    ->from('[email protected]')
    ->to('[email protected]')
    ->subject('You have a new customer!')
    ->lines(
    'Fabien has just signed up for the most expensive plan.',

    'That\'s an additional revenue of 1€ per year.'
    )
    ->action(
    'Check the invoice'
    $urlGen->generate('invoice', ['id' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
    )
    ->importance(SystemEmail::HIGH)
    ;
    $mailer->send($email);
    A typical System Email

    View Slide

  9. A typical System Email
    Color depends

    on the importance

    of the email
    Lines
    Action

    View Slide

  10. $email = (new SystemEmail())
    ->from('[email protected]')
    ->to('[email protected]')
    ->exception($exception)
    ->...
    ;
    $mailer->send($email);
    When something goes wrong?

    View Slide

  11. # PROJECT_DIR/templates/email/system.html.twig
    $email = (new SystemEmail())
    ->htmlTemplate('email/system.html.twig')
    ->textTemplate('email/system.txt.twig')
    ->...
    ;
    ...and configurable

    View Slide

  12. {% extends "@email/system.html.twig" %}
    {% block style %}
    {{ parent() }}
    .container.body_alert {
    border-top: 30px solid #ec5840;
    }
    {% endblock %}
    {% block lines %}
    This is an automated email for the MyApp application.
    {{ parent() }}
    {% endblock %}
    {% block action %}
    {{ parent() }}

    Go to MyApp
    {% endblock %}
    {% block exception %}{% endblock %}
    {% block footer_content %}
    © MyApp
    {% endblock %}
    ...and configurable
    Productivity && Flexibility

    View Slide

  13. Email are Messages...

    View Slide

  14. Sending SMS Messages

    View Slide

  15. /**
    * @Route("/checkout/thankyou")
    */
    public function thankyou(Texter $texter /* ... */)
    {
    $sms = new SmsMessage('+1415999888', 'Revenue has just increased by 1€ per year!');
    $texter->send($sms);
    return $this->render('checkout/thankyou.html.twig', [
    // ...
    ]);
    }
    Sending SMS Messages the easy way

    View Slide

  16. Symfony HttpClient

    + Symfony Messenger

    + Built-on third-party providers
    = Sending async SMS in one LOC!
    Symfony Texter
    Similar infrastructure

    as Mailer

    View Slide

  17. $sms = new SmsMessage('+1415999888', 'Revenue has just increased!');
    $twilio = Transport::fromDsn('twilio://SID:TOKEN@default?from=FROM');
    $twilio->send($sms);
    $nexmo = Transport::fromDsn('nexmo://KEY:SECRET@default?from=FROM');
    $nexmo->send($sms);
    SMS... low-level API
    Similar infrastructure

    as Mailer

    View Slide

  18. $texter = new Texter($twilio, $bus);
    $texter->send($sms);
    $transports = new Transports(['twilio' => $twilio, 'nexmo' => $nexmo]);
    $texter = new Texter($transports, $bus);
    $texter->send($sms);
    $sms->setTransport('nexmo');
    $texter->send($sms);
    $bus->dispatch($sms);
    SMS... higher-level API

    View Slide

  19. $dsn = 'failover(twilio://SID:TOKEN@default?from=FROM nexmo://KEY:SECRET@default?from=FROM)';
    SMS... higher-level API
    Similar infrastructure

    as Mailer

    View Slide

  20. Sending ... Messages

    View Slide

  21. /**
    * @Route("/checkout/thankyou")
    */
    public function thankyou(Chatter $chatter /* ... */)
    {
    $message = new ChatMessage('Revenue increased by 1€ per year...');
    $chatter->send($message);
    return $this->render('checkout/thankyou.html.twig', [
    // ...
    ]);
    }
    Sending Messages the easy way

    View Slide

  22. $message = new ChatMessage('Revenue increased by 1€ per year...');
    $slack = Transport::fromDsn('slack://TOKEN@default?channel=CHANNEL');
    $slack->send($sms);
    $telegram = Transport::fromDsn('telegram://TOKEN@default?channel=CHAT_ID');
    $telegram->send($sms);
    Messages... low-level API

    View Slide

  23. $transports = Transport::fromDsns([
    'slack' => 'slack://TOKEN@default?channel=CHANNEL',
    'telegram' => 'telegram://TOKEN@default?channel=CHAT_ID'
    ]);
    $chatter = new Chatter($transports, $bus);
    $chatter->send($message);
    $message->setTransport('telegram');
    $chatter->send($message);
    $bus->dispatch($message);
    Messages... higher-level API

    View Slide

  24. $options = (new SlackOptions())
    ->iconEmoji('tada')
    ->iconUrl('https://symfony.com')
    ->username('SymfonyNext')
    ->channel($channel)
    ->block((new SlackSectionBlock())->text('Some Text'))
    ->block(new SlackDividerBlock())
    ->block((new SlackSectionBlock())
    ->text('Some Text in another block')
    ->accessory(new SlackImageBlockElement('http://placekitten.com/700/500', 'kitten'))
    )
    ;
    $message = new ChatMessage('Default Text', $options);
    Messages... higher-level API

    View Slide

  25. $dsn = 'all(slack://TOKEN@default?channel=CHANNEL telegram://TOKEN@default?channel=CHAT_ID)';
    Messages... higher-level API

    View Slide

  26. Symfony HttpClient

    + Symfony Messenger

    + Built-on third-party providers
    = Sending async Messages in one LOC!
    Symfony Chatter
    Similar infrastructure

    as Mailer

    View Slide

  27. interface TransportInterface
    {
    public function send(MessageInterface $message): void;
    public function supports(MessageInterface $message): bool;
    public function __toString(): string;
    }
    A common transport layer
    interface MessageInterface
    {
    public function getRecipientId(): ?string;
    public function getText(): string;
    public function getOptions(): ?MessageOptionsInterface;
    public function getTransport(): ?string;
    }

    View Slide

  28. failover(twilio://SID:TOKEN@default?from=FROM telegram://TOKEN@default?channel=CHAT_ID)
    all(twilio://SID:TOKEN@default?from=FROM telegram://TOKEN@default?channel=CHAT_ID)
    SMS or/and Chat Message?

    View Slide

  29. Building flexible high-level abstractions

    on top of low-level ones
    Mailer +Texter + Chatter = ?

    View Slide

  30. Notifier

    View Slide

  31. /**
    * @Route("/checkout/thankyou")
    */
    public function thankyou(NotifierInterface $notifier /* ... */)
    {
    $notification = new Notification('New customer!', ['sms', 'chat/slack', 'email']);
    $notifier->send($notification, new Receiver('[email protected]'));
    return $this->render('checkout/thankyou.html.twig', [
    // ...
    ]);
    }
    Notify Messages!
    Channels: email, sms, chat, ...
    Transport: slack, telegram, twilio, ...

    View Slide

  32. class InvoiceNotification extends Notification
    {
    private $price;
    public function __construct(int $price)
    {
    parent::__construct('You have a new invoice.');
    $this->price = $price;
    }
    public function getChannels(Receiver $receiver): array
    {
    if ($this->price > 1000 && $receiver->hasPhone()) {
    return ['sms', 'email'];
    }
    return ['email'];
    }
    }
    Customize Notifications

    View Slide

  33. class InvoiceNotification extends Notification implements ChatNotificationInterface
    {
    public function asChatMessage(Receiver $receiver, string $transport): ChatMessage
    {
    if ('slack' === $transport) {
    return new ChatMessage('FOR SLACK: '.$this->getText());
    }
    if ('telegram' === $transport) {
    return new ChatMessage('FOR TELEGRAM: '.$this->getText());
    }
    return null;
    }
    }
    Customize Notifications
    and/or SmsNotificationInterface
    ::asSmsMessage()

    View Slide

  34. framework:
    notifier:
    chatter_transports:
    slack: '%env(SLACK_DSN)%'
    telegram: '%env(TELEGRAM_DSN)%'
    texter_transports:
    twilio: '%env(TWILIO_DSN)%'
    nexmo: '%env(NEXMO_DSN)%'
    $ composer req twilio-notifier telegram-notifier
    Notifier Semantic Configuration
    NotifierInterface
    $notifier
    Chatter $chatter
    Texter $texter

    View Slide

  35. Channels
    Mailer

    Chatter

    Texter

    Browser ~= $request->getSession()->getFlashBag()->add('notice', 'New customer!');

    Pusher - iOS/Android/Desktop native notifications

    Database - Web Notification Center

    ...
    = A unified way to notify Users

    via a unified Transport layer

    View Slide

  36. Telegram ~ 40 LOCs

    Nexmo ~ 40 LOCs

    Twilio ~ 40 LOCs

    ... including DSN support
    Each integration is a few LOCs
    namespace Symfony\Component\Notifier\Bridge\Twilio;
    final class TwilioTransport extends AbstractTransport
    {
    protected const HOST = 'api.twilio.com';
    public function __construct(string $accountSid, string $authToken, string $from, HttpClientInter
    {
    $this->accountSid = $accountSid; $this->authToken = $authToken; $this->from = $from;
    parent::__construct($client, $dispatcher);
    }
    public function __toString(): string
    {
    return sprintf('twilio://%s?from=%s', $this->getEndpoint(), $this->from);
    }
    public function supports(MessageInterface $message): bool
    {
    return $message instanceof SmsMessage;
    }
    protected function doSend(MessageInterface $message): void
    {
    if (!$message instanceof SmsMessage) {
    throw new LogicException(sprintf('The "%s" transport only support instances of "%s".', _
    }
    $endpoint = sprintf('https://%s/2010-04-01/Accounts/%s/Messages.json', $this->getEndpoint(),
    $response = $this->client->request('POST', $endpoint, [
    'auth_basic' => $this->accountSid.':'.$this->authToken,
    'body' => ['From' => $this->from, 'To' => $message->getPhone(), 'Body' => $message->getT
    ],
    ]);
    if (201 !== $response->getStatusCode()) {
    $error = json_decode($response->getContent(false), true);
    throw new TransportException(sprintf('Unable to send the SMS: %s (see %s).', $error['mes
    }
    }
    }
    Help needed

    to support more transports

    View Slide

  37. /**
    * @Route("/checkout/thankyou")
    */
    public function thankyou(NotifierInterface $notifier /* ... */)
    {
    $notification = new Notification('Invoice paid!', ['browser']);
    $notifier->send($notification, new NoReceiver());
    return $this->render('checkout/thankyou.html.twig', [
    // ...
    ]);
    }
    Many Channels / Transports

    View Slide

  38. What about a SystemNotification?
    Remember SystemEmail?

    View Slide

  39. /**
    * @Route("/checkout/thankyou")
    */
    public function thankyou(NotifierInterface $notifier /* ... */)
    {
    $notifier->send(new SystemNotification('New customer!'), ...Notifier::getSystemReceivers());
    return $this->render('checkout/thankyou.html.twig', [
    // ...
    ]);
    }
    SystemNotification for admin messages
    Global

    Receivers
    Auto-configured
    Channels

    View Slide

  40. (new SystemNotification('New customer!')->importance(SystemNotification::URGENT))
    Importance to configure the Channels
    Defines the
    channels

    View Slide

  41. framework:
    notifier:
    channel_policy:
    urgent: ['email', 'chat/slack', 'sms']
    high: ['email', 'chat/slack']
    medium: ['email']
    low: ['pigeon']
    Channels auto-configuration
    Importance
    Channels/
    Transports

    View Slide

  42. class SystemNotification extends Notification implements ChatNotificationInterface, EmailNotificationInterface
    {
    public function asEmailMessage(Receiver $receiver): ?Email
    {
    return (new SystemEmail())
    ->to($receiver->getEmail())
    ->subject($this->getText())
    ->text($this->getText())
    ->importance($this->getImportance())
    ;
    }
    public function asChatMessage(Receiver $receiver, string $transport): ?ChatMessage
    {
    if ('slack' === $transport) {
    return new ChatMessage($this->getText(), (new SlackOptions())->iconEmoji($this->getEmoji()));
    }
    return null;
    }
    }
    Enhanced Representation

    View Slide

  43. $notification = new ExceptionNotification(new \RuntimeException('Something went ...'));
    ExceptionNotification
    Uses
    SystemEmail with
    Exception support

    View Slide

  44. Send Messages via a unified API (Email, Browser, SMS, Chat, ...)

    Send to one or many Receivers

    Default configuration or custom one

    Async via Messenger

    Extensible to more third-party providers and more channels
    Symfony Notifier

    View Slide

  45. How can we leverage this new infrastructure?

    View Slide

  46. Monolog

    NotifierHandler
    Triggered on Error level logs
    Uses the Notifier channel configuration
    Converts Error level logs to importance levels
    Configurable like any other Notification 40 LOCs of glue code
    errors:
    type: fingers_crossed
    action_level: error
    handler: notifier
    excluded_http_codes: [404, 405]
    notifier:
    type: service
    id: notifier.monolog_handler

    View Slide

  47. Messenger
    Failed Messages Listener
    Triggered when a failed message failed
    Uses the Notifier channel configuration
    20 LOCs of glue code
    public function onMessageFailed(WorkerMessageFailedEvent $event)
    {
    if ($event->willRetry()) {
    return;
    }
    $throwable = $event->getThrowable();
    if ($throwable instanceof HandlerFailedException) {
    $throwable = $throwable->getNestedExceptions()[0];
    }
    $envelope = $event->getEnvelope();
    $notification = (new SystemNotification(sprintf('A "%s" message has just failed.', \get_cla)
    ->importance(SystemNotification::HIGH);
    $this->notifier->notify($notification, ...Notifier::getSystemReceivers());
    }

    View Slide

  48. Your own

    integration
    Use these new building blocks
    to create more powerful high-level abstractions
    with a few lines of code

    View Slide

  49. Many

    Possibilities
    Go fast with the Notifier and the Full-Stack framework
    Be creative with the standalone low-level classes
    SystemEmail, TwilioTransport, Texter, SlackTransport, Chatter, ...

    View Slide

  50. Experimental Component in 5.0
    Notifier

    View Slide

  51. Thank you! ❤

    View Slide