Symfony Notifier

Symfony Notifier

9a22d09f92d50fa3d2a16766d0ba52f8?s=128

Fabien Potencier

May 07, 2020
Tweet

Transcript

  1. Back to the basics... The Notifier Component Fabien Potencier @fabpot

  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,...
  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 ...
  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
  5. Building flexible high-level abstractions on top of low-level ones What's

    next?
  6. Building flexible high-level abstractions on top of low-level ones Notification

    Emails?
  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
  8. use Symfony\Bridge\Twig\Mime\NotificationEmail; $email = (new NotificationEmail()) ->from('fabien@symfony.com') ->to('fabien@symfony.com') ->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
  9. A typical Notification Email Color depends
 on the importance
 of

    the email Content Action
  10. $email = (new NotificationEmail()) ->from('fabien@symfony.com') ->to('fabien@symfony.com') ->exception($exception) ->... ; $mailer->send($email);

    When something goes wrong?
  11. # PROJECT_DIR/templates/email/notification.html.twig $email = (new NotificationEmail()) ->htmlTemplate('email/notification.html.twig') ->textTemplate('email/notification.txt.twig') ->... ;

    ...and configurable
  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() }} <spacer size="16"></spacer> <button class="secondary" href="https://myapp.com/">Go to MyApp</button> {% endblock %} {% block footer_content %} <p><small>&copy; MyApp</small></p> {% endblock %} ...and configurable Productivity && Flexibility
  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
  14. Email are Messages... Sending Notifications...

  15. Sending SMS Messages

  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
  17. Symfony HttpClient + Symfony Messenger + Built-in third-party providers =

    Sending async SMS in one LOC! Symfony Texter Similar infrastructure
 as Mailer
  18. $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
  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
  20. $dsn = 'failover(twilio://SID:TOKEN@default?from=FROM nexmo://KEY:SECRET@default?from=FROM)'; SMS... higher-level API Similar infrastructure
 as

    Mailer
  21. Sending ... Messages

  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
  23. $message = new ChatMessage('Revenue increased by 1€ per year...'); $slack

    = Transport::fromDsn('slack://TOKEN@default?channel=CHANNEL'); $slack->send($message); $telegram = Transport::fromDsn('telegram://TOKEN@default?channel=CHAT_ID'); $telegram->send($message); Messages... low-level API
  24. $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
  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
  26. $dsn = 'all(slack://TOKEN@default?channel=CHANNEL telegram://TOKEN@default?channel=CHAT_ID)'; Messages... higher-level API

  27. Symfony HttpClient + Symfony Messenger + Built-on third-party providers =

    Sending async Messages in one LOC! Symfony Chatter Similar infrastructure
 as Mailer
  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; }
  29. 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?

  30. Building flexible high-level abstractions on top of low-level ones Mailer

    +Texter + Chatter = ?
  31. Notifier

  32. /** * @Route("/checkout/thankyou") */ public function thankyou(NotifierInterface $notifier /* ...

    */) { $notification = new Notification('New customer!', ['sms', 'chat/slack', 'email']); $notifier->send($notification, new Recipient('fabien@symfony.com')); return $this->render('checkout/thankyou.html.twig', [ // ... ]); } Notify Messages! Channels: email, sms, chat, ... Transport: slack, telegram, twilio, ...
  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
  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()
  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
  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
  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
  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 ❤
  39. Currently supported transports 5.0+ Nexmo Slack Telegram Twilio 5.1+ Firebase

    Mattermost OvhCloud RocketChat Sinch
  40. What about using it in a Notification? Remember NotificationEmail?

  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
  42. (new SystemNotification('New customer!')->importance(SystemNotification::URGENT)) Importance to configure the Channels Defines the

    channels
  43. framework: notifier: channel_policy: urgent: ['email', 'chat/slack', 'sms'] high: ['email', 'chat/slack']

    medium: ['email'] low: ['pigeon'] Channels auto-configuration Importance Channels/ Transports
  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
  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
  46. How can we leverage this new infrastructure?

  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
  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()); }
  49. Your own integration Use these new building blocks to create

    more powerful high-level abstractions with a few lines of code
  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, ...
  51. Experimental Component in 5.0 and 5.1 Notifier

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

    Italian Brazilian Portuguese English
  53. Thank you! ❤