Корисні репозиторії з Eloquent?

Минулого тижня я написав статтю про марність шаблону Репозиторій для 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*. Якщо почитати про Принцип розділення інтерфейсу (літера I в SOLID), то стане зрозуміло, що наш інтерфейс вийшов занадто великим і виконує як мінімум два різні обов'язки. Час ділити його на два. Метод getById необхідний в обох, однак при ускладненні застосування його реалізації будуть різними. Це ми побачимо трохи згодом. Про марність write-частини я писав у минулій статті, тому в цій я про неї просто забуду.

Read ж частина мені здається не такою вже й марною, оскільки навіть для Eloquent тут може бути кілька реалізацій. Як назвати клас? Можна, можливо ReadPostRepository, але до шаблону Сховище він уже має мале відношення. Можна просто 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 і в контролер буде підставлено цей клас.

Давайте уявимо, що наша програма стала дуже популярною. Тисячі користувачів за хвилину відкривають сторінку з останніми публікаціями. Найбільш популярні публікації також читаються дуже часто. Бази даних не дуже добре справляються з такими навантаженнями, тому використовують стандартне рішення – кеш. Крім бази даних, певний зліпок даних зберігається в сховищі, оптимізованому до певних операцій. memcached або радіс.

Логіка кешування зазвичай не така складна, але реалізовувати її в 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();
            });
    }

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

Не звертайте уваги на інтерфейс Сховище у конструкторі. З незрозумілих причин так вирішили назвати інтерфейс для кешування в Laravel.

клас CachedPostQueries реалізує лише кешування. $this->cache->remember перевіряє чи немає цього запису в кеші і якщо ні, то викликає callback і записує в кеш значення, що повернулося. Залишилося тільки впровадити цей клас у додаток. Нам необхідно, щоб усі класи, які у додатку просять реалізацію інтерфейсу PostQueries стали отримувати екземпляр класу CachedPostQueries. Однак сам CachedPostQueries як параметр у конструктор повинен отримати клас EloquentPostQueriesоскільки він не може працювати без «справжньої» реалізації. Змінюємо 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);
    }
}

Усі мої побажання досить природно описуються у провайдері. Таким чином, ми реалізували кешування для наших запитів, тільки написавши один клас і змінивши конфігурацію контейнера. Код решти програми не змінився.

Зрозуміло, для повноцінної реалізації кешування необхідно ще реалізувати інвалідацію, щоб віддалена стаття не висіла на сайті ще якийсь час, а пішла відразу. Але це вже дрібниці.

Підсумок: ми використовували не один, а два шаблони. Шаблон Command Query Responsibility Segregation (CQRS) пропонує повністю розділити операції читання та записи на рівні інтерфейсів. Я прийшов до нього через Принцип розділення інтерфейсуЗрозуміло, не кожному проекту необхідна така абстракція на вибірки сутностей, але я поділюся з вами фокусом. На початковому етапі розробки програми можна просто створити клас PostQueries із звичайною реалізацією через Eloquent:

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

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

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

Всі ці фокуси з класами, інтерфейсами, Ін'єкційна залежність и CQRS докладно описані в моїй книзі «Архітектура складних веб-додатків». Там же розгадка загадки чому всі мої класи в прикладах цієї статті позначені як final.

Джерело: habr.com

Додати коментар або відгук