symfony/messenger
Symfony Messenger helps PHP apps send and handle messages asynchronously via queues or between services. It provides message buses, handlers, transports, retries, and failure handling to build reliable background jobs and event-driven workflows.
Installation (updated for v8.1.0-BETA3):
composer require symfony/messenger:^8.1.0-BETA3
For Laravel, use symfony/messenger alongside symfony/amqp-messenger (RabbitMQ) or symfony/redis-messenger (Redis) as needed.
Define a Message Class (unchanged):
namespace App\Messages;
class SendEmailMessage
{
public function __construct(public string $email, public string $subject, public string $body) {}
}
Create a Message Handler (updated for retry strategy):
namespace App\MessageHandlers;
use App\Messages\SendEmailMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException;
#[AsMessageHandler]
class SendEmailHandler
{
public function __invoke(SendEmailMessage $message)
{
try {
// Logic to send email
} catch (\Exception $e) {
throw new RecoverableMessageHandlingException($e->getMessage(), $e->getCode(), $e);
}
}
}
Dispatch a Message (unchanged):
use Symfony\Component\Messenger\MessageBusInterface;
public function __construct(private MessageBusInterface $bus) {}
public function sendWelcomeEmail(string $email)
{
$this->bus->dispatch(new SendEmailMessage(
$email,
'Welcome!',
'Thanks for signing up!'
));
}
Configure a Transport (updated for signing):
return [
'transports' => [
'async' => [
'dsn' => 'sync://',
'options' => [
'signing_key' => '%env(MESSENGER_SIGNING_KEY)%',
],
],
'failed' => [
'dsn' => 'doctrine://default?queue_name=failed',
],
],
'routing' => [
SendEmailMessage::class => 'async',
],
'serializer' => [
'default' => 'messenger.transport.serializer',
'default_options' => [
'sign_messages' => true,
],
],
];
Run the Worker (unchanged):
php artisan messenger:consume async -vv
// In UserController
public function register(Request $request)
{
$user = User::create($request->validated());
$this->bus->dispatch(new SendEmailMessage(
$user->email,
'Welcome!',
'Thanks for signing up!'
));
return response()->json(['message' => 'User created!']);
}
// In SendEmailHandler (updated)
public function __invoke(SendEmailMessage $message)
{
try {
Mail::to($message->email)
->send(new WelcomeEmail($message->subject, $message->body));
} catch (MailerException $e) {
throw new RecoverableMessageHandlingException(
'Failed to send email: ' . $e->getMessage(),
0,
$e
);
}
}
sign_messages: true in serializer options and provide a signing key.
'serializer' => [
'default_options' => [
'sign_messages' => true,
],
],
Handlers (updated):
Use RecoverableMessageHandlingException for failures that should trigger retries (e.g., transient network issues). Non-recoverable failures should throw generic exceptions.
throw new RecoverableMessageHandlingException('Transient error', 0, $e);
Middleware (unchanged)
Batch Processing (unchanged)
Transport Config (updated): Configure signing for transports requiring message integrity checks.
'async' => [
'dsn' => 'amqp://guest:guest@localhost:5672/%2f/messages',
'options' => [
'signing_key' => '%env(MESSENGER_SIGNING_KEY)%',
],
],
Retry Logic (updated):
Retry strategies now respect RecoverableMessageHandlingException. Configure retry limits in retry_strategy:
'retry_strategy' => [
'max_retries' => 3,
'delay' => 1000,
'multiplier' => 2,
'max_delay' => 0,
],
Failure Transport (unchanged)
Signed Messages:
Verify message signing in tests by inspecting the SigningStamp:
$message->last(Symfony\Component\Messenger\Stamp\SigningStamp::class)->signature;
Retry Behavior:
Mock RecoverableMessageHandlingException to test retry logic:
$this->expectException(RecoverableMessageHandlingException::class);
Serializable Instances (new):
Ensure PhpSerializer correctly handles Serializable instances by testing with custom serializable message classes:
class SerializableMessage implements Serializable
{
public function serialize(): string {}
public function unserialize(string $serialized): void {}
}
Message Serialization (updated):
Ensure sign_messages is set to true in serializer options if using signed transports. Without signing, messages may be rejected by strict transports.
'serializer' => [
'default_options' => [
'sign_messages' => true,
],
],
Transport Locks (unchanged)
Failed Messages (unchanged)
Worker Timeouts (unchanged)
Retry Strategy Misuse (unchanged):
Avoid throwing RecoverableMessageHandlingException for non-retryable failures (e.g., invalid data).
Serializable Instances (new):
Ensure message classes implementing Serializable are correctly handled by PhpSerializer. Test with custom serializable classes to avoid serialization errors.
Logging (updated): Enable debug logging for signing issues:
'logging' => [
'enabled' => true,
'level' => 'debug',
],
Message Inspection (updated): Inspect signed messages for integrity:
php artisan messenger:failed:list --format=json | grep "signature"
Transport-Specific Issues (updated):
sign_messages option is consistent across all transports to avoid serialization mismatches.PhpSerializer Fixes (new):
If using PhpSerializer with Serializable instances, verify the payload structure after deserialization. The fix in #64261 ensures proper handling of such cases.
Signing Security (unchanged):
SigningSerializer verifies signatures before decoding messages.
Retry Strategy Respect (unchanged):
RecoverableMessageHandlingException properly triggers retry logic.
Exception Normalization (unchanged): Trace arguments are no longer included in flattened exceptions.
Configuration Clarity (unchanged):
Explicitly document the need for sign_messages: true in serializer options.
PhpSerializer Fix (new):
Fixes PhpSerializer::getMessageType() when handling payloads with Serializable instances (#64261).
General Hardening (new): Various fixes and hardenings to improve stability and security (#64237). Ensure your application is updated to avoid potential edge-case issues.
How can I help you explore Laravel packages today?