it-swarm.com.de

Wie soll ein Modell in MVC aufgebaut sein?

Ich verstehe gerade erst das MVC-Framework und frage mich oft, wie viel Code im Modell enthalten sein soll. Ich habe in der Regel eine Datenzugriffsklasse mit folgenden Methoden:

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 der Datenbanktabelle zugeordnet ist.

Sollte das Modellobjekt alle der Datenbank zugeordneten Eigenschaften sowie den obigen Code aufweisen, oder ist es in Ordnung, den Code herauszusondern, der tatsächlich für die Datenbank funktioniert?

Werde ich am Ende vier Schichten haben?

540
Dietpixel

Haftungsausschluss: Im Folgenden wird beschrieben, wie ich MVC-ähnliche Muster im Kontext von PHP-basierten Webanwendungen verstehe. Alle externen Links, die in den Inhalten verwendet werden, dienen dazu, Begriffe und Konzepte zu erläutern und nicht meine eigene Glaubwürdigkeit in diesem Bereich zu implizieren.

Das erste, was ich klären muss, ist: Das Modell ist eine Ebene .

Zweitens: Es gibt einen Unterschied zwischen klassischem MVC und dem, was wir in der Webentwicklung verwenden. Hier ist eine ältere Antwort, die ich geschrieben habe und die kurz beschreibt, wie sie sich unterscheiden.

Was für ein Modell ist NICHT:

Das Modell ist keine Klasse oder ein einzelnes Objekt. Es ist ein sehr häufiger Fehler, zu machen (das habe ich auch getan, obwohl die ursprüngliche Antwort geschrieben wurde, als ich anfing, etwas anderes zu lernen) , weil die meisten Frameworks dieses Missverständnis aufrechterhalten.

Weder ist es eine Object-Relational-Mapping-Technik (ORM) noch eine Abstraktion von Datenbanktabellen. Jeder, der Ihnen etwas anderes sagt, versucht höchstwahrscheinlich, ein anderes brandneues ORM oder ein ganzes Framework zu "verkaufen" .

Was für ein Modell ist:

Bei korrekter MVC-Anpassung enthält das M die gesamte Domänen-Geschäftslogik und die Modellschicht wird meistens erstellt aus drei Arten von Strukturen:

  • Domain Objects

    Ein Domänenobjekt ist ein logischer Container mit reinen Domäneninformationen. Es stellt normalerweise eine logische Entität im problematischen Domänenbereich dar. Wird allgemein als Geschäftslogik bezeichnet.

    Hier legen Sie fest, wie Daten vor dem Senden einer Rechnung überprüft oder die Gesamtkosten einer Bestellung berechnet werden. Zur gleichen Zeit sind Domain-Objekte nicht über den Speicher informiert - weder von wo (SQL-Datenbank, REST API, Textdatei usw.) und auch nicht , wenn sie gespeichert oder abgerufen werden.

  • Data Mappers

    Diese Objekte sind nur für die Speicherung verantwortlich. Wenn Sie Informationen in einer Datenbank speichern, befindet sich dort die SQL. Oder Sie verwenden eine XML-Datei zum Speichern von Daten, und Ihre Data Mappers analysieren XML-Dateien.

  • Services

    Sie können sich diese als "übergeordnete Domänenobjekte" vorstellen. Statt der Geschäftslogik sind jedoch Dienste für die Interaktion zwischen Domänenobjekten verantwortlich. ) und Mapper . Diese Strukturen bilden letztendlich eine "öffentliche" Schnittstelle für die Interaktion mit der Domänen-Geschäftslogik. Sie können sie vermeiden, aber mit der Strafe, Domänenlogik in Controller zu lecken.

    Zu diesem Thema gibt es eine verwandte Antwort in der Frage ACL-Implementierung - es könnte nützlich sein.

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

  • es hilft, das Prinzip der einmaligen Verantwortung (SRP) durchzusetzen
  • bietet zusätzlichen Spielraum für den Fall, dass sich die Logik ändert
  • hält die Steuerung so einfach wie möglich
  • gibt einen klaren Entwurf, wenn Sie jemals eine externe API benötigen

Wie interagiere ich mit einem Modell?

Voraussetzungen: Vorlesungen ansehen "Global State and Singletons" und "Don ' t Look for Things! " aus den Clean Code Talks.

Zugriff auf Service-Instanzen erhalten

Für die Instanzen View und Controller (wie Sie es nennen könnten: "UI-Schicht") gibt es Zugriff auf diese Dienste zwei allgemeine Ansätze:

  1. Sie können die erforderlichen Services direkt in die Konstruktoren Ihrer Views und Controller einfügen, vorzugsweise mithilfe eines DI-Containers.
  2. Verwenden einer Factory 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 (obwohl er für Anfänger nicht die einfachste ist). Die zwei Bibliotheken, die ich für diese Funktionalität empfehlen würde, wären Syfmony's Standalone DependencyInjection-Komponente oder Auryn .

Mit den Lösungen, die einen Factory- und einen DI-Container verwenden, können Sie auch die Instanzen verschiedener Server gemeinsam nutzen, die für den ausgewählten Controller und die Ansicht für einen bestimmten Anforderungs-Antwort-Zyklus gemeinsam genutzt werden sollen.

Änderung des Modellzustands

Jetzt, da Sie auf die Modellebene in den Controllern zugreifen können, müssen Sie sie tatsächlich 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 sehr klare Aufgabe: Nehmen Sie die Benutzereingaben entgegen und ändern Sie anhand dieser Eingaben den aktuellen Status der Geschäftslogik. In diesem Beispiel sind die Status, zwischen denen gewechselt wird, "anonymer Benutzer" und "angemeldeter Benutzer".

Der Controller ist nicht für die Überprüfung der Benutzereingaben verantwortlich, da dies Teil der Geschäftsregeln ist und der Controller definitiv keine SQL-Abfragen aufruft, wie Sie es sehen würden hier oder hier (bitte nicht Ich hasse sie nicht, sie sind fehlgeleitet, nicht böse.

Zeigt dem Benutzer den Statuswechsel an.

Ok, Benutzer hat sich angemeldet (oder ist fehlgeschlagen). Was nun? Der Benutzer ist sich dessen immer noch nicht bewusst. Sie müssen also tatsächlich eine Antwort erstellen, und das liegt in der Verantwortung 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, basierend auf dem aktuellen Status des Modell-Layers. Für einen anderen Anwendungsfall müsste die Ansicht verschiedene Vorlagen zum Rendern auswählen, basierend auf etwas wie "aktuell ausgewähltem Artikel".

Die Präsentationsebene kann tatsächlich ziemlich aufwändig werden, wie hier beschrieben: Grundlegendes zu MVC-Ansichten in PHP .

Aber ich erstelle gerade eine REST API!

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

MVC ist nur eine konkrete Lösung für das Separation of Concerns Prinzip. MVC trennt die Benutzeroberfläche von der Geschäftslogik, und es trennt in der Benutzeroberfläche die Behandlung von Benutzereingaben und der Präsentation. Dies ist von entscheidender Bedeutung. Während die Leute es oft als "Dreiklang" bezeichnen, besteht es eigentlich nicht aus drei unabhängigen Teilen. Die Struktur ist eher so:

MVC separation

Dies bedeutet, dass, wenn die Logik Ihrer Präsentationsebene so gut wie nicht vorhanden ist, der pragmatische Ansatz darin besteht, sie als einzelne Ebene beizubehalten. Es kann auch einige Aspekte der Modellschicht wesentlich 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 zum Rendern eines Antwortkörpers haben, ist diese Vereinfachung für trivialere Szenarien sehr nützlich. Aber sei gewarnt , dieser Ansatz wird zu einem Albtraum, wenn versucht wird, in großen Codebasen mit komplexer Präsentationslogik zu arbeiten.

Wie baue ich das Modell?

Da es keine einzige "Modell" -Klasse gibt (wie oben erläutert), wird das Modell nicht wirklich "erstellt". Stattdessen beginnen Sie damit, Dienste zu erstellen, die bestimmte Methoden ausführen können. Implementieren Sie dann Domänenobjekte und Mapper .

Ein Beispiel für eine Dienstmethode:

In den beiden obigen Ansätzen gab es diese Anmeldemethode für den Identifikationsdienst. Wie würde es eigentlich aussehen? Ich benutze 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 abgerufen wurden. Es kann sich um eine Datenbank handeln, es kann sich aber auch nur um ein Scheinobjekt zu Testzwecken handeln. Sogar die Data Mappers, die tatsächlich dafür verwendet werden, sind in den private Methoden dieses Dienstes versteckt.

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, Mapper zu erstellen

Um eine Abstraktion der Persistenz zu implementieren, müssen bei den flexibelsten Ansätzen benutzerdefinierte Data Mappers erstellt werden.

Mapper diagram

Von: PoEAA Buch

In der Praxis werden sie für die Interaktion mit bestimmten Klassen oder Superklassen implementiert. Nehmen wir an, Sie haben Customer und Admin in Ihrem Code (beide erben von einer User Superklasse). Beide haben wahrscheinlich einen separaten passenden Mapper, da sie unterschiedliche Felder enthalten. Sie werden jedoch auch gemeinsame und häufig verwendete Vorgänge ausführen müssen. Zum Beispiel: Aktualisierung der "zuletzt online gesehen" Zeit. Anstatt die vorhandenen Mapper zu verwickeln, besteht der pragmatischere Ansatz darin, einen allgemeinen "User Mapper" zu verwenden, der nur diesen Zeitstempel aktualisiert.

Einige zusätzliche Kommentare:

  1. Datenbanktabellen und -modell

    Während in größeren Projekten manchmal eine direkte 1: 1: 1-Beziehung zwischen einer Datenbanktabelle, Domain Object und Mapper besteht Es ist möglicherweise weniger verbreitet als Sie erwarten:

    • Von einem einzelnen Domänenobjekt verwendete Informationen können aus verschiedenen Tabellen zugeordnet werden, während das Objekt selbst keine Persistenz in der Datenbank aufweist.

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

    • Ein einzelner Mapper kann sich auf mehrere Tabellen auswirken.

      Beispiel: Wenn Sie Daten vom Objekt User speichern, kann dieses Domänenobjekt eine Sammlung anderer Domänenobjekte enthalten - Group Instanzen. Wenn Sie sie ändern und das User speichern, muss der Data Mapper Einträge in mehrere Tabellen aktualisieren und/oder einfügen.

    • Daten von einem einzelnen Domänenobjekt werden in mehr als einer Tabelle gespeichert.

      Beispiel: In großen Systemen (denken Sie an ein mittelgroßes soziales Netzwerk) kann es pragmatisch sein, Benutzerauthentifizierungsdaten und Daten, auf die häufig zugegriffen wird, getrennt von größeren Inhaltsblöcken zu speichern wird selten benötigt. In diesem Fall haben Sie möglicherweise immer noch eine einzelne User -Klasse, aber die darin enthaltenen Informationen hängen davon ab, ob vollständige Details abgerufen wurden.

    • Für jedes Domain-Objekt kann es mehr als einen Mapper geben

      Beispiel: Sie haben eine News-Site mit einem gemeinsam genutzten Code, der sowohl für die Öffentlichkeit als auch für die Verwaltungssoftware verwendet wird. Obwohl beide Schnittstellen dieselbe Article -Klasse verwenden, benötigt das Management viel mehr Informationen. In diesem Fall hätten Sie zwei separate Mapper: "intern" und "extern". Jeder führt unterschiedliche Abfragen durch oder verwendet sogar unterschiedliche Datenbanken (wie bei Master oder Slave).

  2. Eine Ansicht ist keine Vorlage

    View -Instanzen in MVC (wenn Sie nicht die MVP-Variation des Musters verwenden) sind für die Präsentationslogik verantwortlich. Dies bedeutet, dass jede Ansicht normalerweise mindestens einige Vorlagen jongliert. Es erfasst Daten von der Modellebene und wählt dann auf der Grundlage der empfangenen Informationen eine Vorlage aus und legt Werte fest.

    Einer der Vorteile, die Sie daraus ziehen, ist die Wiederverwendbarkeit. Wenn Sie eine ListView -Klasse erstellen, können Sie mit gut geschriebenem Code dieselbe Klasse haben, die die Präsentation der Benutzerliste und der Kommentare unter einem Artikel behandelt. Weil beide dieselbe Präsentationslogik haben. Sie wechseln nur die Vorlagen.

    Sie können entweder native PHP Vorlagen oder eine Template-Engine eines Drittanbieters verwenden. Möglicherweise gibt es auch Bibliotheken von Drittanbietern, die View -Instanzen vollständig ersetzen können.

  3. Was ist mit der alten Version der Antwort?

    Die einzige wesentliche Änderung ist, dass das, was in der alten Version als Model bezeichnet wird, tatsächlich ein Service ist. Der Rest der "Bibliotheksanalogie" hält ziemlich gut mit.

    Der einzige Fehler, den ich sehe, ist, dass dies eine wirklich seltsame Bibliothek wäre, weil sie Ihnen Informationen aus dem Buch zurückgeben würde, aber Sie das Buch selbst nicht berühren lassen würden, da sonst die Abstraktion zu "lecken" beginnen würde. Möglicherweise muss ich mir eine passendere Analogie überlegen.

  4. Wie ist die Beziehung zwischen View - und Controller -Instanzen?

    Die MVC-Struktur besteht aus zwei Ebenen: ui und model. Die Hauptstrukturen in der UI-Ebene sind Ansichten und Controller.

    Wenn Sie mit Websites arbeiten, die MVC-Entwurfsmuster verwenden, besteht die beste Möglichkeit darin, eine 1: 1-Beziehung zwischen Ansichten und Controllern herzustellen. Jede Ansicht stellt eine ganze Seite Ihrer Website dar und verfügt über einen dedizierten Controller, der alle eingehenden Anforderungen für diese bestimmte Ansicht verarbeitet.

    Um beispielsweise einen geöffneten Artikel darzustellen, müssten Sie \Application\Controller\Document und \Application\View\Document. Dies würde alle Hauptfunktionen für die UI-Ebene enthalten, wenn es um den Umgang mit Artikeln geht (natürlich könnten Sie einige XHR -Komponenten haben, die nicht direkt mit Artikeln zusammenhängen ) .

883
tereško

Alles, was Geschäftslogik ist, gehört in ein Modell, 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. Sie können es mit Services, Mappern und was auch immer beschönigen, aber die eigentliche Definition eines Modells ist eine Ebene, die Geschäftslogik verarbeitet, nicht mehr und nicht weniger. Es kann eine Klasse, eine Funktion oder ein vollständiges Modul mit einer Unmenge von Objekten sein, wenn Sie dies wünschen.

Es ist immer einfacher, ein separates Objekt zu haben, das die Datenbankabfragen tatsächlich ausführt, als dass sie direkt im Modell ausgeführt werden: Dies ist besonders praktisch, wenn Unit-Tests durchgeführt werden (da es einfach ist, eine nachgebildete Datenbankabhängigkeit in Ihr Modell einzufügen):

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 müssen Sie in PHP selten Ausnahmen abfangen/erneut auslösen, da die Rückverfolgung beibehalten wird, insbesondere in einem Fall wie Ihrem Beispiel. Lassen Sie die Ausnahme einfach ausgelöst werden und fangen Sie sie stattdessen im Controller ab.

36
netcoder

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

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

Die meisten PHP Frameworks folgen einem flacheren Ansatz, bei dem das Modell nur eine Datenbankschnittstelle ist. Zumindest sollten diese Modelle jedoch die eingehenden Daten und Beziehungen noch validieren.

In beiden Fällen sind Sie nicht weit davon entfernt, wenn Sie die SQL-Informationen oder Datenbankaufrufe in eine andere Ebene unterteilen. Auf diese Weise müssen Sie sich nur mit den tatsächlichen Daten/dem tatsächlichen Verhalten befassen, nicht mit der tatsächlichen Speicher-API. (Es ist jedoch unvernünftig, dies zu übertreiben. Sie können beispielsweise ein Datenbank-Backend niemals durch einen Dateispeicher ersetzen, wenn dies nicht im Voraus geplant wurde.)

20
mario

Häufig haben die meisten Anwendungen Daten-, Anzeige- und Verarbeitungsteile, und wir setzen all diese in die Buchstaben M, V und C ein.

Model (M) -> Hat die Attribute, die den Anwendungsstatus haben, und weiß nichts über V und C.

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

Controller (C) ----> Verarbeitet einen Teil der Anwendung und fungiert als Verdrahtung zwischen M und V, und es hängt von beiden M, V ab M und V.

Insgesamt besteht zwischen beiden eine Trennung der Bedenken. Zukünftig können Änderungen oder Erweiterungen sehr einfach hinzugefügt werden.

In meinem Fall habe ich eine Datenbankklasse, die alle direkten Datenbankinteraktionen wie Abfragen, Abrufen usw. verarbeitet. Wenn ich also meine Datenbank von MySQL auf PostgreSQL ändern müsste, gäbe es kein Problem. Das Hinzufügen dieser zusätzlichen Ebene kann daher nützlich sein.

Jede Tabelle kann ihre eigene Klasse und ihre spezifischen Methoden haben, aber um die Daten tatsächlich zu erhalten, lässt sie die Datenbankklasse damit umgehen:

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

    ....
}

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

0
Ibu