Files
MtoRagSystem/src/Service/DocumentService.php
2026-04-20 16:36:28 +02:00

182 lines
4.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Document;
use App\Entity\DocumentVersion;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
final readonly class DocumentService
{
public function __construct(
private EntityManagerInterface $em,
private TagRebuildJobService $tagRebuildJobService,
) {
}
/**
* Creates a new document including version 1.
*/
public function createDocument(
string $title,
string $filePath,
User $user
): Document {
$document = new Document();
$document->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();
}
}
}