Question:
I just can't find any explanatory documentation for this pattern. We need at least one complete example of how to implement it.
Answer:
Took the description from one of the sources below:
"A repository is usually used as a data store, often for security or safety reasons" – Wikipedia.
This is how Wikipedia describes repositories. And it so happened that, unlike other various definitions that we come across, this fits perfectly. A repository embodies the concept of a collection store for a particular entity type.
Repository as a collection
Probably the most important property of repositories is that they represent a collection of entities. They are not database storage or cache or the like. Repositories are collections. How you use these collections is just an implementation detail.
I want to clarify a little at this stage. A repository is a collection, a collection that contains entities that can be filtered and returned in some way depending on the requirements of your application. How exactly they contain these entities is an IMPLEMENTATION DETAIL.
In the PHP world, we use a Request / Response loop, followed by the death of the PHP process. Everything that is not stored externally is destroyed forever in this case. Nowadays, not all platforms work on this principle.
I find it a good thought experiment to understand repositories to imagine that your application is always running and that objects always remain in memory. We are not worried about critical issues in this experiment. Imagine you have a single repository for the Member entity – MemberRepository.
Then you create a new Member and add it to the repository. Later, you query the repository for all members and get back the collection that contains the Member you added. You might want to get a separate Member by ID, you can do that too. It is easy to imagine that within the repository these Member objects are stored as an array, or better as a collection of objects.
In simple terms, a repository is a special type of managing collection that you use over and over to store and retrieve entities.
Interacting with the repository
Imagine that you have created a Member entity. You are satisfied with the Member object, then when the request ends, the Member object disappears. The participant then tries to log in to your application and cannot. Obviously, we need to make the Member available in other parts of our application.
<?php
$member = Member::register($email, $password);
$memberRepository->save($member);
Then we wanted to get a Member later, like this:
<?php
$member = $memberRepository->findByEmail($email);
// или
$members = $memberRepository->getAll();
We can now store Member objects in one part of our application and then retrieve them in another part.
Should the repository create entities?
You could be doing something like this:
<?php
$member = $memberRepository->create($email, $password);
I've seen people make arguments for this approach. But I highly discourage it.
Once again, repositories are collections. I'm not sure if the collection should also be a factory. I've heard arguments like .. they store state, why not also store entity creation?
In my mind, this is an anti-pattern. Why not let the Member have their own ideas about their creation, or why not have a factory specifically designed to allow for the creation of more complex objects.
If we use our repositories as simple collections, then we give them one responsibility. I don't want collection classes that are also factories.
What is the advantage of repositories?
The main advantage of repositories is the abstract storage mechanism for the managing collection of entities.
When we create the MemberRepository interface, we allow any number of its concrete implementations to exist:
<?php
interface MemberRepository {
public function save(Member $member);
public function getAll();
public function findById(MemberId $memberId);
}
First implementation:
<?php
class ArrayMemberRepository implements MemberRepository {
private $members = [];
public function save(Member $member) {
$this->members[(string)$member->getId()] = $member;
}
public function getAll() {
return $this->members;
}
public function findById(MemberId $memberId) {
if (isset($this->members[(string)$memberId])) {
return $this->members[(string)$memberId];
}
}
}
And another one:
<?php
class RedisMemberRepository implements MemberRepository {
public function save(Member $member) {
// ...
}
// you get the point
}
If we go this way, then our application knows only the abstract concept of MemberRepository and our use of this repository can be divided into several implementations. It's quite flexible.
Do the repositories belong to the domain layer or the application layer?
This is an interesting question. First, let's define the application layer as a layered architecture that is responsible for implementing specific details of the application, such as working with the database, knowledge about the data transfer protocol (sending email, interacting with the API), etc.
Let's define a domain layer as a layered architecture that is responsible for storing business rules and business logic.
Working with these definitions, which one does our repository fit into?
Let's take a look at our example from the code above:
<?php
class ArrayMemberRepository implements MemberRepository {
private $members = [];
public function save(Member $member) {
$this->members[(string) $member->getId()] = $member;
}
public function getAll() {
return $this->members;
}
public function findById(MemberId $memberId) {
if (isset($this->members[(string)$memberId])) {
return $this->members[(string)$memberId];
}
}
}
In this example, I see a lot of implementation details. These implementation details are clearly application-specific.
Let's remove all the implementation details from this class …
<?php
class ArrayMemberRepository implements MemberRepository {
public function save(Member $member) {
}
public function getAll() {
}
public function findById(MemberId $memberId) {
}
}
Hmm, that sounds familiar enough. Where was it?
Maybe this reminds you of this?
<?php
interface MemberRepository {
public function save(Member $member);
public function getAll();
public function findById(MemberId $memberId);
}
This means that the interface is on the border of the layers. The interface itself can contain domain-specific concepts, but the interface implementation must not.
The repository interface belongs to the domain layer. The interface implementation belongs to the application layer. This means that we can write hints to our repositories in the domain layer without even touching the application layer.
Freedom to choose a data warehouse
You've probably heard something like this in conversations about the concepts of object-oriented programming – "and you are completely unlimited in changing one data store to another later."
I have come to the conclusion that this is not entirely true .. it is a very weak argument. The biggest problem with this explanation is that it leads to the question, "Do you really want to change the data store?" I don't want the answer to this question to determine whether or not the repository pattern should be used.
Any well-designed object-oriented application automatically comes with this type of advantage. The central concept of object-orientation is encapsulation. You can show the API and hide the implementation.
The truth is, you probably don't want to swap one ORM for another. But you will at least have a good way to implement it. However, changing repository implementations is great for testing.
Testability with the repository pattern
Guess what? This is delicious. Let's say you have an object that contains something similar to the registration of participants:
<?php
class RegisterMemberHandler {
private $members;
public function __construct(MemberRepository $members) {
$this->members = $members;
}
public function handle(RegisterMember $command) {
$member = Member::register($command->email, $command->password);
$this->members->save($member);
}
}
During normal operations, you can inject the MemberRepository implementation – DoctrineMemberRepository. However, during testing, you can replace it with ArrayMemberRepository. They both implement the interface
A simplified version of the test might look like this ..
<?php
$repo = new ArrayMemberRepository;
$handler = new RegisterMemberHandler($repo);
$request = $this->createRequest(['email' => 'bob@bob.com', 'password' => 'angelofdestruction']);
$handler->handle(RegisterMember::usingForm($request));
AssertCount(1, $repo->findByEmail('bob@bob.com'));
In this example, we are testing the handler. We do not need to test that the repository stores data in the database or elsewhere. We need to test the behavior of this object by asking the Member class for a new member based on the command arguments, then pushing it into the repository.
Collection-Oriented or State-Oriented
In Implementing DDD, Vaughn Vernon distinguishes between state-based and collection-based repositories. In short, the idea behind collection-based repositories is that data is treated like an in-memory storage of an array. But in a state-oriented repository, it all comes down to the fact that the data is stored deeper. This comes from their names.
I have no opinion right now on which one to use. However, I am careful with this. My current focus is on repositories as collections of objects, with the same responsibility that any other collection of objects can have.
Outcome
I believe that …
.. it's important to give the repositories the only job – to function as a collection of objects
.. we shouldn't use repositories to create entities
.. we must easily be able to change the technology with which we receive data, as this brings a lot of benefits that are difficult to underestimate
In the future, I want to write a few more articles about repositories, for example, how to cache the results of a repository using the decorator pattern, making queries using the criterion pattern, the role of the repository in building a single implementation case, and implementing a group of operations with a large number of objects.
List of found sources:
- https://habrahabr.ru/post/248505/
- http://metanit.com/sharp/articles/mvc/11.php
- https://gist.github.com/Rhincodon/5e504b3d377c0c1985bb
- http://citforum.ru/SE/project/pattern/p_3.shtml