Implementing a state machine on Zend Framework 3 + Doctine2

Introduction: Why do we need a state machine


In applications, it is often necessary to restrict access to certain actions on an object. To do this, use RBAC-modules that solve the problem of restricting access, depending on user rights. The task of managing actions depending on the state of the object remains unsolved. This problem is well solved with the help of a finite state machine or a state machine. A convenient state machine allows not only to collect in one place all the rules for transitions between the states of an object, but also imposes some order in the code separating the rules of transitions, checking conditions and handlers and subordinating them to general rules.


I want to share implementation of a steytmashina under Zend Framework 3 with use of Doctrine 2
to work with the database. The project itself can be found at the link .


And here I want to share the basic principles laid down.


Let's get started




The description of the transition graph will be stored in the database table because:


  1. This is clearly.
  2. Allows you to use the same state dictionary that is used in the interest
    us object having states.
  3. Allows you to guarantee the integrity of the database using foreign keys.

Using a non-deterministic finite state machine will increase the flexibility of our solution.


The transition graph will be described using a pair of tables A and B, interconnected by a one-to-many relationship.


Table 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 

Table 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 

I will omit the definition of the classes of an entity; see an example here:


  1. table A
  2. table B

Everything is standard here, including a description of the relationship between entities.


To use the state machine we need only a few public methods.


 /** *            * @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=[]) 

For comfortable use, there are several public methods, but here I would like to pay attention to the algorithm of the main doAction() method.


From the object we get its state.


Knowing him and the action identifier, Entiti-A is easily found in transition table A.
The condition obtained by the condition identifier, which lies in the condition of entiti-A, allows you to check the ability to perform an action. In particular, the RBAC mentioned at the beginning of the article can be used in the condition validator.


The validator will be found by the identifier from the condition field through the ValidatorPluginManager and
should implement \Zend\Validator\ValidatorInterface . I prefer to use heirs from ValidatorChain . This makes it easy to change the composition of controlled conditions and reuse simple validators as part of test chains.


Determined with the transition, the condition checked. Since we have nondeterministic
state machine , the result of the action may be one of several states.


Such cases are not very common, but the proposed project is easy to implement.
By linking A - <B, we get a collection of possible new states of the object (Entity-B).
To select the only state, we check the conditions of the entity B from the resulting collection in turn, sorting them by weight from larger to smaller. The first successful condition check gives us the B entity, in which there is a new object state (see the dst_id field).


The new state is defined. Now before changing the state, the state machine will execute
the actions defined in the prefunctor will then change the state and perform the actions
defined in the postfunctor. The state machine will retrieve the functors based on the name from the pre_functor field for the prefunctor and the post_functor for the post_functor using the plugin manager and call the __invoke () method for the received objects.


Changing the state with the help of functors is not necessary. This is the task of the state machine. If there is no need to perform additional actions when changing the state, then in the above fields set to null.


Other chips:


  1. I use aliases in the fields of the condition , pre_funtor , post_functor conversion tables, in my opinion this is convenient.
  2. For visual convenience, create a view from tables A and B.
  3. I use string identifiers as the primary key in the dictionaries of states and actions. It is not necessary, but convenient. Dictionaries with numeric identifiers can also be used.
  4. Since a non-deterministic finite-state machine is used, the action does not have to lead to a change of status. This allows you to describe such actions as viewing, for example.
  5. In addition to the methods of checking the action and performing the action, there are also a number of public methods that allow you to get, for example, a list of actions for a given object state or a list of available actions for a given state of an object, taking into account checks. Often, in the interface in the grids, each entry needs to show a set of actions. These methods of state machine will help to get the necessary list.
  6. Of course, other state machines can be called inside functors, moreover, you can call yourself, but with a different object or with the same object, but after a change of state (that is, in a postfunctor). This is sometimes useful for organizing cascade transitions under changed "suddenly" conditions from the customer;)

Conclusion


Despite the many tasks that are ideally suited for the use of state machines, web programmers use them relatively rarely. The same solutions that I saw seemed monster to me.


I hope that the proposed solution will help someone save time on implementation.

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


All Articles