Chapter VI field events
Software events are things of interest to other components of the system. PHP programmers generally do not use events in their work, because this is not a feature of the language. However, it is now more common for new frameworks and libraries to adopt them to provide a new way to decouple, reuse, and speed up code.
Domain events are related events when domain changes. Domain events are events in the domain, which are concerned by domain experts.
In domain driven design, domain events are basic building blocks that can:
- Communicating with other boundary contexts
- Improve performance and scalability for ultimate consistency
- As a historical archive
The essence of domain events is asynchronous communication. For more information on this topic, we recommend Gregor Hohpe and Bobby Woolf, "enterprise integration patterns: designing, building, and deploying messaging solutions.".
brief introduction
Suppose there is a Javascript 2D platform game with a large number of different component interactions on the screen at the same time. One component represents life remaining, the other shows all scores, and the other shows the time remaining to complete the current level. Every time your character jumps on an enemy, the score increases. When your score is higher than a score, you get an extra life. When your character picks up a key, a door usually opens. But how do all these components interact with each other? What is the best framework for this scenario?
There are two options: the first is to combine each component with the components it connects to. However, in the above example, it means that there are a large number of components coupled together, and each additional function addition requires developers to modify the code. But do you remember the open close principle (OCP)? Adding a new component should not make it necessary to update the first component, which will have too much work to maintain.
Second, a better way is to connect all the components to a single object that handles all the important events in the game. It receives events from each component and forwards them to a specific component. For example, the scoring component might be interested in an EnemyKilled event, while the lifecapped event is quite useful for player entities and remaining life array pieces. Typically, all components are coupled with a single component that manages all notifications. Using this method, adding or removing components does not affect existing components.
When developing a single application, events are very useful for decoupled components. When developing an entire domain in a distributed way, events are useful for decoupling each service or application that plays a role in the domain. The key points are the same, but the scale is different.
Definition
Domain event is a special type of event, which is used to inform local or remote domain context of changes.
Vaughn Vernon defines domain events as:
What happened in the field.
Eric Evans defines domain events as:
Domain event is a complete part of the domain model, and it is the representation of the events in the domain. Field activities that are not suddenly touched, and events that domain experts want to track, or are notified, or are related to changes in the state of objects in other fields.
Martin Flower defines domain events as:
A class of records that capture interest in the area of influence.
Examples of domain events in web applications include user registration, ordering, user transfer and product addition.
Little story
In a ticket agency, the operations manager decided to raise the price of U2 shows. She went backstage to edit the show. A ShowPriceChanged domain event is published, and new program prices are persisted to the database in the same transaction.
A batch process takes the domain event and posts it to the RabbitMQ queue. Domain events are formed into two queues: one is the same local bound context, and the other is used for business intelligence purposes.
In the first queue, a worker process retrieves the corresponding program through the ID in the event and pushes it to the elastic search server, so that users can see the latest price when searching.
In the second queue, another process inserts information into a log server or data pool, where you can run a report or data mining process.
An external application that cannot integrate domain events into the system can access all ShowPriceChanged events through the REST API provided by the local bound context.
As you can see, domain events are very useful for handling final consistency and integrating different boundary contexts. Aggregations create and publish events. Subscribers can store events and then forward them to other remote subscribers.
metaphor
On Tuesday, we went to Babu hotel for dinner and paid by credit card. This can be modeled as an event of type "order", subject "my credit card" and occurrence time "Tuesday". If Babu rice uses the old manual system and does not transmit the transaction until Friday, the transaction will take effect on Friday.
That's how it happened. Not everything makes sense, some are worth noting but don't react. However, it is usually the most interesting things that react. Many need to respond to events of interest. Most of the time you need to know why a system responds like this.
By transferring system input to the domain event stream, you can record all system input. This helps you organize your processing logic and also allows you to keep an audit log of system input.
Practice
Try to locate potential domain events in your current domain
Real case
Before going into the details of domain events, let's take a look at a real domain event instance and how they can help our application and the whole domain.
Let's consider a simple Application Service, new user registration, such as an e-commerce context. Application Service is covered in other chapters, so you don't have to worry too much on the surface. Instead, just focus on the execution method:
class SignUpUserService implements ApplicationService { private $userRepository; private $userFactory; private $userTransformer; public function __construct( UserRepository $userRepository, UserFactory $userFactory, UserTransformer $userTransformer ) { $this->userRepository = $userRepository; $this->userFactory = $userFactory; $this->userTransformer = $userTransformer; } /** * @param SignUpUserRequest $request * @return User * @throws UserAlreadyExistsException */ public function execute(SignUpUserRequest $request) { $email = $request->email(); $password = $request->password(); $user = $this->userRepository->userOfEmail($email); if ($user) { throw new UserAlreadyExistsException(); } $user = $this->userFactory->build( $this->userRepository->nextIdentity(), $email, $password ); $this->userRepository->add($user); $this->userTransformer->write($user); } }
As shown above, the Application Service section checks if the user exists. If not, a new user is created and added to the user repository.
Now consider an additional requirement: a new user needs to register with an email alert. You don't need to think too much. The first way we think about is to update the Application Service and add a piece of code that can complete this work. It may be EmailSender code that runs after adding the method. Now, though, let's consider another approach.
How about triggering a UserRegistered event and sending an email after another component listens? There are some cool benefits to this new approach. First, when new users register, we don't need to update the Application Service code every time. Second, it's easier to test. Application services are also simpler. Every time we have new action development, we only need to write test cases for this action.
Later, in the same e-commerce project, we were told to integrate an open-source game platform written in non PHP. Every time users place an order or browse a product in our e-commerce context, they can see the obtained badge on their e-commerce users' home page or be notified by email. How can we model this problem?
According to the first method, we will update the application service with the method of confirming e-mail before and integrate it into the new platform. Using the domain event method, we can create another listener event for UserRegistered, which can be connected to the game platform in the way of REST or SOA. Even better, it can put events on a message queue like RabbitMQ so that the game bounds context can subscribe and automatically receive notifications. We don't need to understand the game context at all.
Features
Domain events are usually immutable because they are a record of something in the past. In addition to the description of the event, a domain event usually contains a time stamp of the event occurrence time and the entity identifier involved in the event. In addition, a domain event usually has a separate timestamp to indicate when the event enters the system and the identity of the person who entered the event. The identity of the domain event itself can be based on these property sets. For example, if two instances of the same event reach a node, they can be considered the same.
The essence of domain event is that you can use it to capture the things that can trigger changes in the application, or the changes you are interested in in in other applications in the domain. These subsequently processed event objects will cause system changes and be stored in the audit system.
Naming convention
All events must be represented by past tense verbs because they all happened in the past. For example, CustomerRelocated, CargoShipped, or InventoryLossageRecorded. There are some interesting examples in English. People may tend to use nouns instead of past tense verbs. For example, for a national conference interested in natural disasters, "earthquake" or "collapse" is a related event. We suggest that we try to avoid the temptation of using similar nouns in domain events, but stick to the past tense of verbs.
Domain events and general language
When we talk about the side effects of "repositioning users," think about the differences in common languages. This event makes the concept clear, while in the past, the changes between aggregations or multiple aggregations will leave behind implicit concepts, which need to be explored and defined. For example, in most systems, when a side effect occurs on a library such as Hibernate or entity framework, it does not affect the domain. From the client's point of view, these events are implicit and transparent. The introduction of events makes the concept clear and part of the general language. "Relocating users" not only changes some content, but also explicitly generates CustomerRelocatedEvent events in the language.
Invariance
As we mentioned, domain events focus on past changes in the domain. By definition, you can't change the past unless you're Marty McFly and have a DeLorean. So remember that domain events are immutable.
Symfony event dispatcher
Some PHP frameworks support events. However, do not confuse these events with domain events. They are different in character and purpose. For example, Symfony has an Event Dispatcher component that you can rely on if you need to implement an event system for a state machine. In Symfony, the transformation between request and response is also handled by events. However, Symfony Events is mutable, and each listeners listener can modify, add, or update information in an event.
Event modeling
To accurately describe your domain business, you need to work closely with domain experts to define a common language. This requires the use of domain events, entities, value objects, and so on to complete domain concepts. When modeling events, events and their attributes are named in the context of their bounds according to the general language. If an event is the result of an operation performed by a command on an aggregate, the name is usually derived from the executed command. It is very important that event names reflect the past nature of events.
Let's consider the user registration feature. Domain events need to represent it. The following code shows the minimum interface for basic domain events:
interface DomainEvent { /** * @return DateTimeImmutable */ public function occurredOn(); }
As you can see, the minimum necessary information is DateTimeImmutable, which is to know when the event happened.
Now let's use the following code to model user registration events. As we mentioned above, event name must be verb past tense, so UserRegistered is a good choice:
class UserRegistered implements DomainEvent { private $userId; public function __construct(UserId $userId) { $this->userId = $userId; $this->occurredOn = new \DateTimeImmutable(); } public function userId() { return $this->userId; } public function occurredOn() { return $this->occurredOn; } }
The minimum amount of information necessary to notify subscribers of new user creation is UserId. With this information, any process, command, or application service - whether from the same boundary context or not - may respond to this event.
generally speaking
- Domain events are often designed to be immutable
- The constructor initializes the entire state of the domain event
- Domain events have getters accessors to access their properties
- The domain event contains the aggregate root that performs this action
- Domain events contain other aggregate roots associated with the first event
- The domain event contains the parameters that trigger the event, if useful
But what happens if the same or different clearance context requires more information? Let's look at modeling domain events with more information - for example, email address:
class UserRegistered implements DomainEvent { private $userId; private $userEmail; public function __construct(UserId $userId, $userEmail) { $this->userId = $userId; $this->userEmail = $userEmail; $this->occurredOn = new DateTimeImmutable(); } public function userId() { return $this->userId; } public function userEmail() { return $this->userEmail; } public function occurredOn() { return $this->occurredOn; } }
Above, we added the mailbox address, adding more information to a domain event can help improve performance or simplify the integration of different boundary contexts. It is also helpful to model events from the perspective of another bounding context. When a new user is created in the upstream bound context, the downstream context creates its own user. Adding a user mailbox saves a synchronization request to the upstream context in case the downstream context needs it.
Do you remember the example of Gamification? In order to create platform users, that is, players, a UserId from the e-commerce clearance context may be enough. But what if the game platform wants to notify users of the winning news by email? In this case, an email address is necessary. So, if the email address is included in the source domain event, we can do it. If not, game bound context needs to use REST or SOA to get this information from e-commerce context.
Why not use the entire user entity
Would you like to know if you should include the entire user entity in the domain event in a bounded context? Our suggestion is no need. Domain events are generally used for message communication within a given context or outside another context. In other words, in the context of C2C e-commerce product catalog, who is the seller and who is the product review author in product feedback. The two can share the same ID or email, but the seller and the author are different concepts, representing different boundary contexts. Therefore, entities from one boundary context have no meaning or are completely different in another context.
Doctrine event
Domain events are more than just bulk jobs, such as sending mail or communicating with other contexts. They are also interested in performance and scalable improvements. Let's take an example.
Consider the following scenario: you have an e-commerce application, your main persistence mechanism tool is MySQL, but for browsing or filtering your product catalog, you use a better method, such as elastic search or Solr. In elastic search, you get part of the information stored in the complete database. How to keep data synchronized? What happens when the content team updates the catalog through background tools?
From time to time, the index of the whole catalog has been rebuilt. It's very expensive and slow. A more sensible approach is to update one or some documentation with the updated product. What should we do? The answer is to use domain events.
But if you're already using Doctrine, it's not new to you. According to Doctrine 2 ORM 2 Documentation:
Doctrine 2 has a lightweight event system that is part of the Common package. Doctrine uses it for high system events, mainly life cycle events. You can also use it for your custom events.
In addition, it states:
Lifecycle callbacks are defined on an entity class. They allow you to trigger callbacks when instances of the entity encounter related lifecycle events. Multiple callbacks can be defined for each lifecycle event. Lifecycle callbacks are best used for simple operations in the lifecycle of a specific entity class.
Let's look at an example from Doctrine Events Documentation:
/** @Entity @HasLifecycleCallbacks */ class User { // ... /** * @Column(type="string", length=255) */ public $value; /** @Column(name="created_at", type="string", length=255) */ private $createdAt; /** @PrePersist */ public function doStuffOnPrePersist() { $this->createdAt = date('Y-m-d H:i:s'); } /** @PrePersist */ public function doOtherStuffOnPrePersist() { $this->value = 'changed from prePersist callback!'; } /** @PostPersist */ public function doStuffOnPostPersist() { $this->value = 'changed from postPersist callback!'; } /** @PostLoad */ public function doStuffOnPostLoad() { $this->value = 'changed from postLoad callback!'; } /** @PreUpdate */ public function doStuffOnPreUpdate() { $this->value = 'changed from preUpdate callback!'; } }
You can mount specific tasks to each important moment in the life cycle of a Doctrine entity. For example, on PostPersist, you can generate JSON documents for entities and place them in elastic search. In this way, it is easy to keep the data consistent between different persistence mechanisms.
The Doctrine event is a good example of the benefits of using events around your entity. But you can wonder what the problem is with using them. This is because they are coupled to the framework, they are synchronous, and they work at your application level, not for communication purposes. So that's why domain events are still very interesting, even though they are difficult to implement and handle.
Persistent domain events
Persistent events are always a good idea. Some of you can wonder why you can't publish domain events directly to a messaging or logging system. This is because there are some interesting benefits of persisting them:
- You can expose your domain events to other boundary contexts through the REST interface
- You can push domain events and aggregate changes to RabbitMQ to persist them to the same database transaction. (you don't want to send notifications of events that haven't happened, just as you don't want to miss notifications of events that have happened.)
- Business intelligence systems can use these data to analyze and predict trends.
- You can review your physical changes.
- For the event sourcing mechanism, you can rebuild aggregations from domain events.
Event Storage
Where do we persist domain events? In an Event Store. Event memory is a domain event repository, which exists in our domain space as an abstract (interface or abstract class). Its responsibility is to attach domain events and query. A possible basic interface is as follows:
interface EventStore { public function append(DomainEvent $aDomainEvent); public function allStoredEventsSince($anEventId); }
However, depending on the purpose of your domain event, the previous interface can have more ways to query events.
In terms of implementation, you can decide to use Doctrine Repository, dbal, or ordinary PDO. Because domain events are immutable, using the Doctrine Repository can increase unnecessary performance loss, although for small and medium-sized programs, Doctrine may be sufficient. Let's look at the possible implementation of Doctrine:
class DoctrineEventStore extends EntityRepository implements EventStore { private $serializer; public function append(DomainEvent $aDomainEvent) { $storedEvent = new StoredEvent( get_class($aDomainEvent), $aDomainEvent->occurredOn(), $this->serializer()->serialize($aDomainEvent, 'json') ); $this->getEntityManager()->persist($storedEvent); } public function allStoredEventsSince($anEventId) { $query = $this->createQueryBuilder('e'); if ($anEventId) { $query->where('e.eventId > :eventId'); $query->setParameters(['eventId' => $anEventId]); } $query->orderBy('e.eventId'); return $query->getQuery()->getResult(); } private function serializer() { if (null === $this->serializer) { /** \JMS\Serializer\Serializer\SerializerBuilder */ $this->serializer = SerializerBuilder::create()->build(); } return $this->serializer; } }
StoreEvent requires the Doctrine entity to map to the database. As you can see, after attaching and persisting the Store, there is no flush method call. If this operation is within the Doctrine transaction, it is unnecessary. Therefore, we will put it aside for now, and we will discuss it further in the chapter of application services.
Now let's look at the implementation of StoreEvent:
class StoredEvent implements DomainEvent { private $eventId; private $eventBody; private $occurredOn; private $typeName; /** * @param string $aTypeName * @param \DateTimeImmutable $anOccurredOn * @param string $anEventBody */ public function __construct( $aTypeName, \DateTimeImmutable $anOccurredOn, $anEventBody ) { $this->eventBody = $anEventBody; $this->typeName = $aTypeName; $this->occurredOn = $anOccurredOn; } public function eventBody() { return $this->eventBody; } public function eventId() { return $this->eventId; } public function typeName() { return $this->typeName; } public function occurredOn() { return $this->occurredOn; } }
Here is its mapping:
Ddd\Domain\Event\StoredEvent: type: entity table: event repositoryClass: Ddd\Infrastructure\Application\Notification\DoctrineEventStore id: eventId: type: integer column: event_id generator: strategy: AUTO fields: eventBody: column: event_body type: text typeName: column: type_name type: string length: 255 occurredOn: column: occurred_on type: datetime
In order to persist domain events with different fields, we will have to concatenate these fields into a serialized string. The typeName field describes the scope of the domain event. An entity or value object only makes sense in a bound context, but domain events define a communication protocol between bound contexts.
In a distributed system, garbage will occur. You will have to deal with domain transactions that are not published, are lost somewhere in the transaction chain, or have been published more than once. That's why it's important to use ID to persist domain events, which makes it easy to track which domain events have been published and which have been lost.
Publish events from domain model
Domain events should be published when the facts they represent occur. For example, when a new user is registered, a new user registered event should be published.
Refer to the following newspaper metaphor:
- Modeling a domain event is like writing a new article
- Publishing a domain event is like printing an article in a newspaper
- Spreading a domain event is like delivering a newspaper so that everyone can read it
The recommended way to publish domain events is to use a simple listener observer pattern to implement domain event publisher.
Publish events from entities
Continue to use the example of new user registration in our application, let's see how the corresponding domain events are published:
class User { protected $userId; protected $email; protected $password; public function __construct(UserId $userId, $email, $password) { $this->setUserId($userId); $this->setEmail($email); $this->setPassword($password); DomainEventPublisher::instance()->publish( new UserRegistered($this->userId) ); } // ... }
As shown in the example, a new UserRegistered event will be published when the user is created. This is done inside the constructor of the entity, not outside. Because with this method, we can easily maintain the consistency of our domain; any client that creates a new user will publish the corresponding event. On the other hand, this complicates the infrastructure that requires the creation of user entities without using their constructors. For example, Doctrine uses serialization and deserialization techniques to recreate objects without calling constructors. However, if you have to create your own application, it won't be as easy as Doctrine.
Generally speaking, constructing objects from simple data (such as arrays) is called hydration (hydration reaction). Let's take a look at a simple way to build new users from the database. First, let's extract the publication of domain events as our own method by applying the Factory Method pattern.
According to Wikipedia
Template method pattern is a behavior design pattern, which defines the framework of an algorithm in an operation and delays the implementation to a sub step
class User { protected $userId; protected $email; protected $password; public function __construct(UserId $userId, $email, $password) { $this->setUserId($userId); $this->setEmail($email); $this->setPassword($password); $this->publishEvent(); } protected function publishEvent() { DomainEventPublisher::instance()->publish( new UserRegistered($this->userId) ); } // ... }
Now, let's extend the current User class with a new infrastructure entity that will do this for us. The trick here is to make the publishEvent method do nothing so that domain events are not published:
class CustomOrmUser extends User { protected function publishEvent() { } public static function fromRawData($data) { return new self( new UserId($data['user_id']), $data['email'], $data['password'] ); } }
Remember to use this method. You may get invalid objects from the persistence mechanism. Because domain rules are always changing. Another way of not using the parent constructor might be as follows:
class CustomOrmUser extends User { public function __construct() { } public static function fromRawData($data) { $user = new self(); $user->userId = new UserId($data['user_id']); $user->email = $data['email']; $user->password = $data['password']; return $user; } }
In this way, the parent constructor cannot be called and the User's properties must be protected. Other methods include reflection, passing the identity in the native constructor, using a proxy library such as proxy manager, or using an ORM such as Doctrine.
Other ways to release domain events
As you can see in the previous example, we used static classes to publish domain events. As an alternative, others, especially when using event sources, suggest that you save all triggered events in one field within the entity. To access all events, use the getter method in the aggregation. This is also an effective method. However, it is sometimes difficult to track which entities have triggered events. It can also be difficult to trigger events where they are not entities, such as domain services. On the plus side, it's much easier to test whether an entity triggers an event.
Publish events from domain or application services
You should try to publish domain events from a deeper transaction chain. The closer the interior of an entity or value object, the better. As we saw in the previous section, sometimes it's not easy, but in the end it's easier for the client. We see developers publishing domain events from application services or domain services. This may seem easier to implement, but will eventually lead to anemia domain models. This is no different from pushing business logic in domain services rather than in your entities.
How Domain Event Publisher works
Domain Event Publisher is a singleton class, which comes from the bound context in which we need to publish domain events. It also pays for additional listeners, and domain event subscribers listen to any domain event they are interested in. This is not much different from jQuery subscription events using the on method:
class DomainEventPublisher { private $subscribers; private static $instance = null; public static function instance() { if (null === static::$instance) { static::$instance = new static(); } return static::$instance; } private function __construct() { $this->subscribers = []; } public function __clone() { throw new BadMethodCallException('Clone is not supported'); } public function subscribe( DomainEventSubscriber $aDomainEventSubscriber ) { $this->subscribers[] = $aDomainEventSubscriber; } public function publish(DomainEvent $anEvent) { foreach ($this->subscribers as $aSubscriber) { if ($aSubscriber->isSubscribedTo($anEvent)) { $aSubscriber->handle($anEvent); } } } }
The publish method checks all possible subscribers to see if they are interested in the published domain event. If so, the subscriber's handle method is called.
The subscribe method adds a new DomainEventSubscriber, which listens for the specified domain event type:
interface DomainEventSubscriber { /** * @param DomainEvent $aDomainEvent */ public function handle($aDomainEvent); /** * * @param DomainEvent $aDomainEvent * @return bool */ public function isSubscribedTo($aDomainEvent); }
As we've discussed, it's a good idea to persist all domain events. We can easily persist all published domain events in our application by using the specified subscribers. We now create a DomainEventSubscriber, which listens for all domain events and will persist to our event store no matter what type.
class PersistDomainEventSubscriber implements DomainEventSubscriber { private $eventStore; public function __construct(EventStore $anEventStore) { $this->eventStore = $anEventStore; } public function handle($aDomainEvent) { $this->eventStore->append($aDomainEvent); } public function isSubscribedTo($aDomainEvent) { return true; } }
$eventStore can be a custom Doctrine Repository, or as you can see, other objects capable of persisting DomainEvents to the database.
Set domain event listener
What is the best place to set up domain event publisher subscribers? It depends on need. For global subscribers that may affect the entire request cycle, the best location may be where DomainEventPublisher initializes itself. For subscribers affected by special application services, service instantiation may be a better choice. Let's look at an example of using Silex.
In Silex, the best way to register domain event publisher is to persist all domain events by using one application middleware. According to Silex 2.0 Documentation:
A before application middleware allows you to adjust requests before controller executes.
This is the correct location for the listener that the subscription is responsible for persisting these events to the database. These events will be sent to RabbitMQ later:
// ... $app['em'] = $app->share(function () { return (new EntityManagerFactory())->build(); }); $app['event_repository'] = $app->share(function ($app) { return $app['em']->getRepository( 'Ddd\Domain\Model\Event\StoredEvent' ); }); $app['event_publisher'] = $app->share(function ($app) { return DomainEventPublisher::instance(); }); $app->before( function (Symfony\Component\HttpFoundation\Request $request) use ($app) { $app['event_publisher']->subscribe( new PersistDomainEventSubscriber( $app['event_repository'] ) ); } );
With this setting, each time a realm event is published as an aggregate, it is persisted to the database. Mission accomplished.
Practice
If you use Symfony, Laravel, or other PHP frameworks, find a way to subscribe to globally designated subscribers and perform tasks around your domain events.
If you want to do anything with all domain events when the request is about to complete, you can create a listener that stores all published domain events in memory. If you add a getter accessor to this listener to return all domain events, you can decide what to do. As suggested earlier, this is useful if you don't want or can't persist events to the same transaction.
Test domain events
You already know how to publish domain events, but how do you unit test this and make sure that UserRegistered is actually triggered? The simplest way is to use a specified EventListener, which is used as a Spy to record whether domain events are published or not. Let's take a look at the unit test example of the User entity:
use Ddd\Domain\DomainEventPublisher; use Ddd\Domain\DomainEventSubscriber; class UserTest extends \PHPUnit_Framework_TestCase { // ... /** * @test */ public function itShouldPublishUserRegisteredEvent() { $subscriber = new SpySubscriber(); $id = DomainEventPublisher::instance()->subscribe($subscriber); $userId = new UserId(); new User($userId, 'valid@email.com', 'password'); DomainEventPublisher::instance()->unsubscribe($id); $this->assertUserRegisteredEventPublished($subscriber, $userId); } private function assertUserRegisteredEventPublished( $subscriber, $userId ) { $this->assertInstanceOf( 'UserRegistered', $subscriber->domainEvent ); $this->assertTrue( $subscriber->domainEvent->serId()->equals($userId) ); } } class SpySubscriber implements DomainEventSubscriber { public $domainEvent; public function handle($aDomainEvent) { $this->domainEvent = $aDomainEvent; } public function isSubscribedTo($aDomainEvent) { return true; } }
There are some alternatives to the above. You can use static setter s for domain event publisher or some reflection frameworks to detect calls. However, we think the way we share is more natural. Last but not least, remember to clean up your Spy subscription. To avoid affecting the execution of other unit tests.
Broadcast event to remote clearance context
In order to communicate a set of domain events to local or remote context, there are two main strategies: message or REST API. The first is to use a messaging system such as RabbitMQ to transport domain events. The second should create a REST API to access domain events in a specific context.
Message middleware
As all domain events persist into the database, the only thing left is to push them to our favorite messaging system. We prefer RabbitMQ, but any other system (such as ActiveMQ or ZeroMQ) can work. There are not many options for using PHP to integrate RabbitMQ, but PHP amqplib can do this.
First, we need a service that can send persistent domain events to RabbitMQ. You can want to query the EventStore for all events and send each event, which is not a bad thing. However, we can push the same domain event multiple times, and generally, we need to minimize the number of re published domain events. It would be better if the domain event to be relaunched is 0. In order not to resend domain events, we need some component to track which domain events have been pushed and which remain. Last but not least, once we know which domain events must be pushed, we send them and track the last event published to the messaging system. Let's look at the possible implementation of this service:
class NotificationService { private $serializer; private $eventStore; private $publishedMessageTracker; private $messageProducer; public function __construct( EventStore $anEventStore, PublishedMessageTracker $aPublishedMessageTracker, MessageProducer $aMessageProducer, Serializer $aSerializer ) { $this->eventStore = $anEventStore; $this->publishedMessageTracker = $aPublishedMessageTracker; $this->messageProducer = $aMessageProducer; $this->serializer = $aSerializer; } /** * @return int */ public function publishNotifications($exchangeName) { $publishedMessageTracker = $this->publishedMessageTracker(); $notifications = $this->listUnpublishedNotifications( $publishedMessageTracker ->mostRecentPublishedMessageId($exchangeName) ); if (!$notifications) { return 0; } $messageProducer = $this->messageProducer(); $messageProducer->open($exchangeName); try { $publishedMessages = 0; $lastPublishedNotification = null; foreach ($notifications as $notification) { $lastPublishedNotification = $this->publish( $exchangeName, $notification, $messageProducer ); $publishedMessages++; } } catch (\Exception $e) { // Log your error (trigger_error, Monolog, etc.) } $this->trackMostRecentPublishedMessage( $publishedMessageTracker, $exchangeName, $lastPublishedNotification ); $messageProducer->close($exchangeName); return $publishedMessages; } protected function publishedMessageTracker() { return $this->publishedMessageTracker; } /** * @return StoredEvent[] */ private function listUnpublishedNotifications( $mostRecentPublishedMessageId ) { return $this ->eventStore() ->allStoredEventsSince($mostRecentPublishedMessageId); } protected function eventStore() { return $this->eventStore; } private function messageProducer() { return $this->messageProducer; } private function publish( $exchangeName, StoredEvent $notification, MessageProducer $messageProducer ) { $messageProducer->send( $exchangeName, $this->serializer()->serialize($notification, 'json'), $notification->typeName(), $notification->eventId(), $notification->occurredOn() ); return $notification; } private function serializer() { return $this->serializer; } private function trackMostRecentPublishedMessage( PublishedMessageTracker $publishedMessageTracker, $exchangeName, $notification ) { $publishedMessageTracker->trackMostRecentPublishedMessage( $exchangeName, $notification ); } }
NotificationService relies on three interfaces. We have seen the event store, which is mainly responsible for adding and querying domain events. The second is published message tracker, which is mainly used to track pushed messages. The third is message producer, an interface representing our message system:
interface PublishedMessageTracker { /** * @param string $exchangeName * @return int */ public function mostRecentPublishedMessageId($exchangeName); /** * @param string $exchangeName * @param StoredEvent $notification */ public function trackMostRecentPublishedMessage( $exchangeName, $notification ); }
The mostRecentPublishedMessageId method returns the ID of the last published message, so this process can start next time. Trackmustrecentpublished message is responsible for tracking which message was last sent, so as to resend the message when you may need it. Exchangenome represents the communication channel to which we will send domain events. Let's take a look at a published message tracker implemented by Doctrine:
class DoctrinePublishedMessageTracker extends EntityRepository\ implements PublishedMessageTracker { /** * @param $exchangeName * @return int */ public function mostRecentPublishedMessageId($exchangeName) { $messageTracked = $this->findOneByExchangeName($exchangeName); if (!$messageTracked) { return null; } return $messageTracked->mostRecentPublishedMessageId(); } /** * @param $exchangeName * @param StoredEvent $notification */ public function trackMostRecentPublishedMessage( $exchangeName, $notification ) { if (!$notification) { return; } $maxId = $notification->eventId(); $publishedMessage = $this->findOneByExchangeName($exchangeName); if (null === $publishedMessage) { $publishedMessage = new PublishedMessage( $exchangeName, $maxId ); } $publishedMessage->updateMostRecentPublishedMessageId($maxId); $this->getEntityManager()->persist($publishedMessage); $this->getEntityManager()->flush($publishedMessage); } }
The code here is very straightforward. The only extreme situation we need is that the system has not released any domain events.
Why switch name?
We will cover this in more detail in Chapter 12, integration clearance context. However, when the system is running and the new bounding context starts to work, you may be interested in resending all domain events to the new bounding context. As a result, tracking the last released domain events and their improved channels may come in handy later.
To track published domain events, we need a switch name and a notification ID. Here is a possible implementation:
class PublishedMessage { private $mostRecentPublishedMessageId; private $trackerId; private $exchangeName; /** * @param string $exchangeName * @param int $aMostRecentPublishedMessageId */ public function __construct( $exchangeName, $aMostRecentPublishedMessageId ) { $this->mostRecentPublishedMessageId = $aMostRecentPublishedMessageId; $this->exchangeName = $exchangeName; } public function mostRecentPublishedMessageId() { return $this->mostRecentPublishedMessageId; } public function updateMostRecentPublishedMessageId($maxId) { $this->mostRecentPublishedMessageId = $maxId; } public function trackerId() { return $this->trackerId; } }
This is the mapping relationship:
Ddd\Domain\Event\PublishedMessage: type: entity table: event_published_message_tracker repositoryClass: Ddd\Infrastructure\Application\Notification\ DoctrinePublished\MessageTracker id: trackerId: column: tracker_id type: integer generator: strategy: AUTO fields: mostRecentPublishedMessageId: column: most_recent_published_message_id type: bigint exchangeName: type: string column: exchange_name
Now, let's look at what the MessageProducer interface is used for and its implementation details:
interface MessageProducer { public function open($exchangeName); /** * @param $exchangeName * @param string $notificationMessage * @param string $notificationType * * @param int $notificationId * @param \DateTimeImmutable $notificationOccurredOn * @return */ public function send( $exchangeName, $notificationMessage, $notificationType, $notificationId, \DateTimeImmutable $notificationOccurredOn ); public function close($exchangeName); }
Simple! The open and close methods open and close a message system connection. The send method takes a message body (message name and message ID) and sends it to our message engine, regardless of what it is. Because we choose RabbitMQ, we need to implement the connection and sending process:
abstract class RabbitMqMessaging { protected $connection; protected $channel; public function __construct(AMQPConnection $aConnection) { $this->connection = $aConnection; $this->channel = null; } private function connect($exchangeName) { if (null !== $this->channel) { return; } $channel = $this->connection->channel(); $channel->exchange_declare( $exchangeName, 'fanout', false, true, false ); $channel->queue_declare( $exchangeName, false, true, false, false ); $channel->queue_bind($exchangeName, $exchangeName); $this->channel = $channel; } public function open($exchangeName) { } protected function channel($exchangeName) { $this->connect($exchangeName); return $this->channel; } public function close($exchangeName) { $this->channel->close(); $this->connection->close(); } } class RabbitMqMessageProducer extends RabbitMqMessaging implements MessageProducer { /** * @param $exchangeName * @param string $notificationMessage * @param string $notificationType * @param int $notificationId * @param \DateTimeImmutable $notificationOccurredOn */ public function send( $exchangeName, $notificationMessage, $notificationType, $notificationId, \DateTimeImmutable $notificationOccurredOn ) { $this->channel($exchangeName)->basic_publish( new AMQPMessage( $notificationMessage, [ 'type' => $notificationType, 'timestamp' => $notificationOccurredOn->getTimestamp(), 'message_id' => $notificationId ] ), $exchangeName ); } }
Now that we have a domain service that can push domain events to messaging systems like RabbitMQ, it's time to execute them. We need to choose a delivery mechanism to run the service. Our personal suggestion is to create a Symfony Console command:
class PushNotificationsCommand extends Command { protected function configure() { $this ->setName('domain:events:spread') ->setDescription('Notify all domain events via messaging') ->addArgument( 'exchange-name', InputArgument::OPTIONAL, 'Exchange name to publish events to', 'my-bc-app' ); } protected function execute( InputInterface $input, OutputInterface $output ) { $app = $this->getApplication()->getContainer(); $numberOfNotifications = $app['notification_service'] ->publishNotifications( $input->getArgument('exchange-name') ); $output->writeln( sprintf( '<comment>%d</comment>' . '<info>notification(s) sent!</info>', $numberOfNotifications ) ); } }
According to the Silex example, let's look at the definition of $app ['notification [service '] defined in the Silex Pimple Service Container:
// ... $app['event_store'] = $app->share(function ($app) { return $app['em']->getRepository('Ddd\Domain\Event\StoredEvent'); }); $app['message_tracker'] = $app->share(function ($app) { return $app['em'] ->getRepository('Ddd\Domain\Event\Published\Message'); }); $app['message_producer'] = $app->share(function () { return new RabbitMqMessageProducer( new AMQPStreamConnection('localhost', 5672, 'guest', 'guest') ); }); $app['message_serializer'] = $app->share(function () { return SerializerBuilder::create()->build(); }); $app['notification_service'] = $app->share(function ($app) { return new NotificationService( $app['event_store'], $app['message_tracker'], $app['message_producer'], $app['message_serializer'] ); }); //...
Synchronizing domain services with REST
With the EventStore implemented in the message tradition, it should be easy to add some distribution functions, domain query events and render JSON or XML expressions to publish rest APIs. Why is it so interesting? Well, distributed systems using message middleware have to face many different problems, such as message not arriving, message repeatedly arriving, or message arrival out of order. That's why you need an API to publish your domain events so that other boundary contexts can request some missing information. As an example only, consider an HTTP request for a / event endpoint. A possible implementation is as follows:
[ { "id": 1, "version": 1, "typeName": "Lw\\Domain\\Model\\User\\UserRegistered", "eventBody": { "user_id": { "id": "459a4ffc-cd57-4cf0-b3a2-0f2ccbc48234" } }, "occurredOn": { "date": "2016-05-26 06:06:07.000000", "timezone_type": 3, "timezone": "UTC" } }, { "id": 2, "version": 2, "typeName": "Lw\\Domain\\Model\\Wish\\WishWasMade", "eventBody": { "wish_id": { "id": "9e90435a-395c-46b0-b4c4-d4b769cbf201" }, "user_id": { "id": "459a4ffc-cd57-4cf0-b3a2-0f2ccbc48234" }, "address": "john@example.com", "content": "This is my new wish!" }, "occurredOn": { "date": "2016-05-26 06:06:27.000000", "timezone_type": 3, "timezone": "UTC" }, "timeTaken": "650" } //... ]
As you can see in the previous example, we exposed a set of domain events in a JSON REST API. In the output example, you can see a JSON representation of each domain event. Here are some interesting points. The first is the version field. Sometimes your domain events will evolve: they will contain more fields, they will change the behavior of some existing fields, or they will delete some existing fields. That's why it's important to add a version field to a domain event. If other boundary contexts are listening for such events, they can use the version field to resolve domain events in different ways. You may encounter the same problem when versioning rest APIs.
The other is the name. If you want to use the class name of the domain event, most of the time. The problem is that when the team decides to change the class name due to refactoring, in this case, all the bound context listening to the name will stop working. This problem occurs only if you publish different domain events in the same queue. If you publish each domain event type to a different queue, it's not really a problem, but if you choose this method, you will face a series of different problems, such as receiving unordered events. As in many other cases, this requires trade-offs. We strongly recommend that you read * * enterprise integration patterns: designing, building, and deploying messaging systems solutions. In this book, you will learn different patterns for integrating multiple applications using asynchronous methods. Since domain events send messages on the integration channel, all message patterns apply to them.
Practice
Consider the advantages and disadvantages of using rest APIs for domain events. Consider bound context coupling. You can also implement rest APIs for your current applications.
Summary
We've seen techniques for modeling a suitable domain event using a basic interface, learned where to publish domain events (closer to the entity is better), and learned about policies for propagating these domain events to local and remote bounds contexts. Now, the only thing left is to listen to, read and execute the corresponding application service or command in the message system. We'll see how to do this in Chapter 12, integrating limited context and Chapter 5, services.