Share
Embed
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
Transcript
85 slides
-
Going Asynchronous with Symfony Messenger Feb. 26th 2025 - Confoo
Conference - Montréal, Canada 🇨🇦 -
@hhamon / [email protected] Hugo HAMON Consultant PHP / Symfony @
KODERO Symfony Certi fi ed Expert Developer -
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(), ]); } } -
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 } -
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 -
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 { // ... } } -
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; } } -
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); } } -
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); } } -
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 -
“ 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. -
# 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 -
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. -
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); } } -
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); } } -
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. -
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); } } -
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); } } -
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. -
Message interface MessageBusInterface { /** * Dispatches the given message.
* * @throws ExceptionInterface */ public function dispatch(object $message, array $stamps = []): Envelope; } -
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(), ]); } } -
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. -
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 -
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)%' -
Message #[AsMessage('async')] final readonly class SendWelcomeEmailMessage { // ... }
#[AsMessage('async')] final readonly class SyncIntercomUserContactMessage { // ... } -
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: { } -
Message ❯ symfony console messenger:stats ----------- ------- Transport Count -----------
------- async 2 ----------- ------- -
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 ... ---------------------------------------------------------------------------------- -
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 -------------------- ------------- -
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. ✉ -
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 ), ); -
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 { // ... } -
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. -
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); } } -
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 } -
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); } } -
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; } } -
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. -
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. -
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 -
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()], ); } } -
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 -
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 -
Message #[AsMessage('async_priority_high')] final readonly class SendWelcomeEmailMessage { // ... }
#[AsMessage('async_priority_low')] final readonly class SyncIntercomUserContactMessage { // ... } -
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 --------------------- ------- -
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] ... -
Message ❯ symfony console messenger:stats --------------------- ------- Transport Count ---------------------
------- async 0 async_priority_high 88 async_priority_low 91 --------------------- ------- -
Message interface UserRepository { /** * @throws DomainException When user
is not found */ public function byId(Uuid $id): User; // ... } -
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()); // ... } } -
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); } // ... } } -
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); } } -
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. -
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 -
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); } } -
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 -
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. -
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. -
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 -
Message when@test: framework: messenger: transports: async: 'in-memory://' async_priority_high: 'in-memory://' async_priority_low:
'in-memory://' Symfony built-in in-memory transport -
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()); // ... } } -
Message when@test: framework: messenger: transports: async: 'test://' async_priority_high: 'test://' async_priority_low:
'test://' Zenstruck/messenger-test 3rd party library -
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()); } }