Specialized tools tailored to a company’s needs play an important role in the business world. This is how ERP (Enterprise Resource Planning), PIM (Product Information Management), or CRM (Customer Relationship Management) systems were developed. Whether these applications come from a single vendor or not, they need to communicate with each other. At the end of the day, each of these systems has information that another system is most likely interested in. This includes a system from the eCommerce sector, of which Sylius is a very good representative.
The purpose of this blog is to delve technically into the issues related to communication between Sylius and ERP, PIM, and CRM tools. In our other post, you can find theoretical issues that describe communication needs more broadly.
Quick jump
- The division into commands and events
- Sylius as an extension focused framework
- Sending commands and events in Sylius
- Summary
The division into commands and events
Distributed systems, also called microservices architecture, communicate in two directions:
- If we delegate a task that should be performed to another system, then we send a command to the target system.
- If a certain operation has been performed and we want to give the other system a chance to react to this fact, then we send an event for which other systems can listen.
In both cases, there is a message to be created and then sent. Sylius, as a platform focused on extensibility, has very good mechanisms for this.
Sylius as an extension-focused framework
Sylius is often called the 'Symfony eCommerce Bundle.’ In addition to being a full-featured sales platform, Sylius is also a framework, which makes it the perfect basis for further extension. It implements many patterns that, at their base, implement the „Open-Closed” rule known from SOLID principles:
„You should be able to extend the behavior of a system without having to modify that system.”
The patterns implementing the above rule, which are also implemented in Sylius, are specifically Event Dispatching and State Machine. More information about these patterns is provided later in this blog.
Setting up Message Bus with Symfony Messenger
The most common (and the best) library for sending and receiving messages is Symfony Messenger. It is a component of the Symfony framework, which, like Sylius, surprises with its extensibility and, therefore, with the simplicity of working with it.
To install Symfony Messenger, we will need the Composer tool, which can be obtained by following the instructions. Once we have installed Composer, we can use it to install Symfony Messenger inside the project:
```bash
composer require symfony/messenger
```
At the time of configuration, we need to know the answer to the following question: which message broker, i.e., the system that performs the sending of messages from application to application, do we want to use? As a good example, we can use the RabbitMQ queuing system, which will serve as an example later in the article.
After successfully installing the Symfony Messenger library, we can move on to its configuration. The subject of today’s blog is the configuration for sending commands and events. The configuration of Symfony Messenger can be found in the `config/packages/messenger.yaml` file. If such a file does not exist in your project, you should create one. Below is the configuration, which will be discussed further:
```yaml
framework:
messenger:
default_bus: app.outgoing_bus
buses:
app.command_bus: ~
app.event_bus:
allow_no_handlers: true
transports:
async_outgoing: "%env(MESSENGER_TRANSPORT_DSN)%"
async_incoming: "%env(MESSENGER_TRANSPORT_DSN)%"
routing:
'App\Command\OrderHasBeenCompletedEvent': async_outgoing
'App\Event\VariantPriceHasBeenChanged: async_incoming
```
The above configuration defines two buses: `app.command_bus`, which is used to send commands, and `app.event_bus`, which is used to send events. Due to this, two transports have been defined:
- `async_outgoing`, which is used to physically communicate with RabbitMQ to send messages to the bus,
- `async_incoming`, which is used to physically communicate with RabbitMQ to receive messages from the bus.
The last part of this configuration is the definition of routing: which message to which queue system (on which bus) should go.
RabbitMQ configuration proceeds by providing a properly formatted address (DSN), which should be configured in the `.env.local` environment file. An example configuration of this value for a local (development) environment is as follows:
```bash
MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
```
Messenger defined in that manner can be used in the platform. When we want to send a message to the system, we need to inject the corresponding service into the appropriate service:
```xml
<service id="..." class="...">
<argument type="service" id="@app.command_bus" />
</service>
```
Then, using the `dispatch($message)` method, we can send the message created inside to the bus.
In addition to communicating towards the external system, we also need to configure an appropriate set of classes called handlers to handle messages coming from external systems. To configure our system to be a message handler, configure the appropriate tag in the dependency injection configuration:
```xml
<service id="..." class="App\MessageBus\InvoiceHasBeenIssuedHandler">
<!-- list of arguments... -->
<tag name="messenger.message_handler" bus="app.command_bus"/>
</service>
```
Below is a sample class of the configured handler:
```php
<?php
namespace App\MessageHandler;
use App\Message\InvoiceHasBeenIssuedEvent;
final class InvoiceHasBeenIssuedHandler
{
public function __invoke(InvoiceHasBeenIssuedEvent $event)
{
// your implementation...
}
}
```
The information above means that if the external system has sent a message of the `InvoiceHasBeenIssuedEvent` class to the `app.command_bus` bus, it will be processed by the class we have defined.
To complete the whole process, remember to run at least one process on the server, the so-called consumer, which will receive messages from the outside and then process them according to the configuration. Here is an example:
```bash
bin/console messenger:consume incoming_async -vvv
```
Sending commands and events in Sylius
As an extensibility-focused platform, Sylius implements several mechanisms that greatly facilitate the implementation of system-to-system communication.
Sylius Resource Events
One of the core elements of Sylius is configurable Resources. This is an abstraction that allows you to „plug” Doctrine entities into the mechanisms that Sylius offers. This mechanism, which should be mentioned because of the topic of the current entry, is events:
- Before persisting a resource: `sylius.<resource>.pre_create`
- After flushing already created resource: `sylius.<resource>.post_create`
- Before flushing already updated resource: `sylius.<resource>.pre_update`
- After flushing already updated resource: `sylius.<resource>.post_update`
- Before removing resource: `sylius.<resource>.pre_delete`
- After flushing resource removal: `sylius.<resource>.post_delete`
- Before creating 'create resource’ view: `sylius.<resource>.initialize_create`
- Before creating 'update resource’ view: `sylius.<resource>.initialize_update`
The above events, especially `post_create,` `post_update,` and `post_delete,` are ideal places to inject a command/event bus and then send a command or event to the information bus that another system, such as a PIM or CRM, can (or must) respond to.
Events regarding Resources have been implemented using the native Symfony Event Dispatcher. To create a service that listens for a specific event, configure the appropriate tag in the service definition:
```xml
<service id="..." class="App\EventListener\ProductHasBeenUpdatedListener">
<!-- list of arguments... -->
<tag name=""kernel.event_listener" event="sylius.product.pre_update"/>
</service>
```
For more information related to Sylius Resources events, see the official Sylius documentation. Information related to how Symfony Event Dispatcher works can be found in the official Symfony documentation.
Sylius Machine State
In some cases, especially those related to order processing, Sylius uses the State Machine pattern. Each transition between states allows you to plug in so-called callbacks. These are services that will be triggered when a particular transition between states takes place.
Sylius has several state machines in its implementation, including:
- `sylius_order_checkout` – related to the checkout process
- `sylius_order_payment` – related to changes in the payment status of an order
- `sylius_order_shipping` – related to changes in the status of shipment(s) in the order
- `sylius_product_review` – related to the product review process by customers
Particularly noteworthy are the first three positions, which give you the option to hook up the service to the store’s buying mechanism. With this option, you can inform the external system, for example, that an order has been placed and that payment has been made.
Assuming that we have 'app.notifier.order_notifier’ defined in the system, here is how to attach it to the list of callbacks of the `sylius_order_checkout` state machine:
```yaml
winzou_state_machine:
sylius_order_checkout:
callbacks:
after:
sylius_process_cart:
on: ["complete"]
do: ["@app.notifier.order_notifier", "notify"]
args: ["object"]
```
Apart from the requirement of service definition presence, there is no requirement related to its configuration. Below you can find an example of a pre-configured class:
```php
<?php
declare(strict_types=1);
namespace App\Notifier;
use App\Command\NotifyCustomerAboutCompletedOrderCommand;
use Symfony\Component\Messenger\MessageBusInterface;
use Sylius\Component\Order\Model\OrderInterface;
final class OrderNotifier
{
public function __construct(
private readonly MessageBusInterface $commandBus
) {
}
public function notify(OrderInterface $order): void
{
$command = new NotifyCustomerAboutCompletedOrderCommand(/*...*/);
$this->commandBus->dispatch($command);
}
}
```
For more information about the Sylius State Machine, see the official Sylius documentation.
Doctrine Event Listeners
If, during the implementation of the functionality, a part of the entity could not be configured as a Sylius Resource, we can still hook into the mechanisms related to creating a new copy or updating an existing one. This is related to the implementation of Doctrine, which also leaves events similar to Sylius Resource.
There is a significant difference between Sylius Resource and Doctrine Entity Events. While Sylius Resource implements events related to CRUD mechanics (Create/Read/Update/Delete), Doctrine implements events typical of database transactivity. Hence, if there is a possibility, it is recommended to use the events implemented in Sylius Resource first.
The complete list of events that the Doctrine implements can be found in the official Doctrine documentation. Below you can find an example implementation of the Doctrine listener:
```xml
<service id="..." class="App\EventListener\WishlistHasBeenChangedListener">
<tag name="doctrine.orm.entity_listener"
event="postUpdate"
entity="App\Entity\Wishlist"
method="exportWishlistInformation"
/>
</service>
```
The service configured in this way can be implemented as follows:
```php
<?php
declare(strict_types=1);
namespace App\EventListener;
use App\Command\NotifyCustomerAboutCompletedOrderCommand;
use App\Entity\WishlistInterface;
use Symfony\Component\Messenger\MessageBusInterface;
final class WishlistHasBeenChangedListener
{
public function __construct(
private readonly MessageBusInterface $commandBus,
) {
}
public function exportWishlistInformation(WishlistInterface $wishlist): void
{
$command = new NotifyCustomerAboutCompletedOrderCommand(/*...*/);
$this->commandBus->dispatch($command);
}
}
```
In relation to Doctrine events, special attention should be paid to the number of entity operations within a single HTTP transaction/request. These events, unlike Sylius Resource events, can occur multiple times within a transaction or current HTTP request.
Symfony – The Powerful Web App Development PHP Framework
Summary
Sylius, as an eCommerce platform focused on extensibility, is well suited for B2B platforms. The particular value of extensibility can be found where the eCommerce platform is one of many tools used in an organization. The design patterns and mechanics implemented in Sylius, such as Sylius Resource and Sylius State Machine, combined with the Symfony Messenger component, provide an excellent and powerful tool – especially useful for microservices communication.
If you have any questions about Sylius or Symfony, contact us.