Introduction: pourquoi avons-nous besoin d'une machine d'état
Dans les applications, il est souvent nécessaire de restreindre l'accès à certaines actions sur un objet. Pour cela, des modules RBAC sont utilisés pour résoudre le problème de restriction d'accès en fonction des droits des utilisateurs. La tâche de gestion des actions, selon l'état de l'objet, n'est toujours pas résolue. Ce problème est bien résolu en utilisant une machine d'état ou une machine d'état. Une machine à états pratique vous permet non seulement de rassembler en un seul endroit toutes les règles de transition entre les états de l'objet, mais aussi de mettre de l'ordre dans le code en séparant les règles de transition, en vérifiant les conditions et la manipulation et en les soumettant à des règles générales.
Je veux partager l'implémentation de la machine d'état pour Zend Framework 3 en utilisant Doctrine 2
pour travailler avec la base de données. Le projet lui-même peut être trouvé ici .
Et ici, je veux partager les principes de base énoncés.
Commençons
Nous allons stocker la description du graphe de transition dans la table de base de données car:
- C'est clair.
- Permet d'utiliser le même dictionnaire d'état que celui utilisé dans celui qui vous intéresse
nous un objet ayant un état. - Vous permet de garantir l'intégrité de la base de données à l'aide de clés étrangères.
L'utilisation d'une machine à états finis non déterministe augmentera la flexibilité de notre solution.
Le graphe de transition sera décrit à l'aide d'une paire de tableaux A et B, reliés par une relation un-à-plusieurs.
Tableau a:
CREATE TABLE `tr_a` ( `id` int(11) NOT NULL AUTO_INCREMENT, `src_id` varchar(32) COLLATE utf8_unicode_ci NOT NULL, `action_id` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT ' ', `condition` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT ' ', PRIMARY KEY (`id`), KEY `IDX_96B84B3BFF529AC` (`src_id`), KEY `IDX_96B84B3B9D32F035` (`action_id`), CONSTRAINT `FK_96B84B3B9D32F035` FOREIGN KEY (`action_id`) REFERENCES `action` (`id`), CONSTRAINT `FK_96B84B3BFF529AC` FOREIGN KEY (`src_id`) REFERENCES `state` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Tableau B:
CREATE TABLE `tr_b` ( `id` int(11) NOT NULL AUTO_INCREMENT, `transition_a_id` int(11) NOT NULL, `dst_id` varchar(32) COLLATE utf8_unicode_ci NOT NULL, `weight` int(11) DEFAULT NULL COMMENT ' ,- , null- ', `condition` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT ' ', `pre_functor` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT ' , , ', `post_functor` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT ' , , ', PRIMARY KEY (`id`), KEY `IDX_E12699CB85F4C374` (`transition_a_id`), KEY `IDX_E12699CBE1885D19` (`dst_id`), CONSTRAINT `FK_E12699CB85F4C374` FOREIGN KEY (`transition_a_id`) REFERENCES `tr_a` (`id`), CONSTRAINT `FK_E12699CBE1885D19` FOREIGN KEY (`dst_id`) REFERENCES `state` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Je vais omettre la définition des classes d'entités; vous pouvez voir un exemple ici:
- table a
- tableau B
Tout est standard ici, y compris une description de la relation entre les entités.
Pour utiliser la machine d'état, nous n'avons besoin que de quelques méthodes publiques
public function doAction($objE, $action, array $data = []) /** * * @param object $objE * @param string $action * @param array $data * @return bool */ public function hasAction($objE, $action, $data=[])
Il existe plusieurs autres méthodes publiques pour une utilisation confortable, mais ici, je voudrais faire attention à l'algorithme de la doAction()
principale doAction()
.
De l'objet on obtient son état.
Le connaissant et l'identifiant de l'action, l'entité-A est facilement trouvée dans la table de transition A.
La condition obtenue par l'identificateur de condition, qui se trouve dans la condition
de l'entité-A, vous permet de vérifier si l'action peut être effectuée. En particulier, la clause RBAC mentionnée au début peut être utilisée dans le validateur de condition.
Le validateur sera trouvé par l'identifiant du champ de condition
via le ValidatorPluginManager
et
devrait implémenter \Zend\Validator\ValidatorInterface
. Je préfère utiliser les héritiers de ValidatorChain
. Cela facilite la modification de la composition des conditions contrôlées et la réutilisation de validateurs simples dans le cadre de chaînes de test.
Nous avons déterminé la transition, vérifié l'état. Puisque nous avons un non-déterministe
machine d'état , le résultat de l'action peut être l'un de plusieurs états.
De tels cas ne sont pas très courants, mais le projet proposé est facile à mettre en œuvre.
Par la connexion A - <B, nous obtenons une collection de nouveaux états possibles de l'objet (entité-B).
Pour sélectionner un seul état, nous vérifions tour à tour les conditions de l'entité-B de la collection résultante, en les triant par weight
de plus grand à plus petit dans le champ de weight
. La première vérification réussie de la condition nous donne l'Entité-B, dans laquelle il y a un nouvel état de l'objet (voir le champ dst_id
).
Une nouvelle condition est définie Maintenant, avant de changer l'état, la machine d'état s'exécutera
actions définies dans le préfet, puis il va changer d'état et effectuer des actions,
défini dans la post-fonction. La machine d'état obtiendra des foncteurs basés sur le nom du champ pre_functor
pour la préfonction et post_functor
pour la fonction post à l'aide du gestionnaire de plug-ins et appellera la méthode __invoke () pour les objets reçus.
Il n'est pas nécessaire de changer d'état à l'aide de foncteurs. C'est la tâche de la machine d'état. S'il n'est pas nécessaire d'effectuer des actions supplémentaires lors du changement d'état, définissez null sur les champs ci-dessus.
Autres puces:
- Dans les champs de la
condition
tables de transition, pre_funtor
, post_functor
j'utilise des alias, à mon avis c'est pratique. - Pour plus de commodité visuelle, créez une vue à partir des tableaux A et B.
- J'utilise des identificateurs de chaîne comme clé primaire dans les dictionnaires d'état et d'action. Ce n'est pas nécessaire, mais pratique. Des dictionnaires avec des identificateurs numériques peuvent également être utilisés.
- Puisqu'une machine à états finis non déterministe est utilisée, l'action n'a pas à conduire à un changement d'état. Cela vous permet de décrire des actions telles que la visualisation, par exemple.
- En plus des méthodes de vérification d'une action et d'exécution d'une action, il existe un certain nombre de méthodes publiques qui permettent, par exemple, d'obtenir une liste d'actions pour un état donné d'un objet ou une liste d'actions disponibles pour un état donné d'un objet, en tenant compte des vérifications. Souvent, dans l'interface des grilles pour chaque enregistrement, vous devez afficher un ensemble d'actions. Ces méthodes de machines d'état aideront à obtenir la liste nécessaire.
- Bien sûr, d'autres machines d'état peuvent être appelées à l'intérieur de foncteurs, de plus, vous pouvez vous appeler vous-même, mais avec un objet différent ou avec le même objet, mais après un changement d'état (c'est-à-dire dans un post-foncteur). Ceci est parfois utile pour organiser des transitions en cascade dans des conditions «soudaines» changeantes du client;)
Conclusion
Malgré les nombreuses tâches qui conviennent parfaitement à l'utilisation de machines d'état, les programmeurs Web les utilisent relativement rarement. Les mêmes décisions que j'ai vues me semblaient monstrueuses.
J'espère que la solution proposée aidera quelqu'un à gagner du temps sur la mise en œuvre.