Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Building flexible high-level abstractions on top of low-level ones What's next?

Slide 6

Slide 6 text

Building flexible high-level abstractions on top of low-level ones Notification Emails?

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

A typical Notification Email Color depends
 on the importance
 of the email Content Action

Slide 10

Slide 10 text

$email = (new NotificationEmail()) ->from('fabien@symfony.com') ->to('fabien@symfony.com') ->exception($exception) ->... ; $mailer->send($email); When something goes wrong?

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

{% 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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Email are Messages... Sending Notifications...

Slide 15

Slide 15 text

Sending SMS Messages

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

$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

Slide 19

Slide 19 text

$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

Slide 20

Slide 20 text

$dsn = 'failover(twilio://SID:TOKEN@default?from=FROM nexmo://KEY:SECRET@default?from=FROM)'; SMS... higher-level API Similar infrastructure
 as Mailer

Slide 21

Slide 21 text

Sending ... Messages

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

$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

Slide 24

Slide 24 text

$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

Slide 25

Slide 25 text

$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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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; }

Slide 29

Slide 29 text

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?

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Notifier

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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()

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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 ❤

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

What about using it in a Notification? Remember NotificationEmail?

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

How can we leverage this new infrastructure?

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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()); }

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Experimental Component in 5.0 and 5.1 Notifier

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Thank you! ❤