Skip to content

Commit

Permalink
Allow adding versions manually (#44)
Browse files Browse the repository at this point in the history
* feat: Allow adding versions later on

* fix: Merge `attributes` in snapshot mode

* Update Version.php

Co-authored-by: 安正超 <[email protected]>
  • Loading branch information
marijoo and overtrue authored Oct 7, 2022
1 parent 41547ec commit db60651
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 27 deletions.
46 changes: 41 additions & 5 deletions src/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Overtrue\LaravelVersionable;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;

/**
* @property Model|\Overtrue\LaravelVersionable\Versionable $versionable
Expand Down Expand Up @@ -50,9 +52,12 @@ public function versionable(): \Illuminate\Database\Eloquent\Relations\MorphTo
/**
* @param \Illuminate\Database\Eloquent\Model $model
* @param array $attributes
* @param string|DateTimeInterface|null $time
* @return \Overtrue\LaravelVersionable\Version
*
* @throws \Carbon\Exceptions\InvalidFormatException
*/
public static function createForModel(Model $model, array $attributes = []): Version
public static function createForModel(Model $model, array $attributes = [], $time = null): Version
{
/* @var \Overtrue\LaravelVersionable\Versionable|Model $model */
$versionClass = $model->getVersionModel();
Expand All @@ -64,7 +69,11 @@ public static function createForModel(Model $model, array $attributes = []): Ver
$version->versionable_id = $model->getKey();
$version->versionable_type = $model->getMorphClass();
$version->{\config('versionable.user_foreign_key')} = $model->getVersionUserId();
$version->contents = \array_merge($attributes, $model->getVersionableAttributes());
$version->contents = $model->getVersionableAttributes($attributes);

if ($time) {
$version->created_at = Carbon::parse($time);
}

$version->save();

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

public function scopeOrderOldestFirst(Builder $query): Builder
{
return $query->oldest()->oldest('id');
}

public function scopeOrderLatestFirst(Builder $query): Builder
{
return $query->latest()->latest('id');
}

public function previousVersion(): ?static
{
return $this->versionable->versions()->where('id', '<', $this->id)->latest('id')->first();
return $this->versionable->history()
->where(function ($query) {
$query->where('created_at', '<', $this->created_at)
->orWhere(function ($query) {
$query->where('id', '<', $this->getKey())
->where('created_at', '<=', $this->created_at);
});
})
->first();
}

public function nextVersion(): ?static
{
return $this->versionable->versions()->where('id', '>', $this->id)->oldest('id')->first();
return $this->versionable->versions()
->where(function ($query) {
$query->where('created_at', '>', $this->created_at)
->orWhere(function ($query) {
$query->where('id', '>', $this->getKey())
->where('created_at', '>=', $this->created_at);
});
})
->orderOldestFirst()
->first();
}

public function diff(Version $toVersion = null, array $differOptions = [], array $renderOptions = []): Diff
{
if (! $toVersion) {
if (!$toVersion) {
$toVersion = $this->previousVersion() ?? new static();
}

Expand Down
65 changes: 43 additions & 22 deletions src/Versionable.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static function bootVersionable()
{
static::saved(
function (Model $model) {
static::createVersionForModel($model);
$model->autoCreateVersion();
}
);

Expand All @@ -31,39 +31,62 @@ function (Model $model) {
if ($model->forceDeleting) {
$model->forceRemoveAllVersions();
} else {
static::createVersionForModel($model);
$model->autoCreateVersion();
}
}
);
}

private static function createVersionForModel(Model $model): void
private function autoCreateVersion(): ?Version
{
/* @var \Overtrue\LaravelVersionable\Versionable|Model $model */
if (static::$versioning && $model->shouldVersioning()) {
Version::createForModel($model);
$model->removeOldVersions($model->getKeepVersionsCount());
if (static::$versioning) {
return $this->createVersion();
}

return null;
}

/**
* @param array $attributes
* @param string|DateTimeInterface|null $time
* @return ?Version
*
* @throws \Carbon\Exceptions\InvalidFormatException
*/
public function createVersion(array $attributes = [], $time = null): ?Version
{
if ($this->shouldBeVersioning() || !empty($attributes)) {
return tap(Version::createForModel($this, $attributes, $time), function () {
$this->removeOldVersions($this->getKeepVersionsCount());
});
}

return null;
}

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

public function history(): MorphMany
{
return $this->versions()->orderLatestFirst();
}

public function lastVersion(): MorphOne
{
return $this->latestVersion();
}

public function latestVersion(): MorphOne
{
return $this->morphOne($this->getVersionModel(), 'versionable')->latest('id');
return $this->morphOne($this->getVersionModel(), 'versionable')->orderLatestFirst();
}

public function firstVersion(): MorphOne
{
return $this->morphOne($this->getVersionModel(), 'versionable')->oldest('id');
return $this->morphOne($this->getVersionModel(), 'versionable')->orderOldestFirst();
}

/**
Expand All @@ -77,10 +100,8 @@ public function firstVersion(): MorphOne
*/
public function versionAt($time = null, $tz = null): ?Version
{
return $this->versions()
return $this->history()
->where('created_at', '<=', Carbon::parse($time, $tz))
->orderByDesc('created_at')
->orderByDesc($this->getKey())
->first();
}

Expand Down Expand Up @@ -110,7 +131,7 @@ public function removeOldVersions(int $keep = 1): void
return;
}

$this->versions()->skip($keep)->take(PHP_INT_MAX)->get()->each->delete();
$this->history()->skip($keep)->take(PHP_INT_MAX)->get()->each->delete();
}

public function removeVersions(array $ids)
Expand Down Expand Up @@ -155,36 +176,36 @@ public function forceRemoveAllVersions(): void
$this->versions->each->forceDelete();
}

public function shouldVersioning(): bool
public function shouldBeVersioning(): bool
{
return ! empty($this->getVersionableAttributes());
return !empty($this->getVersionableAttributes());
}

public function getVersionableAttributes(): array
public function getVersionableAttributes(array $attributes = []): array
{
$changes = $this->getDirty();

if (empty($changes)) {
if (empty($changes) && empty($attributes)) {
return [];
}

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

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

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

/**
* @throws \Exception
*/
public function setVersionable(array $attributes): static
{
if (! \property_exists($this, 'versionable')) {
if (!\property_exists($this, 'versionable')) {
throw new \Exception('Property $versionable not exist.');
}

Expand All @@ -198,7 +219,7 @@ public function setVersionable(array $attributes): static
*/
public function setDontVersionable(array $attributes): static
{
if (! \property_exists($this, 'dontVersionable')) {
if (!\property_exists($this, 'dontVersionable')) {
throw new \Exception('Property $dontVersionable not exist.');
}

Expand Down Expand Up @@ -227,7 +248,7 @@ public function getVersionStrategy(): string
*/
public function setVersionStrategy(string $strategy): static
{
if (! \property_exists($this, 'versionStrategy')) {
if (!\property_exists($this, 'versionStrategy')) {
throw new \Exception('Property $versionStrategy not exist.');
}

Expand Down
83 changes: 83 additions & 0 deletions tests/FeatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Tests;

use Illuminate\Support\Carbon;
use Overtrue\LaravelVersionable\Diff;
use Overtrue\LaravelVersionable\Version;
use Overtrue\LaravelVersionable\VersionStrategy;

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

/**
* @test
*/
public function user_can_get_previous_version()
{
$post = Post::create(['title' => 'version1', 'content' => 'version1 content']);
$post->update(['title' => 'version2']);
$post->update(['title' => 'version3']);

$post->refresh();

$this->assertEquals('version3', $post->latestVersion->contents['title']);
$this->assertEquals('version2', $post->latestVersion->previousVersion()->contents['title']);
$this->assertEquals('version1', $post->latestVersion->previousVersion()->previousVersion()->contents['title']);
$this->assertNull($post->latestVersion->previousVersion()->previousVersion()->previousVersion());
}

/**
* @test
*/
public function user_can_get_next_version()
{
$post = Post::create(['title' => 'version1', 'content' => 'version1 content']);
$post->update(['title' => 'version2']);
$post->update(['title' => 'version3']);

$post->refresh();

$this->assertEquals('version1', $post->firstVersion->contents['title']);
$this->assertEquals('version2', $post->firstVersion->nextVersion()->contents['title']);
$this->assertEquals('version3', $post->firstVersion->nextVersion()->nextVersion()->contents['title']);
$this->assertNull($post->firstVersion->nextVersion()->nextVersion()->nextVersion());
}

/**
* @test
*/
public function previous_versions_created_later_on_will_have_correct_order()
{
$this->travelTo(Carbon::create(2022, 10, 2, 14, 0));

$post = Post::create(['title' => 'version1', 'content' => 'version1 content']);
$post->update(['title' => 'version2']);

$this->travelTo(Carbon::create(2022, 10, 2, 15, 0));
$post->update(['title' => 'version5']);

$post->refresh();

$post->title = 'version4';
$post->createVersion([], Carbon::create(2022, 10, 2, 14, 30));
$post->createVersion(['title' => 'version3'], Carbon::create(2022, 10, 2, 14, 0));

$post->refresh();

$this->assertEquals('version5', $post->title);
$this->assertEquals('version5', $post->latestVersion->contents['title']);
$this->assertEquals('version4', $post->latestVersion->previousVersion()->contents['title']);
$this->assertEquals('version3', $post->latestVersion->previousVersion()->previousVersion()->contents['title']);
$this->assertEquals('version2', $post->latestVersion->previousVersion()->previousVersion()->previousVersion()->contents['title']);
$this->assertEquals('version1', $post->latestVersion->previousVersion()->previousVersion()->previousVersion()->previousVersion()->contents['title']);
$this->assertNull($post->latestVersion->previousVersion()->previousVersion()->previousVersion()->previousVersion()->previousVersion());
}

/**
* @test
*/
public function user_can_get_ordered_history()
{
$post = Post::create(['title' => 'version2', 'content' => 'version2 content']);
$post->update(['title' => 'version3']);
$post->update(['title' => 'version4']);

$post->createVersion(['title' => 'version1'], Carbon::now()->subDay(1));

$this->assertEquals(
['version4', 'version3', 'version2', 'version1'],
$post->history->pluck('contents.title')->toArray(),
);
}

/**
* @test
*/
Expand Down
Loading

0 comments on commit db60651

Please sign in to comment.