115 votes

Gérer les relations dans Laravel, en respectant le modèle de répertoire

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 repositories pour chaque table de l'application.

J'ai fini par obtenir la structure de table suivante:

  • Étudiants : id, nom
  • Cours : id, nom, teacher_id
  • Enseignants : id, nom
  • Devoirs : id, nom, course_id
  • Notes (agit comme un pivot entre les étudiants et les devoirs) : student_id, assignment_id, note

J'ai des classes de repository avec des méthodes find, create, update et delete pour toutes ces tables. Chaque repository 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.

Lorsque je crée un nouveau cours, tout ce que je fais est d'appeler la méthode create sur le Repository des Cours. Ce cours a des devoirs, donc lors de la création d'un, je veux également créer une entrée dans la table des notes pour chaque étudiant du cours. Je fais cela à travers le Repository des Devoirs. Cela implique que le repository des devoirs communique avec deux modèles Eloquent, avec le modèle des Devoirs et des Étudiants..

Ma question est la suivante : étant donné que cette application va probablement prendre de l'ampleur et que d'autres relations seront introduites, est-il bon de pratique de communiquer avec différents modèles Eloquent dans les repositories ou cela devrait-il être fait en utilisant d'autres repositories à la place (je veux dire appeler d'autres repositories depuis le repository des Devoirs) ou cela devrait-il être fait dans les modèles Eloquent tous ensemble ?

Aussi, est-il bon de pratique d'utiliser la table des notes comme un pivot entre les devoirs et les étudiants ou cela devrait-il être fait ailleurs ?

214voto

Kyle Noland Points 1284

Je suis en train de finaliser un grand projet en utilisant Laravel 4 et j'ai dû répondre à toutes les questions que vous posez actuellement. Après avoir lu tous les livres Laravel disponibles sur Leanpub et fait des tonnes de recherches sur Google, j'ai mis en place la structure suivante.

  1. Une classe de modèle Eloquent par table de base de données
  2. Une classe de dépôt par modèle Eloquent
  3. Une classe de service qui peut communiquer entre plusieurs classes de dépôt.

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

  • Film
  • Studio
  • Réalisateur
  • Acteur
  • Critique

Une classe de dépôt encapsulerait chaque classe de modèle Eloquent et serait responsable des opérations CRUD sur la base de données. Les classes de dépôt pourraient ressembler à ceci :

  • DepotFilm
  • DepotStudio
  • DepotRealisateur
  • DepotActeur
  • DepotCritique

Chaque classe de dépôt é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 de service est utilisée pour assembler plusieurs dépôts et contient la vraie "logique métier" de l'application. Les contrôleurs communiquent uniquement avec les classes de service pour les actions de création, de mise à jour et de suppression.

Donc quand je veux créer un nouveau enregistrement de Film dans la base de données, ma classe MovieController pourrait 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();
    }

    // Le nouveau film a été enregistré avec succès. Faites ce que vous avez à faire ici.
}

C'est à vous de déterminer comment vous envoyez des données POST à 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' => 'Quand le père de Doug, un pilote de l'Air Force, est abattu par des MiGs appartenant à un État du Moyen-Orient radical, personne ne semble capable de le sortir. Doug trouve Chappy, un colonel de l'Air Force intrigué par l'idée d'envoyer deux chasseurs pilotés par lui-même et Doug pour secourir le père de Doug après avoir bombardé la base MiG.'
    ),
    'actors' => array(
        0 => 'Louis Gossett Jr.',
        1 => 'Jason Gedrick',
        2 => 'Larry B. Scott'
    ),
    'director' => 'Sidney J. Furie',
    'studio' => 'TriStar Pictures'
)

Puisque le MovieRepository ne devrait pas savoir comment créer des enregistrements Acteur, Réalisateur 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'];

    // Dans un exemple plus complet, vous voudriez probablement mettre en œuvre des transactions de base de données et effectuer une validation des données en utilisant la classe Laravel Validator ici.

    // Créez le nouvel enregistrement de film
    $movie = $this->movieRepository->create($movieData);

    // Créez les nouveaux enregistrements d'acteur et associez-les à l'enregistrement de film
    foreach($actors as $actor)
    {
        $actorModel = $this->actorRepository->create($actor);
        $movie->actors()->save($actorModel);
    }

    // Créez l'enregistrement du réalisateur et associez-le à l'enregistrement de film
    $director = $this->directorRepository->create($directorData);
    $director->movies()->associate($movie);

    // Créez l'enregistrement du studio et associez-le à l'enregistrement de film
    $studio = $this->studioRepository->create($studioData);
    $studio->movies()->associate($movie);

    // Supposons que tout a fonctionné. Dans le monde réel, vous devrez implémenter des vérifications.
    return true;
}

Il nous reste donc une belle et sensée séparation des préoccupations. Les dépôts ne sont conscients que du modèle Eloquent qu'ils insèrent et récupèrent de la base de données. Les contrôleurs se désintéressent des dépôts, ils transmettent simplement les données collectées auprès de l'utilisateur et les passent au service approprié. Le service ne se soucie pas comment les données qu'il reçoit sont enregistrées dans la base de données, il transmet simplement les données pertinentes qui lui ont été données par le contrôleur aux dépôts appropriés.

69voto

fideloper Points 6471

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

Voici la mienne :

TL;DR: Oui, c'est bien.

Vous faites du bon travail!

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

Cependant, j'organise souvent les dépôts autour de la logique métier au lieu d'avoir un dépôt par table. Cela est utile car il s'agit d'un point de vue centré sur la façon dont votre application devrait résoudre votre "problème métier".

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

Votre dépôt de "Cours" devrait être capable de renvoyer un Cours et les attributs / Assignations du cours (y compris l'Assignation).

Vous pouvez accomplir cela avec Eloquent, heureusement.

(Je me retrouve souvent avec un dépôt par table, mais certains dépôts sont beaucoup plus utilisés que d'autres, et donc ont beaucoup plus de méthodes. Votre dépôt de "cours" peut être beaucoup plus complet que votre dépôt d'Assignations, par exemple, si votre application est plus centrée sur les Cours et moins sur la collection d'Assignations d'un Cours).

La partie délicate

J'utilise souvent des dépôts à l'intérieur de mes dépôts pour effectuer certaines actions de base de données.

Tout dépôt qui implémente Eloquent pour gérer les données renverra probablement des modèles Eloquent. Dans cette optique, il est bon que votre modèle de Cours utilise des relations intégrées pour récupérer ou enregistrer des Assignations (ou tout autre cas d'utilisation). Notre "implémentation" est basée sur Eloquent.

D'un point de vue pratique, cela a du sens. Il est peu probable que nous changions de sources de données pour 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, du moins pour moi, est de déterminer si Eloquent nous aide réellement ou nous nuit. Les ORM sont un sujet délicat, car bien qu'ils nous aident grandement d'un point de vue pratique, ils lient également votre code des "entités de logique métier" avec le code effectuant la récupération des données.

Cela brouille un peu la responsabilité de votre dépôt, qui est en fait de traiter les données ou de gérer la récupération / mise à jour des entités (entités du domaine métier).

De plus, ils agissent comme les objets mêmes que vous passez à vos vues. Si vous devez plus tard cesser d'utiliser des modèles Eloquent dans un dépôt, vous devrez vous assurer que les variables transmises à vos vues se comportent de la même manière ou disposent des mêmes méthodes disponibles, sinon le changement de vos sources de données entraînera des modifications dans vos vues, et vous avez (partiellement) perdu le but d'abstraction de votre logique vers des dépôts à l'origine - la maintenabilité de votre projet en pâtit.

En tout cas, ce ne sont que des réflexions incomplètes. Ce sont, comme indiqué, simplement mon opinion, qui est le résultat de la lecture de Domain Driven Design et du visionnage de vidéos comme la conférence de "l'oncle bob" à Ruby Midwest au cours de la dernière année.

5voto

Ryan Tablada Points 91

Pensez aux référentiels comme un classeur cohérent de vos données (pas seulement vos ORMs). L'idée est que vous voulez récupérer des données dans une API simple à utiliser de manière cohérente.

Si vous vous retrouvez simplement à faire Model::all(), Model::find(), Model::create(), vous ne tirerez probablement pas beaucoup de bénéfices en abstraction en utilisant un référentiel. En revanche, si vous souhaitez ajouter un peu plus de logique métier à vos requêtes ou actions, vous voudrez peut-être créer un référentiel pour faciliter l'utilisation de l'API pour traiter les données.

Je pense que vous demandiez si un référentiel serait le meilleur moyen de gérer certaines des syntaxes plus verbeuses nécessaires pour connecter des modèles liés. Selon la situation, voici quelques choses que je pourrais faire:

  1. Pour accrocher 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, $instanceDeModeleParent) et cela ajouterait simplement $instanceDeModeleParent->id dans le champ parent_id des attributs et appellerait la création.

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

  3. Pour créer des modèles liés en une seule fois, je crée quelque chose que j'appelle une passerelle (cela peut être un peu éloigné des définitions de Fowler). Ainsi je peux appeler $gateway->createParentAndChild($attributsParent, $attributsEnfant) au lieu d'une série de logiques qui pourraient changer ou compliquer la logique que j'ai dans un contrôleur ou une commande.

5voto

Oddman Points 567

J'aime penser en termes de ce que mon code fait et de ce pour quoi il est responsable, plutôt que de "bien ou mal". C'est ainsi que je divise mes responsabilités :

  • Les contrôleurs sont la couche HTTP et routent les demandes vers les API sous-jacentes (c'est-à-dire, ils contrôlent 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, quelles relations elles peuvent avoir, ainsi que les attributs globaux qui peuvent être nécessaires (comme une méthode nom pour retourner un nom concaténé de famille et de prénom)
  • Les dépôts représentent les requêtes plus complexes et les interactions avec les modèles (je ne fais pas de requêtes sur les méthodes des modèles).
  • Les moteurs de recherche - des classes qui m'aident à construire des requêtes de recherche complexes.

Avec cela à l'esprit, cela a du sens à chaque fois d'utiliser un dépôt (que vous créiez des interfaces, etc. est tout un autre sujet). J'aime cette approche, car cela signifie que je sais exactement où aller lorsque j'ai besoin de faire un certain travail.

J'ai aussi tendance à construire un dépôt de base, généralement une classe abstraite qui définit les valeurs par défaut principales - essentiellement les opérations CRUD, et ensuite chaque enfant peut simplement étendre et ajouter des méthodes au besoin, ou surcharger les valeurs par défaut. L'injection de votre modèle aide également à rendre ce modèle assez robuste.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X