Полезни репозитории с Eloquent?

Миналата седмица написах статия за безсмислието на шаблона Repository за Eloquent ентитети, но обещах да разкажа как може частично да се използва с полза. За това ще се опитам да анализирам как обикновено се използва този шаблон в проектите. Минимално необходимият набор от методи за репозитория:

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

Въпреки това, в реалните проекти, ако наистина е решено да се използват репозитории, в тях често се добавят методи за извличане на записи:

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

Тези методи биха могли да бъдат реализирани чрез Eloquent scopes, но натоварването на класовете на ентитетите с отговорности за извличане на самите себе си — не е най-добрата идея и изнасянето на тази отговорност в класовете на репозиториите изглежда логично. Така ли е? Специално визуално разделих този интерфейс на две части. Първата част от методите ще бъде използвана в операции по запис.

Стандартни операции по запис са:

  • конструирането на нов обект и извикването на PostRepository::save
  • PostRepository::getById, манипулации с ентитета и извикването на PostRepository::save
  • извикване PostRepository::delete

В операциите по запис няма използване на методи за извличане. В операциите по четене обаче се използват само методи get*. Ако прочетете за Interface Segregation Principle (буква I в SOLID), ще стане ясно, че нашият интерфейс се е получил твърде голям и изпълнява поне две различни отговорности. Време е да го разделим на два. Методът getById е необходим и в двата, но при усложняване на приложението неговите реализации ще бъдат различни. Това ще видим малко по-късно. За безсмислието на частта за запис писах в предната статия, затова в тази просто ще я забравя.

Частта за четене обаче не ми се струва толкова безполезна, тъй като дори за Eloquent тук могат да бъдат няколко реализации. Как да се нарече класът? Може ReadPostRepository, но той вече има малко общо с шаблона Repository . Може просто PostQueries:

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

Неговата реализация с помощта на Eloquent е доста проста:

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

Интерфейсът трябва да бъде свързан с реализация, например в AppServiceProvider:

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

Този клас вече е полезен. Той реализира собствената си отговорност, облекчавайки контролерите или класовете на съществата. В контролера може да се използва така:

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

Методът PostsController::lastPosts просто иска реализиране на някаква реализация PostsQueries и работи с нея. В провайдера ние свързахме PostQueries с класа EloquentPostQueries и в контролера ще бъде подставен този клас.

Нека си представим, че нашето приложение е станало много популярно. Хиляди потребители в минута отварят страницата с последните публикации. Най-популярните публикации също се четат много често. Базите данни не се справят много добре с такива натоварвания, затова се използва стандартно решение — кеширане. Освен базата данни, определен snapshot на данни се съхранява в хранилище, оптимизирано за определени операции — memcached или redis.

Логиката на кеширане обикновено не е толкова сложна, но реализирането ѝ в EloquentPostQueries не е много правилно (понеже), Принцип на единствената отговорност). Много по-естествено е да се използва шаблона Декоратор и да се реализира кеширането като декориране на основното действие:

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

    // другите методи са практически същите
}

Не обръщайте внимание на интерфейса Repository в конструктора. По неясна причина така решили да нарекат интерфейса за кеширане в Laravel.

Клас CachedPostQueries реализира само кеширането. $this->cache->remember проверява дали данната запис е в кеша и, ако не, извиква callback и записва върнатата стойност в кеша. Остана само да внедрим този клас в приложението. Необходимо е всички класове, които в приложението искат реализация на интерфейса PostQueries да получат екземпляр на класа CachedPostQueries. Обаче самият CachedPostQueries като параметър в конструктора трябва да получи класа EloquentPostQueries, тъй като не може да работи без 'истинска' реализация. Променяме AppServiceProvider:

app->bind(PostQueries::class, 
            CachedPostQueries::class);

        $this->app->when(CachedPostQueries::class)
            ->needs(PostQueries::class)
            ->give(EloquentPostQueries::class);
    }
}

Всички мои желания се описват доста естествено в провайдера. По този начин, реализирахме кеширането за нашите заявки само с написване на един клас и промяна на конфигурацията на контейнера. Кодът на останалата част от приложението не се промени.

Разбира се, за пълната реализация на кеширането е необходимо също така да реализираме инвалидация, за да не остава премахнатата статия на сайта още известно време, а да бъде премахната веднага. Но това е дреболия.

Резултат: използвахме не един, а цели два шаблона. Шаблон Command Query Responsibility Segregation (CQRS) предлага напълно разделяне на операции за четене и запис на ниво интерфейси. Достигнах до него чрез Interface Segregation Principle, което говори, че умело манипулирам шаблони и принципи и извеждам едно от друго като теорема 🙂 Разбира се, не на всеки проект е необходима такава абстракция при избор на същности, но ще споделя с вас трик. На начален етап от разработката на приложението може просто да се създаде клас PostQueries с обикновена реализация чрез Eloquent:

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

    // другите методи
}

Когато възникне нужда от кеширане, с леко движение може да се създаде интерфейс (или абстрактен клас) на мястото на този клас PostQueries, реализацията му да се копира в класа EloquentPostQueries и да се премине към схемата, описана от мен по-рано. Останалият код на приложението не е нужно да се променя.

Всички тези трикове с класове, интерфейси, Dependency Injection и CQRS са подробно описани в моята книга «Архитектура на сложни уеб приложения». Там е обяснението защо всичките ми класове в примерите към тази статия са обозначени като final.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster