Notifiche per tutti
L'applicazione Guestbook raccoglie i feedback sulle conferenze. Ma non siamo bravi a dare feedback ai nostri utenti.
Poiché i commenti sono moderati, probabilmente gli utenti non capiranno perché i loro commenti non vengono pubblicati immediatamente. Potrebbero anche re-inviarli pensando che ci siano stati dei problemi tecnici. Dare loro un feedback dopo aver inviato un commento sarebbe fantastico.
Inoltre, probabilmente dovremmo far loro sapere quando il commento è stato pubblicato. Chiediamo la loro email, quindi faremmo meglio ad usarla.
Ci sono molti modi per notificare gli utenti. L'e-mail è il primo mezzo a cui si potrebbe pensare, ma è anche possibile inviare notifiche nell'applicazione web. Potremmo anche pensare di inviare messaggi SMS, postare un messaggio su Slack o Telegram. Ci sono molte opzioni.
Il componente Notifier di Symfony implementa molte strategie di notifica.
Invio di notifiche tramite browser
Come primo passo, notifichiamo direttamente nel browser agli utenti che i commenti sono moderati dopo il loro invio:
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);
Il notificatore invia una notifica ai destinatari attraverso un canale.
Una notifica ha un oggetto, un contenuto facoltativo e un'importanza.
Una notifica viene inviata su uno o più canali a seconda della sua importanza. È possibile inviare notifiche urgenti via SMS e notifiche regolari, ad esempio, via e-mail.
Per le notifiche nel browser, non abbiamo destinatari.
La notifica nel browser utilizza messaggi flash tramite la sezione notifiche. Dobbiamo visualizzarli aggiornando il template della conferenza:
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">×</span></button>
+ </div>
+ {% endfor %}
+
<h2 class="mb-5">
{{ conference }} Conference
</h2>
Gli utenti saranno ora informati che l'invio di commenti è moderato:
Come bonus aggiuntivo, abbiamo una bella notifica nella parte superiore del sito in caso di errori nel form:
Tip
I messaggi Flash utilizzano il sistema di sessione HTTP come supporto di memorizzazione. La conseguenza principale è che la cache HTTP è disabilitata in quanto il sistema di sessione deve essere avviato per controllare la presenza di messaggi.
Questo è il motivo per cui abbiamo aggiunto lo snippet dei messaggi flash nel template show.html.twig
e non in quello di base, per non perdere la cache HTTP per la homepage.
Notificare gli amministratori via e-mail
Invece di avvisare l'amministratore che un commento è stato appena pubblicato, attraverso MailerInterface
, usiamo il componente Notifier nell'handler dei messaggi:
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());
Il metodo getAdminRecipients()
restituisce i destinatari "amministratori" come configurato nelle impostazioni del Notifier; aggiorniamolo ora per aggiungere il nostro indirizzo email:
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)%" }
Ora, creiamo la classe CommentReviewNotification
:
Il metodo asEmailMessage()
, definito nell'interfaccia EmailNotificationInterface
, è opzionale, ma permette di personalizzare l'email.
Uno dei vantaggi nell'utilizzo del Notifier al posto del mailer, per inviare e-mail, è la separazione tra notifica e il "canale" utilizzato per inviarla. Come possiamo vedere, nulla dice esplicitamente che la notifica deve essere inviata via e-mail.
Il canale viene invece configurato nel file config/packages/notifier.yaml
in funzione dell'importanza della notifica (predefinita a low
):
Abbiamo parlato dei canali email
e browser
. Vediamone altri.
Chattare con gli amministratori
Siamo onesti, aspettiamo tutti un feedback positivo. O almeno un feedback costruttivo. Se qualcuno pubblica un commento con parole come "grande" o "fantastico", potremmo volerli accettare più velocemente degli altri.
Per tali messaggi, vogliamo essere avvisati su un sistema di messaggistica istantanea come Slack o Telegram, in aggiunta alla normale posta elettronica.
Installare il supporto di Slack per Symfony Notifier:
1
$ symfony composer req slack-notifier
Per iniziare costruiamo il DSN per Slack. Ci occorre il token e l'identificativo del canale Slack a cui inviare i messaggi: slack://ACCESS_TOKEN@default?channel=CHANNEL
.
Poiché il token di accesso deve essere riservato, salviamo il DSN per Slack nel portachiavi:
1
$ symfony console secrets:set SLACK_DSN
Fare lo stesso per l'ambiente di produzione:
1
$ symfony console secrets:set SLACK_DSN --env=prod
Attivare il supporto alla chat 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)%'
Aggiorniamo la classe di notifica per inviare i messaggi a seconda del contenuto del testo del commento (usiamo un'espressione regolare):
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'];
+ }
}
Abbiamo anche cambiato l'importanza dei commenti "regolari" in quanto questo modifica leggermente il design dell'email.
È fatta! Inviando un commento con scritto "awesome" nel testo, dovremmo ricevere un messaggio su Slack.
Per quanto riguarda la posta elettronica, è possibile implementare l'interfaccia ChatNotificationInterface
per cambiare il rendering predefinito dei messaggi 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())) {
È meglio, ma facciamo un passo avanti. Non sarebbe fantastico poter accettare o rifiutare un commento direttamente da Slack?
Modifichiamo la notifica per accettare l'URL di revisione e aggiungere due pulsanti nel messaggio 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;
Ora si tratta di seguire i cambiamenti a ritroso. Come primo passo aggiorniamo l'handler dei messaggi affinché includa l'URL di revisione:
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());
Come si può vedere, l'URL di recensione dovrebbe essere parte del commento, aggiungiamolo ora:
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;
Infine, aggiornare i controller per generare l'URL di revisione e passarlo al costruttore del commento:
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']));
Disaccoppiare il codice significa fare cambiamenti in più parti, ma rende il codice più facile da testare, validare e riutilizzare.
Riproviamoci, il messaggio dovrebbe essere ora corretto:
Chiamate asincrone in tutto il progetto
Con le impostazioni predefinite le notifiche sono inviate in maniera asincrona, così come le email:
Se avessimo disabilitato i mesasggi asincroni, avremmo avuto un leggero problema. Per ogni commento riceviamo un'e-mail e un messaggio Slack. Se il messaggio Slack è errato (id canale sbagliato, token sbagliato, ....), messenger riproverà tre volte prima scartare il messaggio. Ma siccome l'email viene inviata per prima, riceveremo tre email e nessun messaggio Slack.
Non appena tutto è asincrono, i messaggi diventano indipendenti. I messaggi SMS sono gia configurati come asincroni nel caso in cui volessimo essere avvisati sul telefono.
Notificare gli utenti via e-mail
L'ultimo punto è quello di notificare gli utenti quando il loro commento è approvato. Che ne dite di implementarlo da soli?