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

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 full-size 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 full-size 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 full-size 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 full-size slide

  5. Building flexible high-level abstractions

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

    View full-size slide

  6. Building flexible high-level abstractions

    on top of low-level ones
    System Emails?

    View full-size 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 full-size 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 full-size slide

  9. A typical System Email
    Color depends

    on the importance

    of the email
    Lines
    Action

    View full-size slide

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

    View full-size 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 full-size 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 full-size slide

  13. Email are Messages...

    View full-size slide

  14. Sending SMS Messages

    View full-size 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 full-size slide

  16. Symfony HttpClient

    + Symfony Messenger

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

    as Mailer

    View full-size 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 full-size 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 full-size 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 full-size slide

  20. Sending ... Messages

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

  26. Symfony HttpClient

    + Symfony Messenger

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

    as Mailer

    View full-size 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 full-size 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 full-size slide

  29. Building flexible high-level abstractions

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

    View full-size slide

  30. /**
    * @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 full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

  36. /**
    * @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 full-size slide

  37. What about a SystemNotification?
    Remember SystemEmail?

    View full-size slide

  38. /**
    * @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 full-size slide

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

    View full-size slide

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

    View full-size slide

  41. 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 full-size slide

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

    View full-size slide

  43. 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 full-size slide

  44. How can we leverage this new infrastructure?

    View full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. Your own

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

    View full-size slide

  48. 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 full-size slide

  49. Experimental Component in 5.0
    Notifier

    View full-size slide

  50. Thank you! ❤

    View full-size slide