Contrôleurs et modèles Fat: un problème inévitable pour la plupart des projets à grande échelle basés sur des frameworks MVC tels que Yii et Laravel. La principale chose qui engraisse les contrôleurs et les modèles est le Enregistrement actif , une composante puissante et essentielle de ces cadres.
L'enregistrement actif est un modèle architectural, une approche d'accès aux données d'une base de données. Il a été nommé par Martin Fowler dans son livre de 2003 Modèles d'architecture d'application d'entreprise et est largement utilisé dans PHP Cadres.
Bien qu'il s'agisse d'une approche très nécessaire, le modèle d'enregistrement actif (AR) enfreint le principe de responsabilité unique (SRP) car les modèles AR:
Cette violation du SRP est un bon compromis pour un développement rapide lorsque vous devez créer un prototype d'application dès que possible, mais elle est assez nuisible lorsque l'application se transforme en un projet de moyenne ou grande envergure. Les modèles «Dieu» et les gros contrôleurs sont difficiles à tester et à maintenir, et l'utilisation libre de modèles partout dans les contrôleurs conduit à d'énormes difficultés lorsque vous devez inévitablement changer la structure de la base de données.
La solution est simple: divisez la responsabilité de l'enregistrement actif en plusieurs couches et injectez des dépendances entre couches. Cette approche simplifiera également les tests car elle vous permet de simuler les couches qui ne sont pas actuellement testées.
Une application PHP MVC «grosse» a des dépendances partout, imbriquées et sujettes aux erreurs, tandis qu'une structure en couches utilise l'injection de dépendances pour garder les choses propres et claires.
Nous allons couvrir cinq couches principales:
Pour implémenter une structure en couches, nous avons besoin d'un conteneur d'injection de dépendance , un objet qui sait comment instancier et configurer des objets. Vous n’avez pas besoin de créer une classe car le framework gère toute la magie. Considérer ce qui suit:
class SiteController extends IlluminateRoutingController { protected $userService; public function __construct(UserService $userService) { $this->userService = $userService; } public function showUserProfile(Request $request) { $user = $this->userService->getUser($request->id); return view('user.profile', compact('user')); } } class UserService { protected $userRepository; public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } public function getUser($id) { $user = $this->userRepository->getUserById($id); $this->userRepository->logSession($user); return $user; } } class UserRepository { protected $userModel, $logModel; public function __construct(User $user, Log $log) { $this->userModel = $user; $this->logModel = $log; } public function getUserById($id) { return $this->userModel->findOrFail($id); } public function logSession($user) { $this->logModel->user = $user->id; $this->logModel->save(); } }
Dans l'exemple ci-dessus, UserService
est injecté dans SiteController
, UserRepository
est injecté dans UserService
et les modèles AR User
et Logs
sont injectés dans le UserRepository
classe. Ce code de conteneur est assez simple, alors parlons des couches.
Les frameworks MVC modernes comme Laravel et Yii relèvent pour vous bon nombre des défis des contrôleurs traditionnels: la validation d'entrée et les pré-filtres sont déplacés vers une autre partie de l'application (dans Laravel, c'est dans ce qu'on appelle middleware alors qu'en Yii, il s'appelle comportement ) tandis que les règles de routage et de verbe HTTP sont gérées par le framework. Cela laisse une fonctionnalité très étroite au programmeur pour coder dans un contrôleur.
L'essence d'un contrôleur est d'obtenir une demande et de fournir les résultats. Un contrôleur ne doit contenir aucune logique métier d'application; sinon, il est difficile de réutiliser le code ou de modifier la façon dont l’application communique. Si vous devez créer une API au lieu de rendre des vues, par exemple, et que votre contrôleur ne contient aucune logique, vous changez simplement la façon dont vous renvoyez vos données et vous êtes prêt à partir.
Cette fine couche de contrôleur confond souvent les programmeurs et, comme un contrôleur est une couche par défaut et le point d'entrée le plus élevé, de nombreux développeurs continuent d'ajouter du nouveau code à leurs contrôleurs sans aucune réflexion supplémentaire sur l'architecture. En conséquence, des responsabilités excessives s'ajoutent, des responsabilités telles que:
Prenons un exemple de contrôleur surdimensionné:
//A bad example of a controller public function user(Request $request) { $user = User::where('id', '=', $request->id) ->leftjoin('posts', function ($join) { $join->on('posts.user_id', '=', 'user.id') ->where('posts.status', '=', Post::STATUS_APPROVED); }) ->first(); if (!empty($user)) { $user->last_login = date('Y-m-d H:i:s'); } else { $user = new User(); $user->is_new = true; $user->save(); } return view('user.index', compact('user')); }
Pourquoi cet exemple est-il mauvais? Pour de nombreuses raisons:
last_login
champ, vous devez le changer dans tous les contrôleurs.Un contrôleur doit être mince; vraiment, tout ce qu'il devrait faire est de prendre une demande et de renvoyer les résultats. Voici un bon exemple:
//A good example of a controller public function user (Request $request) { $user = $this->userService->getUserById($request->id); return view('user.index', compact('user')); }
Mais où vont tous ces autres trucs? Il appartient à la couche de service .
La couche de service est une couche de logique métier. Ici, et seulement ici, les informations sur le flux des processus métier et l'interaction entre les modèles commerciaux doivent être situées. Il s'agit d'une couche abstraite et elle sera différente pour chaque application, mais le principe général est l'indépendance de votre source de données (la responsabilité d'un contrôleur) et le stockage des données (la responsabilité d'une couche inférieure).
C'est l'étape avec le plus de potentiel de problèmes de croissance. Souvent, un modèle d'enregistrement actif est renvoyé à un contrôleur et, par conséquent, la vue (ou dans le cas d'une réponse API, le contrôleur) doit fonctionner avec le modèle et être conscient de ses attributs et dépendances. Cela rend les choses désordonnées; si vous décidez de changer une relation ou un attribut d'un modèle Active Record, vous devez le changer partout dans toutes vos vues et contrôleurs.
Voici un exemple courant que vous pourriez rencontrer d'un modèle d'enregistrement actif utilisé dans une vue:
@foreach($user->posts as $post) - {{$post->title}}
@endforeach
Cela semble simple, mais si je renomme le first_name
champ, je dois soudainement changer toutes les vues qui utilisent le champ de ce modèle, un processus sujet aux erreurs. Le moyen le plus simple d'éviter cette énigme consiste à utiliser des objets de transfert de données, ou DTO.
Les données de la couche de service doivent être enveloppées dans un simple objet immuable - ce qui signifie qu'elles ne peuvent pas être modifiées après leur création - nous n'avons donc pas besoin de setters pour un DTO. En outre, la classe DTO doit être indépendante et ne pas étendre les modèles Active Record. Attention, cependant, un modèle commercial n'est pas toujours le même qu'un modèle AR.
Envisagez une application de livraison d'épicerie. Logiquement, une commande d'épicerie doit inclure des informations de livraison, mais dans la base de données, nous stockons les commandes et les lions à un utilisateur, et l'utilisateur est lié à une adresse de livraison. Dans ce cas, il existe plusieurs modèles AR, mais les couches supérieures ne devraient pas en avoir connaissance. Notre classe DTO contiendra non seulement la commande, mais également les informations de livraison et toute autre pièce qui correspond à un modèle commercial. Si nous modifions les modèles AR liés à ce modèle commercial (par exemple, nous déplaçons les informations de livraison dans la table de commande), nous modifierons uniquement le mappage des champs dans l'objet DTO, plutôt que de modifier votre utilisation des champs de modèle AR partout dans le code.
En utilisant une approche DTO, nous supprimons la tentation de changer le modèle d'enregistrement actif dans le contrôleur ou dans la vue. Deuxièmement, l'approche DTO résout le problème de la connectivité entre le stockage physique des données et la représentation logique d'un modèle d'entreprise abstrait. Si quelque chose doit être modifié au niveau de la base de données, les modifications affecteront l'objet DTO plutôt que les contrôleurs et les vues. Vous voyez un modèle?
Jetons un coup d'œil à un DTO simple:
//Example of simple DTO class. You can add any logic of conversion from an Active Record object to business model here class DTO { private $entity; public static function make($model) { return new self($model); } public function __construct($model) { $this->entity = (object) $model->toArray(); } public function __get($name) { return $this->entity->{$name}; } }
L'utilisation de notre nouveau DTO est tout aussi simple:
//usage example public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); return view('user.index', compact('user')); }
Pour séparer la logique de vue (comme choisir la couleur d’un bouton en fonction d’un état), il est judicieux d’utiliser une couche supplémentaire de décorateurs. UNE décorateur est un modèle de conception qui permet d'embellir un objet principal en l'enveloppant avec des méthodes personnalisées. Cela se produit généralement dans la vue avec une logique quelque peu spéciale.
Alors qu'un objet DTO peut effectuer le travail d'un décorateur, il ne fonctionne vraiment que pour les actions courantes telles que le formatage de la date. Un DTO doit représenter un modèle commercial, tandis qu'un décorateur embellit les données avec du HTML pour des pages spécifiques.
Examinons un extrait d'une icône d'état de profil utilisateur qui n'emploie pas de décorateur:
@if($user->status == AppModelsUser::STATUS_ONLINE) Online @else Offline @endif {{date('F j, Y', strtotime($user->lastOnline))}}
Bien que cet exemple soit simple, il serait facile pour un développeur de se perdre dans une logique plus compliquée. C'est là qu'intervient un décorateur pour nettoyer la lisibilité du HTML. Développons notre extrait d'icône d'état en un cours de décorateur complet:
class UserProfileDecorator { private $entity; public static function decorate($model) { return new self($model); } public function __construct($model) { $this->entity = $model; } public function __get($name) { $methodName = 'get' . $name; if (method_exists(self::class, $methodName)) { return $this->$methodName(); } else { return $this->entity->{$name}; } } public function __call($name, $arguments) { return $this->entity->$name($arguments); } public function getStatus() { if($this->entity->status == AppModelsUser::STATUS_ONLINE) { return 'Online'; } else { return 'Offline'; } } public function getLastOnline() { return date('F j, Y', strtotime($this->entity->lastOnline)); } }
L'utilisation du décorateur est simple:
public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); $user = UserProfileDecorator::decorate($user); return view('user.index', compact('user')); }
Nous pouvons désormais utiliser les attributs de modèle dans la vue sans aucune condition ni logique, et c'est beaucoup plus lisible:
{{$user->status}} {{$user->lastOnline}}
Les décorateurs peuvent également être combinés:
public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); $user = UserDecorator::decorate($user); $user = UserProfileDecorator::decorate($user); return view('user.index', compact('user')); }
Chaque décorateur fera son travail et ne décorera que sa propre partie. Cette intégration récursive de plusieurs décorateurs permet une combinaison dynamique de leurs fonctionnalités sans introduire de classes supplémentaires.
La couche référentiel fonctionne avec la mise en œuvre concrète du stockage de données. Il est préférable d’injecter le référentiel via une interface pour plus de flexibilité et un remplacement facile. Si vous modifiez votre stockage de données, vous devez créer un nouveau référentiel qui implémente votre interface de référentiel, mais au moins vous n'avez pas à modifier les autres couches.
Le référentiel joue le rôle d'un objet de requête: il récupère les données de la base de données et conduit le travail de plusieurs modèles Active Record. Les modèles Active Record, dans ce contexte, jouent le rôle d'entités de modèle de données uniques - tout objet du système sur lequel vous souhaitez modéliser et stocker des informations. Bien que chaque entité contienne des informations, elle ne sait pas comment elles sont apparues (si elles ont été créées ou obtenues à partir de la base de données), ni comment enregistrer et modifier son propre état. La responsabilité du référentiel est de sauvegarder et / ou de mettre à jour une entité; cela permet une meilleure séparation des préoccupations en maintenant la gestion des entités dans le référentiel et en simplifiant les entités.
Voici un exemple simple de méthode de référentiel qui crée une requête en utilisant les connaissances sur la base de données et les relations Active Record:
public function getUsers() { return User::leftjoin('posts', function ($join) { $join->on('posts.user_id', '=', 'user.id') ->where('posts.status', '=', Post::STATUS_APPROVED); }) ->leftjoin('orders', 'orders.user_id', '=', 'user.id') ->where('user.status', '=', User::STATUS_ACTIVE) ->where('orders.price', '>', 100) ->orderBy('orders.date') ->with('info') ->get(); }
Dans une application nouvellement créée, vous ne trouverez que des dossiers pour les contrôleurs, les modèles et les vues. Ni Yii ni Laravel n’ajoutent de couches supplémentaires dans la structure de leur exemple d’application. Simple et intuitive, même pour les novices, la structure MVC simplifie le travail avec le framework, mais il est important de comprendre que leur exemple d'application est un exemple; ce n’est pas un standard ou un style et n’impose aucune règle sur l’architecture des applications. En divisant les tâches en couches de responsabilité distinctes et uniques, nous obtenons une architecture flexible et extensible qui est facile à maintenir. Rappelles toi:
Donc, si vous démarrez un projet complexe ou un projet qui a une chance de se développer à l'avenir, envisagez une répartition claire des responsabilités entre les couches contrôleur, service et référentiel.