php länge - Wie sollte ein Modell in MVC strukturiert sein?




für meta (5)

Disclaimer: Im Folgenden wird beschrieben, wie ich MVC-ähnliche Muster im Zusammenhang mit PHP-basierten Webanwendungen verstehe. Alle externen Links, die im Inhalt verwendet werden, sind dazu da, Begriffe und Konzepte zu erklären und nicht meine eigene Glaubwürdigkeit in Bezug auf das Thema zu implizieren.

Die erste Sache, die ich aufklären muss, ist: Das Modell ist eine Schicht .

Zweitens: Es gibt einen Unterschied zwischen dem klassischen MVC und dem, was wir in der Webentwicklung verwenden. Here's ein bisschen eine ältere Antwort, die ich schrieb, die kurz beschreibt, wie sie anders sind.

Was ein Modell NICHT ist:

Das Modell ist keine Klasse oder irgendein einzelnes Objekt. Es ist ein sehr verbreiteter Fehler (ich tat es auch, 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:

In der richtigen MVC-Anpassung enthält das M die gesamte Geschäftslogik der Domäne und die Modellschicht besteht hauptsächlich aus drei Arten von Strukturen:

  • Domänenobjekte

    Ein Domänenobjekt ist ein logischer Container mit reinen Domäneninformationen. Es stellt normalerweise eine logische Entität im Problemdomänenraum dar. Gemeinhin als Geschäftslogik bezeichnet .

    Hier definieren Sie, wie Daten validiert werden, bevor eine Rechnung gesendet wird, oder um die Gesamtkosten einer Bestellung zu berechnen. Zur gleichen Zeit ist es den Domänenobjekten völlig verborgen, dass sie keinen Speicher haben - weder von wo (SQL-Datenbank, REST-API, Textdatei usw.) noch selbst wenn sie gespeichert oder abgerufen werden.

  • Datenmapper

    Diese Objekte sind nur für den Speicher verantwortlich. Wenn Sie Informationen in einer Datenbank speichern, ist dies der Ort, an dem SQL lebt. Oder Sie verwenden eine XML-Datei zum Speichern von Daten und Ihre Data Mapper analysieren von und zu XML-Dateien.

  • Services

    Sie können sich diese als " Domänenobjekte höherer Ebenen" vorstellen , aber statt der Geschäftslogik sind Services für die Interaktion zwischen Domänenobjekten und Mappern verantwortlich . Diese Strukturen schaffen schließlich eine "öffentliche" Schnittstelle für die Interaktion mit der Geschäftslogik der Domäne. Sie können sie vermeiden, aber mit dem Nachteil, dass einige Domänenlogiken in Controller eindringen .

    Es gibt eine verwandte Antwort auf dieses Thema in der ACL-Implementierungsfrage - es könnte nützlich sein.

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

  • es hilft, das Single Responsibility-Prinzip (SRP) durchzusetzen
  • bietet zusätzlichen 'Spielraum' für den Fall, dass sich die Logik ändert
  • hält den Controller so einfach wie möglich
  • Gibt einen klaren Plan, wenn Sie jemals eine externe API benötigen

Wie interagiere ich mit einem Model?

Voraussetzungen: Schauen Sie sich die Vorträge "Global State und Singletons" und "Achten Sie nicht auf die Dinge!" aus den Clean Code Talks.

Zugriff auf Serviceinstanzen erhalten

Für die View- und Controller- Instanzen (was Sie "UI-Layer" nennen könnten), um auf diese Services zugreifen zu können, gibt es zwei allgemeine Ansätze:

  1. Sie können die erforderlichen Dienste direkt in die Konstruktoren Ihrer Sichten und Controller einfügen, vorzugsweise über einen DI-Container.
  2. Verwenden einer Factory für Services 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 es nicht die einfachste für einen Anfänger ist). Die beiden Bibliotheken, die ich für diese Funktionalität in Betracht ziehe, wären Syfmonys eigenständige DependencyInjection-Komponente oder Auryn .

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

Veränderung des Modellzustandes

Da Sie nun auf die Modellschicht 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 und ändern Sie anhand dieser Eingabe den aktuellen Stand der Geschäftslogik. In diesem Beispiel sind die Zustände, zwischen denen gewechselt wird, "anonymer Benutzer" und "angemeldeter Benutzer".

Der Controller ist nicht dafür verantwortlich, Benutzereingaben zu validieren, da dies Teil von Geschäftsregeln ist und der Controller definitiv keine SQL-Abfragen aufruft, wie das, was Sie here oder here (bitte hassen Sie nicht, sie sind fehlgeleitet, nicht böse).

Benutzer den Statuswechsel anzeigen.

Ok, Benutzer hat sich angemeldet (oder ist fehlgeschlagen). Was jetzt? Der Benutzer ist sich dessen immer noch nicht bewusst. Sie müssen also tatsächlich eine Antwort produzieren, und das ist die Verantwortung einer Sichtweise.

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

In diesem Fall ergab die Ansicht basierend auf dem aktuellen Status der Modellschicht eine von zwei möglichen Antworten. Für einen anderen Anwendungsfall würden Sie die Ansicht auswählen, indem Sie verschiedene Vorlagen zum Rendern auswählen, basierend auf "aktuell ausgewähltem Artikel".

Die Präsentationsschicht kann, wie hier beschrieben, ziemlich aufwendig werden: MVC Views in PHP verstehen .

Aber ich mache nur eine REST API!

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

MVC ist nur eine konkrete Lösung für das Prinzip " Separation of Concerns ". MVC trennt die Benutzeroberfläche von der Geschäftslogik und trennt in der Benutzeroberfläche die Handhabung von Benutzereingaben und der Präsentation. Das ist entscheidend. Während es oft als "Triade" beschrieben wird, besteht es nicht aus drei unabhängigen Teilen. Die Struktur ist mehr wie folgt:

Es bedeutet, dass wenn die Logik Ihrer Präsentationsebene nicht existent ist, der pragmatische Ansatz darin besteht, sie als einzelne Ebene zu behalten. Es kann auch einige Aspekte der Modellschicht wesentlich vereinfachen.

Mit diesem Ansatz kann das Login-Beispiel (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);
}

Obwohl dies nicht nachhaltig ist, wenn Sie komplizierte Logik zum Rendern eines Antwortkörpers haben, ist diese Vereinfachung sehr nützlich für trivialere Szenarien. Aber seien Sie gewarnt , dieser Ansatz wird zum Albtraum, wenn Sie versuchen, große Codebasen mit komplexer Darstellungslogik zu verwenden.

Wie baut man das Modell?

Da es keine einzige "Model" -Klasse gibt (wie oben erklärt), "modellierst du wirklich nicht". Stattdessen beginnen Sie mit der Erstellung von Diensten , die bestimmte Methoden ausführen können. Und dann implementieren Sie Domain-Objekte und Mapper .

Ein Beispiel für eine Servicemethode:

In den beiden obigen Ansätzen gab es diese Login-Methode für den Identifizierungsdienst. Wie würde es eigentlich aussehen? Ich benutze eine leicht modifizierte Version der gleichen Funktionalität aus einer 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 können, gibt es auf dieser Abstraktionsebene keinen Hinweis darauf, woher die Daten abgerufen wurden. Es könnte eine Datenbank sein, aber es könnte auch nur ein Mock-Objekt für Testzwecke sein. Sogar die Datenmapper, 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 zum Erstellen von Mappern

Um eine Abstraktion der Persistenz zu implementieren, ist es am flexibelsten, benutzerdefinierte Datenmapper zu erstellen.

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 ). Beide würden wahrscheinlich einen separaten passenden Mapper haben, da sie verschiedene Felder enthalten. Aber Sie werden auch mit gemeinsamen und häufig verwendeten Operationen enden. Zum Beispiel: Aktualisierung der "Zuletzt gesehen Online" -Zeit. Und anstatt die vorhandenen Mapper zu verschachteln, ist der pragmatischere Ansatz, einen allgemeinen "User Mapper" zu haben, der nur diesen Timestamp aktualisiert.

Einige zusätzliche Kommentare:

  1. Datenbanktabellen und -modell

    Während es manchmal eine direkte 1: 1: 1-Beziehung zwischen einer Datenbanktabelle, einem Domain-Objekt und einem Mapper gibt , ist es in größeren Projekten möglicherweise weniger üblich als erwartet:

    • Informationen, die von einem einzelnen Domänenobjekt verwendet werden, können aus verschiedenen Tabellen zugeordnet 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 in der Datenbank.

    • Ein einzelner Mapper kann mehrere Tabellen betreffen.

      Beispiel: Wenn Sie Daten vom User speichern, könnte dieses Domänenobjekt die Sammlung anderer Domänenobjekte - Gruppeninstanzen enthalten. Wenn Sie sie ändern und den User speichern, muss der Data Mapper Einträge in mehreren 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 häufig verwendete Daten getrennt von größeren Inhaltsblöcken zu speichern, was selten erforderlich ist. In diesem Fall haben Sie möglicherweise noch eine einzelne User , aber die darin enthaltenen Informationen hängen davon ab, ob vollständige Details abgerufen wurden.

    • Für jedes Domain-Objekt kann es mehrere Mapper geben

      Beispiel: Sie verfügen über eine Nachrichtenwebsite mit einem gemeinsamen Code für die öffentliche und die Verwaltungssoftware. Während beide Schnittstellen die gleiche Article Klasse verwenden, benötigt die Verwaltung jedoch viel mehr Informationen. In diesem Fall hätten Sie zwei separate Mapper: "intern" und "extern". Jede führt andere Abfragen aus oder verwendet sogar verschiedene Datenbanken (wie in Master oder Slave).

  2. Eine Ansicht ist keine Vorlage

    Instanzen in MVC (wenn Sie nicht die MVP-Variante des Musters verwenden) sind für die Darstellungslogik verantwortlich. Dies bedeutet, dass jede Ansicht in der Regel mindestens einige Vorlagen jongliert. Er erfasst Daten von der Modellschicht und wählt dann basierend auf den 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 die Präsentation von Benutzerliste und Kommentaren unterhalb eines Artikels übergeben. Weil sie beide die gleiche Darstellungslogik haben. Sie wechseln nur die Vorlagen.

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

  3. Was ist mit der alten Version der Antwort?

    Die einzige große Änderung ist, dass das, was in der alten Version Model genannt wird, eigentlich ein Service ist . Der Rest der "Bibliotheksanalogie" hält sich ziemlich gut.

    Der einzige Makel, 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 anfassen könnten, weil sonst die Abstraktion zu "lecken" beginnt. Ich muss vielleicht an eine passendere Analogie denken.

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

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

    Wenn Sie mit Websites arbeiten, die MVC-Entwurfsmuster verwenden, besteht der beste Weg darin, eine 1: 1-Beziehung zwischen Ansichten und Controllern zu erstellen. 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 wichtigen Funktionen für den UI-Layer enthalten, wenn es um den Umgang mit Artikeln geht (natürlich könnten Sie einige XHR Komponenten haben, die nicht direkt mit Artikeln in Verbindung stehen) .

Ich bin gerade dabei, das MVC-Framework zu verstehen, und ich frage mich oft, wie viel Code in das Modell passen sollte. Ich habe tendenziell 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 Eigenschaften der Datenbank und den obigen Code haben oder ist es in Ordnung, den Code zu trennen, der tatsächlich die Datenbank funktioniert?

Werde ich am Ende vier Schichten haben?


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

Das ursprüngliche Konzept (1) beschreibt das Modell als 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. Aber zumindest sollten diese Modelle die eingehenden Daten und Beziehungen noch validieren.

In beiden Fällen sind Sie nicht weit entfernt, wenn Sie die SQL-Stuff- oder Datenbankaufrufe in eine andere Schicht aufteilen. Auf diese Weise müssen Sie sich nur mit dem tatsächlichen Daten / Verhalten befassen, nicht mit der tatsächlichen Speicher-API. (Es ist jedoch unvernünftig, es zu übertreiben. Sie werden zB niemals in der Lage sein, ein Datenbank-Backend durch ein Dateispeicher zu ersetzen, wenn das nicht vorher geplant war.)


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

Modell ( M ) -> Hat die Attribute, die den Status der Anwendung halten und nichts über V und C wissen.

View ( V ) -> Zeigt das Format für die Anwendung an und weiß nur, wie das Modell zu verarbeiten ist, und kümmert sich nicht um C

Controller ( C ) ----> Hat die Verarbeitung Teil der Anwendung und fungiert als Verdrahtung zwischen M und V und es hängt von beiden M , V Gegensatz zu M und V

Insgesamt gibt es eine Trennung von Interesse zwischen jedem. 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. behandelt. Also, wenn ich meine Datenbank von MySQL zu PostgreSQL ändern musste, wird es kein Problem geben. Daher kann das Hinzufügen dieser zusätzlichen Ebene nützlich sein.

Jede Tabelle kann ihre eigene Klasse haben 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 schaffen.


Ich würde immer einen Unix-Zeitstempel verwenden, wenn ich mit MySQL und PHP arbeite. Der Hauptgrund dafür ist die Standardmethode in PHP, die einen Zeitstempel als Parameter verwendet, sodass keine Analyse erforderlich ist.

Um den aktuellen Unix-Zeitstempel in PHP abzurufen, führen Sie einfach time();
und in MySQL SELECT UNIX_TIMESTAMP(); .





php oop model-view-controller architecture model