setTitle(trim($title)); $document->setCreatedBy($user); $version = new DocumentVersion(); $version->setVersionNumber(1); $version->setFilePath($filePath); $version->setChecksum($this->calculateChecksum($filePath)); $version->setCreatedBy($user); $version->setActive(true); $document->addVersion($version); $document->setCurrentVersion($version); $this->em->persist($document); $this->em->persist($version); $this->em->flush(); return $document; } /** * Adds a new immutable version to an existing document. */ public function addVersion( Document $document, string $filePath, User $user ): DocumentVersion { $nextVersionNumber = $this->getNextVersionNumber($document); $version = new DocumentVersion(); $version->setVersionNumber($nextVersionNumber); $version->setFilePath($filePath); $version->setChecksum($this->calculateChecksum($filePath)); $version->setCreatedBy($user); $version->setActive(false); $document->addVersion($version); $this->em->persist($version); $this->em->flush(); return $version; } /** * Activates a document version and marks it for re-ingest. */ public function activateVersion(DocumentVersion $version): void { $document = $version->getDocument(); foreach ($document->getVersions() as $existingVersion) { $existingVersion->setActive(false); } $version->setActive(true); $document->setCurrentVersion($version); $version->setIngestStatus(DocumentVersion::INGEST_PENDING); $this->em->flush(); } /** * Archives a document. * * If the document had tag assignments, the tag index is rebuilt so the * routing layer no longer works with an outdated active document set. */ public function archive(Document $document): void { if ($document->getStatus() === Document::STATUS_ARCHIVED) { return; } $shouldRebuildTags = $this->hasTagAssignments($document); $document->archive(); $this->em->flush(); if ($shouldRebuildTags) { $this->triggerTagRebuildIfIdle(); } } /** * Deletes a document. * * If the document had tag assignments, the tag index is rebuilt after the * removal so stale document references disappear from tag-based routing. */ public function delete(Document $document): void { $shouldRebuildTags = $this->hasTagAssignments($document); $this->em->remove($document); $this->em->flush(); if ($shouldRebuildTags) { $this->triggerTagRebuildIfIdle(); } } /** * Calculates the SHA256 checksum for a file path. */ private function calculateChecksum(string $filePath): string { $filePath = trim($filePath); if ($filePath === '') { throw new RuntimeException('File path must not be empty.'); } if (!is_file($filePath)) { throw new RuntimeException('File not found for checksum.'); } $checksum = hash_file('sha256', $filePath); if ($checksum === false) { throw new RuntimeException('Could not calculate file checksum.'); } return $checksum; } /** * Determines the next version number for a document. */ private function getNextVersionNumber(Document $document): int { $max = 0; foreach ($document->getVersions() as $version) { $max = max($max, $version->getVersionNumber()); } return $max + 1; } private function hasTagAssignments(Document $document): bool { return $document->getDocumentTags()->count() > 0; } private function triggerTagRebuildIfIdle(): void { if (!$this->tagRebuildJobService->hasActiveJob()) { $this->tagRebuildJobService->enqueueAndStartAsync(); } } }