|
| 1 | +.. index:: |
| 2 | + single: Message |
| 3 | + single: Components; Message |
| 4 | + |
| 5 | +The Message Component |
| 6 | +===================== |
| 7 | + |
| 8 | + The Message component helps application to send and receive messages |
| 9 | + to/from other applications or via |
| 10 | + |
| 11 | +Concepts |
| 12 | +-------- |
| 13 | + |
| 14 | +.. image:: /_images/components/message/overview.png |
| 15 | + |
| 16 | +1. **Sender** |
| 17 | + Responsible for serializing and sending the message to _something_. This something can be a message broker or a 3rd |
| 18 | + party API for example. |
| 19 | + |
| 20 | +2. **Receiver** |
| 21 | + Responsible for deserializing and forwarding the messages to handler(s). This can be a message queue puller or an API |
| 22 | + endpoint for example. |
| 23 | + |
| 24 | +3. **Handler** |
| 25 | + Given a received message, contains the user business logic related to the message. In practice, that is just a PHP |
| 26 | + callable. |
| 27 | + |
| 28 | +Bus |
| 29 | +--- |
| 30 | + |
| 31 | +The bus is used to dispatch messages. MessageBus' behaviour is in its ordered middleware stack. When using |
| 32 | +the message bus with Symfony's FrameworkBundle, the following middlewares are configured for you: |
| 33 | + |
| 34 | +1. `LoggingMiddleware` (log the processing of your messages) |
| 35 | +2. `SendMessageMiddleware` (enable asynchronous processing) |
| 36 | +3. `HandleMessageMiddleware` (call the registered handle) |
| 37 | + |
| 38 | + use App\Message\MyMessage; |
| 39 | + |
| 40 | + $result = $this->get('message_bus')->handle(new MyMessage(/* ... */)); |
| 41 | +
|
| 42 | +Handlers |
| 43 | +-------- |
| 44 | + |
| 45 | +Once dispatched to the bus, messages will be handled by a "message handler". A message handler is a PHP callable |
| 46 | +(i.e. a function or an instance of a class) that will do the required processing for your message. It _might_ return a |
| 47 | +result. |
| 48 | + |
| 49 | + namespace App\MessageHandler; |
| 50 | + |
| 51 | + use App\Message\MyMessage; |
| 52 | + |
| 53 | + class MyMessageHandler |
| 54 | + { |
| 55 | + public function __invoke(MyMessage $message) |
| 56 | + { |
| 57 | + // Message processing... |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + |
| 62 | + <service id="App\Handler\MyMessageHandler"> |
| 63 | + <tag name="message_handler" /> |
| 64 | + </service> |
| 65 | + |
| 66 | +**Note:** If the message cannot be guessed from the handler's type-hint, use the `handles` attribute on the tag. |
| 67 | + |
| 68 | +### Asynchronous messages |
| 69 | + |
| 70 | +Using the Message Component is useful to decouple your application but it also very useful when you want to do some |
| 71 | +asychronous processing. This means that your application will produce a message to a queuing system and consume this |
| 72 | +message later in the background, using a _worker_. |
| 73 | + |
| 74 | +#### Adapters |
| 75 | + |
| 76 | +The communication with queuing system or 3rd parties is for delegated to libraries for now. You can use one of the |
| 77 | +following adapters: |
| 78 | + |
| 79 | +- [PHP Enqueue bridge](https://github.com/sroze/enqueue-bridge) to use one of their 10+ compatible queues such as |
| 80 | + RabbitMq, Amazon SQS or Google Pub/Sub. |
| 81 | + |
| 82 | +Routing |
| 83 | +------- |
| 84 | + |
| 85 | +When doing asynchronous processing, the key is to route the message to the right sender. As the routing is |
| 86 | +application-specific and not message-specific, the configuration can be made within the `framework.yaml` |
| 87 | +configuration file as well: |
| 88 | + |
| 89 | + framework: |
| 90 | + message: |
| 91 | + routing: |
| 92 | + 'My\Message\MessageAboutDoingOperationalWork': my_operations_queue_sender |
| 93 | + |
| 94 | +Such configuration would only route the `MessageAboutDoingOperationalWork` message to be asynchronous, the rest of the |
| 95 | +messages would still be directly handled. |
| 96 | + |
| 97 | +If you want to do route all the messages to a queue by default, you can use such configuration: |
| 98 | + |
| 99 | + framework: |
| 100 | + message: |
| 101 | + routing: |
| 102 | + 'My\Message\MessageAboutDoingOperationalWork': my_operations_queue_sender |
| 103 | + '*': my_default_sender |
| 104 | + |
| 105 | +Note that you can also route a message to multiple senders at the same time: |
| 106 | + |
| 107 | + framework: |
| 108 | + message: |
| 109 | + routing: |
| 110 | + 'My\Message\AnImportantMessage': [my_default_sender, my_audit_sender] |
| 111 | + |
| 112 | +Same bus received and sender |
| 113 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 114 | + |
| 115 | +To allow us to receive and send messages on the same bus and prevent a loop, the message bus is equipped with the |
| 116 | +`WrapIntoReceivedMessage` received. It will wrap the received messages into `ReceivedMessage` objects and the |
| 117 | +`SendMessageMiddleware` middleware will know it should not send these messages. |
| 118 | + |
| 119 | +Your own sender |
| 120 | +--------------- |
| 121 | + |
| 122 | +Using the `SenderInterface`, you can easily create your own message sender. Let's say you already have an |
| 123 | +`ImportantAction` message going through the message bus and handled by a handler. Now, you also want to send this |
| 124 | +message as an email. |
| 125 | + |
| 126 | +1. Create your sender |
| 127 | + |
| 128 | + namespace App\MessageSender; |
| 129 | + |
| 130 | + use Symfony\Component\Message\SenderInterface; |
| 131 | + use App\Message\ImportantAction; |
| 132 | + |
| 133 | + class ImportantActionToEmailSender implements SenderInterface |
| 134 | + { |
| 135 | + private $toEmail; |
| 136 | + private $mailer; |
| 137 | + |
| 138 | + public function __construct(\Swift_Mailer $mailer, string $toEmail) |
| 139 | + { |
| 140 | + $this->mailer = $mailer; |
| 141 | + $this->toEmail = $toEmail; |
| 142 | + } |
| 143 | + |
| 144 | + public function send($message) |
| 145 | + { |
| 146 | + if (!$message instanceof ImportantAction) { |
| 147 | + throw new \InvalidArgumentException(sprintf('Producer only supports "%s" messages.', ImportantAction::class)); |
| 148 | + } |
| 149 | + |
| 150 | + $this->mailer->send( |
| 151 | + (new \Swift_Message('Important action made')) |
| 152 | + ->setTo($this->toEmail) |
| 153 | + ->setBody( |
| 154 | + '<h1>Important action</h1><p>Made by '.$message->getUsername().'</p>', |
| 155 | + 'text/html' |
| 156 | + ) |
| 157 | + ); |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | +2. Register your sender service |
| 162 | + |
| 163 | + services: |
| 164 | + App\MessageSender\ImportantActionToEmailSender: |
| 165 | + arguments: |
| 166 | + - "@mailer" |
| 167 | + - "%to_email%" |
| 168 | + |
| 169 | + tags: |
| 170 | + - message.sender |
| 171 | + |
| 172 | +3. Route your important message to the sender |
| 173 | + |
| 174 | + framework: |
| 175 | + message: |
| 176 | + routing: |
| 177 | + 'App\Message\ImportantAction': [App\MessageSender\ImportantActionToEmailSender, ~] |
| 178 | + |
| 179 | +**Note:** this example shows you how you can at the same time send your message and directly handle it using a `null` |
| 180 | +(`~`) sender. |
| 181 | + |
| 182 | +Your own receiver |
| 183 | +----------------- |
| 184 | + |
| 185 | +A consumer is responsible of receiving messages from a source and dispatching them to the application. |
| 186 | + |
| 187 | +Let's say you already proceed some "orders" on your application using a `NewOrder` message. Now you want to integrate with |
| 188 | +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. |
| 189 | + |
| 190 | +You will read this CSV file and dispatch a `NewOrder` message. All you need to do is your custom CSV consumer and Symfony will do the rest. |
| 191 | + |
| 192 | +1. Create your receiver |
| 193 | + |
| 194 | + namespace App\MessageReceiver; |
| 195 | + |
| 196 | + use Symfony\Component\Message\ReceiverInterface; |
| 197 | + use Symfony\Component\Serializer\SerializerInterface; |
| 198 | + |
| 199 | + use App\Message\NewOrder; |
| 200 | + |
| 201 | + class NewOrdersFromCsvFile implements ReceiverInterface |
| 202 | + { |
| 203 | + private $serializer; |
| 204 | + private $filePath; |
| 205 | + |
| 206 | + public function __construct(SerializerInteface $serializer, string $filePath) |
| 207 | + { |
| 208 | + $this->serializer = $serializer; |
| 209 | + $this->filePath = $filePath; |
| 210 | + } |
| 211 | + |
| 212 | + public function receive() : \Generator |
| 213 | + { |
| 214 | + $ordersFromCsv = $this->serializer->deserialize(file_get_contents($this->filePath), 'csv'); |
| 215 | + |
| 216 | + foreach ($ordersFromCsv as $orderFromCsv) { |
| 217 | + yield new NewOrder($orderFromCsv['id'], $orderFromCsv['account_id'], $orderFromCsv['amount']); |
| 218 | + } |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | +2. Register your receiver service |
| 223 | + |
| 224 | + services: |
| 225 | + App\MessageReceiver\NewOrdersFromCsvFile: |
| 226 | + arguments: |
| 227 | + - "@serializer" |
| 228 | + - "%new_orders_csv_file_path%" |
| 229 | + |
| 230 | + tags: |
| 231 | + - message.receiver |
| 232 | + |
| 233 | +3. Use your consumer |
| 234 | + |
| 235 | + $ bin/console message:consume App\MessageReceived\NewOrdersFromCsvFile |
0 commit comments