mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-07 21:33:18 +01:00
Add marker for auditable models without owner
Models implementing AuditableWithoutOwner intentionally have no audit owner, either because they belong to neither an organization nor a user, or because they and their audits have to be kept when the owner is deleted (for example billing records). Adds helpers and a scope on the audit model to find audits that are missing an owner although they should have one.
This commit is contained in:
committed by
Constantin Graf
parent
30a90f80e0
commit
f921452419
@@ -4,9 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Concerns\AuditableWithoutOwner;
|
||||||
use Database\Factories\AuditFactory;
|
use Database\Factories\AuditFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use OwenIt\Auditing\Models\Audit as PackageAuditModel;
|
use OwenIt\Auditing\Models\Audit as PackageAuditModel;
|
||||||
|
|
||||||
@@ -31,6 +34,7 @@ use OwenIt\Auditing\Models\Audit as PackageAuditModel;
|
|||||||
* @property-read Organization|null $ownerOrganization
|
* @property-read Organization|null $ownerOrganization
|
||||||
*
|
*
|
||||||
* @method static AuditFactory factory()
|
* @method static AuditFactory factory()
|
||||||
|
* @method static Builder<Audit> whereMissingOwner()
|
||||||
*/
|
*/
|
||||||
class Audit extends PackageAuditModel
|
class Audit extends PackageAuditModel
|
||||||
{
|
{
|
||||||
@@ -52,4 +56,41 @@ class Audit extends PackageAuditModel
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Organization::class, 'owner_organization_id');
|
return $this->belongsTo(Organization::class, 'owner_organization_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether audits of the given auditable type intentionally have no owner (see AuditableWithoutOwner).
|
||||||
|
*/
|
||||||
|
public static function isAuditableTypeWithoutOwner(string $auditableType): bool
|
||||||
|
{
|
||||||
|
$modelClass = Relation::getMorphedModel($auditableType) ?? $auditableType;
|
||||||
|
|
||||||
|
return is_subclass_of($modelClass, AuditableWithoutOwner::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auditable types (morph aliases) whose audits intentionally have no owner (see AuditableWithoutOwner).
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function getAuditableTypesWithoutOwner(): array
|
||||||
|
{
|
||||||
|
return collect(Relation::morphMap())
|
||||||
|
->filter(fn (string $modelClass): bool => is_subclass_of($modelClass, AuditableWithoutOwner::class))
|
||||||
|
->keys()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audits that have neither an owner organization nor an owner user, although their auditable type should have one.
|
||||||
|
* These are audits whose owner no longer exists or could not be determined (yet).
|
||||||
|
*
|
||||||
|
* @param Builder<Audit> $builder
|
||||||
|
*/
|
||||||
|
public function scopeWhereMissingOwner(Builder $builder): void
|
||||||
|
{
|
||||||
|
$builder->whereNull('owner_organization_id')
|
||||||
|
->whereNull('owner_user_id')
|
||||||
|
->whereNotIn('auditable_type', self::getAuditableTypesWithoutOwner());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
app/Models/Concerns/AuditableWithoutOwner.php
Normal file
14
app/Models/Concerns/AuditableWithoutOwner.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models\Concerns;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks an auditable model whose audits intentionally have no owner (neither an organization nor a user),
|
||||||
|
* either because the model belongs to neither of them, or because the model and its audits have to be kept
|
||||||
|
* when its organization or user is deleted (for example billing records).
|
||||||
|
* Audits of these models are not deleted together with an organization or user,
|
||||||
|
* and are not considered as missing an owner (for example by the audit backfill command).
|
||||||
|
*/
|
||||||
|
interface AuditableWithoutOwner {}
|
||||||
@@ -5,12 +5,15 @@ declare(strict_types=1);
|
|||||||
namespace Tests\Unit\Model;
|
namespace Tests\Unit\Model;
|
||||||
|
|
||||||
use App\Models\Audit;
|
use App\Models\Audit;
|
||||||
|
use App\Models\Concerns\AuditableWithoutOwner;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\ProjectMember;
|
use App\Models\ProjectMember;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
|
||||||
#[CoversClass(Audit::class)]
|
#[CoversClass(Audit::class)]
|
||||||
@@ -119,4 +122,52 @@ class AuditModelTest extends ModelTestAbstract
|
|||||||
$this->assertSame(0, Audit::query()->where('auditable_id', $organization->getKey())->count());
|
$this->assertSame(0, Audit::query()->where('auditable_id', $organization->getKey())->count());
|
||||||
$this->assertSame(1, Audit::query()->where('auditable_id', $otherUser->getKey())->count());
|
$this->assertSame(1, Audit::query()->where('auditable_id', $otherUser->getKey())->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_auditable_types_without_owner_are_determined_by_the_marker_interface(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$originalMorphMap = Relation::morphMap();
|
||||||
|
$modelWithoutOwner = new class extends Model implements AuditableWithoutOwner {};
|
||||||
|
Relation::morphMap(['model-without-owner' => $modelWithoutOwner::class]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Act
|
||||||
|
$typesWithoutOwner = Audit::getAuditableTypesWithoutOwner();
|
||||||
|
$isWithoutOwner = Audit::isAuditableTypeWithoutOwner('model-without-owner');
|
||||||
|
$isTimeEntryWithoutOwner = Audit::isAuditableTypeWithoutOwner((new TimeEntry)->getMorphClass());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertContains('model-without-owner', $typesWithoutOwner);
|
||||||
|
$this->assertNotContains((new TimeEntry)->getMorphClass(), $typesWithoutOwner);
|
||||||
|
$this->assertTrue($isWithoutOwner);
|
||||||
|
$this->assertFalse($isTimeEntryWithoutOwner);
|
||||||
|
} finally {
|
||||||
|
Relation::morphMap($originalMorphMap, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scope_where_missing_owner_only_returns_audits_without_owner_whose_type_should_have_one(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$originalMorphMap = Relation::morphMap();
|
||||||
|
$modelWithoutOwner = new class extends Model implements AuditableWithoutOwner {};
|
||||||
|
Relation::morphMap(['model-without-owner' => $modelWithoutOwner::class]);
|
||||||
|
$organization = Organization::factory()->create();
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Audit::query()->delete();
|
||||||
|
$missingOwnerAudit = Audit::factory()->create(['auditable_type' => (new TimeEntry)->getMorphClass()]);
|
||||||
|
Audit::factory()->create(['auditable_type' => (new TimeEntry)->getMorphClass(), 'owner_organization_id' => $organization->getKey()]);
|
||||||
|
Audit::factory()->create(['owner_user_id' => $user->getKey()]);
|
||||||
|
Audit::factory()->create(['auditable_type' => 'model-without-owner']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Act
|
||||||
|
$auditIds = Audit::query()->whereMissingOwner()->pluck('id')->all();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertSame([$missingOwnerAudit->getKey()], $auditIds);
|
||||||
|
} finally {
|
||||||
|
Relation::morphMap($originalMorphMap, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user