The Repository interface
Here is the definition of a repository for Eric Evans in his Domain Driven Design book:
A repository represents all objects of a certain type as a conceptual set (usually emulated). It acts like a collection, except with more elaborate querying capability. [...] For each type of object that needs global access, create an object that can provide the illusion of an in-memory collection of all objects of that type.
While I love Doctrine, I really dislike
their repository interface
because it doesn't look and act like a collection.
And that's too bad because Doctrine even provides a very good abstraction for collections
through the doctrine/collection
project. It even supports filtering with criterias over collections and repositories.
I know that Doctrine is not targeted at Domain Driven Design only, but I think having a better repository interface would still benefit the project and the users.
Here is a basic repository interface I tend to use instead:
interface Repository
{
function add($entity);
function remove($entity);
function count();
/**
* @throws NotFoundException
* @return object
*/
function get($id);
/**
* @return array
*/
function find(Criteria $criteria);
function toArray();
}
Of course, you should use this interface as a base and write your own repository interfaces for each aggregate root.
Collection verbs
Why use verbs likeadd
and remove
instead of load
, save
, delete
, persist
, …?
Because those are persistence-related terms and have nothing to do in an interface that is going to be used/extended in the domain layer.
Get versus Find
I really dislike that you can only find
in Doctrine: it will return null if no entity is found.
Most of the time, that is not what you want. You actually don't want to "search and find" the entity, you just want to get it by its id and you assume it exists.
That's why you need to clearly differentiate between find and get:
- method starting with
get
should always return an entity, or throw an exception if not found - method starting with
find
should always return a collection (that could be empty)
Going further: the collection interface
I've said it already, but a repository should behave like a collection. The interface shown above is not completely satisfying yet because it doesn't totally behave like a collection. For example you can't iterate it.
So the best solution is simple: write a sensible collection interface and have the repository extend it.
interface Collection extends Countable, IteratorAggregate, ArrayAccess
{
function add($element);
function remove($element);
function clear();
function contains($element);
function get($key);
function find(Criteria $criteria);
function toArray();
function slice($offset, $length = null);
}
interface Repository extends Collection
{
}
(this is a very simple version, not exhaustive at all)
Now this is much better. You can even type-hint against the collection and accept at the same time collections and repositories!
You can then iterate or filter the collection without having to care what the object really is.
For example, you can write a ProductCriteria
and use it both on the repository and collections of products.
Example: let's say you write a controller to display a product list:
class ProductListController
{
/**
* @var ProductCollection
*/
private $products;
public function __construct(ProductCollection $products)
{
$this->products = $products;
}
public function listAction()
{
return new View('product-list.twig.html', [$this->products]);
}
}
Here your controller is completely reusable. If you give it the ProductRepository
, it can display the whole
product list. If you give it the list of favorite products of the user, it will work too.
Thank you dependency injection!
And the day PHP gets generics, that will be even nicer (Collection<Product>
) but that's a debate for another day!