Domain Library
Dave RedfernPublished: 21 Feb 01:23 in Standalone
Introduction
Somnambulist Domain Package
Provides a core set of support classes for building domain oriented projects. This library compiles the previously separate domain and mapping libraries into a single project for easier maintenance.
It consists of:
- Commands
- CommandBus interface / abstract command
- SF Messenger implementation
- Doctrine
- Enumeration factories + Type bindings
- Additional types / type overrides for the Doctrine Type system
- Abstract EntityLocator that extends EntityRepository
- Custom Postgres DQL functions
- Custom traits for EntityRepository
- Entities
- Contracts - value object interface definitions
- Types - a collection of value-objects, enumerations and an aggregate root
- AggregateRoot - an aggregate root stub implementation that can raise events
- AbstractEntity and AbstractEntityCollection - child entities and helper for an aggregate root
- Events
- EventBus interface / abstract event
- SF Messenger EventBus implementation
- Doctrine subscriber that broadcasts onFlush all events from aggregate roots
- Custom serializer to handle encode/decode when the event class does not exist
- Jobs
- JobQueue interface / abstract job
- SF Messenger implementation
- Queries
- QueryBus interface / abstract query
- SF Messenger implementation
- default XML mappings for embeddable objects in Doctrine .dcm.xml and Symfony .orm.xml conventions
Requirements
- PHP 8.0+
- mb_string
- bcmath
- beberlei/assert
- eloquent/enumeration
- somnambulist/collection
- symfony/messenger for the Messenger bus implementations.
Installation
Install using composer, or checkout / pull the files from github.com.
- composer require somnambulist/domain
Usage
To get the most from this library, it is strongly advisable to read about Domain Driven Design and in particular how DDD can influence and help you model the business problem you are trying to solve. Without a clear understanding of your businesses domain and the problems that are being faced, it can be difficult to implement the logic.
With that said, there are several key things to bare in mind:
- try to keep a single aggregate root per context
- do not directly link aggregate roots - only use the identity
- do not cross-reference entities from other contexts
- keep reads and writes as distinctly separate tasks
- name your objects, events, commands, queries etc. for actual things in your business
- avoid magic as much as possible - especially Doctrine lifecycle event subscribers.
Links
Aggregate Root
Aggregate Root
Implements a generalised Aggregate Root for raising domain events in an entity.
- Doctrine and Domain Events
- Decoupling applications with Domain Events
- Gist for Doctrine Implementation
Using
An Aggregate is a Domain Driven Design concept that encapsulates a set of related domain concepts that should be managed together. See Fowler: Aggregate Root Examples include: Order, User, Customer. In your domain code, only the aggregate should be loaded.
First identify what your aggregate roots are within your domain objects. Then extend the abstract AggregateRoot into your root entity. This is the entry point for changes to that tree of objects.
Next: implement your domain logic and raise appropriate events for each of the changes that your aggregate should allow / manage. Your events should mirror business process and ideally all your methods and objects should follow the businesses terminology.
Raising Events
To raise an event, decide which actions should result in a domain event. These should coincide with state changes in the domain objects and the events should originate from your main entities (aggregate roots).
For example: you may want to raise an event when a new User entity is created or that a role was added to the user.
This does necessitate some changes to how you typically work with entities and Doctrine in that you should remove setters and nullable constructor arguments. Instead you will need to manage changes to your entity through specific methods, for example:
- completeOrder()
- grantPermissions()
- revokePermissions()
- publishStory()
Internally, after updating the entity state, call: $this->raise(NameOfEvent::class, [])
and pass any specific parameters into the event that you want to make available to the listener. This could be the old vs new or the entire entity reference, it is entirely up to you.
<?php
use Somnambulist\Components\Domain\Entities\AggregateRoot;
class MyAggregate extends AggregateRoot
{
public function __construct($id, $name, $another)
{
$this->id = $id;
$this->name = $name;
$this->another = $another;
$this->initializeTimestamps();
$this->raise(MyEntityCreatedEvent::class, ['id' => $id, 'name' => $name, 'another' => $another]);
}
}
Generally it is better to not raise events in the constructor but instead to use named constructors for primary object creation:
<?php
use Somnambulist\Components\Domain\Entities\AggregateRoot;
class MyAggregate extends AggregateRoot
{
private function __construct($id, $name, $another)
{
$this->id = $id;
$this->name = $name;
$this->another = $another;
$this->initializeTimestamps();
}
public static function create($id, $name, $another)
{
$entity = new static($id, $name, $another, new DateTime());
$entity->raise(MyEntityCreatedEvent::class, ['id' => $id, 'name' => $name, 'another' => $another]);
return $entity;
}
}
Dealing with Timestamps
When dealing with Aggregates, the aggregate should maintain its state; this includes any timestamps. If these are deferred to the database or ORM layer, then your Aggregate is being changed outside separately to when the state was changed.
Instead of relying on the ORM or database, you should use the initializeTimestamps()
and updateTimestamps()
methods that are part of the AggregateRoot class. The updatedAt
property is automatically updated when you raise an event (just before adding the event to the stack).
Dealing with Sub-Objects
When creating an aggregate root, it is tempting to place all functionality within the scope of that single class, even though there may be child objects related to it. If those need to be updated, then they should be the ones to do that update.
As the raise()
method is public and the aggregate is passed to the child, you can use the aggregate to raise more events from inside those child objects.
For example: an Order and an OrderItem, the quantity needs to be updated on the order item:
$order->lineItem($ref)->changeQuantity(4);
The OrderItem method might be something like:
<?php
class OrderItem
{
public function changeQuantity(int $quantity): void
{
Assert::that($quantity, 'quantity')->gt(0)->lte(20);
$previousQuantity = $this->quantity;
$this->quantity = $quantity;
$this->order->raise(OrderItemQuantityChanged::class, [
'id' => $this->order->id(),
'item_id' => $this->id(),
'quantity' => $this->quantity,
], [
'previous' => [
'quantity' => $previousQuantity,
]
]);
}
}
Dealing with Collections
Similar to child objects, it is common to apply all the collection handling logic directly in the main aggregate.
However: we want to avoid exposing the internals to modification so instead or allowing direct access to the underlying collection instance, you can wrap it in a broker that can mediate access to the entities it controls. This way events on delete can be triggered easily and manipulations can be controlled.
For example: a User can have several addresses of certain types:
<?php
class UserAddresses
{
private $user;
private $addresses;
public function __construct(User $user, Collection $addresses)
{
// assign vars
}
public function for(AddressType $type)
{
if (!$addr = $this->addresses->get((string)$type)) {
throw new DomainException('User does not have an address for type: ' . $type);
}
return $addr;
}
public function add(AddressType $type, AddressInfo $info): UserAddress
{
$this->entities->set((string)$type, $ua = new UserAddress($this->user, $type, $info));
return $ua;
}
}
Now to update the address it would be something like:
$user->addresses()->for(AddressType::DELIVERY())->updateAddressTo($address);
If the User does not have a delivery address, an exception would be raised. In this example an Enumeration is used for the type to provide type safety and the Address is passed as a value object, again, providing type safety.
Built-in Entity Collection Helper / AbstractEntity
Alternatively the AbstractEntityCollection
class can be used to provide suitable defaults that you can extend to add your domain logic.
To use this, your child entities should extend the AbstractEntity
class. This is required to be able to use the AbstractEntityCollection
. Note that these classes are set up to use basic integers as the identity of the child object. If you require something more sophisticated, you would need to implement your own alternative.
Using the same example as above this would be implemented as:
<?php
use Somnambulist\Components\Domain\Entities\AbstractEntityCollection;
class UserAddresses extends AbstractEntityCollection
{
public function for(AddressType $type)
{
if (!$addr = $this->addresses->get((string)$type)) {
throw new DomainException('User does not have an address for type: ' . $type);
}
return $addr;
}
public function add(AddressType $type, AddressInfo $info): UserAddress
{
$this->entities->set((string)$type, $ua = new UserAddress($this->root, $this->nextId(), $type, $info));
$this->raise(AddressAddedToUser::class, [
// add payload info
]);
return $ua;
}
}
On the AggregateRoot the method to load the helper changes slightly:
use Somnambulist\Components\Domain\Entities\AggregateRoot;
use Somnambulist\Components\Domain\Entities\Behaviours\AggregateEntityCollectionHelper;
class User extends AggregateRoot
{
use AggregateEntityCollectionHelper;
public function addresses(): UserAddresses
{
return $this->collectionHelperFor($this->addresses, UserAddresses::class);
}
}
The helper will be instantiated if it is not already set and then the same instance will be returned each time. Only AbstractEntityCollection
instances can be used with this trait.
To map this to Doctrine the following should be used on the child mapping definition:
<doctrine-mapping>
<entity name="UserAddress" table="user_address">
<id name="root" association-key="true" />
<id name="id" type="integer" />
<many-to-one field="root" target-entity="User" inversed-by="addresses">
<cascade>
<cascade-persist/>
</cascade>
<join-column name="user_id" />
</many-to-one>
</entity>
</doctrine-mapping>
The important part is the change to the id
field: there are now 2, with the first having the attribute association-key
. This tells Doctrine to use the identity from the linked object, in this case the AggregateRoot (that is held in the root
property on the AbstractEntity class). The second field tells Doctrine that the id
property is also part of the identity. In effect the actual identity of our child entity is now + and is a compound key.
Firing Domain Events
See Domain Events for integrating various strategies for dispatching domain events raised from the aggregate root.
Be sure to read the posts by Benjamin Eberlei mentioned earlier and check out his Assertion library for low dependency entity validation.
Domain Events
Domain Events
Domain events are part of the main AggregateRoot class. Extend this to be able to raise events in your entity.
- Doctrine and Domain Events
- Decoupling applications with Domain Events
- Gist for Doctrine Implementation
Raising Events
To raise an event, decide which actions should result in a domain event. These should coincide with state changes in the domain objects and the events should originate from your main entities (aggregate roots).
For example: you may want to raise an event when a new User entity is created or that a role was added to the user.
This does necessitate some changes to how you typically work with entities and Doctrine in that you should remove setters and nullable constructor arguments; instead you will need to manage changes to your entity through specific methods, for example:
- completeOrder()
- updatePermissions()
- revokePermissions()
- publishStory()
Internally, after updating the entity state, call: $this->raise(NameOfEvent::class, [])
and pass any specific parameters into the event that you want to make available to the listener. This could be the old vs new or the entire entity reference, it is entirely up to you.
<?php
use Somnambulist\Components\Domain\Entities\AggregateRoot;
class SomeObject extends AggregateRoot
{
public function __construct($id, $name, $another, $createdAt)
{
$this->id = $id;
$this->name = $name;
$this->another = $another;
$this->createdAt = $createdAt;
$this->raise(MyEntityCreatedEvent::class, ['id' => $id, 'name' => $name, 'another' => $another]);
}
}
Generally it is better to not raise events in the constructor but instead to use named constructors for primary object creation:
<?php
use Somnambulist\Components\Domain\Entities\AggregateRoot;
class SomeObject extends AggregateRoot
{
private function __construct($id, $name, $another, $createdAt)
{
$this->id = $id;
$this->name = $name;
$this->another = $another;
$this->createdAt = $createdAt;
}
public static function create($id, $name, $another)
{
$entity = new static($id, $name, $another, new DateTime());
$entity->raise(MyEntityCreatedEvent::class, ['id' => $id, 'name' => $name, 'another' => $another]);
return $entity;
}
}
Defining an Event
To define your own event extend the AbstractDomainEvent object. That's basically it!
<?php
use Somnambulist\Components\Domain\Events\AbstractEvent;
class MyEntityCreatedEvent extends AbstractEvent
{
}
You can create an intermediary to add base methods to your events e.g.: if you want to broadcast through a message queue you may want the event to name itself:
<?php
use Somnambulist\Components\Domain\Events\AbstractEvent;
abstract class AppDomainEvent extends AbstractEvent
{
protected string $group = 'app';
public function getEventName(): string
{
return sprintf('%s.%s', $this->getGroup(), strtolower($this->getName()));
}
}
And then extend it with the overrides you need:
<?php
class MyEntityCreatedEvent extends AppDomainEvent
{
}
Notifying Domain Events
Doctrine Integration
This implementation includes a Doctrine subscriber that will listen for AggregateRoots. These are collected and once the Unit of Work has been flushed successfully will be dispatched via the EventBus implementation that is in use (default Messenger).
- Note: it is not required to use the
DomainEventPublisher
subscriber. You can implement your own event dispatcher, use another dispatcher entirely (the frameworks) and then manually trigger the domain events by flushing the changes and then manually callingreleaseAndResetEvents
and dispatching the events.
To use the included listener, add it to your list of event subscribers in the Doctrine configuration. This is per entity manager.
- Note: to use listeners with domain events that rely on Doctrine repositories it is necessary to defer loading those subscribers until after Doctrine has been resolved.
As of v3 the events are only broadcast on the event bus and are not sent to Doctrines event manager.
Messenger Integration
This dispatcher allows you to register aggregates roots with the dispatcher, and then once you have finished manipulating the domain; notify all event changes to the bound event bus (default Messenger).
This dispatcher uses an abstract base that includes methods for sorting and collecting the events. It can be extended to perform other tasks.
This dispatcher can be registered with the kernel.terminate events so that any collected events are fired at the end of the current request.
Remember that you will need to register the objects that will raise events before hand.
Note: the Messenger dispatcher does not release monitored aggregates after event dispatch. You will need to specifically stop listening for events to clear the listener.
Be sure to read the posts by Benjamin Eberlei mentioned earlier and check out his Assertion library for low dependency entity validation.
Value Objects
Value Objects Library
Value Objects (VOs) are Immutable domain objects that represent some value in your domain model but without a thread of continuous identity i.e. their identity is through their properties. VOs allow your entities to encapsulate properties and provide type safety.
This library provides an abstract base class that provides a basic equality test and foundation for your VOs along with a couple of basic types. As VOs form part of YOUR domain, you should implement the VOs that you need for your domain, following your domain naming (e.g. if you do not call an email address an EmailAddress then create your own VO for that purpose).
VOs should be self-validating during construction. For this purpose, the Assertion library by Benjamin Eberlei is used, however you may wish to use another or filter_var() etc directly.
If you see something missing or have suggestions for other methods, submit a PR or ticket.
Usage
Extend the abstract value object and implement the single required method toString(). The default equality method (equals()) uses reflection on the VO properties and compares them directly - only between VO types.
For example:
<?php
use Assert\Assert;
use Somnambulist\Components\Domain\Entities\AbstractValueObject;
final class Uuid extends AbstractValueObject
{
private string $uuid;
public function __construct(string $uuid)
{
Assert::that($uuid, null, 'uuid')->notEmpty()->uuid();
$this->uuid = $uuid;
}
public function toString(): string
{
return $this->uuid;
}
}
$uuid = new Uuid(\Ramsey\Uuid\Uuid::uuid4());
Usage with Doctrine
These VOs may be used with Doctrine as Embeddable objects - however if you allow the VO to be null, it will be instantiated empty so your methods / toString() should handle that case e.g.:
A User has a nullable Profile VO, when Doctrine hydrates the User, the Profile VO will also be hydrated but empty, so if the Profile has a nickname() or avatar() method, these must support returning null and your toString() method must cast null to a string to avoid type errors.
Note: when referencing UUIDs if the UUID type is registered and your field type is set to uuid
Doctrine will hydrate a Uuid object - not a string. Be sure to use guid
as the type in these cases; or do not register the UUID type mapping, or map that to something else.
Enumerations
Enumerations
Enumerations are provided via the eloquent/enumeration library (not be confused with Laravel Eloquent). An enumeration is essentially a typed constant where there is only ever one instance of that value.
For example: HTTP verbs could be represented as an enumeration because there is never more than one instance of GET, POST, PUT, PATCH, DELETE, HEAD etc.
The most useful feature of an enumeration is that it can only be one of the defined values so it can be safely type-hinted using the enumeration class name.
Usage
Continuing with the above example of HTTP verb, we create an enumeration as follow:
<?php
namespace App\Domain;
use Somnambulist\Components\Domain\Entities\AbstractEnumeration;
final class HTTPMethod extends AbstractEnumeration
{
const GET = 'GET';
const POST = 'POST';
const PATCH = 'PATCH';
const PUT = 'PUT';
const DELETE = 'DELETE';
const HEAD = 'HEAD';
}
And then to use it:
$verb = HTTPMethod::GET();
Each constant is converted to a method via __callStatic
. It can be type-hinted on a class e.g.:
<?php
class RequestLog
{
public function __construct(string $resource, HTTPMethod $method)
{
}
}
Multitons
Enumerations are a simple type of multiton. A multiton is more or less the same thing, but can have many properties. The example from the library is a Planet
but you can consider Countries, or Currencies as multitons. In fact that is how they are handled in this library.
An important difference between the multiton and the enumeration, is that we must define and pre-load any of the instances by overloading: initializeMembers
. Then in that method we create our instances:
<?php
use Somnambulist\Components\Domain\Entities\AbstractMultiton;
final class Planet extends AbstractMultiton
{
protected function __construct($key, $name, $diameter, $mass, $distanceToSun)
{
$this->name = $name;
$this->diamemter = $diameter;
$this->mass = $mass;
$this->distanceToSun = $distanceToSun;
// very important! be sure to pass the unique key to the parent constructor
parent::__construct($key);
}
protected static function initializeMembers()
{
new static($key, $name, $diameter, $mass, $distanceToSun);
}
}
Additionally: as a multiton can have many properties, we must define which one should be used when casting to string:
<?php
use Somnambulist\Components\Domain\Entities\AbstractMultiton;
final class Planet extends AbstractMultiton
{
public function toString(): string
{
return (string)$this->name();
}
}
The planet can then be accessed using: Planet::memberByKey('<the name>');
Important: once created an enumeration / multiton CANNOT be extended! So always mark them as final
.
Checkout the Currency and Country objects for examples of multitons.
See Doctrine Enum Bridge for how to integrate enumerations with Doctrine.
Doctrine Mappings
Doctrine Mappings for Value Objects and Enumerations
Provides a basic set of mapping information for the somnambulist/value-objects library for use with Doctrine. Mappings are available for Doctrine (.dcm.yml) and Symfony (.orm.yml). The mappings are symlinked from symfony to doctrine.
A TypeBootstrapper
is included for automatically registering the value-object enumerations as Doctrine types.
Usage
Copy or link the mapping files to your project in the Doctrine configuration. These are needed per entity manager. It is highly recommended to extend the value-objects to your own and then copy and adapt the mappings as you need.
Remember: value-objects are part of your domain model and should be treated with care.
Note: enumerations are used in these mappings.
Register Enumeration Handlers
To register the enumeration handlers add the following to your applications bootstrap code (e.g.: AppBundle::boot or AppServiceProvider::register|boot):
<?php
Somnambulist\Components\Domain\Doctrine\TypeBootstrapper::registerEnumerations();
This will pre-register the following enumerations:
- Geography\Country as country
- Money\Currency as currency
- Measure\AreaUnit as area_unit
- Measure\DistanceUnit as distance_unit
- Geography\Srid as srid
In addition extra helpers are registered to allow the Country and Currency value objects to be used as enumerations. These are stored using the respective ISO 3-char codes.
Note: concrete enumerations cannot be extended. If the built-in ones do not meet your needs, create your own.
See Doctrine Enumeration Bridge for more on using the bridge.
Register Custom Types
Custom types are included for:
- datetime
- datetimetz
- date
- time
- json
- ip_v4_address
- ip_v6_address
- phone
- url
- uuid
The date types override the default Doctrine types and uses the VO DateTime that is an extended DateTimeImmutable object.
json, jsonb and json_collection are equivalent and allow JSON data to be converted to and from a Collection object instead of a plain array.
To register all the standard types add the following to your application bootstrap:
<?php
Somnambulist\Components\Domain\Doctrine\TypeBootstrapper::registerTypes(TypeBootstrapper::$types);
Note: if you register uuid
as a type, and then use it in e.g.: an embeddable your embeddable will receive an Uuid object, not an Uuid string. Ensure the type is set to guid
to get just the string value.
Mapping Files
To use the types and enumerations, in your mapping files set the type appropriately:
fields:
createdAt:
type: datetime
attributes:
type: json
country:
type: country
currency:
type: currency
To embed the value-objects instead of using type casting:
embedded:
contact:
class: Somnambulist\Components\Domain\Entities\Types\Identity\EmailAddress
homepage:
class: Somnambulist\Components\Domain\Entities\Types\Web\Url
Or in XML format:
<entity name="My\Entity">
<embedded name="contact" class="Somnambulist\Components\Domain\Entities\Types\Identity\EmailAddress" />
<embedded name="homepage" class="Somnambulist\Components\Domain\Entities\Types\Web\Url" />
</entity>
When using embeddables, be sure to have added the necessary mapping files.
Alternatively if the extended types are registered you can instead use:
<entity name="My\Entity">
<field name="email" class="email" length="200" />
<field name="phone" class="phone" length="20" />
<field name="homepage" class="url" length="400" />
</entity>
The primary difference is that the type can be customised per column, whereas an embedded object is shared with a common config and therefore, fixed length or nullable or not.
Configuring Types for Symfony
Within a Symfony project, add a new mapping area to your orm configuration within the doctrine
section:
doctrine:
# snip ...
orm:
mappings:
App\Entities:
mapping: true
type: xml
dir: '%kernel.project_dir%/config/mappings/entities'
is_bundle: false
prefix: App\Entities
Somnambulist\Components\Domain\Entities\Types:
mapping: true
type: xml
dir: '%kernel.project_dir%/config/mappings/somnambulist'
is_bundle: false
prefix: Somnambulist\Components\Domain\Entities\Types
Then either copy or symlink the appropriate config files from vendor config folder to your projects mapping config section. If you have different requirements for field type, copy and update as appropriate. It is recommended to copy and not link the mapping files to avoid issues with this library changing.
Links
Doctrine Enumeration Bridge
Doctrine Enum Bridge
Provides a bridge between different enumeration implementations and Doctrine. Any type of PHP enumerable (e.g. Eloquent\Enumeration or myclabs/php-enum can be used with this adaptor.
A default, string casting, serializer is used if no serializer is provided.
All enumerations are stored using the DBs native varchar format. If you wish to use a custom DB type, extend and re-implement the getSQLDeclaration()
method.
Usage
In your frameworks application service provider / bundle boot method, register your enums and the appropriate callables to create / serialize as needed. A default serializer that casts the enumerable to a string will be used if none is provided.
The callbacks will receive:
- value - the current value either a PHP type, or the database type (for constructor)
- platform - the Doctrine AbstractPlatform instance
For example, in a Symfony project, in your AppBundle class:
<?php
use Somnambulist\Components\Domain\Doctrine\Types\EnumerationBridge;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function boot()
{
EnumerationBridge::registerEnumType(Action::class, function ($value) {
if (Action::isValid($value)) {
return new Action($value);
}
throw new InvalidArgumentException(sprintf(
'The value "%s" is not valid for the enum "%s". Expected one of ["%s"]',
$value,
Action::class,
implode('", "', Action::toArray())
));
});
}
}
In Laravel, add to your AppServiceProvider (register
and boot
should both work):
<?php
use Somnambulist\Components\Domain\Doctrine\Types\EnumerationBridge;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
EnumerationBridge::registerEnumType(Action::class, function ($value) {
if (Action::isValid($value)) {
return new Action($value);
}
throw new InvalidArgumentException(sprintf(
'The value "%s" is not valid for the enum "%s". Expected one of ["%s"]',
$value,
Action::class,
implode('", "', Action::toArray())
));
});
}
}
When registering the type, you can either use the fully qualified class name, or an alias / short string. The only limitation is that it should be unique for each enumeration. In the above example we could register the enumeration as http_action
instead.
Note: the bridge will check if the type has already been registered and skip it if that is the case. If you wish to replace an existing type then you should use Type::overrideType()
, however that will only work if the type has already been registered.
Note: when using short aliases you MUST explicitly set the class in the constructor for hydrating the object. This means that constructors cannot be shared with other types.
Register Multiple Types
Multiple enumerations can be registered at once by calling registerEnumTypes()
and passing an array of enum name and either an array of callables (constructor, serializer) or just the constructor:
<?php
use Somnambulist\Components\Domain\Doctrine\Types\EnumerationBridge;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function boot()
{
EnumerationBridge::registerEnumTypes(
[
'gender' => [
function ($value) {
if (Gender::isValidValue($value)) {
return Gender::memberByValue($value);
}
throw new InvalidArgumentException(sprintf(
'The value "%s" is not valid for the enum "%s"', $value, Gender::class
));
},
function ($value, $platform) {
return is_null($value) ? 'default' : $value->value();
}
]
]
);
}
}
Usage in Doctrine Mapping Files
In your Doctrine mapping files simply set the type on the field:
fields:
name:
type: string
length: 255
gender:
type: gender
action:
type: AppBundle\Enumerable\Action
The type should be set to whatever you used when registering. If this is the class name, use that; if you used a short name - use that instead. It is recommended to use short names as it is easier to manage them than figuring out the full class name (that does not usually auto-complete).
Note: Doctrine has deprecated Yaml config, use XML instead.
Built-in Enumeration Constructors
The following value-object constructors are provided in the library in the Doctrine\Enumerations
namespace:
- CountryEnumeration
- CurrencyEnumeration
- GenericEloquentEnumeration
- GenericEloquentMultiton
- NullableGenericEloquentEnumeration
When using Country or Currency the custom serializer should be registered to correctly convert the VO to the ISO code for storage. These would be setup as follows:
<?php
use Somnambulist\Components\Domain\Doctrine\Enumerations\Constructors\CountryConstructor;
use Somnambulist\Components\Domain\Doctrine\Enumerations\Constructors\CurrencyConstructor;
use Somnambulist\Components\Domain\Doctrine\Enumerations\Serializers\CountrySerializer;
use Somnambulist\Components\Domain\Doctrine\Enumerations\Serializers\CurrencySerializer;
use Somnambulist\Components\Domain\Doctrine\Types\EnumerationBridge;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function boot()
{
EnumerationBridge::registerEnumType('country', new CountryConstructor(), new CountrySerializer());
EnumerationBridge::registerEnumType('currency', new CurrencyConstructor(), new CurrencySerializer());
}
}
Note: the first argument of registerEnumType
is the alias/name for how to refer to this type. If you use the fully qualified class name via the ::class
constant, then the Doctrine mapping must reference this type:
<field name="currency" type="Somnambulist\Components\Domain\Entities\Types\Money\Currency" length="3" nullable="false"/>
vs:
<field name="currency" type="currency" length="3" nullable="false"/>
By default short aliases are registered by this library.
Links
Symfony Messenger
Symfony Messenger Integration
Symfony Messenger can be used to implement:
- CommandBus
- QueryBus
- EventBus
- JobQueue
These implementations are based on the Symfony documentation:
- https://symfony.com/doc/current/messenger/handler_results.html
- https://symfony.com/doc/current/messenger/multiple_buses.html
This requires setting up messenger as follows:
framework:
messenger:
failure_transport: failed
default_bus: command.bus
buses:
# creates a MessageBusInterface instance available on the $commandBus argument
command.bus:
middleware:
- validation
- doctrine_transaction
query.bus:
middleware:
- validation
event.bus:
middleware:
- validation
job.queue:
middleware:
- validation
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
domain_events:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%/domain_events'
options:
exchange:
name: domain_events
type: fanout
job_queue:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%/jobs'
options:
exchange:
name: jobs
type: direct
# optional to capture failures
failed: 'doctrine://default?queue_name=failed'
# synchronous transport
sync: 'sync://'
routing:
# Route your messages to the transports
Somnambulist\Components\Domain\Events\AbstractEvent: domain_events
Somnambulist\Components\Domain\Jobs\AbstractJob: job_queue
Somnambulist\Components\Domain\Commands\AbstractCommand: sync
Somnambulist\Components\Domain\Queries\AbstractQuery: sync
The above configuration will automatically route all extended Commands and Queries to the sync transport and the DomainEvent instances to the event bus named domain_events
. Jobs will go to the job_queue
.
Then the following services should be defined in services.yaml
:
services:
Somnambulist\Components\Domain\Events\Adapters\MessengerEventBus:
Somnambulist\Components\Domain\Events\EventBus:
alias: Somnambulist\Components\Domain\Events\Adapters\MessengerEventBus
public: true
Somnambulist\Components\Domain\Jobs\Adapters\MessengerJobQueue:
Somnambulist\Components\Domain\Jobs\JobQueue:
alias: Somnambulist\Components\Domain\Jobs\Adapters\MessengerJobQueue
public: true
Somnambulist\Components\Domain\Commands\Adapters\MessengerCommandBus:
Somnambulist\Components\Domain\Commands\CommandBus:
alias: Somnambulist\Components\Domain\Commands\Adapters\MessengerCommandBus
public: true
Somnambulist\Components\Domain\Queries\Adapters\MessengerQueryBus:
Somnambulist\Components\Domain\Queries\QueryBus:
alias: Somnambulist\Components\Domain\Queries\Adapters\MessengerQueryBus
public: true
To use the underlying Messenger instances, type-hint a MessageBusInterface
and then use the appropriate camelCased variable name:
<?php
use Symfony\Component\Messenger\MessageBusInterface;
class MyController extends Controller
{
public function __construct(MessageBusInterface $commandBus)
{
// the command bus Messenger instance will be injected
$this->commandBus = $commandBus;
}
}
Now the services can be type-hinted using the interfaces and auto-wired correctly.
The EventBus
can be injected into the Doctrine subscriber to allow for the domain events to be automatically broadcast postFlush.
Broadcast Domain Events
The Doctrine event subscriber supports broadcasting domain events if the EventBus is configured. To enable the event subscriber add the following to your services.yaml
:
services:
Somnambulist\Components\Domain\Events\Publishers\DoctrineEventPublisher:
tags: ['doctrine.event_subscriber']
This will register a Doctrine event subscriber that listens to:
- prePersist
- preFlush
- postFlush
Events are queued, sorted by the timestamp to ensure the correct order and sent postFlush.
Note: by default Messenger 4.3+ defaults to PHP native serializer. This will mean that the message payload contains PHP serialized objects. To send JSON payloads, a custom serializer is needed. This must be configured as follows:
Install Symfony Serializer if not already installed: composer req symfony/serializer symfony/property-access
.
Note: property-access
is required to enable the ObjectNormalizer
that is used to serialize the envelope stamp objects.
Add the following service definition and optional alias if desired:
services:
Somnambulist\Components\Domain\Events\Adapters\MessengerSerializer:
somnambulist.domain.event_serializer:
alias: Somnambulist\Components\Domain\Events\Adapters\MessengerSerializer
Set the serializer on the domain_events transport:
framework:
messenger:
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
domain_events:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%/domain_events'
serializer: somnambulist.domain.event_serializer
You will need to require the Symfony serializer component for this to work. See: https://symfony.com/doc/current/messenger.html#serializing-messages for further documentation.
To use the Symfony Serializer by default for all serialization (except domain events):
framework:
messenger:
serializer:
default_serializer: messenger.transport.symfony_serializer
symfony_serializer:
format: json
context: { }
Note: the EventBus
provided here is specifically for domain events. For general events consider adding a separate bus.
Note: since v3 the EventBus can handle generic events - they will not have an aggregate associated with them.