-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathRevision.php
More file actions
293 lines (249 loc) · 7.81 KB
/
Copy pathRevision.php
File metadata and controls
293 lines (249 loc) · 7.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
<?php
namespace TestMonitor\Revisable\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\Relations\Relation;
use TestMonitor\Revisable\Contracts\Revision as RevisionContract;
use TestMonitor\Revisable\Diff;
use TestMonitor\Revisable\Enums\RevisionType;
use TestMonitor\Revisable\RelationType;
use TestMonitor\Revisable\RevisableServiceProvider;
class Revision extends Model implements RevisionContract
{
protected $table = 'revisions';
protected $fillable = [
'name',
'metadata',
'properties',
'changed',
'type',
'revisionable_id',
'revisionable_type',
'user_id',
];
protected $casts = [
'metadata' => 'array',
'properties' => 'array',
'changed' => 'array',
'type' => RevisionType::class,
];
/**
* Get the model that this revision belongs to.
*
* @return MorphTo<Model, $this>
*/
public function revisionable(): MorphTo
{
return $this->morphTo();
}
/**
* Get the user that created this revision.
*
* @return BelongsTo<Model, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(RevisableServiceProvider::determineUserModel(), 'user_id');
}
/**
* Scope revisions to those created by the given user.
*/
#[Scope]
protected function forUser(Builder $query, Model $user): void
{
$query->where('user_id', $user->id);
}
/**
* Scope revisions to those belonging to a specific model instance.
*/
#[Scope]
protected function forModel(Builder $query, Model $model): void
{
$query->where([
'revisionable_id' => $model->getKey(),
'revisionable_type' => get_class($model),
]);
}
/**
* Scope revisions to those that are not rollback revisions.
*/
#[Scope]
protected function notRollback(Builder $query): void
{
$query->whereNot('type', RevisionType::Rollback->value);
}
/**
* Scope revisions to those that are rollback revisions.
*/
#[Scope]
protected function onlyRollbacks(Builder $query): void
{
$query->where('type', RevisionType::Rollback->value);
}
/**
* Scope revisions to those that are initial revisions.
*/
#[Scope]
protected function onlyInitial(Builder $query): void
{
$query->where('type', RevisionType::Initial->value);
}
/**
* Determine whether this is a regular revision.
*/
public function isDefault(): bool
{
return $this->type === RevisionType::Default;
}
/**
* Determine whether this revision was created on model creation.
*/
public function isInitial(): bool
{
return $this->type === RevisionType::Initial;
}
/**
* Determine whether this revision was created after a rollback.
*/
public function isRollback(): bool
{
return $this->type === RevisionType::Rollback;
}
/**
* Merge one or more key/value pairs into the properties column and save.
*/
public function setProperties(array $properties): static
{
$this->fill(['properties' => [...($this->properties ?? []), ...$properties]])->save();
return $this;
}
/**
* Set a single key/value pair on the properties column and save.
*/
public function setProperty(string $key, mixed $value): static
{
return $this->setProperties([$key => $value]);
}
/**
* Remove all properties from the properties column and save.
*/
public function clearProperties(): static
{
$this->fill(['properties' => []])->save();
return $this;
}
/**
* Remove a single key from the properties column and save.
*/
public function removeProperty(string $key): static
{
$properties = $this->properties ?? [];
unset($properties[$key]);
$this->fill(['properties' => $properties])->save();
return $this;
}
/**
* Return the revision that directly precedes this one for the same model.
*/
public function previous(): ?static
{
return static::query()
->where([
['revisionable_type', $this->revisionable_type],
['revisionable_id', $this->revisionable_id],
[$this->getKeyName(), '<', $this->getKey()],
])
->latest($this->getKeyName())
->first();
}
/**
* Return the revision that directly follows this one for the same model.
*/
public function next(): ?static
{
return static::query()
->where([
['revisionable_type', $this->revisionable_type],
['revisionable_id', $this->revisionable_id],
[$this->getKeyName(), '>', $this->getKey()],
])
->oldest($this->getKeyName())
->first();
}
/**
* Determine whether this is the oldest revision for its model.
*/
public function isFirstRevision(): bool
{
return $this->exists && $this->previous() === null;
}
/**
* Determine whether this is the most recent revision for its model.
*/
public function isLastRevision(): bool
{
return $this->exists && $this->next() === null;
}
/**
* Compare this revision against another revision or its predecessor.
*/
public function diff(?RevisionContract $target = null): Diff
{
if ($target instanceof RevisionContract) {
return Diff::fromRevisions($this, $target);
}
$previous = $this->previous();
if ($previous === null) {
return Diff::empty();
}
return Diff::fromRevisions($previous, $this);
}
/**
* Reconstruct the revisionable model as it existed at the time of this revision.
*/
public function toModel(): Model
{
$class = Relation::getMorphedModel($this->revisionable_type) ?? $this->revisionable_type;
/** @var Model $model */
$model = new $class;
$attributes = $this->metadata['attributes'] ?? [];
$attributes[$model->getKeyName()] = $this->revisionable_id;
$model->setRawAttributes($attributes);
$model->exists = true;
foreach ($this->metadata['relations'] ?? [] as $name => $data) {
$model->setRelation($name, $this->buildRelatedModels($data));
}
return $model;
}
/**
* Reconstruct related models from stored revision data, returning a single model or collection.
*/
protected function buildRelatedModels(array $data): Model|EloquentCollection|null
{
$relatedClass = $data['class'];
$items = $data['records']['items'] ?? [];
$collection = new EloquentCollection;
foreach ($items as $index => $attributes) {
/** @var Model $related */
$related = new $relatedClass;
$related->setRawAttributes($attributes);
$related->exists = true;
if (RelationType::isPivoted($data['type']) && isset($data['pivots']['items'][$index])) {
$pivot = new Pivot;
$pivot->setRawAttributes($data['pivots']['items'][$index]);
$pivot->exists = true;
$related->setRelation('pivot', $pivot);
}
$collection->push($related);
}
if (RelationType::isSingular($data['type'])) {
return $collection->first();
}
return $collection;
}
}