*/ public array $selectedItemIds, /** * @var array */ public array $groupMapping, public string $groupMappingFingerprint, public string $fingerprint, ) { if (! in_array($this->scopeMode, ['all', 'selected'], true)) { throw new InvalidArgumentException('Restore scope mode must be all or selected.'); } } public static function fromInputs( mixed $backupSetId, mixed $scopeMode, mixed $selectedItemIds, mixed $groupMapping, ): self { $normalizedBackupSetId = is_numeric($backupSetId) ? max(1, (int) $backupSetId) : null; $normalizedScopeMode = $scopeMode === 'selected' ? 'selected' : 'all'; $normalizedSelectedItemIds = self::normalizeItemIds( $normalizedScopeMode === 'selected' ? $selectedItemIds : [] ); $normalizedGroupMapping = self::normalizeGroupMapping($groupMapping); $groupMappingFingerprint = self::hashPayload($normalizedGroupMapping); return new self( backupSetId: $normalizedBackupSetId, scopeMode: $normalizedScopeMode, selectedItemIds: $normalizedSelectedItemIds, groupMapping: $normalizedGroupMapping, groupMappingFingerprint: $groupMappingFingerprint, fingerprint: self::hashPayload([ 'backup_set_id' => $normalizedBackupSetId, 'scope_mode' => $normalizedScopeMode, 'selected_item_ids' => $normalizedSelectedItemIds, 'group_mapping_fingerprint' => $groupMappingFingerprint, ]), ); } public static function fromArray(mixed $payload): ?self { if (! is_array($payload)) { return null; } if ( ! array_key_exists('backup_set_id', $payload) && ! array_key_exists('scope_mode', $payload) && ! array_key_exists('fingerprint', $payload) ) { return null; } return self::fromInputs( $payload['backup_set_id'] ?? null, $payload['scope_mode'] ?? null, $payload['selected_item_ids'] ?? [], $payload['group_mapping'] ?? [], ); } public function matches(?string $fingerprint): bool { return is_string($fingerprint) && $fingerprint !== '' && hash_equals($this->fingerprint, $fingerprint); } /** * @return array{ * backup_set_id: ?int, * scope_mode: string, * selected_item_ids: list, * group_mapping: array, * group_mapping_fingerprint: string, * fingerprint: string * } */ public function toArray(): array { return [ 'backup_set_id' => $this->backupSetId, 'scope_mode' => $this->scopeMode, 'selected_item_ids' => $this->selectedItemIds, 'group_mapping' => $this->groupMapping, 'group_mapping_fingerprint' => $this->groupMappingFingerprint, 'fingerprint' => $this->fingerprint, ]; } /** * @return list */ private static function normalizeItemIds(mixed $selectedItemIds): array { if (! is_array($selectedItemIds)) { return []; } $normalized = []; foreach ($selectedItemIds as $itemId) { if (is_int($itemId) && $itemId > 0) { $normalized[] = $itemId; continue; } if (is_string($itemId) && ctype_digit($itemId) && (int) $itemId > 0) { $normalized[] = (int) $itemId; } } $normalized = array_values(array_unique($normalized)); sort($normalized); return $normalized; } /** * @return array */ private static function normalizeGroupMapping(mixed $groupMapping): array { if ($groupMapping instanceof \Illuminate\Contracts\Support\Arrayable) { $groupMapping = $groupMapping->toArray(); } if ($groupMapping instanceof \stdClass) { $groupMapping = (array) $groupMapping; } if (! is_array($groupMapping)) { return []; } $normalized = []; foreach ($groupMapping as $sourceGroupId => $targetGroupId) { if (! is_string($sourceGroupId) || trim($sourceGroupId) === '') { continue; } if ($targetGroupId instanceof \BackedEnum) { $targetGroupId = $targetGroupId->value; } if (! is_string($targetGroupId)) { continue; } $targetGroupId = trim($targetGroupId); if ($targetGroupId === '') { continue; } $normalized[trim($sourceGroupId)] = strtoupper($targetGroupId) === 'SKIP' ? 'SKIP' : $targetGroupId; } ksort($normalized); return $normalized; } /** * @param array $payload */ private static function hashPayload(array $payload): string { try { return hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR)); } catch (JsonException $exception) { throw new InvalidArgumentException('Restore scope payload could not be fingerprinted.', previous: $exception); } } }