Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Going Asynchronous with Symfony Messenger

Hugo Hamon
February 26, 2025

Going Asynchronous with Symfony Messenger

Sending notifications, synchronizing data with third-party services, or performing time / memory heavy tasks are use cases that lend themselves well for asynchronous programming. Thanks to the Symfony Messenger, you'll not only learn how to dispatch, route, serialize and process async messages ; but also learn how to monitor them, perform logging, retry failing jobs, deal with priority queues and even tests asynchronous messages in a Symfony app.

Hugo Hamon

February 26, 2025
Tweet

More Decks by Hugo Hamon

Other Decks in Programming

Transcript

  1. @hhamon / [email protected] Hugo HAMON Consultant PHP / Symfony @

    KODERO Symfony Certi fi ed Expert Developer
  2. Message final class RegisterUserController extends AbstractController { // ... #[Route('/registration',

    name: 'app_register_user', methods: ['GET', 'POST'])] public function __invoke(Request $request): Response { $form = $this->createForm(UserRegistrationType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { /** @var UserRegistration $data */ $data = $form->getData(); $this->registerUser->registerUser( $data->getEmail(), $data->getPassword(), $data->getGender(), $data->getFullName(), $data->getCountry(), $data->getBirthdate(), ); return $this->redirectToRoute('app_confirm_user_registration'); } return $this->render('register_user/index.html.twig', [ 'form' => $form->createView(), ]); } }
  3. Message #[UniqueEntity( fields: ['email'], message: 'This email is already taken.',

    entityClass: User::class, )] final class UserRegistration { #[NotBlank] #[Email] private ?string $email = null; #[NotBlank] #[PasswordStrength(minScore: PasswordStrength::STRENGTH_MEDIUM)] private ?string $password = null; #[NotBlank] private ?Gender $gender = null; #[NotBlank] #[Length(min: 3, max: 150)] private ?string $fullName = null; #[Country] private ?string $country = null; #[Range(max: '-18 years')] private ?DateTimeImmutable $birthdate = null; // ... getters and setters }
  4. On user registration, we would like to… - Hash the

    user’s submitted password - Create a User record and save it to the database - Send a welcome email with a validation link - Share the user’s profile with Intercom - Save the returned Intercom ID to the database
  5. Message class RegisterUser { private readonly PasswordHasherInterface $passwordHasher; public function

    __construct( private readonly PasswordHasherFactoryInterface $passwordHasherFactory, private readonly UserRepository $userRepository, private readonly UserEmailVerifier $userEmailVerifier, private readonly IntercomClient $intercomClient, ) { $this->passwordHasher = $this->passwordHasherFactory->getPasswordHasher(User::class); } public function registerUser( string $email, string $password, Gender|string $gender, string $fullName, ?string $country = null, DateTimeInterface|string|null $birthdate = null, ): User { // ... } }
  6. Message class RegisterUser { // ... public function registerUser(...): User

    { // ... $hashedPassword = $this->passwordHasher->hash($password); $user = User::register( $email, $hashedPassword, $gender, $fullName, $country, $birthdate, ); $this->userRepository->save($user); $this->userEmailVerifier->sendEmailConfirmation($user); $intercomId = $this->intercomClient->createUser($user); $user->setIntercomId($intercomId->id); $this->userRepository->save($user); return $user; } }
  7. Message class UserEmailVerifier { // ... public function sendEmailConfirmation(User $user):

    void { $signatureComponents = $this->verifyEmailHelper->generateSignature( 'app_verify_user_email', (string) $user->getId(), $user->getEmail(), ['id' => (string) $user->getId()], ); $email = new TemplatedEmail() ->from(new Address('[email protected]', 'DevCon 2025')) ->to(new Address($user->getEmail(), $user->getFullName())) ->subject('Please Confirm your Email') ->htmlTemplate('email/user_confirmation_email.html.twig'); $context = $email->getContext(); $context['greetings'] = $user->getFullName(); $context['signedUrl'] = $signatureComponents->getSignedUrl(); $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey(); $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData(); $email->context($context); $this->mailer->send($email); } }
  8. Message final class IntercomProxyClient implements IntercomClient { // ... public

    function __construct( private readonly IntercomContacts $intercomContacts, ) { } public function createUser(User $user): IntercomContact { // Make a POST HTTP request to Intercom REST API $contact = $this->intercomContacts->create([ 'type' => 'user', 'external_id' => (string) $user->getId(), 'email' => $user->getEmail(), 'name' => $user->getFullName(), 'signed_up_at' => $user->getCreatedAt()->getTimestamp(), 'custom_attributes' => [ 'gender' => $user->getGender()->value, 'birthdate' => $user->getBirthdate()?->format('Y-m-d'), 'country' => $user->getCountry(), 'email_verified_at' => $user->getEmailVerifiedAt()?->getTimestamp(), ], ]); return IntercomContact::fromStdClass($contact); } }
  9. Although this code works, it may have issues at some

    point. - Latency (because of IOs / network) - Sending the confirmation email may fail - Intercom API may be not responding - End user can “feel” the page is not responding fast enough
  10. “ Messenger provides a message bus with the ability to

    send messages and then handle them immediately in your application or send them through transports (e.g. queues) to be handled later. ” — Symfony Official Doc.
  11. # config/packages/messenger.yaml framework: messenger: # Uncomment this to send failed

    messages to this transport for later handling. # failure_transport: failed transports: # https://symfony.com/doc/current/messenger.html#transport-configuration # async: '%env(MESSENGER_TRANSPORT_DSN)%' # failed: 'doctrine://default?queue_name=failed' # sync: 'sync://' routing: # Route your messages to the transports # 'App\Message\YourMessage': async Installation $ composer require symfony/messenger
  12. A message is a data structure that holds the needed

    information for the message handler to process that message later. It mainly takes the shape of a pure PHP object that has the sole requirement of being serializable.
  13. Message namespace App\User\Message; use Symfony\Component\Messenger\Attribute\AsMessage; use Symfony\Component\Uid\Uuid; #[AsMessage] final readonly

    class SendWelcomeEmailMessage { public function __construct( private string $userId, ) { } public function getUserId(): Uuid { return Uuid::fromString($this->userId); } }
  14. Message namespace App\Intercom\Message; use Symfony\Component\Messenger\Attribute\AsMessage; use Symfony\Component\Uid\Uuid; #[AsMessage] final readonly

    class SyncIntercomUserContactMessage { public function __construct( private string $userId, ) { } public function getUserId(): Uuid { return Uuid::fromString($this->userId); } }
  15. The message handler is a callable that is responsible for

    processing the message it receives. It mainly takes the shape of an invokable PHP service class.
  16. Message namespace App\User\MessageHandler; use App\Repository\UserRepository; use App\User\Message\SendWelcomeEmailMessage; use App\User\UserEmailVerifier; use

    Symfony\Component\Messenger\Attribute\AsMessageHandler; #[AsMessageHandler] final class SendWelcomeEmailMessageHandler { public function __construct( private readonly UserRepository $userRepository, private readonly UserEmailVerifier $userEmailVerifier, ) { } public function __invoke(SendWelcomeEmailMessage $message): void { $user = $this->userRepository->byId($message->getUserId()); $this->userEmailVerifier->sendEmailConfirmation($user); } }
  17. Message namespace App\Intercom\MessageHandler; // ... #[AsMessageHandler] final class SyncIntercomUserContactMessageHandler {

    public function __construct( private readonly UserRepository $userRepository, private readonly IntercomClient $intercomClient, ) { } public function __invoke(SyncIntercomUserContactMessage $message): void { $user = $this->userRepository->byId($message->getUserId()); if ((string) $user->getIntercomId() !== '') { $this->intercomClient->updateUser($user); return; } $contact = $this->intercomClient->createUser($user); $user->setIntercomId($contact->id); $this->userRepository->save($user); } }
  18. The message bus is a service object that is responsible

    for conveying the message to its message handler. To do so, it delegates to a transport layer and a series of middleware to manipulate the message.
  19. Message interface MessageBusInterface { /** * Dispatches the given message.

    * * @throws ExceptionInterface */ public function dispatch(object $message, array $stamps = []): Envelope; }
  20. Message final class RegisterUserController extends AbstractController { public function __construct(

    private readonly RegisterUser $registerUser, private readonly MessageBusInterface $messageBus, ) { } #[Route('/registration', name: 'app_register_user', methods: ['GET', 'POST'])] public function __invoke(Request $request): Response { $form = $this->createForm(UserRegistrationType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { /** @var UserRegistration $data */ $data = $form->getData(); $user = $this->registerUser->registerUser(...); $this->messageBus->dispatch(new SendWelcomeEmailMessage((string) $user->getId())); $this->messageBus->dispatch(new SyncIntercomUserContactMessage((string) $user->getId())); return $this->redirectToRoute('app_confirm_user_registration'); } return $this->render('register_user/index.html.twig', [ 'form' => $form->createView(), ]); } }
  21. The transport is the piece of software that is responsible

    for communicating with a message broker or any third parties. It provides layers for both sending and receiving a message.
  22. Why do you sometimes need async tasks? 📨 Sending a

    notification 🗂 Updating a search index ⚙ Syncing data with external tools 📋 Computing data or documents (reports, archives, etc.) 🎥 Processing large pieces of documents
  23. Message # .env ###> symfony/messenger ### # Choose one of

    the transports below # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###< symfony/messenger ### # config/packages/messenger.yaml framework: messenger: transports: async: '%env(MESSENGER_TRANSPORT_DSN)%'
  24. Message #[AsMessage('async')] final readonly class SendWelcomeEmailMessage { // ... }

    #[AsMessage('async')] final readonly class SyncIntercomUserContactMessage { // ... }
  25. Message ❯ symfony console debug:config framework messenger Current configuration for

    "framework.messenger" =============================================== transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' serializer: null options: { } failure_transport: null retry_strategy: service: null max_retries: 3 delay: 1000 multiplier: 2 max_delay: 0 jitter: 0.1 rate_limiter: null routing: { } enabled: true serializer: default_serializer: messenger.transport.native_php_serializer symfony_serializer: format: json context: { } failure_transport: null stop_worker_on_signals: { } default_bus: null buses: messenger.bus.default: default_middleware: enabled: true allow_no_handlers: false allow_no_senders: true middleware: { }
  26. Message ❯ symfony console debug:messenger
 Messenger ========= messenger.bus.default --------------------- The

    following messages can be dispatched: ---------------------------------------------------------------------------------- App\Intercom\Message\SyncIntercomUserContactMessage handled by App\Intercom\MessageHandler\SyncIntercomUserContactMessageHandler App\User\Message\SendWelcomeEmailMessage handled by App\User\MessageHandler\SendWelcomeEmailMessageHandler ... ----------------------------------------------------------------------------------
  27. Message ❯ symfony console messenger:monitor Messenger Monitor Overview ========================== --------

    --------- -- Messenger Workers ---------- ---------- Status Up Time Transports Queues Messages Memory -------- --------- ------------ -------- ---------- ---------- idle 19 secs async n/a 2 33.55 MB -------- --------- ------------ -------- ---------- ---------- ------ Messenger Transports ------- Name Queued Messages Workers ------- ----------------- --------- async 0 1 ------- ----------------- --------- ------ Historical Snapshot ------- Period In Last Day Messages Processed 6 Fail Rate 0% Avg. Wait Time 11 hrs Avg. Handling Time < 1 sec Handled Per Minute 0 Handled Per Hour 0.25 Handled Per Day 6 -------------------- -------------
  28. Each message is wrapped into an envelope, on which any

    number of stamps can be sticked to in order to attach some useful metadata to it. ✉
  29. Message $message = new SyncIntercomUserContactMessage((string) $user->getId()); // Shorthand syntax $this->messageBus->dispatch($message,

    [ DelayStamp::delayFor(new DateInterval('PT20S')), new ValidationStamp(['user_registration']), // + any other stamps ]); // Expanded syntax $this->messageBus->dispatch( new Envelope($message)->with( DelayStamp::delayFor(new DateInterval('PT20S')), new ValidationStamp(['user_registration']), // + any other stamps ), );
  30. Message use Symfony\Component\Messenger\Attribute\AsMessage; use Symfony\Component\Uid\Uuid; use Zenstruck\Messenger\Monitor\Stamp\TagStamp; #[AsMessage('async')] #[TagStamp('notification')] #[TagStamp('notification.email')]

    final readonly class SendWelcomeEmailMessage { // ... } #[AsMessage('async')] #[TagStamp('marketing')] #[TagStamp('intercom')] final readonly class SyncIntercomUserContactMessage { // ... }
  31. Middleware are pieces of software that sit around the bus,

    so that they can access any messages and their wrapper (the envelope) while they’re being dispatched. They are cross cutting concerns applicable throughout the application and affecting the entire message bus. Their responsibilities can be any global concern such as logging, validating a message, starting a transaction, ... They are also responsible for calling the next middleware in the chain, which means they can tweak the envelope, by adding stamps to it or even replacing it, as well as interrupt the middleware chain.
  32. Message class ValidationMiddleware implements MiddlewareInterface { public function __construct( private

    ValidatorInterface $validator, ) { } public function handle(Envelope $envelope, StackInterface $stack): Envelope { $message = $envelope->getMessage(); $groups = null; /** @var ValidationStamp|null $validationStamp */ if ($validationStamp = $envelope->last(ValidationStamp::class)) { $groups = $validationStamp->getGroups(); } $violations = $this->validator->validate($message, null, $groups); if (\count($violations)) { throw new ValidationFailedException($message, $violations, $envelope); } return $stack->next()->handle($envelope, $stack); } }
  33. Message #[AsMessage] #[UniqueEntity(fields: ['email'], entityClass: User::class, errorPath: 'email')] final readonly

    class RegisterUser { public function __construct( private string $id, #[NotBlank] #[Email] private string $email, #[NotBlank] #[Choice(choices: ['male', 'female', 'other'])] private string $gender, #[NotBlank] #[Length(min: 2, max: 255)] private string $fullName, #[NotBlank] #[PasswordStrength(minScore: PasswordStrength::STRENGTH_STRONG)] private string $password, #[Country] private ?string $country = null, #[Date] private ?string $birthdate = null, ) { } public function getId(): Uuid { return Uuid::fromString($this->id); } // + getter methods }
  34. Message #[AsMessageHandler(bus: ‘messenger.bus.default’)] class RegisterUserHandler { public function __construct( private

    readonly PasswordHasherFactoryInterface $passwordHasherFactory, private readonly EntityManagerInterface $entityManager, ) { } public function __invoke(RegisterUser $command): void { $passwordHasher = $this->passwordHasherFactory->getPasswordHasher(User::class); $user = User::register( $command->getId(), $command->getEmail(), $passwordHasher->hash($command->getPassword()), $command->getGender(), $command->getFullName(), $command->getCountry(), $command->getBirthdate(), ); $this->entityManager->persist($user); } }
  35. Message #[AsCommand('app:register-user')] final class RegisterUserCommand extends Command { // ...

    protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $id = Uuid::v7(); $command = new RegisterUser( id: (string) $id, email: mb_strtolower($input->getArgument('email')), gender: $input->getArgument('gender'), fullName: $input->getArgument('full-name'), password: $io->askHidden('Password:'), country: $input->getOption('country'), birthdate: $input->getOption('birthdate'), ); try { $this->messageBus->dispatch($command); } catch (ValidationFailedException $e) { $this->displayValidationErrors($io, $e); return self::FAILURE; } $io->success(\sprintf('User %s was registered.', $id)); return Command::SUCCESS; } }
  36. Message ❯ symfony console app:register-user [email protected] wip "Hello World" Password::

    > [WARNING] Validation failed. * email: This value is already used. * gender: The value you selected is not a valid choice. * password: The password strength is too low. Please use a stronger password.
  37. Message ❯ symfony console app:register-user \
 [email protected] \
 female \

    "Alice Smith" \ --country US \ --birthdate 1983-01-08 Password:: > [OK] User 01952d82-ef09-7b25-8905-e60116786617 was registered.
  38. Message [Application] Feb 22 11:55:10 |INFO | DOCTRI Connecting with

    parameters array{"use_savepoints":true,"driver":"pdo_pgsql","idle_connection_ttl":600,"host":"127.0.0.1","port":32812,"user":"app","password":"<redacted>","driverO ptions":[],"defaultTableOptions":[],"dbname":"confoo_2025_messenger","serverVersion":"16","charset":"utf8"} params={“charset":"utf8","dbname":"confoo_2025_messenger","defaultTableOptions":[],"driver":"pdo_pgsql","driverOptions": [],"host":"127.0.0.1","idle_connection_ttl":600,"password":"\u003credacted\u003e","port":32812,"serverVersion":"16","use_savepoints":true,"user":"app"} [Application] Feb 22 11:55:10 |DEBUG | DOCTRI Executing statement: SELECT t0.id AS id_1, t0.email AS email_2, t0.password AS password_3, t0.gender AS gender_4, t0.full_name AS full_name_5, t0.country AS country_6, t0.intercom_id AS intercom_id_7, t0.birthdate AS birthdate_8, t0.email_verified_at AS email_verified_at_9, t0.created_at AS created_at_10 FROM "user" t0 WHERE t0.email = ? LIMIT 2 (parameters: array{"1":"[email protected]"}, types: array{"1":2}) params={"1":"[email protected]"} sql="SELECT t0.id AS id_1, t0.email AS email_2, t0.password AS password_3, t0.gender AS gender_4, t0.full_name AS full_name_5, t0.country AS country_6, t0.intercom_id AS intercom_id_7, t0.birthdate AS birthdate_8, t0.email_verified_at AS email_verified_at_9, t0.created_at AS created_at_10 FROM \"user\" t0 WHERE t0.email = ? LIMIT 2" types={"1":2} [Application] Feb 22 11:55:10 |DEBUG | DOCTRI Beginning transaction [Application] Feb 22 11:55:11 |INFO | MESSEN Message App\User\Message\Command\RegisterUser handled by App\User\MessageHandler\CommandHandler\RegisterUserHandler::__invoke class="App\\User\\Message\\Command\\RegisterUser" handler="App\\User\ \MessageHandler\\CommandHandler\\RegisterUserHandler::__invoke" [Application] Feb 22 11:55:11 |DEBUG | DOCTRI Executing statement: SAVEPOINT DOCTRINE_2 sql="SAVEPOINT DOCTRINE_2" [Application] Feb 22 11:55:11 |DEBUG | DOCTRI Executing statement: INSERT INTO "user" (id, email, password, gender, full_name, country, intercom_id, birthdate, email_verified_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) (parameters: array{"1":"01952d82-ef09-7b25-8905- e60116786617","2":"[email protected]","3":"$2y$13$pJAAJcIWaUWqagfKZRd1ouE3SFOosfjpzikREIShb9d.Kdz0JDNX2","4":"female","5":"Alice Smith","6":"US","7":null,"8":"1983-01-08","9":null,"10":"2025-02-22 11:55:11"}, types: array{"1":2,"2":2,"3":2,"4":2,"5":2,"6":2,"7":2,"8":2,"9":2,"10":2}) params={"1":"01952d82-ef09-7b25-8905-e60116786617","10":"2025-02-22 11:55:11","2":"[email protected]","3":"$2y$13$pJAAJcIWaUWqagfKZRd1ouE3SFOosfjpzikREIShb9d.Kdz0JDNX2","4":"female","5":"Alice Smith","6":"US","7":null,"8":"1983-01-08","9":null} sql="INSERT INTO \"user\" (id, email, password, gender, full_name, country, intercom_id, birthdate, email_verified_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" types={"1":2,"10":2,"2":2,"3":2,"4":2,"5":2,"6":2,"7":2,"8":2,"9":2} [Application] Feb 22 11:55:11 |DEBUG | DOCTRI Executing statement: RELEASE SAVEPOINT DOCTRINE_2 sql="RELEASE SAVEPOINT DOCTRINE_2" [Application] Feb 22 11:55:11 |DEBUG | DOCTRI Committing transaction [Application] Feb 22 11:55:11 |INFO | DOCTRI Disconnecting
  39. Message #[AsMessageHandler(bus: 'messenger.bus.default')] class RegisterUserHandler { public function __construct( //

    ... private readonly MessageBusInterface $messageBus, ) { } public function __invoke(RegisterUser $command): void { // ... $this->messageBus->dispatch( new SendWelcomeEmailMessage((string) $user->getId()), [new DispatchAfterCurrentBusStamp()], ); $this->messageBus->dispatch( new SyncIntercomUserContactMessage((string) $user->getId()), [new DispatchAfterCurrentBusStamp()], ); } }
  40. Message [Application] Feb 22 12:12:33 |INFO | DOCTRI Connecting with

    parameters array{"use_savepoints":true,"driver":"pdo_pgsql","idle_connection_ttl":600,"host":"127.0.0.1","port":32812,"user":"app","password":"<redacted>","driverOptions": [],"defaultTableOptions":[],"dbname":"confoo_2025_messenger","serverVersion":"16","charset":"utf8"} params={"charset":"utf8","dbname":"confoo_2025_messenger","defaultTableOptions":[],"driver":"pdo_pgsql","driverOptions": [],"host":"127.0.0.1","idle_connection_ttl":600,"password":"\u003credacted\u003e","port":32812,"serverVersion":"16","use_savepoints":true,"user":"app"} [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: SELECT t0.id AS id_1, t0.email AS email_2, t0.password AS password_3, t0.gender AS gender_4, t0.full_name AS full_name_5, t0.country AS country_6, t0.intercom_id AS intercom_id_7, t0.birthdate AS birthdate_8, t0.email_verified_at AS email_verified_at_9, t0.created_at AS created_at_10 FROM "user" t0 WHERE t0.email = ? LIMIT 2 (parameters: array{"1":"[email protected]"}, types: array{"1":2}) params={"1":"[email protected]"} sql="SELECT t0.id AS id_1, t0.email AS email_2, t0.password AS password_3, t0.gender AS gender_4, t0.full_name AS full_name_5, t0.country AS country_6, t0.intercom_id AS intercom_id_7, t0.birthdate AS birthdate_8, t0.email_verified_at AS email_verified_at_9, t0.created_at AS created_at_10 FROM \"user\" t0 WHERE t0.email = ? LIMIT 2" types={"1":2} [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Beginning transaction [Application] Feb 22 12:12:33 |INFO | MESSEN Message App\User\Message\Command\RegisterUser handled by App\User\MessageHandler\CommandHandler\RegisterUserHandler::__invoke class="App\\User\\Message\\Command\\RegisterUser" handler="App\\User\\MessageHandler\\CommandHandler\\RegisterUserHandler::__invoke" [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: SAVEPOINT DOCTRINE_2 sql="SAVEPOINT DOCTRINE_2" [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: INSERT INTO "user" (id, email, password, gender, full_name, country, intercom_id, birthdate, email_verified_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) (parameters: array{"1":"01952d92-d853-7cc6-9d9b-0d1b6ddec17d","2":"[email protected]","3":"$2y$13$hI2T6ea1Zl3ZxsKo6GJz5Op0XR.nZLd37Ia498K.VwMW2qZYLEqC6","4":"male","5":"Tom Hardy","6":"GB","7":null,"8":"1977-09-15","9":null,"10":"2025-02-22 12:12:33"}, types: array{"1":2,"2":2,"3":2,"4":2,"5":2,"6":2,"7":2,"8":2,"9":2,"10":2}) params={"1":"01952d92-d853-7cc6-9d9b-0d1b6ddec17d","10":"2025-02-22 12:12:33","2":"[email protected]","3":"$2y$13$hI2T6ea1Zl3ZxsKo6GJz5Op0XR.nZLd37Ia498K.VwMW2qZYLEqC6","4":"male","5":"Tom Hardy","6":"GB","7":null,"8":"1977-09-15","9":null} sql="INSERT INTO \"user\" (id, email, password, gender, full_name, country, intercom_id, birthdate, email_verified_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" types={"1":2,"10":2,"2":2,"3":2,"4":2,"5":2,"6":2,"7":2,"8":2,"9":2} [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: RELEASE SAVEPOINT DOCTRINE_2 sql="RELEASE SAVEPOINT DOCTRINE_2" [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Committing transaction [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Beginning transaction [Application] Feb 22 12:12:33 |INFO | MESSEN Sending message App\User\Message\SendWelcomeEmailMessage with async sender using Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport alias="async" class="App\\User\ \Message\\SendWelcomeEmailMessage" sender="Symfony\\Component\\Messenger\\Bridge\\Doctrine\\Transport\\DoctrineTransport" [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: SAVEPOINT DOCTRINE_2 sql="SAVEPOINT DOCTRINE_2" [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: INSERT INTO messenger_messages (body, headers, queue_name, created_at, available_at) VALUES(?, ?, ?, ?, ?) RETURNING id (parameters: array{"1":"O:36:\\\"Symfony\\\ \Component\\\\Messenger\\\\Envelope\\\":2:{s:44:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Envelope\\0stamps\\\";a:2:{s:46:\\\"Symfony\\\\Component\\\\Messenger\\\\Stamp\\\\BusNameStamp\\\";a:1:{i:0;O:46:\\\"Symfony\\\\Component\\\ \Messenger\\\\Stamp\\\\BusNameStamp\\\":1:{s:55:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Stamp\\\\BusNameStamp\\0busName\\\";s:21:\\\"messenger.bus.default\\\";}}s:46:\\\"Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\ \";a:1:{i:0;O:46:\\\"Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\\":2:{s:53:\\\"\\0Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\0runId\\\";i:401261662;s:60:\\\"\\0Zenstruck\\\\Messenger\\\\Monitor\\\ \Stamp\\\\MonitorStamp\\0dispatchedAt\\\";O:33:\\\"Symfony\\\\Component\\\\Clock\\\\DatePoint\\\":3:{s:4:\\\"date\\\";s:26:\\\"2025-02-22 12:12:33.700516\\\";s:13:\\\"timezone_type\\\";i:3;s:8:\\\"timezone\\\";s:3:\\\"UTC\\\";}}}} s:45:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Envelope\\0message\\\";O:40:\\\"App\\\\User\\\\Message\\\\SendWelcomeEmailMessage\\\":1:{s:48:\\\"\\0App\\\\User\\\\Message\\\\SendWelcomeEmailMessage\\0userId\\\";s:36:\\\"01952d92- d853-7cc6-9d9b-0d1b6ddec17d\\\";}}","2":"[]","3":"default","4":"2025-02-22 12:12:33","5":"2025-02-22 12:12:33"}, types: array{"1":2,"2":2,"3":2,"4":2,"5":2}) params={"1":"O:36:\\\"Symfony\\\\Component\\\\Messenger\\\\Envelope\\\":2: {s:44:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Envelope\\0stamps\\\";a:2:{s:46:\\\"Symfony\\\\Component\\\\Messenger\\\\Stamp\\\\BusNameStamp\\\";a:1:{i:0;O:46:\\\"Symfony\\\\Component\\\\Messenger\\\\Stamp\\\\BusNameStamp\\\":1: {s:55:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Stamp\\\\BusNameStamp\\0busName\\\";s:21:\\\"messenger.bus.default\\\";}}s:46:\\\"Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\\";a:1:{i:0;O:46:\\\"Zenstruck\\\ \Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\\":2:{s:53:\\\"\\0Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\0runId\\\";i:401261662;s:60:\\\"\\0Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\ \0dispatchedAt\\\";O:33:\\\"Symfony\\\\Component\\\\Clock\\\\DatePoint\\\":3:{s:4:\\\"date\\\";s:26:\\\"2025-02-22 12:12:33.700516\\\";s:13:\\\"timezone_type\\\";i:3;s:8:\\\"timezone\\\";s:3:\\\"UTC\\\";}}}}s:45:\\\"\\0Symfony\\\ \Component\\\\Messenger\\\\Envelope\\0message\\\";O:40:\\\"App\\\\User\\\\Message\\\\SendWelcomeEmailMessage\\\":1:{s:48:\\\"\\0App\\\\User\\\\Message\\\\SendWelcomeEmailMessage\\0userId\\\";s:36:\\\"01952d92- d853-7cc6-9d9b-0d1b6ddec17d\\\";}}","2":"[]","3":"default","4":"2025-02-22 12:12:33","5":"2025-02-22 12:12:33"} sql="INSERT INTO messenger_messages (body, headers, queue_name, created_at, available_at) VALUES(?, ?, ?, ?, ?) RETURNING id" types={"1":2,"2":2,"3":2,"4":2,"5":2} [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: RELEASE SAVEPOINT DOCTRINE_2 sql="RELEASE SAVEPOINT DOCTRINE_2" [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Committing transaction [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Beginning transaction [Application] Feb 22 12:12:33 |INFO | MESSEN Sending message App\Intercom\Message\SyncIntercomUserContactMessage with async sender using Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport alias="async" class="App\\Intercom\\Message\\SyncIntercomUserContactMessage" sender="Symfony\\Component\\Messenger\\Bridge\\Doctrine\\Transport\\DoctrineTransport" [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: SAVEPOINT DOCTRINE_2 sql="SAVEPOINT DOCTRINE_2" [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: INSERT INTO messenger_messages (body, headers, queue_name, created_at, available_at) VALUES(?, ?, ?, ?, ?) RETURNING id (parameters: array{"1":"O:36:\\\"Symfony\\\ \Component\\\\Messenger\\\\Envelope\\\":2:{s:44:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Envelope\\0stamps\\\";a:2:{s:46:\\\"Symfony\\\\Component\\\\Messenger\\\\Stamp\\\\BusNameStamp\\\";a:1:{i:0;O:46:\\\"Symfony\\\\Component\\\ \Messenger\\\\Stamp\\\\BusNameStamp\\\":1:{s:55:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Stamp\\\\BusNameStamp\\0busName\\\";s:21:\\\"messenger.bus.default\\\";}}s:46:\\\"Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\ \";a:1:{i:0;O:46:\\\"Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\\":2:{s:53:\\\"\\0Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\0runId\\\";i:174116437;s:60:\\\"\\0Zenstruck\\\\Messenger\\\\Monitor\\\ \Stamp\\\\MonitorStamp\\0dispatchedAt\\\";O:33:\\\"Symfony\\\\Component\\\\Clock\\\\DatePoint\\\":3:{s:4:\\\"date\\\";s:26:\\\"2025-02-22 12:12:33.709604\\\";s:13:\\\"timezone_type\\\";i:3;s:8:\\\"timezone\\\";s:3:\\\"UTC\\\";}}}} s:45:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Envelope\\0message\\\";O:51:\\\"App\\\\Intercom\\\\Message\\\\SyncIntercomUserContactMessage\\\":1:{s:59:\\\"\\0App\\\\Intercom\\\\Message\\\\SyncIntercomUserContactMessage\\0userId\\ \";s:36:\\\"01952d92-d853-7cc6-9d9b-0d1b6ddec17d\\\";}}","2":"[]","3":"default","4":"2025-02-22 12:12:33","5":"2025-02-22 12:12:33"}, types: array{"1":2,"2":2,"3":2,"4":2,"5":2}) params={"1":"O:36:\\\"Symfony\\\\Component\\\ \Messenger\\\\Envelope\\\":2:{s:44:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Envelope\\0stamps\\\";a:2:{s:46:\\\"Symfony\\\\Component\\\\Messenger\\\\Stamp\\\\BusNameStamp\\\";a:1:{i:0;O:46:\\\"Symfony\\\\Component\\\\Messenger\\\ \Stamp\\\\BusNameStamp\\\":1:{s:55:\\\"\\0Symfony\\\\Component\\\\Messenger\\\\Stamp\\\\BusNameStamp\\0busName\\\";s:21:\\\"messenger.bus.default\\\";}}s:46:\\\"Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\\";a:1: {i:0;O:46:\\\"Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\\":2:{s:53:\\\"\\0Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\\MonitorStamp\\0runId\\\";i:174116437;s:60:\\\"\\0Zenstruck\\\\Messenger\\\\Monitor\\\\Stamp\\\ \MonitorStamp\\0dispatchedAt\\\";O:33:\\\"Symfony\\\\Component\\\\Clock\\\\DatePoint\\\":3:{s:4:\\\"date\\\";s:26:\\\"2025-02-22 12:12:33.709604\\\";s:13:\\\"timezone_type\\\";i:3;s:8:\\\"timezone\\\";s:3:\\\"UTC\\\";}}}}s:45:\\\"\ \0Symfony\\\\Component\\\\Messenger\\\\Envelope\\0message\\\";O:51:\\\"App\\\\Intercom\\\\Message\\\\SyncIntercomUserContactMessage\\\":1:{s:59:\\\"\\0App\\\\Intercom\\\\Message\\\\SyncIntercomUserContactMessage\\0userId\\\";s:36:\\ \"01952d92-d853-7cc6-9d9b-0d1b6ddec17d\\\";}}","2":"[]","3":"default","4":"2025-02-22 12:12:33","5":"2025-02-22 12:12:33"} sql="INSERT INTO messenger_messages (body, headers, queue_name, created_at, available_at) VALUES(?, ?, ?, ?, ?) RETURNING id" types={"1":2,"2":2,"3":2,"4":2,"5":2} [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: RELEASE SAVEPOINT DOCTRINE_2 sql="RELEASE SAVEPOINT DOCTRINE_2" [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Committing transaction [Application] Feb 22 12:12:33 |DEBUG | DOCTRI Executing statement: UNLISTEN "messenger_messages" sql="UNLISTEN \"messenger_messages\"" [Application] Feb 22 12:12:33 |INFO | DOCTRI Disconnecting
  41. Message framework: messenger: # ... transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' options:

    queue_name: default async_priority_high: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' options: queue_name: high async_priority_low: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' options: queue_name: low
  42. Message #[AsMessage('async_priority_high')] final readonly class SendWelcomeEmailMessage { // ... }

    #[AsMessage('async_priority_low')] final readonly class SyncIntercomUserContactMessage { // ... }
  43. Message ❯ symfony console app:register-many-users --limit 91 ❯ symfony console

    messenger:stats --------------------- ------- Transport Count --------------------- ------- async 0 async_priority_high 91 async_priority_low 91 --------------------- -------
  44. Message ❯ symfony console messenger:consume async_priority_high async async_priority_low --limit 3

    --time-limit 60 --memory-limit 128M -vv [OK] Consuming messages from transports "async, async_priority_high, async_priority_low". // The worker will automatically exit once it has processed 100 messages, exceeded 128M of memory, been running for 60s // or received a stop signal via the messenger:stop-workers command. // Quit the worker with CONTROL-C. 12:45:26 INFO [messenger] Received message App\User\Message\SendWelcomeEmailMessage ["class" => "App\User\Message\SendWelcomeEmailMessage"] 12:45:26 INFO [messenger] Message Symfony\Component\Mailer\Messenger\SendEmailMessage handled by Symfony\Component\Mailer\Messenger\MessageHandler::__invoke ["class" => "Symfony\Component\Mailer\Messenger\SendEmailMessage","handler" => "Symfony\Component\Mailer\Messenger\MessageHandler::__invoke"] 12:45:26 INFO [messenger] Message App\User\Message\SendWelcomeEmailMessage handled by App\User\MessageHandler\SendWelcomeEmailMessageHandler::__invoke ["class" => "App\User\Message\SendWelcomeEmailMessage","handler" => "App\User\MessageHandler\SendWelcomeEmailMessageHandler::__invoke"] 12:45:26 INFO [messenger] App\User\Message\SendWelcomeEmailMessage was handled successfully (acknowledging to transport). ["class" => "App\User\Message\SendWelcomeEmailMessage","message_id" => 43] 12:45:26 INFO [messenger] Received message App\User\Message\SendWelcomeEmailMessage ["class" => "App\User\Message\SendWelcomeEmailMessage"] 12:45:26 INFO [messenger] Message Symfony\Component\Mailer\Messenger\SendEmailMessage handled by Symfony\Component\Mailer\Messenger\MessageHandler::__invoke ["class" => "Symfony\Component\Mailer\Messenger\SendEmailMessage","handler" => "Symfony\Component\Mailer\Messenger\MessageHandler::__invoke"] 12:45:26 INFO [messenger] Message App\User\Message\SendWelcomeEmailMessage handled by App\User\MessageHandler\SendWelcomeEmailMessageHandler::__invoke ["class" => "App\User\Message\SendWelcomeEmailMessage","handler" => "App\User\MessageHandler\SendWelcomeEmailMessageHandler::__invoke"] 12:45:26 INFO [messenger] App\User\Message\SendWelcomeEmailMessage was handled successfully (acknowledging to transport). ["class" => "App\User\Message\SendWelcomeEmailMessage","message_id" => 45] 12:45:26 INFO [messenger] Received message App\User\Message\SendWelcomeEmailMessage ["class" => "App\User\Message\SendWelcomeEmailMessage"] 12:45:26 INFO [messenger] Message Symfony\Component\Mailer\Messenger\SendEmailMessage handled by Symfony\Component\Mailer\Messenger\MessageHandler::__invoke ["class" => "Symfony\Component\Mailer\Messenger\SendEmailMessage","handler" => "Symfony\Component\Mailer\Messenger\MessageHandler::__invoke"] 12:45:26 INFO [messenger] Message App\User\Message\SendWelcomeEmailMessage handled by App\User\MessageHandler\SendWelcomeEmailMessageHandler::__invoke ["class" => "App\User\Message\SendWelcomeEmailMessage","handler" => "App\User\MessageHandler\SendWelcomeEmailMessageHandler::__invoke"] 12:45:26 INFO [messenger] App\User\Message\SendWelcomeEmailMessage was handled successfully (acknowledging to transport). ["class" => "App\User\Message\SendWelcomeEmailMessage","message_id" => 47] ...
  45. Message ❯ symfony console messenger:stats --------------------- ------- Transport Count ---------------------

    ------- async 0 async_priority_high 88 async_priority_low 91 --------------------- -------
  46. Message interface UserRepository { /** * @throws DomainException When user

    is not found */ public function byId(Uuid $id): User; // ... }
  47. Message #[AsMessageHandler] final class SyncIntercomUserContactMessageHandler { public function __construct( private

    readonly UserRepository $userRepository, private readonly IntercomClient $intercomClient, ) { } public function __invoke(SyncIntercomUserContactMessage $message): void { // What if repository method cannot find the user? $user = $this->userRepository->byId($message->getUserId()); // ... } }
  48. Message use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; #[AsMessageHandler] final class SyncIntercomUserContactMessageHandler { // ..

    public function __invoke(SyncIntercomUserContactMessage $message): void { try { $user = $this->userRepository->byId($message->getUserId()); } catch (\DomainException $e) { throw new UnrecoverableMessageHandlingException($e->getMessage(), previous: $e); } // ... } }
  49. Message #[AsMessageHandler] final class SyncIntercomUserContactMessageHandler { // ... public function

    __invoke(SyncIntercomUserContactMessage $message): void { // ... // What if sending the contact details to Intercom raises an exception? $contact = $this->intercomClient->createUser($user); $user->setIntercomId($contact->id); } }
  50. Message 16:57:16 INFO [messenger] Received message App\Intercom\Message\SyncIntercomUserContactMessage (...) 16:57:16 WARNING

    [messenger] Error thrown while handling message ... Sending for retry #1 using 962 ms delay. 16:57:16 INFO [messenger] Received message App\Intercom\Message\SyncIntercomUserContactMessage (...) 16:57:16 WARNING [messenger] Error thrown while handling message ... Sending for retry #2 using 2021 ms delay. ... 16:58:17 INFO [messenger] Received message App\Intercom\Message\SyncIntercomUserContactMessage (...) 16:58:17 WARNING [messenger] Error thrown while handling message ... Sending for retry #3 using 4081 ms delay. ... 16:58:17 INFO [messenger] Stopping worker. 16:58:17 INFO [messenger] Worker stopped due to time limit of 60s exceeded ... 17:07:08 INFO [messenger] Received message App\Intercom\Message\SyncIntercomUserContactMessage (...) 17:07:08 CRITICAL [messenger] Error thrown while handling message ... Removing from transport after 3 retries.
  51. Message framework: messenger: transports: # ... async_priority_low: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' options:

    queue_name: low # default configuration retry_strategy: max_retries: 3 # milliseconds delay delay: 1000 # causes the delay to be higher before each retry # e.g. 1 second delay, 2 seconds, 4 seconds multiplier: 2
  52. Message #[AsMessageHandler] final class SyncIntercomUserContactMessageHandler { // ... public function

    __invoke(SyncIntercomUserContactMessage $message): void { // ... try { $contact = $this->intercomClient->createUser($user); } catch (\Psr\Http\Client\Common\Exception\ClientExceptionInterface $e) { // In case of a network outage, or when HTTP request failed, // then always force retrying with a delay. throw new RecoverableMessageHandlingException( message: $e->getMessage(), previous: $e, retryDelay: 6000, ); } $user->setIntercomId($contact->id); } }
  53. Message 17:40:38 INFO [messenger] Received message App\Intercom\Message\SyncIntercomUserContactMessage (...) 17:40:38 WARNING

    [messenger] Error thrown while handling message ... Sending for retry #1 using 962 ms delay. 17:40:38 INFO [messenger] Received message App\Intercom\Message\SyncIntercomUserContactMessage (...) 17:40:38 WARNING [messenger] Error thrown while handling message ... Sending for retry #2 using 2021 ms delay. ... 17:41:38 INFO [messenger] Received message App\Intercom\Message\SyncIntercomUserContactMessage (...) 17:41:38 WARNING [messenger] Error thrown while handling message ... Sending for retry #3 using 4081 ms delay. ... 17:42:19 INFO [messenger] Stopping worker. 17:42:19 INFO [messenger] Worker stopped due to time limit of 60s exceeded ... 17:42:24 INFO [messenger] Received message App\Intercom\Message\SyncIntercomUserContactMessage (…) 17:42:24 CRITICAL [messenger] Error thrown while handling message ... Removing from transport after 3 retries. 17:42:24 INFO [messenger] Rejected message App\Intercom\Message\SyncIntercomUserContactMessage will be sent to the failure transport
  54. Message ❯ symfony console messenger:failed:show There is 1 message pending

    in the failure transport. ----- ----------------------------------------------------- --------------------- ------------------ Id Class Failed at Error ----- ----------------------------------------------------- --------------------- ------------------ 240 App\Intercom\Message\SyncIntercomUserContactMessage 2025-02-22 17:42:24 Failed to ... ----- ----------------------------------------------------- --------------------- ------------------ // Run messenger:failed:show {id} --transport=failed -vv to see message details.
  55. Message ❯ symfony console messenger:failed:show 240 --transport=failed -vv There is

    1 message pending in the failure transport. Failed Message Details ====================== ------------- ---------------------------------------------------------------------------------- Class App\Intercom\Message\SyncIntercomUserContactMessage Message Id 240 Failed at 2025-02-22 17:42:24 Error Failed to create Intercom contact for user 01952ebf-018c-72ba-ad63-c1761ea22a5a. Error Code 0 Error Class RuntimeException Transport async_priority_low ------------- ---------------------------------------------------------------------------------- Message history: * Message failed at 2025-02-22 17:40:38 and was redelivered * Message failed at 2025-02-22 17:40:38 and was redelivered * Message failed at 2025-02-22 17:41:38 and was redelivered * Message failed at 2025-02-22 17:42:24 and was redelivered Message: ======== App\Intercom\Message\SyncIntercomUserContactMessage^ { -userId: "01952ebf-018c-72ba-ad63-c1761ea22a5a" } Exception: ========== RuntimeException^ { message: "Failed to create Intercom contact for user 01952ebf-018c-72ba-ad63-c1761ea22a5a." code: 0 file: "/Users/hhamon/Code/confoo-2025-messenger/src/Intercom/IntercomProxyClient.php" line: 40 trace: (...) previous: null } Run messenger:failed:retry 240 --transport=failed to retry this message. Run messenger:failed:remove 240 --transport=failed to delete it.
  56. On user registration, how to test that… - The async_priority_high

    transport has 1 enqueued message - The async_priority_low transport has 1 enqueued message - Enqueued messages are processed successfully - User received a registration confirmation email - User has been synced successfully with Intercom
  57. Message final class RegisterUserControllerTest extends WebTestCase { public function testRegisterUser():

    void { $client = static::createClient(); $client->request('GET', ‘/registration'); $client->submitForm('Submit', [ 'user_registration[gender]' => 'male', 'user_registration[fullName]' => 'John Smith', 'user_registration[birthdate]' => '1978-02-26', 'user_registration[country]' => 'US', 'user_registration[email]' => '[email protected]', 'user_registration[password][first]' => 'jt/%Xq}EW"8`5?wmVxhN&;', 'user_registration[password][second]' => 'jt/%Xq}EW"8`5?wmVxhN&;', ]); /** @var InMemoryTransport $transport */ $transport = self::getContainer()->get('messenger.transport.async_priority_high'); $this->assertCount(1, $transport->getSent()); /** @var InMemoryTransport $transport */ $transport = self::getContainer()->get('messenger.transport.async_priority_low'); $this->assertCount(1, $transport->getSent()); // ... } }
  58. Message final class RegisterUserControllerTest extends WebTestCase { use InteractsWithMailer; use

    InteractsWithMessenger; public function testRegisterUser(): void { $this->transport('async_priority_high')->queue()->assertEmpty(); $this->transport('async_priority_low')->queue()->assertEmpty(); $client = static::createClient(); $client->request('GET', '/registration'); // ... $this->transport('async_priority_high')->queue()->assertCount(1); $this->transport('async_priority_low')->queue()->assertCount(1); $this->transport('async_priority_high')->processOrFail(); $this->transport('async_priority_low')->processOrFail(); $this->mailer() ->assertSentEmailCount(1) ->assertEmailSentTo('[email protected]', 'Please Confirm your Email'); /** @var User $user */ $user = self::getContainer() ->get(UserRepository::class) ->findOneBy(['email' => '[email protected]']); self::assertSame(md5('[email protected]'), $user->getIntercomId()); } }