TenantAtlas/apps/platform/app/Support/Resources/ResourceIdentity.php
Ahmed Darrazi fb2642e941
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m9s
feat(resources): implement provider resource identity binding
Added ProviderResourceBinding model, migrations, policies, and supporting framework for canonical resource identity mapping as defined in Spec 381.
2026-06-15 17:37:06 +02:00

215 lines
6.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Resources;
use Illuminate\Contracts\Support\Arrayable;
use InvalidArgumentException;
use JsonSerializable;
/**
* @implements Arrayable<string, mixed>
*/
final readonly class ResourceIdentity implements Arrayable, JsonSerializable
{
public const string ProviderResource = 'provider_resource';
public const string CanonicalBuiltin = 'canonical_builtin';
public const string CanonicalDefault = 'canonical_default';
public const string CanonicalVirtualTarget = 'canonical_virtual_target';
public const string Unsupported = 'unsupported';
public const string Unknown = 'unknown';
public function __construct(
public string $providerKey,
public string $identityKind,
public ?string $providerResourceType = null,
public ?string $providerResourceId = null,
public ?string $canonicalDiscriminator = null,
) {
$this->assertValid();
}
public static function providerResource(string $providerKey, string $resourceType, string $resourceId): self
{
return new self(
providerKey: $providerKey,
identityKind: self::ProviderResource,
providerResourceType: $resourceType,
providerResourceId: $resourceId,
);
}
public static function canonicalBuiltin(string $providerKey, string $resourceType, string $discriminator): self
{
return new self(
providerKey: $providerKey,
identityKind: self::CanonicalBuiltin,
providerResourceType: $resourceType,
canonicalDiscriminator: $discriminator,
);
}
public static function canonicalDefault(string $providerKey, string $resourceType, string $discriminator): self
{
return new self(
providerKey: $providerKey,
identityKind: self::CanonicalDefault,
providerResourceType: $resourceType,
canonicalDiscriminator: $discriminator,
);
}
public static function virtualTarget(string $providerKey, string $resourceType, string $discriminator): self
{
return new self(
providerKey: $providerKey,
identityKind: self::CanonicalVirtualTarget,
providerResourceType: $resourceType,
canonicalDiscriminator: $discriminator,
);
}
public static function unsupported(string $providerKey, string $resourceType, string $discriminator): self
{
return new self(
providerKey: $providerKey,
identityKind: self::Unsupported,
providerResourceType: $resourceType,
canonicalDiscriminator: $discriminator,
);
}
public static function unknown(string $providerKey, string $resourceType, string $discriminator): self
{
return new self(
providerKey: $providerKey,
identityKind: self::Unknown,
providerResourceType: $resourceType,
canonicalDiscriminator: $discriminator,
);
}
/**
* @param array<string, mixed> $payload
*/
public static function fromArray(array $payload): self
{
return new self(
providerKey: (string) ($payload['provider_key'] ?? ''),
identityKind: (string) ($payload['identity_kind'] ?? ''),
providerResourceType: self::nullableString($payload['provider_resource_type'] ?? null),
providerResourceId: self::nullableString($payload['provider_resource_id'] ?? null),
canonicalDiscriminator: self::nullableString($payload['canonical_discriminator'] ?? null),
);
}
public function stableIdentityValue(): ?string
{
return $this->identityKind === self::ProviderResource
? $this->providerResourceId
: $this->canonicalDiscriminator;
}
public function fingerprint(): string
{
return hash('sha256', json_encode($this->payload(), JSON_THROW_ON_ERROR));
}
/**
* @return array{
* provider_key: string,
* identity_kind: string,
* provider_resource_type: ?string,
* provider_resource_id: ?string,
* canonical_discriminator: ?string,
* fingerprint: string
* }
*/
public function toArray(): array
{
$payload = $this->payload();
return $payload + [
'fingerprint' => hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR)),
];
}
/**
* @return array{
* provider_key: string,
* identity_kind: string,
* provider_resource_type: ?string,
* provider_resource_id: ?string,
* canonical_discriminator: ?string
* }
*/
private function payload(): array
{
return [
'provider_key' => $this->providerKey,
'identity_kind' => $this->identityKind,
'provider_resource_type' => $this->providerResourceType,
'provider_resource_id' => $this->providerResourceId,
'canonical_discriminator' => $this->canonicalDiscriminator,
];
}
public function jsonSerialize(): array
{
return $this->toArray();
}
private function assertValid(): void
{
if (trim($this->providerKey) === '') {
throw new InvalidArgumentException('Resource identities require a provider key.');
}
if (! in_array($this->identityKind, self::validKinds(), true)) {
throw new InvalidArgumentException(sprintf('Unsupported resource identity kind [%s].', $this->identityKind));
}
if ($this->providerResourceType !== null && trim($this->providerResourceType) === '') {
throw new InvalidArgumentException('Provider resource type must be non-empty when supplied.');
}
if ($this->identityKind === self::ProviderResource) {
if ($this->providerResourceType === null || trim($this->providerResourceType) === '' || $this->providerResourceId === null || trim($this->providerResourceId) === '') {
throw new InvalidArgumentException('Provider resource identities require provider resource type and ID.');
}
return;
}
if ($this->canonicalDiscriminator === null || trim($this->canonicalDiscriminator) === '') {
throw new InvalidArgumentException('Canonical resource identities require a discriminator.');
}
}
/**
* @return list<string>
*/
private static function validKinds(): array
{
return [
self::ProviderResource,
self::CanonicalBuiltin,
self::CanonicalDefault,
self::CanonicalVirtualTarget,
self::Unsupported,
self::Unknown,
];
}
private static function nullableString(mixed $value): ?string
{
return is_string($value) && trim($value) !== '' ? trim($value) : null;
}
}