Doing some Preliminary Groundwork
The process of implementing a repository can be pretty complex, because it actually hides all the nuts and bolts of injecting and handling the Data Mappers behind a simplified collection-like API, which in turn also inject some kind of persistence adapter, and so on. This successive injection of dependencies, coupled to the hiding of extensive logic, explains why a repository is often considered a plain Façade, even when some opinions currently diverge from that concept. In either case, the first step that we should take to get a functional repository up and running is create a basic Domain Model. The one that I plan to use here will be charged with the task of modelling generic users, and its bare-bones structure looks like this:<?php
namespace Model;
interface UserInterface
{
public function setId($id);
public function getId();
public function setName($name);
public function getName();
public function setEmail($email);
public function getEmail();
public function setRole($role);
public function getRole();
}
<?php
namespace Model;
class User implements UserInterface
{
const ADMINISTRATOR_ROLE = "Administrator";
const GUEST_ROLE = "Guest";
protected $id;
protected $name;
protected $email;
protected $role;
public function __construct($name, $email, $role = self::GUEST_ROLE) {
$this->setName($name);
$this->setEmail($email);
$this->setRole($role);
}
public function setId($id) {
if ($this->id !== null) {
throw new BadMethodCallException(
"The ID for this user has been set already.");
}
if (!is_int($id) || $id < 1) {
throw new InvalidArgumentException(
"The user ID is invalid.");
}
$this->id = $id;
return $this;
}
public function getId() {
return $this->id;
}
public function setName($name) {
if (strlen($name) < 2 || strlen($name) > 30) {
throw new InvalidArgumentException(
"The user name is invalid.");
}
$this->name = htmlspecialchars(trim($name), ENT_QUOTES);
return $this;
}
public function getName() {
return $this->name;
}
public function setEmail($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
"The user email is invalid.");
}
$this->email = $email;
return $this;
}
public function getEmail() {
return $this->email;
}
public function setRole($role) {
if ($role !== self::ADMINISTRATOR_ROLE
&& $role !== self::GUEST_ROLE) {
throw new InvalidArgumentException(
"The user role is invalid.");
}
$this->role = $role;
return $this;
}
public function getRole() {
return $this->role;
}
}
In this case in particular, the Domain Model is a pretty skeletal layer, barely above a plain data holder capable of validating itself, which defines through just a segregated interface and a banal implementer the data and behavior of some fictional users. To keep things uncluttered and easy to understand, I’m going to keep the model that thin.
With the model already going about its business in relaxed isolation, let’s make it a little bit richer by adding to it an additional class, responsible for handling collections of user objects. This “addendum” component is just a classic array wrapper implementing the Countable
, ArrayAccess
and IteratorAggregate
SPL interfaces:
<?php
namespace ModelCollection;
use MapperUserCollectionInterface,
ModelUserInterface;
class UserCollection implements UserCollectionInterface
{
protected $users = array();
public function add(UserInterface $user) {
$this->offsetSet($user);
}
public function remove(UserInterface $user) {
$this->offsetUnset($user);
}
public function get($key) {
return $this->offsetGet($key);
}
public function exists($key) {
return $this->offsetExists($key);
}
public function clear() {
$this->users = array();
}
public function toArray() {
return $this->users;
}
public function count() {
return count($this->users);
}
public function offsetSet($key, $value) {
if (!$value instanceof UserInterface) {
throw new InvalidArgumentException(
"Could not add the user to the collection.");
}
if (!isset($key)) {
$this->users[] = $value;
}
else {
$this->users[$key] = $value;
}
}
public function offsetUnset($key) {
if ($key instanceof UserInterface) {
$this->users = array_filter($this->users,
function ($v) use ($key) {
return $v !== $key;
});
}
else if (isset($this->users[$key])) {
unset($this->users[$key]);
}
}
public function offsetGet($key) {
if (isset($this->users[$key])) {
return $this->users[$key];
}
}
public function offsetExists($key) {
return ($key instanceof UserInterface)
? array_search($key, $this->users)
: isset($this->users[$key]);
}
public function getIterator() {
return new ArrayIterator($this->users);
}
}
In fact, placing this array collection within the model’s boundaries is entirely optional, as pretty much the same results can be yielded by using a plain array. In this case however, by relying on a standalone collection class makes easier to access sets of user objects fetched from the database through an object-oriented API.
In addition, considering that the Domain Model must be entirely ignorant about the underlying storage set down in the infrastructure, the next logical step that we should take is implement a mapping layer that keeps it nicely separated from the database. Here are the elements that compose this tier:
<?php
namespace Mapper;
use ModelUserInterface;
interface UserCollectionInterface extends Countable, ArrayAccess, IteratorAggregate
{
public function add(UserInterface $user);
public function remove(UserInterface $user);
public function get($key);
public function exists($key);
public function clear();
public function toArray();
}
<?php
namespace Mapper;
use ModelRepositoryUserMapperInterface,
ModelUser;
class UserMapper implements UserMapperInterface
{
protected $entityTable = "users";
protected $collection;
public function __construct(DatabaseAdapterInterface $adapter, UserCollectionInterface $collection) {
$this->adapter = $adapter;
$this->collection = $collection;
}
public function fetchById($id) {
$this->adapter->select($this->entityTable,
array("id" => $id));
if (!$row = $this->adapter->fetch()) {
return null;
}
return $this->createUser($row);
}
public function fetchAll(array $conditions = array()) {
$this->adapter->select($this->entityTable, $conditions);
$rows = $this->adapter->fetchAll();
return $this->createUserCollection($rows);
}
protected function createUser(array $row) {
$user = new User($row["name"], $row["email"],
$row["role"]);
$user->setId($row["id"]);
return $user;
}
protected function createUserCollection(array $rows) {
$this->collection->clear();
if ($rows) {
foreach ($rows as $row) {
$this->collection[] = $this->createUser($row);
}
}
return $this->collection;
}
}
Out of the box, the batch of tasks performed by UserMapper
are fairly straightforward, limited to just exposing a couple of generic finders which are charged with pulling in users from the database and reconstructing the corresponding entities through the createUser()
method. Moreover, if you’ve already sunk your teeth into a few mappers before, or even written your own mapping masterpieces, surely the above should be pretty easy to understand. Quite possibly the only subtle detail worth stressing is that the UserCollectionInterface
has been placed into the mapping layer, rather than in the model’s. I decided to do so pretty much deliberately in this case, as that way the abstraction (the protocol) that the user collection depends on is explicitly declared and owned by the higher-level UserMapper
, in consonance with the guidelines promoted by the Dependency Inversion Principle.
With the mapper already set, we could just consume it right out of the box and pull in a few user objects from storage to get the model hydrated in a snap. While at first glance this would seem to be the right path to pick up indeed, in fact we’d be unnecessarily polluting application logic with infrastructure, as the mapper is effectively a part of it. What if down the road it becomes necessary to query user entities according to more distilled, domain-specific conditions, other than just the blanket ones exposed by the mapper’s finders?
In such cases, there would be a real need to place an additional layer on top of the mapping one, which not only would provide a higher level of data access, but it would carry chunks of query logic through one single point. This is, in the last instance, the wealth of benefits we’d expect to get from a repository.
Implementing a User Repository
In production, repositories can implement under their surface pretty much every thing one can think of in order to expose onto the model the illusion of an in-memory collection of aggregate roots. Nevertheless, in this case we just can’t be so naive and expect to enjoy of such expensive luxuries for free, since the repository that we’ll be building will be a pretty contrived structure, responsible for fetching users from the database:<?php
namespace ModelRepository;
interface UserMapperInterface
{
public function fetchById($id);
public function fetchAll(array $conditions = array());
}
<?php
namespace ModelRepository;
interface UserRepositoryInterface
{
public function fetchById($id);
public function fetchByName($name);
public function fetchbyEmail($email);
public function fetchByRole($role);
}
<?php
namespace ModelRepository;
class UserRepository implements UserRepositoryInterface
{
protected $userMapper;
public function __construct(UserMapperInterface $userMapper) {
$this->userMapper = $userMapper;
}
public function fetchById($id) {
return $this->userMapper->fetchById($id);
}
public function fetchByName($name) {
return $this->fetch(array("name" => $name));
}
public function fetchByEmail($email) {
return $this->fetch(array("email" => $email));
}
public function fetchByRole($role) {
return $this->fetch(array("role" => $role));
}
protected function fetch(array $conditions) {
return $this->userMapper->fetchAll($conditions);
}
}
Although sitting on top of a somewhat lightweight structure, the implementation of UserRepository
is pretty intuitive considering that its API allows it to pull in collections of user objects from storage that conform to refined predicates which are closely related to the model’s language. Furthermore, in its current state, the repository exposes just some simplistic finders to client code, which in turn exploit the functionality of the data mapper to gain access to the storage.
In a more realistic environment, a repository should have the capability of persisting aggregate roots as well. If you’re in the mood to pitch an insert()
method or something else along that line to UserRepository
, feel free to do so.
In either case, one effective manner to catch the actual advantages of using a repository is by example.
<?php
use LibraryLoaderAutoloader,
LibraryDatabasePdoAdapter,
MapperUserMapper,
ModelCollectionUserCollection,
ModelRepositoryUserRepository;
require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader;
$autoloader->register();
$adapter = new PdoAdapter("mysql:dbname=users", "myfancyusername", "mysecretpassword");
$userRepository = new UserRepository(new UserMapper($adapter,
new UserCollection()));
$users = $userRepository->fetchByName("Rachel");
foreach ($users as $user) {
echo $user->getName() . " " . $user->getEmail() . "<br>";
}
$users = $userRepository->fetchByEmail("username@domain.com");
foreach ($users as $user) {
echo $user->getName() . " " . $user->getEmail() . "<br>";
}
$administrators = $userRepository->fetchByRole("administrator");
foreach ($administrators as $administrator) {
echo $administrator->getName() . " " .
$administrator->getEmail() . "<br>";
}
$guests = $userRepository->fetchByRole("guest");
foreach ($guests as $guest) {
echo $guest->getName() . " " . $guest->getEmail() . "<br>";
}
As noted previously, the repository effectively interchanges business terminology with client code (the so-called Ubiquitous Language coined by Eric Evans in his book Domain Driven Design), rather than a lower-level, technical one. Unlike the ambiguity present in the data mapper’s finders, the repository’s methods on the other hand describe themselves in terms of “name,” “email,” and “role,” which are certainly a part of the attributes that model user entities.
This distilled higher level of data abstraction, along with the set of full-fledged capabilities required when it comes to encapsulating query logic in complex systems, are certainly among the most compelling reasons which make using repositories appealing in multi-tiered design. Of course, most of the times there’s an implicit trade-off between getting those benefits up front and going through the hassle of deploying an additional abstraction layer, which in more modest applications may be bloated overkill.
Closing Thoughts
Being one of the central concepts of Domain Driven Design, repositories can be found in applications written in several other languages, like Java and C#, just to name a few. In PHP however, they’re still relatively unknown, just making their first shy steps in the world. Despite this, there are some well-trusted frameworks, such as FLOW3 and of course Doctrine 2.x, which will help you embrace the DDD paradigm. As with any development methodology out there, you don’t have to use repositories in your applications or even smash them unnecessarily with the pile of concepts sitting behind DDD. Just use common sense and pick them up only when you think they’re going to fit your needs. It’s really just that simple. Image via Chance Agrella / Freerangestock.comFrequently Asked Questions (FAQs) about Handling Collections of Aggregate Roots
What is an Aggregate Root in Domain-Driven Design?
In Domain-Driven Design (DDD), an Aggregate Root is a cluster of associated objects that are treated as a single unit. These objects are bound together by a root entity, also known as the Aggregate Root. The Aggregate Root maintains the consistency of changes being made within the aggregate by forbidding external objects from holding references to its members.
How does an Aggregate Root differ from regular entities?
The main difference between an Aggregate Root and regular entities lies in their responsibilities. While regular entities encapsulate behavior and state, an Aggregate Root additionally ensures the integrity of the entire aggregate by controlling access to its members. It’s the only member of the aggregate that outside objects are allowed to hold references to.
How do I identify an Aggregate Root in my domain model?
Identifying an Aggregate Root requires a deep understanding of the business domain. It’s typically a high-level entity that has a global identity and encapsulates other entities and value objects. For example, in an e-commerce domain, an Order could be an Aggregate Root that encapsulates line items and shipping information.
How should I handle collections of Aggregate Roots?
Handling collections of Aggregate Roots can be challenging. It’s important to remember that each Aggregate Root is a consistency boundary, so changes to one should not affect others. Therefore, when dealing with collections, it’s often best to load and persist each Aggregate Root separately to maintain consistency.
Can an Aggregate Root reference another Aggregate Root?
Yes, an Aggregate Root can reference another Aggregate Root, but it should do so by identity only. This means it should not hold a direct reference to the other Aggregate Root object, but rather its ID. This helps to maintain the consistency boundary of each Aggregate Root.
How does an Aggregate Root relate to a Repository in DDD?
In DDD, a Repository provides methods to retrieve and store Aggregate Roots. It abstracts the underlying storage mechanism, allowing the domain model to remain ignorant of the details of data persistence. Each Aggregate Root typically has its own Repository.
What is the role of an Aggregate Root in enforcing business rules?
An Aggregate Root plays a crucial role in enforcing business rules. It ensures that all changes to the aggregate leave it in a valid state. This means that any business rule that spans multiple entities or value objects should be enforced by the Aggregate Root.
How does an Aggregate Root contribute to reducing complexity in a domain model?
By acting as a consistency boundary and controlling access to its members, an Aggregate Root helps to reduce complexity in a domain model. It simplifies the model by providing a single point of interaction for each aggregate, making it easier to reason about the system.
Can an Aggregate Root be part of more than one aggregate?
No, an Aggregate Root should not be part of more than one aggregate. This would violate the consistency boundary of the aggregates and could lead to inconsistencies in the domain model.
How should I handle concurrency issues with Aggregate Roots?
Concurrency issues with Aggregate Roots can be handled using various strategies, such as optimistic locking or pessimistic locking. The choice of strategy depends on the specific requirements of your application and the nature of the concurrency issues you are facing.
Alejandro Gervasio is a senior System Analyst from Argentina who has been involved in software development since the mid-80's. He has more than 12 years of experience in PHP development, 10 years in Java Programming, Object-Oriented Design, and most of the client-side technologies available out there.