Useful repositories with Eloquent?

Last week I wrote an article about the uselessness of the Repository template for Eloquent entities, but promised to tell how you can partially use it to good use. 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 it was decided to use repositories, 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 selecting themselves is not the best idea, and moving this responsibility into repository classes looks logical. Is it so? I deliberately visually divided this interface into two parts. The first part of the methods will be used in write operations.

The standard write operation is:

  • constructing a new object and calling PostRepository::save
  • PostRepository::getById, entity manipulation and call PostRepository::save
  • call PostRepository::delete

Write operations do not use fetch methods. In read operations, only get* methods are used. If you read about Interface Segregation Principle (letter I Π² SOLID), then it will become clear that our interface turned out to be too large and perform at least two different duties. Time to split it in two. Method getById necessary in both, but as the application becomes more complex, its implementation will be different. We will see this a little later. I wrote about the uselessness of the write part in the last article, so in this one I will simply forget about it.

The Read part seems to me not so useless, since even for Eloquent there can be several implementations here. How to name the class? Can ReadPostRepository, but to the template Repository he already has little to do with it. You can just Post Queries:

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

Its implementation with Eloquent is quite 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();
    }
}

An interface must be linked to an implementation, for example in 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 offloading either the controllers or the entity class. In a controller it can be used like this:

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

Method PostsController::lastPosts just asks for some implementation Posts Queries and work with her. In the provider we have linked Post Queries with class EloquentPostQueries and this class will be substituted in the controller.

Let's imagine that our application has become very popular. Thousands of users per minute open the page with the latest publications. The most popular publications are also read very often. Databases are not very good at handling such loads, so they use the standard solution - cache. In addition to the database, a certain snapshot of data is stored in 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 (if only because of Single Responsibility Principle). Much more natural to use a template Decorator and implement caching as decoration of the main action:

<?php
use IlluminateContractsCacheRepository;

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();
            });
    }

    // Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ практичСски Ρ‚Π°ΠΊΠΈΠ΅ ΠΆΠ΅
}

Pay no attention to the interface Repository in the constructor. For some unknown reason, this is how they decided to call the interface for caching in Laravel.

Class CachedPostQueries only implements caching. $this->cache->remember checks if the given entry is in the cache, and if not, then calls callback and writes the returned value to the cache. It remains only to implement this class in the application. We need that all classes that in the application ask for an implementation of an interface Post Queries began to receive an instance of the class CachedPostQueries. However, CachedPostQueries as a parameter to the constructor must receive a class EloquentPostQueries, because it can't work without a "real" implementation. We change 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 requests by only writing one class and changing the container configuration. The code for the rest of the application has not changed.

Of course, for the full implementation of caching, it is also necessary to implement invalidation so that the deleted article does not hang on the site for some more time, but is deleted immediately. But these are trifles.

Bottom line: we used not one, but two whole templates. Sample Command Query Responsibility Segregation (CQRS) proposes to completely separate read and write operations at the interface level. I came to him through Interface Segregation Principle, which means that I skillfully manipulate templates and principles and deduce one from the other as a theorem πŸ™‚ Of course, not every project needs such an abstraction for entity selections, but I will share the trick with you. At the initial stage of application development, you can simply create a class Post Queries with the usual implementation via Eloquent:

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

    // Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹
}

When the need for caching arises, it is easy to create an interface (or abstract class) in place of this class Post Queries, copy its implementation to the class EloquentPostQueries and go to the scheme described by me earlier. The rest of the application code does not need to be changed.

All these tricks with classes, interfaces, Dependency Injection ΠΈ CQRS described in detail in my book The Architecture of Complex Web Applications. In the same place, the solution to the riddle why all my classes in the examples for this article are marked as final.

Source: habr.com

Add a comment