Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
62 / 62 |
|
100.00% |
40 / 40 |
|
58.62% |
17 / 29 |
|
100.00% |
11 / 11 |
CRAP | |
100.00% |
1 / 1 |
| InstanceStorage | |
100.00% |
62 / 62 |
|
100.00% |
40 / 40 |
|
58.62% |
17 / 29 |
|
100.00% |
11 / 11 |
64.81 | |
100.00% |
1 / 1 |
| undo | |
100.00% |
10 / 10 |
|
100.00% |
5 / 5 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
3.33 | |||
| redo | |
100.00% |
10 / 10 |
|
100.00% |
5 / 5 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
3.33 | |||
| clear | |
100.00% |
6 / 6 |
|
100.00% |
4 / 4 |
|
33.33% |
1 / 3 |
|
100.00% |
1 / 1 |
3.19 | |||
| getPast | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getFuture | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getUsers | |
100.00% |
8 / 8 |
|
100.00% |
8 / 8 |
|
0.00% |
0 / 6 |
|
100.00% |
1 / 1 |
4 | |||
| doPreSave | |
100.00% |
6 / 6 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| doPostSave | |
100.00% |
4 / 4 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| getAllRevisionsQuery | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| clearFuture | |
100.00% |
2 / 2 |
|
100.00% |
4 / 4 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
2.15 | |||
| trimPast | |
100.00% |
4 / 4 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\display_builder\Entity; |
| 6 | |
| 7 | use Drupal\Core\Entity\EntityInterface; |
| 8 | use Drupal\Core\Entity\Query\QueryInterface; |
| 9 | use Drupal\Core\Entity\RevisionableInterface; |
| 10 | use Drupal\Core\Entity\RevisionLogInterface; |
| 11 | use Drupal\Core\Entity\Sql\SqlContentEntityStorage; |
| 12 | |
| 13 | /** |
| 14 | * Instance entity storage handler. |
| 15 | */ |
| 16 | class InstanceStorage extends SqlContentEntityStorage { |
| 17 | |
| 18 | private const MAX_HISTORY = 20; |
| 19 | |
| 20 | /** |
| 21 | * Load the last past revision as default. |
| 22 | * |
| 23 | * @param \Drupal\Core\Entity\RevisionableInterface $entity |
| 24 | * A revisionable entity. |
| 25 | * |
| 26 | * @return \Drupal\Core\Entity\RevisionableInterface |
| 27 | * The default entity revision. |
| 28 | */ |
| 29 | public function undo(RevisionableInterface $entity): RevisionableInterface { |
| 30 | $past = $this->getPast($entity); |
| 31 | |
| 32 | if (\count($past) === 0) { |
| 33 | return $entity; |
| 34 | } |
| 35 | |
| 36 | // @todo Use array_last() once we drop Drupal 11 support. |
| 37 | $previous = $past[\array_key_last($past)] ?? NULL; |
| 38 | |
| 39 | if ($previous) { |
| 40 | $previous->isDefaultRevision(TRUE); |
| 41 | $previous->save(); |
| 42 | $entity->isDefaultRevision(FALSE); |
| 43 | $entity->save(); |
| 44 | } |
| 45 | |
| 46 | return $previous; |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * Load the first future revision as default. |
| 51 | * |
| 52 | * @param \Drupal\Core\Entity\RevisionableInterface $entity |
| 53 | * A revisionable entity. |
| 54 | * |
| 55 | * @return \Drupal\Core\Entity\RevisionableInterface |
| 56 | * The default entity revision. |
| 57 | */ |
| 58 | public function redo(RevisionableInterface $entity): RevisionableInterface { |
| 59 | $future = $this->getFuture($entity); |
| 60 | |
| 61 | if (\count($future) === 0) { |
| 62 | return $entity; |
| 63 | } |
| 64 | |
| 65 | // @todo Use array_first() once we drop Drupal 11 support. |
| 66 | $next = $future[\array_key_first($future)] ?? NULL; |
| 67 | |
| 68 | if ($next) { |
| 69 | $next->isDefaultRevision(TRUE); |
| 70 | $next->save(); |
| 71 | $entity->isDefaultRevision(FALSE); |
| 72 | $entity->save(); |
| 73 | } |
| 74 | |
| 75 | return $next; |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Delete all revisions except the default one. |
| 80 | * |
| 81 | * @param \Drupal\Core\Entity\RevisionableInterface $entity |
| 82 | * A revisionable entity. |
| 83 | */ |
| 84 | public function clear(RevisionableInterface $entity): void { |
| 85 | $query = $this->getAllRevisionsQuery($entity); |
| 86 | $query->condition('revision', (int) $entity->getRevisionId(), '<>'); |
| 87 | // QueryInterface::execute() returns an integer for count queries or an |
| 88 | // array of ids. |
| 89 | /** @var array $revisions */ |
| 90 | $revisions = $query->execute(); |
| 91 | |
| 92 | foreach (\array_keys($revisions) as $vid) { |
| 93 | $this->deleteRevision($vid); |
| 94 | } |
| 95 | |
| 96 | $entity->save(); |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * Get the past revisions. |
| 101 | * |
| 102 | * @param \Drupal\Core\Entity\RevisionableInterface $entity |
| 103 | * A revisionable entity. |
| 104 | * |
| 105 | * @return \Drupal\Core\Entity\RevisionableInterface[] |
| 106 | * An array of entity revisions keyed by their revision ID, or an empty |
| 107 | * array if none found. |
| 108 | */ |
| 109 | public function getPast(RevisionableInterface $entity): array { |
| 110 | $query = $this->getAllRevisionsQuery($entity); |
| 111 | // Compare revision using vid, the older revision has smaller ID. |
| 112 | $query->condition('revision', (int) $entity->getRevisionId(), '<'); |
| 113 | // QueryInterface::execute() returns an integer for count queries or an |
| 114 | // array of ids. |
| 115 | /** @var array $revisions */ |
| 116 | $revisions = $query->execute(); |
| 117 | $revisions = $this->loadMultipleRevisions(\array_keys($revisions)); |
| 118 | |
| 119 | return $revisions; |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * Get the future revisions. |
| 124 | * |
| 125 | * @param \Drupal\Core\Entity\RevisionableInterface $entity |
| 126 | * A revisionable entity. |
| 127 | * |
| 128 | * @return \Drupal\Core\Entity\RevisionableInterface[] |
| 129 | * An array of entity revisions keyed by their revision ID, or an empty |
| 130 | * array if none found. |
| 131 | */ |
| 132 | public function getFuture(RevisionableInterface $entity): array { |
| 133 | $query = $this->getAllRevisionsQuery($entity); |
| 134 | // Compare revision using vid, the newer revision has bigger ID. |
| 135 | $query->condition('revision', (int) $entity->getRevisionId(), '>'); |
| 136 | // QueryInterface::execute() returns an integer for count queries or an |
| 137 | // array of ids. |
| 138 | /** @var array $revisions */ |
| 139 | $revisions = $query->execute(); |
| 140 | $revisions = $this->loadMultipleRevisions(\array_keys($revisions)); |
| 141 | |
| 142 | return $revisions; |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * Get users. |
| 147 | * |
| 148 | * All users which have authored a step in present, past or future, with the |
| 149 | * most recent date of action. |
| 150 | * |
| 151 | * @param \Drupal\Core\Entity\RevisionLogInterface $entity |
| 152 | * A revisionable entity. |
| 153 | * |
| 154 | * @return array |
| 155 | * Each key is an User entity ID, each value is a timestamp. |
| 156 | */ |
| 157 | public function getUsers(RevisionLogInterface $entity): array { |
| 158 | // An associative array where keys are User IDs and values are timestamps. |
| 159 | $users = []; |
| 160 | // QueryInterface::execute() returns an integer for count queries or an |
| 161 | // array of ids. |
| 162 | /** @var array $revisions */ |
| 163 | $revisions = $this->getAllRevisionsQuery($entity)->execute(); |
| 164 | $revisions = $this->loadMultipleRevisions(\array_keys($revisions)); |
| 165 | |
| 166 | foreach ($revisions as $step) { |
| 167 | /** @var \Drupal\display_builder\InstanceInterface $step */ |
| 168 | // User ID is 0 when anonymous and NULL if not set or user was deleted. |
| 169 | // Let's standardize around anonymous so every revision has an user. |
| 170 | $user_id = (int) $step->getRevisionUserId(); |
| 171 | |
| 172 | if (!isset($users[$user_id]) || $step->getRevisionCreationTime() > $users[$user_id]) { |
| 173 | $users[$user_id] = (int) $step->getRevisionCreationTime(); |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | return $users; |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * {@inheritdoc} |
| 182 | */ |
| 183 | protected function doPreSave(EntityInterface $entity): mixed { |
| 184 | /** @var \Drupal\Core\Entity\RevisionableInterface $entity */ |
| 185 | $return = parent::doPreSave($entity); |
| 186 | $isNewRevision = $entity->isNewRevision(); |
| 187 | $original = $entity->getOriginal(); |
| 188 | |
| 189 | if ($original && $isNewRevision) { |
| 190 | $this->clearFuture($original); |
| 191 | } |
| 192 | |
| 193 | return $return; |
| 194 | } |
| 195 | |
| 196 | /** |
| 197 | * {@inheritdoc} |
| 198 | */ |
| 199 | protected function doPostSave(EntityInterface $entity, $update): void { |
| 200 | /** @var \Drupal\Core\Entity\RevisionableInterface $entity */ |
| 201 | $isNewRevision = $entity->isNewRevision(); |
| 202 | parent::doPostSave($entity, $update); |
| 203 | |
| 204 | if ($isNewRevision) { |
| 205 | $this->trimPast($entity); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | /** |
| 210 | * Get query for all revisions. |
| 211 | */ |
| 212 | private function getAllRevisionsQuery(RevisionableInterface $entity): QueryInterface { |
| 213 | $query = $this->getQuery()->accessCheck(FALSE)->allRevisions(); |
| 214 | |
| 215 | return $query->condition('id', $entity->id())->sort('revision', 'ASC'); |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Delete all future revisions. |
| 220 | */ |
| 221 | private function clearFuture(RevisionableInterface $entity): void { |
| 222 | foreach (\array_keys($this->getFuture($entity)) as $vid) { |
| 223 | $this->deleteRevision($vid); |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | /** |
| 228 | * Keep only the last x history. |
| 229 | */ |
| 230 | private function trimPast(RevisionableInterface $entity): void { |
| 231 | $past = $this->getPast($entity); |
| 232 | |
| 233 | if (\count($past) > self::MAX_HISTORY) { |
| 234 | $oldest_revision_id = \array_key_first($past); |
| 235 | $this->deleteRevision($oldest_revision_id); |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 84 | public function clear(RevisionableInterface $entity): void { |
| 85 | $query = $this->getAllRevisionsQuery($entity); |
| 86 | $query->condition('revision', (int) $entity->getRevisionId(), '<>'); |
| 87 | // QueryInterface::execute() returns an integer for count queries or an |
| 88 | // array of ids. |
| 89 | /** @var array $revisions */ |
| 90 | $revisions = $query->execute(); |
| 91 | |
| 92 | foreach (\array_keys($revisions) as $vid) { |
| 92 | foreach (\array_keys($revisions) as $vid) { |
| 92 | foreach (\array_keys($revisions) as $vid) { |
| 93 | $this->deleteRevision($vid); |
| 92 | foreach (\array_keys($revisions) as $vid) { |
| 93 | $this->deleteRevision($vid); |
| 94 | } |
| 95 | |
| 96 | $entity->save(); |
| 97 | } |
| 221 | private function clearFuture(RevisionableInterface $entity): void { |
| 222 | foreach (\array_keys($this->getFuture($entity)) as $vid) { |
| 222 | foreach (\array_keys($this->getFuture($entity)) as $vid) { |
| 222 | foreach (\array_keys($this->getFuture($entity)) as $vid) { |
| 223 | $this->deleteRevision($vid); |
| 222 | foreach (\array_keys($this->getFuture($entity)) as $vid) { |
| 223 | $this->deleteRevision($vid); |
| 224 | } |
| 225 | } |
| 199 | protected function doPostSave(EntityInterface $entity, $update): void { |
| 200 | /** @var \Drupal\Core\Entity\RevisionableInterface $entity */ |
| 201 | $isNewRevision = $entity->isNewRevision(); |
| 202 | parent::doPostSave($entity, $update); |
| 203 | |
| 204 | if ($isNewRevision) { |
| 205 | $this->trimPast($entity); |
| 206 | } |
| 207 | } |
| 207 | } |
| 183 | protected function doPreSave(EntityInterface $entity): mixed { |
| 184 | /** @var \Drupal\Core\Entity\RevisionableInterface $entity */ |
| 185 | $return = parent::doPreSave($entity); |
| 186 | $isNewRevision = $entity->isNewRevision(); |
| 187 | $original = $entity->getOriginal(); |
| 188 | |
| 189 | if ($original && $isNewRevision) { |
| 189 | if ($original && $isNewRevision) { |
| 189 | if ($original && $isNewRevision) { |
| 190 | $this->clearFuture($original); |
| 191 | } |
| 192 | |
| 193 | return $return; |
| 193 | return $return; |
| 194 | } |
| 212 | private function getAllRevisionsQuery(RevisionableInterface $entity): QueryInterface { |
| 213 | $query = $this->getQuery()->accessCheck(FALSE)->allRevisions(); |
| 214 | |
| 215 | return $query->condition('id', $entity->id())->sort('revision', 'ASC'); |
| 216 | } |
| 132 | public function getFuture(RevisionableInterface $entity): array { |
| 133 | $query = $this->getAllRevisionsQuery($entity); |
| 134 | // Compare revision using vid, the newer revision has bigger ID. |
| 135 | $query->condition('revision', (int) $entity->getRevisionId(), '>'); |
| 136 | // QueryInterface::execute() returns an integer for count queries or an |
| 137 | // array of ids. |
| 138 | /** @var array $revisions */ |
| 139 | $revisions = $query->execute(); |
| 140 | $revisions = $this->loadMultipleRevisions(\array_keys($revisions)); |
| 141 | |
| 142 | return $revisions; |
| 143 | } |
| 109 | public function getPast(RevisionableInterface $entity): array { |
| 110 | $query = $this->getAllRevisionsQuery($entity); |
| 111 | // Compare revision using vid, the older revision has smaller ID. |
| 112 | $query->condition('revision', (int) $entity->getRevisionId(), '<'); |
| 113 | // QueryInterface::execute() returns an integer for count queries or an |
| 114 | // array of ids. |
| 115 | /** @var array $revisions */ |
| 116 | $revisions = $query->execute(); |
| 117 | $revisions = $this->loadMultipleRevisions(\array_keys($revisions)); |
| 118 | |
| 119 | return $revisions; |
| 120 | } |
| 157 | public function getUsers(RevisionLogInterface $entity): array { |
| 158 | // An associative array where keys are User IDs and values are timestamps. |
| 159 | $users = []; |
| 160 | // QueryInterface::execute() returns an integer for count queries or an |
| 161 | // array of ids. |
| 162 | /** @var array $revisions */ |
| 163 | $revisions = $this->getAllRevisionsQuery($entity)->execute(); |
| 164 | $revisions = $this->loadMultipleRevisions(\array_keys($revisions)); |
| 165 | |
| 166 | foreach ($revisions as $step) { |
| 166 | foreach ($revisions as $step) { |
| 170 | $user_id = (int) $step->getRevisionUserId(); |
| 171 | |
| 172 | if (!isset($users[$user_id]) || $step->getRevisionCreationTime() > $users[$user_id]) { |
| 172 | if (!isset($users[$user_id]) || $step->getRevisionCreationTime() > $users[$user_id]) { |
| 172 | if (!isset($users[$user_id]) || $step->getRevisionCreationTime() > $users[$user_id]) { |
| 166 | foreach ($revisions as $step) { |
| 167 | /** @var \Drupal\display_builder\InstanceInterface $step */ |
| 168 | // User ID is 0 when anonymous and NULL if not set or user was deleted. |
| 169 | // Let's standardize around anonymous so every revision has an user. |
| 170 | $user_id = (int) $step->getRevisionUserId(); |
| 171 | |
| 172 | if (!isset($users[$user_id]) || $step->getRevisionCreationTime() > $users[$user_id]) { |
| 173 | $users[$user_id] = (int) $step->getRevisionCreationTime(); |
| 166 | foreach ($revisions as $step) { |
| 166 | foreach ($revisions as $step) { |
| 167 | /** @var \Drupal\display_builder\InstanceInterface $step */ |
| 168 | // User ID is 0 when anonymous and NULL if not set or user was deleted. |
| 169 | // Let's standardize around anonymous so every revision has an user. |
| 170 | $user_id = (int) $step->getRevisionUserId(); |
| 171 | |
| 172 | if (!isset($users[$user_id]) || $step->getRevisionCreationTime() > $users[$user_id]) { |
| 173 | $users[$user_id] = (int) $step->getRevisionCreationTime(); |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | return $users; |
| 178 | } |
| 58 | public function redo(RevisionableInterface $entity): RevisionableInterface { |
| 59 | $future = $this->getFuture($entity); |
| 60 | |
| 61 | if (\count($future) === 0) { |
| 62 | return $entity; |
| 66 | $next = $future[\array_key_first($future)] ?? NULL; |
| 67 | |
| 68 | if ($next) { |
| 69 | $next->isDefaultRevision(TRUE); |
| 70 | $next->save(); |
| 71 | $entity->isDefaultRevision(FALSE); |
| 72 | $entity->save(); |
| 73 | } |
| 74 | |
| 75 | return $next; |
| 75 | return $next; |
| 76 | } |
| 230 | private function trimPast(RevisionableInterface $entity): void { |
| 231 | $past = $this->getPast($entity); |
| 232 | |
| 233 | if (\count($past) > self::MAX_HISTORY) { |
| 234 | $oldest_revision_id = \array_key_first($past); |
| 235 | $this->deleteRevision($oldest_revision_id); |
| 236 | } |
| 237 | } |
| 237 | } |
| 29 | public function undo(RevisionableInterface $entity): RevisionableInterface { |
| 30 | $past = $this->getPast($entity); |
| 31 | |
| 32 | if (\count($past) === 0) { |
| 33 | return $entity; |
| 37 | $previous = $past[\array_key_last($past)] ?? NULL; |
| 38 | |
| 39 | if ($previous) { |
| 40 | $previous->isDefaultRevision(TRUE); |
| 41 | $previous->save(); |
| 42 | $entity->isDefaultRevision(FALSE); |
| 43 | $entity->save(); |
| 44 | } |
| 45 | |
| 46 | return $previous; |
| 46 | return $previous; |
| 47 | } |