¿Repositorios útiles con Eloquent?

la semana pasada escribi artículo sobre la inutilidad de la plantilla de Repositorio para entidades Eloquent, sin embargo, prometió decirme cómo utilizarlo parcialmente a su favor. Para ello, intentaré analizar cómo se suele utilizar esta plantilla en los proyectos. El conjunto mínimo requerido de métodos para un repositorio:

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

Sin embargo, en proyectos reales, si se decidió utilizar repositorios, a menudo se les agregan métodos para recuperar 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);
}

Estos métodos podrían implementarse a través de ámbitos de Eloquent, pero sobrecargar las clases de entidad con la responsabilidad de buscarse a sí mismas no es la mejor idea, y trasladar esta responsabilidad a las clases de repositorio parece lógico. ¿Es tan? Específicamente dividí visualmente esta interfaz en dos partes. La primera parte de los métodos se utilizará en operaciones de escritura.

La operación de escritura estándar es:

  • construcción de un nuevo objeto y desafío PostRepositorio::guardar
  • PostRepositorio::getById, manipulación de entidades e invocación PostRepositorio::guardar
  • el reto PublicarRepositorio::eliminar

Las operaciones de escritura no utilizan métodos de recuperación. En las operaciones de lectura, solo se utilizan los métodos get*. Si lees sobre Principio de segregación de interfaz (carta I в SOLID), entonces quedará claro que nuestra interfaz es demasiado grande y realiza al menos dos responsabilidades diferentes. Es hora de dividirlo por dos. Método obtener por Id es necesario en ambos, pero a medida que la aplicación se vuelve más compleja, sus implementaciones diferirán. Esto lo veremos un poco más adelante. Escribí sobre la inutilidad de la parte de escritura en un artículo anterior, así que en este simplemente lo olvidaré.

La parte Leer no me parece tan inútil, ya que incluso para Eloquent puede haber varias implementaciones aquí. ¿Cómo nombrar la clase? Poder ReadPostRepositorio, pero a la plantilla Repositorio ya tiene poca relevancia. Tu puedes sólo Postconsultas:

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

Implementarlo con Eloquent es bastante simple:

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

La interfaz debe estar asociada con la implementación, por ejemplo en AppServiceProviderAppServiceProvider:

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

Esta clase ya es útil. Se da cuenta de su responsabilidad relevando a los controladores o a la clase de entidad. En un controlador se puede utilizar así:

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

método Controlador de publicaciones::últimas publicaciones solo estoy pidiendo alguna implementación PublicacionesConsultas y trabaja con él. En el proveedor que vinculamos Postconsultas con clase ElocuentesPostQueries y esta clase será sustituida en el controlador.

Imaginemos que nuestra aplicación se ha vuelto muy popular. Miles de usuarios por minuto abren la página con las últimas publicaciones. Las publicaciones más populares también se leen con mucha frecuencia. Las bases de datos no soportan muy bien este tipo de cargas, por lo que utilizan una solución estándar: el caché. Además de la base de datos, una determinada instantánea de los datos se almacena en un almacenamiento optimizado para determinadas operaciones: memcached o redis.

La lógica del almacenamiento en caché no suele ser tan complicada, pero implementarla en EloquentPostQueries no es muy correcto (aunque solo sea porque Principio de responsabilidad única). Es mucho más natural usar una plantilla. Decorador e implementar el almacenamiento en caché como decoración para la 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();
            });
    }

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

Ignorar la interfaz Repositorio en el constructor. Por alguna razón desconocida, decidieron nombrar la interfaz de almacenamiento en caché en Laravel de esta manera.

clase Consultas de publicaciones en caché implementa el almacenamiento en caché únicamente. $this->cache->recordar comprueba si esta entrada está en la memoria caché y, si no, llama a la devolución de llamada y escribe el valor devuelto en la memoria caché. Todo lo que queda es implementar esta clase en la aplicación. Necesitamos que todas las clases que están en la aplicación soliciten una implementación de la interfaz. Postconsultas comenzó a recibir una instancia de la clase Consultas de publicaciones en caché. Sin embargo, él mismo Consultas de publicaciones en caché el constructor debe recibir una clase como parámetro ElocuentesPostQueriesya que no puede funcionar sin una implementación "real". Cambiamos AppServiceProviderAppServiceProvider:

<?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 mis deseos se describen de forma bastante natural en el proveedor. Por lo tanto, implementamos el almacenamiento en caché para nuestras solicitudes solo escribiendo una clase y cambiando la configuración del contenedor. El resto del código de la aplicación no ha cambiado.

Por supuesto, para implementar completamente el almacenamiento en caché, también es necesario implementar la invalidación para que el artículo eliminado no se quede colgado en el sitio por un tiempo, sino que se elimine inmediatamente. Pero estas son cosas menores.

En pocas palabras: utilizamos no una, sino dos plantillas. Muestra Segregación de responsabilidad de consultas de comando (CQRS) propone separar completamente las operaciones de lectura y escritura en el nivel de interfaz. Llegué a él a través Principio de segregación de interfaz, lo que sugiere que manipulo hábilmente patrones y principios y deduzco uno del otro como un teorema :) Por supuesto, no todos los proyectos necesitan tal abstracción para seleccionar entidades, pero compartiré el truco contigo. desarrollo, simplemente puedes crear una clase Postconsultas con la implementación habitual a través de Eloquent:

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

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

Cuando surge la necesidad de almacenamiento en caché, con un simple movimiento puede crear una interfaz (o clase abstracta) en lugar de esta clase. Postconsultas, copia su implementación a la clase ElocuentesPostQueries y vaya al esquema que describí anteriormente. No es necesario cambiar el resto del código de la aplicación.

Todos estos trucos con clases, interfaces, Inyección de dependencia и CQRS descrito en detalle en mi libro “Arquitectura de aplicaciones web complejas”. También hay una solución al enigma de por qué todas mis clases en los ejemplos de este artículo están marcadas como finales.

Fuente: habr.com

Añadir un comentario