579 Stimmen

Wie sollte ein Modell in MVC strukturiert sein?

Ich bin gerade dabei, das MVC-Framework zu verstehen, und ich frage mich oft, wie viel Code in das Modell gehören sollte. Ich neige dazu, eine Datenzugriffsklasse zu haben, die Methoden wie diese hat:

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

Meine Modelle sind in der Regel eine Entitätsklasse, die auf die Datenbanktabelle abgebildet wird.

Sollte das Modell-Objekt alle Datenbank zugeordneten Eigenschaften sowie den Code oben haben oder ist es OK, dass Code heraus zu trennen, die tatsächlich die Datenbank Arbeit tut?

Werde ich am Ende vier Schichten haben?

963voto

tereško Punkte 57124

<strong>Haftungsausschluss: </strong>im Folgenden beschreibe ich, wie ich MVC-ähnliche Muster im Kontext von PHP-basierten Webanwendungen verstehe. Alle externen Links, die im Inhalt verwendet werden, dienen der Erläuterung von Begriffen und Konzepten, und <strong>no </strong>um meine eigene Glaubwürdigkeit in dieser Angelegenheit zu untermauern.

Das erste, was ich klarstellen muss, ist: das Modell ist eine Schicht .

Zweitens: Es gibt einen Unterschied zwischen klassisches MVC und was wir in der Webentwicklung verwenden. Hier ist ein Teil einer älteren Antwort, die ich geschrieben habe und in der kurz beschrieben wird, wie sie sich unterscheiden.

Was ein Modell NICHT ist:

Das Modell ist weder eine Klasse noch ein einzelnes Objekt. Es ist ein sehr häufiger Fehler (Das habe ich auch getan, obwohl die ursprüngliche Antwort geschrieben wurde, als ich begann, etwas anderes zu lernen) weil die meisten Rahmenwerke diesen Irrglauben aufrechterhalten.

Es handelt sich weder um eine objektrelationale Mapping-Technik (ORM) noch um eine Abstraktion von Datenbanktabellen. Jeder, der Ihnen etwas anderes erzählt, versucht höchstwahrscheinlich verkaufen'. ein weiteres brandneues ORM oder ein ganzes Framework.

Was ein Modell ist:

Bei einer richtigen MVC-Anpassung enthält die M die gesamte Geschäftslogik des Bereichs und die Modell-Schicht es meist die aus drei Arten von Strukturen bestehen:

  • Domänen-Objekte

    Ein Domänenobjekt ist ein logischer Behälter mit reinen Domäneninformationen; es stellt in der Regel eine logische Einheit im Problemdomänenraum dar. Üblicherweise wird es bezeichnet als Geschäftslogik .

    Hier können Sie festlegen, wie die Daten vor dem Versenden einer Rechnung validiert oder die Gesamtkosten einer Bestellung berechnet werden sollen. Zur gleichen Zeit, Domänen-Objekte sind völlig ahnungslos in Bezug auf die Speicherung - weder von wobei (SQL-Datenbank, REST-API, Textdatei usw.) noch nicht einmal wenn sie gespeichert oder abgerufen werden.

  • Daten-Mapper

    Diese Objekte sind nur für die Speicherung zuständig. Wenn Sie Informationen in einer Datenbank speichern, ist dies der Ort, an dem sich das SQL befindet. Oder vielleicht verwenden Sie eine XML-Datei zum Speichern von Daten und Ihre Daten-Mapper werden von und zu XML-Dateien geparst.

  • Dienstleistungen

    Man kann sie sich als "Domänenobjekte höherer Ebene" vorstellen, aber anstelle von Geschäftslogik, Dienstleistungen sind verantwortlich für die Interaktion zwischen Domänen-Objekte y Kartenzeichner . Diese Strukturen schaffen eine "öffentliche" Schnittstelle für die Interaktion mit der Geschäftslogik der Domäne. Man kann sie vermeiden, aber auf Kosten des Durchsickerns einiger Domänenlogik in Steuerungen .

    Eine verwandte Antwort zu diesem Thema findet sich in der ACL-Implementierung Frage - sie könnte nützlich sein.

Die Kommunikation zwischen der Modellschicht und anderen Teilen der MVC-Trias sollte nur über Dienstleistungen . Die klare Trennung hat einige zusätzliche Vorteile:

  • es hilft bei der Durchsetzung der Grundsatz der einzigen Verantwortung (SVB)
  • bietet zusätzlichen "Spielraum" für den Fall, dass sich die Logik ändert
  • den Controller so einfach wie möglich zu halten
  • gibt einen klaren Plan, falls Sie jemals eine externe API benötigen

Wie kann man mit einem Modell interagieren?

<em><strong>Voraussetzungen: </strong>Vorträge ansehen <a href="http://www.youtube.com/watch?v=-FRm3VPhseI" rel="noreferrer">"Globaler Zustand und Singletons" </a>y <a href="http://www.youtube.com/watch?v=RlfLCWKxHJ0" rel="noreferrer">"Suchen Sie nicht nach Dingen!" </a>aus den Clean Code Talks.</em>

Zugang zu Dienstinstanzen erhalten

Sowohl für die Siehe y Controller Instanzen (man könnte sagen: "UI-Schicht") den Zugang zu diesen Diensten zu ermöglichen, gibt es zwei allgemeine Ansätze:

  1. Sie können die benötigten Dienste direkt in die Konstruktoren Ihrer Views und Controller injizieren, vorzugsweise unter Verwendung eines DI-Containers.
  2. Verwendung einer Fabrik für Dienste als obligatorische Abhängigkeit für alle Ihre Ansichten und Controller.

Wie Sie vielleicht vermuten, ist der DI-Container eine viel elegantere Lösung (wenn auch nicht die einfachste für einen Anfänger). Die beiden Bibliotheken, die ich für diese Funktionalität empfehle, sind Syfmonys Standalone DependencyInjection-Komponente ou Auryn .

Bei beiden Lösungen, die eine Fabrik und einen DI-Container verwenden, können Sie auch die Instanzen verschiedener Server gemeinsam nutzen, die zwischen dem ausgewählten Controller und der Ansicht für einen bestimmten Anfrage-Antwort-Zyklus gemeinsam genutzt werden.

Änderung des Zustands des Modells

Jetzt, da Sie in den Controllern auf die Modellebene zugreifen können, müssen Sie damit beginnen, sie tatsächlich zu verwenden:

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

Ihre Controller haben eine ganz klare Aufgabe: Sie nehmen die Benutzereingaben entgegen und ändern auf der Grundlage dieser Eingaben den aktuellen Zustand der Geschäftslogik. In diesem Beispiel sind die Zustände, zwischen denen gewechselt wird, "anonymer Benutzer" und "angemeldeter Benutzer".

Der Controller ist nicht für die Validierung der Benutzereingaben verantwortlich, da dies Teil der Geschäftsregeln ist und der Controller definitiv keine SQL-Abfragen aufruft, wie Sie es sehen würden aquí ou aquí (Bitte hassen Sie sie nicht, sie sind fehlgeleitet, nicht böse).

Zeigt dem Benutzer die Zustandsänderung an.

Ok, der Benutzer hat sich angemeldet (oder ist gescheitert). Was nun? Der besagte Nutzer ist sich dessen immer noch nicht bewusst. Sie müssen also eine Antwort geben, und das ist die Aufgabe einer Ansicht.

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

In diesem Fall erzeugte die Ansicht eine von zwei möglichen Antworten, je nach dem aktuellen Zustand der Modellebene. In einem anderen Anwendungsfall würde die Ansicht verschiedene Vorlagen zum Rendern auswählen, die auf etwas wie "aktuell ausgewählter Artikel" basieren.

Die Präsentationsebene kann, wie hier beschrieben, recht aufwendig sein: MVC-Ansichten in PHP verstehen .

Aber ich mache gerade eine REST-API!

Natürlich gibt es Situationen, in denen dies ein Overkill ist.

MVC ist nur eine konkrete Lösung für Trennung der Belange Prinzip. MVC trennt die Benutzeroberfläche von der Geschäftslogik, und in der UI trennt es die Verarbeitung von Benutzereingaben und die Präsentation. Dies ist von entscheidender Bedeutung. Obwohl er oft als "Dreiklang" bezeichnet wird, besteht er eigentlich nicht aus drei unabhängigen Teilen. Die Struktur ist eher wie folgt:

MVC separation

Das heißt, wenn die Logik Ihrer Präsentationsschicht so gut wie nicht vorhanden ist, besteht der pragmatische Ansatz darin, sie als einzelne Schicht zu erhalten. Dies kann auch einige Aspekte der Modellebene erheblich vereinfachen.

Mit diesem Ansatz kann das Anmeldebeispiel (für eine API) wie folgt geschrieben werden:

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

Dies ist zwar nicht nachhaltig, aber wenn Sie eine komplizierte Logik für das Rendern eines Antwortkörpers haben, ist diese Vereinfachung für trivialere Szenarien sehr nützlich. Aber gewarnt sein Dieser Ansatz wird zu einem Alptraum, wenn man versucht, ihn in großen Codebasen mit komplexer Präsentationslogik zu verwenden.

Wie wird das Modell gebaut?

Da es keine einzige Klasse "Modell" gibt (wie oben erläutert), wird das Modell nicht wirklich "gebaut". Stattdessen beginnen Sie mit der Erstellung von Dienstleistungen die in der Lage sind, bestimmte Methoden durchzuführen. Und dann implementieren Domänen-Objekte y Kartenzeichner .

Ein Beispiel für eine Dienstmethode:

In den beiden obigen Ansätzen gab es diese Anmeldemethode für den Identifikationsdienst. Wie würde das eigentlich aussehen. Ich verwende eine leicht modifizierte Version der gleichen Funktionalität aus eine Bibliothek die ich geschrieben habe weil ich faul bin:

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

Wie Sie sehen, gibt es auf dieser Abstraktionsebene keinen Hinweis darauf, woher die Daten geholt wurden. Es könnte sich um eine Datenbank handeln, aber auch nur um ein Scheinobjekt zu Testzwecken. Sogar die Daten-Mapper, die tatsächlich dafür verwendet werden, sind in der private Methoden dieses Dienstes.

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

Möglichkeiten der Erstellung von Mappern

Um eine Abstraktion der Persistenz zu implementieren, besteht einer der flexibelsten Ansätze darin, benutzerdefinierte Datenabbildner .

Mapper diagram

De: <a href="https://martinfowler.com/books/eaa.html" rel="noreferrer">PoEAA </a>Buch

In der Praxis werden sie zur Interaktion mit bestimmten Klassen oder Oberklassen eingesetzt. Sagen wir, Sie haben Customer y Admin in Ihrem Code (beide erben von einer User Oberklasse). Beide würden wahrscheinlich mit einem separaten passenden Mapper enden, da sie unterschiedliche Felder enthalten. Aber Sie werden auch gemeinsame und häufig verwendete Operationen haben. Zum Beispiel: Aktualisieren der "zuletzt online gesehen" Zeit. Anstatt die bestehenden Mapper noch komplizierter zu machen, wäre es pragmatischer, einen allgemeinen "Benutzer-Mapper" einzurichten, der nur diesen Zeitstempel aktualisiert.

Einige zusätzliche Kommentare:

  1. Datenbanktabellen und Modell

    Manchmal besteht zwar eine direkte 1:1:1-Beziehung zwischen einer Datenbanktabelle, Domänenobjekt und Mapper Bei größeren Projekten ist dies vielleicht weniger häufig der Fall, als Sie erwarten:

    • Informationen, die von einem einzelnen Domänenobjekt können aus verschiedenen Tabellen abgebildet werden, während das Objekt selbst keine Persistenz in der Datenbank hat.

      Beispiel: wenn Sie einen monatlichen Bericht erstellen. Dies würde Informationen aus verschiedenen Tabellen sammeln, aber es gibt keine magische MonthlyReport Tabelle in der Datenbank.

    • Ein einzelner Mapper kann mehrere Tabellen betreffen.

      Beispiel: wenn Sie die Daten aus dem User Objekt, dieses Domänenobjekt könnte eine Sammlung von anderen Domänenobjekten enthalten - Group Instanzen. Wenn Sie sie ändern und die User die Daten-Mapper müssen Einträge in mehreren Tabellen aktualisieren und/oder einfügen.

    • Daten aus einer einzigen Domänenobjekt in mehr als einer Tabelle gespeichert ist.

      Beispiel: In großen Systemen (z. B. in einem mittelgroßen sozialen Netzwerk) kann es pragmatisch sein, Daten zur Benutzerauthentifizierung und Daten, auf die häufig zugegriffen wird, getrennt von größeren, selten benötigten Inhalten zu speichern. In diesem Fall könnte man immer noch eine einzige User Klasse, aber die darin enthaltenen Informationen würden davon abhängen, ob vollständige Details abgerufen wurden.

    • Für jeden Domänenobjekt es kann mehr als einen Mapper geben

      Beispiel: Sie haben eine News-Site mit einer gemeinsamen Codebasis für die öffentliche Seite und die Verwaltungssoftware. Doch während beide Schnittstellen denselben Code verwenden Article Klasse, die Verwaltung braucht noch viel mehr Informationen, die darin enthalten sind. In diesem Fall würden Sie zwei separate Mapper haben: "intern" und "extern". Jeder führt unterschiedliche Abfragen durch oder verwendet sogar unterschiedliche Datenbanken (wie Master oder Slave).

  2. Eine Ansicht ist keine Vorlage

    Siehe Instanzen in MVC (wenn Sie nicht die MVP-Variante des Musters verwenden) sind für die Präsentationslogik verantwortlich. Das bedeutet, dass jede Siehe jongliert normalerweise mit mindestens ein paar Vorlagen. Es bezieht Daten aus dem Modell-Schicht und wählt dann auf der Grundlage der erhaltenen Informationen eine Vorlage aus und legt Werte fest.

    Einer der Vorteile, die sich daraus ergeben, ist die Wiederverwendbarkeit. Wenn Sie eine ListView Klasse, dann können Sie mit gut geschriebenem Code dieselbe Klasse haben, die die Darstellung der Benutzerliste und der Kommentare unter einem Artikel übernimmt. Denn beide haben die gleiche Präsentationslogik. Sie wechseln einfach die Vorlagen.

    Sie können entweder native PHP-Vorlagen oder eine Templating-Engine eines Drittanbieters verwenden. Es könnte auch einige Bibliotheken von Drittanbietern geben, die in der Lage sind, folgende Funktionen vollständig zu ersetzen Siehe Instanzen.

  3. Was ist mit der alten Version der Antwort?

    Die einzige größere Änderung ist, dass die so genannte Modell in der alten Version, ist eigentlich ein Dienst . Der Rest der "Bibliotheksanalogie" hält sich ziemlich gut.

    Das einzige Manko, das ich sehe, ist, dass dies eine wirklich merkwürdige Bibliothek wäre, weil sie zwar Informationen aus dem Buch zurückliefert, aber nicht zulässt, dass man das Buch selbst berührt, weil sonst die Abstraktion "auslaufen" würde. Vielleicht muss ich mir eine passendere Analogie ausdenken.

  4. Wie ist die Beziehung zwischen Siehe y Controller Instanzen?

    Die MVC-Struktur besteht aus zwei Schichten: Benutzeroberfläche und Modell. Die wichtigsten Strukturen in der UI-Schicht sind Views und Controller.

    Wenn Sie es mit Websites zu tun haben, die das MVC-Designmuster verwenden, ist es am besten, eine 1:1-Beziehung zwischen Ansichten und Controllern herzustellen. Jede Ansicht stellt eine ganze Seite in Ihrer Website dar und hat einen eigenen Controller, der alle eingehenden Anfragen für diese bestimmte Ansicht bearbeitet.

    Um zum Beispiel einen geöffneten Artikel darzustellen, müssten Sie \Application\Controller\Document y \Application\View\Document . Dies würde alle wichtigen Funktionen für die UI-Schicht enthalten, wenn es um den Umgang mit Artikeln geht (natürlich könnten Sie einige XHR Komponenten, die nicht direkt mit Artikeln in Verbindung stehen) .

37voto

netcoder Punkte 64375

Alles, was ist Geschäftslogik in ein Modell gehört, sei es eine Datenbankabfrage, Berechnungen, ein REST-Aufruf usw.

Sie können den Datenzugriff im Modell selbst haben, das MVC-Muster schränkt Sie nicht ein, das zu tun. Man kann es mit Diensten, Mappern und dergleichen überziehen, aber die eigentliche Definition eines Modells ist eine Schicht, die Geschäftslogik verarbeitet, nicht mehr und nicht weniger. Es kann eine Klasse, eine Funktion oder ein komplettes Modul mit einer Unmenge von Objekten sein, wenn Sie das wollen.

Es ist immer einfacher, ein separates Objekt zu haben, das die Datenbankabfragen tatsächlich ausführt, anstatt sie direkt im Modell auszuführen: Dies ist besonders beim Unit-Testing nützlich (weil es so einfach ist, eine Mock-Datenbank-Abhängigkeit in Ihr Modell zu injizieren):

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

Außerdem ist es in PHP nur selten notwendig, Ausnahmen zu fangen/auszulösen, da der Backtrace erhalten bleibt, insbesondere in einem Fall wie Ihrem Beispiel. Lassen Sie einfach die Ausnahme ausgelöst werden und fangen Sie sie stattdessen im Controller ab.

21voto

mario Punkte 141130

In Web-"MVC" können Sie tun, was Sie wollen.

Das ursprüngliche Konzept (1) beschrieb das Modell als die Geschäftslogik. Es sollte den Anwendungszustand darstellen und eine gewisse Datenkonsistenz erzwingen. Dieser Ansatz wird oft als "fettes Modell" bezeichnet.

Die meisten PHP-Frameworks verfolgen einen eher oberflächlichen Ansatz, bei dem das Modell lediglich eine Datenbankschnittstelle darstellt. Zumindest aber sollten diese Modelle die eingehenden Daten und Beziehungen validieren.

Wie auch immer, Sie sind nicht weit davon entfernt, wenn Sie den SQL-Kram oder die Datenbankaufrufe in eine andere Schicht auslagern. Auf diese Weise müssen Sie sich nur mit den eigentlichen Daten/Verhaltensweisen befassen, nicht mit der eigentlichen Speicher-API. (Es ist jedoch unvernünftig, es zu übertreiben. Man wird z.B. nie in der Lage sein, ein Datenbank-Backend durch einen Dateispeicher zu ersetzen, wenn das nicht im Voraus geplant wurde).

7voto

Nagappa L M Punkte 1330

Meistens haben die Anwendungen einen Daten-, einen Anzeige- und einen Verarbeitungsteil, und wir setzen all diese Teile einfach in die Buchstaben M , V y C .

Modell( M ) -->Hat die Attribute, die den Zustand der Anwendung enthalten und weiß nichts über V y C .

Ansicht( V ) -->Hat ein Anzeigeformat für die Anwendung und weiß nur, wie man das Modell darauf verdaut und kümmert sich nicht um C .

Controller( C ) ---->Hat einen verarbeitenden Teil der Anwendung und fungiert als Verkabelung zwischen M und V und hängt von beiden ab M , V im Gegensatz zu M y V .

Alles in allem gibt es eine Trennung zwischen den einzelnen Anliegen. In Zukunft kann jede Änderung oder Erweiterung sehr einfach hinzugefügt werden.

0voto

Ibu Punkte 41144

In meinem Fall habe ich eine Datenbank-Klasse, die alle die direkte Datenbank-Interaktion wie Abfragen, Abrufen und so behandeln. Wenn ich also meine Datenbank ändern müsste von MySQL a PostgreSQL wird es keine Probleme geben. Es kann also nützlich sein, diese zusätzliche Schicht hinzuzufügen.

Jede Tabelle kann ihre eigene Klasse und ihre spezifischen Methoden haben, aber um die Daten tatsächlich zu erhalten, überlässt sie es der Datenbankklasse, dies zu tun:

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

Tabellenobjekt KlasseL

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

    ....
}

Ich hoffe, dieses Beispiel hilft Ihnen, eine gute Struktur zu erstellen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X