Gestionar relaciones en Laravel, adhiriéndose al patrón de repositorio

120

Mientras creaba una aplicación en Laravel 4 después de leer el libro de T.Otwell sobre buenos patrones de diseño en Laravel, me encontré creando repositorios para cada tabla de la aplicación.

Terminé con la siguiente estructura de tabla:

  • Estudiantes: id, nombre
  • Cursos: id, nombre, teacher_id
  • Profesores: id, nombre
  • Asignaciones: id, nombre, course_id
  • Puntajes (actúa como un pivote entre los estudiantes y las tareas): student_id, task_id, scores

Tengo clases de repositorio con métodos de búsqueda, creación, actualización y eliminación para todas estas tablas. Cada repositorio tiene un modelo Eloquent que interactúa con la base de datos. Las relaciones se definen en el modelo según la documentación de Laravel: http://laravel.com/docs/eloquent#relationships .

Al crear un nuevo curso, todo lo que hago es llamar al método create en el repositorio de cursos. Ese curso tiene asignaciones, por lo que al crear una, también quiero crear una entrada en la tabla de puntuación para cada alumno del curso. Hago esto a través del Repositorio de asignaciones. Esto implica que el repositorio de tareas se comunica con dos modelos Eloquent, con el modelo Assignment y Student.

Mi pregunta es: como esta aplicación probablemente crecerá en tamaño y se introducirán más relaciones, ¿es una buena práctica comunicarse con diferentes modelos de Eloquent en repositorios o debería hacerlo usando otros repositorios en su lugar (me refiero a llamar a otros repositorios desde el repositorio de asignaciones ) ¿O debería hacerse en los modelos Eloquent todos juntos?

Además, ¿es una buena práctica usar la tabla de puntajes como un pivote entre las tareas y los estudiantes o debería hacerse en otro lugar?

ehp
fuente

Respuestas:

71

Tenga en cuenta que está pidiendo opiniones: D

Aquí está el mío:

TL; DR: Sí, está bien.

¡Lo estás haciendo bien!

Hago exactamente lo que haces a menudo y encuentro que funciona muy bien.

Sin embargo, a menudo organizo repositorios en torno a la lógica empresarial en lugar de tener un repositorio por tabla. Esto es útil ya que es un punto de vista centrado en cómo su aplicación debería resolver su "problema comercial".

Un curso es una "entidad", con atributos (título, id, etc) e incluso otras entidades (asignaciones, que tienen sus propios atributos y posiblemente entidades).

Su repositorio de "cursos" debería poder devolver un curso y los atributos / asignaciones de los cursos (incluida la asignación).

Puedes lograr eso con Eloquent, afortunadamente.

(A menudo termino con un repositorio por tabla, pero algunos repositorios se utilizan mucho más que otros, por lo que tienen muchos más métodos. Su repositorio de "cursos" puede tener muchas más funciones que su repositorio de Tareas, por ejemplo, si su la aplicación se centra más en Cursos y menos en una colección de Asignaciones de Cursos).

La parte complicada

A menudo uso repositorios dentro de mis repositorios para realizar algunas acciones en la base de datos.

Cualquier repositorio que implemente Eloquent para manejar datos probablemente devolverá modelos de Eloquent. En ese sentido, está bien si su modelo de curso utiliza relaciones integradas para recuperar o guardar asignaciones (o cualquier otro caso de uso). Nuestra "implementación" se basa en Eloquent.

Desde un punto de vista práctico, esto tiene sentido. Es poco probable que cambiemos las fuentes de datos a algo que Eloquent no pueda manejar (a una fuente de datos que no sea SQL).

ORMS

La parte más complicada de esta configuración, al menos para mí, es determinar si Eloquent realmente nos está ayudando o perjudicando. Los ORM son un tema complicado, porque si bien nos ayudan mucho desde un punto de vista práctico, también acoplan su código de "entidades lógicas de negocio" con el código que realiza la recuperación de datos.

Este tipo de confunde si la responsabilidad de su repositorio es en realidad manejar datos o manejar la recuperación / actualización de entidades (entidades de dominio comercial).

Además, actúan como los mismos objetos que pasa a sus puntos de vista. Si más tarde tiene que dejar de usar modelos Eloquent en un repositorio, deberá asegurarse de que las variables pasadas a sus vistas se comporten de la misma manera o tengan los mismos métodos disponibles; de lo contrario, cambiar sus fuentes de datos se traducirá en cambiar su vistas, y ha perdido (parcialmente) el propósito de abstraer su lógica a los repositorios en primer lugar: la capacidad de mantenimiento de su proyecto se reduce a.

De todos modos, estos son pensamientos algo incompletos. Son, como he dicho, simplemente mi opinión, que resulta ser el resultado de leer Domain Driven Design y ver videos como el discurso principal del "tío Bob" en Ruby Midwest durante el último año.

fideloper
fuente
1
En su opinión, ¿sería una buena alternativa si los repositorios devuelven objetos de transferencia de datos en lugar de objetos elocuentes? Por supuesto, esto implicaría una conversión adicional de elocuente a dto, pero de esta manera, al menos, aísla sus controladores / vistas de la implementación de orm actual.
federivo
1
Yo mismo he experimentado un poco con eso y lo encontré un poco poco práctico. Dicho esto, me gusta esa idea en abstracto. Sin embargo, los objetos Collection de la base de datos de Illuminate actúan como matrices y los objetos Model actúan como objetos StdClass lo suficiente como para que podamos, prácticamente hablando, seguir con Eloquent y seguir usando matrices / objetos en el futuro si es necesario.
fideloper
4
@fideloper Siento que si uso repositorios pierdo toda la belleza del ORM que proporciona Eloquent. Al recuperar un objeto de cuenta a través de mi método de repositorio, $a = $this->account->getById(1)no puedo simplemente encadenar métodos como $a->getActiveUsers(). Bien, podría usar $a->users->..., pero luego estoy devolviendo una colección Eloquent y ningún objeto stdClass y estoy vinculado a Eloquent nuevamente. ¿Cuál es la solución a esto? Declarar otro método en el repositorio de usuarios como $user->getActiveUsersByAccount($a->id);? Me encantaría saber cómo resuelves esto ...
santacruz
1
Los ORM son terribles para la arquitectura de nivel empresarial (ish) porque causan problemas como este. Al final, debes decidir qué tiene más sentido para tu aplicación. Personalmente, cuando uso repositorios con Eloquent (¡el 90% del tiempo!), Uso Eloquent y hago todo lo posible para tratar modelos y colecciones como stdClasses & Arrays (¡porque tú puedes!), Así que si lo necesito, cambiar a otra cosa es posible.
fideloper
5
Siga adelante y use modelos de carga diferida. Puede hacer que los modelos de dominio reales funcionen así si alguna vez omite el uso de Eloquent. Pero en serio, ¿vas a cambiar a Eloquent alguna vez? ¡En por un centavo, en una libra! (¡No se exceda tratando de ceñirse a "las reglas"! Rompo todas las mías todo el tiempo)
fideloper
224

Estoy terminando un gran proyecto usando Laravel 4 y tuve que responder todas las preguntas que me está haciendo en este momento. Después de leer todos los libros de Laravel disponibles en Leanpub y toneladas de Google, se me ocurrió la siguiente estructura.

  1. Una clase de modelo Eloquent por tabla datable
  2. Una clase de repositorio por modelo elocuente
  3. Una clase de servicio que puede comunicarse entre varias clases de repositorio.

Digamos que estoy construyendo una base de datos de películas. Tendría al menos las siguientes clases de Eloquent Model:

  • Película
  • Estudio
  • Director
  • Actor
  • revisión

Una clase de repositorio encapsularía cada clase de Eloquent Model y sería responsable de las operaciones CRUD en la base de datos. Las clases del repositorio podrían verse así:

  • MovieRepository
  • StudioRepository
  • DirectorRepositorio
  • ActorRepositorio
  • ReviewRepository

Cada clase de repositorio ampliaría una clase BaseRepository que implementa la siguiente interfaz:

interface BaseRepositoryInterface
{
    public function errors();

    public function all(array $related = null);

    public function get($id, array $related = null);

    public function getWhere($column, $value, array $related = null);

    public function getRecent($limit, array $related = null);

    public function create(array $data);

    public function update(array $data);

    public function delete($id);

    public function deleteWhere($column, $value);
}

Una clase de servicio se utiliza para unir varios repositorios y contiene la "lógica empresarial" real de la aplicación. Los controladores solo se comunican con las clases de servicio para las acciones de creación, actualización y eliminación.

Entonces, cuando quiero crear un nuevo registro de película en la base de datos, mi clase MovieController puede tener los siguientes métodos:

public function __construct(MovieRepositoryInterface $movieRepository, MovieServiceInterface $movieService)
{
    $this->movieRepository = $movieRepository;
    $this->movieService = $movieService;
}

public function postCreate()
{
    if( ! $this->movieService->create(Input::all()))
    {
        return Redirect::back()->withErrors($this->movieService->errors())->withInput();
    }

    // New movie was saved successfully. Do whatever you need to do here.
}

Depende de usted determinar cómo envía los datos a sus controladores, pero digamos que los datos devueltos por Input :: all () en el método postCreate () se ven así:

$data = array(
    'movie' => array(
        'title'    => 'Iron Eagle',
        'year'     => '1986',
        'synopsis' => 'When Doug\'s father, an Air Force Pilot, is shot down by MiGs belonging to a radical Middle Eastern state, no one seems able to get him out. Doug finds Chappy, an Air Force Colonel who is intrigued by the idea of sending in two fighters piloted by himself and Doug to rescue Doug\'s father after bombing the MiG base.'
    ),
    'actors' => array(
        0 => 'Louis Gossett Jr.',
        1 => 'Jason Gedrick',
        2 => 'Larry B. Scott'
    ),
    'director' => 'Sidney J. Furie',
    'studio' => 'TriStar Pictures'
)

Dado que MovieRepository no debería saber cómo crear registros de Actor, Director o Studio en la base de datos, usaremos nuestra clase MovieService, que podría verse así:

public function __construct(MovieRepositoryInterface $movieRepository, ActorRepositoryInterface $actorRepository, DirectorRepositoryInterface $directorRepository, StudioRepositoryInterface $studioRepository)
{
    $this->movieRepository = $movieRepository;
    $this->actorRepository = $actorRepository;
    $this->directorRepository = $directorRepository;
    $this->studioRepository = $studioRepository;
}

public function create(array $input)
{
    $movieData    = $input['movie'];
    $actorsData   = $input['actors'];
    $directorData = $input['director'];
    $studioData   = $input['studio'];

    // In a more complete example you would probably want to implement database transactions and perform input validation using the Laravel Validator class here.

    // Create the new movie record
    $movie = $this->movieRepository->create($movieData);

    // Create the new actor records and associate them with the movie record
    foreach($actors as $actor)
    {
        $actorModel = $this->actorRepository->create($actor);
        $movie->actors()->save($actorModel);
    }

    // Create the director record and associate it with the movie record
    $director = $this->directorRepository->create($directorData);
    $director->movies()->associate($movie);

    // Create the studio record and associate it with the movie record
    $studio = $this->studioRepository->create($studioData);
    $studio->movies()->associate($movie);

    // Assume everything worked. In the real world you'll need to implement checks.
    return true;
}

Así que lo que nos queda es una separación de preocupaciones agradable y sensata. Los repositorios solo conocen el modelo Eloquent que insertan y recuperan de la base de datos. Los controladores no se preocupan por los repositorios, simplemente entregan los datos que recopilan del usuario y los pasan al servicio apropiado. Al servicio no le importa cómo se guardan los datos que recibe en la base de datos, simplemente entrega los datos relevantes que le dio el controlador a los repositorios apropiados.

Kyle Noland
fuente
8
Este comentario es, con mucho, el enfoque más limpio, escalable y fácil de mantener.
Andreas
4
+1! Eso me ayudará mucho, ¡gracias por compartir con nosotros! Si se pregunta cómo logró validar las cosas dentro de los servicios, si es posible, ¿podría explicar brevemente lo que hizo? ¡Gracias de todos modos! :)
Paulo Freitas
6
Como dijo @PauloFreitas, sería interesante ver cómo maneja la parte de validación, y también me interesaría la parte de excepciones (¿usa excepción, eventos o simplemente maneja esto como parece sugerir en su controlador a través de un retorno booleano en sus servicios?). ¡Gracias!
Nicolas
11
Buen escrito, aunque no estoy seguro de por qué está inyectando movieRepository en MovieController ya que el controlador no debería estar haciendo nada directamente con el repositorio, ni su método postCreate está usando el movieRepository, así que supongo que lo dejó por error ?
davidnknight
15
Pregunta sobre esto: ¿por qué está usando repositorios en este ejemplo? Esta es una pregunta honesta: para mí, parece que estás usando repositorios, pero al menos en este ejemplo, el repositorio no hace nada más que proporcionar la misma interfaz que Eloquent, y al final, todavía estás atado a Eloquent porque su clase de servicio está usando elocuente directamente en él ( $studio->movies()->associate($movie);).
Kevin Mitchell
5

Me gusta pensar en ello en términos de lo que hace mi código y de lo que es responsable, en lugar de "correcto o incorrecto". Así es como separo mis responsabilidades:

  • Los controladores son la capa HTTP y enrutan las solicitudes a las apis subyacentes (también conocido como, controla el flujo)
  • Los modelos representan el esquema de la base de datos y le dicen a la aplicación cómo se ven los datos, qué relaciones pueden tener, así como cualquier atributo global que pueda ser necesario (como un método de nombre para devolver un nombre y apellido concatenados)
  • Los repositorios representan las consultas e interacciones más complejas con los modelos (no hago ninguna consulta sobre los métodos del modelo).
  • Motores de búsqueda: clases que me ayudan a crear consultas de búsqueda complejas.

Con esto en mente, siempre tiene sentido usar un repositorio (si crea interfaces, etc. es un tema completamente diferente). Me gusta este enfoque, porque significa que sé exactamente a dónde ir cuando necesito hacer cierto trabajo.

También tiendo a construir un repositorio base, generalmente una clase abstracta que define los valores predeterminados principales, básicamente operaciones CRUD, y luego cada niño puede simplemente extender y agregar métodos según sea necesario, o sobrecargar los valores predeterminados. Inyectar su modelo también ayuda a que este patrón sea bastante robusto.

Hombre impar
fuente
¿Puede mostrar su implementación de su BaseRepository? De hecho, yo también hago esto y tengo curiosidad por saber qué hiciste.
Odyssee
Piense en getById, getByName, getByTitle, save type methods.etc. - generalmente métodos que se aplican a todos los repositorios dentro de varios dominios.
Oddman
5

Piense en los repositorios como un archivador consistente de sus datos (no solo sus ORM). La idea es que desee capturar datos en una API consistente y fácil de usar.

Si se encuentra haciendo Model :: all (), Model :: find (), Model :: create (), probablemente no se beneficiará mucho de abstraer un repositorio. Por otro lado, si desea aplicar un poco más de lógica empresarial a sus consultas o acciones, es posible que desee crear un repositorio para facilitar el uso de la API para tratar los datos.

Creo que estabas preguntando si un repositorio sería la mejor manera de lidiar con algunas de las sintaxis más detalladas necesarias para conectar modelos relacionados. Dependiendo de la situación, hay algunas cosas que puedo hacer:

  1. Al colgar un nuevo modelo secundario de un modelo principal (uno-uno o uno-muchos), agregaría un método al repositorio secundario createWithParent($attributes, $parentModelInstance)y esto simplemente agregaría el $parentModelInstance->iden el parent_idcampo de los atributos y llamaría a crear.

  2. Adjuntando una relación de muchos-muchos, en realidad creo funciones en los modelos para poder ejecutar $ instance-> attachChild ($ childInstance). Tenga en cuenta que esto requiere elementos existentes en ambos lados.

  3. Al crear modelos relacionados en una sola ejecución, creo algo que llamo Gateway (puede estar un poco alejado de las definiciones de Fowler). Manera en que puedo llamar $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) en lugar de un montón de lógica que puede cambiar o que complicaría la lógica que tengo en un controlador o comando.

Ryan Tablada
fuente