diff --git a/README.md b/README.md index af0ca35..530a673 100755 --- a/README.md +++ b/README.md @@ -171,6 +171,43 @@ withoutRevisions() This query scope is used to query the models without taking revisioning into consideration. +### Dynamic Relationships + +Inspired by https://reinink.ca/articles/dynamic-relationships-in-laravel-using-subqueries, this package supplies a few +dynamic relationships as a convenience for navigating through a model's revision history. The following scopes will run +subqueries to get the additional columns and eagerload the corresponding relations, saving you the hassle of caching +them on each of the tables for your revisionable models. As a fallback when these scopes are not applied, we use get +mutators to run queries and fetch the same columns, making sure the relations are always available but at the expense +of running a bit more queries. *NOTE: when applying these scopes, you will have extra columns in your models attributes, +**any update or insert operations will not work.*** + +#### withNewestAt($until, $since) +```php +/** + * @param $until Checkpoint|Carbon|string + * @param $since Checkpoint|Carbon|string + */ +withNewestAt($until = null, $since = null) +``` +This scope will retrieve the id of the newest model given the until / since constraints. Stored in the newest_id +attribute, this allows you to use `->newest()` relation as a quick way to navigate to that model. Defaults to the +newest model in the revision history. + +#### withNewest() +This scope is a shortcut of `withNewestAt` with the default parameters. Uses the same attribute, mutator and relation. + +#### withInitial() +This scope will retrieve the id of the initial model from its revision history. Stored in the initial_id attribute, +this allows you to use `->initial()` relation as a quick way to navigate to that first item in the revision history. + +#### withPrevious() +This scope will retrieve the id of the previous model from its revision history. Stored in the previous_id attribute, +this allows you to use `->previous()` relation as a quick way to navigate to that previous item in the revision history. + +#### withNext() +This scope will retrieve the id of the next model from its revision history. Stored in the next_id attribute, +this allows you to use `->next()` relation as a quick way to navigate to that next item in the revision history. + ### Revision Metadata & Uniqueness As a workaround to some package compatibility issues, this package offers a convenient way to store the values of some columns as ```metadata``` on the ```revisions``` table. The primary use-case for this feature is to deal with columns or diff --git a/src/Concerns/HasRevisions.php b/src/Concerns/HasRevisions.php index fe1a701..b32e9e3 100755 --- a/src/Concerns/HasRevisions.php +++ b/src/Concerns/HasRevisions.php @@ -247,8 +247,8 @@ public function getNewestIdAttribute($value) if ($value !== null || array_key_exists('newest_id', $this->attributes)) { return $value; } - // dependency on latest boolean column, alternative to using max id - return $this->revisions()->where('latest', true)->first()->revisionable_id; + // when value isn't set by extra subselect scope, fetch from relations + return $this->revision->newest->revisionable_id ?? null; } @@ -413,9 +413,7 @@ public function performRevision() protected function replicateRelationsTo(Model $copy) { $relationHelper = resolve(RelationHelper::class); - - $excluded = $this->getExcludedRelations(); - $relations = collect($relationHelper::getModelRelations($this))->map->type->except($excluded); + $relations = collect($relationHelper::getModelRelations($this))->map->type; foreach ($relations as $relation => $type) { $shortType = substr($type, strrpos($type, '\\') + 1); diff --git a/src/Helpers/RelationHelper.php b/src/Helpers/RelationHelper.php index 5a53a75..a4a9cfb 100644 --- a/src/Helpers/RelationHelper.php +++ b/src/Helpers/RelationHelper.php @@ -161,11 +161,12 @@ public static function isChildMultiple(string $relation): bool * Not just the eager loaded ones present in the $relations Eloquent property. * * @param Model|class-string $model + * @param array $except * @param bool $refresh * @return array * @throws ReflectionException */ - public static function getModelRelations($model, bool $refresh = false): array + public static function getModelRelations($model, array $except = [], bool $refresh = false): array { /** @var class-string $class */ $class = ($model instanceof Model) ? get_class($model) : $model; @@ -176,6 +177,10 @@ public static function getModelRelations($model, bool $refresh = false): array return static::$relations[$class]; } + if (method_exists($model, 'getExcludedRelations')) { + $except = array_merge($except, $model->getExcludedRelations()); + } + static::$relations[$class] = []; foreach (get_class_methods($model) as $method) { @@ -199,7 +204,7 @@ public static function getModelRelations($model, bool $refresh = false): array $code = substr($code, $begin, strrpos($code, '}') - $begin + 1); foreach (static::$relationTypes as $type) { - if (stripos($code, '$this->'.$type.'(')) { + if (stripos($code, '$this->'.$type.'(') && !in_array($method, $except, true)) { $relation = $model->$method(); if ($relation instanceof Relation) { diff --git a/src/Models/Revision.php b/src/Models/Revision.php index 4aa9c25..49fc727 100644 --- a/src/Models/Revision.php +++ b/src/Models/Revision.php @@ -26,6 +26,7 @@ * @property-read int|null $all_revisions_count * @property-read \Illuminate\Database\Eloquent\Collection|Revision[] $otherRevisions * @property-read int|null $other_revisions_count + * @property-read Revision|null $newest * @property-read Revision|null $next * @property-read Revision|null $previous * @property-read Checkpoint|null $checkpoint @@ -234,6 +235,18 @@ public function next(): HasOne return $this->hasOne(static::class, 'previous_revision_id', $this->getKeyName()); } + /** + * Return latest revision + * + * @return HasOne + */ + public function newest(): HasOne + { + return $this->hasOne(static::class, 'revisionable_type', 'revisionable_type') + ->where('original_revisionable_id', $this->original_revisionable_id) + ->where('latest', true)->latest(); + } + /** * Returns true if this is the most current revision for an item * diff --git a/tests/Feature/RevisionObservablesTest.php b/tests/Feature/RevisionObservablesTest.php index ece755b..58d90a2 100644 --- a/tests/Feature/RevisionObservablesTest.php +++ b/tests/Feature/RevisionObservablesTest.php @@ -141,8 +141,14 @@ public function force_deleted_posts_preserves_revision_history(): void $r1->refresh(); $this->assertEquals(0, $r1->next()->count()); $this->assertNull($r1->previous_revision_id); - $this->assertCount(1, Post::withoutRevisions()->get()); + $posts = Post::withoutRevisions()->get(); + $this->assertCount(1, $posts); $this->assertCount(1, Revision::all()); + $post = $posts->first(); + $post->forceDelete(); + $this->assertCount(0, $r1->otherRevisions()->get()); + $this->assertNull(null, $post->newest()->get()); + $this->assertCount(0, Post::withoutRevisions()->get()); } /**