Repositorios útiles con Eloquent?

A semana pasada escribín artigo sobre a inutilidade do modelo de Repositorio para entidades elocuentes, con todo, prometeu dicirme como usalo parcialmente no seu beneficio. Para iso, tentarei analizar como se adoita empregar este modelo nos proxectos. O conxunto mínimo de métodos necesarios para un repositorio:

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

Non obstante, nos proxectos reais, se se decidiu utilizar repositorios, adoitan engadirlles métodos para recuperar rexistros:

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

Estes métodos poderían implementarse a través de ámbitos Eloquent, pero sobrecargar as clases de entidades coa responsabilidade de buscarse non é a mellor idea, e mover esta responsabilidade ás clases do repositorio parece lóxico. É así? Específicamente dividín visualmente esta interface en dúas partes. A primeira parte dos métodos empregarase nas operacións de escritura.

A operación de escritura estándar é:

  • construción dun novo obxecto e reto PostRepository::save
  • PostRepository::getById, manipulación de entidades e convocatoria PostRepository::save
  • un reto PostRepository::eliminar

As operacións de escritura non usan métodos de recuperación. Nas operacións de lectura só se utilizan os métodos get*. Se le sobre Principio de segregación de interfaces (carta I в SÓLIDO), entón quedará claro que a nosa interface é demasiado grande e realiza polo menos dúas responsabilidades diferentes. É hora de dividilo por dous. Método getById é necesario en ambos, pero a medida que a aplicación se faga máis complexa, as súas implementacións serán diferentes. Isto veremos un pouco máis tarde. Escribín sobre a inutilidade da parte de escritura nun artigo anterior, así que neste simplemente esquecereime.

A parte Read non paréceme tan inútil, xa que incluso para Eloquent pode haber varias implementacións aquí. Como poñerlle o nome á clase? Pode ReadPostRepository, pero ao modelo Repositorio xa ten pouca relevancia. Podes só PostConsultas:

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

Implementalo con Eloquent é bastante sinxelo:

<?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 debe estar asociada á implementación, por exemplo en AppServiceProvider:

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

Esta clase xa é útil. Dáse conta da súa responsabilidade relevando os controladores ou a clase de entidade. Nun controlador pódese usar así:

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

Método PostsController::lastPosts só pedindo algunha implementación PostsQueries e traballa con el. No provedor que vinculamos PostConsultas con clase EloquentPostQueries e esta clase substituirase no controlador.

Imaxinemos que a nosa aplicación se fixo moi popular. Miles de usuarios por minuto abren a páxina coas últimas publicacións. Tamén se len con moita frecuencia as publicacións máis populares. As bases de datos non manexan este tipo de cargas moi ben, polo que usan unha solución estándar: unha caché. Ademais da base de datos, gárdase unha determinada instantánea de datos nun almacenamento optimizado para determinadas operacións: memcached ou redis.

A lóxica de caché non adoita ser tan complicada, pero implementala en EloquentPostQueries non é moi correcta (aínda que só sexa porque Principio de responsabilidade única). É moito máis natural usar un modelo Decorador e implementar caché como decoración para a acción 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();
            });
    }

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

Ignora a interface Repositorio no construtor. Por algún motivo descoñecido, decidiron nomear a interface para almacenar na caché en Laravel deste xeito.

Clase CachedPostQueries só implementa a caché. $this->caché->lembra comproba se esta entrada está na caché e, se non, chama a devolución de chamada e escribe o valor devolto na caché. Todo o que queda é implementar esta clase na aplicación. Necesitamos todas as clases que na aplicación soliciten unha implementación da interface PostConsultas comezou a recibir unha instancia da clase CachedPostQueries. Con todo, el mesmo CachedPostQueries o construtor debe recibir unha clase como parámetro EloquentPostQueriesxa que non pode funcionar sen unha implementación "real". Cambiamos 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 desexos descríbense de forma bastante natural no provedor. Así, implementamos a caché para as nosas solicitudes só escribindo unha clase e cambiando a configuración do contedor. O resto do código da aplicación non cambiou.

Por suposto, para implementar completamente o caché, tamén é necesario implementar a invalidación para que o artigo eliminado non se colgue no sitio durante algún tempo, senón que se elimine inmediatamente. Pero estas son cousas menores.

Conclusión: non usamos un, senón dous modelos. Mostra Segregación de responsabilidades de consulta de comandos (CQRS) propón separar completamente as operacións de lectura e escritura a nivel de interface. Cheguei a el a través Principio de segregación de interfaces, o que suxire que manipulo con habilidade patróns e principios e que derive uns dos outros como un teorema :) Por suposto, non todos os proxectos necesitan tal abstracción para seleccionar entidades, pero compartirei o truco contigo. Na fase inicial da aplicación desenvolvemento, pode simplemente crear unha clase PostConsultas coa implementación habitual a través de Eloquent:

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

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

Cando xorde a necesidade de almacenar na caché, cun simple movemento pode crear unha interface (ou clase abstracta) en lugar desta clase PostConsultas, copie a súa implementación na clase EloquentPostQueries e vai ao esquema que describín anteriormente. O resto do código da aplicación non é necesario cambiar.

Todos estes trucos con clases, interfaces, Inxección de dependencia и CQRS descrito en detalle en o meu libro "Arquitectura de aplicacións web complexas". Tamén hai unha solución ao enigma por que todas as miñas clases nos exemplos deste artigo están marcadas como finais.

Fonte: www.habr.com

Engadir un comentario