<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:
- Sie können die benötigten Dienste direkt in die Konstruktoren Ihrer Views und Controller injizieren, vorzugsweise unter Verwendung eines DI-Containers.
- 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:
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 .
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:
-
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).
-
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.
-
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.
-
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) .