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); }
/** * Define state machine using $machine_definition. */ public function initializeMachine($args) { parent::initializeMachine($args); }
/** * 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; } }