policy = $policy ?? new KnowledgePolicy(); $this->history = $history ?? new KnowledgeHistory(); $this->fingerprint = $fingerprint ?? new KnowledgeFingerprint(); } public function store(KnowledgeEntry $entry): KnowledgeEntry { $versionedEntry = $this->history->record($entry); $providerKey = strtolower($versionedEntry->getProvider()); $this->storage[$versionedEntry->getFingerprint()][$providerKey] = $versionedEntry; return $versionedEntry; } public function has(string $fingerprint): bool { return isset($this->storage[$fingerprint]) && $this->storage[$fingerprint] !== []; } public function hasReusable(string $normalizedIdentifier, string $domain, \DateTimeImmutable $at, bool $allowStale = true): bool { return $this->find($normalizedIdentifier, $domain, $at, $allowStale) !== null; } public function find( string $normalizedIdentifier, string $domain, \DateTimeImmutable $at, bool $allowStale = true ): ?KnowledgeEntry { return $this->get( $this->fingerprint->create($normalizedIdentifier, $domain), $at, $allowStale ); } public function get(string $fingerprint, \DateTimeImmutable $at, bool $allowStale = true): ?KnowledgeEntry { $entries = array_values(array_filter( $this->storage[$fingerprint] ?? [], fn (KnowledgeEntry $entry): bool => $this->policy->isReusable($entry, $at, $allowStale) )); usort($entries, static function (KnowledgeEntry $left, KnowledgeEntry $right): int { $qualityComparison = $right->getQuality()->compare($left->getQuality()); if ($qualityComparison !== 0) { return $qualityComparison; } return $right->getRetrievedAt() <=> $left->getRetrievedAt(); }); return $entries[0] ?? null; } public function getAll(string $fingerprint): array { return array_values($this->storage[$fingerprint] ?? []); } public function merge(string $fingerprint, \DateTimeImmutable $at): ?KnowledgeEntry { $entries = array_values(array_filter( $this->getAll($fingerprint), fn (KnowledgeEntry $entry): bool => $this->policy->isReusable($entry, $at, true) && ! $entry->isMerged() )); if ($entries === []) { return null; } usort($entries, static fn (KnowledgeEntry $left, KnowledgeEntry $right): int => $left->getQuality()->compare($right->getQuality())); $metadata = []; $images = []; $providers = []; $providerScores = []; foreach ($entries as $entry) { $metadata = array_replace_recursive($metadata, $entry->getMetadata()); $images = array_merge($images, $entry->getImages()); $providers[] = $entry->getProvider(); $providerScores[] = $entry->getProviderScore(); } $best = $entries[array_key_last($entries)]; $images = $this->uniqueImages($images); $source = new KnowledgeSource( 'Merged(' . implode(',', array_values(array_unique($providers))) . ')', array_sum($providerScores) / count($providerScores), ['providers' => array_values(array_unique($providers))] ); $quality = new KnowledgeQuality( max(array_map(static fn (KnowledgeEntry $entry): float => $entry->getQualityScore(), $entries)), $source->getScore(), ['provider_count' => count($entries)] ); $hash = $this->fingerprint->hash([ 'metadata' => $metadata, 'images' => $images, 'providers' => $source->getAttributes()['providers'], ]); $retrievedAt = max(array_map(static fn (KnowledgeEntry $entry): \DateTimeImmutable => $entry->getRetrievedAt(), $entries)); $expiresAt = max(array_map(static fn (KnowledgeEntry $entry): \DateTimeImmutable => $entry->getExpiresAt(), $entries)); return $this->store(new KnowledgeEntry( $best->getIdentifier(), $best->getNormalizedIdentifier(), $best->getDomain(), $source, $retrievedAt, $expiresAt, $metadata, $images, $quality, $fingerprint, $hash, $best->getLanguage(), $best->getCountry(), new KnowledgeVersion(1, $hash, $retrievedAt), true, true )); } public function getHistory(): KnowledgeHistory { return $this->history; } public function clear(): void { $this->storage = []; } private function uniqueImages(array $images): array { $unique = []; foreach ($images as $image) { $key = is_array($image) ? (string) ($image['url'] ?? json_encode($image)) : (string) $image; $unique[$key] = $image; } return array_values($unique); } }