class RevisionTreeIndex in Multiversion 8
Same name and namespace in other branches
- 8.2 src/Entity/Index/RevisionTreeIndex.php \Drupal\multiversion\Entity\Index\RevisionTreeIndex
The revision tree index.
@todo: {@link https://www.drupal.org/node/2597444 Consider caching once/if rev and rev tree indices are merged.}
Hierarchy
- class \Drupal\multiversion\Entity\Index\RevisionTreeIndex implements RevisionTreeIndexInterface
Expanded class hierarchy of RevisionTreeIndex
1 file declares its use of RevisionTreeIndex
- RevisionTreeIndexTest.php in tests/
src/ Functional/ RevisionTreeIndexTest.php
1 string reference to 'RevisionTreeIndex'
1 service uses RevisionTreeIndex
File
- src/
Entity/ Index/ RevisionTreeIndex.php, line 17
Namespace
Drupal\multiversion\Entity\IndexView source
class RevisionTreeIndex implements RevisionTreeIndexInterface {
/**
* The key value factory service.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValueFactory;
/**
* The workspace manager service.
*
* @var \Drupal\multiversion\Workspace\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* The index factory service.
*
* @var \Drupal\multiversion\Entity\Index\MultiversionIndexFactory
*/
protected $indexFactory;
/**
* The workspace ID.
*
* @var string
*/
protected $workspaceId;
/**
* Constructs the Revision Tree Index.
*
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
* The key value factory service.
* @param \Drupal\multiversion\Workspace\WorkspaceManagerInterface $workspace_manager
* The workspace manager service.
* @param \Drupal\multiversion\Entity\Index\MultiversionIndexFactory $index_factory
* The index factory service.
*/
public function __construct(KeyValueFactoryInterface $key_value_factory, WorkspaceManagerInterface $workspace_manager, MultiversionIndexFactory $index_factory) {
$this->keyValueFactory = $key_value_factory;
$this->workspaceManager = $workspace_manager;
$this->indexFactory = $index_factory;
}
/**
* {@inheritdoc}
*/
public function useWorkspace($id) {
$this->workspaceId = $id;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTree($uuid) {
$values = $this
->buildTree($uuid);
return $values['tree'];
}
/**
* {@inheritdoc}
*/
public function getGraph($uuid) {
$tree = $this
->getTree($uuid);
$graph = new Graph();
$rev_ids = [];
$this
->storeNodesId($tree, $rev_ids);
$vertices = $this
->generateVertices($graph, $rev_ids);
$this
->generateEdges($vertices, $tree);
return $graph;
}
/**
* Helper function to store all revision IDs in an array.
*
* @param array $tree
* An associative array containing information about tree.
* @param array $revision_ids
* An array to store all revision ID.
*/
protected function storeNodesId(array $tree, array &$revision_ids) {
foreach ($tree as $value) {
$current_id = $value['#rev'];
$revision_ids[$current_id] = $current_id;
if (count($value['children'])) {
$this
->storeNodesId($value['children'], $revision_ids);
}
}
}
/**
* Helper function to create Edges between parent and children.
*
* @param array $revisions_array
* Associative array containing graph nodes.
* @param array $tree
* Associative array containing tree structure.
* @param int $parent
* Parent vertex Id.
*/
protected function generateEdges(array $revisions_array, array $tree, $parent = -1) {
foreach ($tree as $item) {
$current_id = $item['#rev'];
if ($parent != -1) {
$revisions_array[$parent]
->createEdgeTo($revisions_array[$current_id]);
}
if (count($item['children'])) {
$this
->generateEdges($revisions_array, $item['children'], $current_id);
}
}
}
/**
* Generates vertices for Graph.
*
* @param Graph $graph
* A graph object.
* @param array $revision_ids
* The revision ids to generate vertices for.
*
* @return array
* An array of vertices.
*/
protected function generateVertices(Graph $graph, array $revision_ids) {
foreach ($revision_ids as $id) {
$ids[] = $id;
}
return $graph
->createVertices($ids)
->getMap();
}
/**
* {@inheritdoc}
*/
public function updateTree(ContentEntityInterface $entity, array $branch = []) {
if ($entity
->getEntityType()
->get('workspace') === FALSE) {
$this
->keyValueStore($entity
->uuid(), 0)
->setMultiple($branch);
}
else {
$this
->keyValueStore($entity
->uuid())
->setMultiple($branch);
}
return $this;
}
/**
* {@inheritdoc}
*
* @todo: {@link https://www.drupal.org/node/2597422 The revision tree also
* contain missing revisions. We need a better way to count.}
*/
public function countRevs($uuid) {
$values = $this
->buildTree($uuid);
return count($values['default_branch']);
}
/**
* {@inheritdoc}
*/
public function getDefaultRevision($uuid) {
$values = $this
->buildTree($uuid);
return $values['default_rev']['#rev'];
}
/**
* {@inheritdoc}
*/
public function getDefaultBranch($uuid) {
$values = $this
->buildTree($uuid);
return $values['default_branch'];
}
/**
* {@inheritdoc}
*/
public function getOpenRevisions($uuid) {
$revs = [];
$values = $this
->buildTree($uuid);
foreach ($values['open_revs'] as $rev => $element) {
$revs[$rev] = $element['#rev_info']['status'];
}
return $revs;
}
/**
* {@inheritdoc}
*/
public function getConflicts($uuid) {
$revs = [];
$values = $this
->buildTree($uuid);
foreach ($values['conflicts'] as $rev => $element) {
$revs[$rev] = $element['#rev_info']['status'];
}
return $revs;
}
/**
* {@inheritdoc}
*/
public static function sortRevisions(array $a, array $b) {
$a_deleted = $a['#rev_info']['status'] == 'deleted' ? TRUE : FALSE;
$b_deleted = $b['#rev_info']['status'] == 'deleted' ? TRUE : FALSE;
// The goal is to sort winning revision candidates from low to high. The
// criteria are:
// 1. Non-deleted always win over deleted.
// 2. When IDs match, higher ASCII sort on revision hash wins.
// 3. Otherwise, the highest ID wins.
if ($a_deleted && !$b_deleted) {
return 1;
}
elseif (!$a_deleted && $b_deleted) {
return -1;
}
list($a_id) = explode('-', $a['#rev']);
list($b_id) = explode('-', $b['#rev']);
if ($a_id === $b_id) {
return $a['#rev'] < $b['#rev'] ? 1 : -1;
}
else {
return $a_id < $b_id ? 1 : -1;
}
}
/**
* {@inheritdoc}
*/
public static function sortTree(array &$tree) {
// Sort all tree elements according to the algorithm before recursing.
usort($tree, [
__CLASS__,
'sortRevisions',
]);
foreach ($tree as &$element) {
if (!empty($element['children'])) {
self::sortTree($element['children']);
}
}
}
/**
* @param string $uuid
* The UUID for get the key value store for.
* @param int|null $workspace_id
* The workspace to get the key value store for.
*
* @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
* The key value store.
*/
protected function keyValueStore($uuid, $workspace_id = NULL) {
if (!is_numeric($workspace_id)) {
$workspace_id = $this
->getWorkspaceId();
}
return $this->keyValueFactory
->get("multiversion.entity_index.rev.tree.{$workspace_id}.{$uuid}");
}
/**
* Helper method to build the revision tree.
*/
protected function buildTree($uuid) {
$revs = $this
->keyValueStore($uuid)
->getAll();
if (!$revs) {
$revs = $this
->keyValueStore($uuid, 0)
->getAll();
}
// Build the keys to fetch from the rev index.
$keys = [];
foreach (array_keys($revs) as $rev) {
$keys[] = "{$uuid}:{$rev}";
}
$workspace = Workspace::load($this
->getWorkspaceId());
$revs_info = $this->indexFactory
->get('multiversion.entity_index.rev', $workspace)
->getMultiple($keys);
return self::doBuildTree($uuid, $revs, $revs_info);
}
/**
* Recursive helper method to build the revision tree.
*
* @return array
* Returns an array containing the built tree, open revisions, default
* revision, default branch and conflicts.
*
* @todo: {@link https://www.drupal.org/node/2597430 Implement
* 'deleted_conflicts'.}
*/
protected static function doBuildTree($uuid, $revs, $revs_info, $parse = 0, &$tree = [], &$open_revs = [], &$conflicts = []) {
foreach ($revs as $rev => $parent_revs) {
foreach ($parent_revs as $parent_rev) {
if ($rev == 0) {
continue;
}
if ($parent_rev == $parse) {
// Avoid bad data to cause endless loops.
// @todo: {@link https://www.drupal.org/node/2597434 Needs test.}
if ($rev == $parse) {
throw new \InvalidArgumentException('Child and parent revision can not be the same value.');
}
// Build an element structure compatible with Drupal's Render API.
$i = count($tree);
$tree[$i] = [
'#type' => 'rev',
'#uuid' => $uuid,
'#rev' => $rev,
'#rev_info' => [
'status' => isset($revs_info["{$uuid}:{$rev}"]['status']) ? $revs_info["{$uuid}:{$rev}"]['status'] : 'missing',
'default' => FALSE,
'open_rev' => FALSE,
'conflict' => FALSE,
],
'children' => [],
];
// Recurse down through the children.
self::doBuildTree($uuid, $revs, $revs_info, $rev, $tree[$i]['children'], $open_revs, $conflicts);
// Find open revisions and conflicts. Only revisions with no children,
// and that are not missing can be an open revision or a conflict.
if (empty($tree[$i]['children']) && $tree[$i]['#rev_info']['status'] != 'missing') {
$tree[$i]['#rev_info']['open_rev'] = TRUE;
$open_revs[$rev] = $tree[$i];
// All open revisions, except deleted and default revisions, are
// conflicts by definition. We will revert the conflict flag when we
// find the default revision later on.
if ($tree[$i]['#rev_info']['status'] != 'deleted') {
$tree[$i]['#rev_info']['conflict'] = TRUE;
$conflicts[$rev] = $tree[$i];
}
}
}
}
}
// Now when the full tree is built we'll find the default revision and
// its branch.
if ($parse == 0) {
$default_rev = 0;
uasort($open_revs, [
__CLASS__,
'sortRevisions',
]);
$default_rev = reset($open_revs);
// Remove the default revision from the conflicts array and sort it.
unset($conflicts[$default_rev['#rev']]);
uasort($conflicts, [
__CLASS__,
'sortRevisions',
]);
// Update the default revision in the tree and sort it.
self::updateDefaultRevision($tree, $default_rev);
self::sortTree($tree);
// Find the branch of the default revision.
$default_branch = [];
$rev = $default_rev['#rev'];
while ($rev != 0) {
$default_branch[$rev] = isset($revs_info["{$uuid}:{$rev}"]['status']) ? $revs_info["{$uuid}:{$rev}"]['status'] : 'missing';
// Only the first parent gets included in the default branch.
$rev = $revs[$rev][0];
}
return [
'tree' => $tree,
'default_rev' => $default_rev,
'default_branch' => array_reverse($default_branch),
'open_revs' => $open_revs,
'conflicts' => $conflicts,
];
}
}
/**
* Helper method to update the default revision.
*/
protected static function updateDefaultRevision(&$tree, $default_rev) {
// @todo: {@link https://www.drupal.org/node/2597442 We can temporarily
// flip the sort to find the default rev earlier.}
foreach ($tree as &$element) {
if (isset($element['#rev']) && $element['#rev'] == $default_rev['#rev']) {
$element['#rev_info']['default'] = TRUE;
$element['#rev_info']['conflict'] = FALSE;
break;
}
if (!empty($element['children'])) {
self::updateDefaultRevision($element['children'], $default_rev);
}
}
}
/**
* Helper method to get the workspace ID to query.
*/
protected function getWorkspaceId() {
return $this->workspaceId ?: $this->workspaceManager
->getActiveWorkspaceId();
}
}