Repositórios úteis com Eloquent?

Semana passada eu escrevi artigo sobre a inutilidade do modelo Repositório para entidades Eloquent, no entanto, ele prometeu me dizer como usar isso parcialmente em seu benefício. Para isso, tentarei analisar como esse template costuma ser utilizado em projetos. O conjunto mínimo necessário de métodos para um repositório:

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

Porém, em projetos reais, se for decidido usar repositórios, métodos para recuperar registros são frequentemente adicionados a eles:

<?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 por meio de escopos do Eloquent, mas sobrecarregar as classes de entidade com a responsabilidade de buscar a si mesmas não é a melhor ideia, e mover essa responsabilidade para as classes do repositório parece lógico. É assim? Eu dividi visualmente especificamente essa interface em duas partes. A primeira parte dos métodos será usada em operações de escrita.

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

  • construção de um novo objeto e desafio PostRepositório::salvar
  • PostRepositório::getById, manipulação e convocação de entidades PostRepositório::salvar
  • desafiar PostRepository::delete

As operações de gravação não usam métodos de busca. Nas operações de leitura, apenas métodos get* são usados. Se você ler sobre Princípio de Segregação de Interface (carta I в SOLID), ficará claro que nossa interface é muito grande e executa pelo menos duas responsabilidades diferentes. É hora de dividir por dois. Método getById é necessário em ambos, mas à medida que a aplicação se torna mais complexa, suas implementações serão diferentes. Veremos isso um pouco mais tarde. Escrevi sobre a inutilidade da parte escrita em um artigo anterior, então neste vou simplesmente esquecer.

A parte Read não me parece tão inútil, pois mesmo para o Eloquent pode haver várias implementações aqui. Como nomear a classe? Pode ReadPostRepositório, mas para o modelo Repositório ele já tem pouca relevância. Você pode apenas Pós-consultas:

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

Implementá-lo com o 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 associada à 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. Ele realiza sua responsabilidade dispensando os controladores ou a classe de entidade. Em um controlador pode ser usado assim:

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

método PostsController::lastPostagens apenas pedindo alguma implementação PostagensConsultas e trabalha com isso. No provedor que vinculamos Pós-consultas com classe EloquentPostQueries e esta classe será substituída no controlador.

Vamos imaginar que nosso aplicativo se tornou muito popular. Milhares de usuários por minuto abrem a página com as últimas publicações. As publicações mais populares também são lidas com frequência. Os bancos de dados não lidam muito bem com essas cargas, então eles usam uma solução padrão - um cache. Além do banco de dados, um determinado instantâneo de dados é armazenado em um armazenamento otimizado para determinadas operações - memcached ou redis.

A lógica de cache geralmente não é tão complicada, mas implementá-la no EloquentPostQueries não é muito correta (até porque Princípio de Responsabilidade Única). É muito mais natural usar um modelo Decorador e implemente o cache como decoração para a 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();
            });
    }

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

Ignorar a interface Repositório no construtor. Por alguma razão desconhecida, eles decidiram nomear a interface de cache no Laravel desta forma.

Classe CachedPostQueries implementa apenas cache. $this->cache->lembrar verifica se esta entrada está no cache e, caso contrário, chama o retorno de chamada e grava o valor retornado no cache. Resta apenas implementar esta classe na aplicação. Precisamos de todas as classes que estão na aplicação para solicitar uma implementação da interface Pós-consultas começou a receber uma instância da classe CachedPostQueries. Contudo, ele mesmo CachedPostQueries o construtor deve receber uma classe como parâmetro EloquentPostQueriesjá que não pode funcionar sem uma implementação "real". Nós mudamos 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);
    }
}

Todos os meus desejos são descritos naturalmente no provedor. Assim, implementamos o cache para nossas solicitações apenas escrevendo uma classe e alterando a configuração do contêiner. O restante do código do aplicativo não foi alterado.

É claro que, para implementar totalmente o cache, também é necessário implementar a invalidação para que o artigo excluído não fique pendurado no site por algum tempo, mas seja excluído imediatamente. Mas estas são coisas menores.

Resumindo: usamos não um, mas dois modelos. Amostra Segregação de responsabilidade de consulta de comando (CQRS) propõe separar completamente as operações de leitura e gravação no nível da interface. Eu cheguei até ele através Princípio de Segregação de Interface, o que sugere que eu manipule padrões e princípios habilmente e deduza um do outro como um teorema :) É claro que nem todo projeto precisa de tal abstração para selecionar entidades, mas compartilharei o truque com você. No estágio inicial de aplicação desenvolvimento, você pode simplesmente criar uma classe Pós-consultas com a implementação usual via Eloquent:

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

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

Quando surge a necessidade de cache, com um simples movimento você pode criar uma interface (ou classe abstrata) no lugar desta classe Pós-consultas, copie sua implementação para a classe EloquentPostQueries e vá para o esquema que descrevi anteriormente. O restante do código do aplicativo não precisa ser alterado.

Todos esses truques com classes, interfaces, Injeção de dependência и CQRS descrito em detalhes em meu livro “Arquitetura de Aplicações Web Complexas”. Há também uma solução para o enigma de por que todas as minhas aulas nos exemplos deste artigo são marcadas como finais.

Fonte: habr.com

Adicionar um comentário