Skip to content

Commit db60651

Browse files
marijooovertrue
andauthored
Allow adding versions manually (#44)
* feat: Allow adding versions later on * fix: Merge `attributes` in snapshot mode * Update Version.php Co-authored-by: 安正超 <[email protected]>
1 parent 41547ec commit db60651

File tree

4 files changed

+307
-27
lines changed

4 files changed

+307
-27
lines changed

src/Version.php

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace Overtrue\LaravelVersionable;
44

5+
use Illuminate\Database\Eloquent\Builder;
56
use Illuminate\Database\Eloquent\Model;
67
use Illuminate\Database\Eloquent\SoftDeletes;
8+
use Illuminate\Support\Carbon;
79

810
/**
911
* @property Model|\Overtrue\LaravelVersionable\Versionable $versionable
@@ -50,9 +52,12 @@ public function versionable(): \Illuminate\Database\Eloquent\Relations\MorphTo
5052
/**
5153
* @param \Illuminate\Database\Eloquent\Model $model
5254
* @param array $attributes
55+
* @param string|DateTimeInterface|null $time
5356
* @return \Overtrue\LaravelVersionable\Version
57+
*
58+
* @throws \Carbon\Exceptions\InvalidFormatException
5459
*/
55-
public static function createForModel(Model $model, array $attributes = []): Version
60+
public static function createForModel(Model $model, array $attributes = [], $time = null): Version
5661
{
5762
/* @var \Overtrue\LaravelVersionable\Versionable|Model $model */
5863
$versionClass = $model->getVersionModel();
@@ -64,7 +69,11 @@ public static function createForModel(Model $model, array $attributes = []): Ver
6469
$version->versionable_id = $model->getKey();
6570
$version->versionable_type = $model->getMorphClass();
6671
$version->{\config('versionable.user_foreign_key')} = $model->getVersionUserId();
67-
$version->contents = \array_merge($attributes, $model->getVersionableAttributes());
72+
$version->contents = $model->getVersionableAttributes($attributes);
73+
74+
if ($time) {
75+
$version->created_at = Carbon::parse($time);
76+
}
6877

6978
$version->save();
7079

@@ -81,19 +90,46 @@ public function revertWithoutSaving(): ?Model
8190
return $this->versionable->forceFill($this->contents);
8291
}
8392

93+
public function scopeOrderOldestFirst(Builder $query): Builder
94+
{
95+
return $query->oldest()->oldest('id');
96+
}
97+
98+
public function scopeOrderLatestFirst(Builder $query): Builder
99+
{
100+
return $query->latest()->latest('id');
101+
}
102+
84103
public function previousVersion(): ?static
85104
{
86-
return $this->versionable->versions()->where('id', '<', $this->id)->latest('id')->first();
105+
return $this->versionable->history()
106+
->where(function ($query) {
107+
$query->where('created_at', '<', $this->created_at)
108+
->orWhere(function ($query) {
109+
$query->where('id', '<', $this->getKey())
110+
->where('created_at', '<=', $this->created_at);
111+
});
112+
})
113+
->first();
87114
}
88115

89116
public function nextVersion(): ?static
90117
{
91-
return $this->versionable->versions()->where('id', '>', $this->id)->oldest('id')->first();
118+
return $this->versionable->versions()
119+
->where(function ($query) {
120+
$query->where('created_at', '>', $this->created_at)
121+
->orWhere(function ($query) {
122+
$query->where('id', '>', $this->getKey())
123+
->where('created_at', '>=', $this->created_at);
124+
});
125+
})
126+
->orderOldestFirst()
127+
->first();
92128
}
93129

94130
public function diff(Version $toVersion = null, array $differOptions = [], array $renderOptions = []): Diff
95131
{
96-
if (! $toVersion) {
132+
if (!$toVersion) {
97133
$toVersion = $this->previousVersion() ?? new static();
98134
}
99135

src/Versionable.php

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static function bootVersionable()
2121
{
2222
static::saved(
2323
function (Model $model) {
24-
static::createVersionForModel($model);
24+
$model->autoCreateVersion();
2525
}
2626
);
2727

@@ -31,39 +31,62 @@ function (Model $model) {
3131
if ($model->forceDeleting) {
3232
$model->forceRemoveAllVersions();
3333
} else {
34-
static::createVersionForModel($model);
34+
$model->autoCreateVersion();
3535
}
3636
}
3737
);
3838
}
3939

40-
private static function createVersionForModel(Model $model): void
40+
private function autoCreateVersion(): ?Version
4141
{
42-
/* @var \Overtrue\LaravelVersionable\Versionable|Model $model */
43-
if (static::$versioning && $model->shouldVersioning()) {
44-
Version::createForModel($model);
45-
$model->removeOldVersions($model->getKeepVersionsCount());
42+
if (static::$versioning) {
43+
return $this->createVersion();
4644
}
45+
46+
return null;
47+
}
48+
49+
/**
50+
* @param array $attributes
51+
* @param string|DateTimeInterface|null $time
52+
* @return ?Version
53+
*
54+
* @throws \Carbon\Exceptions\InvalidFormatException
55+
*/
56+
public function createVersion(array $attributes = [], $time = null): ?Version
57+
{
58+
if ($this->shouldBeVersioning() || !empty($attributes)) {
59+
return tap(Version::createForModel($this, $attributes, $time), function () {
60+
$this->removeOldVersions($this->getKeepVersionsCount());
61+
});
62+
}
63+
64+
return null;
4765
}
4866

4967
public function versions(): MorphMany
5068
{
5169
return $this->morphMany($this->getVersionModel(), 'versionable');
5270
}
5371

72+
public function history(): MorphMany
73+
{
74+
return $this->versions()->orderLatestFirst();
75+
}
76+
5477
public function lastVersion(): MorphOne
5578
{
5679
return $this->latestVersion();
5780
}
5881

5982
public function latestVersion(): MorphOne
6083
{
61-
return $this->morphOne($this->getVersionModel(), 'versionable')->latest('id');
84+
return $this->morphOne($this->getVersionModel(), 'versionable')->orderLatestFirst();
6285
}
6386

6487
public function firstVersion(): MorphOne
6588
{
66-
return $this->morphOne($this->getVersionModel(), 'versionable')->oldest('id');
89+
return $this->morphOne($this->getVersionModel(), 'versionable')->orderOldestFirst();
6790
}
6891

6992
/**
@@ -77,10 +100,8 @@ public function firstVersion(): MorphOne
77100
*/
78101
public function versionAt($time = null, $tz = null): ?Version
79102
{
80-
return $this->versions()
103+
return $this->history()
81104
->where('created_at', '<=', Carbon::parse($time, $tz))
82-
->orderByDesc('created_at')
83-
->orderByDesc($this->getKey())
84105
->first();
85106
}
86107

@@ -110,7 +131,7 @@ public function removeOldVersions(int $keep = 1): void
110131
return;
111132
}
112133

113-
$this->versions()->skip($keep)->take(PHP_INT_MAX)->get()->each->delete();
134+
$this->history()->skip($keep)->take(PHP_INT_MAX)->get()->each->delete();
114135
}
115136

116137
public function removeVersions(array $ids)
@@ -155,36 +176,36 @@ public function forceRemoveAllVersions(): void
155176
$this->versions->each->forceDelete();
156177
}
157178

158-
public function shouldVersioning(): bool
179+
public function shouldBeVersioning(): bool
159180
{
160-
return ! empty($this->getVersionableAttributes());
181+
return !empty($this->getVersionableAttributes());
161182
}
162183

163-
public function getVersionableAttributes(): array
184+
public function getVersionableAttributes(array $attributes = []): array
164185
{
165186
$changes = $this->getDirty();
166187

167-
if (empty($changes)) {
188+
if (empty($changes) && empty($attributes)) {
168189
return [];
169190
}
170191

171192
$changes = $this->versionableFromArray($changes);
172193
$changedKeys = array_keys($changes);
173194

174-
if ($this->getVersionStrategy() === VersionStrategy::SNAPSHOT && ! empty($changes)) {
195+
if ($this->getVersionStrategy() === VersionStrategy::SNAPSHOT && (!empty($changes) || !empty($attributes))) {
175196
$changedKeys = array_keys($this->getAttributes());
176197
}
177198

178199
// to keep casts and mutators works, we need to get the updated attributes from the model
179-
return $this->only($changedKeys);
200+
return \array_merge($this->only($changedKeys), $attributes);
180201
}
181202

182203
/**
183204
* @throws \Exception
184205
*/
185206
public function setVersionable(array $attributes): static
186207
{
187-
if (! \property_exists($this, 'versionable')) {
208+
if (!\property_exists($this, 'versionable')) {
188209
throw new \Exception('Property $versionable not exist.');
189210
}
190211

@@ -198,7 +219,7 @@ public function setVersionable(array $attributes): static
198219
*/
199220
public function setDontVersionable(array $attributes): static
200221
{
201-
if (! \property_exists($this, 'dontVersionable')) {
222+
if (!\property_exists($this, 'dontVersionable')) {
202223
throw new \Exception('Property $dontVersionable not exist.');
203224
}
204225

@@ -227,7 +248,7 @@ public function getVersionStrategy(): string
227248
*/
228249
public function setVersionStrategy(string $strategy): static
229250
{
230-
if (! \property_exists($this, 'versionStrategy')) {
251+
if (!\property_exists($this, 'versionStrategy')) {
231252
throw new \Exception('Property $versionStrategy not exist.');
232253
}
233254

tests/FeatureTest.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace Tests;
44

5+
use Illuminate\Support\Carbon;
56
use Overtrue\LaravelVersionable\Diff;
7+
use Overtrue\LaravelVersionable\Version;
68
use Overtrue\LaravelVersionable\VersionStrategy;
79

810
class FeatureTest extends TestCase
@@ -146,6 +148,87 @@ public function user_can_get_diff_of_version()
146148
$this->assertSame(['title' => ['old' => 'version1', 'new' => 'version2']], $post->lastVersion->diff()->toArray());
147149
}
148150

151+
/**
152+
* @test
153+
*/
154+
public function user_can_get_previous_version()
155+
{
156+
$post = Post::create(['title' => 'version1', 'content' => 'version1 content']);
157+
$post->update(['title' => 'version2']);
158+
$post->update(['title' => 'version3']);
159+
160+
$post->refresh();
161+
162+
$this->assertEquals('version3', $post->latestVersion->contents['title']);
163+
$this->assertEquals('version2', $post->latestVersion->previousVersion()->contents['title']);
164+
$this->assertEquals('version1', $post->latestVersion->previousVersion()->previousVersion()->contents['title']);
165+
$this->assertNull($post->latestVersion->previousVersion()->previousVersion()->previousVersion());
166+
}
167+
168+
/**
169+
* @test
170+
*/
171+
public function user_can_get_next_version()
172+
{
173+
$post = Post::create(['title' => 'version1', 'content' => 'version1 content']);
174+
$post->update(['title' => 'version2']);
175+
$post->update(['title' => 'version3']);
176+
177+
$post->refresh();
178+
179+
$this->assertEquals('version1', $post->firstVersion->contents['title']);
180+
$this->assertEquals('version2', $post->firstVersion->nextVersion()->contents['title']);
181+
$this->assertEquals('version3', $post->firstVersion->nextVersion()->nextVersion()->contents['title']);
182+
$this->assertNull($post->firstVersion->nextVersion()->nextVersion()->nextVersion());
183+
}
184+
185+
/**
186+
* @test
187+
*/
188+
public function previous_versions_created_later_on_will_have_correct_order()
189+
{
190+
$this->travelTo(Carbon::create(2022, 10, 2, 14, 0));
191+
192+
$post = Post::create(['title' => 'version1', 'content' => 'version1 content']);
193+
$post->update(['title' => 'version2']);
194+
195+
$this->travelTo(Carbon::create(2022, 10, 2, 15, 0));
196+
$post->update(['title' => 'version5']);
197+
198+
$post->refresh();
199+
200+
$post->title = 'version4';
201+
$post->createVersion([], Carbon::create(2022, 10, 2, 14, 30));
202+
$post->createVersion(['title' => 'version3'], Carbon::create(2022, 10, 2, 14, 0));
203+
204+
$post->refresh();
205+
206+
$this->assertEquals('version5', $post->title);
207+
$this->assertEquals('version5', $post->latestVersion->contents['title']);
208+
$this->assertEquals('version4', $post->latestVersion->previousVersion()->contents['title']);
209+
$this->assertEquals('version3', $post->latestVersion->previousVersion()->previousVersion()->contents['title']);
210+
$this->assertEquals('version2', $post->latestVersion->previousVersion()->previousVersion()->previousVersion()->contents['title']);
211+
$this->assertEquals('version1', $post->latestVersion->previousVersion()->previousVersion()->previousVersion()->previousVersion()->contents['title']);
212+
$this->assertNull($post->latestVersion->previousVersion()->previousVersion()->previousVersion()->previousVersion()->previousVersion());
213+
}
214+
215+
/**
216+
* @test
217+
*/
218+
public function user_can_get_ordered_history()
219+
{
220+
$post = Post::create(['title' => 'version2', 'content' => 'version2 content']);
221+
$post->update(['title' => 'version3']);
222+
$post->update(['title' => 'version4']);
223+
224+
$post->createVersion(['title' => 'version1'], Carbon::now()->subDay(1));
225+
226+
$this->assertEquals(
227+
['version4', 'version3', 'version2', 'version1'],
228+
$post->history->pluck('contents.title')->toArray(),
229+
);
230+
}
231+
149232
/**
150233
* @test
151234
*/

0 commit comments

Comments
 (0)