mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-07 21:33:18 +01:00
Add audit owner through parent model
Models that belong to an organization via a parent model (for example project members via their project) implement AuditableThroughParent and return the parent relation. The owner organization of their audits is taken from the loaded parent if it matches the foreign key, otherwise it is queried. The declaration is also used to backfill the owner of existing audits.
This commit is contained in:
committed by
Constantin Graf
parent
6fe721fc26
commit
34273a4863
22
app/Models/Concerns/AuditableThroughParent.php
Normal file
22
app/Models/Concerns/AuditableThroughParent.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks an auditable model whose audits are owned by the owner of its parent model,
|
||||||
|
* because the model itself has no organization_id (for example a project member belongs to an organization via its project).
|
||||||
|
* The parent model has to have an organization_id column.
|
||||||
|
* This is used to set the owner of new audits (CustomAuditable) and to backfill the owner of existing audits.
|
||||||
|
*/
|
||||||
|
interface AuditableThroughParent
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<covariant Model, covariant Model>
|
||||||
|
*/
|
||||||
|
public function getAuditParentRelation(): BelongsTo;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Models\Concerns;
|
namespace App\Models\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Facades\Config;
|
use Illuminate\Support\Facades\Config;
|
||||||
use OwenIt\Auditing\Auditable;
|
use OwenIt\Auditing\Auditable;
|
||||||
|
|
||||||
@@ -27,6 +28,36 @@ trait CustomAuditable
|
|||||||
*/
|
*/
|
||||||
public function getAuditOwnerOrganizationId(): ?string
|
public function getAuditOwnerOrganizationId(): ?string
|
||||||
{
|
{
|
||||||
|
if ($this instanceof AuditableThroughParent) {
|
||||||
|
$relation = $this->getAuditParentRelation();
|
||||||
|
$parentId = $this->getAttributes()[$relation->getForeignKeyName()] ?? null;
|
||||||
|
if ($parentId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The loaded parent is only used if it still matches the foreign key and was loaded with the organization
|
||||||
|
$relationName = $relation->getRelationName();
|
||||||
|
if ($this->relationLoaded($relationName)) {
|
||||||
|
$parent = $this->getRelation($relationName);
|
||||||
|
if ($parent instanceof Model
|
||||||
|
&& $parent->getAttribute($relation->getOwnerKeyName()) === $parentId
|
||||||
|
&& array_key_exists('organization_id', $parent->getAttributes())) {
|
||||||
|
/** @var string|null $organizationId */
|
||||||
|
$organizationId = $parent->getAttributes()['organization_id'];
|
||||||
|
|
||||||
|
return $organizationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var string|null $organizationId */
|
||||||
|
$organizationId = $relation->getRelated()->newQuery()
|
||||||
|
->toBase()
|
||||||
|
->where($relation->getOwnerKeyName(), $parentId)
|
||||||
|
->value('organization_id');
|
||||||
|
|
||||||
|
return $organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->getAttributes()['organization_id'] ?? null;
|
return $this->getAttributes()['organization_id'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Concerns\AuditableThroughParent;
|
||||||
use App\Models\Concerns\CustomAuditable;
|
use App\Models\Concerns\CustomAuditable;
|
||||||
use App\Models\Concerns\HasUuids;
|
use App\Models\Concerns\HasUuids;
|
||||||
use Database\Factories\ProjectMemberFactory;
|
use Database\Factories\ProjectMemberFactory;
|
||||||
@@ -29,7 +30,7 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
|||||||
* @method static Builder<ProjectMember> whereBelongsToOrganization(Organization $organization)
|
* @method static Builder<ProjectMember> whereBelongsToOrganization(Organization $organization)
|
||||||
* @method static ProjectMemberFactory factory()
|
* @method static ProjectMemberFactory factory()
|
||||||
*/
|
*/
|
||||||
class ProjectMember extends Model implements AuditableContract
|
class ProjectMember extends Model implements AuditableContract, AuditableThroughParent
|
||||||
{
|
{
|
||||||
use CustomAuditable;
|
use CustomAuditable;
|
||||||
|
|
||||||
@@ -83,11 +84,11 @@ class ProjectMember extends Model implements AuditableContract
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAuditOwnerOrganizationId(): ?string
|
/**
|
||||||
|
* @return BelongsTo<Project, $this>
|
||||||
|
*/
|
||||||
|
public function getAuditParentRelation(): BelongsTo
|
||||||
{
|
{
|
||||||
/** @var string|null $organizationId */
|
return $this->project();
|
||||||
$organizationId = Project::query()->whereKey($this->project_id)->value('organization_id');
|
|
||||||
|
|
||||||
return $organizationId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Models\TimeEntry;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
|
||||||
#[CoversClass(Audit::class)]
|
#[CoversClass(Audit::class)]
|
||||||
@@ -84,6 +85,41 @@ class AuditModelTest extends ModelTestAbstract
|
|||||||
$this->assertNull($audit->owner_user_id);
|
$this->assertNull($audit->owner_user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_owner_organization_through_parent_uses_the_loaded_parent_without_query(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$organization = Organization::factory()->create();
|
||||||
|
$project = Project::factory()->forOrganization($organization)->create();
|
||||||
|
$projectMember = ProjectMember::factory()->forProject($project)->forMember(Member::factory()->forOrganization($organization)->create())->create();
|
||||||
|
$projectMember->load('project');
|
||||||
|
DB::enableQueryLog();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$ownerOrganizationId = $projectMember->getAuditOwnerOrganizationId();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertSame($organization->getKey(), $ownerOrganizationId);
|
||||||
|
$this->assertCount(0, DB::getQueryLog());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_owner_organization_through_parent_ignores_a_loaded_parent_that_does_not_match_the_foreign_key(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$organization = Organization::factory()->create();
|
||||||
|
$otherOrganization = Organization::factory()->create();
|
||||||
|
$project = Project::factory()->forOrganization($organization)->create();
|
||||||
|
$otherProject = Project::factory()->forOrganization($otherOrganization)->create();
|
||||||
|
$projectMember = ProjectMember::factory()->forProject($project)->forMember(Member::factory()->forOrganization($organization)->create())->create();
|
||||||
|
$projectMember->load('project');
|
||||||
|
$projectMember->project_id = $otherProject->getKey();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$ownerOrganizationId = $projectMember->getAuditOwnerOrganizationId();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertSame($otherOrganization->getKey(), $ownerOrganizationId);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_audits_of_an_organization_have_the_organization_itself_as_owner(): void
|
public function test_audits_of_an_organization_have_the_organization_itself_as_owner(): void
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
Reference in New Issue
Block a user