A Repository is a design pattern that solves a common problem: the need to save a domain object and later reconstitute it.
A client provides the ID of the entity it wants to retrieve, and the repository takes the data for the corresponding entity from the database and finally reconstitutes the entire entity object using this data. If the repository can’t find an entity with the provided ID, it will return a null value.
public function save(Order $order): void;
public function ofId(int $orderId): ?Order;
The OrderRepository interface.
Mapping entity data to table columns using an ORM
We need to map the entity data to table columns to write the implementation that saves an entity to the database.
The most common option is to use an ORM.
If, for example, we use Doctrine ORM, we need to add mapping configuration to the entity class and its properties. We can do that using annotations, so Doctrine should be able to save the object’s data in the right table and columns.
use Doctrine\ORM\Mapping as ORM;
final class Order
private int $id;
private string $emailAddress;
private int $quantityOrdered;
private int $pricePerUnitInCents;
Using annotations for mapping configuration.
“How about adding Doctrine mapping annotations to the entities? Does that result in infrastructure code?”
TL;DR - No.Instantiating an entity with mapping annotations doesn’t require any particular setup, and calling any method on it doesn’t need external dependencies to be available. So, our entity should still be considered core code, not infrastructure code.
However, an entity with Doctrine annotations contains technical implementation details (like table, column names, and column types). So when we get to the point where we want to switch databases after all, we will still have to modify this code. This is no reason to move the mapping code outside the entity. In particular, keeping the entity’s properties and mapping code closely together is so convenient.
We can now write a very straightforward implementation of the OrderRepository interface, which uses Doctrine’s EntityManager to persist Order objects.
final class OrderRepositoryUsingDoctrineOrm implements OrderRepository
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
$this−>entityManager = $entityManager;
public function save(Order $order): void
An implementation of OrderRepository using Doctrine ORM.
How is Doctrine able to get the data out of the entity? It uses reflection ⤴️ to reach the object, copy the data from its private properties, and prepare the desired array.
“The rules to use an ORM” by Matthias Noback
1. Only use simple mapping configuration; no table inheritance, “embeddables”, custom types, etc.
2. Stick to one-to-many associations.
3. Reference entities by their ID.
4. Don’t jump from entity to entity using association fields.
These rules have much in common with the rules for “Effective Aggregate Design” described by Vaughn Vernon ⤴️
“Data Mapper vs Active Record”
When using the Repository design pattern, we are also following the Data Mapper design pattern. This means that we have an object, and when we want to store it, we give it to a repository, which takes out the data and stores it in the database.
A common alternative for storing entities is the Active Record design pattern. Here, the entity will be able to load itself from the database, and it can save and delete itself as well. This extra functionality is usually achieved by extending the entity class from a class provided by the framework.
This may look very convenient, but there are some downsides from a design perspective:
- By inheriting a lot of infrastructure code, we lose the isolation we need for proper unit testing of an entity.
- Active Record frameworks usually require a lot of custom code inside the entities to make everything work well. This code is specific to the framework, making your domain model directly coupled to and only functional in the presence of that framework.
- Clients of the entity can do many more things with the object than they most likely should be allowed to do.