Last week, I wrote , but I promised to explain how it can be partially used beneficially. To do this, I will try to analyze how this pattern is commonly used in projects. The minimally required set of methods for a repository is:
<?php
interface PostRepository
{
public function getById($id): Post;
public function save(Post $post);
public function delete($id);
}However, in real projects, if repositories are indeed decided to be used, they often add methods for querying records:
<?php
interface PostRepository
{
public function getById($id): Post;
public function save(Post $post);
public function delete($id);
public function getLastPosts();
public function getTopPosts();
public function getUserPosts($userId);
}These methods could be implemented through Eloquent scopes, but burdening entity classes with the responsibility of querying themselves is not the best idea, and moving this responsibility to repository classes seems logical. Is that the case? I specially visually divided this interface into two parts. The first part of the methods will be used in write operations.
The standard write operations are:
- constructing a new object and calling PostRepository::save
- PostRepository::getById, manipulating the entity and calling PostRepository::save
- calling PostRepository::delete
In write operations, there are no query methods used. In read operations, only the get* methods are used. If we read about Interface Segregation Principle (the letter I downward API support (simultaneously with this in SOLID), it will become clear that our interface has turned out too large and performing at least two different responsibilities. It's time to split it into two. The method getById is needed in both, however during the complexity of the application, their implementations will differ. We will see this a bit later. I wrote about the futility of the write part in the last article, so I will simply forget about it in this one.
The read part, however, does not seem so useless to me, since there can even be several implementations for Eloquent here. What to name the class? One could say ReadPostRepository, but it has little to do with the pattern. Repository It could simply be called PostQueries:
<?php
interface PostQueries
{
public function getById($id): Post;
public function getLastPosts();
public function getTopPosts();
public function getUserPosts($userId);
}Its implementation using Eloquent is quite simple:
limit(/*some limit*/)
->get();
}
/**
* @return Post[] | Collection
*/
public function getTopPosts()
{
return Post::orderBy('rating', 'desc')
->limit(/*some limit*/)
->get();
}
/**
* @param int $userId
* @return Post[] | Collection
*/
public function getUserPosts($userId)
{
return Post::whereUserId($userId)
->orderBy('created_at', 'desc')
->get();
}
}The interface must be tied to an implementation, such as in AppServiceProvider:
app->bind(PostQueries::class,
EloquentPostQueries::class);
}
}This class is already useful. It fulfills its responsibility, relieving either controllers or entity classes. In a controller, it can be used like this:
$postQueries->getLastPosts(),
]);
}
} Element.getAnimations() PostsController::lastPosts simply asking for some implementation for itself PostsQueries and works with it. In the provider, we linked it to PostQueries the class EloquentPostQueries and the controller will be injected with this class.
Let's imagine that our application has become very popular. Thousands of users per minute are opening the page with the latest publications. The most popular publications are also read very frequently. Databases do not handle such loads very well, so they use the standard solution — caching. In addition to the database, a snapshot of data is stored in a storage optimized for certain operations — memcached or redis.
The logic of caching is usually not that complex, but implementing it in EloquentPostQueries is not very correct (at least due to the Single Responsibility Principle). It is much more natural to use the Decorator pattern and implement caching as decorating the main action:
base = $base;
$this->cache = $cache;
}
/**
* @return Post[] | Collection
*/
public function getLastPosts()
{
return $this->cache->remember('last_posts',
self::LASTS_DURATION,
function(){
return $this->base->getLastPosts();
});
}
// other methods are practically the same
}Don't pay attention to the interface Repository in the builder. For some unknown reason, this interface for caching in Laravel was decided to be named as such.
Class CachedPostQueries only implements caching. $this->cache->remember checks if the record exists in the cache, and if not, calls the callback and stores the returned value in the cache. We just need to implement this class in the application. We need all classes that require the implementation of the interface PostQueries to start receiving an instance of the class CachedPostQueries. However, the CachedPostQueries as a parameter in the constructor must receive the class EloquentPostQueries, since it cannot work without a 'real' implementation. Changing AppServiceProvider:
app->bind(PostQueries::class,
CachedPostQueries::class);
$this->app->when(CachedPostQueries::class)
->needs(PostQueries::class)
->give(EloquentPostQueries::class);
}
}All my requirements are quite naturally described in the provider. Thus, we implemented caching for our queries by writing just one class and changing the container configuration. The code of the rest of the application did not change.
Of course, for full caching implementation, invalidation also needs to be realized so that the removed article does not stay on the site for a while and gets deleted immediately. But that's already minor details.
In summary: we used not one, but two patterns. The pattern Command Query Responsibility Segregation (CQRS) suggests completely separating read and write operations at the interface level. I came to it through Interface Segregation Principle, which indicates that I skillfully manipulate patterns and principles and derive one from another as a theorem 🙂 Naturally, not every project requires such abstraction for entity selections, but I will share a trick with you. At the initial stage of application development, you can just create a class PostQueries with a regular implementation via Eloquent:
<?php
final class PostQueries
{
public function getById($id): Post
{
return Post::findOrFail($id);
}
// other methods
}When the need for caching arises, with a simple action, you can create an interface (or abstract class) in place of this class PostQueries, copy its implementation into the class EloquentPostQueries and switch to the scheme I described earlier. The rest of the application code does not need to change.
All these tricks with classes, interfaces, Dependency Injection and CQRS are described in detail in Therein lies the answer to the mystery of why all my classes in the examples of this article are marked as final.
Source: habr.com
