$workflows * @return array */ public static function edgesOf(iterable $workflows): array { $edges = []; foreach ($workflows as $workflow) { $edges[(int)$workflow->getId()] = [(int)$workflow->getSourceStackId(), (int)$workflow->getTargetStackId()]; } return $edges; } /** * The workflows leading from stack $from to stack $to, in order, or * `null` if there is no way. `[]` if $from already is $to. * * @param array $edges workflow id => [source stack id, target stack id] * @return list|null */ public static function findPath(array $edges, int $from, int $to): ?array { if ($from === $to) { return []; } // Breadth-first, remembering how each stack was reached. $reachedBy = [$from => null]; $queue = [$from]; while ($queue !== []) { $stack = array_shift($queue); foreach ($edges as $id => [$source, $target]) { if ($source !== $stack || array_key_exists($target, $reachedBy)) { continue; } $reachedBy[$target] = [$id, $stack]; if ($target === $to) { $path = []; for ($at = $to; $reachedBy[$at] !== null; $at = $reachedBy[$at][1]) { array_unshift($path, $reachedBy[$at][0]); } return $path; } $queue[] = $target; } } return null; } /** * Ids of the workflows that lie on a loop: those whose target leads back * to their own source. * * @param array $edges workflow id => [source stack id, target stack id] * @return list */ public static function findLoopingWorkflows(array $edges): array { $looping = []; foreach ($edges as $id => [$source, $target]) { if (self::findPath($edges, $target, $source) !== null) { $looping[] = $id; } } return $looping; } }