Domain-Driven Design, part 6 — Doctrine Repository
The repository is a persistent collection that we know from repository article. We expect that we know the cart model from model and implementation articles. But it doesn’t make any harm if we repeat the cart model again.
While talking about the domain design we were thinking in object model all the time. But if we want to use a relational database, we have to think in relations again. So we have a cart entity which has zero or more item entities. Of course, both entities have couple of their own attributes.
The relation between the item and the cart is many-to-one. This relation needs the item to know which cart it is in. But we have a different approach in our model. The cart knows about the item but the item has no idea about the cart. This thinking changes the relation into many-to-many.
This model allows us to use items as a collection in PHP. The original model would force us to pass the cart into the item. It would change the domain model and that is something we want to avoid as long as it is possible.
Doctrine Influence on Object Model
The Doctrine allows us to store pure domain objects, which is great. But still, it has an influence on the implemented model.
All persisted entities must have an identifier, an identifier which is unique in the whole system. This requirement influences the cart item which doesn’t have a system-wide identifier yet. We know that inside the cart the item is identified by the product identifier. But there can be items in different carts that hold the same product identifier. The product doesn’t identify the item in the whole system, so we must find another way to identify the item. Since this requirement is here just because of the Doctrine, we can also let the Doctrine to deal with these artificial identifiers. The Doctrine can generate a unique identifier by itself, and the only thing needed is a mapped field. So the item now contains a new field just for the Doctrine.
Doctrine doesn’t hydrate an object collection as an array, it hydrates the PersistentCollection.
So the type hint in the cart is not right. This is important for working with the collection — an array is passed as a copy but an object is passed as a reference. And what is worse, the persistent collection has so many responsibilities so it would become difficult to understand.
Persistent collection implements the Collection interface and we can use this fact in the code. If we declare that the collection is not the
PersistentCollection but an abstract
Collection, we can initialize it in the constructor with an ArrayCollection. So the type hint is true if the object is new -
ArrayCollection, and it is also true if the object is hydrated by the Doctrine -
The Doctrine repository is an infrastructure for the domain, so it is implemented in the infrastructure layer.
The Doctrine mapping belongs also to the infrastructure as do the repository. Using PHP annotations is not preferred. Mapping should not be part of the domain because we want to have the domain infrastructure-free if possible. It also mixes the PHP domain language with the mapping annotation language. Static PHP mapping is much better, but it is a bit verbose and difficult to read. YAML seems to be a good choice, but it is deprecated in future Doctrine versions. Finally, the XML format seems to be fine and it is also supported. So our final choice is XML.
As we already have a repository test from previous article, it should be possible to write a Doctrine test only by extending the abstract test. We test against a real database so we can catch problems that we face in the production environment. The code below allows us to test against MySQL and PostgreSQL.
Database Connection and Entity Manager
In the beginning, we need a connection and an empty database. This is done by the
It uses a dirty trick to get database credentials — it reads global variables. These variables are defined in the
This is probably the worst part of the whole project, but it allows us to test against a real database.
We use the
EntityManagerFactory to create the
EntityManager. The factory defines the mapping location, forbids proxy generation and returns the
EntityManager. It also uses the
SchemaTool to create a database schema.
In this test, we also implement the
flush method. It flushes the
EntityManager and also cleans it forcing database reading. This method also simulates the persistence wrapper and a new life of the system.
After the test finishes, we have to also close the database connection, it is done in
We can write a Doctrine-specific test if it makes sense. It may happen that we have a trouble with mapping and we want to make sure everything is ok. We check that there isn’t any mess in the database after the cart is removed in the last test.
We have some integration in tests but it is not a part of the main code. The reason is that we have no idea how the real project is integrated. The real project can run on a framework or on a custom integration. Tests are testing only what they are supposed to test. The cart aggregate is unit-tested, the Doctrine repository is system tested. In a real project, we may have also end-to-end tests which prepare the environment, integrate the project and run tests from the entry point to the infrastructure and back. But these tests don’t belong here.
The infrastructure can influence the implemented model. The Doctrine repository is the cart repository implementation with mapping in separated files. Everything is in the infrastructure layer. We can test the Doctrine repository against a real database.
DOCTRINE TEAM. Doctrine 2 ORM 2 documentation [online]. 2018 [2018–03–03]. Available: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/
Are you designing architecture and like DDD approach? Hire me, I can help you — svatasimara.cz