Skip to content

Instantly share code, notes, and snippets.

@lt
Last active September 19, 2019 15:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lt/5805875 to your computer and use it in GitHub Desktop.
Save lt/5805875 to your computer and use it in GitHub Desktop.
Class to perform a Topological Sort based on the nodes of a Directed Acyclic Graph representing dependencies.
<?php
/**
* Directed acyclic graph based dependency resolver.
*
* Class DependencyResolver
*/
class DependencyResolver {
/**
* Internal map of outgoing edges for each node.
*
* @var array
*/
private $forwardMap = [];
/**
* Add a node to the graph, along with its outgoing edges.
*
* @param string $sourceNode
* @param array $destinationNodes
* @throws \RuntimeException
* @throws \InvalidArgumentException
*/
public function addNode($sourceNode, array $destinationNodes)
{
if (!is_string($sourceNode) || is_numeric($sourceNode)) {
throw new \InvalidArgumentException('Source node name must be a non-numeric string.');
}
if (isset($this->forwardMap[$sourceNode])) {
throw new \RuntimeException('Cannot add the same source node more than once.');
}
$this->forwardMap[$sourceNode] = $destinationNodes;
}
/**
* Reset the internal state (delete all connections).
*/
public function reset()
{
$this->forwardMap = [];
}
/**
* Resolve the dependency order. Returns an ordered array or false on failure.
*
* @return array|bool
*/
public function resolve()
{
$forwardMap = $this->forwardMap;
// Precompute a map of incoming connections for each node.
$reverseMap = [];
foreach ($forwardMap as $n => $e) {
$m = array_fill_keys($e, [$n]);
$reverseMap = array_merge_recursive($reverseMap, $m);
}
// $L is used for the sorted list.
$L = [];
// $S holds nodes with no incoming connections. The reverse map contains incoming connections.
$S = array_diff_key($forwardMap, $reverseMap);
// While we have nodes to process...
while (!empty($S)) {
// Pick a node n from S and add it to L.
$L[] = $n = key($S);
// Remove n from S and for each node m where an edge e connects n to m.
foreach (array_shift($S) as $m) {
$e = array_search($n, $reverseMap[$m], true);
// Remove edge from the graph.
unset($reverseMap[$m][$e]);
// If m has no more incoming connections...
if (empty($reverseMap[$m])) {
// But has outgoing connections...
if (isset($forwardMap[$m])) {
// Add it to S.
$S[$m] = $forwardMap[$m];
}
else {
// Otherwise add it to L.
$L[] = $m;
}
// Remove resolved node from the graph.
unset($reverseMap[$m]);
}
}
}
// If the reverse node map is not empty, the graph was cyclic and cannot be resolved.
return empty($reverseMap) ? $L : false;
}
}
@lt
Copy link
Author

lt commented Sep 24, 2014

Add nodes to the resolver like:

$resolver->addNode('sourceNode', ['destNode1', 'destNode2', ...]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment