Slide 1

Slide 1 text

Going Asynchronous with Symfony Messenger Feb. 26th 2025 - Confoo Conference - Montréal, Canada 🇨🇦

Slide 2

Slide 2 text

@hhamon / [email protected] Hugo HAMON Consultant PHP / Symfony @ KODERO Symfony Certi fi ed Expert Developer

Slide 3

Slide 3 text

Getting Started https://unsplash.com/@foxxmd

Slide 4

Slide 4 text

Message

Slide 5

Slide 5 text

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(), ]); } }

Slide 6

Slide 6 text

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 }

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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 { // ... } }

Slide 9

Slide 9 text

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; } }

Slide 10

Slide 10 text

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); } }

Slide 11

Slide 11 text

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); } }

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Messenger Core Concepts https://unsplash.com/@hungngng

Slide 14

Slide 14 text

“ 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.

Slide 15

Slide 15 text

# 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

Slide 16

Slide 16 text

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.

Slide 17

Slide 17 text

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); } }

Slide 18

Slide 18 text

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); } }

Slide 19

Slide 19 text

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.

Slide 20

Slide 20 text

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); } }

Slide 21

Slide 21 text

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); } }

Slide 22

Slide 22 text

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.

Slide 23

Slide 23 text

Message interface MessageBusInterface { /** * Dispatches the given message. * * @throws ExceptionInterface */ public function dispatch(object $message, array $stamps = []): Envelope; }

Slide 24

Slide 24 text

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(), ]); } }

Slide 25

Slide 25 text

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.

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

Handling Messages Asynchronously https://unsplash.com/@andriklangfield

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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)%'

Slide 32

Slide 32 text

Message #[AsMessage('async')] final readonly class SendWelcomeEmailMessage { // ... } #[AsMessage('async')] final readonly class SyncIntercomUserContactMessage { // ... }

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

$ symfony console messenger:consume --all --limit 5 --time-limit 10 --memory-limit 128M -vv

Slide 36

Slide 36 text

Debugging & Monitoring Tools https://unsplash.com/@clarke_designs_photography

Slide 37

Slide 37 text

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: { }

Slide 38

Slide 38 text

Message ❯ symfony console messenger:stats
 ----------- ------- Transport Count ----------- ------- async 2 ----------- -------

Slide 39

Slide 39 text

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 ... ----------------------------------------------------------------------------------

Slide 40

Slide 40 text

ZenstruckMessengerMonitorBundle Web UI

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

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 -------------------- -------------

Slide 43

Slide 43 text

Envelope & Stamps https://unsplash.com/@polarmermaid

Slide 44

Slide 44 text

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. ✉

Slide 45

Slide 45 text

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 ), );

Slide 46

Slide 46 text

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 { // ... }

Slide 47

Slide 47 text

Message

Slide 48

Slide 48 text

Middleware https://unsplash.com/@purzlbaum

Slide 49

Slide 49 text

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.

Slide 50

Slide 50 text

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); } }

Slide 51

Slide 51 text

Message framework: messenger: buses: messenger.bus.default: middleware: - validation - dispatch_after_current_bus - doctrine_transaction

Slide 52

Slide 52 text

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 }

Slide 53

Slide 53 text

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); } }

Slide 54

Slide 54 text

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; } }

Slide 55

Slide 55 text

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.

Slide 56

Slide 56 text

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.

Slide 57

Slide 57 text

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":"","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

Slide 58

Slide 58 text

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()], ); } }

Slide 59

Slide 59 text

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":"","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

Slide 60

Slide 60 text

Prioritized Transports https://unsplash.com/@joshduneebon

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

Message #[AsMessage('async_priority_high')] final readonly class SendWelcomeEmailMessage { // ... } #[AsMessage('async_priority_low')] final readonly class SyncIntercomUserContactMessage { // ... }

Slide 63

Slide 63 text

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 --------------------- -------

Slide 64

Slide 64 text

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] ...

Slide 65

Slide 65 text

Message ❯ symfony console messenger:stats --------------------- ------- Transport Count --------------------- ------- async 0 async_priority_high 88 async_priority_low 91 --------------------- -------

Slide 66

Slide 66 text

Failures https://unsplash.com/@shekai

Slide 67

Slide 67 text

Message interface UserRepository { /** * @throws DomainException When user is not found */ public function byId(Uuid $id): User; // ... }

Slide 68

Slide 68 text

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()); // ... } }

Slide 69

Slide 69 text

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); } // ... } }

Slide 70

Slide 70 text

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); } }

Slide 71

Slide 71 text

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.

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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); } }

Slide 74

Slide 74 text

Message framework: messenger: failure_transport: failed transports: failed: 'doctrine://default?queue_name=failed' # ...

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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.

Slide 77

Slide 77 text

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.

Slide 78

Slide 78 text

ZenstruckMessengerMonitorBundle Web UI

Slide 79

Slide 79 text

Testing Messages https://unsplash.com/@aaronburden

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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()); // ... } }

Slide 83

Slide 83 text

Message when@test: framework: messenger: transports: async: 'test://' async_priority_high: 'test://' async_priority_low: 'test://' Zenstruck/messenger-test 3rd party library

Slide 84

Slide 84 text

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()); } }

Slide 85

Slide 85 text

[email protected] Thank You! https://unsplash.com/@aaronburden