Question Comment un modèle devrait-il être structuré en MVC?


Je commence juste à comprendre le framework MVC et je me demande souvent quelle quantité de code devrait être dans le modèle. J'ai tendance à avoir une classe d'accès aux données qui a des méthodes comme celle-ci:

public function CheckUsername($connection, $username)
{
    try
    {
        $data = array();
        $data['Username'] = $username;

        //// SQL
        $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE Username = :Username";

        //// Execute statement
        return $this->ExecuteObject($connection, $sql, $data);
    }
    catch(Exception $e)
    {
        throw $e;
    }
}

Mes modèles ont tendance à être une classe d'entité mappée à la table de base de données.

L'objet de modèle doit-il avoir toutes les propriétés mappées de la base de données ainsi que le code ci-dessus ou est-il correct de séparer ce code de la base de données?

Vais-je finir par avoir quatre couches?


510
2018-05-03 00:28


origine


Réponses:


Avertissement: Ce qui suit est une description de la façon dont je comprends les modèles de type MVC dans le contexte des applications Web basées sur PHP. Tous les liens externes utilisés dans le contenu expliquent les termes et les concepts, et ne pas impliquer ma propre crédibilité sur le sujet.

La première chose que je dois éclaircir est: le modèle est une couche.

Deuxièmement: il y a une différence entre MVC classique et ce que nous utilisons dans le développement web. Voici un peu une réponse plus ancienne que j'ai écrit, qui décrit brièvement comment ils sont différents.

Qu'est-ce qu'un modèle n'est PAS:

Le modèle n'est pas une classe ou un objet unique. C'est une erreur très commune à faire (Je l'ai fait aussi, bien que la réponse originale ait été écrite quand j'ai commencé à apprendre autrement), parce que la plupart des cadres perpétuent cette idée fausse.

Il ne s'agit pas non plus d'une technique ORM (Object-Relational Mapping) ni d'une abstraction des tables de base de données. Toute personne qui vous dit le contraire est plus susceptible d'essayer de 'vendre' un autre ORM flambant neuf ou un cadre complet.

Qu'est-ce qu'un modèle est:

Dans une adaptation MVC correcte, le M contient toute la logique métier du domaine et le Couche de modèle est la plupart fabriqué à partir de trois types de structures:

  • Objets de domaine

    Un objet de domaine est un conteneur logique d'informations purement de domaine; il représente généralement une entité logique dans l'espace du domaine de problème. Communément appelé logique métier.

    Ce serait là que vous définissez comment valider les données avant d'envoyer une facture, ou de calculer le coût total d'une commande. En même temps, Objets de domaine sont complètement inconscients du stockage - ni de  (Base de données SQL, API REST, fichier texte, etc.) ni même si ils sont sauvés ou récupérés.

  • Data Mappers

    Ces objets sont uniquement responsables du stockage. Si vous stockez des informations dans une base de données, ce sera là que réside le SQL. Ou peut-être utilisez-vous un fichier XML pour stocker des données, et votre Data Mappers sont l'analyse de et vers des fichiers XML.

  • Prestations de service

    Vous pouvez les considérer comme des "objets de domaine de niveau supérieur", mais au lieu de la logique métier, Prestations de service sont responsables de l'interaction entre Objets de domaine et Mappers. Ces structures finissent par créer une interface "publique" pour interagir avec la logique métier du domaine. Vous pouvez les éviter, mais à la peine de divulguer une certaine logique de domaine dans Contrôleurs.

    Il y a une réponse connexe à ce sujet dans le Implémentation ACL question - cela pourrait être utile.

La communication entre la couche modèle et les autres parties de la triade MVC ne doit se faire que par Prestations de service. La séparation claire a quelques avantages supplémentaires:

  • il aide à appliquer le principe de responsabilité unique (SRP)
  • fournit une «marge de manœuvre» supplémentaire au cas où la logique change
  • maintient le contrôleur aussi simple que possible
  • donne un schéma clair, si vous avez besoin d'une API externe

Comment interagir avec un modèle?

Conditions préalables: regarder des conférences "État mondial et singletons" et "Ne cherchez pas les choses!" des discussions sur le code propre.

Accéder aux instances de service

Pour les deux Vue et Manette instances (ce que vous pourriez appeler: "couche d'interface utilisateur") pour avoir accès à ces services, il existe deux approches générales:

  1. Vous pouvez injecter directement les services requis dans les constructeurs de vos vues et contrôleurs, de préférence en utilisant un conteneur DI.
  2. Utiliser une usine pour les services comme dépendance obligatoire pour toutes vos vues et contrôleurs.

Comme vous pouvez le suspecter, le conteneur DI est une solution beaucoup plus élégante (tout en n'étant pas la plus facile pour un débutant). Les deux bibliothèques, que je recommande d'envisager pour cette fonctionnalité serait autonome de Syfmony Composant DependencyInjection ou Auryn.

Les solutions utilisant une fabrique et un conteneur DI vous permettent également de partager les instances des différents serveurs à partager entre le contrôleur sélectionné et la vue pour un cycle requête-réponse donné.

Modification de l'état du modèle

Maintenant que vous pouvez accéder à la couche de modèle dans les contrôleurs, vous devez commencer à les utiliser:

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $identity = $this->identification->findIdentityByEmailAddress($email);
    $this->identification->loginWithPassword(
        $identity,
        $request->get('password')
    );
}

Vos contrôleurs ont une tâche très claire: prendre l'entrée de l'utilisateur et, en fonction de cette entrée, changer l'état actuel de la logique métier. Dans cet exemple, les états qui sont modifiés sont "utilisateur anonyme" et "utilisateur connecté".

Le contrôleur n'est pas responsable de la validation de l'entrée de l'utilisateur, car cela fait partie des règles métier et le contrôleur n'appelle certainement pas les requêtes SQL, comme ce que vous verriez ici ou ici (S'il vous plaît ne les déteste pas, ils sont égarés, pas mal).

Montrer à l'utilisateur le changement d'état.

Ok, l'utilisateur s'est connecté (ou a échoué). Maintenant quoi? Cet utilisateur n'en est toujours pas conscient. Vous devez donc produire une réponse et c'est la responsabilité d'un point de vue.

public function postLogin()
{
    $path = '/login';
    if ($this->identification->isUserLoggedIn()) {
        $path = '/dashboard';
    }
    return new RedirectResponse($path); 
}

Dans ce cas, la vue a produit l'une des deux réponses possibles, en fonction de l'état actuel de la couche du modèle. Pour un cas d'utilisation différent, vous aurez la vue de choisir différents modèles à rendre, sur la base de quelque chose comme "actuellement sélectionné de l'article".

La couche de présentation peut effectivement devenir assez élaborée, comme décrit ici: Comprendre les vues MVC en PHP.

Mais je fais juste une API REST!

Bien sûr, il y a des situations, quand c'est trop.

MVC est juste une solution concrète pour Séparation des préoccupations principe. MVC sépare l'interface utilisateur de la logique métier et, dans l'interface utilisateur, elle sépare la gestion de l'entrée utilisateur et de la présentation. C'est crucial. Bien que les gens le décrivent souvent comme une «triade», ce n'est pas composé de trois parties indépendantes. La structure est plus comme ceci:

MVC separation

Cela signifie que lorsque la logique de votre couche de présentation est quasiment inexistante, l'approche pragmatique consiste à les conserver en tant que couche unique. Il peut également considérablement simplifier certains aspects de la couche du modèle.

En utilisant cette approche, l'exemple de connexion (pour une API) peut être écrit comme:

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $data = [
        'status' => 'ok',
    ];
    try {
        $identity = $this->identification->findIdentityByEmailAddress($email);
        $token = $this->identification->loginWithPassword(
            $identity,
            $request->get('password')
        );
    } catch (FailedIdentification $exception) {
        $data = [
            'status' => 'error',
            'message' => 'Login failed!',
        ]
    }

    return new JsonResponse($data);
}

Bien que cela ne soit pas durable, lorsque vous avez une logique compliquée pour rendre un corps de réponse, cette simplification est très utile pour des scénarios plus triviaux. Mais être averti, cette approche deviendra un cauchemar, en essayant d'utiliser dans de grandes bases de code avec une logique de présentation complexe.

Comment construire le modèle?

Comme il n'y a pas une seule classe "Model" (comme expliqué ci-dessus), vous n'avez vraiment pas "construire le modèle". Au lieu de cela, vous commencez à faire Prestations de service, qui sont capables d'effectuer certaines méthodes. Et ensuite mettre en œuvre Objets de domaine et Mappers.

Un exemple de méthode de service:

Dans les deux approches ci-dessus, il y avait cette méthode de connexion pour le service d'identification. A quoi ressemblerait-il réellement? J'utilise une version légèrement modifiée de la même fonctionnalité de une bibliothèque, que j'ai écrit .. parce que je suis paresseux:

public function loginWithPassword(Identity $identity, string $password): string
{
    if ($identity->matchPassword($password) === false) {
        $this->logWrongPasswordNotice($identity, [
            'email' => $identity->getEmailAddress(),
            'key' => $password, // this is the wrong password
        ]);

        throw new PasswordMismatch;
    }

    $identity->setPassword($password);
    $this->updateIdentityOnUse($identity);
    $cookie = $this->createCookieIdentity($identity);

    $this->logger->info('login successful', [
        'input' => [
            'email' => $identity->getEmailAddress(),
        ],
        'user' => [
            'account' => $identity->getAccountId(),
            'identity' => $identity->getId(),
        ],
    ]);

    return $cookie->getToken();
}

Comme vous pouvez le voir, à ce niveau d'abstraction, il n'y a aucune indication de l'endroit où les données ont été extraites. Il pourrait s'agir d'une base de données, mais il pourrait également s'agir d'un objet simulé à des fins de test. Même les mappeurs de données, qui sont utilisés pour cela, sont cachés dans le private méthodes de ce service.

private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
    $identity->setStatus($status);
    $identity->setLastUsed(time());
    $mapper = $this->mapperFactory->create(Mapper\Identity::class);
    $mapper->store($identity);
}

Les moyens de créer des mappeurs

Pour implémenter une abstraction de la persistance, les approches les plus flexibles consistent à créer des des mappeurs de données.

Mapper diagram

De: PoEAA livre

En pratique, ils sont mis en œuvre pour l'interaction avec des classes ou superclasses spécifiques. Disons que vous avez Customer et Admin dans votre code (héritant d'un User superclasse). Les deux finiraient probablement par avoir un mappeur de correspondance séparé, car ils contiennent des champs différents. Mais vous finirez également avec des opérations partagées et couramment utilisées. Par exemple: mettre à jour le "vu en ligne" temps. Et au lieu de rendre les mappers existants plus compliqués, l’approche plus pragmatique consiste à avoir un "User Mapper" général, qui ne met à jour que cet horodatage.

Quelques commentaires supplémentaires:

  1. Tables de base de données et modèle

    Bien qu'il y ait parfois une relation directe 1: 1: 1 entre une table de base de données, Objet de domaine, et Mappeur, dans les grands projets, il pourrait être moins fréquent que prévu:

    • Informations utilisées par un seul Objet de domaine peut être mappé à partir de tables différentes, alors que l'objet lui-même n'a pas de persistance dans la base de données.

      Exemple: si vous générez un rapport mensuel. Cela permettrait de recueillir des informations de différentes tables, mais il n'y a pas de magie MonthlyReport table dans la base de données.

    • Un célibataire ou Individual Mappeur peut affecter plusieurs tables.

      Exemple: lorsque vous stockez des données du User objet, ceci Objet de domaine pourrait contenir la collection d'autres objets de domaine - Group instances. Si vous les modifiez et stockez User, la Data Mapper devra mettre à jour et / ou insérer des entrées dans plusieurs tables.

    • Données d'un seul Objet de domaine est stocké dans plus d'une table.

      Exemple: dans les grands systèmes (pensez à un réseau social de taille moyenne), il peut être pragmatique de stocker les données d'authentification des utilisateurs et les données souvent consultées séparément des blocs de contenu plus importants, ce qui est rarement nécessaire. Dans ce cas, vous pourriez toujours avoir un seul User class, mais les informations qu'il contient dépendraient du fait que tous les détails aient été récupérés.

    • Pour chaque Objet de domaine il peut y avoir plus d'un mappeur

      Exemple: vous disposez d'un site d'actualités avec un code partagé pour les logiciels de gestion publique et de gestion de l'information. Mais, alors que les deux interfaces utilisent le même Article classe, la gestion a besoin de beaucoup plus d'informations. Dans ce cas, vous auriez deux mappeurs distincts: "interne" et "externe". Chacun effectuant des requêtes différentes, ou même utilisant des bases de données différentes (comme dans le maître ou l'esclave).

  2. Une vue n'est pas un modèle

    Vue les instances dans MVC (si vous n'utilisez pas la variation MVP du modèle) sont responsables de la logique de présentation. Cela signifie que chaque Vue jonglera généralement avec au moins quelques modèles. Il acquiert des données de Couche de modèle puis, en fonction des informations reçues, choisit un modèle et définit des valeurs.

    L'un des avantages que vous en retirerez est la réutilisation. Si vous créez un ListView classe, puis, avec un code bien écrit, vous pouvez avoir la même classe en remettant la présentation de la liste d'utilisateurs et des commentaires ci-dessous un article. Parce qu'ils ont tous les deux la même logique de présentation. Vous venez de changer de modèle.

    Vous pouvez utiliser soit modèles PHP natifs ou utiliser un moteur de template tiers. Il pourrait également y avoir des bibliothèques tierces, qui sont en mesure de remplacer complètement Vue instances.

  3. Qu'en est-il de l'ancienne version de la réponse?

    Le seul changement majeur est que, ce qu'on appelle Modèle dans l'ancienne version, est en fait un Un service. Le reste de "l'analogie de la bibliothèque" se maintient plutôt bien.

    La seule faille que je vois est que ce serait une bibliothèque vraiment étrange, car elle vous renverrait des informations du livre, mais ne vous laisserait pas toucher au livre lui-même, sinon l'abstraction commencerait à "fuir". Je devrais penser à une analogie plus appropriée.

  4. Quelle est la relation entre Vue et Manette instances?

    La structure MVC est composée de deux couches: l'interface utilisateur et le modèle. Les principales structures du Couche d'interface utilisateur sont des vues et un contrôleur.

    Lorsque vous traitez avec des sites Web qui utilisent le modèle de conception MVC, le meilleur moyen est d'avoir une relation 1: 1 entre les vues et les contrôleurs. Chaque vue représente une page entière dans votre site Web et dispose d'un contrôleur dédié pour gérer toutes les demandes entrantes pour cette vue particulière.

    Par exemple, pour représenter un article ouvert, vous auriez \Application\Controller\Document et \Application\View\Document. Cela contiendrait toutes les fonctionnalités principales pour la couche d'interface utilisateur, quand il s'agit de traiter des articles (Bien sûr, vous pourriez en avoir XHR composants qui ne sont pas directement liés aux articles).


820
2018-05-03 00:56



Tout ce qui est logique métier appartient à un modèle, qu'il s'agisse d'une requête de base de données, de calculs, d'un appel REST, etc.

Vous pouvez avoir l'accès aux données dans le modèle lui-même, le modèle MVC ne vous en empêche pas. Vous pouvez mettre du sucre sur les services, les mappeurs et autres, mais la définition même d'un modèle est une couche qui gère la logique métier, rien de plus, rien de moins. Ce peut être une classe, une fonction ou un module complet avec un objet gazillion si c'est ce que vous voulez.

Il est toujours plus facile d'avoir un objet séparé qui exécute réellement les requêtes de base de données au lieu de les exécuter directement dans le modèle: cela sera particulièrement utile lors des tests unitaires (en raison de la facilité d'injection d'une dépendance de base de données).

class Database {
   protected $_conn;

   public function __construct($connection) {
       $this->_conn = $connection;
   }

   public function ExecuteObject($sql, $data) {
       // stuff
   }
}

abstract class Model {
   protected $_db;

   public function __construct(Database $db) {
       $this->_db = $db;
   }
}

class User extends Model {
   public function CheckUsername($username) {
       // ...
       $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE ...";
       return $this->_db->ExecuteObject($sql, $data);
   }
}

$db = new Database($conn);
$model = new User($db);
$model->CheckUsername('foo');

De plus, en PHP, vous avez rarement besoin d'attraper / de relancer des exceptions car le backtrace est préservé, en particulier dans un cas comme votre exemple. Laissez simplement l'exception être lancée et attrapez-la dans le contrôleur à la place.


32
2018-05-03 00:42



En Web- "MVC" vous pouvez faire ce que vous voulez.

Le concept original (1) décrit le modèle comme la logique métier. Il devrait représenter l'état de l'application et appliquer une certaine cohérence des données. Cette approche est souvent décrite comme "modèle de graisse".

La plupart des frameworks PHP suivent une approche plus superficielle, où le modèle est juste une interface de base de données. Mais à tout le moins, ces modèles devraient toujours valider les données et les relations entrantes.

De toute façon, vous n'êtes pas très loin si vous séparez les choses SQL ou les appels de base de données dans une autre couche. De cette façon, vous n'avez qu'à vous préoccuper des données / comportements réels, et non de l'API de stockage réelle. (Il est cependant déraisonnable de l'exagérer. Vous ne pourrez par exemple jamais remplacer un moteur de base de données par un stockage de fichiers si cela n'a pas été conçu à l'avance.)


18
2018-05-03 00:41



Plus souvent la plupart des applications auront des données, l'affichage et le traitement de la partie et nous avons simplement mis tous ceux dans les lettres M,V et C.

Modèle(M)-> A les attributs qui détient l'état d'application et il ne sait rien à propos de V et C.

Vue(V)-> A le format d'affichage pour l'application et et ne sait que sur comment modéliser dessus et ne se soucie pas de C.

Manette(C)----> A traiter une partie de l'application et agit comme câblage entre M et V et cela dépend à la fois M,V contrairement à M et V.

Au total, il y a une séparation des préoccupations entre chacun. À l'avenir, tout changement ou amélioration peut être ajouté très facilement.


4
2018-06-30 13:09



Dans mon cas, j'ai une classe de base de données qui gère toutes les interactions directes avec la base de données telles que l'interrogation, la récupération, etc. Donc, si je devais changer ma base de données MySQL à PostgreSQL il n'y aura pas de problème. Donc, ajouter cette couche supplémentaire peut être utile.

Chaque table peut avoir sa propre classe et avoir ses méthodes spécifiques, mais pour obtenir réellement les données, elle laisse la classe de base de données gérer:

Fichier Database.php

class Database {
    private static $connection;
    private static $current_query;
    ...

    public static function query($sql) {
        if (!self::$connection){
            self::open_connection();
        }
        self::$current_query = $sql;
        $result = mysql_query($sql,self::$connection);

        if (!$result){
            self::close_connection();
            // throw custom error
            // The query failed for some reason. here is query :: self::$current_query
            $error = new Error(2,"There is an Error in the query.\n<b>Query:</b>\n{$sql}\n");
            $error->handleError();
        }
        return $result;
    }
 ....

    public static function find_by_sql($sql){
        if (!is_string($sql))
            return false;

        $result_set = self::query($sql);
        $obj_arr = array();
        while ($row = self::fetch_array($result_set))
        {
            $obj_arr[] = self::instantiate($row);
        }
        return $obj_arr;
    }
}

Table objet classL

class DomainPeer extends Database {

    public static function getDomainInfoList() {
        $sql = 'SELECT ';
        $sql .='d.`id`,';
        $sql .='d.`name`,';
        $sql .='d.`shortName`,';
        $sql .='d.`created_at`,';
        $sql .='d.`updated_at`,';
        $sql .='count(q.id) as queries ';
        $sql .='FROM `domains` d ';
        $sql .='LEFT JOIN queries q on q.domainId = d.id ';
        $sql .='GROUP BY d.id';
        return self::find_by_sql($sql);
    }

    ....
}

J'espère que cet exemple vous aidera à créer une bonne structure.


0
2018-05-03 00:47