From 9e9307e40bf866f6990231aeb79d9a2749ce2957 Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Mon, 21 Sep 2026 18:54:03 +0000 Subject: [PATCH] =?UTF-8?q?Migre=20l'entit=C3=A9=20SponsorScan=20de=20Ting?= =?UTF-8?q?=20vers=20Doctrine=20(#2383)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpstan-baseline.php | 66 --------- .../Event/SponsorScan/DeleteAction.php | 8 +- .../Event/SponsorScan/ExportAction.php | 2 +- .../Event/SponsorScan/FlashAction.php | 33 +++-- .../Event/SponsorScan/IndexAction.php | 2 +- .../Repository/SponsorScanRepository.php | 48 +++++++ .../AppBundle/Event/Entity/SponsorScan.php | 31 ++++ .../Repository/SponsorScanRepository.php | 82 ----------- sources/AppBundle/Event/Model/SponsorScan.php | 134 ------------------ .../Repository/SponsorScanRepositoryTest.php | 114 +++++++++++++++ 10 files changed, 223 insertions(+), 297 deletions(-) create mode 100644 sources/AppBundle/Event/Entity/Repository/SponsorScanRepository.php create mode 100644 sources/AppBundle/Event/Entity/SponsorScan.php delete mode 100644 sources/AppBundle/Event/Model/Repository/SponsorScanRepository.php delete mode 100644 sources/AppBundle/Event/Model/SponsorScan.php create mode 100644 tests/integration/AppBundle/Event/Entity/Repository/SponsorScanRepositoryTest.php diff --git a/phpstan-baseline.php b/phpstan-baseline.php index f85ead6d8..5b459b9ea 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -4483,54 +4483,12 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/Speaker/SuggestionAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#', - 'identifier' => 'foreach.nonIterable', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'created_on\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'email\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'nom\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'prenom\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method format\\(\\) on DateTime\\|null\\.$#', 'identifier' => 'method.nonObject', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$sponsorTicketId of method AppBundle\\\\Event\\\\Model\\\\SponsorScan\\:\\:setSponsorTicketId\\(\\) expects int, int\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/SponsorScan/FlashAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$ticketId of method AppBundle\\\\Event\\\\Model\\\\SponsorScan\\:\\:setTicketId\\(\\) expects int, int\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Event/SponsorScan/FlashAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'code\' on mixed\\.$#', 'identifier' => 'offsetAccess.nonOffsetAccessible', @@ -6649,30 +6607,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/SpeakerSuggestionRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\SponsorScanRepository\\:\\:getBySponsorTicket\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/SponsorScanRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\SponsorScanRepository\\:\\: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/SponsorScanRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\SponsorScanRepository\\:\\: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/SponsorScanRepository.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/SponsorScanRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\SponsorTicketRepository\\:\\:getByEvent\\(\\) return type with generic interface CCMBenchmark\\\\Ting\\\\Repository\\\\CollectionInterface does not specify its types\\: T$#', 'identifier' => 'missingType.generics', diff --git a/sources/AppBundle/Controller/Event/SponsorScan/DeleteAction.php b/sources/AppBundle/Controller/Event/SponsorScan/DeleteAction.php index 4524b4e8b..60d86b7a8 100644 --- a/sources/AppBundle/Controller/Event/SponsorScan/DeleteAction.php +++ b/sources/AppBundle/Controller/Event/SponsorScan/DeleteAction.php @@ -6,9 +6,9 @@ use AppBundle\Controller\Event\EventActionHelper; use AppBundle\Controller\Exception\InvalidSponsorTokenException; -use AppBundle\Event\Model\Repository\SponsorScanRepository; +use AppBundle\Event\Entity\Repository\SponsorScanRepository; +use AppBundle\Event\Entity\SponsorScan; use AppBundle\Event\Model\Repository\SponsorTicketRepository; -use AppBundle\Event\Model\SponsorScan; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -32,10 +32,10 @@ public function __invoke(Request $request, string $eventSlug, string $scanId): R return $this->redirectToRoute('sponsor_ticket_home', ['eventSlug' => $eventSlug]); } - $scan = $this->sponsorScanRepository->getOneBy(['sponsorTicketId' => $sponsorTicket->getId(), 'id' => $scanId]); + $scan = $this->sponsorScanRepository->findOneBy(['sponsorTicketId' => $sponsorTicket->getId(), 'id' => (int) $scanId]); if ($scan instanceof SponsorScan) { - $scan->setDeletedOn(new \DateTime('now')); + $scan->deletedOn = new \DateTimeImmutable('now'); $this->sponsorScanRepository->save($scan); $this->addFlash('success', "QR Code supprimé !"); } diff --git a/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php b/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php index f6c7b19ed..a2b5bc21c 100644 --- a/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php +++ b/sources/AppBundle/Controller/Event/SponsorScan/ExportAction.php @@ -6,7 +6,7 @@ use AppBundle\Controller\Event\EventActionHelper; use AppBundle\Controller\Exception\InvalidSponsorTokenException; -use AppBundle\Event\Model\Repository\SponsorScanRepository; +use AppBundle\Event\Entity\Repository\SponsorScanRepository; use AppBundle\Event\Model\Repository\SponsorTicketRepository; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Request; diff --git a/sources/AppBundle/Controller/Event/SponsorScan/FlashAction.php b/sources/AppBundle/Controller/Event/SponsorScan/FlashAction.php index 4941a8b34..ad5799bbf 100644 --- a/sources/AppBundle/Controller/Event/SponsorScan/FlashAction.php +++ b/sources/AppBundle/Controller/Event/SponsorScan/FlashAction.php @@ -6,10 +6,12 @@ use AppBundle\Controller\Event\EventActionHelper; use AppBundle\Controller\Exception\InvalidSponsorTokenException; -use AppBundle\Event\Model\Repository\SponsorScanRepository; +use AppBundle\Event\Entity\Repository\SponsorScanRepository; +use AppBundle\Event\Entity\SponsorScan; use AppBundle\Event\Model\Repository\SponsorTicketRepository; use AppBundle\Event\Model\Repository\TicketRepository; -use AppBundle\Event\Model\SponsorScan; +use AppBundle\Event\Model\SponsorTicket; +use AppBundle\Event\Model\Ticket; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -42,25 +44,38 @@ public function __invoke(Request $request, string $code, string $eventSlug): Red return $this->redirectToRoute('sponsor_scan', ['eventSlug' => $eventSlug]); } - $scan = $this->sponsorScanRepository->getOneBy(['sponsorTicketId' => $sponsorTicket->getId(), 'ticketId' => $ticket->getId()]); + $scan = $this->sponsorScanRepository->findOneBy(['sponsorTicketId' => $sponsorTicket->getId(), 'ticketId' => $ticket->getId()]); - if ($scan instanceof SponsorScan && $scan->getDeletedOn() === null) { + if ($scan instanceof SponsorScan && $scan->deletedOn === null) { $this->addFlash('error', 'Code déjà scanné.'); return $this->redirectToRoute('sponsor_scan', ['eventSlug' => $eventSlug]); } if (!$scan instanceof SponsorScan) { - $scan = new SponsorScan() - ->setSponsorTicketId($sponsorTicket->getId()) - ->setTicketId($ticket->getId()); + $scan = $this->createSponsorScan($sponsorTicket, $ticket); } - $scan->setCreatedOn(new \DateTime('now')) - ->setDeletedOn(null); + $scan->createdOn = new \DateTimeImmutable('now'); + $scan->deletedOn = null; $this->sponsorScanRepository->save($scan); $this->addFlash('success', 'QR Code ajouté !'); return $this->redirectToRoute('sponsor_scan', ['eventSlug' => $eventSlug]); } + + private function createSponsorScan(SponsorTicket $sponsorTicket, Ticket $ticket): SponsorScan + { + $sponsorTicketId = $sponsorTicket->getId(); + $ticketId = $ticket->getId(); + if ($sponsorTicketId === null || $ticketId === null) { + throw new \InvalidArgumentException('Le ticket sponsor et le billet doivent être enregistrés'); + } + + $scan = new SponsorScan(); + $scan->sponsorTicketId = $sponsorTicketId; + $scan->ticketId = $ticketId; + + return $scan; + } } diff --git a/sources/AppBundle/Controller/Event/SponsorScan/IndexAction.php b/sources/AppBundle/Controller/Event/SponsorScan/IndexAction.php index 4c93ed5f8..c57845766 100644 --- a/sources/AppBundle/Controller/Event/SponsorScan/IndexAction.php +++ b/sources/AppBundle/Controller/Event/SponsorScan/IndexAction.php @@ -6,7 +6,7 @@ use AppBundle\Controller\Event\EventActionHelper; use AppBundle\Controller\Exception\InvalidSponsorTokenException; -use AppBundle\Event\Model\Repository\SponsorScanRepository; +use AppBundle\Event\Entity\Repository\SponsorScanRepository; use AppBundle\Event\Model\Repository\SponsorTicketRepository; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/sources/AppBundle/Event/Entity/Repository/SponsorScanRepository.php b/sources/AppBundle/Event/Entity/Repository/SponsorScanRepository.php new file mode 100644 index 000000000..886cf8158 --- /dev/null +++ b/sources/AppBundle/Event/Entity/Repository/SponsorScanRepository.php @@ -0,0 +1,48 @@ + + */ +final class SponsorScanRepository extends EntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, SponsorScan::class); + } + + /** + * Liste des scans actifs d'un ticket sponsor, avec les informations du billet scanné. + * + * @return list> + */ + public function getBySponsorTicket(SponsorTicket $sponsorTicket): array + { + $sponsorTicketId = $sponsorTicket->getId(); + if ($sponsorTicketId === null) { + throw new \InvalidArgumentException('Le ticket sponsor doit être enregistré'); + } + + $queryBuilder = $this->getEntityManager()->getConnection()->createQueryBuilder() + ->select('scan.id', 'inscription.nom', 'inscription.prenom', 'inscription.email', 'scan.created_on') + ->from('afup_forum_sponsor_scan', 'scan') + ->innerJoin('scan', 'afup_inscription_forum', 'inscription', 'inscription.id = scan.ticket_id') + ->where('scan.sponsor_ticket_id = :sponsorTicketId') + ->andWhere('scan.deleted_on IS NULL') + ->orderBy('scan.created_on', 'DESC') + ->setParameter('sponsorTicketId', $sponsorTicketId); + + /** @var list> $scans */ + $scans = $queryBuilder->executeQuery()->fetchAllAssociative(); + + return $scans; + } +} diff --git a/sources/AppBundle/Event/Entity/SponsorScan.php b/sources/AppBundle/Event/Entity/SponsorScan.php new file mode 100644 index 000000000..ec3a3100c --- /dev/null +++ b/sources/AppBundle/Event/Entity/SponsorScan.php @@ -0,0 +1,31 @@ + - */ -class SponsorScanRepository extends Repository implements MetadataInitializer -{ - public function getBySponsorTicket(SponsorTicket $sponsorTicket) - { - $sql = 'SELECT afup_forum_sponsor_scan.id, - afup_inscription_forum.nom, - afup_inscription_forum.prenom, - afup_inscription_forum.email, - afup_forum_sponsor_scan.created_on - FROM afup_forum_sponsor_scan - INNER JOIN afup_inscription_forum ON afup_inscription_forum.id = afup_forum_sponsor_scan.ticket_id - AND afup_forum_sponsor_scan.sponsor_ticket_id = :sponsorTicketId - AND afup_forum_sponsor_scan.deleted_on IS NULL - ORDER BY afup_forum_sponsor_scan.created_on DESC'; - - return $this->getPreparedQuery($sql) - ->setParams(['sponsorTicketId' => $sponsorTicket->getId()]) - ->query($this->getCollection(new HydratorArray())); - } - - /** - * @inheritDoc - */ - public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) - { - $metadata = new Metadata($serializerFactory); - - $metadata->setEntity(SponsorScan::class); - $metadata->setConnectionName('main'); - $metadata->setDatabase($options['database']); - $metadata->setTable('afup_forum_sponsor_scan'); - - $metadata - ->addField([ - 'columnName' => 'id', - 'fieldName' => 'id', - 'primary' => true, - 'autoincrement' => true, - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'sponsor_ticket_id', - 'fieldName' => 'sponsorTicketId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'ticket_id', - 'fieldName' => 'ticketId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'created_on', - 'fieldName' => 'createdOn', - 'type' => 'datetime', - ]) - ->addField([ - 'columnName' => 'deleted_on', - 'fieldName' => 'deletedOn', - 'type' => 'datetime', - ]) - ; - - return $metadata; - } -} diff --git a/sources/AppBundle/Event/Model/SponsorScan.php b/sources/AppBundle/Event/Model/SponsorScan.php deleted file mode 100644 index 5913ba25c..000000000 --- a/sources/AppBundle/Event/Model/SponsorScan.php +++ /dev/null @@ -1,134 +0,0 @@ -id; - } - - /** - * @param int $id - * @return $this - */ - public function setId($id): self - { - $this->propertyChanged('id', $this->id, $id); - $this->id = $id; - return $this; - } - - /** - * @return int - */ - public function getSponsorTicketId() - { - return $this->sponsorTicketId; - } - - /** - * @param int $sponsorTicketId - * @return $this - */ - public function setSponsorTicketId($sponsorTicketId): self - { - $this->propertyChanged('token', $this->sponsorTicketId, $sponsorTicketId); - $this->sponsorTicketId = $sponsorTicketId; - return $this; - } - - /** - * @return \DateTime - */ - public function getCreatedOn() - { - return $this->createdOn; - } - - /** - * @param \DateTime $createdOn - * @return $this - */ - public function setCreatedOn($createdOn): self - { - $this->propertyChanged('createdOn', $this->createdOn, $createdOn); - $this->createdOn = $createdOn; - return $this; - } - - /** - * @return \DateTime|null - */ - public function getDeletedOn() - { - return $this->deletedOn; - } - - /** - * @param \DateTime|null $deletedOn - * @return $this - */ - public function setDeletedOn($deletedOn): self - { - $this->propertyChanged('deletedOn', $this->deletedOn, $deletedOn); - $this->deletedOn = $deletedOn; - return $this; - } - - /** - * @return int - */ - public function getTicketId() - { - return $this->ticketId; - } - - /** - * @param int $ticketId - * @return $this - */ - public function setTicketId($ticketId): self - { - $this->propertyChanged('manager', $this->ticketId, $ticketId); - $this->ticketId = $ticketId; - - return $this; - } -} diff --git a/tests/integration/AppBundle/Event/Entity/Repository/SponsorScanRepositoryTest.php b/tests/integration/AppBundle/Event/Entity/Repository/SponsorScanRepositoryTest.php new file mode 100644 index 000000000..feaf2f226 --- /dev/null +++ b/tests/integration/AppBundle/Event/Entity/Repository/SponsorScanRepositoryTest.php @@ -0,0 +1,114 @@ +get(SponsorScanRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $ticketId1 = $this->insertInscription($connection, 'Dupont', 'Pierre', 'pierre.dupont@example.com'); + $ticketId2 = $this->insertInscription($connection, 'Martin', 'Claire', 'claire.martin@example.com'); + + $this->insertScan($connection, $ticketId1, '2026-06-01 10:00:00'); + $this->insertScan($connection, $ticketId2, '2026-06-02 10:00:00'); + // Un scan supprimé et un scan d'un autre ticket sponsor ne doivent pas remonter + $deletedScanId = $this->insertScan($connection, $ticketId1, '2026-06-03 10:00:00'); + $connection->update('afup_forum_sponsor_scan', ['deleted_on' => '2026-06-04 10:00:00'], ['id' => $deletedScanId]); + $this->insertScan($connection, $ticketId2, '2026-06-04 10:00:00', 20); + + $scans = $repository->getBySponsorTicket($this->buildSponsorTicket(self::SPONSOR_TICKET_ID)); + + self::assertCount(2, $scans); + // Tri par date de création décroissante + self::assertSame('claire.martin@example.com', $scans[0]['email']); + self::assertSame('pierre.dupont@example.com', $scans[1]['email']); + self::assertSame('Martin', $scans[0]['nom']); + self::assertSame('Claire', $scans[0]['prenom']); + self::assertSame('2026-06-02 10:00:00', $scans[0]['created_on']); + self::assertArrayHasKey('id', $scans[0]); + } + + public function testFindOneBySponsorTicketAndTicket(): void + { + $repository = self::getContainer()->get(SponsorScanRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $ticketId = $this->insertInscription($connection, 'Dupont', 'Pierre', 'pierre.dupont@example.com'); + $scanId = $this->insertScan($connection, $ticketId, '2026-06-01 10:00:00'); + + $scan = $repository->findOneBy(['sponsorTicketId' => self::SPONSOR_TICKET_ID, 'ticketId' => $ticketId]); + + self::assertInstanceOf(SponsorScan::class, $scan); + self::assertSame($scanId, $scan->id); + self::assertSame($ticketId, $scan->ticketId); + self::assertSame(self::SPONSOR_TICKET_ID, $scan->sponsorTicketId); + self::assertNull($scan->deletedOn); + + self::assertNull($repository->findOneBy(['sponsorTicketId' => self::SPONSOR_TICKET_ID, 'ticketId' => 999])); + } + + public function testSoftDeleteIsPersistedAndExcludedFromList(): void + { + $repository = self::getContainer()->get(SponsorScanRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $ticketId = $this->insertInscription($connection, 'Dupont', 'Pierre', 'pierre.dupont@example.com'); + $this->insertScan($connection, $ticketId, '2026-06-01 10:00:00'); + + $scan = $repository->findOneBy(['sponsorTicketId' => self::SPONSOR_TICKET_ID, 'ticketId' => $ticketId]); + self::assertInstanceOf(SponsorScan::class, $scan); + + // Suppression logique : le scan reste en base mais n'apparait plus dans la liste + $scan->deletedOn = new \DateTimeImmutable('now'); + $repository->save($scan); + + self::assertNotNull($repository->findOneBy(['sponsorTicketId' => self::SPONSOR_TICKET_ID, 'id' => $scan->id])); + self::assertSame([], $repository->getBySponsorTicket($this->buildSponsorTicket(self::SPONSOR_TICKET_ID))); + } + + private function buildSponsorTicket(int $id): SponsorTicket + { + $sponsorTicket = new SponsorTicket(); + $sponsorTicket->setId($id); + + return $sponsorTicket; + } + + private function insertInscription(Connection $connection, string $nom, string $prenom, string $email): int + { + $connection->insert('afup_inscription_forum', [ + 'reference' => 'REF-' . uniqid(), + 'nom' => $nom, + 'prenom' => $prenom, + 'email' => $email, + 'id_forum' => 42, + 'etat' => 0, + ]); + + return (int) $connection->lastInsertId(); + } + + private function insertScan(Connection $connection, int $ticketId, string $createdOn, int $sponsorTicketId = self::SPONSOR_TICKET_ID): int + { + $connection->insert('afup_forum_sponsor_scan', [ + 'sponsor_ticket_id' => $sponsorTicketId, + 'ticket_id' => $ticketId, + 'created_on' => $createdOn, + ]); + + return (int) $connection->lastInsertId(); + } +}