I have users and groups. Each user can have any number of groups. I want to show user groups in paginated form, n groups at a time. I know how to implement an ordinary pagination, however I don't know how to integrate it into my Domain Driven Design (and without leading to code duplication later). I would like it to work something like this:
$adapter = new DatabaseAdapter(...);
$userRepository = new UserRepository($adapter);
$user = $userRepository->fetchById(1);
$groups = $user->getGroups()->getRange($offset, $limit);
and the same for other domain entities:
$projects = $user->getProjects()->getRange($offset, $limit);
...
Simplified, my code looks like this:
class Group
{
private $_id;
private $_name;
public function __construct($id, $name) {
$this->setId($id);
$this->setName($name);
}
public function setId() {
$this->_id = $id;
}
public function getId() {
return $this->_id;
}
public function setName($name) {
$this->_name = $name
}
public function getName() {
return $this->_name;
}
}
class Groups
{
private $_elements = array();
public function __construct(array $groups) {
foreach($groups as $group) {
if(!($group instanceof Group)) {
throw new Exception();
}
$this->_elements[] = $group;
}
}
public function toArray() {
return $this->_elements;
}
}
class GroupMapper
{
private $_adapter;
public function __construct(DatabaseAdapterInterface $adapter) {
$this->_adapter = adapter;
}
public function fetchById($id) {
$row = $this->_adapter->select(...)->fetch();
return $this->createEntity($row);
}
public function fetchAll() {
$rows = $this->_adapter->select(...)->fetchAll();
return $this->createEntityCollection($rows);
}
private function createEntityCollection(array $rows) {
$collection = array();
foreach($rows as $row) {
$collection[] = $this->createEntity($row);
}
return $collection;
}
private function createEntity(array $row) {
return new Group($row['id'], $row['name']);
}
}
class User
{
private $_id;
private $_name;
private $_groups;
public function getId() {
return $this->_id;
}
public function setName($name) {
$this->_name = $name;
}
public function getName() {
return $this->_name;
}
public function setGroups($groups) {
$this->_groups = $groups;
}
public function getGroups() {
return $this->_grous;
}
}
class UserRepository
{
private $_userMapper = null;
private $_groupMapper = null;
public function __construct(DatabaseAdapterInterface $adapter) {
$this->_groupMapper = new GroupMapper($adapter);
$this->_userMapper = new UserMapper($adapter);
}
public function fetchById($id) {
$user = $this->_userMapper->fetchById($id);
if($user) {
$groups = $this->_groupMapper->fetchAllByUser($id);
$user->setGroups($groups);
}
}
public function fetchAll() {
...
}
}
Thank you for your help!
In DDD this is usually implemented by providing paginated query methods directly on the repository. If a group is strictly a child entity of the User aggregate, then just add a query method to the user repository. If a group is its own aggregate, add that method to the group repository. Also, this is a strictly read-only scenario that is used, probably exclusively, for a UI so its not part of the core domain - it is more of a technical concern.
The alternative is to use an ORM that can inject a collection proxy which executes commands against the database behind the scenes. These approaches are usually more trouble than they're worth and make the domain code more difficult to reason about and maintain.
My take on this is to simply not query your domain model. By querying your domain you will immediately run into issues. Once you realise that you do not always need all the, say, Order
data or you do not always need to load the OrderLine
instances for an Order
then you know you have entered that minefield :)
It may seem strange at first but just as you have a UserRepository
for getting to your domain model you could have UserQuery
to get to your read data. You could have a look at CQRS (Command/Query Responsibility Segregation) but to start off with simple queries directed at your use-cases should suffice.
You should quickly realize that some denormalized data could help at which point you could start introducing CQRS.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With