Gérer les relations dans Laravel, en adhérant au modèle de référentiel

120

En créant une application dans Laravel 4 après avoir lu le livre de T.Otwell sur les bons modèles de conception dans Laravel, je me suis retrouvé à créer des référentiels pour chaque table de l'application.

J'ai fini avec la structure de table suivante:

  • Étudiants: identifiant, nom
  • Cours: identifiant, nom, id_enseignant
  • Enseignants: identifiant, nom
  • Devoirs: id, nom, course_id
  • Scores (agit comme un pivot entre les étudiants et les devoirs): student_id, assignment_id, scores

J'ai des classes de référentiel avec des méthodes de recherche, de création, de mise à jour et de suppression pour toutes ces tables. Chaque référentiel a un modèle Eloquent qui interagit avec la base de données. Les relations sont définies dans le modèle selon la documentation de Laravel: http://laravel.com/docs/eloquent#relationships .

Lors de la création d'un nouveau cours, je ne fais qu'appeler la méthode create sur le référentiel de cours. Ce cours a des devoirs, donc lors de la création d'un, je souhaite également créer une entrée dans le tableau des scores pour chaque étudiant du cours. Je fais cela via le référentiel d'affectation. Cela implique que le référentiel de devoirs communique avec deux modèles Eloquent, avec le modèle Assignment et Student.

Ma question est la suivante: comme cette application augmentera probablement en taille et que plus de relations seront introduites, est-ce une bonne pratique de communiquer avec différents modèles Eloquent dans les référentiels ou devrait-on le faire en utilisant d'autres référentiels à la place (je veux dire appeler d'autres référentiels à partir du référentiel d'affectation ) ou devrait-il être fait dans les modèles Eloquent tous ensemble?

De plus, est-ce une bonne pratique d'utiliser le tableau des scores comme pivot entre les devoirs et les étudiants ou devrait-il être fait ailleurs?

ehp
la source

Réponses:

71

Gardez à l'esprit que vous demandez des opinions: D

Voici la mienne:

TL; DR: Oui, c'est très bien.

Tu t'en sors bien!

Je fais exactement ce que vous faites souvent et je trouve que cela fonctionne très bien.

Cependant, j'organise souvent les référentiels autour de la logique métier au lieu d'avoir un référentiel par table. Ceci est utile car c'est un point de vue centré sur la manière dont votre application doit résoudre votre "problème commercial".

Un cours est une «entité», avec des attributs (titre, id, etc.) et même d'autres entités (devoirs, qui ont leurs propres attributs et éventuellement des entités).

Votre référentiel "Cours" doit pouvoir renvoyer un cours et les attributs / devoirs des cours (y compris les devoirs).

Vous pouvez accomplir cela avec Eloquent, heureusement.

(Je me retrouve souvent avec un référentiel par table, mais certains référentiels sont beaucoup plus utilisés que d'autres, et ont donc beaucoup plus de méthodes. Votre référentiel "cours" peut être beaucoup plus complet que votre référentiel Devoirs, par exemple, si votre l'application se concentre davantage sur les cours et moins sur la collection de devoirs d'un cours).

La partie délicate

J'utilise souvent des référentiels à l'intérieur de mes référentiels afin d'effectuer certaines actions sur la base de données.

Tout référentiel qui implémente Eloquent afin de gérer des données renverra probablement des modèles Eloquent. Dans cette optique, il n'y a rien de mal si votre modèle de cours utilise des relations intégrées afin de récupérer ou d'enregistrer des devoirs (ou tout autre cas d'utilisation). Notre «implémentation» est construite autour d'Eloquent.

D'un point de vue pratique, cela a du sens. Il est peu probable que nous changions les sources de données en quelque chose qu'Eloquent ne peut pas gérer (vers une source de données non SQL).

ORMS

La partie la plus délicate de cette configuration, pour moi du moins, est de déterminer si Eloquent nous aide ou nous nuit réellement. Les ORM sont un sujet délicat, car bien qu'ils nous aident grandement d'un point de vue pratique, ils couplent également votre code «d'entités de logique métier» avec le code effectuant la récupération des données.

Ce genre de confusion permet de savoir si la responsabilité de votre référentiel est réellement de gérer les données ou de gérer la récupération / mise à jour d'entités (entités du domaine métier).

De plus, ils agissent comme les objets mêmes que vous transmettez à vos vues. Si vous devez par la suite éviter d'utiliser les modèles Eloquent dans un référentiel, vous devrez vous assurer que les variables transmises à vos vues se comportent de la même manière ou que les mêmes méthodes sont disponibles, sinon la modification de vos sources de données entraînera la modification de votre vues, et vous avez (partiellement) perdu l'objectif de l'abstraction de votre logique vers les référentiels en premier lieu - la maintenabilité de votre projet diminue comme.

Quoi qu'il en soit, ce sont des pensées quelque peu incomplètes. Ils ne sont, comme indiqué, que mon opinion, qui se trouve être le résultat de la lecture de Domain Driven Design et du visionnage de vidéos comme la keynote de «oncle bob» à Ruby Midwest au cours de l'année dernière.

fideloper
la source
1
À votre avis, serait-ce une bonne alternative si les référentiels renvoient des objets de transfert de données au lieu d'objets éloquents? Bien sûr, cela impliquerait une conversion supplémentaire d'éloquent en dto, mais de cette façon, au moins, vous isolez vos contrôleurs / vues de l'implémentation orm actuelle.
federivo
1
J'ai expérimenté cela moi-même un peu et je l'ai trouvé un peu peu pratique. Cela étant dit, j'aime cette idée dans l'abstrait. Cependant, les objets Collection de base de données d'Illuminate agissent comme les tableaux et les objets Model agissent suffisamment comme les objets StdClass pour que nous puissions, en pratique, nous en tenir à Eloquent et continuer à utiliser des tableaux / objets à l'avenir si nous en avons besoin.
fideloper
4
@fideloper J'ai le sentiment que si j'utilise des référentiels, je perds toute la beauté de l'ORM qu'offre Eloquent. Lors de la récupération d'un objet de compte via ma méthode de référentiel, $a = $this->account->getById(1)je ne peux pas simplement enchaîner des méthodes comme $a->getActiveUsers(). D'accord, je pourrais utiliser $a->users->..., mais je retourne une collection Eloquent et aucun objet stdClass et je suis à nouveau lié à Eloquent. Quelle est la solution à cela? Déclarer une autre méthode dans le référentiel utilisateur comme $user->getActiveUsersByAccount($a->id);? J'adorerais entendre comment vous résolvez cela ...
Santacruz
1
Les ORM sont terribles pour l'architecture de niveau Enterprise (ish) car ils causent des problèmes comme celui-ci. En fin de compte, vous devez décider de ce qui a le plus de sens pour votre application. Personnellement, lorsque j'utilise des référentiels avec Eloquent (90% du temps!) J'utilise Eloquent et je fais de mon mieux pour traiter des modèles et des collections comme stdClasses & Arrays (parce que vous pouvez!) Donc si j'en ai besoin, il est possible de passer à autre chose.
fideloper
5
Allez-y et utilisez des modèles chargés paresseusement. Vous pouvez faire en sorte que de vrais modèles de domaine fonctionnent comme ça si jamais vous évitez d'utiliser Eloquent. Mais sérieusement, allez- vous jamais changer Eloquent? Pour un sou, pour une livre! (N'allez pas trop loin en essayant de vous en tenir aux "règles"! Je brise toutes les miennes tout le temps).
fideloper
224

Je termine un grand projet avec Laravel 4 et je devais répondre à toutes les questions que vous vous posez en ce moment. Après avoir lu tous les livres Laravel disponibles sur Leanpub, et des tonnes de googling, j'ai trouvé la structure suivante.

  1. Une classe Eloquent Model par table datable
  2. Une classe de référentiel par modèle éloquent
  3. Une classe de service qui peut communiquer entre plusieurs classes de référentiel.

Alors disons que je construis une base de données de films. J'aurais au moins les classes suivantes de modèle éloquent:

  • Film
  • Studio
  • Réalisateur
  • Acteur
  • La revue

Une classe de référentiel encapsulerait chaque classe Eloquent Model et serait responsable des opérations CRUD sur la base de données. Les classes du référentiel peuvent ressembler à ceci:

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActeurRepository
  • ReviewRepository

Chaque classe de référentiel étendrait une classe BaseRepository qui implémente l'interface suivante:

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

Une classe Service est utilisée pour coller plusieurs référentiels ensemble et contient la véritable «logique métier» de l'application. Les contrôleurs communiquent uniquement avec les classes de service pour les actions Créer, Mettre à jour et Supprimer.

Ainsi, lorsque je souhaite créer un nouvel enregistrement de film dans la base de données, ma classe MovieController peut avoir les méthodes suivantes:

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.
}

C'est à vous de déterminer comment vous POSTEZ les données sur vos contrôleurs, mais disons que les données renvoyées par Input :: all () dans la méthode postCreate () ressemblent à ceci:

$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'
)

Étant donné que MovieRepository ne doit pas savoir comment créer des enregistrements Actor, Director ou Studio dans la base de données, nous utiliserons notre classe MovieService, qui pourrait ressembler à ceci:

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

Il nous reste donc une belle séparation judicieuse des préoccupations. Les référentiels ne connaissent que le modèle Eloquent qu'ils insèrent et récupèrent de la base de données. Les contrôleurs ne se soucient pas des référentiels, ils transmettent simplement les données qu'ils collectent auprès de l'utilisateur et les transmettent au service approprié. Le service ne se soucie pas de la manière dont les données qu'il reçoit sont enregistrées dans la base de données, il transmet simplement les données pertinentes qu'il a été fournies par le contrôleur aux référentiels appropriés.

Kyle Noland
la source
8
Ce commentaire est de loin l'approche la plus propre, la plus évolutive et la plus maintenable.
Andreas
4
+1! Cela m'aidera beaucoup, merci de partager avec nous! Vous vous demandez comment vous avez réussi à valider les choses à l'intérieur des services, si possible, pourriez-vous expliquer brièvement ce que vous avez fait? Merci quand même! :)
Paulo Freitas
6
Comme @PauloFreitas l'a dit, il serait intéressant de voir comment vous gérez la partie validation, et je serais également intéressé par la partie exceptions (utilisez-vous des exceptions, des événements ou gérez-vous simplement cela comme vous semblez le suggérer dans votre contrôleur via un retour booléen dans vos services?). Merci!
Nicolas
11
Bonne rédaction, même si je ne sais pas pourquoi vous injectez movieRepository dans MovieController car le contrôleur ne devrait rien faire directement avec le référentiel, ni votre méthode postCreate utilisant le movieRepository, donc je suppose que vous l'avez laissé par erreur ?
davidnknight
15
Question à ce sujet: pourquoi utilisez-vous des référentiels dans cet exemple? C'est une question honnête - pour moi, il semble que vous utilisez des référentiels, mais au moins dans cet exemple, le référentiel ne fait vraiment rien d'autre que fournir la même interface qu'Eloquent, et à la fin vous êtes toujours lié à Eloquent parce que votre classe de service utilise éloquent directement dans it ( $studio->movies()->associate($movie);).
Kevin Mitchell
5

J'aime y penser en termes de ce que fait mon code et de ce dont il est responsable, plutôt que de «bien ou de mal». C'est ainsi que je dissocie mes responsabilités:

  • Les contrôleurs sont la couche HTTP et acheminent les requêtes vers les API sous-jacentes (c'est-à-dire, il contrôle le flux)
  • Les modèles représentent le schéma de la base de données et indiquent à l'application à quoi ressemblent les données, les relations qu'elles peuvent avoir, ainsi que tous les attributs globaux qui peuvent être nécessaires (comme une méthode de nom pour renvoyer un prénom et un nom concaténés)
  • Les référentiels représentent les requêtes et interactions les plus complexes avec les modèles (je ne fais aucune requête sur les méthodes de modèle).
  • Moteurs de recherche - classes qui m'aident à créer des requêtes de recherche complexes.

Dans cet esprit, il est logique à chaque fois d'utiliser un référentiel (que vous créiez interfaces.etc. Est un tout autre sujet). J'aime cette approche, car cela signifie que je sais exactement où aller lorsque j'ai besoin d'effectuer certains travaux.

J'ai aussi tendance à créer un référentiel de base, généralement une classe abstraite qui définit les principales valeurs par défaut - essentiellement des opérations CRUD, et chaque enfant peut simplement étendre et ajouter des méthodes si nécessaire, ou surcharger les valeurs par défaut. L'injection de votre modèle permet également à ce modèle d'être assez robuste.

Oddman
la source
Pouvez-vous montrer votre implémentation de votre BaseRepository? Je fais ça aussi et je suis curieux de savoir ce que vous avez fait.
Odyssee
Pensez à getById, getByName, getByTitle, enregistrez le type methods.etc. - généralement des méthodes qui s'appliquent à tous les référentiels dans divers domaines.
Oddman le
5

Considérez les référentiels comme un classeur cohérent de vos données (pas seulement vos ORM). L'idée est que vous souhaitez récupérer des données dans une API cohérente et simple à utiliser.

Si vous ne faites que Model :: all (), Model :: find (), Model :: create (), vous ne bénéficierez probablement pas beaucoup de l'abstraction d'un référentiel. D'autre part, si vous souhaitez appliquer un peu plus de logique métier à vos requêtes ou actions, vous pouvez créer un référentiel pour faciliter l'utilisation d'une API pour traiter les données.

Je pense que vous vous demandiez si un référentiel serait le meilleur moyen de traiter certaines des syntaxes les plus verbeuses requises pour connecter des modèles connexes. Selon la situation, il y a quelques choses que je peux faire:

  1. En accrochant un nouveau modèle enfant à un modèle parent (un-un ou un-plusieurs), j'ajouterais une méthode au référentiel enfant quelque chose comme createWithParent($attributes, $parentModelInstance)et cela ajouterait simplement le $parentModelInstance->iddans le parent_idchamp des attributs et appellerais create.

  2. En attachant une relation plusieurs-plusieurs, je crée en fait des fonctions sur les modèles afin de pouvoir exécuter $ instance-> attachChild ($ childInstance). Notez que cela nécessite des éléments existants des deux côtés.

  3. En créant des modèles associés en une seule fois, je crée quelque chose que j'appelle une passerelle (cela peut être un peu différent des définitions de Fowler). Façon que je peux appeler $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) au lieu d'un tas de logique qui peut changer ou qui compliquerait la logique que j'ai dans un contrôleur ou une commande.

Ryan Tablada
la source