Symfony Notifier

Symfony Notifier

9a22d09f92d50fa3d2a16766d0ba52f8?s=128

Fabien Potencier

September 13, 2019
Tweet

Transcript

  1. Back to the basics... 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 System

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

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

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

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

  14. Sending SMS Messages

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

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

    Mailer
  20. Sending ... Messages

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

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

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

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

    +Texter + Chatter = ?
  30. Notifier

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

    */) { $notification = new Notification('New customer!', ['sms', 'chat/slack', 'email']); $notifier->send($notification, new Receiver('fabien@symfony.com')); return $this->render('checkout/thankyou.html.twig', [ // ... ]); } Notify Messages! Channels: email, sms, chat, ... Transport: slack, telegram, twilio, ...
  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
  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()
  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
  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
  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 ❤
  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
  38. What about a SystemNotification? Remember SystemEmail?

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

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

    medium: ['email'] low: ['pigeon'] Channels auto-configuration Importance Channels/ Transports
  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
  43. $notification = new ExceptionNotification(new \RuntimeException('Something went ...')); ExceptionNotification Uses SystemEmail

    with Exception support
  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
  45. How can we leverage this new infrastructure?

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

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

  51. Thank you! ❤