Symfony 通知程序将自定义元数据附加到信封

发布于 2025-01-14 16:22:58 字数 5581 浏览 0 评论 0原文

我正在使用 Symfony 通知程序和信使组件异步发送 SMS 消息(以及将来的推送和电子邮件通知)。

一切工作正常,但是一旦发送消息,我想记录有关它的信息。

我可以通过订阅 WorkerMessageHandledEvent 来捕获成功的消息,它为我提供了 Message 对象,以及包含的 Envelope 及其所有 Stamp 里面的对象。根据所有可用信息,我将使用名为 MessageLog 的实体将其记录在我的数据库中。

class MessengerSubscriber implements EventSubscriberInterface {

    public static function getSubscribedEvents() {
        return [
            WorkerMessageHandledEvent::class => ['onHandled']
        ];
    }

    public function onHandled(WorkerMessageHandledEvent $event) {
        $log = new MessageLog();
        $log->setSentAt(new DateTime());

        if($event->getEnvelope()->getMessage() instanceof SmsMessage) {
            $log->setSubject($event->getEnvelope()->getMessage()->getSubject());
            $log->setRecipient($event->getEnvelope()->getMessage()->getPhone());
        }

        // Do more tracking
    }

}

我想做的是跟踪“调用”消息的对象。例如,如果我有一个新闻提要,并且发布帖子会发出通知,我想将每条记录的消息归因于该帖子(以显示每个帖子的受众覆盖率/投放统计数据 - 以及来自管理员 POV 审核和报告) )。

我尝试添加 Stamp 或其他尝试将自定义元数据附加到消息的方法,但在使用 symfony/notifier 时它似乎被抽象了捆。

下面是我用来发送通知的内容(或多或少是 WIP):

class PostService {

    protected NotifierInterface $notifier;

    public function ___construct(NotifierInterface $notifier) {
        $this->notifier = $notifier;
    }

    public function sendNotifications(Post $post) {
        $notification = new PostNotification($post);
        
        $recipients = [];
        foreach($post->getNewsFeed()->getSubscribers() as $user) {
            $recipients[] = new Recipient($user->getEmail(), $user->getMobilePhone());
        }

        $this->notifier->send($notification, ...$recipients);
    }

}
class PostNotification extends Notification implements SmsNotificationInterface {

    protected Post $post;

    public function __construct(Post $post) {
        parent::__construct();
        $this->post = $post;
    }

    public function getChannels(RecipientInterface $recipient): array {
        return ['sms'];
    }

    public function asSmsMessage(SmsRecipientInterface $recipient, string $transport = null): ?SmsMessage {
        if($transport === 'sms') {
            return new SmsMessage($recipient->getPhone(), $this->getPostContentAsSms());
        }

        return null;
    }

    private function getPostContentAsSms() {
        return $post->getTitle()."\n\n".$post->getContent();
    }

}

当这一切完成时,这就是我在 WorkerMessageHandledEvent 中所拥有的全部内容

^ Symfony\Component\Messenger\Event\WorkerMessageHandledEvent^ {#5590
  -envelope: Symfony\Component\Messenger\Envelope^ {#8022
    -stamps: array:7 [
      "Symfony\Component\Messenger\Stamp\BusNameStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\BusNameStamp^ {#10417
          -busName: "messenger.bus.default"
        }
      ]
      "Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp" => array:1 [
        0 => Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp^ {#10419
          -id: "2031"
        }
      ]
      "Symfony\Component\Messenger\Stamp\TransportMessageIdStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\TransportMessageIdStamp^ {#10339
          -id: "2031"
        }
      ]
      "Symfony\Component\Messenger\Stamp\ReceivedStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\ReceivedStamp^ {#5628
          -transportName: "async"
        }
      ]
      "Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp^ {#7306}
      ]
      "Symfony\Component\Messenger\Stamp\AckStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\AckStamp^ {#7159
          -ack: Closure(Envelope $envelope, Throwable $e = null)^ {#6205
            class: "Symfony\Component\Messenger\Worker"
            this: Symfony\Component\Messenger\Worker {#5108 …}
            use: {
              $transportName: "async"
              $acked: & false
            }
          }
        }
      ]
      "Symfony\Component\Messenger\Stamp\HandledStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\HandledStamp^ {#11445
          -result: Symfony\Component\Notifier\Message\SentMessage^ {#2288
            -original: Symfony\Component\Notifier\Message\NullMessage^ {#6625
              -decoratedMessage: Symfony\Component\Notifier\Message\SmsMessage^ {#10348
                -transport: null
                -subject: ".................................................."
                -phone: "0412345678"
              }
            }
            -transport: "null"
            -messageId: null
          }
          -handlerName: "Symfony\Component\Notifier\Messenger\MessageHandler::__invoke"
        }
      ]
    ]
    -message: Symfony\Component\Notifier\Message\SmsMessage^ {#10348}
  }
  -receiverName: "async"
}

doco 向我展示了添加我自己的通知的方法邮票到信封,我猜我可以用它来附加元数据,例如我的 Post 对象,但这意味着我需要使用 MessageBusInterface 来发送通知。我不想这样做,因为我想通过 NotifierInterface 路由消息,以获得渠道策略、短信传输等的所有好处。


tl;dr:我如何获取一些元数据如果我使用 NotifierInterface 发送消息,则为 WorkerMessageHandledEvent

I'm using the Symfony notifier and messenger components to asynchronously send SMS messages (and in the future push and email notifications).

Everything works just fine, however once a message is sent, I'd like to log information about it.

I can catch a successful message by subscribing to WorkerMessageHandledEvent which provides me the Message object, along with the containing Envelope and all its Stamp objects inside. From all the available information, I'll be logging this in my database using an entity named MessageLog.

class MessengerSubscriber implements EventSubscriberInterface {

    public static function getSubscribedEvents() {
        return [
            WorkerMessageHandledEvent::class => ['onHandled']
        ];
    }

    public function onHandled(WorkerMessageHandledEvent $event) {
        $log = new MessageLog();
        $log->setSentAt(new DateTime());

        if($event->getEnvelope()->getMessage() instanceof SmsMessage) {
            $log->setSubject($event->getEnvelope()->getMessage()->getSubject());
            $log->setRecipient($event->getEnvelope()->getMessage()->getPhone());
        }

        // Do more tracking
    }

}

What I'd like to do, is track the object that "invoked" the message. For example, if I have a news feed, and posting a post sends out a notification, I'd like to attribute each logged message to that post (to display audience reach/delivery stats per post - and from an admin POV auditing and reporting).

I've tried to go about adding a Stamp, or other means of trying to attach custom metadata to the message, but it seems to be abstracted when using the symfony/notifier bundle.

The below is what I'm using to send notifications (more or less WIP):

class PostService {

    protected NotifierInterface $notifier;

    public function ___construct(NotifierInterface $notifier) {
        $this->notifier = $notifier;
    }

    public function sendNotifications(Post $post) {
        $notification = new PostNotification($post);
        
        $recipients = [];
        foreach($post->getNewsFeed()->getSubscribers() as $user) {
            $recipients[] = new Recipient($user->getEmail(), $user->getMobilePhone());
        }

        $this->notifier->send($notification, ...$recipients);
    }

}
class PostNotification extends Notification implements SmsNotificationInterface {

    protected Post $post;

    public function __construct(Post $post) {
        parent::__construct();
        $this->post = $post;
    }

    public function getChannels(RecipientInterface $recipient): array {
        return ['sms'];
    }

    public function asSmsMessage(SmsRecipientInterface $recipient, string $transport = null): ?SmsMessage {
        if($transport === 'sms') {
            return new SmsMessage($recipient->getPhone(), $this->getPostContentAsSms());
        }

        return null;
    }

    private function getPostContentAsSms() {
        return $post->getTitle()."\n\n".$post->getContent();
    }

}

By the time this is all done, this is all I have in the WorkerMessageHandledEvent

^ Symfony\Component\Messenger\Event\WorkerMessageHandledEvent^ {#5590
  -envelope: Symfony\Component\Messenger\Envelope^ {#8022
    -stamps: array:7 [
      "Symfony\Component\Messenger\Stamp\BusNameStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\BusNameStamp^ {#10417
          -busName: "messenger.bus.default"
        }
      ]
      "Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp" => array:1 [
        0 => Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp^ {#10419
          -id: "2031"
        }
      ]
      "Symfony\Component\Messenger\Stamp\TransportMessageIdStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\TransportMessageIdStamp^ {#10339
          -id: "2031"
        }
      ]
      "Symfony\Component\Messenger\Stamp\ReceivedStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\ReceivedStamp^ {#5628
          -transportName: "async"
        }
      ]
      "Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp^ {#7306}
      ]
      "Symfony\Component\Messenger\Stamp\AckStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\AckStamp^ {#7159
          -ack: Closure(Envelope $envelope, Throwable $e = null)^ {#6205
            class: "Symfony\Component\Messenger\Worker"
            this: Symfony\Component\Messenger\Worker {#5108 …}
            use: {
              $transportName: "async"
              $acked: & false
            }
          }
        }
      ]
      "Symfony\Component\Messenger\Stamp\HandledStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\HandledStamp^ {#11445
          -result: Symfony\Component\Notifier\Message\SentMessage^ {#2288
            -original: Symfony\Component\Notifier\Message\NullMessage^ {#6625
              -decoratedMessage: Symfony\Component\Notifier\Message\SmsMessage^ {#10348
                -transport: null
                -subject: ".................................................."
                -phone: "0412345678"
              }
            }
            -transport: "null"
            -messageId: null
          }
          -handlerName: "Symfony\Component\Notifier\Messenger\MessageHandler::__invoke"
        }
      ]
    ]
    -message: Symfony\Component\Notifier\Message\SmsMessage^ {#10348}
  }
  -receiverName: "async"
}

The doco shows me ways to add my own stamps to the envelope, which I'm guessing I can use to attach metadata such as my Post object, but this means I need to use the MessageBusInterface to send notifications. I don't want to do because I would like to route messages through the NotifierInterface to gain all the benefits of channel policies, texter transports, etc.


tl;dr: how do I get some metadata through to a WorkerMessageHandledEvent if I send a message using the NotifierInterface

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

也只是曾经 2025-01-21 16:22:58

我找到了让它发挥作用的方法!

本质上,我们这里有两个组件,Symfony notifier 和 Symfony messenger。当一起使用时,它们创建了一种向任意数量的端点发送消息的强大方法。

首先,我所做的是创建一个名为 NotificationStampsInterface 的接口和一个名为 NotificationStamps 的特征,该特征满足该接口(通过使用接口方法存储受保护的数组来读取/写入它) 。

class NotificationStampsInterface {
    
    public function getStamps(): array;

    public function addStamp(StampInterface $stamp);

    public function removeStamp(StampInterface $stamp);
}

然后可以将此接口添加到您的自定义通知对象(在本例中是 PostNotification),以及 NotificationStamps 特征以满足接口方法的要求。

这里的技巧是,当通过通知程序发送通知时,它最终会调用消息组件来发送消息。处理这个问题的位是 Symfony\Component\Notifier\Channel\SmsChannel。本质上,如果 MessageBus 可用,它将通过该总线推送消息,而不是直接通过通知程序。

我们可以扩展 SmsChannel 类,在 notify() 方法中添加我们自己的逻辑。

class SmsNotify extends \Symfony\Component\Notifer\Channel\SmsChannel {
   
    public function notify(Notification $notification, RecipientInterface $recipient, string $transportName = null): void {
        $message = null;
        if ($notification instanceof SmsNotificationInterface) {
            $message = $notification->asSmsMessage($recipient, $transportName);
        }

        if (null === $message) {
            $message = SmsMessage::fromNotification($notification, $recipient);
        }

        if (null !== $transportName) {
            $message->transport($transportName);
        }

        if (null === $this->bus) {
            $this->transport->send($message);
        } else {
            // New logic
            if($notification instanceof NotificationStampsInterface) {
                $envelope = Envelope::wrap($message, $notification->getStamps());
                $this->bus->dispatch($envelope);
            } else {
                $this->bus->dispatch($message);
            }

            // Old logic
            // $this->bus->dispatch($message);
        }
    }
   
}

最后,我们需要通过在 services.yaml 中添加以下内容来覆盖服务

    notifier.channel.sms:
        class: App\Notifier\Channel\SmsChannel
        arguments: ['@texter.transports', '@messenger.default_bus']
        tags:
            - { name: notifier.channel, channel: sms }

,就是这样!我们现在有一种方法可以将标记附加到我们的Notification对象中,该对象将一直传递到WorkerMessageHandledEvent

一个示例用途是(至少对于我的情况)

class RelatedEntityStamp implements StampInterface {

    private string $className;
    private int $classId;

    public function __construct(object $entity) {
        $this->className = get_class($entity);
        $this->classId = $entity->getId();
    }

    /**
     * @return string
     */
    public function getClassName(): string {
        return $this->className;
    }

    /**
     * @return int
     */
    public function getClassId(): int {
        return $this->classId;
    }

}
class PostService {

    protected NotifierInterface $notifier;

    public function ___construct(NotifierInterface $notifier) {
        $this->notifier = $notifier;
    }

    public function sendNotifications(Post $post) {
        $notification = new PostNotification($post);
        $stamp = new RelatedEntityStamp($post);        // Solution
        $notification->addStamp($stamp);               // Solution
        
        $recipients = [];
        foreach($post->getNewsFeed()->getSubscribers() as $user) {
            $recipients[] = new Recipient($user->getEmail(), $user->getMobilePhone());
        }

        $this->notifier->send($notification, ...$recipients);
    }

}

一旦发送消息,转储结果表明我们确实在事件触发时注册了我们的标记。

^ Symfony\Component\Messenger\Event\WorkerMessageHandledEvent^ {#1078
  -envelope: Symfony\Component\Messenger\Envelope^ {#1103
    -stamps: array:8 [
      "App\Notification\Stamp\RelatedEntityStamp" => array:1 [
        0 => App\Notification\Stamp\RelatedEntityStamp^ {#1062
          -className: "App\Entity\Post"
          -classId: 207
        }
      ]
      "Symfony\Component\Messenger\Stamp\BusNameStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\BusNameStamp^ {#1063
          -busName: "messenger.bus.default"
        }
      ]
      "Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp" => array:1 [
        0 => Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp^ {#1066
          -id: "2590"
        }
      ]
      "Symfony\Component\Messenger\Stamp\TransportMessageIdStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\TransportMessageIdStamp^ {#1067
          -id: "2590"
        }
      ]
      "Symfony\Component\Messenger\Stamp\ReceivedStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\ReceivedStamp^ {#1075
          -transportName: "async"
        }
      ]
      "Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp^ {#1076}
      ]
      "Symfony\Component\Messenger\Stamp\AckStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\AckStamp^ {#1077
          -ack: Closure(Envelope $envelope, Throwable $e = null)^ {#1074
            class: "Symfony\Component\Messenger\Worker"
            this: Symfony\Component\Messenger\Worker {#632 …}
            use: {
              $transportName: "async"
              $acked: & false
            }
          }
        }
      ]
      "Symfony\Component\Messenger\Stamp\HandledStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\HandledStamp^ {#1101
          -result: Symfony\Component\Notifier\Message\SentMessage^ {#1095
            -original: Symfony\Component\Notifier\Message\NullMessage^ {#1091
              -decoratedMessage: Symfony\Component\Notifier\Message\SmsMessage^ {#1060
                -transport: null
                -subject: ".................................................."
                -phone: "0412345678"
              }
            }
            -transport: "null"
            -messageId: null
          }
          -handlerName: "Symfony\Component\Notifier\Messenger\MessageHandler::__invoke"
        }
      ]
    ]
    -message: Symfony\Component\Notifier\Message\SmsMessage^ {#1060}
  }
  -receiverName: "async"
}

I've found a way to make it work!

Essentially what happens is that we have two components here, the Symfony notifier and the Symfony messenger. When used together, they create a powerful way to send messages to any number of endpoints.

Firstly what I did was create an interface called NotificationStampsInterface and a trait called NotificationStamps that satisfies the interface (by storing a protected array using the interface methods to read/write to it).

class NotificationStampsInterface {
    
    public function getStamps(): array;

    public function addStamp(StampInterface $stamp);

    public function removeStamp(StampInterface $stamp);
}

This interface can then be added onto your custom notification object, in this instance PostNotification, alongside with the NotificationStamps trait to satisfy the interface methods.

The trick here is that when sending a notification via the notifier, it ultimately calls on the messenger component to send the message. The bit that handles this is Symfony\Component\Notifier\Channel\SmsChannel. Essentially, if a MessageBus is available, it will push messages through that rather than going straight though the notifier.

We can extend the SmsChannel class to add our own logic inside notify() method.

class SmsNotify extends \Symfony\Component\Notifer\Channel\SmsChannel {
   
    public function notify(Notification $notification, RecipientInterface $recipient, string $transportName = null): void {
        $message = null;
        if ($notification instanceof SmsNotificationInterface) {
            $message = $notification->asSmsMessage($recipient, $transportName);
        }

        if (null === $message) {
            $message = SmsMessage::fromNotification($notification, $recipient);
        }

        if (null !== $transportName) {
            $message->transport($transportName);
        }

        if (null === $this->bus) {
            $this->transport->send($message);
        } else {
            // New logic
            if($notification instanceof NotificationStampsInterface) {
                $envelope = Envelope::wrap($message, $notification->getStamps());
                $this->bus->dispatch($envelope);
            } else {
                $this->bus->dispatch($message);
            }

            // Old logic
            // $this->bus->dispatch($message);
        }
    }
   
}

Lastly we need to override the service by adding the following in services.yaml

    notifier.channel.sms:
        class: App\Notifier\Channel\SmsChannel
        arguments: ['@texter.transports', '@messenger.default_bus']
        tags:
            - { name: notifier.channel, channel: sms }

And that's it! We now have a way to append stamps to our Notification object that will carry all the way through to the WorkerMessageHandledEvent.

An example use would be (for my situation at least)

class RelatedEntityStamp implements StampInterface {

    private string $className;
    private int $classId;

    public function __construct(object $entity) {
        $this->className = get_class($entity);
        $this->classId = $entity->getId();
    }

    /**
     * @return string
     */
    public function getClassName(): string {
        return $this->className;
    }

    /**
     * @return int
     */
    public function getClassId(): int {
        return $this->classId;
    }

}
class PostService {

    protected NotifierInterface $notifier;

    public function ___construct(NotifierInterface $notifier) {
        $this->notifier = $notifier;
    }

    public function sendNotifications(Post $post) {
        $notification = new PostNotification($post);
        $stamp = new RelatedEntityStamp($post);        // Solution
        $notification->addStamp($stamp);               // Solution
        
        $recipients = [];
        foreach($post->getNewsFeed()->getSubscribers() as $user) {
            $recipients[] = new Recipient($user->getEmail(), $user->getMobilePhone());
        }

        $this->notifier->send($notification, ...$recipients);
    }

}

Once the message is sent, dumping the result shows that we do indeed have our stamp registered at the point where our event fires.

^ Symfony\Component\Messenger\Event\WorkerMessageHandledEvent^ {#1078
  -envelope: Symfony\Component\Messenger\Envelope^ {#1103
    -stamps: array:8 [
      "App\Notification\Stamp\RelatedEntityStamp" => array:1 [
        0 => App\Notification\Stamp\RelatedEntityStamp^ {#1062
          -className: "App\Entity\Post"
          -classId: 207
        }
      ]
      "Symfony\Component\Messenger\Stamp\BusNameStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\BusNameStamp^ {#1063
          -busName: "messenger.bus.default"
        }
      ]
      "Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp" => array:1 [
        0 => Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp^ {#1066
          -id: "2590"
        }
      ]
      "Symfony\Component\Messenger\Stamp\TransportMessageIdStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\TransportMessageIdStamp^ {#1067
          -id: "2590"
        }
      ]
      "Symfony\Component\Messenger\Stamp\ReceivedStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\ReceivedStamp^ {#1075
          -transportName: "async"
        }
      ]
      "Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp^ {#1076}
      ]
      "Symfony\Component\Messenger\Stamp\AckStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\AckStamp^ {#1077
          -ack: Closure(Envelope $envelope, Throwable $e = null)^ {#1074
            class: "Symfony\Component\Messenger\Worker"
            this: Symfony\Component\Messenger\Worker {#632 …}
            use: {
              $transportName: "async"
              $acked: & false
            }
          }
        }
      ]
      "Symfony\Component\Messenger\Stamp\HandledStamp" => array:1 [
        0 => Symfony\Component\Messenger\Stamp\HandledStamp^ {#1101
          -result: Symfony\Component\Notifier\Message\SentMessage^ {#1095
            -original: Symfony\Component\Notifier\Message\NullMessage^ {#1091
              -decoratedMessage: Symfony\Component\Notifier\Message\SmsMessage^ {#1060
                -transport: null
                -subject: ".................................................."
                -phone: "0412345678"
              }
            }
            -transport: "null"
            -messageId: null
          }
          -handlerName: "Symfony\Component\Notifier\Messenger\MessageHandler::__invoke"
        }
      ]
    ]
    -message: Symfony\Component\Notifier\Message\SmsMessage^ {#1060}
  }
  -receiverName: "async"
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文