Example #1
0
 public static function postprocessDefinition(&$machine_def)
 {
     if (!isset($machine_def['bpmn_fragments'])) {
         return;
     }
     $bpmn_fragments = $machine_def['bpmn_fragments'];
     unset($machine_def['bpmn_fragments']);
     $states = [];
     $actions = [];
     // Each included BPMN file provided one fragment
     foreach ($bpmn_fragments as $fragment_file => $fragment) {
         $primary_process_id = $fragment['process_id'];
         $state_machine_participant_id = $fragment['state_machine_participant_id'];
         $prefix = "bpmn_" . (0xffff & crc32($fragment_file)) . '_';
         $diagram = "\tsubgraph cluster_{$prefix} {\n\t\tlabel= \"BPMN:\\n" . basename($fragment_file) . "\"; color=\"#5373B4\";\n\n";
         // Calculate neighbour nodes
         $next_node = [];
         foreach ($fragment['arrows'] as $id => $a) {
             if ($a['type'] == 'sequenceFlow') {
                 $next_node[$a['source']][] = $a['target'];
             }
         }
         // Collect start nodes
         $start_nodes = [];
         $end_nodes = [];
         foreach ($fragment['nodes'] as $id => $n) {
             if ($n['type'] == 'startEvent') {
                 $start_nodes[] = $id;
                 $fragment['nodes'][$id]['_distance'] = 0;
             } else {
                 // Mark all other nodes as unreachable (but define the distance)
                 $fragment['nodes'][$id]['_distance'] = null;
             }
         }
         // Calculate distance of each node from nearest start event to detect backward arrows (DFS)
         $queue = $start_nodes;
         while (!empty($queue)) {
             $id = array_pop($queue);
             $distance = $fragment['nodes'][$id]['_distance'] + 1;
             if (isset($next_node[$id])) {
                 foreach ($next_node[$id] as $next_id) {
                     $n = $fragment['nodes'][$next_id];
                     if (!isset($n['_distance'])) {
                         $fragment['nodes'][$next_id]['_distance'] = $distance;
                         $queue[] = $next_id;
                     }
                 }
             }
         }
         /*
          * State machine synthesis
          */
         // Find nodes connected to state machine participant
         $state_machine_participant_id = $fragment['state_machine_participant_id'];
         $invoking_actions = [];
         $receiving_nodes = [];
         $starting_nodes = [];
         $ending_nodes = [];
         foreach ($fragment['arrows'] as $a_id => $a) {
             if ($a['target'] == $state_machine_participant_id) {
                 $invoking_actions[$a['source']] = $a;
                 //$fragment['nodes'][$a['source']];
             }
             if ($a['source'] == $state_machine_participant_id) {
                 $receiving_nodes[$a['target']] = $fragment['nodes'][$a['target']];
             }
         }
         // Find start & end nodes
         foreach ($fragment['nodes'] as $n_id => $n) {
             if ($n['type'] == 'startEvent') {
                 $starting_nodes[$n_id] = $n;
             } else {
                 if ($n['type'] == 'endEvent') {
                     $ending_nodes[$n_id] = $n;
                 }
             }
         }
         // Find receiving nodes for each invoking node
         // (DFS to next task, the receiver cannot be further than that)
         $inv_rc_nodes = [];
         foreach ($invoking_actions as $in_id => $invoking_action) {
             $queue = [$in_id];
             $inv_rc_nodes[$in_id] = [];
             $in = $fragment['nodes'][$in_id];
             $cur_process = $in['process'];
             while (!empty($queue)) {
                 $id = array_pop($queue);
                 if (isset($next_node[$id])) {
                     foreach ($next_node[$id] as $next_id) {
                         $n = $fragment['nodes'][$next_id];
                         if ($n['process'] != $cur_process || isset($invoking_actions[$next_id])) {
                             // The receiving node must be within the same process and it must not be invoking node.
                             continue;
                         }
                         if (isset($receiving_nodes[$next_id])) {
                             $inv_rc_nodes[$in_id][$next_id] = $n;
                         } else {
                             if (!isset($seen[$next_id])) {
                                 $queue[] = $next_id;
                                 $seen[$next_id] = true;
                             }
                         }
                     }
                 }
             }
             if (empty($inv_rc_nodes[$in_id])) {
                 $inv_rc_nodes[$in_id][$in_id] = $in;
                 $receiving_nodes[$in_id] = $in;
             }
         }
         // Find connections to next transition invocations
         $sm_next_node = [];
         $seen = [];
         foreach (array_merge($starting_nodes, $receiving_nodes) as $in_id => $in) {
             $queue = [$in_id];
             while (!empty($queue)) {
                 $id = array_pop($queue);
                 if (isset($next_node[$id])) {
                     foreach ($next_node[$id] as $next_id) {
                         if (isset($invoking_actions[$next_id]) || isset($receiving_nodes[$next_id]) || isset($ending_nodes[$next_id])) {
                             $sm_next_node[$in_id][] = $next_id;
                         } else {
                             if (!isset($seen[$next_id])) {
                                 $queue[] = $next_id;
                                 $seen[$next_id] = true;
                             }
                         }
                     }
                 }
             }
         }
         $action_no = 1;
         $eq_states = [];
         $eq_start_states = [];
         $eq_end_states = [];
         // Add initial states
         foreach ($starting_nodes as $s_id => $s_n) {
             $q = 'Qs_' . $s_id;
             $states[$q] = [];
             // Connect to initial state (for now)
             $eq_start_states[] = $q;
         }
         // Build isolated fragments of the state machine (blue arrows; invoking--receiving groups)
         // Part 1/2: States
         foreach ($invoking_actions as $in_id => $in_a) {
             $qi = 'Qi_' . $in_id;
             $states[$qi] = [];
             foreach ($inv_rc_nodes[$in_id] as $rcv_id => $rcv_n) {
                 $qr = 'Qr_' . $rcv_id;
                 $states[$qr] = [];
             }
         }
         // Add final states
         foreach ($ending_nodes as $e_id => $e_n) {
             $q = 'Qe_' . $e_id;
             $states[$q] = [];
             // Connect to initial state
             $eq_end_states[] = $q;
         }
         // Connect fragments of the state machine (green arrows)
         foreach ($sm_next_node as $src => $dst_list) {
             $qr = isset($starting_nodes[$src]) ? 'Qs_' . $src : 'Qr_' . $src;
             foreach ($dst_list as $dst) {
                 $qi = isset($ending_nodes[$dst]) ? 'Qe_' . $dst : 'Qi_' . $dst;
                 $eq_states[$qr][] = $qi;
             }
         }
         // Build replacement table
         $uf = new UnionFind();
         $uf->add('');
         foreach ($states as $s_id => $s) {
             $uf->add($s_id);
         }
         foreach ($starting_nodes as $s_id => $s_n) {
             $uf->union('', 'Qs_' . $s_id);
         }
         foreach ($eq_states as $src => $dst_list) {
             foreach ($dst_list as $dst) {
                 $uf->union($src, $dst);
             }
         }
         foreach ($ending_nodes as $e_id => $e_n) {
             $uf->union('', 'Qe_' . $e_id);
         }
         $state_replace = $uf->findAll();
         /*
         			echo "<pre>";
         			foreach ($state_replace as $src => $dst) {
         				if ($src == $dst) {
         					echo "<span style=\"color:grey\">$src == $dst</span>\n";
         				} else {
         					echo "$src == $dst\n";
         				}
         			}
         			echo "</pre>";
         			// */
         // Build isolated fragments of the state machine (blue arrows; invoking--receiving groups)
         // Part 2/2: Actions
         foreach ($invoking_actions as $in_id => $in_a) {
             $qi = 'Qi_' . $in_id;
             foreach ($inv_rc_nodes[$in_id] as $rcv_id => $rcv_n) {
                 $qr = 'Qr_' . $rcv_id;
                 $inv_a = $invoking_actions[$in_id];
                 $a = $inv_a['name'] ?: 'A' . $action_no++;
                 $qir = $state_replace[$qi];
                 $qrr = $state_replace[$qr];
                 $actions[$a]['transitions'][$qir]['targets'][] = $qrr;
             }
         }
         // Remove merged states
         foreach ($state_replace as $src => $dst) {
             if ($src !== $dst) {
                 unset($states[$src]);
             }
         }
         /*
          * Render
          */
         // Draw arrows
         foreach ($fragment['arrows'] as $id => $a) {
             $source = AbstractMachine::exportDotIdentifier($a['source'], $prefix);
             $target = AbstractMachine::exportDotIdentifier($a['target'], $prefix);
             $backwards = $fragment['nodes'][$a['source']]['_distance'] >= $fragment['nodes'][$a['target']]['_distance'] && $fragment['nodes'][$a['target']]['_distance'];
             $diagram .= "\t\t" . $source . ' -> ' . $target . ' [tooltip="' . addcslashes($a['id'], '"') . '"';
             $diagram .= ",label=\"" . addcslashes($a['name'], '"') . "\"";
             if ($backwards) {
                 $diagram .= ',constraint=0';
             }
             switch ($a['type']) {
                 case 'sequenceFlow':
                     $diagram .= 'style=solid,color="#666666"';
                     $w = $backwards ? 3 : 5;
                     break;
                 case 'messageFlow':
                     $diagram .= 'style=dashed,color="#666666",arrowhead=empty,arrowtail=odot';
                     $w = 0;
                     break;
                 default:
                     $diagram .= 'color=red';
                     break;
             }
             if ($primary_process_id && $a['process'] == $primary_process_id) {
                 $diagram .= ',color="#44aa44",penwidth=1.5';
             }
             $diagram .= ",weight={$w}];\n";
             $nodes[$source] = $a['source'];
             $nodes[$target] = $a['target'];
         }
         $diagram .= "\n";
         // Draw nodes
         foreach ($fragment['nodes'] as $id => $n) {
             $graph_id = AbstractMachine::exportDotIdentifier($id, $prefix);
             $diagram .= "\t\t" . $graph_id . " [tooltip=\"" . addcslashes($n['id'], '"') . "\"";
             switch ($n['type']) {
                 case 'startEvent':
                 case 'endEvent':
                 case 'intermediateCatchEvent':
                 case 'intermediateThrowEvent':
                     //$diagram .= ",xlabel=\"".addcslashes($n['name'], '"')."\"";
                     break;
                 default:
                     $diagram .= ",label=\"" . addcslashes($n['name'], '"') . "\"";
                     break;
             }
             switch ($n['type']) {
                 case 'participant':
                     $diagram .= ',shape=box,style=filled,fillcolor="#ffffff",penwidth=2';
                     break;
                 case 'startEvent':
                     $diagram .= ',shape=circle,width=0.4,height=0.4,label="",root=1';
                     break;
                 case 'intermediateCatchEvent':
                     $diagram .= ',shape=doublecircle,width=0.35,label=""';
                     break;
                 case 'intermediateThrowEvent':
                     $diagram .= ',shape=doublecircle,width=0.35,label=""';
                     break;
                 case 'endEvent':
                     $diagram .= ',shape=circle,width=0.4,height=0.4,penwidth=3,label=""';
                     break;
                 case 'exclusiveGateway':
                     $diagram .= ',shape=diamond,style=filled,height=0.5,width=0.5,label="X"';
                     break;
                 case 'parallelGateway':
                     $diagram .= ',shape=diamond,style=filled,height=0.5,width=0.5,label="+"';
                     break;
                 case 'inclusiveGateway':
                     $diagram .= ',shape=diamond,style=filled,height=0.5,width=0.5,label="O"';
                     break;
                 case 'complexGateway':
                     $diagram .= ',shape=diamond,style=filled,height=0.5,width=0.5,label="*"';
                     break;
                 case 'eventBasedGateway':
                     $diagram .= ',shape=diamond,style=filled,height=0.5,width=0.5,label="E"';
                     break;
             }
             // Algorithm-specific nodes
             if ($primary_process_id && $n['process'] == $primary_process_id || $state_machine_participant_id && $n['id'] == $state_machine_participant_id) {
                 $diagram .= ',color="#44aa44",fillcolor="#eeffdd"';
             }
             if (isset($starting_nodes[$id]) || isset($ending_nodes[$id])) {
                 $diagram .= ',fillcolor="#ffccaa"';
             }
             // Receiving/invoking background
             if (isset($invoking_actions[$id]) && isset($receiving_nodes[$id])) {
                 $diagram .= ',fillcolor="#ffff88;0.5:#aaddff",gradientangle=270';
             } else {
                 if (isset($invoking_actions[$id])) {
                     $diagram .= ',fillcolor="#ffff88"';
                 } else {
                     if (isset($receiving_nodes[$id])) {
                         $diagram .= ',fillcolor="#aaddff"';
                     }
                 }
             }
             $diagram .= "];\n";
         }
         // Draw groups
         foreach ($fragment['groups'] as $id => $g) {
             $graph_id = AbstractMachine::exportDotIdentifier($id, $prefix);
             $diagram .= "\n\t\tsubgraph cluster_{$graph_id} {\n\t\t\tlabel= \"" . basename($g['name']) . "\"; color=\"#aaaaaa\";\n\n";
             foreach ($g['nodes'] as $n_id) {
                 $graph_n_id = AbstractMachine::exportDotIdentifier($n_id, $prefix);
                 $diagram .= "\t\t\t" . $graph_n_id . ";\n";
             }
             $diagram .= "\t\t}\n";
         }
         //-------------------------------------------
         // Draw $sm_next_node
         foreach ($sm_next_node as $src => $dst_list) {
             foreach ($dst_list as $dst) {
                 $source = AbstractMachine::exportDotIdentifier($src, $prefix);
                 $target = AbstractMachine::exportDotIdentifier($dst, $prefix);
                 $diagram .= "\t\t" . $source . ' -> ' . $target . ' [constraint=0,splines=line,penwidth=5,color="#88dd6688"' . "]\n";
             }
         }
         // Draw $inv_rc_nodes
         foreach ($inv_rc_nodes as $src => $dst_list) {
             foreach ($dst_list as $dst_node) {
                 $dst = $dst_node['id'];
                 $source = AbstractMachine::exportDotIdentifier($src, $prefix);
                 $target = AbstractMachine::exportDotIdentifier($dst, $prefix);
                 $diagram .= "\t\t" . $source . ' -> ' . $target . ' [constraint=0,splines=line,penwidth=5,color="#66aaff88"' . "]\n";
             }
         }
         // Draw $eq_states
         /*
         			foreach ($eq_start_states as $q) {
         				$actions['=']['transitions']['']['targets'][] = $q;
         				$actions['=']['transitions']['']['color'] = '#88dd66';
         			}
         			foreach ($eq_states as $src => $dst_list) {
         				foreach($dst_list as $dst) {
         					$actions['=']['transitions'][$src]['targets'][] = $dst;
         					$actions['=']['transitions'][$src]['color'] = '#88dd66';
         				}
         			}
         			foreach ($eq_end_states as $q) {
         				$actions['=']['transitions'][$q]['targets'][] = '';
         				$actions['=']['transitions'][$q]['color'] = '#88dd66';
         			}
         			// */
         //-------------------------------------------
         // Walk from each task to next tasks, collecting state machine actions
         $paths = [];
         foreach ($fragment['nodes'] as $n_id => $n) {
             // If node is task and it is from the primary process
             if (($n['type'] == 'task' || $n['type'] == 'startEvent' || $n['type'] == 'endEvent') && $n['process'] == $primary_process_id) {
                 // Add task to paths, so we know about all tasks
                 if (!isset($paths[$n_id])) {
                     $paths[$n_id] = [];
                 }
                 // Find all next tasks (DFS limited to non-task nodes)
                 $queue = [$n_id];
                 $visited = [$n_id => true];
                 while (!empty($queue)) {
                     $id = array_pop($queue);
                     if (isset($next_node[$id])) {
                         foreach ($next_node[$id] as $next_id) {
                             $next_n = $fragment['nodes'][$next_id];
                             if ($next_n['process'] == $primary_process_id) {
                                 if ($next_n['type'] == 'task' || $next_n['type'] == 'endEvent') {
                                     $paths[$n_id][] = $next_id;
                                 } else {
                                     if (empty($visited[$next_id])) {
                                         $visited[$next_id] = true;
                                         $queue[] = $next_id;
                                     }
                                 }
                             }
                         }
                     }
                 }
             }
         }
         // To invoke an action, we have to be in a state, so lets add a state in front of each action
         $preceding_states = [];
         foreach ($paths as $src => $dst_list) {
             $n = $fragment['nodes'][$src];
             if ($n['type'] == 'task') {
                 $preceding_states[$src] = 'before_' . $n['name'];
             }
             if ($n['type'] == 'endEvent') {
                 $preceding_states[$src] = '';
             }
         }
         // But if there is path between start event and action,
         // then action's preceding state is the start event.
         foreach ($paths as $src => $dst_list) {
             $n = $fragment['nodes'][$src];
             if ($n['type'] == 'startEvent') {
                 foreach ($dst_list as $dst) {
                     $preceding_states[$dst] = '';
                 }
             }
         }
         // Register preceding states
         foreach ($preceding_states as $s) {
             if ($s != '') {
                 $states[$s] = [];
             }
         }
         // So the actions start in the newly assigned states...
         foreach ($paths as $src => $dst_list) {
             if (!isset($preceding_states[$src])) {
                 continue;
             }
             $src_state = $preceding_states[$src];
             $src_n = $fragment['nodes'][$src];
             $action_name = $src_n['name'];
             foreach ($dst_list as $dst) {
                 $dst_state = $preceding_states[$dst];
                 $actions[$action_name]['transitions'][$src_state]['targets'][] = $dst_state;
             }
         }
         //var_dump($actions);
         // Draw found paths
         $diagram .= "\tsubgraph cluster_" . $prefix . "_sm {\n\t\tlabel= \"Action paths\"; color=\"#44aa44\"; fillcolor=\"#ffffee\"; style=filled;\n\n";
         foreach ($paths as $src => $dst_list) {
             // Draw the action
             $n = $fragment['nodes'][$src];
             $graph_src = AbstractMachine::exportDotIdentifier($src, $prefix . '_action');
             $diagram .= $graph_src . "[label=\"" . addcslashes($n['name'], '"') . "\"];\n";
             // Draw all follow-up actions
             foreach ($dst_list as $dst) {
                 $source = AbstractMachine::exportDotIdentifier($src, $prefix . '_action');
                 $target = AbstractMachine::exportDotIdentifier($dst, $prefix . '_action');
                 $diagram .= "\t\t" . $source . ' -> ' . $target . ";\n";
             }
         }
         $diagram .= "\t}\n";
         //echo "<pre>", $diagram, "</pre>";
         $diagram .= "\t}\n";
         // Add BPMN diagram to state diagram
         $machine_def['state_diagram_extras'][] = $diagram;
     }
     // Update the definition
     $machine_def = array_replace_recursive(['states' => $states, 'actions' => $actions], $machine_def);
 }
Example #2
0
 /**
  * Define state machine using $machine_definition.
  */
 public function initializeMachine($args)
 {
     parent::initializeMachine($args);
 }
Example #3
0
 /**
  * Invoke state machine transition. State machine is not instance of
  * this class, but it is represented by record in database.
  *
  * If transition creates a transaction and throws an exception, the
  * transaction will be rolled back automatically before re-throwing
  * the exception.
  */
 public function invokeTransition(Reference $ref, $transition_name, $args, &$returns, callable $new_id_callback = null)
 {
     $transaction_before_transition = $this->flupdo->inTransaction();
     try {
         return parent::invokeTransition($ref, $transition_name, $args, $returns, $new_id_callback);
     } catch (\Exception $ex) {
         if (!$transaction_before_transition && $this->flupdo->inTransaction()) {
             $this->flupdo->rollback();
         }
         throw $ex;
     }
 }