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. 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,...
  2. 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 ...
  3. 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
  4. 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
  5. 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
  6. {% 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
  7. 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
  8. /** * @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
  9. Symfony HttpClient + Symfony Messenger + Built-in third-party providers =

    Sending async SMS in one LOC! Symfony Texter Similar infrastructure
 as Mailer
  10. $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
  11. $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
  12. /** * @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
  13. $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
  14. $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
  15. $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
  16. Symfony HttpClient + Symfony Messenger + Built-on third-party providers =

    Sending async Messages in one LOC! Symfony Chatter Similar infrastructure
 as Mailer
  17. 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; }
  18. /** * @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, ...
  19. 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
  20. 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()
  21. 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
  22. 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
  23. /** * @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
  24. 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 ❤
  25. /** * @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
  26. framework: notifier: channel_policy: urgent: ['email', 'chat/slack', 'sms'] high: ['email', 'chat/slack']

    medium: ['email'] low: ['pigeon'] Channels auto-configuration Importance Channels/ Transports
  27. 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
  28. 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
  29. 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
  30. 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()); }
  31. Your own integration Use these new building blocks to create

    more powerful high-level abstractions with a few lines of code
  32. 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, ...