TenantAtlas/apps/platform/app/Support/Resources/ProviderResourceDescriptor.php
ahmido 04d0d6184f feat(resources): implement provider resource identity binding (#452)
Added `ProviderResourceBinding` model, migrations, policies, and supporting framework for canonical resource identity mapping as defined in Spec 381. This provides the structural capability to resolve baseline and posture discrepancies by binding logical entities across source providers to canonical identities.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #452
2026-06-15 18:45:38 +00:00

129 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Resources;
use App\Support\Baselines\SubjectClass;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;
/**
* @implements Arrayable<string, mixed>
*/
final readonly class ProviderResourceDescriptor implements Arrayable, JsonSerializable
{
/**
* @param array<string, string|int|float|bool|null> $sourceReferences
*/
public function __construct(
public ResourceIdentity $identity,
public ?string $displayLabel,
public string $subjectDomain,
public SubjectClass|string $subjectClass,
public string $subjectTypeKey,
public array $sourceReferences = [],
public ?string $fingerprint = null,
public ?string $lastSeenAt = null,
) {}
/**
* @param array<string, string|int|float|bool|null> $sourceReferences
*/
public static function fromIdentity(
ResourceIdentity $identity,
string $subjectDomain,
SubjectClass|string $subjectClass,
string $subjectTypeKey,
?string $displayLabel = null,
array $sourceReferences = [],
?string $fingerprint = null,
?string $lastSeenAt = null,
): self {
return new self(
identity: $identity,
displayLabel: $displayLabel,
subjectDomain: $subjectDomain,
subjectClass: $subjectClass,
subjectTypeKey: $subjectTypeKey,
sourceReferences: self::safeSourceReferences($sourceReferences),
fingerprint: $fingerprint,
lastSeenAt: $lastSeenAt,
);
}
/**
* @param array<string, mixed> $payload
*/
public static function fromArray(array $payload): self
{
$identityPayload = $payload['identity'] ?? [];
return new self(
identity: ResourceIdentity::fromArray(is_array($identityPayload) ? $identityPayload : []),
displayLabel: self::nullableString($payload['display_label'] ?? null),
subjectDomain: (string) ($payload['subject_domain'] ?? ''),
subjectClass: (string) ($payload['subject_class'] ?? ''),
subjectTypeKey: (string) ($payload['subject_type_key'] ?? ''),
sourceReferences: self::safeSourceReferences(is_array($payload['source_references'] ?? null) ? $payload['source_references'] : []),
fingerprint: self::nullableString($payload['fingerprint'] ?? null),
lastSeenAt: self::nullableString($payload['last_seen_at'] ?? null),
);
}
/**
* @return array{
* identity: array<string, mixed>,
* display_label: ?string,
* subject_domain: string,
* subject_class: string,
* subject_type_key: string,
* source_references: array<string, string|int|float|bool|null>,
* fingerprint: ?string,
* last_seen_at: ?string
* }
*/
public function toArray(): array
{
return [
'identity' => $this->identity->toArray(),
'display_label' => $this->displayLabel,
'subject_domain' => $this->subjectDomain,
'subject_class' => $this->subjectClass instanceof SubjectClass ? $this->subjectClass->value : $this->subjectClass,
'subject_type_key' => $this->subjectTypeKey,
'source_references' => self::safeSourceReferences($this->sourceReferences),
'fingerprint' => $this->fingerprint,
'last_seen_at' => $this->lastSeenAt,
];
}
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* @param array<string, mixed> $sourceReferences
* @return array<string, string|int|float|bool|null>
*/
private static function safeSourceReferences(array $sourceReferences): array
{
$safe = [];
foreach ($sourceReferences as $key => $value) {
if (! is_string($key) || trim($key) === '' || (! is_scalar($value) && $value !== null)) {
continue;
}
$safe[$key] = $value;
}
return $safe;
}
private static function nullableString(mixed $value): ?string
{
return is_string($value) && trim($value) !== '' ? trim($value) : null;
}
}