diff --git a/_images/components/messenger/overview.png b/_images/components/messenger/overview.png new file mode 100644 index 00000000000..074255b4667 Binary files /dev/null and b/_images/components/messenger/overview.png differ diff --git a/components/messenger.rst b/components/messenger.rst new file mode 100644 index 00000000000..31f73bf8c37 --- /dev/null +++ b/components/messenger.rst @@ -0,0 +1,192 @@ +.. index:: + single: Messenger + single: Components; Messenger + +The Messenger Component +======================= + + The Messenger component helps applications send and receive messages to/from other applications or via message queues. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/messenger + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc + +Concepts +-------- + +.. image:: /_images/components/messenger/overview.png + +**Sender**: + Responsible for serializing and sending messages to _something_. This + something can be a message broker or a third party API for example. + +**Receiver**: + Responsible for deserializing and forwarding messages to handler(s). This + can be a message queue puller or an API endpoint for example. + +**Handler**: + Responsible for handling messages using the business logic applicable to the messages. + +Bus +--- + +The bus is used to dispatch messages. The behaviour of the bus is in its ordered +middleware stack. The component comes with a set of middleware that you can use. + +When using the message bus with Symfony's FrameworkBundle, the following middleware +are configured for you: + +#. ``LoggingMiddleware`` (logs the processing of your messages) +#. ``SendMessageMiddleware`` (enables asynchronous processing) +#. ``HandleMessageMiddleware`` (calls the registered handle) + +Example:: + + use App\Message\MyMessage; + use Symfony\Component\Messenger\MessageBus; + use Symfony\Component\Messenger\HandlerLocator; + use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; + + $bus = new MessageBus([ + new HandleMessageMiddleware(new HandlerLocator([ + MyMessage::class => $handler, + ])), + ]); + + $result = $bus->handle(new MyMessage(/* ... */)); + +.. note: + + Every middleware needs to implement the ``MiddlewareInterface`` interface. + +Handlers +-------- + +Once dispatched to the bus, messages will be handled by a "message handler". A +message handler is a PHP callable (i.e. a function or an instance of a class) +that will do the required processing for your message:: + + namespace App\MessageHandler; + + use App\Message\MyMessage; + + class MyMessageHandler + { + public function __invoke(MyMessage $message) + { + // Message processing... + } + } + +Adapters +-------- + +In order to send and receive messages, you will have to configure an adapter. An +adapter will be responsible of communicating with your message broker or 3rd parties. + +Your own sender +~~~~~~~~~~~~~~~ + +Using the ``SenderInterface``, you can easily create your own message sender. +Let's say you already have an ``ImportantAction`` message going through the +message bus and handled by a handler. Now, you also want to send this message as +an email. + +First, create your sender:: + + namespace App\MessageSender; + + use App\Message\ImportantAction; + use Symfony\Component\Message\SenderInterface; + + class ImportantActionToEmailSender implements SenderInterface + { + private $toEmail; + private $mailer; + + public function __construct(\Swift_Mailer $mailer, string $toEmail) + { + $this->mailer = $mailer; + $this->toEmail = $toEmail; + } + + public function send($message) + { + if (!$message instanceof ImportantAction) { + throw new \InvalidArgumentException(sprintf('Producer only supports "%s" messages.', ImportantAction::class)); + } + + $this->mailer->send( + (new \Swift_Message('Important action made')) + ->setTo($this->toEmail) + ->setBody( + '

Important action

Made by '.$message->getUsername().'

', + 'text/html' + ) + ); + } + } + +Your own receiver +~~~~~~~~~~~~~~~~~ + +A receiver is responsible for receiving messages from a source and dispatching +them to the application. + +Let's say you already processed some "orders" in your application using a +``NewOrder`` message. Now you want to integrate with a 3rd party or a legacy +application but you can't use an API and need to use a shared CSV file with new +orders. + +You will read this CSV file and dispatch a ``NewOrder`` message. All you need to +do is to write your custom CSV receiver and Symfony will do the rest. + +First, create your receiver:: + + namespace App\MessageReceiver; + + use App\Message\NewOrder; + use Symfony\Component\Message\ReceiverInterface; + use Symfony\Component\Serializer\SerializerInterface; + + class NewOrdersFromCsvFile implements ReceiverInterface + { + private $serializer; + private $filePath; + + public function __construct(SerializerInteface $serializer, string $filePath) + { + $this->serializer = $serializer; + $this->filePath = $filePath; + } + + public function receive(callable $handler) : void + { + $ordersFromCsv = $this->serializer->deserialize(file_get_contents($this->filePath), 'csv'); + + foreach ($ordersFromCsv as $orderFromCsv) { + $handler(new NewOrder($orderFromCsv['id'], $orderFromCsv['account_id'], $orderFromCsv['amount'])); + } + } + + public function stop(): void + { + // noop + } + } + +Receiver and Sender on the same bus +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To allow us to receive and send messages on the same bus and prevent an infinite +loop, the message bus is equipped with the ``WrapIntoReceivedMessage`` middleware. +It will wrap the received messages into ``ReceivedMessage`` objects and the +``SendMessageMiddleware`` middleware will know it should not route these +messages again to an adapter. diff --git a/index.rst b/index.rst index b9e4a8d316b..ab5212eff54 100644 --- a/index.rst +++ b/index.rst @@ -41,6 +41,7 @@ Topics frontend http_cache logging + messenger performance profiler routing diff --git a/messenger.rst b/messenger.rst new file mode 100644 index 00000000000..0dbd7c15e9f --- /dev/null +++ b/messenger.rst @@ -0,0 +1,233 @@ +.. index:: + single: Messenger + +How to Use the Messenger +======================== + +Symfony's Messenger provide a message bus and some routing capabilities to send +messages within your application and through adapaters such as message queues. +Before using it, read the :doc:`Messenger component docs ` +to get familiar with its concepts. + +Installation +------------ + +In applications using :doc:`Symfony Flex `, run this command to +install messenger before using it: + +.. code-block:: terminal + + $ composer require messenger + +Using the Messenger Service +--------------------------- + +Once enabled, the ``message_bus`` service can be injected in any service where +you need it, like in a controller:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use App\Message\SendNotification; + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Component\Messenger\MessageBusInterface; + + class DefaultController extends Controller + { + public function index(MessageBusInterface $bus) + { + $bus->dispatch(new SendNotification('A string to be sent...')); + } + } + +Registering Handlers +-------------------- + +In order to do something when your message is dispatched, you need to create a +message handler. It's a class with an `__invoke` method:: + + // src/MessageHandler/MyMessageHandler.php + namespace App\MessageHandler; + + class MyMessageHandler + { + public function __invoke(MyMessage $message) + { + // do something with it. + } + } + +Once you've created your handler, you need to register it: + +.. code-block:: xml + + + + + +.. note:: + + If the message cannot be guessed from the handler's type-hint, use the + ``handles`` attribute on the tag. + +Adapters +-------- + +The communication with queuing system or third parties is delegated to +libraries for now. The built-in AMQP adapter allows you to communicate with +most of the AMQP brokers such as RabbitMQ. + +.. note:: + + If you need more message brokers, you should have a look to `Enqueue's adapter`_ + which supports things like Kafka, Amazon SQS or Google Pub/Sub. + +An adapter is registered using a "DSN", which is a string that represents the +connection credentials and configuration. By default, when you've installed +the messenger component, the following configuration should have been created: + +.. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + adapters: + amqp: "%env(MESSENGER_DSN)%" + +.. code-block:: bash + + # .env + ###> symfony/messenger ### + MESSENGER_DSN=amqp://guest:guest@localhost:5672/%2f/messages + ###< symfony/messenger ### + +This is enough to allow you to route your message to the ``amqp``. This will also +configure the following services for you: + +1. A ``messenger.sender.amqp`` sender to be used when routing messages. +2. A ``messenger.receiver.amqp`` receiver to be used when consuming messages. + +Routing +------- + +Instead of calling a handler, you have the option to route your message(s) to a +sender. Part of an adapter, it is responsible for sending your message somewhere. +You can configure which message is routed to which sender with the following +configuration: + +.. code-block:: yaml + + framework: + messenger: + routing: + 'My\Message\Message': amqp # The name of the defined adapter + +Such configuration would only route the ``My\Message\Message`` message to be +asynchronous, the rest of the messages would still be directly handled. + +You can route all classes of message to a sender using an asterisk instead of a class name: + +.. code-block:: yaml + + framework: + messenger: + routing: + 'My\Message\MessageAboutDoingOperationalWork': another_adapter + '*': amqp + +A class of message can also be routed to multiple senders by specifying a list: + +.. code-block:: yaml + + framework: + messenger: + routing: + 'My\Message\ToBeSentToTwoSenders': [amqp, audit] + +By specifying a ``null`` sender, you can also route a class of messages to a sender +while still having them passed to their respective handler: + +.. code-block:: yaml + + framework: + messenger: + routing: + 'My\Message\ThatIsGoingToBeSentAndHandledLocally': [amqp, ~] + +Consuming messages +------------------ + +Once your messages have been routed, you will like to consume your messages in most +of the cases. Do to so, you can use the ``messenger:consume-messages`` command +like this: + +.. code-block:: terminal + + $ bin/console messenger:consume-messages amqp + +The first argument is the receiver's service name. It might have been created by +your ``adapters`` configuration or it can be your own receiver. + +Your own Adapters +----------------- + +Once you have written your adapter's sender and receiver, you can register your +adapter factory to be able to use it via a DSN in the Symfony application. + +Create your adapter Factory +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You need to give FrameworkBundle the opportunity to create your adapter from a +DSN. You will need an adapter factory:: + + use Symfony\Component\Messenger\Adapter\Factory\AdapterFactoryInterface; + use Symfony\Component\Messenger\Transport\ReceiverInterface; + use Symfony\Component\Messenger\Transport\SenderInterface; + + class YourAdapterFactory implements AdapterFactoryInterface + { + public function createReceiver(string $dsn, array $options): ReceiverInterface + { + return new YourReceiver(/* ... */); + } + + public function createSender(string $dsn, array $options): SenderInterface + { + return new YourSender(/* ... */); + } + + public function supports(string $dsn, array $options): bool + { + return 0 === strpos($dsn, 'my-adapter://'); + } + } + +Register your factory +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: xml + + + + + +Use your adapter +~~~~~~~~~~~~~~~~ + +Within the ``framework.messenger.adapters.*`` configuration, create your +named adapter using your own DSN: + +.. code-block:: yaml + + framework: + messenger: + adapters: + yours: 'my-adapter://...' + +In addition of being able to route your messages to the ``yours`` sender, this +will give you access to the following services: + +1. ``messenger.sender.hours``: the sender. +2. ``messenger.receiver.hours``: the receiver. + +.. _`enqueue's adapter`: https://github.com/sroze/enqueue-bridge