fusonic / ddd-extensions
Symfony domain-driven design building blocks for usage with Doctrine ORM.
Installs: 4 452
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 7
Forks: 0
Open Issues: 0
Requires
- php: >=8.2
- beberlei/assert: ^3.2
Requires (Dev)
- doctrine/orm: ^3.2
- friendsofphp/php-cs-fixer: ^3.64
- infection/infection: ^0.29
- nelmio/api-doc-bundle: ^v4.29
- nikic/php-parser: ^5.1
- phpstan/phpstan: ^1.12
- phpstan/phpstan-deprecation-rules: ^1.2
- phpstan/phpstan-phpunit: ^1.4
- phpstan/phpstan-strict-rules: ^1.6
- phpstan/phpstan-symfony: ^1.4
- phpunit/phpunit: ^11.3
- symfony/cache: ^6.4 || ^7.0
- symfony/messenger: ^6.4 || ^7.0
- symfony/serializer: ^6.4 || ^7.0
- tomasvotruba/type-coverage: ^0.3
Suggests
- doctrine/orm: ^3.2
- nelmio/api-doc-bundle: ^4.29
- symfony/messenger: ^^6.4 || ^7.0
- symfony/serializer: ^6.4 || ^7.0
README
About
This library provides some base classes for implementing a domain driven design in PHP. Helpful tools are provided for usage with Symfony and Doctrine, however you are not required to use those.
Install
Use composer to install the lib from packagist.
composer require fusonic/ddd-extensions
Configuration
services: # This service dispatches the domain events raised on aggregate roots to the given message bus. Fusonic\DDDExtensions\Doctrine\LifecycleListener\DomainEventLifecycleListener: arguments: $bus: '@Symfony\Component\Messenger\MessageBusInterface' tags: # Note: The listener has to be tagged individually for each event - { name: doctrine.event_listener, event: 'postPersist', priority: 500 } - { name: doctrine.event_listener, event: 'postUpdate', priority: 500 } - { name: doctrine.event_listener, event: 'postRemove', priority: 500 } - { name: doctrine.event_listener, event: 'postFlush', priority: 500 } # Optionally configure a ModelDescriber if you are using NelmioApiDocBundle to display EntityId objects in the # generated documentation Fusonic\DDDExtensions\ModelDescriber\EntityIdDescriber: tags: - { name: nelmio_api_doc.model_describer, priority: 1000 } # Optionally configure a normalizer to automatically serialize/deserialize AbstractIntegerId objects Fusonic\DDDExtensions\Normalizer\EntityIdNormalizer: tags: - { name: serializer.normalizer }
Usage and recommendations
For an example, see the examples in the tests.
Value objects
Value objects must extend Fusonic\DDDExtensions\Domain\Model\ValueObject
. Value objects must be immutable.
All the properties that it needs must be set when initiating the object.
The value object can not have any setter properties.
For comparing value objects you must implement the equals
function.
Aggregate roots
Aggregate roots are the entry points to the bounded context. Domain objects that extend Fusonic\DDDExtensions\Domain\Model\AggregateRoot
are aggregate roots. Only the aggregate roots can be created/modified directly outside of the bounded contexts.
All sub-entities are modified/created through the aggregate root.
Domain entities
Domain entities must implement Fusonic\DDDExtensions\Domain\Model\EntityInterface
.
You should not do operations on domain entities (that are not aggregate roots) directly. Everything should go through the aggregate root.
The "id" returned by getId()
can return anything, however it is recommended to create a dedicated "id" class for each domain entity. For example a User
class with a UserId
class.
The EntityId
extended class must implement a __toString
method and a getValue()
method which will return the internal value.
The implementation of the internal
value is up to you. For Doctrine you could use an integer and use the Fusonic\DDDExtensions\Domain\Model\EntityIntegerId
class
as a base class, see this example.
In order to have consistent return types and to avoid null-checks everywhere, you cannot return null. If you
use the AbstractIntegerId
base class, the default internal value will be 0
. To check for this a convenient isDefined()
method is implemented.
public function getId(): UserId { return new UserId($this->id); }
Assertions
The domain layer does not depend on any validation services. Validation logic has to be inside the models themselves.
For assertions in the domain there is a static helper class with common assertion functions.
See: Fusonic\DDDExtensions\Domain\Validation\Assert
.
Domain exceptions
Domain exceptions can only be thrown from within the domain. All domain exceptions must implement
the Fusonic\DDDExtensions\Domain\Exception\DomainExcetionInterface
.
Domain events
Domain objects that extend Fusonic\DomainDrivenDoctrin\Domain\Model\AggregateRoot
can
raise events. Inside the class you can call $this->raise(...)
with an event that implements
Fusonic\DDDExtensions\Domain\Event\DomainEventInterface
.
All raised domain events will be dispatched when Doctrine flush
is called.
The Fusonic\DDDExtensions\Doctrine\LifecycleListener\DomainEventLifecycleListener
handles this.
ORM Mapping
You must not use PHP annotations or attributes for defining your ORM mapping. Mapping should be configured outside of the domain. For Doctrine, you can use XML or PHP mapping.
Value objects as embeddables
Value object mapping can be done using Doctrine embeddables, but only if it is a one-to-one relation. The advantage of embeddables is that you can use the Doctrine query language to query the fields easily.
Value objects as JSON
Another way to map value objects is to define a custom Doctrine type. Extend the Fusonic\DDDExtensions\Doctrine\Type\ValueObjectType
and implement the convertToDatabaseValue
and convertToPHPValue
methods to define the mapping for a value object. The class provides
four helper methods for serialization: serialize
, deserialize
and serializeArray
, deserializeArray
in case you want to store
and array of the value objects (one-to-many relation).
Example here and here.
Inside the database the objects will be stored as json
.
After implementing the custom type you need to register it.
Querying for json data is not possible out-of-the-box in Doctrine, it is however possible using extensions (example).