Générateur de widgets CRUD pour Yii

Qu'ont en commun les commentaires sur l'article sur Habré et les options supplémentaires lors de l'achat d'une voiture?



Du point de vue de la modélisation des données, les deux sont des entités «imbriquées» qui n'ont pas de signification indépendante isolément de l'objet parent.

Dans Yii ( framework php ), il y a Gii - un générateur de code intégré qui vous permet de créer des interfaces CRUD de base en utilisant un modèle de données en quelques clics de souris, ce qui accélère considérablement le développement, mais ne s'applique qu'à des entités indépendantes, comme l'article ou la machine dans les exemples ci-dessus.

Ce serait formidable de pouvoir générer quelque chose de similaire pour les objets de données «imbriqués», non? Maintenant - vous pouvez, bienvenue à Kat pour plus de détails.

Pour les plus impatients à la fin de l'article, des instructions sont données pour un démarrage rapide.

Et pour ceux qui s'intéressent à l'article, les aspects d'une application métier à un appareil interne sont pris en compte:

  • Analyse de rentabilisation: publication par sujet
    • La liste des sujets sur le principal
    • Liste des postes connexes
  • Sous le capot: un générateur gii basé sur CRUD
    • Modèle de générateur Gii
    • Classe de base du widget
    • Contrôleur de façade intégré
  • Démarrage rapide
    • A propos du support et du développement

Analyse de rentabilisation: publication par sujet


Peut-être commente un habr et un mauvais exemple depuis sont souvent plus utiles que l'article lui-même, mais, dans tous les cas, lors du développement d'une application, il existe souvent des situations où un certain objet du modèle de données présente peu d'intérêt pour l'utilisateur en tant qu'entité indépendante.

Envisagez une tâche commerciale simplifiée: créer un site Web pour publier des messages regroupés par divers sujets.

Le site doit avoir les interfaces suivantes:

  1. La page principale - devrait prendre en charge divers widgets à l'avenir, mais au stade actuel de la mise en œuvre, il n'y en a qu'un: une liste de sujets filtrés par un critère.
  2. Liste complète des sujets - une liste complète des sujets sous forme de tableau;
  3. Page Sujet - informations sur le sujet et une liste des articles publiés dans celui-ci.

Assez standard, non?

Regardons le modèle de données:



Pas de surprise non plus. Deux classes de modèles contiendront notre logique métier:

  • Classe de sujet - données sur le sujet, validation, une liste de publications, ainsi qu'une méthode distincte qui renvoie une liste de sujets filtrés par les critères du widget sur la page principale.
  • La classe Post n'est que des données et de la validation.

L'application sera servie par deux contrôleurs:

  • SiteController - pages standard (à propos de nous, contacts, etc.), autorisation (non requise par les termes de référence, mais nous savons quelque chose) et index - la page principale. Parce que nous nous attendons à de nombreux widgets différents à l'avenir, il est logique de laisser la page principale dans ce contrôleur, et de ne pas la transférer vers un modèle spécifique à un.
  • TopicController est un ensemble standard d'actions: répertorier, créer, modifier, afficher et supprimer des sujets.

Potentiellement, vous pouvez également générer un PostController - à des fins d'administration et / ou copier-coller des morceaux de code dans des widgets personnalisés, mais laisser cela en dehors du champ d'application de cet article.
Jusqu'à présent, la plupart du code peut être généré à l'aide de gii, ce qui accélère le développement et réduit les risques (moins de code manuel = moins de chance de faire une erreur).

Deux questions demeurent:

  1. Comment afficher une liste filtrée de sujets sur la page principale?
  2. Comment afficher une liste des articles par sujet?

Si vous pouvez les résoudre à l'aide d'un générateur automatique - ce sera une réussite solide.

La liste des sujets sur le principal


La page principale desservie par l'adresse du site / index doit contenir une liste de sujets filtrés par un critère prédéterminé. Les critères de filtrage, dans le cadre de la logique métier, nous avons inclus dans le modèle.

Pour l'affichage, il existe plusieurs options de mise en œuvre.

La première, sale et rapide, consiste à tout faire directement dans le fichier de vue ( views / site / index.php ):

  1. Créer ActiveDataProvider ;
  2. Remplissez-le avec les données du modèle de sujet ;
  3. Affichez à l'aide d'un widget ListView / GridView standard, en spécifiant manuellement les champs requis.

Vous pouvez aller un peu plus loin et tout regrouper dans un fichier de vue distinct, quelque chose comme views / site / _topic-list-widget.php , en appelant son rendu à partir du fichier principal. Cela donnera un peu plus de gérabilité et d'extensibilité, mais il semble toujours assez sale.

La plupart d'entre nous sont susceptibles de créer un widget séparé selon toutes les règles, dans un espace de noms distinct ( app \ widgets ou app \ components pour le modèle de base - selon la version que vous utilisez), où ils encapsulent la création d' ActiveDataProvider par modèle et s'affichent dans un espace séparé soumission. Il ne reste plus qu'à appeler ce widget depuis la page principale. Cette solution est la plus correcte du point de vue de la décomposition des classes, de la gérabilité et de l'extensibilité du code.

Mais a-t-on l'impression que le code de ce widget va très bien répéter le code de TopicController en termes de gestion de actionIndex () ? Et c'est tellement ennuyeux d'écrire ce code manuellement.

Il serait préférable de générer ce code automatiquement, puis d'appeler simplement le widget terminé:

<?= \app\widgets\TopicControllerWidget::widget([ 'action' => 'index', 'params' => [ 'query' => app\models\Topic::findBySomeSpecificCriteria() ], ]) ?> 

Liste des postes connexes

La page de visualisation du sujet servi par l'adresse de sujet / vue doit contenir des informations sur le sujet lui-même et une liste des messages qui y sont publiés. Nous obtenons automatiquement la liste des messages pour le sujet dans le modèle si nous avons correctement configuré les relations entre les tables, de sorte que seule la question d'affichage reste.

Par analogie avec la liste filtrée des sujets, nous avons presque les mêmes options.

La première consiste à tout faire dans le code du fichier de vue pour afficher le sujet ( vues / sujet / vue.php ):

  1. Créer ActiveDataProvider ;
  2. Remplissez-le avec les données du modèle $ model-> getPosts () ;
  3. Affichez à l'aide d'un widget ListView / GridView standard, en spécifiant manuellement les champs requis.

La seconde consiste à isoler ce code dans un fichier de présentation distinct: views / topic / _posts-list-widget.php , juste pour ne pas être une horreur - le réutiliser quelque part échouera toujours.

Le troisième est un widget à part entière qui dupliquera largement le code du PostController conditionnel dans la partie actionIndex () , mais écrit manuellement.

Ou générez le code automatiquement et appelez le widget fini:

 <?= app\widgets\PostControllerWidget::widget([ 'action' => 'index', 'params' => [ 'query' => $model->getPosts(), ], ]) ?> 

Sous le capot: un générateur gii basé sur CRUD


La tâche métier est définie, les exigences pour le widget généré sont décrites, nous déterminerons exactement comment nous allons le générer. Gii possède déjà un générateur pour le contrôleur CRUD. Pour un widget CRUD, nous devons créer un nouveau générateur basé sur celui existant.

Quelques liens vers la documentation avant de commencer - cela sera également utile si vous décidez d'écrire votre propre extension:


De toute évidence, toutes les fonctionnalités sont regroupées dans l'extension Yii, qui est installée via Composer et pénètre dans le dossier du fournisseur de votre projet.

L'extension se compose de trois parties:

  1. Le répertoire templates / crud contenant le modèle de générateur gii;
  2. Fichier Controller.php - contrôleur de façade intégré pour les appels de widget;
  3. Le fichier Widget.php est la classe de base pour tous les widgets générés.



Modèle de générateur Gii


L'extension doit générer du code, donc sa partie centrale est le générateur Gii.

Initialement, il était supposé que pour implémenter l'extension, il suffirait d'écrire votre propre modèle pour le générateur CRUD-Controller intégré. Par ailleurs, c'est pourquoi le répertoire est appelé modèles, pas générateurs. Mais il s'est avéré que le générateur CRUD-Controller effectue une validation très intensive des données d'entrée, ce qui n'a pas permis de mettre en œuvre de nombreuses exigences, par exemple, de changer la classe d'héritage. Par conséquent, l'extension contient un générateur à part entière, et pas seulement un modèle.

Le générateur gii se compose des parties suivantes (toutes se trouvent dans le répertoire templates / crud):

  • Le répertoire par défaut est un modèle où toute la magie opère: chaque fichier de ce répertoire correspondra à un fichier généré dans votre projet;
  • Fichier form.php - comme vous pouvez le deviner d'après le nom, c'est un formulaire pour entrer les paramètres de génération (noms de classe, etc.);
  • File Generator.php - un orchestre de génération qui reçoit les données du formulaire, les valide, puis appelle séquentiellement les fichiers de modèle pour créer le résultat.

Les fichiers Generator.php et form.php contiennent principalement des modifications cosmétiques par rapport aux modifications d'origine du générateur CRUD: noms de fichiers, validation, descriptions et textes d'invites, etc.

Les fichiers de modèle sont responsables de la vue générée et du code du widget lui-même. Tout d'abord, le fichier templates / crud / default / controller.php est important, il est chargé de générer directement la classe de widget correspondant à la classe de contrôleur à partir du générateur d'origine.

Le widget doit avoir les mêmes actions que le contrôleur CRUD, mais elles sont générées un peu différemment. Les exemples ci-dessous montrent le résultat de la génération avec des commentaires:

  • actionIndex - au lieu d'une sortie inconditionnelle de tous les modèles, la méthode accepte le paramètre $ query;

     public function actionIndex($query) { $dataProvider = new ActiveDataProvider([ 'query' => $query, ]); return $this->render('index', [ 'dataProvider' => $dataProvider, ]); } 
  • actionCreate et actionUpdate - en cas de succès, au lieu d'une redirection, ils renvoient simplement le code de réussite, un traitement supplémentaire est fourni par le contrôleur de façade intégré;

     public function actionCreate() { $model = new Post(); if ($model->load(Yii::$app->request->post()) && $model->save()) { return 'success'; } return $this->render('create', [ 'model' => $model, ]); } 

  • actionDelete - prend en charge la méthode GET pour afficher le widget de suppression (par défaut - un bouton) et POST pour effectuer l'action; en cas de succès, il n'effectue pas non plus de redirection, mais renvoie un code.

     public function actionDelete($id) { $model = $this->findModel($id); if (Yii::$app->request->method == 'GET') { return $this->render('delete', [ 'model' => $model, ]); } else { $model->delete(); return 'success'; } } 

Enfin, les fichiers de vue contiennent les modifications de base suivantes:

  • Tous les en-têtes sont traduits en h2 au lieu de h1;
  • Suppression du code responsable de l'affichage du titre de la page et du fil d'Ariane - le widget ne devrait pas affecter ces choses;
  • La création et l'édition de modèles se fait à l'aide d'une fenêtre modale (widget Modal intégré);
  • Ajout du modèle de widget de suppression - avec un gros bouton rouge.

Classe de base du widget


Lorsque le générateur a terminé son travail, il crée une classe de widgets dans l'espace de noms de l'application. La chaîne d'héritage ressemble à ceci: les widgets générés pour l'application sont hérités du widget d'extension de base, class \ ianikanov \ wce \ Widget , qui, à son tour, est hérité du widget de base Yii, class \ yii \ base \ Widget .

La classe de base du widget d'extension résout les tâches suivantes:

  1. Définit deux champs principaux: $ action et $ params, par lesquels le contrôle est transféré au widget depuis la vue appelante;
  2. Définit un certain nombre de paramètres standard qui peuvent être remplacés dans la classe générée, tels que le chemin d'accès aux fichiers de vue du widget, le nom et le chemin d'accès au contrôleur de façade (à ce sujet ci-dessous) et des messages d'erreur;
  3. Définit les paramètres standard lors du rendu des vues: render et renderFile;
  4. Fournit une infrastructure d'événements similaire à l'infrastructure du contrôleur afin que les filtres standard tels que AccessControl et VerbFilter fonctionnent ;
  5. Définit une méthode d'exécution qui collecte tout cela ensemble.

Contrôleur de façade intégré

L'affichage de ces données ne pose aucun problème - les widgets sont prévus à cet effet. Mais pour l'édition, de toute façon, vous avez besoin d'un contrôleur. Générez un contrôleur unique pour chaque widget - toute son essence est perdue. L'utilisation d'un CRUD standard n'est pas toujours pertinente, et je ne veux pas dépendre du lancement supplémentaire de gii. Par conséquent, l'option a été utilisée avec une façade de contrôleur intégrée universelle.

Ce contrôleur est enregistré dans la carte d'application via le fichier de configuration et contient une seule méthode - actionIndex, qui effectue les actions suivantes:

  1. Accepte une demande d'un client;
  2. Transfère le contrôle à la classe de widget correspondante;
  3. Gère les erreurs professionnelles résultant du widget;
  4. Redirige vers l'application principale.

Il est peut-être plus important d'indiquer ce que ce contrôleur ne fait PAS:

  1. Il ne vérifie pas les niveaux d'accès - cette logique appartient à des widgets spécifiques;
  2. Il n'effectue aucune manipulation d'entrée - les paramètres sont transmis au widget tel quel;
  3. Il ne manipule pas la sortie, sauf pour vérifier un code de réussite prédéfini.

Cette approche vous permet de maintenir la polyvalence de la façade, laissant la mise en œuvre des exigences métier, y compris les exigences de sécurité, le code d'application de l'application.

Démarrage rapide

Le défi commercial est clair, prêt à démarrer? L'utilisation de l'extension comporte quatre étapes:

  1. Installation;
  2. Configuration;
  3. Génération;
  4. Application.

L'installation de l'extension se fait à l'aide de composer:

 php composer.phar require --prefer-dist ianikanov/yii2-wce "dev-master" 

Ensuite, vous devez apporter plusieurs modifications au fichier de configuration de l'application.

Tout d'abord, ajoutez une référence au générateur gii:

 if (YII_ENV_DEV) { $config['modules']['gii'] = [ 'class' => 'yii\gii\Module', 'allowedIPs' => ['127.0.0.1', '::1', '192.168.0.*', '192.168.178.20'], 'generators' => [ //here 'widgetCrud' => [ 'class' => '\ianikanov\wce\templates\crud\Generator', 'templates' => [ 'WCE' => '@vendor/ianikanov/yii2-wce/templates/crud/default', // template name ], ], ], ]; } 

Deuxièmement, ajoutez le contrôleur de façade intégré à la carte:

 $config = [ ... 'controllerMap' => [ 'wce-embed' => '\ianikanov\wce\Controller', ], ... ]; 

Ceci termine l'installation et la configuration.

Pour générer un widget:

  1. Open gii;
  2. Sélectionnez "Widget du contrôleur CRUD";
  3. Remplissez les champs du formulaire;
  4. Affichez et générez du code.

De plus, pour utiliser le widget, il doit être appelé en spécifiant l'action et les paramètres - presque la même chose que le contrôleur est appelé.

Widget pour visualiser la liste des modèles:

 <?= app\widgets\PostControllerWidget::widget([ 'action' => 'index', 'params' => [ 'query' => $otherModel->getPosts(), ], ]) ?> 

Widget pour visualiser un modèle:

 <?= app\widgets\PostControllerWidget::widget(['action' => 'view', 'params' => ['id' => $post_id]]) ?> 

Widget de création de modèle (bouton + formulaire enveloppé dans Modal):

 <?= app\widgets\PostControllerWidget::widget(['action' => 'create']) ?> 

Widget de changement de modèle (bouton + formulaire enveloppé dans Modal):

 <?= app\widgets\PostControllerWidget::widget(['action' => 'update', 'params'=>['id' => $post_id]]) ?> 

Widget de suppression de modèle (bouton):

 <?= app\widgets\PostControllerWidget::widget(['action' => 'delete', 'params'=>['id' => $post_id]]) ?> 

Le code du widget et toutes les vues appartiennent à l'application et peuvent être facilement modifiés - tout est exactement le même que lors de la génération du contrôleur.

A propos du support et du développement


Quelques mots sur la façon dont l'expansion sera soutenue et développée. J'ai le travail principal et certains de mes projets «secondaires» (pet-projects). Donc, cette extension est un projet parallèle à mes projets secondaires, donc je ne développerai des améliorations que pour les besoins de mes projets.

Dans les meilleures traditions de l'open source, le code est disponible sur le github , et je le soutiendrai en termes de correction de bugs, et j'essaierai également de faire des révisions en temps opportun si quelqu'un veut envoyer une demande de pull, donc si vous êtes intéressé, rejoignez-nous.

Source: https://habr.com/ru/post/fr450696/


All Articles