Replies: 3 comments 9 replies
-
We just added another listener in same way as for email messages, and then use twig to render slack messages, so in twig you can access to translator, generate routes and more :) |
Beta Was this translation helpful? Give feedback.
-
In case someone finds this and needs something similar, here is what I'm endend up doing. <?php
namespace App\Notifier\Notification;
use App\Notifier\Message\DoctrineNotificationMessage;
use App\Notifier\Recipient\UserRecipientInterface;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Message\EmailMessage;
use Symfony\Component\Notifier\Message\MessageInterface;
use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification as BaseNotification;
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;
class Notification extends BaseNotification implements ChatNotificationInterface, DoctrineNotificationInterface, EmailNotificationInterface
{
private const CHANNEL_MAP = [
ChatMessage::class => 'chat/slack',
DoctrineNotificationMessage::class => 'doctrine',
EmailMessage::class => 'email',
];
private array $messages = [
ChatMessage::class => null,
DoctrineNotificationMessage::class => null,
EmailMessage::class => null,
];
public function __construct(MessageInterface ...$messages)
{
foreach ($messages as $message) {
$class = get_class($message);
if (!array_key_exists($class, $this->messages)) {
throw new \LogicException(sprintf(
'This Notificaiton currently only supports %s messages, %s given.',
implode(', ', array_keys($this->messages)),
$class
));
}
$this->messages[$class] = $message;
}
}
public function asChatMessage(RecipientInterface $recipient, string $transport = null): ?ChatMessage
{
return $this->messages[ChatMessage::class];
}
public function asNotificationMessage(UserRecipientInterface $recipient, string $transport = null): ?DoctrineNotificationMessage
{
return $this->messages[DoctrineNotificationMessage::class];
}
public function asEmailMessage(EmailRecipientInterface $recipient, string $transport = null): ?EmailMessage
{
return $this->messages[EmailMessage::class];
}
public function getChannels(RecipientInterface $recipient): array
{
$channels = [];
foreach ($this->messages as $message) {
if (null === $message) {
continue;
}
$channels[] = self::CHANNEL_MAP[get_class($message)];
}
return $channels;
}
} This allows me to assemble messages before the notification is created in dedicated generators, where I have access to the DIC. Calling it $this->notifier->send(new Notification(...$this->generator->__invoke($task)), $recipient); And the generator: class TaskCreatedNotificationGenerator implements GeneratorInterface
{
private TranslatorInterface $translator;
private UrlGeneratorInterface $router;
public function __construct(TranslatorInterface $translator, UrlGeneratorInterface $router)
{
$this->translator = $translator;
$this->router = $router;
}
public function __invoke(Task $task): \Generator
{
yield $this->doctrine($task);
yield $this->slack($task);
}
private function doctrine(Task $task): ?DoctrineNotificationMessage
{
// ...
}
private function slack(Task $task): ?ChatMessage
{
// ...
}
} |
Beta Was this translation helpful? Give feedback.
-
Same issue here. It's a good idea to make the Notification a object simple DTO, but it does fall short when it needs to access anything else than itself. I briefly considered just providing the translator and URL generator dependencies myself to each notification (I can autowire them and then pass them on), but I rejected that idea shortly after. I ended up with an autowired factory service that does the repetitive work for me with just a few minor downsides (see below the code): <?php
namespace App\Notifier;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class NotificationFactory
{
public function __construct(
private readonly TranslatorInterface $translator,
private readonly UrlGeneratorInterface $urlGenerator
) {
// Void
}
/**
* @template T of Notification
* @psalm-param class-string<T> $className
*
* @return T
*/
public function build(string $className, ...$rest)
{
$notification = new $className(...$rest);
// Sanity check - the above is way too powerful
if (!$notification instanceof Notification) {
throw new \InvalidArgumentException('This factory can only be used to build Notifications.');
}
if ($notification instanceof TranslatedNotificationInterface) {
$notification->setTranslator($this->translator);
}
if ($notification instanceof UrlGeneratingNotificationInterface) {
$notification->setUrlGenerator($this->urlGenerator);
}
return $notification;
}
} Together with two interfaces <?php
namespace App\Notifier;
use Symfony\Contracts\Translation\TranslatorInterface;
interface TranslatedNotificationInterface
{
public function setTranslator(TranslatorInterface $translator): void;
} <?php
namespace App\Notifier;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
interface UrlGeneratingNotificationInterface
{
public function setUrlGenerator(UrlGeneratorInterface $urlGenerator): void;
} And two traits <?php
namespace App\Notifier\Notification;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
trait TranslatedNotificationTrait
{
private readonly TranslatorInterface $translator;
public function setTranslator(TranslatorInterface $translator): void
{
$this->translator = $translator;
}
public function content(string|TranslatableInterface $content): static
{
return parent::content(is_string($content) ? $content : $content->trans($this->translator));
}
public function subject(string|TranslatableInterface $subject): static
{
return parent::subject(is_string($subject) ? $subject : $subject->trans($this->translator));
}
} <?php
namespace App\Notifier\Notification;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
trait UrlGeneratingNotificationTrait
{
private readonly UrlGeneratorInterface $urlGenerator;
public function setUrlGenerator(UrlGeneratorInterface $urlGenerator): void
{
$this->urlGenerator = $urlGenerator;
}
} Now I'm able to build a custom Notification, for example: <?php
namespace App\Notifier\Notification;
use App\Entity\OrganisationUser;
use App\Notifier\NotificationEmail;
use App\Notifier\TranslatedNotificationInterface;
use App\Notifier\UrlGeneratingNotificationInterface;
use Symfony\Component\Mime\Address;
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\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Translation\TranslatableMessage;
class OrganisationInviteNotification extends Notification implements EmailNotificationInterface, TranslatedNotificationInterface, UrlGeneratingNotificationInterface
{
use TranslatedNotificationTrait;
use UrlGeneratingNotificationTrait;
private OrganisationUser $organisationUser;
public function setOrganisationUser(OrganisationUser $organisationUser): self
{
$this->organisationUser = $organisationUser;
$this->subject(new TranslatableMessage('notification.organisation_invite.subject', [
'organisation' => $this->organisationUser->getOrganisation(),
]));
$this->content(new TranslatableMessage('notification.organisation_invite.content', [
'organisation' => $this->organisationUser->getOrganisation(),
'invitedBy' => $this->organisationUser->getInvitedBy() ?: 'colleague',
]));
return $this;
}
public function asEmailMessage(EmailRecipientInterface $recipient, string $transport = null): ?EmailMessage
{
$email = NotificationEmail::asPublicEmail()
->to($recipient->getEmail())
->subject($this->getSubject())
->content($this->getContent())
->action(
(new TranslatableMessage('notification.organisation_invite.respond'))->trans($this->translator),
$this->urlGenerator->generate('security_login', referenceType: UrlGeneratorInterface::ABSOLUTE_URL)
)
;
if ($this->organisationUser->getInvitedBy()) {
$email->addFrom(new Address(
$this->organisationUser->getInvitedBy()->getEmail(),
$this->organisationUser->getInvitedBy()
));
}
return new EmailMessage($email);
}
} And I can get hold of that notification with semi-autowired translation about like this: <?php
// pseudo code
class SomeController {
public function someAction(NotificationFactory $notificationFactory, NotifierInterface $notifier) {
$this->notifier->send(
$this->notificationFactory->build(OrganisationInviteNotification::class)->setOrganisationUser($organisationUser),
new Recipient($organisationUser),
);
}
} It works OK. The only downside is that because the translator and URL generator are not passed to the constructor directly, I can't set the content and subject in the constructor - the services become available after the constructor has finished. The second downside is that I'm now setting readonly properties outside of a constructor, which I think is not the best practice. Previously I was passing the The Notifier component is heavily opinionated, which is out of the ordinary for Symfony, but let's roll with it. It's a good idea in a good direction. I hope this helps someone. |
Beta Was this translation helpful? Give feedback.
-
Hi,
This is my first foray into the notifier component, and I'm a bit conflicted. On one hand, this is exactly what I need. Notifications on multiple channels, and many integrations out of the box.
On the other hand it is an uphill battle, let me show what I would like to achieve, hopefully someone can help me with coming up with something.
So as you could probably guess, the client wanted an internal task system for their production management.
The doctrine channel is just a custom channel I wrote to save a notification entity, not really important here.
But when it comes to the slack integration I would like to rely on dependencies, such as the
router
and thetranslator
, and who knows what else down the line.At first I thought these interfaces are really amazing, but now I find myself lured into a bit of a trap :)
What are the community's thoughts on this one? What would you do?
Inject each manually?
Factory (wouldn't work well IMO, but no better idea for now)?
Something better? (I hope so :))
Thaks,
Adam
Beta Was this translation helpful? Give feedback.
All reactions