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}