Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 5 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Publish the config file and migration:
$ php artisan vendor:publish --provider="TestMonitor\Revisable\RevisableServiceProvider" --tag="config"
$ php artisan vendor:publish --provider="TestMonitor\Revisable\RevisableServiceProvider" --tag="migrations"

Once published, you can configure your user model, revision model, and name generator in `config/revisable.php`.
Once published, you can configure your user model and revision model in `config/revisable.php`.

Run the migration to create the `revisions` table:

Expand Down Expand Up @@ -289,35 +289,15 @@ The window is measured from the revision's last update, so it resets on every sa

The living snapshot captures the post-save state, consistent with normal revision behaviour. After two saves in draft, the snapshot holds the state of the most recent save, which serves as the rollback point.

#### Custom revision naming
#### Version numbers

The default `VersionNameGenerator` names revisions sequentially (v1, v2, …). You can provide your own generator by implementing the `NameGenerator` contract and registering it in the options:
Every revision automatically gets a sequential `version` number (1, 2, 3, …), scoped to its model instance. Unlike a generated name, the version is a plain integer, so it carries no language or formatting choices — prefix it with whatever translated string fits your application:

Comment on lines +292 to 295

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No release yet, so no worries there.

```php
use TestMonitor\Revisable\Contracts\NameGenerator;

class TimestampNameGenerator implements NameGenerator
{
public function generate(Model $model): string
{
return now()->toDateTimeString();
}
}
__('Version :number', ['number' => $revision->version]);
```

```php
public function getRevisionOptions(): RevisableOptions
{
return RevisableOptions::defaults()
->nameRevisionUsing(new TimestampNameGenerator);
}
```

Pass `null` to disable automatic naming entirely:

```php
return RevisableOptions::defaults()->nameRevisionUsing(null);
```
Use `saveAsRevision()` (see below) if you also want a caller-chosen label on top of the version number.

---

Expand Down
7 changes: 0 additions & 7 deletions config/revisionable.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?php

use App\Models\User;
use TestMonitor\Revisable\Generators\VersionNameGenerator;
use TestMonitor\Revisable\Models\Revision;

return [
Expand All @@ -12,12 +11,6 @@
*/
'revision_model' => Revision::class,

/*
* The generator class used to produce a name for each revision.
* Set to null to disable automatic naming.
*/
'name_generator' => VersionNameGenerator::class,

/*
* You can specify an auth driver here that gets user models.
* If this is null we'll use the current Laravel auth driver.
Expand Down
3 changes: 3 additions & 0 deletions database/migrations/create_revisions_table.php.stub
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ return new class extends Migration

$table->morphs('revisionable');
$table->string('name')->nullable();
$table->unsignedInteger('version');
$table->json('metadata')->nullable();
Comment on lines 15 to 18

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

$table->json('properties')->nullable();
$table->json('changed')->nullable();
Expand All @@ -23,6 +24,8 @@ return new class extends Migration
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null')->onUpdate('cascade');

$table->timestamps();

$table->unique(['revisionable_type', 'revisionable_id', 'version']);
});
}

Expand Down
3 changes: 0 additions & 3 deletions src/Concerns/HasRevisions.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ public function createNewRevision(): Revision|bool

$revision = app(Revisioner::class)
->for($this)
->nameUsing($options->nameGenerator)
->onlyFields($options->fields)
->exceptFields($options->exceptFields)
->withRelations($options->relations)
Expand Down Expand Up @@ -232,7 +231,6 @@ public function saveAsRevision(?string $name = null, array $properties = [], ?bo
->for($this)
->name($name)
->properties($properties)
->nameUsing($options->nameGenerator)
->onlyFields($options->fields)
->exceptFields($options->exceptFields)
->withRelations($options->relations)
Expand Down Expand Up @@ -451,7 +449,6 @@ protected function saveAsRollbackRevision(RevisableOptions $options, RevisionCon
{
return app(Revisioner::class)
->for($this)
->nameUsing($options->nameGenerator)
->onlyFields($options->fields)
->exceptFields($options->exceptFields)
->withRelations($options->relations)
Expand Down
10 changes: 0 additions & 10 deletions src/Contracts/NameGenerator.php

This file was deleted.

9 changes: 0 additions & 9 deletions src/Exceptions/InvalidConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Exception;
use Illuminate\Database\Eloquent\Model;
use TestMonitor\Revisable\Contracts\NameGenerator;
use TestMonitor\Revisable\Models\Revision;

class InvalidConfiguration extends Exception
Expand All @@ -23,12 +22,4 @@ public static function invalidUserModel(string $className): self
{
return new static("The given model class `{$className}` does not extend `" . Model::class . '`');
}

public static function invalidNameGenerator(string $className): self
{
return new static(
"The given class `{$className}` does not exist or does not implement `"
. NameGenerator::class . '`'
);
}
}
29 changes: 0 additions & 29 deletions src/Generators/NameGeneratorFactory.php

This file was deleted.

14 changes: 0 additions & 14 deletions src/Generators/VersionNameGenerator.php

This file was deleted.

2 changes: 2 additions & 0 deletions src/Models/Revision.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Revision extends Model implements RevisionContract

protected $fillable = [
'name',
'version',
'metadata',
'properties',
'changed',
Expand All @@ -32,6 +33,7 @@ class Revision extends Model implements RevisionContract
];

protected $casts = [
'version' => 'integer',
'metadata' => 'array',
'properties' => 'array',
'changed' => 'array',
Expand Down
24 changes: 1 addition & 23 deletions src/RevisableOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use TestMonitor\Revisable\Contracts\NameGenerator;
use TestMonitor\Revisable\Contracts\Revision as RevisionContract;
use TestMonitor\Revisable\Generators\NameGeneratorFactory;

class RevisableOptions
{
Expand Down Expand Up @@ -76,22 +74,12 @@ class RevisableOptions
*/
public ?array $exceptRestoringRelations = [];

/**
* The generator used to produce a name for each revision.
* Defaults to the generator configured in config/revisable.php. Set to null to disable auto-naming.
*/
public ?NameGenerator $nameGenerator = null;

/**
* Start configuring model with the default options.
*/
public static function defaults(): self
{
$options = new static;

$options->nameGenerator = NameGeneratorFactory::create();

return $options;
return new static;
}

/**
Expand Down Expand Up @@ -223,14 +211,4 @@ public function withoutRestoringRelations(string ...$relations): self

return $this;
}

/**
* Set a custom name generator for revisions. Pass null to disable auto-naming.
*/
public function nameRevisionUsing(?NameGenerator $generator): self
{
$this->nameGenerator = $generator;

return $this;
}
}
25 changes: 8 additions & 17 deletions src/Revisioner.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Traits\Conditionable;
use TestMonitor\Revisable\Contracts\NameGenerator;
use TestMonitor\Revisable\Contracts\Revision as RevisionContract;
use TestMonitor\Revisable\Enums\RevisionType;
use TestMonitor\Revisable\Models\Revision;
Expand All @@ -34,8 +33,6 @@ class Revisioner

protected ?array $exceptRestoringRelations = [];

protected ?NameGenerator $nameGenerator = null;

protected RevisionType $revisionType = RevisionType::Default;

public function __construct(protected UserResolver $userResolver) {}
Expand Down Expand Up @@ -68,13 +65,6 @@ public function name(?string $name): static
return $this;
}

public function nameUsing(?NameGenerator $generator): static
{
$this->nameGenerator = $generator;

return $this;
}

public function onlyFields(array $fields): static
{
$this->fields = $fields;
Expand Down Expand Up @@ -118,7 +108,7 @@ public function build(): Revision
$revision = new Revision;

$revision->user_id = $this->userResolver->resolve();
$revision->name = $this->resolveName();
$revision->name = $this->name;
$revision->metadata = $this->buildData();
$revision->changed = $this->buildChanges($revision);
Comment on lines 110 to 113

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

$revision->properties = $this->properties ?: null;
Expand All @@ -134,6 +124,7 @@ public function save(): Revision
{
return DB::transaction(function () {
$revision = $this->build();
$revision->version = $this->resolveVersion();

$this->model->revisions()->save($revision);

Expand Down Expand Up @@ -213,13 +204,13 @@ public function prune(): void
}
}

protected function resolveName(): ?string
/**
* Resolve the sequential version number for the new revision.
* Uses the max version rather than the row count, so pruning never reuses a number.
*/
protected function resolveVersion(): int
{
if ($this->name !== null) {
return $this->name;
}

return $this->nameGenerator?->generate($this->model);
return ($this->model->revisions()->max('version') ?? 0) + 1;
}
Comment on lines +211 to 214

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


/**
Expand Down
54 changes: 0 additions & 54 deletions tests/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

use PHPUnit\Framework\Attributes\Test;
use TestMonitor\Revisable\Exceptions\InvalidConfiguration;
use TestMonitor\Revisable\Generators\NameGeneratorFactory;
use TestMonitor\Revisable\Generators\VersionNameGenerator;
use TestMonitor\Revisable\Models\Revision;
use TestMonitor\Revisable\RevisableServiceProvider;
use TestMonitor\Revisable\Tests\Models\Author;
Expand Down Expand Up @@ -48,58 +46,6 @@ public function it_throws_when_the_configured_revision_model_class_does_not_exis
RevisableServiceProvider::determineRevisionModel();
}

// Name generator

#[Test]
public function it_returns_null_when_no_name_generator_is_configured()
{
// Given
config()->set('revisable.name_generator', null);

// When
$generator = NameGeneratorFactory::create();

// Then
$this->assertNull($generator);
}

#[Test]
public function it_creates_a_generator_instance_when_the_class_is_valid()
{
// Given
config()->set('revisable.name_generator', VersionNameGenerator::class);

// When
$generator = NameGeneratorFactory::create();

// Then
$this->assertInstanceOf(VersionNameGenerator::class, $generator);
}

#[Test]
public function it_throws_when_the_configured_name_generator_does_not_implement_the_contract()
{
// Given
config()->set('revisable.name_generator', \stdClass::class);

// When / Then
$this->expectException(InvalidConfiguration::class);

NameGeneratorFactory::create();
}

#[Test]
public function it_throws_when_the_configured_name_generator_class_does_not_exist()
{
// Given
config()->set('revisable.name_generator', 'App\\Generators\\NonExistentGenerator');

// When / Then
$this->expectException(InvalidConfiguration::class);

NameGeneratorFactory::create();
}

// User model

#[Test]
Expand Down
Loading