Repositórios úteis com Eloquent?

Na semana passada, escrevi um artigo sobre a inutilidade do padrão Repository para entidades Eloquent, no entanto, prometi explicar como partes dele podem ser utilizadas com utilidade. Para isso, tentarei analisar como esse padrão costuma ser utilizado em projetos. O conjunto mínimo de métodos para um repositório é:

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

No entanto, em projetos reais, se realmente foi decidido usar repositórios, frequentemente são adicionados métodos para a seleção de registros:

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

Esses métodos poderiam ser implementados através de escopos Eloquent, mas sobrecarregar as classes de entidades com a responsabilidade de serem responsáveis pela seleção de si mesmas não é a melhor ideia, e transferir essa responsabilidade para as classes de repositórios parece lógico. Será que é assim? Eu dividi visualmente essa interface em duas partes. A primeira parte dos métodos será utilizada nas operações de gravação.

A operação padrão de gravação é:

  • a construção de um novo objeto e a chamada PostRepository::save
  • PostRepository::getById, manipulações com a entidade e a chamada PostRepository::save
  • chamada PostRepository::delete

Nas operações de gravação não se utilizam métodos de seleção. Nas operações de leitura, apenas os métodos get* são usados. Se lermos sobre Interface Segregation Principle (a letra I em SOLID), ficará claro que nossa interface se tornou muito grande e desempenhando pelo menos duas responsabilidades diferentes. É hora de dividi-la em duas. O método getById é necessário em ambas, no entanto, com a complexidade crescente do aplicativo, suas implementações serão diferentes. Isso veremos um pouco mais tarde. Sobre a inutilidade da parte de gravação, escrevi no artigo anterior, portanto, neste, simplesmente vou esquecê-la.

A parte de leitura, ao contrário, não me parece tão inútil, pois mesmo para Eloquent pode haver várias implementações. Como nomear a classe? Pode-se ReadPostRepository, mas em relação ao padrão Repository já tem pouca relação. Pode-se simplesmente PostQueries:

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

Sua implementação usando Eloquent é bastante simples:

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

A interface deve estar ligada à implementação, por exemplo em AppServiceProvider:

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

Esta classe já é útil. Ela executa sua responsabilidade, aliviando assim ou os controladores ou a classe entidade. Em um controlador, pode ser utilizada assim:

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

Método PostsController::lastPosts simplesmente pede uma implementação qualquer para si PostsQueries e trabalha com ela. No provedor, vinculamos PostQueries à classe EloquentPostQueries e no controlador será inserida esta classe.

Vamos imaginar que nosso aplicativo se tornou muito popular. Milhares de usuários por minuto acessam a página com as últimas publicações. As publicações mais populares também são lidas com muita frequência. Os bancos de dados não lidam muito bem com tais cargas, então usam a solução padrão - cache. Além do banco de dados, uma cópia dos dados é armazenada em um repositório otimizado para determinadas operações - memcached ou redis.

A lógica de caching geralmente não é tão complexa, mas implementá-la em EloquentPostQueries não é muito correto (pelo menos por causa do Princípio da Responsabilidade Única). Seria muito mais natural usar o padrão Decorator e implementar o caching como uma decoração da ação principal:

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

    // outros métodos são praticamente os mesmos
}

Não se preocupe com a interface Repository no construtor. Por uma razão desconhecida, decidiram nomear a interface de cache no Laravel.

Classe CachedPostQueries implementa apenas o cache. $this->cache->remember verifica se a entrada está no cache e, se não estiver, chama o callback e grava no cache o valor retornado. Resta apenas integrar esta classe na aplicação. Precisamos que todas as classes que solicitam a implementação da interface PostQueries passam a receber uma instância da classe CachedPostQueries. Contudo, o próprio CachedPostQueries como parâmetro no construtor deve receber a classe EloquentPostQueries, pois não pode funcionar sem uma implementação 'real'. Mudamos AppServiceProvider:

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

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

Todos os meus desejos são naturalmente descritos no provedor. Assim, implementamos o cache para nossas consultas escrevendo apenas uma classe e alterando a configuração do contêiner. O código do restante da aplicação não mudou.

Claro, para uma implementação completa do cache, também é necessário implementar a invalidação, para que um artigo removido não permaneça no site por mais tempo e seja excluído imediatamente. Mas isso já são detalhes.

Resultado: usamos não apenas um, mas dois padrões. O padrão Command Query Responsibility Segregation (CQRS) propõe separar completamente as operações de leitura e gravação no nível das interfaces. Cheguei a isso através Interface Segregation Principle, o que demonstra que manipulo padrões e princípios habilmente e deduzo um a partir do outro como um teorema 🙂 Claro, nem todo projeto necessita de tal abstração para consultas de entidades, mas compartilharei com você uma dica. Na fase inicial do desenvolvimento da aplicação, pode-se simplesmente criar uma classe PostQueries com uma implementação comum via Eloquent:

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

    // outros métodos
}

Quando surgir a necessidade de cache, pode-se facilmente criar uma interface (ou classe abstrata) no lugar desta classe PostQueries, copiar sua implementação para a classe EloquentPostQueries e seguir para o esquema que descrevi anteriormente. Não é necessário modificar o restante do código da aplicação.

Todos esses truques com classes, interfaces, Dependency Injection e CQRS estão detalhadamente descritos em meu livro "Arquitetura de Aplicações Web Complexas". Lá está a solução para o mistério de por que todas as minhas classes nos exemplos deste artigo estão marcadas como final.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster