From ca8f1ea40a312fe62ef8212dc6ba1e8b0b241f3e Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Wed, 2 Sep 2026 19:19:44 +0000 Subject: [PATCH] =?UTF-8?q?Migre=20l'association=20conf=C3=A9rencier<->ses?= =?UTF-8?q?sion=20de=20Ting=20vers=20Doctrine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpstan-baseline.php | 40 +--- .../Command/CfpNotificationCommand.php | 4 +- .../Controller/Admin/Talk/AddAction.php | 4 +- .../Controller/Admin/Talk/EditAction.php | 4 +- .../Controller/Event/CFP/InviteAction.php | 4 +- .../Repository/TalkToSpeakerRepository.php | 104 ++++++++++ .../AppBundle/Event/Entity/TalkToSpeaker.php | 21 ++ .../Model/Repository/EventStatsRepository.php | 3 +- .../Repository/TalkToSpeakersRepository.php | 102 ---------- .../AppBundle/Event/Model/TalkToSpeaker.php | 59 ------ .../AppBundle/Event/Talk/TalkFormHandler.php | 4 +- sources/AppBundle/Slack/MessageFactory.php | 6 +- .../TalkToSpeakerRepositoryTest.php | 183 ++++++++++++++++++ 13 files changed, 325 insertions(+), 213 deletions(-) create mode 100644 sources/AppBundle/Event/Entity/Repository/TalkToSpeakerRepository.php create mode 100644 sources/AppBundle/Event/Entity/TalkToSpeaker.php delete mode 100644 sources/AppBundle/Event/Model/Repository/TalkToSpeakersRepository.php delete mode 100644 sources/AppBundle/Event/Model/TalkToSpeaker.php create mode 100644 tests/integration/AppBundle/Event/Entity/Repository/TalkToSpeakerRepositoryTest.php diff --git a/phpstan-baseline.php b/phpstan-baseline.php index f1f8774b8..f819386fd 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -4022,13 +4022,13 @@ 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Speaker/SpeakerRegisterAction.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$speakers of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TalkToSpeakersRepository\\:\\:replaceSpeakers\\(\\) expects array\\, mixed given\\.$#', + 'message' => '#^Parameter \\#2 \\$speakers of method AppBundle\\\\Event\\\\Entity\\\\Repository\\\\TalkToSpeakerRepository\\:\\:replaceSpeakers\\(\\) expects array\\, mixed given\\.$#', 'identifier' => 'argument.type', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Talk/AddAction.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$speakers of method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TalkToSpeakersRepository\\:\\:replaceSpeakers\\(\\) expects array\\, mixed given\\.$#', + 'message' => '#^Parameter \\#2 \\$speakers of method AppBundle\\\\Event\\\\Entity\\\\Repository\\\\TalkToSpeakerRepository\\:\\:replaceSpeakers\\(\\) expects array\\, mixed given\\.$#', 'identifier' => 'argument.type', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Talk/EditAction.php', @@ -6511,12 +6511,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/EventStatsRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$speakers of class AppBundle\\\\Event\\\\Model\\\\EventStats\\\\CFPStats constructor expects int, mixed given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/EventStatsRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#3 \\$paying of class AppBundle\\\\Event\\\\Model\\\\EventStats\\\\TicketTypeStats constructor expects list\\, array\\ given\\.$#', 'identifier' => 'argument.type', @@ -7069,36 +7063,6 @@ 'count' => 2, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'count\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkToSpeakersRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TalkToSpeakersRepository\\:\\:getNumberOfSpeakers\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkToSpeakersRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TalkToSpeakersRepository\\:\\:initMetadata\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#', - 'identifier' => 'missingType.iterableValue', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkToSpeakersRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\TalkToSpeakersRepository\\:\\:initMetadata\\(\\) should return M of CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata but returns CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata\\\\.$#', - 'identifier' => 'return.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkToSpeakersRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$databaseName of method CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata\\\\:\\:setDatabase\\(\\) expects string, mixed given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkToSpeakersRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method format\\(\\) on DateTime\\|null\\.$#', 'identifier' => 'method.nonObject', diff --git a/sources/AppBundle/Command/CfpNotificationCommand.php b/sources/AppBundle/Command/CfpNotificationCommand.php index 057e27e8b..d8c299071 100644 --- a/sources/AppBundle/Command/CfpNotificationCommand.php +++ b/sources/AppBundle/Command/CfpNotificationCommand.php @@ -4,10 +4,10 @@ namespace AppBundle\Command; +use AppBundle\Event\Entity\Repository\TalkToSpeakerRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\EventRepository; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\TalkToSpeakersRepository; use AppBundle\Notifier\SlackNotifier; use AppBundle\Slack\MessageFactory; use Symfony\Component\Console\Command\Command; @@ -22,7 +22,7 @@ public function __construct( private readonly SlackNotifier $slackNotifier, private readonly EventRepository $eventRepository, private readonly TalkRepository $talkRepository, - private readonly TalkToSpeakersRepository $talkToSpeakersRepository, + private readonly TalkToSpeakerRepository $talkToSpeakersRepository, ) { parent::__construct(); } diff --git a/sources/AppBundle/Controller/Admin/Talk/AddAction.php b/sources/AppBundle/Controller/Admin/Talk/AddAction.php index 3c9b08f35..dea560324 100644 --- a/sources/AppBundle/Controller/Admin/Talk/AddAction.php +++ b/sources/AppBundle/Controller/Admin/Talk/AddAction.php @@ -6,9 +6,9 @@ use AppBundle\AuditLog\Audit; use AppBundle\Event\AdminEventSelection; +use AppBundle\Event\Entity\Repository\TalkToSpeakerRepository; use AppBundle\Event\Form\TalkAdminType; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\TalkToSpeakersRepository; use AppBundle\Event\Model\Talk; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -18,7 +18,7 @@ class AddAction extends AbstractController { public function __construct( private readonly TalkRepository $talkRepository, - private readonly TalkToSpeakersRepository $talkToSpeakersRepository, + private readonly TalkToSpeakerRepository $talkToSpeakersRepository, private readonly Audit $audit, ) {} diff --git a/sources/AppBundle/Controller/Admin/Talk/EditAction.php b/sources/AppBundle/Controller/Admin/Talk/EditAction.php index d3ec7e459..c07dc0303 100644 --- a/sources/AppBundle/Controller/Admin/Talk/EditAction.php +++ b/sources/AppBundle/Controller/Admin/Talk/EditAction.php @@ -7,10 +7,10 @@ use AppBundle\AuditLog\Audit; use AppBundle\Event\Form\TalkAdminType; use AppBundle\Event\Form\TalkType; +use AppBundle\Event\Entity\Repository\TalkToSpeakerRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\EventRepository; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\TalkToSpeakersRepository; use AppBundle\Event\Model\Talk; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -21,7 +21,7 @@ class EditAction extends AbstractController public function __construct( private readonly EventRepository $eventRepository, private readonly TalkRepository $talkRepository, - private readonly TalkToSpeakersRepository $talkToSpeakersRepository, + private readonly TalkToSpeakerRepository $talkToSpeakersRepository, private readonly Audit $audit, ) {} diff --git a/sources/AppBundle/Controller/Event/CFP/InviteAction.php b/sources/AppBundle/Controller/Event/CFP/InviteAction.php index 844c56f91..9b9ba220f 100644 --- a/sources/AppBundle/Controller/Event/CFP/InviteAction.php +++ b/sources/AppBundle/Controller/Event/CFP/InviteAction.php @@ -6,9 +6,9 @@ use AppBundle\CFP\SpeakerFactory; use AppBundle\Controller\Event\EventActionHelper; +use AppBundle\Event\Entity\Repository\TalkToSpeakerRepository; use AppBundle\Event\Model\Repository\TalkInvitationRepository; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\TalkToSpeakersRepository; use AppBundle\Event\Model\TalkInvitation; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -22,7 +22,7 @@ public function __construct( private readonly TalkRepository $talkRepository, private readonly SpeakerFactory $speakerFactory, private readonly TalkInvitationRepository $talkInvitationRepository, - private readonly TalkToSpeakersRepository $talkToSpeakersRepository, + private readonly TalkToSpeakerRepository $talkToSpeakersRepository, private readonly TranslatorInterface $translator, ) {} diff --git a/sources/AppBundle/Event/Entity/Repository/TalkToSpeakerRepository.php b/sources/AppBundle/Event/Entity/Repository/TalkToSpeakerRepository.php new file mode 100644 index 000000000..2b3c00fef --- /dev/null +++ b/sources/AppBundle/Event/Entity/Repository/TalkToSpeakerRepository.php @@ -0,0 +1,104 @@ + + */ +final class TalkToSpeakerRepository extends EntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, TalkToSpeaker::class); + } + + public function getNumberOfSpeakers(Event|int $event, ?\DateTime $since = null): int + { + if ($event instanceof Event) { + $event = $event->getId() ?? 0; + } + + $queryBuilder = $this->getEntityManager()->getConnection()->createQueryBuilder() + ->select('COUNT(DISTINCT tts.conferencier_id)') + ->from('afup_conferenciers_sessions', 'tts') + ->innerJoin('tts', 'afup_sessions', 's', 'tts.session_id = s.session_id') + ->where('s.id_forum = :event') + ->setParameter('event', $event); + + if ($since instanceof \DateTime) { + $queryBuilder->andWhere('s.date_soumission >= :since') + ->setParameter('since', $since->format('Y-m-d')); + } + + $count = $queryBuilder->executeQuery()->fetchOne(); + Assert::numeric($count); + + return (int) $count; + } + + /** + * @param Speaker[] $speakers + */ + public function replaceSpeakers(Talk $talk, array $speakers): void + { + $talkId = $talk->getId(); + Assert::notNull($talkId); + + $targetSpeakerIds = []; + foreach ($speakers as $speaker) { + $speakerId = $speaker->getId(); + Assert::notNull($speakerId); + $targetSpeakerIds[$speakerId] = $speakerId; + } + + $entityManager = $this->getEntityManager(); + $existingSpeakerIds = []; + foreach ($this->findBy(['talkId' => $talkId]) as $talkToSpeaker) { + $existingSpeakerIds[$talkToSpeaker->speakerId] = $talkToSpeaker->speakerId; + if (!isset($targetSpeakerIds[$talkToSpeaker->speakerId])) { + $entityManager->remove($talkToSpeaker); + } + } + + foreach (array_diff_key($targetSpeakerIds, $existingSpeakerIds) as $speakerId) { + $entityManager->persist($this->createTalkToSpeaker($talkId, $speakerId)); + } + + $entityManager->flush(); + } + + public function addSpeakerToTalk(Talk $talk, Speaker $speaker): void + { + $talkId = $talk->getId(); + $speakerId = $speaker->getId(); + Assert::notNull($talkId); + Assert::notNull($speakerId); + + if ($this->find(['talkId' => $talkId, 'speakerId' => $speakerId]) !== null) { + return; + } + + $entityManager = $this->getEntityManager(); + $entityManager->persist($this->createTalkToSpeaker($talkId, $speakerId)); + $entityManager->flush(); + } + + private function createTalkToSpeaker(int $talkId, int $speakerId): TalkToSpeaker + { + $talkToSpeaker = new TalkToSpeaker(); + $talkToSpeaker->talkId = $talkId; + $talkToSpeaker->speakerId = $speakerId; + + return $talkToSpeaker; + } +} diff --git a/sources/AppBundle/Event/Entity/TalkToSpeaker.php b/sources/AppBundle/Event/Entity/TalkToSpeaker.php new file mode 100644 index 000000000..95739790a --- /dev/null +++ b/sources/AppBundle/Event/Entity/TalkToSpeaker.php @@ -0,0 +1,21 @@ + - */ -class TalkToSpeakersRepository extends Repository implements MetadataInitializer -{ - public function getNumberOfSpeakers(Event|int $event, ?\DateTime $since = null) - { - if ($event instanceof Event) { - $event = $event->getId(); - } - - $sql = 'SELECT COUNT(distinct conferencier_id) AS count - FROM afup_conferenciers_sessions - JOIN afup_sessions ON (afup_conferenciers_sessions.session_id = afup_sessions.session_id) - WHERE id_forum = :event - '; - $params = ['event' => $event]; - if ($since instanceof \DateTime) { - $sql .= ' AND afup_sessions.date_soumission >= :since '; - $params['since'] = $since->format('Y-m-d'); - } - $query = $this->getQuery($sql); - $query->setParams($params); - - return $query->query($this->getCollection(new HydratorArray()))->first()['count']; - } - - /** - * @param Speaker[] $speakers - * @throws QueryException - */ - public function replaceSpeakers(Talk $talk, array $speakers): void - { - $this->startTransaction(); - try { - $delete = $this->getPreparedQuery('DELETE FROM afup_conferenciers_sessions WHERE session_id = :talk'); - $delete->setParams(['talk' => $talk->getId()])->execute(); - - $insert = $this->getPreparedQuery('INSERT INTO afup_conferenciers_sessions (conferencier_id, session_id) VALUES (:speaker, :talk)'); - foreach ($speakers as $speaker) { - $insert->setParams(['speaker' => $speaker->getId(), 'talk' => $talk->getId()])->execute(); - } - $this->commit(); - } catch (QueryException $exception) { - $this->rollback(); - throw $exception; - } - } - - public function addSpeakerToTalk(Talk $talk, Speaker $speaker): void - { - $insert = $this->getPreparedQuery('REPLACE INTO afup_conferenciers_sessions (conferencier_id, session_id) VALUES (:speaker, :talk)'); - $insert->setParams(['speaker' => $speaker->getId(), 'talk' => $talk->getId()])->execute(); - } - - /** - * @inheritDoc - */ - public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) - { - $metadata = new Metadata($serializerFactory); - - $metadata->setEntity(TalkToSpeaker::class); - $metadata->setConnectionName('main'); - $metadata->setDatabase($options['database']); - $metadata->setTable('afup_conferenciers_sessions'); - - $metadata - ->addField([ - 'columnName' => 'conferencier_id', - 'fieldName' => 'speakerId', - 'primary' => true, - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'session_id', - 'fieldName' => 'talkId', - 'primary' => true, - 'type' => 'int', - ]) - ; - - return $metadata; - } -} diff --git a/sources/AppBundle/Event/Model/TalkToSpeaker.php b/sources/AppBundle/Event/Model/TalkToSpeaker.php deleted file mode 100644 index 5ba3b71cd..000000000 --- a/sources/AppBundle/Event/Model/TalkToSpeaker.php +++ /dev/null @@ -1,59 +0,0 @@ -talkId; - } - - /** - * @param int $talkId - */ - public function setTalkId($talkId): self - { - $this->propertyChanged('talkId', $this->talkId, $talkId); - $this->talkId = $talkId; - return $this; - } - - /** - * @return int - */ - public function getSpeakerId() - { - return $this->speakerId; - } - - /** - * @param int $speakerId - */ - public function setSpeakerId($speakerId): self - { - $this->propertyChanged('speakerId', $this->speakerId, $speakerId); - $this->speakerId = $speakerId; - return $this; - } -} diff --git a/sources/AppBundle/Event/Talk/TalkFormHandler.php b/sources/AppBundle/Event/Talk/TalkFormHandler.php index 3e85e5e1b..05a3676be 100644 --- a/sources/AppBundle/Event/Talk/TalkFormHandler.php +++ b/sources/AppBundle/Event/Talk/TalkFormHandler.php @@ -4,10 +4,10 @@ namespace AppBundle\Event\Talk; +use AppBundle\Event\Entity\Repository\TalkToSpeakerRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\SpeakerRepository; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\TalkToSpeakersRepository; use AppBundle\Event\Model\Speaker; use AppBundle\Event\Model\Talk; use AppBundle\Notifier\SlackNotifier; @@ -26,7 +26,7 @@ public function __construct( private readonly SpeakerRepository $speakerRepository, private readonly SlackNotifier $slackNotifier, private readonly EventDispatcherInterface $eventDispatcher, - private readonly TalkToSpeakersRepository $talkToSpeakersRepository, + private readonly TalkToSpeakerRepository $talkToSpeakersRepository, private readonly UnitOfWork $unitOfWork, private readonly TalkSubmissionConfirmationMail $confirmationMail, private readonly LoggerInterface $logger, diff --git a/sources/AppBundle/Slack/MessageFactory.php b/sources/AppBundle/Slack/MessageFactory.php index 19cc4c821..531221123 100644 --- a/sources/AppBundle/Slack/MessageFactory.php +++ b/sources/AppBundle/Slack/MessageFactory.php @@ -5,11 +5,11 @@ namespace AppBundle\Slack; use AppBundle\Association\Model\Repository\UserRepository; +use AppBundle\Event\Entity\Repository\TalkToSpeakerRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\EventStats\SalesPilotage; use AppBundle\Event\Model\Repository\EventStatsRepository; use AppBundle\Event\Model\Repository\TalkRepository; -use AppBundle\Event\Model\Repository\TalkToSpeakersRepository; use AppBundle\Event\Model\Repository\TicketTypeRepository; use AppBundle\Event\Model\Talk; use AppBundle\Event\Model\Vote; @@ -262,7 +262,7 @@ private function formatPilotageComparison(SalesPilotage $pilotage): string } - public function createMessageForCfpStats(Event $event, TalkRepository $talkRepository, TalkToSpeakersRepository $talkToSpeakersRepository, \DateTime $currentDate, ?\DateTime $since = null): Message + public function createMessageForCfpStats(Event $event, TalkRepository $talkRepository, TalkToSpeakerRepository $talkToSpeakersRepository, \DateTime $currentDate, ?\DateTime $since = null): Message { $message = new Message(); $message @@ -318,7 +318,7 @@ public function createMessageForCfpStats(Event $event, TalkRepository $talkRepos /** * @return Field[] */ - private function prepareCfpStatsFields(TalkRepository $talkRepository, TalkToSpeakersRepository $talkToSpeakersRepository, Event $event, ?\DateTime $since = null): array + private function prepareCfpStatsFields(TalkRepository $talkRepository, TalkToSpeakerRepository $talkToSpeakersRepository, Event $event, ?\DateTime $since = null): array { $infos = [ 'Nombre de talks' => $talkRepository->getNumberOfTalksByEvent($event, $since)['talks'], diff --git a/tests/integration/AppBundle/Event/Entity/Repository/TalkToSpeakerRepositoryTest.php b/tests/integration/AppBundle/Event/Entity/Repository/TalkToSpeakerRepositoryTest.php new file mode 100644 index 000000000..8af6245af --- /dev/null +++ b/tests/integration/AppBundle/Event/Entity/Repository/TalkToSpeakerRepositoryTest.php @@ -0,0 +1,183 @@ +get(TalkToSpeakerRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $this->insertSession($connection, 1, 10, '2026-01-01'); + $this->insertSession($connection, 2, 10, '2026-01-10'); + $this->insertSession($connection, 3, 20, '2026-01-01'); + + $this->insertAssociation($connection, 1, 1); + $this->insertAssociation($connection, 1, 2); + // Le même speaker sur une autre session du même event : ne compte qu'une fois (DISTINCT) + $this->insertAssociation($connection, 2, 1); + $this->insertAssociation($connection, 3, 3); + + self::assertSame(2, $repository->getNumberOfSpeakers(10)); + self::assertSame(1, $repository->getNumberOfSpeakers(20)); + self::assertSame(0, $repository->getNumberOfSpeakers(99)); + } + + public function testGetNumberOfSpeakersAcceptsAnEventObject(): void + { + $repository = self::getContainer()->get(TalkToSpeakerRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $this->insertSession($connection, 1, 10, '2026-01-01'); + $this->insertAssociation($connection, 1, 1); + + self::assertSame(1, $repository->getNumberOfSpeakers($this->buildEvent(10))); + } + + public function testGetNumberOfSpeakersFiltersBySubmissionDateWhenSinceIsProvided(): void + { + $repository = self::getContainer()->get(TalkToSpeakerRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $this->insertSession($connection, 1, 10, '2026-01-01'); + $this->insertSession($connection, 2, 10, '2026-01-10'); + + $this->insertAssociation($connection, 1, 1); + $this->insertAssociation($connection, 2, 2); + + self::assertSame(1, $repository->getNumberOfSpeakers(10, new \DateTime('2026-01-05'))); + self::assertSame(2, $repository->getNumberOfSpeakers(10, new \DateTime('2025-12-01'))); + } + + public function testReplaceSpeakersReplacesExistingAssociations(): void + { + $repository = self::getContainer()->get(TalkToSpeakerRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $talk = $this->buildTalk(1); + $repository->replaceSpeakers($talk, [$this->buildSpeaker(1), $this->buildSpeaker(2)]); + + self::assertSame([1, 2], $this->fetchSpeakerIds($connection, 1)); + + $repository->replaceSpeakers($talk, [$this->buildSpeaker(3), $this->buildSpeaker(4)]); + + self::assertSame([3, 4], $this->fetchSpeakerIds($connection, 1)); + } + + public function testReplaceSpeakersWithNoSpeakerEmptiesAssociations(): void + { + $repository = self::getContainer()->get(TalkToSpeakerRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $this->insertAssociation($connection, 1, 1); + $this->insertAssociation($connection, 1, 2); + + $repository->replaceSpeakers($this->buildTalk(1), []); + + self::assertSame([], $this->fetchSpeakerIds($connection, 1)); + } + + public function testReplaceSpeakersKeepsAnAlreadyAssociatedSpeaker(): void + { + $repository = self::getContainer()->get(TalkToSpeakerRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $this->insertAssociation($connection, 1, 1); + + $repository->replaceSpeakers($this->buildTalk(1), [$this->buildSpeaker(1)]); + + self::assertSame([1], $this->fetchSpeakerIds($connection, 1)); + } + + public function testReplaceSpeakersWithPartialOverlap(): void + { + $repository = self::getContainer()->get(TalkToSpeakerRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $this->insertAssociation($connection, 1, 1); + $this->insertAssociation($connection, 1, 2); + + $repository->replaceSpeakers($this->buildTalk(1), [$this->buildSpeaker(2), $this->buildSpeaker(3)]); + + self::assertSame([2, 3], $this->fetchSpeakerIds($connection, 1)); + } + + public function testAddSpeakerToTalkInsertsTheAssociation(): void + { + $repository = self::getContainer()->get(TalkToSpeakerRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $repository->addSpeakerToTalk($this->buildTalk(1), $this->buildSpeaker(1)); + + self::assertSame([1], $this->fetchSpeakerIds($connection, 1)); + } + + public function testAddSpeakerToTalkIsIdempotent(): void + { + $repository = self::getContainer()->get(TalkToSpeakerRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $repository->addSpeakerToTalk($this->buildTalk(1), $this->buildSpeaker(1)); + $repository->addSpeakerToTalk($this->buildTalk(1), $this->buildSpeaker(1)); + + self::assertSame([1], $this->fetchSpeakerIds($connection, 1)); + } + + private function insertSession(Connection $connection, int $sessionId, int $eventId, string $submissionDate): void + { + $connection->insert('afup_sessions', [ + 'session_id' => $sessionId, + 'id_forum' => $eventId, + 'date_soumission' => $submissionDate, + 'abstract' => 'Abstract de test', + 'skill' => 0, + ]); + } + + private function insertAssociation(Connection $connection, int $talkId, int $speakerId): void + { + $connection->insert('afup_conferenciers_sessions', [ + 'session_id' => $talkId, + 'conferencier_id' => $speakerId, + ]); + } + + /** + * @return list + */ + private function fetchSpeakerIds(Connection $connection, int $talkId): array + { + return array_map( + static fn(array $row): int => (int) $row['conferencier_id'], + $connection->fetchAllAssociative( + 'SELECT conferencier_id FROM afup_conferenciers_sessions WHERE session_id = :talk ORDER BY conferencier_id', + ['talk' => $talkId], + ), + ); + } + + private function buildTalk(int $id): Talk + { + return (new Talk())->setId($id); + } + + private function buildSpeaker(int $id): Speaker + { + return (new Speaker())->setId($id); + } + + private function buildEvent(int $id): Event + { + return (new Event())->setId($id); + } +}