¿Repositorios útiles con Eloquent?

La semana pasada escribí un artículo sobre la inutilidad de la plantilla Repositorio para entidades Eloquent, sin embargo, prometí contar cómo se puede usar parcialmente de manera útil. Para ello, intentaré analizar cómo se usa esta plantilla normalmente en proyectos. El conjunto mínimo necesario de métodos para el repositorio es:

<?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 los repositorios, a menudo se añaden métodos para seleccionar 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 se podrían implementar a través de scopes de Eloquent, pero sobrecargar las clases de entidades con la responsabilidad de seleccionar a sí mismas no es la mejor idea, y trasladar esta responsabilidad a las clases de repositorios parece lógico. ¿Es así? He dividido visualmente esta interfaz en dos partes. La primera parte de los métodos se utilizará en operaciones de escritura.

Las operaciones estándar de escritura son:

  • la construcción de un nuevo objeto y la llamada a PostRepository::save
  • PostRepository::getById, manipulaciones con la entidad y la llamada a PostRepository::save
  • la llamada PostRepository::delete

En las operaciones de escritura no se utilizan métodos de selección. En las operaciones de lectura, solo se utilizan métodos get*. Si leemos sobre Interface Segregation Principle (la letra I en SOLID), se entiende que nuestra interfaz se volvió demasiado grande y cumple al menos dos responsabilidades diferentes. Es hora de dividirla en dos. El método getById es necesario en ambos, sin embargo, a medida que la aplicación se complica, sus implementaciones serán diferentes. Esto lo veremos un poco más adelante. Sobre la inutilidad de la parte de escritura escribí en el artículo anterior, así que en este simplemente olvidaré acerca de ella.

La parte de lectura, en cambio, me parece no tan inútil, ya que incluso para Eloquent puede haber varias implementaciones. ¿Cómo nombrar la clase? Se puede decir ReadPostRepository, pero ya tiene poco que ver con la plantilla. Repositorio Simplemente se puede llamar PostQueries:

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

Su implementación utilizando Eloquent es bastante simple:

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 vinculada a la implementación, por ejemplo en AppServiceProvider:

app->bind(PostQueries::class, 
            EloquentPostQueries::class);
    }
}

Esta clase ya es útil. Implementa su responsabilidad, aliviando así ya sea a los controladores o a la clase de entidad. En el controlador se puede usar así:

$postQueries->getLastPosts(),
        ]);
    }
} 

El método PostsController::lastPosts solo pide alguna implementación para sí misma PostsQueries y trabaja con ella. En el proveedor hemos vinculado PostQueries a la clase EloquentPostQueries y en el controlador se usará esta clase.

Imaginemos que nuestra aplicación se ha vuelto muy popular. Miles de usuarios por minuto están abriendo la página de las últimas publicaciones. Las publicaciones más populares también son leídas con mucha frecuencia. Las bases de datos no manejan muy bien tales cargas, por lo que se utiliza una solución estándar: el caché. Además de la base de datos, hay un snapshot de los datos almacenado en un almacenamiento optimizado para ciertas operaciones — memcached o redis.

La lógica de caché generalmente no es tan compleja, pero implementarla en EloquentPostQueries no es muy correcto (al menos por el tema del Principio de Responsabilidad Única). Es mucho más natural usar el patrón Decorator e implementar el caché como una decoración de la acción principal:

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

    // otros métodos son prácticamente iguales
}

No prestes atención a la interfaz Repositorio en el constructor. Por alguna razón inexplicable decidieron llamar así a la interfaz de caché en Laravel.

Clase CachedPostQueries implementa solo la caché. $this->cache->remember verifica si hay un registro en la caché y, si no lo hay, invoca el callback y guarda en caché el valor devuelto. Solo queda implementar esta clase en la aplicación. Necesitamos que todas las clases que en la aplicación solicitan la implementación de la interfaz PostQueries comiencen a recibir una instancia de la clase CachedPostQueries. Sin embargo, el mismo CachedPostQueries como parámetro en el constructor debe obtener la clase EloquentPostQueries, ya que no puede funcionar sin una implementación 'real'. Cambiamos AppServiceProvider:

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

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

Todos mis deseos se describen de manera bastante natural en el proveedor. Así, hemos implementado la caché para nuestras consultas solo escribiendo una clase y cambiando la configuración del contenedor. El código del resto de la aplicación no ha cambiado.

Por supuesto, para una implementación completa de la caché, también es necesario implementar la invalidación, para que un artículo eliminado no permanezca en el sitio un tiempo adicional y se elimine de inmediato. Pero eso son solo detalles.

Resultado: hemos utilizado no uno, sino dos patrones. El patrón Command Query Responsibility Segregation (CQRS) sugiere separar completamente las operaciones de lectura y escritura a nivel de interfaces. Llegué a él a través de Interface Segregation Principle, lo que indica que manejo hábilmente los patrones y principios y deduzco uno de otro como un teorema 🙂 Por supuesto, no todos los proyectos requieren tal abstracción para las selecciones de entidades, pero compartiré un truco contigo. En la fase inicial de desarrollo de la aplicación, se puede simplemente crear una clase PostQueries con una implementación normal mediante Eloquent:

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

    // otros métodos
}

Cuando surja la necesidad de caché, con un movimiento ligero se puede crear una interfaz (o clase abstracta) en lugar de esta clase PostQueries, copiar su implementación en la clase EloquentPostQueries y pasar 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 Dependencias y CQRS se describen en detalle en mi libro 'Arquitectura de aplicaciones web complejas'. Ahí está la clave de por qué todas mis clases en los ejemplos de este artículo están marcadas como final.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster