Collection-Oriented Repositories

Repositories mimic a collection by implementing their common interface characteristics. As a collection, a Repository shouldn't leak any intentions of persistence behavior, such as the notion of saving to a store.

The underlying persistence mechanism has to support this need. You shouldn't be required to handle changes to the objects over their lifetime. The collection references the most recent changes to the object, meaning that upon each access, you get the latest object state.

Repositories implement a concrete collection type, the Set. A Set is a data structure with an invariant that doesn't contain duplicate entries. If you try to add an element that's already present to a Set, it won't be added. This is useful in our use case, as each Aggregate has a unique identity that's associated with the Root Entity.

Consider, for example, that we have the following Domain Model:

namespace DomainModel;

class Post
{
const EXPIRE_EDIT_TIME = 120; // seconds

private $id;
private $body;
private $createdAt;

public function __construct(PostId $anId, Body $aBody)
{
$this->id = $anId;
$this->body = $aBody;
$this->createdAt = new DateTimeImmutable();
}

public function editBody(Body $aNewBody)
{
if($this->editExpired()) {
throw new RuntimeException('Edit time expired');
}

$this->body = $aNewBody;
}

private function editExpired()
{
$expiringTime= $this->createdAt->getTimestamp() +
self::EXPIRE_EDIT_TIME;

return $expiringTime < time();
}

public function id()
{
return $this->id;
}

public function body()
{
return $this->body;
}

public function createdAt()
{
return $this->createdAt;
}
}

class Body
{
const MIN_LENGTH = 3;
const MAX_LENGTH = 250;

private $content;

public function __construct($content)
{
$this->setContent(trim($content));
}

private function setContent($content)
{
$this->assertNotEmpty($content);
$this->assertFitsLength($content);

$this->content = $content;
}

private function assertNotEmpty($content)
{
if(empty($content)) {
throw new DomainException('Empty body');
}
}

private function assertFitsLength($content)
{
if(strlen($content) < self::MIN_LENGTH) {
throw new DomainException('Body is too short');
}

if(strlen($content) > self::MAX_LENGTH) {
throw new DomainException('Body is too long');
}
}

public function content()
{
return $this->content;
}
}

class PostId
{
private $id;

public function __construct($id = null)
{
$this->id = $id ?: uniqid();
}

public function id()
{
return $this->id;
}

public function equals(PostId $anId)
{
return $this->id === $anId->id();
}
}

If we wanted to persist this Post Entity, a simple in-memory Post Repository could be created like this:

class SimplePostRepository 
{
private $post = [];

public add(Post $aPost)
{
$this->posts[(string) $aPost->id()] = $aPost;
}

public function postOfId(PostId $anId)
{
if (isset($this->posts[(string) $anId])) {
return $this->posts[(string) $anId];
}

return null;
}
}

And, as you would expect, it's handled as a collection:

$id = new PostId();
$repository = new SimplePostRepository();
$repository->add(new Post($id, 'Random content'));

// later ...
$post = $repository->postOfId($id);
$post->editBody('Updated content');

// even later ...
$post = $repository->postOfId($id);
assert('Updated content' === $post->body());

As you can see, from the collection's point of view, there's no need for a save method in the Repository. Changes affecting the object are correctly handled by the underlying persistence layer. Collection-oriented Repositories are the ones that don't need to add an Aggregate that was persisted before. This mainly happens with the Repositories that are memory based, but we also have ways to do this with the Persisted-Oriented Repositories. We'll look at this in a moment; additionally, we'll cover this more in depth in the Chapter 11, Application.

The first step to design a Repository is to define a collection-like interface for it. The interface needs to define the usual collection methods, like so:

interface PostRepository 
{
public function add(Post $aPost);
public function addAll(array $posts);
public function remove(Post $aPost); 
public function removeAll(array $posts);
// ...
}

For implementing such an interface, you could also use an abstract class. In general, when we talk about an interface, we refer to the general concept and not just the specific PHP interface. To keep your design simple, don't add methods you don't need; the Repository interface definition and its corresponding Aggregate should be placed in the same Module.

Sometimes remove doesn't physically delete the Aggregate from the database. This strategy - where the Aggregate has a status field that's updated to a deleted value - is known as a soft delete. Why is this approach interesting? It can be interesting for auditing changes and performance. In those cases, you can instead mark the Aggregate as disabled or logically removed. The interface could be updated accordingly by removing the removal methods or providing disable behavior in the Repository.

Another important aspect of Repositories are the finder methods, like the following:

interface PostRepository 
{
// ...


/**
* @return Post
*/
public function postOfId(PostId $anId);

/**
* @return Post[]
*/
public function latestPosts(DateTimeImmutable $sinceADate);
}

As we suggested in Chapter 4, Entities, we prefer Application-Generated Identities. The best place to generate a new Identity for an Aggregate is its Repository. So to retrieve the globally unique ID for a Post, a logical place to include it is in PostRepository:

interface PostRepository
{
// ...

/**
* @return PostId
*/
public function nextIdentity();
}

The code responsible for building up each Post instance calls nextIdentity to get a unique identifier, PostId:

$post = newPost($postRepository->nextIdentity(), $body);

Some developers favor placing the implementation close to the interface definition as a subpackage of the Module. However, because we want a clear Separation of Concerns, we recommend instead placing it inside the Infrastructure layer.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset