Je pensais prendre une fissure à répondre à ma propre question. Ce qui suit est juste une façon de résoudre les questions 1 à 3 dans ma question initiale.
Avertissement: je ne peut pas toujours utiliser les bons termes lors de la description des modèles ou techniques. Désolé pour ça.
Les Objectifs:
- Créer un exemple complet d'un contrôleur de base pour la visualisation et l'édition d'
Users
.
- Tout le code doit être entièrement vérifiés et mockable.
- Le contrôleur doit avoir aucune idée de l'endroit où sont stockées les données (ce qui signifie qu'il peut être changé).
- Exemple pour montrer un SQL de mise en œuvre (les plus courantes).
- Pour des performances maximales, les contrôleurs devraient recevoir uniquement les données dont ils ont besoin-pas de champs supplémentaires.
- La mise en œuvre devrait tirer parti de certains type de mapper des données pour faciliter le développement.
- Mise en œuvre devrait avoir la capacité d'exécuter le complexe de données de recherches.
La Solution
Je vais partager mon stockage persistant (base de données) de l'interaction en deux catégories: R (Lecture) et de la CUD (Create, Update, Delete). Mon expérience a été que les lectures sont vraiment quelles sont les causes d'une demande de ralentir. Et tandis que la manipulation de données (CUD) est en fait plus lentement, il se passe beaucoup moins souvent, et est donc beaucoup moins d'inquiétude.
De la CUD (Create, Update, Delete) est facile. Ceci implique une collaboration avec de véritables modèles, qui sont ensuite transmis à mes Repositories
pour la persistance. Remarque, mon référentiels de toujours fournir une méthode de Lecture, mais simplement pour la création d'objet, pas d'affichage. Plus sur cela plus tard.
R (Lu) n'est pas si facile. Pas de modèles ici, juste des objets de valeur. Utiliser des tableaux si vous préférez. Ces objets peuvent représenter un modèle unique ou un mélange de plusieurs modèles, n'importe quoi vraiment. Ces ne sont pas très intéressantes sur leur propre, mais comment ils sont générés. J'utilise ce que j'appelle Query Objects
.
Le Code:
Modèle Utilisateur
Commençons simple avec notre base de modèle d'utilisateur. Notez qu'il n'y a pas d'ORM extension ou la base de données des trucs à tous. Juste pur modèle de gloire. Ajouter votre getters, setters, de validation, de quoi que ce soit.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Référentiel D'Interface
Avant que je créer mon dépôt, je veux créer mon référentiel de l'interface. Cela permettra de définir le "contrat" que les référentiels doivent suivre dans l'ordre pour être utilisé par mon contrôleur. Rappelez-vous, mon contrôleur ne sais pas où les données sont stockées.
Notez que mon référentiels que tous contiennent ces trois méthodes. L' save()
méthode est à la fois responsable de la création et de la mise à jour des utilisateurs, tout simplement en fonction de si oui ou non l'objet utilisateur dispose d'un identifiant.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
SQL Référentiel de mise en Œuvre
Maintenant, pour créer mon implémentation de l'interface. Comme mentionné, mon exemple ne pouvait être qu'avec une base de données SQL. Remarque l'utilisation d'un mapper des données pour éviter d'avoir à écrire répétitif des requêtes SQL.
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');
}
}
Objet De Requête De L'Interface
Maintenant, avec la CUD (Create, Update, Delete) pris en charge par notre référentiel, nous pouvons nous concentrer sur la R (Read). Les objets de requête sont tout simplement une encapsulation de certains types de données logique de recherche. Ils sont pas générateurs de requêtes. En faisant abstraction comme notre référentiel, nous pouvons changer la mise en oeuvre et tester plus facilement. Un exemple d'un Objet de Requête peut être un AllUsersQuery
ou AllActiveUsersQuery
, ou même MostCommonUserFirstNames
.
Vous pensez peut-être "je ne peux pas juste de créer des méthodes dans mes dépots à ces requêtes?" Oui, mais ici, c'est pourquoi je ne le fais pas:
- Mes dépôts sont conçus pour travailler avec des objets de modèle. Dans un monde réel, app, pourquoi aurais-je besoin d'obtenir l'
password
si je suis à la recherche de la liste de tous mes utilisateurs?
- Les dépôts sont souvent des modèles spécifiques, mais les requêtes impliquent souvent plus d'un modèle. Donc, ce référentiel ne vous mettez votre méthode?
- Ce qui me permet de garder des référentiels très simple-pas de ballonnement classe de méthodes.
- Toutes les requêtes sont désormais organisés dans leurs propres classes.
- Vraiment, à ce stade, les référentiels existent tout simplement abstraction de ma couche de base de données.
Pour mon exemple, je vais créer un objet de requête de recherche "AllUsers". Voici l'interface:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Objet De Requête De Mise En Œuvre
C'est là que nous pouvons utiliser un mappeur de données de nouveau pour aider à accélérer le développement. Notez que je suis en permettant un réglage pour le retour de l'ensemble de données-les champs. C'est à peu près aussi loin que je veux aller à la manipulation de la requête. Souvenez-vous, mes objets de requête ne sont pas générateurs de requêtes. Ils suffit d'effectuer une requête spécifique. Cependant, depuis que je sais que je vais probablement utiliser beaucoup celle-ci, dans un certain nombre de situations différentes, je me donne la possibilité de spécifier les champs. Je ne veux plus jamais retourner les champs je n'ai pas besoin!
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();
}
}
Avant de passer à la manette, je veux montrer un autre exemple pour illustrer la puissance de ce qui est. J'ai peut-être un moteur de génération de rapports et la nécessité de créer un rapport pour AllOverdueAccounts
. Ce qui peut être délicat avec mon mapper des données, et je veux écrire quelques - SQL
dans cette situation. Pas de problème, voici ce que cet objet de requête pourrait ressembler à:
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...";
}
}
Ce bien garde toute ma logique pour ce rapport dans une classe, et il est facile de tester. Je peux moquer de mon cœur le contenu, ou même utiliser une mise en œuvre différente entièrement.
Le Contrôleur
Maintenant la partie amusante-rassembler tous les morceaux ensemble. Notez que je suis à l'aide de l'injection de dépendance. Généralement, les dépendances sont injectés dans le constructeur, mais je préfère de les injecter dans mes méthodes de contrôleur (routes). Cela réduit le contrôleur de l'objet graphique, et je trouve cela plus lisible. Remarque, si vous n'aimez pas cette approche, il suffit d'utiliser la traditionnelle méthode de constructeur.
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;
}
}
Réflexions Finales:
Les choses importantes à noter ici sont que lorsque je fais des modifications (création, mise à jour ou suppression) d'entités, je suis en train de travailler avec de vrais objets de modèle, et l'accomplissement de la persistance à travers mes dépôts.
Cependant, quand je suis à l'affichage (sélection des données et de les envoyer sur le point de vue) je ne travaille pas avec des objets de modèle, mais plutôt de la plaine de vieux objets de valeur. Je sélectionner uniquement les champs dont j'ai besoin, et il est conçu de sorte que je peux au maximum mes données de recherche de performance.
Mon dépôts séjour très propre, et, au lieu de ce "désordre" est organisé dans mon modèle de requêtes.
J'utilise un mappeur de données à l'aide au développement, comme c'est juste ridicule d'écrire répétitif SQL pour les tâches courantes. Cependant, vous ne pouvez absolument écrire de SQL en cas de besoin (les requêtes compliquées, rapports, etc.). Et quand vous faites, il est bien rangé dans un bien nommé de la classe.
J'aimerais entendre votre point de vue sur mon approche!