entityManager->createQueryBuilder() ->select('m') ->from(EntityManga::class, 'm') ->orderBy("m.$sortBy", $sortOrder) ->setFirstResult($offset) ->setMaxResults($limit); return array_map( fn (EntityManga $entity) => $this->toDomain($entity), $queryBuilder->getQuery()->getResult() ); } public function count(): int { return $this->entityManager->createQueryBuilder() ->select('COUNT(m.id)') ->from(EntityManga::class, 'm') ->getQuery() ->getSingleScalarResult(); } public function findById(string $id): ?DomainManga { // Convertir le string ID en integer pour la base de données if (!is_numeric($id)) { return null; } $entity = $this->entityManager->find(EntityManga::class, (int) $id); return $entity ? $this->toDomain($entity) : null; } public function findBySlug(MangaSlug $slug): ?DomainManga { $entity = $this->entityManager->getRepository(EntityManga::class) ->findOneBy(['slug' => $slug->getValue()]); return $entity ? $this->toDomain($entity) : null; } public function save(DomainManga $manga): void { // Check if this is an update (manga has a numeric ID) or a new creation $entity = null; if ($manga->getId() && $manga->getId()->getValue() && is_numeric($manga->getId()->getValue())) { $entity = $this->entityManager->find(EntityManga::class, (int) $manga->getId()->getValue()); } if (!$entity) { $entity = new EntityManga(); } $imageUrls = $manga->getImageUrls(); $fullImageUrl = $imageUrls?->getFull(); $thumbnailUrl = $imageUrls?->getThumbnail(); $entity->setTitle($manga->getTitle()->getValue()) ->setSlug($manga->getSlug()->getValue()) ->setDescription($manga->getDescription()) ->setAuthor($manga->getAuthor()) ->setPublicationYear($manga->getPublicationYear()) ->setGenres($manga->getGenres()) ->setStatus($manga->getStatus()) ->setImageUrl($fullImageUrl ?? null) ->setThumbnailUrl($thumbnailUrl ?? null) ->setAlternativeSlugs($manga->getAlternativeSlugs()) ->setMonitored($manga->isMonitoringEnabled()) ->setLastMonitoringCheck($manga->getLastMonitoringCheck()); // Only set externalId if it exists (to avoid setting null on update) if ($manga->getExternalId()) { $entity->setExternalId($manga->getExternalId()->getValue()); } if ($manga->getRating() !== null) { $entity->setRating($manga->getRating()); } $this->entityManager->persist($entity); $this->entityManager->flush(); // Met à jour l'ID du modèle du domaine avec l'ID généré par la BDD if ($entity->getId()) { $manga->updateId(new MangaId((string) $entity->getId())); } // Handle new chapters added through the aggregate foreach ($manga->pullNewChapters() as $chapter) { $mangaEntity = $this->entityManager->find(EntityManga::class, (int) $manga->getId()->getValue()); $chapterEntity = new EntityChapter(); $chapterEntity->setManga($mangaEntity) ->setNumber($chapter->getNumber()) ->setTitle($chapter->getTitle()) ->setVolume($chapter->getVolume()) ->setVisible($chapter->isVisible()) ->setPagesDirectory($chapter->getPagesDirectory()) ->setPageCount($chapter->getPageCount()); $this->entityManager->persist($chapterEntity); } // Handle chapters modified through the aggregate foreach ($manga->pullModifiedChapters() as $chapter) { $chapterEntity = $this->entityManager->find(EntityChapter::class, $chapter->getId()); if ($chapterEntity) { $chapterEntity->setVisible($chapter->isVisible()) ->setPagesDirectory($chapter->getPagesDirectory()) ->setPageCount($chapter->getPageCount()) ->setTitle($chapter->getTitle()) ->setVolume($chapter->getVolume()) ->setCbzPath($chapter->getPagesDirectory()); $this->entityManager->persist($chapterEntity); } } // Handle chapters deleted through the aggregate foreach ($manga->pullChaptersToDelete() as $chapter) { $chapterEntity = $this->entityManager->find(EntityChapter::class, $chapter->getId()); if ($chapterEntity) { $this->entityManager->remove($chapterEntity); } } $this->entityManager->flush(); } public function delete(DomainManga $manga): void { $entity = $this->entityManager->find(EntityManga::class, $manga->getId()->getValue()); if ($entity) { $this->entityManager->remove($entity); $this->entityManager->flush(); } } public function findChapters(string $mangaId, int $page = 1, int $limit = 20, string $sortOrder = 'desc'): array { $offset = ($page - 1) * $limit; $queryBuilder = $this->entityManager->createQueryBuilder() ->select('c') ->from(EntityChapter::class, 'c') ->where('c.manga = :mangaId') ->orderBy('c.number', $sortOrder) ->setParameter('mangaId', $mangaId) ->setFirstResult($offset) ->setMaxResults($limit); return array_map( fn (EntityChapter $entity) => $this->toChapterDomain($entity), $queryBuilder->getQuery()->getResult() ); } public function findAllChapters(string $mangaId, string $sortOrder = 'desc'): array { $queryBuilder = $this->entityManager->createQueryBuilder() ->select('c') ->from(EntityChapter::class, 'c') ->where('c.manga = :mangaId') ->orderBy('c.number', $sortOrder) ->setParameter('mangaId', $mangaId); return array_map( fn (EntityChapter $entity) => $this->toChapterDomain($entity), $queryBuilder->getQuery()->getResult() ); } public function countChapters(string $mangaId): int { return $this->entityManager->createQueryBuilder() ->select('COUNT(c.id)') ->from(EntityChapter::class, 'c') ->where('c.manga = :mangaId') ->setParameter('mangaId', $mangaId) ->getQuery() ->getSingleScalarResult(); } public function countAvailableChapters(string $mangaId): int { return $this->entityManager->createQueryBuilder() ->select('COUNT(c.id)') ->from(EntityChapter::class, 'c') ->where('c.manga = :mangaId') ->andWhere('c.pagesDirectory IS NOT NULL OR c.cbzPath IS NOT NULL') ->setParameter('mangaId', $mangaId) ->getQuery() ->getSingleScalarResult(); } public function findByExternalId(ExternalId $externalId): ?DomainManga { $entity = $this->entityManager->getRepository(EntityManga::class)->findOneBy([ 'externalId' => $externalId->getValue() ]); return $entity ? $this->toDomain($entity) : null; } public function findChapterById(string $id): ?Chapter { $entity = $this->entityManager->find(EntityChapter::class, $id); return $entity ? $this->toChapterDomain($entity) : null; } public function findVisibleChapterById(string $id): ?Chapter { $entity = $this->entityManager->createQueryBuilder() ->select('c') ->from(EntityChapter::class, 'c') ->where('c.id = :id') ->andWhere('c.visible = :visible') ->setParameter('id', $id) ->setParameter('visible', 1) ->getQuery() ->getOneOrNullResult(); return $entity ? $this->toChapterDomain($entity) : null; } public function findChapterByMangaIdAndNumber(string $mangaId, float $chapterNumber): ?Chapter { $entity = $this->entityManager->createQueryBuilder() ->select('c') ->from(EntityChapter::class, 'c') ->where('c.manga = :mangaId') ->andWhere('c.number = :chapterNumber') ->setParameter('mangaId', $mangaId) ->setParameter('chapterNumber', $chapterNumber) ->getQuery() ->getOneOrNullResult(); return $entity ? $this->toChapterDomain($entity) : null; } public function findChaptersByMangaIdAndVolume(string $mangaId, int $volume): array { $entities = $this->entityManager->getRepository(EntityChapter::class) ->findBy(['manga' => $mangaId, 'volume' => $volume]); return array_map([$this, 'toChapterDomain'], $entities); } public function findVisibleChaptersByMangaIdAndVolume(string $mangaId, int $volume): array { $entities = $this->entityManager->getRepository(EntityChapter::class) ->findBy(['manga' => $mangaId, 'volume' => $volume, 'visible' => true]); return array_map([$this, 'toChapterDomain'], $entities); } public function findVisibleChaptersWithPagesByMangaIdAndVolume(string $mangaId, int $volume): array { $entities = $this->entityManager->createQueryBuilder() ->select('c') ->from(EntityChapter::class, 'c') ->where('c.manga = :mangaId') ->andWhere('c.volume = :volume') ->andWhere('c.visible = true') ->andWhere('c.pagesDirectory IS NOT NULL OR c.cbzPath IS NOT NULL') ->setParameter('mangaId', $mangaId) ->setParameter('volume', $volume) ->orderBy('c.number', 'ASC') ->getQuery() ->getResult(); return array_map([$this, 'toChapterDomain'], $entities); } public function search(string $query, int $page = 1, int $limit = 20): array { $offset = ($page - 1) * $limit; // Utiliser une requête native pour supporter la recherche dans le champ JSON AlternativeSlugs $sql = "SELECT m.* FROM manga m WHERE m.title LIKE :query OR m.slug LIKE :query OR CAST(m.alternative_slugs AS TEXT) LIKE :query ORDER BY m.title ASC LIMIT :limit OFFSET :offset"; $rsm = new \Doctrine\ORM\Query\ResultSetMapping(); $rsm->addEntityResult(EntityManga::class, 'm'); $rsm->addFieldResult('m', 'id', 'id'); $rsm->addFieldResult('m', 'title', 'title'); $rsm->addFieldResult('m', 'slug', 'slug'); $rsm->addFieldResult('m', 'image_url', 'imageUrl'); $rsm->addFieldResult('m', 'publication_year', 'publicationYear'); $rsm->addFieldResult('m', 'description', 'description'); $rsm->addFieldResult('m', 'genres', 'genres'); $rsm->addFieldResult('m', 'created_at', 'createdAt'); $rsm->addFieldResult('m', 'rating', 'rating'); $rsm->addFieldResult('m', 'author', 'author'); $rsm->addFieldResult('m', 'external_id', 'externalId'); $rsm->addFieldResult('m', 'status', 'status'); $rsm->addFieldResult('m', 'thumbnail_url', 'thumbnailUrl'); $rsm->addFieldResult('m', 'monitored', 'monitored'); $rsm->addFieldResult('m', 'last_monitoring_check', 'lastMonitoringCheck'); $rsm->addFieldResult('m', 'alternative_slugs', 'AlternativeSlugs'); $nativeQuery = $this->entityManager->createNativeQuery($sql, $rsm); $nativeQuery->setParameter('query', '%' . $query . '%'); $nativeQuery->setParameter('limit', $limit); $nativeQuery->setParameter('offset', $offset); return array_map( fn (EntityManga $entity) => $this->toDomain($entity), $nativeQuery->getResult() ); } public function countSearch(string $query): int { // Utiliser une requête native pour supporter la recherche dans le champ JSON AlternativeSlugs $sql = "SELECT COUNT(m.id) FROM manga m WHERE m.title LIKE :query OR m.slug LIKE :query OR m.author LIKE :query OR m.description LIKE :query OR CAST(m.alternative_slugs AS TEXT) LIKE :query"; $conn = $this->entityManager->getConnection(); $stmt = $conn->prepare($sql); $result = $stmt->executeQuery(['query' => '%' . $query . '%']); return (int) $result->fetchOne(); } /** * @param float[] $chapterNumbers * @return array */ public function findExistingChaptersByNumbers(string $mangaId, array $chapterNumbers): array { $queryBuilder = $this->entityManager->createQueryBuilder() ->select('c') ->from(EntityChapter::class, 'c') ->where('c.manga = :mangaId') ->andWhere('c.number IN (:chapterNumbers)') ->setParameter('mangaId', $mangaId) ->setParameter('chapterNumbers', array_map('floatval', $chapterNumbers)); $chapters = $queryBuilder->getQuery()->getResult(); $chaptersByNumber = []; foreach ($chapters as $chapter) { $chaptersByNumber[(float) $chapter->getNumber()] = $this->toChapterDomain($chapter); } return $chaptersByNumber; } public function findByMonitoringCriteria(MonitoringCriteria $criteria): array { $queryBuilder = $this->entityManager->createQueryBuilder() ->select('m') ->from(EntityManga::class, 'm') ->where('m.monitored = :enabled') ->setParameter('enabled', $criteria->enabled); if ($criteria->lastCheckBefore) { $queryBuilder->andWhere('(m.lastMonitoringCheck IS NULL OR m.lastMonitoringCheck < :lastCheckBefore)') ->setParameter('lastCheckBefore', $criteria->lastCheckBefore); } return array_map( fn (EntityManga $entity) => $this->toDomain($entity), $queryBuilder->getQuery()->getResult() ); } private function toDomain(EntityManga $entity): DomainManga { return new DomainManga( id: new MangaId((string) $entity->getId()), title: new MangaTitle($entity->getTitle()), slug: new MangaSlug($entity->getSlug()), description: $entity->getDescription() ?? '', author: $entity->getAuthor() ?? '', publicationYear: $entity->getPublicationYear() ?? 0, genres: $entity->getGenres() ?? [], status: $entity->getStatus() ?? '', externalId: $entity->getExternalId() ? new ExternalId($entity->getExternalId()) : null, imageUrl: $entity->getImageUrl(), rating: $entity->getRating(), imageUrls: $entity->getImageUrl() ? new ImageUrls($entity->getImageUrl() ?? '', $entity->getThumbnailUrl() ?? '') : null, alternativeSlugs: $entity->getAlternativeSlugs() ?? [], createdAt: $entity->getCreatedAt(), monitoringStatus: $entity->isMonitored() ? MonitoringStatus::enabled() : MonitoringStatus::disabled() ); } private function toChapterDomain(EntityChapter $entity): Chapter { return new Chapter( id: new ChapterId((string) $entity->getId()), mangaId: new MangaId((string) $entity->getManga()->getId()), number: $entity->getNumber(), title: $entity->getTitle(), volume: $entity->getVolume(), isVisible: $entity->isVisible(), // Fallback to cbzPath during transition (Phase 4 will drop cbzPath column) pagesDirectory: $entity->getPagesDirectory() ?? $entity->getCbzPath(), pageCount: $entity->getPageCount(), ); } }