Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
40 / 40
58.62% covered (warning)
58.62%
17 / 29
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
InstanceStorage
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
40 / 40
58.62% covered (warning)
58.62%
17 / 29
100.00% covered (success)
100.00%
11 / 11
64.81
100.00% covered (success)
100.00%
1 / 1
 undo
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
5 / 5
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
3.33
 redo
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
5 / 5
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
3.33
 clear
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
4 / 4
33.33% covered (danger)
33.33%
1 / 3
100.00% covered (success)
100.00%
1 / 1
3.19
 getPast
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFuture
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUsers
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
8 / 8
0.00% covered (danger)
0.00%
0 / 6
100.00% covered (success)
100.00%
1 / 1
4
 doPreSave
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 doPostSave
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getAllRevisionsQuery
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearFuture
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
4 / 4
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 trimPast
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Entity;
6
7use Drupal\Core\Entity\EntityInterface;
8use Drupal\Core\Entity\Query\QueryInterface;
9use Drupal\Core\Entity\RevisionableInterface;
10use Drupal\Core\Entity\RevisionLogInterface;
11use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
12
13/**
14 * Instance entity storage handler.
15 */
16class 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}

Branches

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.

InstanceStorage->clear
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  }
InstanceStorage->clearFuture
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  }
InstanceStorage->doPostSave
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  }
InstanceStorage->doPreSave
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  }
InstanceStorage->getAllRevisionsQuery
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  }
InstanceStorage->getFuture
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  }
InstanceStorage->getPast
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  }
InstanceStorage->getUsers
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  }
InstanceStorage->redo
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  }
InstanceStorage->trimPast
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  }
InstanceStorage->undo
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  }