Ich dachte, ich würde eine Pause einlegen, um meine eigene Frage zu beantworten. Was folgt, ist nur eine Möglichkeit, die Probleme 1-3 in meiner ursprünglichen Frage zu lösen.
Haftungsausschluss: Ich verwende möglicherweise nicht immer die richtigen Begriffe, wenn ich Muster oder Techniken beschreibe. Das tut mir leid.
Die Ziele:
- Erstellen Sie ein vollständiges Beispiel eines Basis-Controllers zum Anzeigen und Bearbeiten
Users
.
- Der gesamte Code muss vollständig testbar und verspottbar sein.
- Der Controller sollte keine Ahnung haben, wo die Daten gespeichert sind (was bedeutet, dass sie geändert werden können).
- Beispiel zum Anzeigen einer SQL-Implementierung (am häufigsten).
- Für maximale Leistung sollten Controller nur die Daten empfangen, die sie benötigen - keine zusätzlichen Felder.
- Die Implementierung sollte eine Art Datenmapper nutzen, um die Entwicklung zu vereinfachen.
- Die Implementierung sollte in der Lage sein, komplexe Datensuchen durchzuführen.
Die Lösung
Ich teile meine permanente Speicherinteraktion (Datenbankinteraktion) in zwei Kategorien auf: R (Lesen) und CUD (Erstellen, Aktualisieren, Löschen). Ich habe die Erfahrung gemacht, dass das Lesen wirklich dazu führt, dass eine Anwendung langsamer wird. Und während die Datenmanipulation (CUD) tatsächlich langsamer ist, kommt sie viel seltener vor und ist daher viel weniger besorgniserregend.
CUD (Erstellen, Aktualisieren, Löschen) ist einfach. Dies beinhaltet die Arbeit mit tatsächlichen Modellen , die dann Repositories
zur Persistenz an meine übergeben werden . Beachten Sie, dass meine Repositorys weiterhin eine Lesemethode bereitstellen, jedoch nur zur Objekterstellung und nicht zur Anzeige. Dazu später mehr.
R (Lesen) ist nicht so einfach. Keine Modelle hier, nur Wertobjekte . Verwenden Sie Arrays, wenn Sie dies bevorzugen . Diese Objekte können ein einzelnes Modell oder eine Mischung aus vielen Modellen darstellen, eigentlich alles. Diese sind für sich genommen nicht sehr interessant, aber wie sie erzeugt werden, ist. Ich benutze was ich rufe Query Objects
.
Der Code:
Benutzermodell
Beginnen wir einfach mit unserem grundlegenden Benutzermodell. Beachten Sie, dass es überhaupt keine ORM-Erweiterung oder Datenbankmaterial gibt. Nur purer Model-Ruhm. Fügen Sie Ihre Getter, Setter, Validierungen, was auch immer hinzu.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Repository-Schnittstelle
Bevor ich mein Benutzer-Repository erstelle, möchte ich meine Repository-Schnittstelle erstellen. Dies definiert den "Vertrag", dem Repositorys folgen müssen, um von meinem Controller verwendet zu werden. Denken Sie daran, mein Controller weiß nicht, wo die Daten tatsächlich gespeichert sind.
Beachten Sie, dass meine Repositorys nur alle diese drei Methoden enthalten. Die save()
Methode ist sowohl für das Erstellen als auch für das Aktualisieren von Benutzern verantwortlich, je nachdem, ob für das Benutzerobjekt eine ID festgelegt wurde oder nicht.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
SQL Repository-Implementierung
Nun erstelle ich meine Implementierung der Schnittstelle. Wie bereits erwähnt, sollte mein Beispiel eine SQL-Datenbank sein. Beachten Sie die Verwendung eines Daten-Mappers , um zu verhindern, dass sich wiederholende SQL-Abfragen geschrieben werden müssen.
class SQLUserRepository implements UserRepositoryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function find($id)
{
// Find a record with the id = $id
// from the 'users' table
// and return it as a User object
return $this->db->find($id, 'users', 'User');
}
public function save(User $user)
{
// Insert or update the $user
// in the 'users' table
$this->db->save($user, 'users');
}
public function remove(User $user)
{
// Remove the $user
// from the 'users' table
$this->db->remove($user, 'users');
}
}
Objektoberfläche abfragen
Jetzt, da CUD (Erstellen, Aktualisieren, Löschen) von unserem Repository erledigt wird, können wir uns auf das R (Lesen) konzentrieren. Abfrageobjekte sind einfach eine Kapselung einer Art von Datensuchlogik. Sie sind keine Abfrage-Builder. Indem wir es wie unser Repository abstrahieren, können wir seine Implementierung ändern und es einfacher testen. Ein Beispiel für ein Abfrageobjekt kann ein AllUsersQuery
oder AllActiveUsersQuery
oder sogar sein MostCommonUserFirstNames
.
Sie denken vielleicht: "Kann ich nicht einfach Methoden für diese Abfragen in meinen Repositorys erstellen?" Ja, aber hier ist, warum ich das nicht mache:
- Meine Repositorys sind für die Arbeit mit Modellobjekten gedacht. Warum sollte ich in einer realen App jemals das
password
Feld abrufen müssen, wenn ich alle meine Benutzer auflisten möchte ?
- Repositorys sind häufig modellspezifisch, Abfragen umfassen jedoch häufig mehr als ein Modell. In welches Repository stellen Sie Ihre Methode?
- Dies hält meine Repositorys sehr einfach - keine aufgeblähte Klasse von Methoden.
- Alle Abfragen sind jetzt in eigenen Klassen organisiert.
- Zu diesem Zeitpunkt existieren wirklich Repositorys, um meine Datenbankebene zu abstrahieren.
In meinem Beispiel erstelle ich ein Abfrageobjekt, um nach "AllUsers" zu suchen. Hier ist die Schnittstelle:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Implementierung des Abfrageobjekts
Hier können wir wieder einen Data Mapper verwenden, um die Entwicklung zu beschleunigen. Beachten Sie, dass ich eine Änderung am zurückgegebenen Datensatz zulasse - die Felder. Dies ist ungefähr so weit, wie ich mit der Manipulation der ausgeführten Abfrage gehen möchte. Denken Sie daran, dass meine Abfrageobjekte keine Abfrageersteller sind. Sie führen einfach eine bestimmte Abfrage durch. Da ich jedoch weiß, dass ich diese wahrscheinlich in verschiedenen Situationen häufig verwenden werde, gebe ich mir die Möglichkeit, die Felder anzugeben. Ich möchte niemals Felder zurückgeben, die ich nicht brauche!
class AllUsersQuery implements AllUsersQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch($fields)
{
return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
}
}
Bevor ich zum Controller übergehe, möchte ich ein weiteres Beispiel zeigen, um zu veranschaulichen, wie leistungsfähig dies ist. Vielleicht habe ich eine Berichts-Engine und muss einen Bericht für erstellen AllOverdueAccounts
. Dies kann mit meinem Daten-Mapper schwierig sein, und ich möchte SQL
in dieser Situation möglicherweise einige aktuelle Informationen schreiben . Kein Problem, so könnte dieses Abfrageobjekt aussehen:
class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch()
{
return $this->db->query($this->sql())->rows();
}
public function sql()
{
return "SELECT...";
}
}
Dadurch bleibt meine gesamte Logik für diesen Bericht in einer Klasse und es ist einfach zu testen. Ich kann es nach Herzenslust verspotten oder sogar eine ganz andere Implementierung verwenden.
Der Controller
Nun der lustige Teil - alle Teile zusammenbringen. Beachten Sie, dass ich die Abhängigkeitsinjektion verwende. Normalerweise werden Abhängigkeiten in den Konstruktor eingefügt, aber ich ziehe es tatsächlich vor, sie direkt in meine Controller-Methoden (Routen) einzufügen. Dies minimiert das Objektdiagramm des Controllers und ich finde es tatsächlich besser lesbar. Hinweis: Wenn Ihnen dieser Ansatz nicht gefällt, verwenden Sie einfach die traditionelle Konstruktormethode.
class UsersController
{
public function index(AllUsersQueryInterface $query)
{
// Fetch user data
$users = $query->fetch(['first_name', 'last_name', 'email']);
// Return view
return Response::view('all_users.php', ['users' => $users]);
}
public function add()
{
return Response::view('add_user.php');
}
public function insert(UserRepositoryInterface $repository)
{
// Create new user model
$user = new User;
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the new user
$repository->save($user);
// Return the id
return Response::json(['id' => $user->id]);
}
public function view(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('view_user.php', ['user' => $user]);
}
public function edit(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('edit_user.php', ['user' => $user]);
}
public function update(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Update the user
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the user
$repository->save($user);
// Return success
return true;
}
public function delete(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Delete the user
$repository->delete($user);
// Return success
return true;
}
}
Abschließende Gedanken:
Die wichtigsten Dinge, die hier zu beachten sind, sind, dass ich beim Ändern (Erstellen, Aktualisieren oder Löschen) von Entitäten mit realen Modellobjekten arbeite und die Persistenz über meine Repositorys durchführe.
Beim Anzeigen (Auswählen von Daten und Senden an die Ansichten) arbeite ich jedoch nicht mit Modellobjekten, sondern mit einfachen alten Wertobjekten. Ich wähle nur die Felder aus, die ich benötige, und sie sind so konzipiert, dass ich meine Daten-Suchleistung maximieren kann.
Meine Repositorys bleiben sehr sauber, und stattdessen ist dieses "Durcheinander" in meinen Modellabfragen organisiert.
Ich verwende einen Daten-Mapper, um bei der Entwicklung zu helfen, da es einfach lächerlich ist, sich wiederholendes SQL für allgemeine Aufgaben zu schreiben. Sie können jedoch bei Bedarf unbedingt SQL schreiben (komplizierte Abfragen, Berichterstellung usw.). Und wenn Sie dies tun, ist es schön in einer richtig benannten Klasse versteckt.
Ich würde gerne Ihre Meinung zu meinem Ansatz hören!
Update Juli 2015:
Ich wurde in den Kommentaren gefragt, wo ich mit all dem gelandet bin. Naja, eigentlich gar nicht so weit weg. Ehrlich gesagt mag ich Repositories immer noch nicht wirklich. Ich finde sie übertrieben für grundlegende Suchvorgänge (insbesondere wenn Sie bereits ein ORM verwenden) und chaotisch, wenn Sie mit komplizierteren Abfragen arbeiten.
Ich arbeite im Allgemeinen mit einem ORM im ActiveRecord-Stil, daher verweise ich meistens nur direkt auf diese Modelle in meiner Anwendung. In Situationen, in denen ich komplexere Abfragen habe, verwende ich Abfrageobjekte, um diese wiederverwendbarer zu machen. Ich sollte auch beachten, dass ich meine Modelle immer in meine Methoden einfüge, damit sie in meinen Tests leichter verspottet werden können.