You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
188 lines
4.1 KiB
188 lines
4.1 KiB
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Util;
|
|
|
|
/**
|
|
* Rudimentary dependency graph solver.
|
|
*
|
|
* @author Adam Pippin <hello@adampippin.ca>
|
|
*/
|
|
class DependencyGraph
|
|
{
|
|
/**
|
|
* All nodes added to this solver.
|
|
* @var DependencyGraphNode[]
|
|
*/
|
|
protected $nodes;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->nodes = [];
|
|
}
|
|
|
|
/**
|
|
* Add a node to this solver.
|
|
*
|
|
* @param string $name name of the node
|
|
* @param string[] $depends_on names of all nodes this node depends on
|
|
* @return void
|
|
*/
|
|
public function add(string $name, array $depends_on = []): void
|
|
{
|
|
if (!isset($this->nodes[$name]))
|
|
{
|
|
$node = new DependencyGraphNode($name);
|
|
$this->nodes[$name] = $node;
|
|
$node->DependsOn = $depends_on;
|
|
}
|
|
else
|
|
{
|
|
// Probably unnecessary -- if the same node is added multiple times
|
|
// with different dependencies something's probably way messed up,
|
|
// but... we'll try our best?!
|
|
$this->nodes[$name]->DependsOn = array_unique(array_merge($this->nodes[$name]->DependsOn, $depends_on));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Link all nodes, solve dependencies, and flatten the graph into a single
|
|
* ordered list representing one possible solution to the processing order
|
|
* guaranteeing that no node will be processed _before_ its dependencies.
|
|
*
|
|
* @return string[] node names
|
|
*/
|
|
public function solve(): array
|
|
{
|
|
$this->link();
|
|
$nodes = $this->flatten();
|
|
return array_values(array_unique(array_map(static function(DependencyGraphNode $node): string { return $node->Name; }, $nodes)));
|
|
}
|
|
|
|
/**
|
|
* Go through nodes, finding ones that depend on other nodes, and marking
|
|
* those nodes as being depended on by the original node. Actually building
|
|
* out a bi-directed graph from the single direction we started with.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function link(): void
|
|
{
|
|
foreach ($this->nodes as $node)
|
|
{
|
|
foreach ($node->DependsOn as $depends_on)
|
|
{
|
|
if (!isset($this->nodes[$depends_on]))
|
|
{
|
|
throw new \Exception('Unmet dependency on node: '.$depends_on);
|
|
}
|
|
|
|
if (!in_array($node->Name, $this->nodes[$depends_on]->DependedOnBy))
|
|
{
|
|
$this->nodes[$depends_on]->DependedOnBy[] = $node->Name;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given the list of nodes, flatten them into one possible solution on
|
|
* processing order and return an ordered list of nodes.
|
|
*
|
|
* @return DependencyGraphNode[]
|
|
*/
|
|
protected function flatten(): array
|
|
{
|
|
$state = [];
|
|
|
|
// Find leaf nodes, add each one to our state array.
|
|
foreach ($this->nodes as $node)
|
|
{
|
|
if (sizeof($node->DependedOnBy) == 0)
|
|
{
|
|
$state[] = $node;
|
|
}
|
|
}
|
|
|
|
// Then walk through the array and insert each node's dependencies before
|
|
// it recursively.
|
|
$iter = 0;
|
|
for ($i = 0; $i < sizeof($state); $i++)
|
|
{
|
|
if ($state[$i]->Processed)
|
|
{
|
|
continue;
|
|
}
|
|
$state[$i]->Processed = true;
|
|
$depends_on = [];
|
|
foreach ($state[$i]->DependsOn as $depends_on_name)
|
|
{
|
|
$depends_on[] = $this->nodes[$depends_on_name];
|
|
}
|
|
array_splice($state, $i, 0, $depends_on);
|
|
--$i;
|
|
}
|
|
|
|
// Reset all nodes in case we run again
|
|
foreach ($this->nodes as $node)
|
|
{
|
|
$node->Processed = false;
|
|
}
|
|
|
|
// Remove all but the first appearance of a node.
|
|
// array_unique guarantees that the _first_ element will be retained,
|
|
// so this satisfies what we're trying to do.
|
|
return $state;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a single node in our dependency graph.
|
|
*
|
|
* @internal
|
|
* @author Adam Pippin <hello@adampippin.ca>
|
|
*/
|
|
class DependencyGraphNode
|
|
{
|
|
/**
|
|
* Name of this node.
|
|
* @var string
|
|
*/
|
|
public $Name;
|
|
|
|
/**
|
|
* List of nodes this node depends on.
|
|
* @var string[]
|
|
*/
|
|
public $DependsOn;
|
|
|
|
/**
|
|
* List of nodes that depend on this node.
|
|
* @var string[]
|
|
*/
|
|
public $DependedOnBy;
|
|
|
|
/**
|
|
* Whether this node has already had its dependencies included into the solver
|
|
* state.
|
|
*
|
|
* Remember how this whole class is @internal? Yep.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $Processed;
|
|
|
|
/**
|
|
* Build a new, empty dependency graph node.
|
|
*
|
|
* @param string $name name of the node
|
|
*/
|
|
public function __construct(string $name)
|
|
{
|
|
$this->Name = $name;
|
|
$this->DependsOn = [];
|
|
$this->DependedOnBy = [];
|
|
$this->Processed = false;
|
|
}
|
|
}
|
|
|