The domain represents the solution to a business problem. It includes all the code necessary to implement and solve that problem; without relying too heavily on third party or framework code. It should be (ideally) framework agnostic and be portable to other frameworks if they prove to be a better fit with a minimum of modifications to the core domain classes. i.e. you do not couple to a framework validator or service container and avoid injecting implementations but use interfaces instead.
The domain is typically discovered during the project setup with discussions with the main stakeholders and domain experts - the people who really know and understand how the business operates. That information is then used to create the software solution. The most important aspect of this is the language that is discovered that allows all people to effectively communicate and know what is meant by specific terms. The language is not set in stone and changes over time as knowledge is gained or the processes are improved. It is important to keep these changes up-to-date and this includes the code itself.
This project suggests and has the following folder layout for the domain:
These are suggestions and you are free to change this up if you wish.
This project is centred around a Domain Driven Design approach, with Doctrine providing persistence for the main domain objects. These models are located in:
src/Domain/Models. All domain models should be located here, including enumerations, value objects, and other data centric models. Unlike standard Symfony projects, models should not contain Doctrine mapping annotations. Add these to the
config/mappings folder in a separate folder (default is
Your models should focus on the domain "state" and how various actions should be applied to it. This means enforcing valid state changes i.e.: you do not need getters and setters. In fact you should avoid adding these as the role of the models is to manage the state and not provide an API to query that state. Essentially your models represent the write operations. In many cases these will use value-objects and enumerables to ensure valid data is passed to the domain at all times. When using simple scalars, strict-types should be enabled and all scalar type hints used.
Within your domain models there will be some that are key and are accessed externally. These are likely to be your aggregate roots. Each aggregate root should raise appropriate domain events after each critical state transition. A doctrine listener is pre-enabled to listen for and propagate the domain events to the pre-configured RabbitMQ fan-out exchange (note that at the time of writing php-amqp is not yet available for PHP 8). Examples of aggregate roots may include User, Account, Order etc. however it will depend on your domain.
In general your domain models will follow the business concepts and use terminology that is familiar to the business. For example: if creating a service for the sales team, and they work with "leads" then your domain should have a "Lead" model and it should have whatever properties they consider to be important. The sales team should be able to look at the code and at least grasp the names and concepts that it expresses.
Services should contain classes that interact with the domain or provide additional support to the code domain models e.g.: transformations, or translations between data types / formats. Repositories are part of the domain services. A key idea though: is that the domain services are not dependent on framework code. They are standalone, and encapsulated - just like the models.
For example a currency converter could be a domain service; or an authenticator that checks if an object is accessible by another object based on domain rules (not framework rules).
Each aggregate root should have a Repository service defined for it. This should be an interface that then receives a Persistence implementation. The interface should be kept as simple as possible, typically:
- find(Uuid $id): Object
- store(Object $object): bool
- destroy(Object $object): bool
The interface should be coded to a specific object type. Under the hood this may use Doctrine ObjectManager to persist and delete objects.
Note that it is not necessary to call
->flush() as a command bus should be used that includes DB transaction wrapping. However: if you do need to persist data outside of the commands, then you would need to either manage your EntityManager directly, or add the flush call to the repository.