View source
<?php
namespace Drupal\ggroup\Graph;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Cache\Cache;
class SqlGroupGraphStorage implements GroupGraphStorageInterface {
protected $ancestors = [];
protected $descendants = [];
protected $directAncestors = [];
protected $directDescendants = [];
protected $connection;
protected $loaded = [];
public function __construct(Connection $connection) {
$this->connection = $connection;
}
protected function invalidate($gids) {
if (!empty($gids)) {
foreach ($gids as $gid) {
$cache_tags[] = "group:{$gid}";
$this->loaded[$gid] = FALSE;
unset($this->ancestors[$gid]);
unset($this->descendants[$gid]);
unset($this->directAncestors[$gid]);
unset($this->directDescendants[$gid]);
}
if (!empty($cache_tags)) {
Cache::invalidateTags($cache_tags);
}
}
}
protected function loadGroupMapping($gid) {
$cid = "ggroup_graph_map:{$gid}";
if ($cache = \Drupal::cache()
->get($cid)) {
$mapping = $cache->data;
}
else {
$query = $this->connection
->select('group_graph', 'gg')
->fields('gg', [
'start_vertex',
'end_vertex',
]);
$or_group = $query
->orConditionGroup();
$or_group
->condition('gg.start_vertex', $gid)
->condition('gg.end_vertex', $gid);
$query
->condition($or_group);
$mapping['descendants'] = $query
->execute()
->fetchAll(\PDO::FETCH_COLUMN | \PDO::FETCH_GROUP);
$mapping['ancestors'] = $query
->execute()
->fetchAll(\PDO::FETCH_COLUMN | \PDO::FETCH_GROUP, 1);
$query = $this->connection
->select('group_graph', 'gg')
->fields('gg', [
'start_vertex',
'end_vertex',
]);
$query
->condition('hops', 0)
->condition($or_group);
$mapping['directDescendants'] = $query
->execute()
->fetchAll(\PDO::FETCH_COLUMN | \PDO::FETCH_GROUP);
$mapping['directAncestors'] = $query
->execute()
->fetchAll(\PDO::FETCH_COLUMN | \PDO::FETCH_GROUP, 1);
$groups = array_merge($mapping['descendants'], $mapping['ancestors']);
$groups[] = $gid;
$cache_tags = [];
$groups = $this
->flattenGroups($groups);
$groups = array_unique($groups, SORT_NUMERIC);
foreach ($groups as $group_id) {
$cache_tags[] = "group:{$group_id}";
}
\Drupal::cache()
->set($cid, $mapping, Cache::PERMANENT, $cache_tags);
}
$this
->mergeMappings($mapping);
$this->loaded[$gid] = TRUE;
}
private function flattenGroups(array $groups) {
$flat_groups = [];
if (!empty($groups)) {
foreach ($groups as $group_val) {
if (is_array($group_val)) {
$flat_groups += $group_val;
}
else {
$flat_groups[] = $group_val;
}
}
}
return $flat_groups;
}
private function mergeMappings(array $mapping) {
if (!empty($mapping)) {
foreach ($mapping as $relation => $relatives) {
if (!empty($relatives)) {
($rootRelation =& $this->{$relation}) ?: [];
foreach ($relatives as $parentGid => $groupMap) {
$groupMap = array_combine($groupMap, $groupMap);
if (!empty($rootRelation[$parentGid])) {
$rootRelation[$parentGid] = $rootRelation[$parentGid] + $groupMap;
}
else {
$rootRelation[$parentGid] = $groupMap;
}
}
}
}
}
return $this;
}
protected function getEdgeId($parent_group_id, $child_group_id) {
$query = $this->connection
->select('group_graph', 'gg')
->fields('gg', [
'id',
]);
$query
->condition('start_vertex', $parent_group_id);
$query
->condition('end_vertex', $child_group_id);
$query
->condition('hops', 0);
return $query
->execute()
->fetchField();
}
protected function insertEdge($parent_group_id, $child_group_id) {
$new_edge_id = $this->connection
->insert('group_graph')
->fields([
'start_vertex' => $parent_group_id,
'end_vertex' => $child_group_id,
'hops' => 0,
])
->execute();
$this->connection
->update('group_graph')
->fields([
'entry_edge_id' => $new_edge_id,
'exit_edge_id' => $new_edge_id,
'direct_edge_id' => $new_edge_id,
])
->condition('id', $new_edge_id)
->execute();
return $new_edge_id;
}
protected function insertEdgesParentIncomingToChild($edge_id, $parent_group_id, $child_group_id) {
$query = $this->connection
->select('group_graph', 'gg');
$query
->addExpression('gg.id', 'entry_edge_id');
$query
->addExpression($edge_id, 'direct_edge_id');
$query
->addExpression($edge_id, 'exit_edge_id');
$query
->addExpression('gg.start_vertex', 'start_vertex');
$query
->addExpression($child_group_id, 'end_vertex');
$query
->addExpression('gg.hops + 1', 'hops');
$query
->condition('end_vertex', $parent_group_id);
$this->connection
->insert('group_graph')
->fields([
'entry_edge_id',
'direct_edge_id',
'exit_edge_id',
'start_vertex',
'end_vertex',
'hops',
])
->from($query)
->execute();
}
protected function insertEdgesParentToChildOutgoing($edge_id, $parent_group_id, $child_group_id) {
$query = $this->connection
->select('group_graph', 'gg');
$query
->addExpression($edge_id, 'entry_edge_id');
$query
->addExpression($edge_id, 'direct_edge_id');
$query
->addExpression('gg.id', 'exit_edge_id');
$query
->addExpression($parent_group_id, 'start_vertex');
$query
->addExpression('gg.end_vertex', 'end_vertex');
$query
->addExpression('gg.hops + 1', 'hops');
$query
->condition('start_vertex', $child_group_id);
$this->connection
->insert('group_graph')
->fields([
'entry_edge_id',
'direct_edge_id',
'exit_edge_id',
'start_vertex',
'end_vertex',
'hops',
])
->from($query)
->execute();
}
protected function insertEdgesParentIncomingToChildOutgoing($edge_id, $parent_group_id, $child_group_id) {
$query = $this->connection
->select('group_graph', 'parent_gg');
$query
->join('group_graph', 'child_gg', 'child_gg.end_vertex = parent_gg.start_vertex');
$query
->addExpression('parent_gg.id', 'entry_edge_id');
$query
->addExpression($edge_id, 'direct_edge_id');
$query
->addExpression('child_gg.id', 'exit_edge_id');
$query
->addExpression('parent_gg.start_vertex', 'start_vertex');
$query
->addExpression('child_gg.end_vertex', 'end_vertex');
$query
->addExpression('parent_gg.hops + child_gg.hops + 1', 'hops');
$query
->condition('parent_gg.end_vertex', $parent_group_id);
$query
->condition('child_gg.start_vertex', $child_group_id);
$this->connection
->insert('group_graph')
->fields([
'entry_edge_id',
'direct_edge_id',
'exit_edge_id',
'start_vertex',
'end_vertex',
'hops',
])
->from($query)
->execute();
}
public function getGraph($group_id) {
$query = $this->connection
->select('group_graph', 'gg')
->fields('gg', [
'start_vertex',
'end_vertex',
])
->orderBy('hops')
->orderBy('start_vertex');
$or_group = $query
->orConditionGroup();
$or_group
->condition('gg.start_vertex', $group_id)
->condition('gg.end_vertex', $group_id);
$query
->condition($or_group);
return $query
->execute()
->fetchAll();
}
public function addEdge($parent_group_id, $child_group_id) {
if ($parent_group_id === $child_group_id) {
return FALSE;
}
$parent_child_edge_id = $this
->getEdgeId($parent_group_id, $child_group_id);
if (!empty($parent_child_edge_id)) {
return $parent_child_edge_id;
}
$child_parent_edge_id = $this
->getEdgeId($parent_group_id, $child_group_id);
if (!empty($child_parent_edge_id)) {
return $child_parent_edge_id;
}
if ($this
->isDescendant($parent_group_id, $child_group_id)) {
throw new CyclicGraphException($parent_group_id, $child_group_id);
}
$new_edge_id = $this
->insertEdge($parent_group_id, $child_group_id);
$this
->insertEdgesParentIncomingToChild($new_edge_id, $parent_group_id, $child_group_id);
$this
->insertEdgesParentToChildOutgoing($new_edge_id, $parent_group_id, $child_group_id);
$this
->insertEdgesParentIncomingToChildOutgoing($new_edge_id, $parent_group_id, $child_group_id);
$this
->invalidate([
$parent_group_id,
$child_group_id,
]);
return $new_edge_id;
}
public function removeEdge($parent_group_id, $child_group_id) {
$edge_id = $this
->getEdgeId($parent_group_id, $child_group_id);
if (empty($edge_id)) {
return;
}
$edges_to_delete = [];
$query = $this->connection
->select('group_graph', 'gg')
->fields('gg', [
'id',
]);
$query
->condition('direct_edge_id', $edge_id);
$results = $query
->execute();
while ($id = $results
->fetchField()) {
$edges_to_delete[] = $id;
}
if (empty($edges_to_delete)) {
return;
}
do {
$total_edges = count($edges_to_delete);
$query = $this->connection
->select('group_graph', 'gg')
->fields('gg', [
'id',
]);
$query
->condition('hops', 0);
$query
->condition('id', $edges_to_delete, 'NOT IN');
$query_or_conditions = new Condition('OR');
$query_or_conditions
->condition('entry_edge_id', $edges_to_delete, 'IN');
$query_or_conditions
->condition('exit_edge_id', $edges_to_delete, 'IN');
$query
->condition($query_or_conditions);
$results = $query
->execute();
while ($id = $results
->fetchField()) {
$edges_to_delete[] = $id;
}
} while (count($edges_to_delete) > $total_edges);
$this->connection
->delete('group_graph')
->condition('id', $edges_to_delete, 'IN')
->execute();
$this
->invalidate([
$parent_group_id,
$child_group_id,
]);
}
private function loadMap($gid) {
if (empty($this->loaded[$gid])) {
$this
->loadGroupMapping($gid);
}
}
public function getDirectDescendants($group_id) {
$this
->loadMap($group_id);
return isset($this->directDescendants[$group_id]) ? $this->directDescendants[$group_id] : [];
}
public function getDirectAncestors($group_id) {
$this
->loadMap($group_id);
return isset($this->directAncestors[$group_id]) ? $this->directAncestors[$group_id] : [];
}
public function getDescendants($group_id) {
$this
->loadMap($group_id);
return isset($this->descendants[$group_id]) ? $this->descendants[$group_id] : [];
}
public function getAncestors($group_id) {
$this
->loadMap($group_id);
return isset($this->ancestors[$group_id]) ? $this->ancestors[$group_id] : [];
}
public function isDirectDescendant($a, $b) {
$this
->loadMap($b);
return isset($this->directDescendants[$b]) ? in_array($a, $this->directDescendants[$b]) : FALSE;
}
public function isDirectAncestor($a, $b) {
$this
->loadMap($b);
return isset($this->directAncestors[$b]) ? in_array($a, $this->directAncestors[$b]) : FALSE;
}
public function isDescendant($a, $b) {
$this
->loadMap($b);
return isset($this->descendants[$b]) ? in_array($a, $this->descendants[$b]) : FALSE;
}
public function isAncestor($a, $b) {
$this
->loadMap($b);
return isset($this->ancestors[$b]) ? in_array($a, $this->ancestors[$b]) : FALSE;
}
public function getPath($parent_group_id, $child_group_id) {
if (!$this
->isAncestor($parent_group_id, $child_group_id)) {
return [];
}
$visited = [];
$solutions = [];
$queue = new \SplQueue();
$queue
->enqueue($child_group_id);
$visited[$child_group_id] = TRUE;
$paths = [];
$paths[$child_group_id][] = $child_group_id;
while (!$queue
->isEmpty() && $queue
->bottom() != $parent_group_id) {
$child_id = $queue
->dequeue();
$this
->loadMap($child_id);
if (isset($this->directAncestors[$child_id])) {
$parent_ids = $this->directAncestors[$child_id];
foreach ($parent_ids as $parent_id) {
if ((int) $parent_id === (int) $parent_group_id) {
$solution = $paths[$child_id];
$solution[] = $parent_id;
$solutions[] = $solution;
}
else {
if (!isset($visited[$parent_id])) {
$queue
->enqueue($parent_id);
$visited[$parent_id] = TRUE;
$paths[$parent_id] = $paths[$child_id];
$paths[$parent_id][] = $parent_id;
}
}
}
}
}
return $solutions;
}
}