Skip to content

Сповіщення всілякими засобами

Застосунок гостьової книги збирає відгуки про конференції. Але ми не найкращі в наданні зворотного зв'язку нашим користувачам.

Оскільки коментарі проходять перевірку, користувачі, ймовірно, не розуміють, чому їх коментарі не публікуються миттєво. Вони навіть можуть відправити їх повторно, думаючи, що є якісь технічні проблеми. Було б чудово надати їм зворотний зв'язок, після публікації коментаря.

Крім того, ми, мабуть, маємо сповістити їх, як тільки їх коментар було опубліковано. Ми просимо вказати їх адресу електронної пошти, тому нам краще використовувати її.

Існує безліч способів оповіщення користувачів. Електронна пошта — це перший засіб, про який ви можете подумати, але сповіщення у веб-застосунку — це ще один засіб. Ми могли б навіть подумати про відправку SMS-повідомлень, відправку повідомлення у Slack або Telegram. Є багато варіантів.

Компонент Symfony Notifier реалізує багато стратегій оповіщення.

Відправка сповіщень веб-застосунку в браузері

У якості першого кроку сповістімо користувачів про те, що коментарі проходять перевірку — безпосередньо в браузері, після їх відправки:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -14,6 +14,8 @@ use Symfony\Component\HttpFoundation\File\Exception\FileException;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Notifier\Notification\Notification;
+use Symfony\Component\Notifier\NotifierInterface;
 use Symfony\Component\Routing\Annotation\Route;
 use Twig\Environment;

@@ -53,7 +55,7 @@ class ConferenceController extends AbstractController
     }

     #[Route('/conference/{slug}', name: 'conference')]
-    public function show(Request $request, Conference $conference, CommentRepository $commentRepository, string $photoDir): Response
+    public function show(Request $request, Conference $conference, CommentRepository $commentRepository, NotifierInterface $notifier, string $photoDir): Response
     {
         $comment = new Comment();
         $form = $this->createForm(CommentFormType::class, $comment);
@@ -82,9 +84,15 @@ class ConferenceController extends AbstractController

             $this->bus->dispatch(new CommentMessage($comment->getId(), $context));

+            $notifier->send(new Notification('Thank you for the feedback; your comment will be posted after moderation.', ['browser']));
+
             return $this->redirectToRoute('conference', ['slug' => $conference->getSlug()]);
         }

+        if ($form->isSubmitted()) {
+            $notifier->send(new Notification('Can you check your submission? There are some problems with it.', ['browser']));
+        }
+
         $offset = max(0, $request->query->getInt('offset', 0));
         $paginator = $commentRepository->getCommentPaginator($conference, $offset);

Сповіщувач відправляє сповіщення до отримувачів за допомогою каналу

У сповіщення є тема, необов'язковий зміст і рівень важливості.

Сповіщення відправляється за допомогою одного або декількох каналів, залежно від рівня його важливості. Ви можете відправляти термінові сповіщення, наприклад, за допомогою SMS, а звичайні — за допомогою електронної пошти.

Для сповіщень браузера у нас немає одержувачів.

Сповіщення браузера використовує миттєві повідомлення за допомогою секції notification. Ми маємо відобразити їх, оновивши шаблон конференції:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--- a/templates/conference/show.html.twig
+++ b/templates/conference/show.html.twig
@@ -3,6 +3,13 @@
 {% block title %}Conference Guestbook - {{ conference }}{% endblock %}

 {% block body %}
+    {% for message in app.flashes('notification') %}
+        <div class="alert alert-info alert-dismissible fade show">
+            {{ message }}
+            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+        </div>
+    {% endfor %}
+
     <h2 class="mb-5">
         {{ conference }} Conference
     </h2>

Тепер користувачі будуть сповіщені про те, що їх подання проходить перевірку:

/conference/amsterdam-2019

У якості додаткового бонусу у нас є приємне сповіщення у верхній частині веб-сайту, якщо у формі є помилка:

/conference/amsterdam-2019

Tip

Миттєві повідомлення використовують систему HTTP-сесії в якості носія даних. Головним наслідком цього є те, що HTTP-кеш вимкнено, оскільки система сесій має бути запущена, щоб перевірити повідомлення.

Саме з цієї причини ми додали фрагмент миттєвих повідомлень у шаблон show.html.twig, а не в базовий, оскільки ми втратили б HTTP-кеш для головної сторінки.

Сповіщення адміністраторів за допомогою електронної пошти

Замість того щоб відправляти електронний лист за допомогою MailerInterface, щоб сповістити адміністратора про те, що щойно опубліковано коментар, перейдіть до використання компонента Notifier в обробнику повідомлень:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
--- a/src/MessageHandler/CommentMessageHandler.php
+++ b/src/MessageHandler/CommentMessageHandler.php
@@ -4,14 +4,14 @@ namespace App\MessageHandler;

 use App\ImageOptimizer;
 use App\Message\CommentMessage;
+use App\Notification\CommentReviewNotification;
 use App\Repository\CommentRepository;
 use App\SpamChecker;
 use Doctrine\ORM\EntityManagerInterface;
 use Psr\Log\LoggerInterface;
-use Symfony\Bridge\Twig\Mime\NotificationEmail;
-use Symfony\Component\Mailer\MailerInterface;
 use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
 use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Notifier\NotifierInterface;
 use Symfony\Component\Workflow\WorkflowInterface;

 class CommentMessageHandler implements MessageHandlerInterface
@@ -21,22 +21,20 @@ class CommentMessageHandler implements MessageHandlerInterface
     private $commentRepository;
     private $bus;
     private $workflow;
-    private $mailer;
+    private $notifier;
     private $imageOptimizer;
-    private $adminEmail;
     private $photoDir;
     private $logger;

-    public function __construct(EntityManagerInterface $entityManager, SpamChecker $spamChecker, CommentRepository $commentRepository, MessageBusInterface $bus, WorkflowInterface $commentStateMachine, MailerInterface $mailer, ImageOptimizer $imageOptimizer, string $adminEmail, string $photoDir, LoggerInterface $logger = null)
+    public function __construct(EntityManagerInterface $entityManager, SpamChecker $spamChecker, CommentRepository $commentRepository, MessageBusInterface $bus, WorkflowInterface $commentStateMachine, NotifierInterface $notifier, ImageOptimizer $imageOptimizer, string $photoDir, LoggerInterface $logger = null)
     {
         $this->entityManager = $entityManager;
         $this->spamChecker = $spamChecker;
         $this->commentRepository = $commentRepository;
         $this->bus = $bus;
         $this->workflow = $commentStateMachine;
-        $this->mailer = $mailer;
+        $this->notifier = $notifier;
         $this->imageOptimizer = $imageOptimizer;
-        $this->adminEmail = $adminEmail;
         $this->photoDir = $photoDir;
         $this->logger = $logger;
     }
@@ -62,13 +60,7 @@ class CommentMessageHandler implements MessageHandlerInterface

             $this->bus->dispatch($message);
         } elseif ($this->workflow->can($comment, 'publish') || $this->workflow->can($comment, 'publish_ham')) {
-            $this->mailer->send((new NotificationEmail())
-                ->subject('New comment posted')
-                ->htmlTemplate('emails/comment_notification.html.twig')
-                ->from($this->adminEmail)
-                ->to($this->adminEmail)
-                ->context(['comment' => $comment])
-            );
+            $this->notifier->send(new CommentReviewNotification($comment), ...$this->notifier->getAdminRecipients());
         } elseif ($this->workflow->can($comment, 'optimize')) {
             if ($comment->getPhotoFilename()) {
                 $this->imageOptimizer->resize($this->photoDir.'/'.$comment->getPhotoFilename());

Метод getAdminRecipients() повертає список адміністраторів-одержувачів, що налаштований в конфігурації сповіщувача; оновіть його зараз, щоб додати власну адресу електронної пошти:

1
2
3
4
5
6
7
8
--- a/config/packages/notifier.yaml
+++ b/config/packages/notifier.yaml
@@ -13,4 +13,4 @@ framework:
             medium: ['email']
             low: ['email']
         admin_recipients:
-            - { email: admin@example.com }
+            - { email: "%env(string:default:default_admin_email:ADMIN_EMAIL)%" }

Тепер створіть клас CommentReviewNotification:

src/Notification/CommentReviewNotification.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace App\Notification;

use App\Entity\Comment;
use Symfony\Component\Notifier\Message\EmailMessage;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;

class CommentReviewNotification extends Notification implements EmailNotificationInterface
{
    private $comment;

    public function __construct(Comment $comment)
    {
        $this->comment = $comment;

        parent::__construct('New comment posted');
    }

    public function asEmailMessage(EmailRecipientInterface $recipient, string $transport = null): ?EmailMessage
    {
        $message = EmailMessage::fromNotification($this, $recipient, $transport);
        $message->getMessage()
            ->htmlTemplate('emails/comment_notification.html.twig')
            ->context(['comment' => $this->comment])
        ;

        return $message;
    }
}

Метод asEmailMessage() із EmailNotificationInterface є необов'язковим, але він дозволяє кастомізувати електронний лист.

Одна з переваг використання сповіщувача замість відправника безпосередньо, для відправки електронних листів, полягає в тому, що він відокремлює сповіщення від використовуваного для нього "каналу". Як ви можете бачити, ніщо явно не говорить про те, що сповіщення має бути відправлено за допомогою електронної пошти.

Натомість канал налаштовується в config/packages/notifier.yaml залежно від рівня важливості сповіщення (низький за замовчуванням):

config/packages/notifier.yaml
1
2
3
4
5
6
7
8
framework:
notifier:
    channel_policy:
        # use chat/slack, chat/telegram, sms/twilio or sms/nexmo
        urgent: ['email']
        high: ['email']
        medium: ['email']
        low: ['email']

Ми говорили про канали браузер і електронна пошта. Подивімося на більш незвичайні з них.

Чат з адміністраторами

Будьмо чесними, ми всі чекаємо позитивних відгуків. Або, принаймні, конструктивний зворотний зв'язок. Якщо хтось публікує коментар з такими словами, як "great" або "awesome", ми, можливо, захочемо прийняти його швидше, ніж інші.

Для таких повідомлень ми хочемо отримувати сповіщення в такій системі обміну миттєвими повідомленнями, як Slack або Telegram, на додаток до звичайного електронного листа.

Встановіть підтримку Slack для Symfony Notifier:

1
$ symfony composer req slack-notifier

Для початку скомпонуйте Slack DSN з токеном доступу Slack і ідентифікатором каналу Slack, куди ви хочете відправляти повідомлення: slack://ACCESS_TOKEN@default?channel=CHANNEL.

Оскільки токен доступу є чутливим, зберігайте Slack DSN у секретному сховищі:

1
$ symfony console secrets:set SLACK_DSN

Зробіть те ж саме для продакшн:

1
$ symfony console secrets:set SLACK_DSN --env=prod

Увімкніть підтримку Slack у режимі чату:

1
2
3
4
5
6
7
8
9
10
11
12
--- a/config/packages/notifier.yaml
+++ b/config/packages/notifier.yaml
@@ -1,7 +1,7 @@
 framework:
     notifier:
-        #chatter_transports:
-        #    slack: '%env(SLACK_DSN)%'
+        chatter_transports:
+            slack: '%env(SLACK_DSN)%'
         #    telegram: '%env(TELEGRAM_DSN)%'
         #texter_transports:
         #    twilio: '%env(TWILIO_DSN)%'

Оновіть клас сповіщення, щоб маршрутизувати повідомлення залежно від вмісту тексту коментаря (простий регулярний вираз виконає цю роботу):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
--- a/src/Notification/CommentReviewNotification.php
+++ b/src/Notification/CommentReviewNotification.php
@@ -7,6 +7,7 @@ use Symfony\Component\Notifier\Message\EmailMessage;
 use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
 use Symfony\Component\Notifier\Notification\Notification;
 use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
+use Symfony\Component\Notifier\Recipient\RecipientInterface;

 class CommentReviewNotification extends Notification implements EmailNotificationInterface
 {
@@ -29,4 +30,15 @@ class CommentReviewNotification extends Notification implements EmailNotificatio

         return $message;
     }
+
+    public function getChannels(RecipientInterface $recipient): array
+    {
+        if (preg_match('{\b(great|awesome)\b}i', $this->comment->getText())) {
+            return ['email', 'chat/slack'];
+        }
+
+        $this->importance(Notification::IMPORTANCE_LOW);
+
+        return ['email'];
+    }
 }

Ми також змінили рівень важливості "звичайних" коментарів, оскільки він трохи змінює дизайн електронного листа.

І готово! Відправте коментар зі словом "awesome" в тексті, ви маєте отримати повідомлення у Slack.

Що стосується електронного листа, ви можете реалізувати ChatNotificationInterface, щоб перевизначити візуалізацію повідомлення Slack за замовчуванням:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
--- a/src/Notification/CommentReviewNotification.php
+++ b/src/Notification/CommentReviewNotification.php
@@ -3,13 +3,18 @@
 namespace App\Notification;

 use App\Entity\Comment;
+use Symfony\Component\Notifier\Bridge\Slack\Block\SlackDividerBlock;
+use Symfony\Component\Notifier\Bridge\Slack\Block\SlackSectionBlock;
+use Symfony\Component\Notifier\Bridge\Slack\SlackOptions;
+use Symfony\Component\Notifier\Message\ChatMessage;
 use Symfony\Component\Notifier\Message\EmailMessage;
+use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
 use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
 use Symfony\Component\Notifier\Notification\Notification;
 use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
 use Symfony\Component\Notifier\Recipient\RecipientInterface;

-class CommentReviewNotification extends Notification implements EmailNotificationInterface
+class CommentReviewNotification extends Notification implements EmailNotificationInterface, ChatNotificationInterface
 {
     private $comment;

@@ -31,6 +36,28 @@ class CommentReviewNotification extends Notification implements EmailNotificatio
         return $message;
     }

+    public function asChatMessage(RecipientInterface $recipient, string $transport = null): ?ChatMessage
+    {
+        if ('slack' !== $transport) {
+            return null;
+        }
+
+        $message = ChatMessage::fromNotification($this, $recipient, $transport);
+        $message->subject($this->getSubject());
+        $message->options((new SlackOptions())
+            ->iconEmoji('tada')
+            ->iconUrl('https://guestbook.example.com')
+            ->username('Guestbook')
+            ->block((new SlackSectionBlock())->text($this->getSubject()))
+            ->block(new SlackDividerBlock())
+            ->block((new SlackSectionBlock())
+                ->text(sprintf('%s (%s) says: %s', $this->comment->getAuthor(), $this->comment->getEmail(), $this->comment->getText()))
+            )
+        );
+
+        return $message;
+    }
+
     public function getChannels(RecipientInterface $recipient): array
     {
         if (preg_match('{\b(great|awesome)\b}i', $this->comment->getText())) {

Краще, але зробімо ще один крок вперед. Хіба не було б чудово мати можливість прийняти або відхилити коментар безпосередньо зі Slack?

Змініть сповіщення, щоб воно приймало URL-адресу огляду, і додайте дві кнопки в повідомлення Slack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
--- a/src/Notification/CommentReviewNotification.php
+++ b/src/Notification/CommentReviewNotification.php
@@ -3,6 +3,7 @@
 namespace App\Notification;

 use App\Entity\Comment;
+use Symfony\Component\Notifier\Bridge\Slack\Block\SlackActionsBlock;
 use Symfony\Component\Notifier\Bridge\Slack\Block\SlackDividerBlock;
 use Symfony\Component\Notifier\Bridge\Slack\Block\SlackSectionBlock;
 use Symfony\Component\Notifier\Bridge\Slack\SlackOptions;
@@ -17,10 +18,12 @@ use Symfony\Component\Notifier\Recipient\RecipientInterface;
 class CommentReviewNotification extends Notification implements EmailNotificationInterface, ChatNotificationInterface
 {
     private $comment;
+    private $reviewUrl;

-    public function __construct(Comment $comment)
+    public function __construct(Comment $comment, string $reviewUrl)
     {
         $this->comment = $comment;
+        $this->reviewUrl = $reviewUrl;

         parent::__construct('New comment posted');
     }
@@ -53,6 +56,10 @@ class CommentReviewNotification extends Notification implements EmailNotificatio
             ->block((new SlackSectionBlock())
                 ->text(sprintf('%s (%s) says: %s', $this->comment->getAuthor(), $this->comment->getEmail(), $this->comment->getText()))
             )
+            ->block((new SlackActionsBlock())
+                ->button('Accept', $this->reviewUrl, 'primary')
+                ->button('Reject', $this->reviewUrl.'?reject=1', 'danger')
+            )
         );

         return $message;

Тепер справа за відстеженням змін у зворотному напрямку. По-перше, оновіть обробник повідомлень, щоб передати URL-адресу огляду:

1
2
3
4
5
6
7
8
9
10
11
12
--- a/src/MessageHandler/CommentMessageHandler.php
+++ b/src/MessageHandler/CommentMessageHandler.php
@@ -60,7 +60,8 @@ class CommentMessageHandler implements MessageHandlerInterface

             $this->bus->dispatch($message);
         } elseif ($this->workflow->can($comment, 'publish') || $this->workflow->can($comment, 'publish_ham')) {
-            $this->notifier->send(new CommentReviewNotification($comment), ...$this->notifier->getAdminRecipients());
+            $notification = new CommentReviewNotification($comment, $message->getReviewUrl());
+            $this->notifier->send($notification, ...$this->notifier->getAdminRecipients());
         } elseif ($this->workflow->can($comment, 'optimize')) {
             if ($comment->getPhotoFilename()) {
                 $this->imageOptimizer->resize($this->photoDir.'/'.$comment->getPhotoFilename());

Як ви можете бачити, URL-адреса огляду має бути частиною повідомлення коментаря, додаймо його зараз:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
--- a/src/Message/CommentMessage.php
+++ b/src/Message/CommentMessage.php
@@ -5,14 +5,21 @@ namespace App\Message;
 class CommentMessage
 {
     private $id;
+    private $reviewUrl;
     private $context;

-    public function __construct(int $id, array $context = [])
+    public function __construct(int $id, string $reviewUrl, array $context = [])
     {
         $this->id = $id;
+        $this->reviewUrl = $reviewUrl;
         $this->context = $context;
     }

+    public function getReviewUrl(): string
+    {
+        return $this->reviewUrl;
+    }
+
     public function getId(): int
     {
         return $this->id;

Нарешті, оновіть контролери, щоб згенерувати URL-адресу огляду й передати його в конструктор повідомлення коментаря:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
--- a/src/Controller/AdminController.php
+++ b/src/Controller/AdminController.php
@@ -12,6 +12,7 @@ use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
 use Symfony\Component\HttpKernel\KernelInterface;
 use Symfony\Component\Messenger\MessageBusInterface;
 use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Component\Workflow\Registry;
 use Twig\Environment;

@@ -47,7 +48,8 @@ class AdminController extends AbstractController
         $this->entityManager->flush();

         if ($accepted) {
-            $this->bus->dispatch(new CommentMessage($comment->getId()));
+            $reviewUrl = $this->generateUrl('review_comment', ['id' => $comment->getId()], UrlGeneratorInterface::ABSOLUTE_URL);
+            $this->bus->dispatch(new CommentMessage($comment->getId(), $reviewUrl));
         }

         return new Response($this->twig->render('admin/review.html.twig', [
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -17,6 +17,7 @@ use Symfony\Component\Messenger\MessageBusInterface;
 use Symfony\Component\Notifier\Notification\Notification;
 use Symfony\Component\Notifier\NotifierInterface;
 use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Twig\Environment;

 class ConferenceController extends AbstractController
@@ -82,7 +83,8 @@ class ConferenceController extends AbstractController
                 'permalink' => $request->getUri(),
             ];

-            $this->bus->dispatch(new CommentMessage($comment->getId(), $context));
+            $reviewUrl = $this->generateUrl('review_comment', ['id' => $comment->getId()], UrlGeneratorInterface::ABSOLUTE_URL);
+            $this->bus->dispatch(new CommentMessage($comment->getId(), $reviewUrl, $context));

             $notifier->send(new Notification('Thank you for the feedback; your comment will be posted after moderation.', ['browser']));

Декомпозиція коду означає зміни в більшій кількості місць, але це полегшує тестування, аналіз і повторне використання.

Спробуйте ще раз, тепер повідомлення має бути у відповідному вигляді:

Перехід до асинхронності у всіх напрямках

Сповіщення, як-от електронні листи, за замовчуванням надсилаються асинхронно.

config/packages/messenger.yaml
1
2
3
4
5
6
7
8
9
framework:
    messenger:
        routing:
            Symfony\Component\Mailer\Messenger\SendEmailMessage: async
            Symfony\Component\Notifier\Message\ChatMessage: async
            Symfony\Component\Notifier\Message\SmsMessage: async

            # Route your messages to the transports
            App\Message\CommentMessage: async

Якби ми вимкнули асинхронні повідомлення, у нас виникла б невелика проблема. Для кожного коментаря ми отримуємо повідомлення електронної пошти й повідомлення Slack. Якщо в повідомленні Slack є помилки (неправильний ідентифікатор каналу, неправильний токен, ...), Messenger буде відправляти повідомлення повторно три рази, перш ніж відкине його. Але оскільки повідомлення електронної пошти відправляється першим, ми отримаємо 3 повідомлення електронної пошти й жодних повідомлень Slack.

Як тільки все стає асинхронним, повідомлення стають незалежними. SMS-повідомлення вже налаштовані як асинхронні на той випадок, якщо ви також хочете отримувати сповіщення на ваш телефон.

Сповіщення користувачів за допомогою електронної пошти

Останнє завдання — повідомити користувачів, коли їх подання буде схвалено. А як щодо того, щоб дозволити вам реалізувати це самостійно?

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version