Implementação de máquinas de estado no Zend Framework3 + Doctine2

Introdução: por que precisamos de uma máquina de estado


Nos aplicativos, geralmente é necessário restringir o acesso a determinadas ações em um objeto. Para isso, são utilizados módulos RBAC que resolvem o problema de restringir o acesso, dependendo dos direitos do usuário. A tarefa de gerenciar ações, dependendo do estado do objeto, permanece sem solução. Esse problema é bem resolvido usando uma máquina de estado ou máquina de estado. Uma máquina de estados conveniente permite não apenas coletar em um só lugar todas as regras de transições entre os estados do objeto, mas também colocar alguma ordem no código, separando as regras de transições, verificando condições e manipulação e sujeitando-as a regras gerais.


Quero compartilhar a implementação da máquina de estado para o Zend Framework 3 usando o Doctrine 2
para trabalhar com o banco de dados. O projeto em si pode ser encontrado aqui .


E aqui quero compartilhar os princípios básicos estabelecidos.


Vamos começar




Armazenaremos a descrição do gráfico de transição na tabela do banco de dados porque:


  1. Isto está claro.
  2. Permite usar o mesmo dicionário de estado usado no de interesse
    nós um objeto tendo um estado.
  3. Permite garantir a integridade do banco de dados usando chaves estrangeiras.

O uso de uma máquina de estados finitos não determinísticos aumentará a flexibilidade de nossa solução.


O gráfico de transição será descrito usando um par de tabelas A e B, conectadas por um relacionamento um para muitos.


Tabela 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 

Quadro 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 

Omitirei a definição de classes de entidade; você pode ver um exemplo aqui:


  1. mesa a
  2. tabela B

Tudo é padrão aqui, incluindo uma descrição do relacionamento entre entidades.


Para usar a máquina de estado, precisamos apenas de alguns métodos públicos


 /** *            * @param object $objE * @param string $action * @param array $data extra data * @return array * @throws ExceptionNS\StateMachineException */ public function doAction($objE, $action, array $data = []) /** *          * @param object $objE * @param string $action * @param array $data * @return bool */ public function hasAction($objE, $action, $data=[]) 

Existem vários métodos públicos para uso confortável, mas aqui eu gostaria de prestar atenção ao algoritmo do método principal doAction() .


Do objeto, obtemos seu estado.


Conhecendo o identificador de ação e a entidade, a entidade A é facilmente encontrada na tabela de transição A.
A condição obtida pelo identificador da condição, que está na condition da entidade A, permite verificar se a ação pode ser executada. Em particular, a cláusula RBAC mencionada no início pode ser usada no validador de condição.


O validador será encontrado pelo identificador no campo de condition por meio do ValidatorPluginManager e
deve implementar \Zend\Validator\ValidatorInterface . Eu prefiro usar os herdeiros do ValidatorChain . Isso facilita a alteração da composição de condições controladas e a reutilização de validadores simples como parte das cadeias de teste.


Determinamos a transição, verificamos a condição. Desde que nós temos um não-determinista
máquina de estado , o resultado da ação pode ser um dos vários estados.


Tais casos não são muito comuns, mas o projeto proposto é fácil de implementar.
Pela conexão A - <B, obtemos uma coleção de possíveis novos estados do objeto (entidade-B).
Para selecionar um único estado, verificamos por sua vez as condições da Entidade-B da coleção resultante, classificando-as por weight de maior a menor, no campo de weight . A primeira verificação bem-sucedida da condição nos fornece a Entidade-B, na qual há um novo estado do objeto (consulte o campo dst_id ).


O novo estado é determinado. Agora, antes de alterar o estado, a máquina de estados executará
ações definidas no prefunctor, ele mudará de estado e executará ações,
definido na pós-função. A máquina de estado obterá functors com base no nome do campo pre_functor para a prefunction e post_functor para a função post usando o gerenciador de plug-in e chamará o método __invoke () para os objetos recebidos.


Não é necessário alterar o estado usando functors. Essa é a tarefa da máquina de estado. Se não for necessário executar ações adicionais ao alterar o estado, defina nulo nos campos acima.


Outros chips:


  1. Nos campos da condition tabelas de transição, pre_funtor , post_functor eu uso aliases, na minha opinião, é conveniente.
  2. Para conveniência visual, crie uma visualização das tabelas A e B.
  3. Eu uso identificadores de string como a chave primária nos dicionários de estado e ação. Isso não é necessário, mas conveniente. Dicionários com identificadores numéricos também podem ser usados.
  4. Como uma máquina de estados finitos não determinísticos é usada, a ação não precisa levar a uma alteração no status. Isso permite que você descreva ações como a visualização, por exemplo.
  5. Além dos métodos para verificar e executar uma ação, existem vários métodos públicos que permitem, por exemplo, obter uma lista de ações para um determinado estado de um objeto ou uma lista de ações disponíveis para um determinado estado de um objeto, levando em consideração as verificações. Freqüentemente, na interface em grades para cada registro, você precisa mostrar um conjunto de ações. Esses métodos de máquinas de estado ajudarão a obter a lista necessária.
  6. Obviamente, outras máquinas de estado podem ser chamadas dentro de functores; além disso, você pode se chamar, mas com um objeto diferente ou com o mesmo objeto, mas após uma mudança de estado (ou seja, em um pós-functor). Às vezes, isso é útil para organizar transições em cascata sob alterações "repentinas" do cliente;)

Conclusão


Apesar das muitas tarefas ideais para o uso de máquinas de estado, os programadores da Web as utilizam relativamente raramente. As mesmas decisões que vi me pareceram monstruosas.


Espero que a solução proposta ajude alguém a economizar tempo na implementação.

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


All Articles