$volatileKeys */ public function hashNormalized(mixed $value, array $volatileKeys = [ '@odata.context', '@odata.etag', 'createdDateTime', 'lastModifiedDateTime', 'modifiedDateTime', 'createdAt', 'updatedAt', ]): string { $normalized = $this->normalizeValue($value, $volatileKeys); return hash('sha256', json_encode($normalized, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } public function fingerprint( int $tenantId, string $scopeKey, string $subjectType, string $subjectExternalId, string $changeType, string $baselineHash, string $currentHash, ): string { $parts = [ (string) $tenantId, $this->normalize($scopeKey), $this->normalize($subjectType), $this->normalize($subjectExternalId), $this->normalize($changeType), $this->normalize($baselineHash), $this->normalize($currentHash), ]; return hash('sha256', implode('|', $parts)); } private function normalize(string $value): string { return trim(mb_strtolower($value)); } /** * @param array $volatileKeys */ private function normalizeValue(mixed $value, array $volatileKeys): mixed { if (is_array($value)) { if ($this->isList($value)) { $items = array_map(fn ($item) => $this->normalizeValue($item, $volatileKeys), $value); usort($items, function ($a, $b): int { return strcmp( json_encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), json_encode($b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ); }); return $items; } $result = []; foreach ($value as $key => $item) { if (is_string($key) && in_array($key, $volatileKeys, true)) { continue; } $result[$key] = $this->normalizeValue($item, $volatileKeys); } ksort($result); return $result; } if (is_string($value)) { return trim($value); } return $value; } private function isList(array $value): bool { if ($value === []) { return true; } return array_keys($value) === range(0, count($value) - 1); } }