📜 ⬆️ ⬇️

Useful repositories with Eloquent?

Last week, I wrote an article about the uselessness of the Repository template for Eloquent entities , but I promised to tell you how to use it partially. To do this, I will try to analyze how this template is usually used in projects. The minimum required set of methods for the repository:


<?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); } 

However, in real projects, if repositories were decided to be used, methods for selecting records are often added to them:


 <?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 overloading entity classes with responsibilities for sampling themselves is not the best idea, and putting this duty into repository classes seems logical. Is it so? I specifically visually divided this interface into two parts. The first part of the methods will be used in write operations.


Standard write operations are:



There is no use of sampling methods in write operations. In read operations, only get * methods are used. If you read about the Interface Segregation Principle (the letter I in SOLID ), then it becomes clear that our interface turned out to be too large and performing at least two different duties. It's time to divide it into two. The getById method is required in both, but if the application becomes more complex, its implementation will be different. This we will see a little later. I wrote about the uselessness of the write part in the last article, so in this one I will just forget about it.


Read the part seems to me not so useless, because even for Eloquent there may be several implementations. How to name a class? It is possible to readPostRepository , but it already has little to do with the Repository template. You can simply PostQueries :


 <?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); } 

Its implementation with Eloquent is fairly simple:


 <?php final class EloquentPostQueries implements PostQueries { public function getById($id): Post { return Post::findOrFail($id); } /** * @return Post[] | Collection */ public function getLastPosts() { return Post::orderBy('created_at', 'desc') ->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 associated with the implementation, for example, in the AppServiceProvider :


 <?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, EloquentPostQueries::class); } } 

This class is already useful. It realizes its responsibility by unloading either the controllers or the entity class. In the controller, it can be used as follows:


 <?php final class PostsController extends Controller { public function lastPosts(PostQueries $postQueries) { return view('posts.last', [ 'posts' => $postQueries->getLastPosts(), ]); } } 

The PostsController :: lastPosts method simply asks itself for some implementation of PostsQueries and works with it. In the provider, we linked PostQueries with the EloquentPostQueries class and this class will be substituted into the controller.


Let's imagine that our application has become very popular. Thousands of users per minute open a page with the latest publications. The most popular publications are also read very often. Databases do not do very well with such loads, so they use a standard solution - a cache. In addition to the database, a certain snapshot of data is stored in a storage optimized for certain operations - memcached or redis .


The caching logic is usually not so complicated, but implementing it in EloquentPostQueries is not very correct (at least because of the Single Responsibility Principle ). It is much more natural to use the Decorator pattern and implement caching as decorating the main action:


 <?php use Illuminate\Contracts\Cache\Repository; final class CachedPostQueries implements PostQueries { const LASTS_DURATION = 10; /** @var PostQueries */ private $base; /** @var Repository */ private $cache; public function __construct( PostQueries $base, Repository $cache) { $this->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(); }); } //      } 

Disregard the Repository interface in the constructor. For some reason, they decided to call the interface for caching in Laravel.


The CachedPostQueries class implements caching only. $ this-> cache-> remember checks if the entry is in the cache and if not, calls the callback and writes the returned value to the cache. It remains only to implement this class in the application. We need all classes that in the application ask for the implementation of the PostQueries interface to get an instance of the CachedPostQueries class. However, CachedPostQueries itself as a parameter to the constructor must receive the EloquentPostQueries class, since it cannot work without a “real” implementation. Change the AppServiceProvider :


 <?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, CachedPostQueries::class); $this->app->when(CachedPostQueries::class) ->needs(PostQueries::class) ->give(EloquentPostQueries::class); } } 

All my wishes are quite naturally described in the provider. Thus, we implemented caching for our queries only by writing one class and changing the configuration of the container. The rest of the application code has not changed.


Of course, for the full implementation of caching, you still need to implement invalidation so that the deleted article does not hang on the site for some time but disappears immediately. But this is the little things.


Bottom line: we used not one, but two whole patterns. The Command Query Responsibility Segregation (CQRS) template offers to completely separate the read and write operations at the interface level. I came to him through the Interface Segregation Principle , which means that I skillfully manipulate patterns and principles and derive one from the other as a theorem :) Of course, not every project needs such an abstraction on entity samples, but I will share with you the focus. At the initial stage of application development, you can simply create a PostQueries class with a normal implementation through Eloquent:


 <?php final class PostQueries { public function getById($id): Post { return Post::findOrFail($id); } //   } 

When the need arises for caching, you can easily create an interface (or abstract class) in place of this PostQueries class, copy its implementation to the EloquentPostQueries class and go to the scheme I described earlier. The rest of the application code does not need to be changed.


However, there is a problem with the use of the same Post entities that can modify data. This is not exactly CQRS.



No one bothers to get Post entity from PostQueries , change it and save changes using simple -> save () . And it will work.
After some time, the team will switch to master-slave replication in the database and PostQueries will work with read-replicas. Write operations on read-replicas are usually blocked. The error will be revealed, but it will take a lot of work to fix all such jambs.


The solution is obvious - to separate the read and write parts completely. You can continue to use Eloquent, but by creating a class for read-only models. Example: https://github.com/adelf/freelance-example/blob/master/app/ReadModels/ReadModel.php All data modification operations are blocked. Create a new model, for example ReadPost (you can leave Post , but move it to another namespace):


 <?php final class ReadPost extends ReadModel { protected $table = 'posts'; } interface PostQueries { public function getById($id): ReadPost; } 

Such models can be used only for reading and can be safely cached.



Another option: opt out of eloquent. There may be several reasons for this:



A simple example of how this might look like:


 <?php final class PostHeader { public int $id; public string $title; public DateTime $publishedAt; } final class Post { public int $id; public string $title; public string $text; public DateTime $publishedAt; } interface PostQueries { public function getById($id): Post; /** * @return PostHeader[] */ public function getLastPosts(); /** * @return PostHeader[] */ public function getTopPosts(); /** * @var int $userId * @return PostHeader[] */ public function getUserPosts($userId); } 

Of course, all this seems like a wild overlap of logic. "Take the Eloquent scopes and everything will be fine. Why invent all this?" For simpler projects, this is correct. Absolutely no need to reinvent scopes. But when a project is large and several developers are involved in development, which often change (they quit and new ones come), the rules of the game become slightly different. It is necessary to write the code protected so that the new developer after several years could not do something wrong. It is, of course, impossible to completely exclude such a probability, but it is necessary to reduce its probability. In addition, this is the usual decomposition of the system. You can collect all caching decorators and classes for cache invalidation into a kind of "caching module", thus freeing the rest of the application about caching information. I had to rummage through large queries that were surrounded by cache calls. It interferes. Especially if the caching logic is not as simple as described above.


')

Source: https://habr.com/ru/post/445452/


All Articles