Haftungsausschluss: Im Folgenden wird beschrieben, wie ich MVC-ähnliche Muster im Kontext von PHP-basierten Webanwendungen verstehe. Alle externen Links, die im Inhalt 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 (ich habe es auch getan, obwohl die ursprüngliche Antwort geschrieben wurde, als ich anfing, etwas anderes zu lernen) , weil die meisten Frameworks dieses Missverständnis aufrechterhalten.
Es handelt sich weder um eine objektrelationale Zuordnungstechnik (ORM) noch um 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 einer ordnungsgemäßen 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 Bereich der Problemdomäne 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 sollen. Gleichzeitig wissen Domänenobjekte nichts von der Speicherung - weder von wo (SQL-Datenbank, REST-API, Textdatei usw.) noch selbst wenn sie gespeichert oder abgerufen werden.
Datenmapper
Diese Objekte sind nur für die Speicherung verantwortlich. Wenn Sie Informationen in einer Datenbank speichern, befindet sich hier SQL. Oder Sie verwenden eine XML-Datei zum Speichern von Daten, und Ihre Data Mapper analysieren von und zu XML-Dateien.
Dienstleistungen
Sie können als von ihnen denken , „higher level Domain Objects“, sondern von Geschäftslogik, Dienstleistungen sind für die Interaktion verantwortlich zwischen Domain - Objekte und Mapper . Diese Strukturen schaffen schließlich eine "öffentliche" Schnittstelle für die Interaktion mit der Domänengeschäftslogik. Sie können sie vermeiden, aber mit der Strafe, dass einige Domänenlogiken in Controller übertragen werden .
In der Frage zur ACL-Implementierung finden Sie eine entsprechende Antwort auf dieses Thema. Dies kann hilfreich 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 bei der Durchsetzung des Single-Responsibility-Prinzips (SRP).
- 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, falls Sie jemals eine externe API benötigen
Wie interagiere ich mit einem Modell?
Voraussetzungen: Sehen Sie sich die Vorträge "Global State and Singletons" und "Don't Look For Things!" An. aus den Clean Code Talks.
Zugriff auf Dienstinstanzen erhalten
Es gibt zwei allgemeine Ansätze, damit sowohl die View- als auch die Controller- Instanz (was Sie als "UI-Schicht" bezeichnen könnten) auf diese Dienste zugreifen können:
- Sie können die erforderlichen Services direkt in die Konstruktoren Ihrer Ansichten und Controller einfügen, vorzugsweise mithilfe eines DI-Containers.
- 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 (für Anfänger jedoch nicht die einfachste). Die beiden Bibliotheken, die ich für diese Funktionalität in Betracht ziehen möchte, sind die eigenständige DependencyInjection-Komponente von Syfmony oder Auryn .
Mit beiden Lösungen, die eine Factory und einen DI-Container verwenden, können Sie auch die Instanzen verschiedener Server gemeinsam nutzen, die von der ausgewählten Steuerung gemeinsam genutzt und für einen bestimmten Anforderungs- / Antwortzyklus angezeigt werden sollen.
Änderung des Modellzustands
Nachdem Sie in den Controllern auf die Modellebene zugreifen können, müssen Sie diese 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 basierend auf diesen Eingaben den aktuellen Status der Geschäftslogik. In diesem Beispiel sind die Status, zwischen denen geändert 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 sie hier oder hier sehen würden (bitte hassen Sie sie nicht, sie sind fehlgeleitet, nicht böse).
Zeigt dem Benutzer die Statusänderung an.
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 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 ergab die Ansicht eine von zwei möglichen Antworten, basierend auf dem aktuellen Status der Modellschicht. Für einen anderen Anwendungsfall hätten Sie die Ansicht, verschiedene Vorlagen zum Rendern auszuwählen, basierend auf etwas wie "aktuell aus Artikel ausgewählt".
Die Präsentationsebene kann tatsächlich ziemlich aufwändig werden, wie hier beschrieben: Grundlegendes zu MVC-Ansichten in PHP .
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 das Prinzip der Trennung von Bedenken . MVC trennt die Benutzeroberfläche von der Geschäftslogik und in der Benutzeroberfläche die Behandlung der Benutzereingaben und der Präsentation. Das ist entscheidend. Während die Leute es oft als "Triade" beschreiben, besteht es nicht aus drei unabhängigen Teilen. Die Struktur ist eher so:
Wenn die Logik Ihrer Präsentationsebene so gut wie nicht vorhanden ist, besteht der pragmatische Ansatz darin, 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 seien Sie gewarnt , dieser Ansatz wird zu einem Albtraum, wenn Sie versuchen, ihn in großen Codebasen mit komplexer Präsentationslogik zu verwenden.
Wie baue ich das Modell?
Da es keine einzige "Modell" -Klasse gibt (wie oben erläutert), erstellen Sie das Modell wirklich nicht. Stattdessen beginnen Sie mit der Erstellung von Diensten , die bestimmte Methoden ausführen können. Und dann implementieren Domain - Objekte und Mapper .
Ein Beispiel für eine Servicemethode:
In beiden oben genannten Ansätzen gab es diese Anmeldemethode für den Identifikationsdienst. Wie würde es eigentlich aussehen? Ich verwende eine leicht modifizierte Version derselben 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 kann sich um eine Datenbank handeln, es kann sich jedoch auch nur um ein Scheinobjekt zu Testzwecken handeln. 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, müssen bei den flexibelsten Ansätzen benutzerdefinierte Datenzuordnungen erstellt werden .
Aus: PoEAA- Buch
In der Praxis werden sie für die Interaktion mit bestimmten Klassen oder Oberklassen implementiert. Nehmen wir an, Sie haben Customer
und Admin
in Ihrem Code (beide erben von einer User
Oberklasse). Beide würden wahrscheinlich einen separaten passenden Mapper haben, da sie unterschiedliche Felder enthalten. Sie werden aber auch gemeinsame und häufig verwendete Vorgänge haben. Zum Beispiel: Aktualisieren der "zuletzt online gesehenen" Zeit. Und anstatt die vorhandenen Mapper komplizierter zu machen, besteht der pragmatischere Ansatz darin, einen allgemeinen "User Mapper" zu haben, der nur diesen Zeitstempel aktualisiert.
Einige zusätzliche Kommentare:
Datenbanktabellen und Modell
Während manchmal eine direkte 1: 1: 1-Beziehung zwischen einer Datenbanktabelle, einem Domänenobjekt und Mapper besteht , ist diese in größeren Projekten möglicherweise weniger häufig 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 aufweist.
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 Daten aus dem User
Objekt speichern , kann dieses Domänenobjekt eine Sammlung anderer Domänenobjekte - Group
Instanzen - enthalten . Wenn Sie sie ändern und speichern User
, 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 Daten, auf die häufig zugegriffen wird, getrennt von größeren Inhaltsblöcken zu speichern, was selten erforderlich ist. In diesem Fall haben Sie möglicherweise noch eine einzelne User
Klasse, aber die darin enthaltenen Informationen hängen davon ab, ob alle Details abgerufen wurden.
Für jedes Domänenobjekt kann es mehr als einen Mapper geben
Beispiel: Sie haben eine Nachrichtenseite 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).
Eine Ansicht ist keine Vorlage
Ansichtsinstanzen in MVC (wenn Sie nicht die MVP-Variante des Musters verwenden) sind für die Präsentationslogik verantwortlich. Dies bedeutet, dass jede Ansicht normalerweise mindestens einige Vorlagen jongliert. Es erfasst Daten aus der Modellebene 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 der Benutzerliste und der Kommentare unter einem Artikel übergeben lassen. Weil beide dieselbe Präsentationslogik haben. Sie wechseln einfach 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 .
Was ist mit der alten Version der Antwort?
Die einzige wesentliche Änderung besteht darin, dass das, was in der alten Version als Modell bezeichnet wird, tatsächlich ein Dienst 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, da sie Ihnen Informationen aus dem Buch zurückgeben würde, Sie das Buch selbst jedoch nicht berühren könnten, da sonst die Abstraktion "auslaufen" würde. Ich muss mir vielleicht eine passendere Analogie überlegen.
Welche Beziehung besteht 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, ist es am besten, 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, hätten 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 haben Sie möglicherweise einige XHR- Komponenten, die nicht direkt mit Artikeln zusammenhängen) .