Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Symfony Notifier

Symfony Notifier

Fabien Potencier

May 07, 2020
Tweet

More Decks by Fabien Potencier

Other Decks in Technology

Transcript

  1. Back to the basics...
    The Notifier Component
    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', '[email protected]
    $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
    Notification 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 notification emails

    View Slide

  8. use Symfony\Bridge\Twig\Mime\NotificationEmail;
    $email = (new NotificationEmail())
    ->from('[email protected]')
    ->to('[email protected]')
    ->subject('You have a new customer!')
    ->markdown(
    '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(NotificationEmail::IMPORTANCE_HIGH)
    ;
    $mailer->send($email);
    A typical Notification Email

    View Slide

  9. A typical Notification Email
    Color depends

    on the importance

    of the email
    Content
    Action

    View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

  13. Bonus: Checking email rendering easily
    Productivity && Flexibility
    # docker-compose.yml
    version: '3'
    services:
    mailcatcher:
    image: schickling/mailcatcher
    ports: [1025, 1080]
    $ docker-compose up -d
    $ symfony server:start -d
    $ symfony open:local:webmail

    View Slide

  14. Email are Messages...
    Sending Notifications...

    View Slide

  15. Sending SMS Messages

    View Slide

  16. /**
    * @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

  17. Symfony HttpClient

    + Symfony Messenger

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

    as Mailer

    View Slide

  18. $sms = new SmsMessage('+1415999888', 'Revenue has just increased!');
    $twilio = Transport::fromDsn('twilio://SID:[email protected]?from=FROM');
    $twilio->send($sms);
    $nexmo = Transport::fromDsn('nexmo://KEY:[email protected]?from=FROM');
    $nexmo->send($sms);
    SMS... low-level API
    Similar infrastructure

    as Mailer

    View Slide

  19. $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

  20. $dsn = 'failover(twilio://SID:[email protected]?from=FROM nexmo://KEY:[email protected]?from=FROM)';
    SMS... higher-level API
    Similar infrastructure

    as Mailer

    View Slide

  21. Sending ... Messages

    View Slide

  22. /**
    * @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

  23. $message = new ChatMessage('Revenue increased by 1€ per year...');
    $slack = Transport::fromDsn('slack://[email protected]?channel=CHANNEL');
    $slack->send($message);
    $telegram = Transport::fromDsn('telegram://[email protected]?channel=CHAT_ID');
    $telegram->send($message);
    Messages... low-level API

    View Slide

  24. $transports = Transport::fromDsns([
    'slack' => 'slack://[email protected]?channel=CHANNEL',
    'telegram' => 'telegram://[email protected]?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

  25. $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

  26. $dsn = 'all(slack://[email protected]?channel=CHANNEL telegram://[email protected]?channel=CHAT_ID)';
    Messages... higher-level API

    View Slide

  27. Symfony HttpClient

    + Symfony Messenger

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

    as Mailer

    View Slide

  28. 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

  29. failover(twilio://SID:[email protected]?from=FROM telegram://[email protected]?channel=CHAT_ID)
    all(twilio://SID:[email protected]?from=FROM telegram://[email protected]?channel=CHAT_ID)
    SMS or/and Chat Message?

    View Slide

  30. Building flexible high-level abstractions
    on top of low-level ones
    Mailer +Texter + Chatter = ?

    View Slide

  31. Notifier

    View Slide

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

    View Slide

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

    View Slide

  34. class InvoiceNotification extends Notification implements ChatNotificationInterface
    {
    public function asChatMessage(Recipient $recipient, 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

  35. 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

  36. 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

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

    View Slide

  38. 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

  39. Currently supported transports
    5.0+
    Nexmo
    Slack
    Telegram
    Twilio
    5.1+
    Firebase
    Mattermost
    OvhCloud
    RocketChat
    Sinch

    View Slide

  40. What about using it in a Notification?
    Remember NotificationEmail?

    View Slide

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

    Recipients
    Auto-configured
    Channels

    View Slide

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

    View Slide

  43. 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

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

    View Slide

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

    Send to one or many Recipients

    Default configuration or custom one

    Async via Messenger

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

    View Slide

  46. How can we leverage this new infrastructure?

    View Slide

  47. 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

  48. 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::getAdminRecipients());
    }

    View Slide

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

    View Slide

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

    View Slide

  51. Experimental Component in 5.0 and 5.1
    Notifier

    View Slide

  52. Support Symfony
    & Learn
    https://leanpub.com/symfony5-the-fast-track
    French German Dutch Russian Spanish Italian Brazilian Portuguese
    English

    View Slide

  53. Thank you! ❤

    View Slide