optimize tag and rebuilding

This commit is contained in:
team2
2026-02-23 08:51:21 +01:00
parent 8c58d6777d
commit dceeaeee52
8 changed files with 678 additions and 167 deletions

134
src/Tag/TagService.php Normal file
View File

@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace App\Tag;
use App\Entity\Tag;
use App\Entity\Document;
use App\Entity\DocumentTag;
use App\Service\TagRebuildJobService;
use Doctrine\ORM\EntityManagerInterface;
final class TagService
{
public function __construct(
private EntityManagerInterface $em,
private TagRebuildJobService $jobs,
) {}
// =========================================================
// TAG CREATE
// =========================================================
public function create(string $slug, string $label, ?string $description = null): Tag
{
$slug = trim($slug);
$label = trim($label);
if ($label === '' || $slug === '') {
throw new \InvalidArgumentException('Label und Slug sind Pflichtfelder.');
}
if ($this->slugExists($slug)) {
throw new \RuntimeException('Slug existiert bereits.');
}
$tag = new Tag($slug, $label, $description);
$this->em->persist($tag);
$this->em->flush();
$this->triggerRebuildIfIdle();
return $tag;
}
// =========================================================
// TAG DELETE
// =========================================================
public function deleteById(string $tagId): void
{
$tag = $this->em->getRepository(Tag::class)->find($tagId);
if (!$tag instanceof Tag) {
throw new \RuntimeException('Tag nicht gefunden.');
}
$this->delete($tag);
}
public function delete(Tag $tag): void
{
$this->em->remove($tag);
$this->em->flush();
$this->triggerRebuildIfIdle();
}
// =========================================================
// DOCUMENT TAG SYNC
// =========================================================
/**
* Synchronisiert alle Tags eines Dokuments.
* Löst einen Rebuild aus, da document_ids Teil des NDJSON sind.
*/
public function syncDocumentTags(Document $document, array $newTagIds): void
{
$newTagIds = array_unique($newTagIds);
$currentRelations = $this->em
->getRepository(DocumentTag::class)
->findBy(['document' => $document]);
$currentTagIds = array_map(
fn(DocumentTag $dt) => (string) $dt->getTag()->getId(),
$currentRelations
);
$toAdd = array_diff($newTagIds, $currentTagIds);
$toRemove = array_diff($currentTagIds, $newTagIds);
foreach ($toAdd as $tagId) {
$tag = $this->em->getRepository(Tag::class)->find($tagId);
if ($tag instanceof Tag) {
$this->em->persist(new DocumentTag($document, $tag));
}
}
foreach ($currentRelations as $relation) {
if (in_array((string) $relation->getTag()->getId(), $toRemove, true)) {
$this->em->remove($relation);
}
}
if ($toAdd || $toRemove) {
$this->em->flush();
$this->triggerRebuildIfIdle();
}
}
// =========================================================
// INTERNAL HELPERS
// =========================================================
private function slugExists(string $slug): bool
{
return (int) $this->em->createQueryBuilder()
->select('COUNT(t.id)')
->from(Tag::class, 't')
->where('t.slug = :slug')
->setParameter('slug', $slug)
->getQuery()
->getSingleScalarResult() > 0;
}
private function triggerRebuildIfIdle(): void
{
if (!$this->jobs->hasActiveJob()) {
$this->jobs->enqueueAndStartAsync();
}
}
}