From f86cf2052a4585f518e308c1aba8c041e7bc7830 Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Mon, 21 Sep 2026 19:06:01 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Migre=20les=20entit=C3=A9s=20Badge=20de=20T?= =?UTF-8?q?ing=20vers=20Doctrine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpstan-baseline.php | 56 +++++----- .../Association/Form/UserBadgeType.php | 12 +-- .../Model/Repository/UserRepository.php | 5 +- .../UserMembership/BadgesComputer.php | 10 +- .../Admin/Members/BadgeListAction.php | 6 +- .../Admin/Members/BadgeNewAction.php | 8 +- .../Admin/Members/UserBadgeDeleteAction.php | 27 +++-- .../Admin/Members/UserBadgeNewAction.php | 23 ++-- .../Admin/Members/UserEditAction.php | 2 +- .../Controller/Website/Badge/ImageAction.php | 4 +- sources/AppBundle/Event/Entity/Badge.php | 24 +++++ .../AppBundle/Event/Entity/BadgeAttribue.php | 19 ---- .../Entity/Repository/BadgeRepository.php | 20 ++++ .../Entity/Repository/UserBadgeRepository.php | 47 ++------ sources/AppBundle/Event/Entity/UserBadge.php | 9 +- sources/AppBundle/Event/Model/Badge.php | 58 ---------- .../Model/Repository/BadgeRepository.php | 55 ---------- .../members/user_badge/user_badges.html.twig | 8 +- .../Repository/UserBadgeRepositoryTest.php | 101 +++++++++--------- 19 files changed, 197 insertions(+), 297 deletions(-) create mode 100644 sources/AppBundle/Event/Entity/Badge.php delete mode 100644 sources/AppBundle/Event/Entity/BadgeAttribue.php create mode 100644 sources/AppBundle/Event/Entity/Repository/BadgeRepository.php delete mode 100644 sources/AppBundle/Event/Model/Badge.php delete mode 100644 sources/AppBundle/Event/Model/Repository/BadgeRepository.php diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 37c40986d..430700860 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -752,21 +752,21 @@ 'path' => __DIR__ . '/sources/AppBundle/Association/Form/UserBadgeType.php', ]; $ignoreErrors[] = [ - 'message' => '#^Class AppBundle\\\\Association\\\\Form\\\\UserBadgeType extends generic class Symfony\\\\Component\\\\Form\\\\AbstractType but does not specify its types\\: TData$#', - 'identifier' => 'missingType.generics', + 'message' => '#^Cannot cast mixed to int\\.$#', + 'identifier' => 'cast.int', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Association/Form/UserBadgeType.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$userId of method AppBundle\\\\Event\\\\Entity\\\\Repository\\\\UserBadgeRepository\\:\\:findByUserId\\(\\) expects int, mixed given\\.$#', - 'identifier' => 'argument.type', + 'message' => '#^Class AppBundle\\\\Association\\\\Form\\\\UserBadgeType extends generic class Symfony\\\\Component\\\\Form\\\\AbstractType but does not specify its types\\: TData$#', + 'identifier' => 'missingType.generics', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Association/Form/UserBadgeType.php', ]; $ignoreErrors[] = [ 'message' => '#^Possibly invalid array key type int\\|null\\.$#', 'identifier' => 'offsetAccess.invalidOffset', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/sources/AppBundle/Association/Form/UserBadgeType.php', ]; $ignoreErrors[] = [ @@ -3481,6 +3481,12 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/HomeAction.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$badgeId of method AppBundle\\\\Association\\\\Model\\\\Repository\\\\UserRepository\\:\\:loadByBadge\\(\\) expects int, int\\|null given\\.$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php', +]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'image\' on mixed\\.$#', 'identifier' => 'offsetAccess.nonOffsetAccessible', @@ -3500,8 +3506,8 @@ 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/BadgeNewAction.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$label of method AppBundle\\\\Event\\\\Model\\\\Badge\\:\\:setLabel\\(\\) expects string, mixed given\\.$#', - 'identifier' => 'argument.type', + 'message' => '#^Property AppBundle\\\\Event\\\\Entity\\\\Badge\\:\\:\\$label \\(string\\) does not accept mixed\\.$#', + 'identifier' => 'assign.propertyType', 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/BadgeNewAction.php', ]; @@ -3595,6 +3601,12 @@ 'count' => 2, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/GeneralMeeting/ReportsAction.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Cannot cast mixed to int\\.$#', + 'identifier' => 'cast.int', + 'count' => 2, + 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$url of class Symfony\\\\Component\\\\HttpFoundation\\\\RedirectResponse constructor expects string, string\\|null given\\.$#', 'identifier' => 'argument.type', @@ -4507,6 +4519,18 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/ValueResolver/AdminEventSelectionValueResolver.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Binary operation "\\." between non\\-falsy\\-string and mixed results in an error\\.$#', + 'identifier' => 'binaryOp.invalid', + 'count' => 1, + 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Badge/ImageAction.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Call to an undefined method AppBundle\\\\Event\\\\Entity\\\\Badge\\:\\:getUrl\\(\\)\\.$#', + 'identifier' => 'method.notFound', + 'count' => 1, + 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Badge/ImageAction.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$code of method AppBundle\\\\Antennes\\\\AntenneRepository\\:\\:findByCode\\(\\) expects string, mixed given\\.$#', 'identifier' => 'argument.type', @@ -5821,24 +5845,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Planning.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\BadgeRepository\\:\\: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/BadgeRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\BadgeRepository\\:\\:initMetadata\\(\\) return type with generic class CCMBenchmark\\\\Ting\\\\Repository\\\\Metadata does not specify its types\\: T$#', - 'identifier' => 'missingType.generics', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/BadgeRepository.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/BadgeRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Argument of an invalid type CCMBenchmark\\\\Ting\\\\Repository\\\\CollectionInterface\\\\|null supplied for foreach, only iterables are supported\\.$#', 'identifier' => 'foreach.nonIterable', diff --git a/sources/AppBundle/Association/Form/UserBadgeType.php b/sources/AppBundle/Association/Form/UserBadgeType.php index 0dd0ad76b..49202dfc1 100644 --- a/sources/AppBundle/Association/Form/UserBadgeType.php +++ b/sources/AppBundle/Association/Form/UserBadgeType.php @@ -4,7 +4,7 @@ namespace AppBundle\Association\Form; -use AppBundle\Event\Model\Repository\BadgeRepository; +use AppBundle\Event\Entity\Repository\BadgeRepository; use AppBundle\Event\Entity\Repository\UserBadgeRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -24,17 +24,17 @@ public function __construct( public function buildForm(FormBuilderInterface $builder, array $options): void { $userBadgesIds = []; - foreach ($this->userBadgeRepository->findByUserId($options['user']->getId()) as $badgeAttribue) { - $userBadgesIds[$badgeAttribue->badgeId] = $badgeAttribue->badgeId; + foreach ($this->userBadgeRepository->findByUserId((int) $options['user']->getId()) as $userBadge) { + $userBadgesIds[$userBadge->badge->id] = $userBadge->badge->id; } $badges = []; - foreach ($this->badgeRepository->getAll() as $badge) { - if (isset($userBadgesIds[$badge->getId()])) { + foreach ($this->badgeRepository->findAll() as $badge) { + if (isset($userBadgesIds[$badge->id])) { continue; } - $badges[$badge->getLabel()] = $badge->getId(); + $badges[$badge->label] = $badge->id; } $builder diff --git a/sources/AppBundle/Association/Model/Repository/UserRepository.php b/sources/AppBundle/Association/Model/Repository/UserRepository.php index 02feab893..eac176d5b 100644 --- a/sources/AppBundle/Association/Model/Repository/UserRepository.php +++ b/sources/AppBundle/Association/Model/Repository/UserRepository.php @@ -8,7 +8,6 @@ use AppBundle\Association\MemberType; use AppBundle\Association\Model\CompanyMember; use AppBundle\Association\Model\User; -use AppBundle\Event\Model\Badge; use Aura\SqlQuery\Common\SelectInterface; use CCMBenchmark\Ting\Driver\Mysqli\Serializer\Boolean; use CCMBenchmark\Ting\Repository\CollectionInterface; @@ -145,7 +144,7 @@ public function loadAll() ; } - public function loadByBadge(Badge $badge) + public function loadByBadge(int $badgeId) { $queryBuilder = $this->getQueryBuilderWithCompleteUser(); @@ -154,7 +153,7 @@ public function loadByBadge(Badge $badge) return $this ->getPreparedQuery($queryBuilder->getStatement()) - ->setParams(['badge_id' => $badge->getId()]) + ->setParams(['badge_id' => $badgeId]) ->query($this->getCollection($this->getHydratorForUser())) ; } diff --git a/sources/AppBundle/Association/UserMembership/BadgesComputer.php b/sources/AppBundle/Association/UserMembership/BadgesComputer.php index 56eedfc74..a358641de 100644 --- a/sources/AppBundle/Association/UserMembership/BadgesComputer.php +++ b/sources/AppBundle/Association/UserMembership/BadgesComputer.php @@ -38,11 +38,13 @@ private function getSpecificBadges(User $user): array { $specific = []; - foreach ($this->userBadgeRepository->findByUserId($user->getId()) as $badgeAttribue) { + $userBadges = $this->userBadgeRepository->findByUserId((int) $user->getId()); + + foreach ($userBadges as $userBadge) { $specific[] = [ - 'date' => $badgeAttribue->issuedAt->format('Y-m-d'), - 'id' => $badgeAttribue->badgeId, - 'tooltip' => $badgeAttribue->badgeLabel, + 'date' => $userBadge->issuedAt->format('Y-m-d'), + 'id' => $userBadge->badge->id, + 'tooltip' => $userBadge->badge->label, ]; } diff --git a/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php b/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php index 0762585ac..0fa27f6bc 100644 --- a/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php +++ b/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php @@ -5,7 +5,7 @@ namespace AppBundle\Controller\Admin\Members; use AppBundle\Association\Model\Repository\UserRepository; -use AppBundle\Event\Model\Repository\BadgeRepository; +use AppBundle\Event\Entity\Repository\BadgeRepository; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Twig\Environment; @@ -21,10 +21,10 @@ public function __construct( public function __invoke(Request $request): Response { $infos = []; - foreach ($this->badgeRepository->getAll() as $badge) { + foreach ($this->badgeRepository->findAll() as $badge) { $infos[] = [ 'badge' => $badge, - 'users' => $this->userRepository->loadByBadge($badge), + 'users' => $this->userRepository->loadByBadge($badge->id), ]; } diff --git a/sources/AppBundle/Controller/Admin/Members/BadgeNewAction.php b/sources/AppBundle/Controller/Admin/Members/BadgeNewAction.php index 0911a770a..f85101093 100644 --- a/sources/AppBundle/Controller/Admin/Members/BadgeNewAction.php +++ b/sources/AppBundle/Controller/Admin/Members/BadgeNewAction.php @@ -5,8 +5,8 @@ namespace AppBundle\Controller\Admin\Members; use AppBundle\Association\Form\BadgeType; -use AppBundle\Event\Model\Badge; -use AppBundle\Event\Model\Repository\BadgeRepository; +use AppBundle\Event\Entity\Badge; +use AppBundle\Event\Entity\Repository\BadgeRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Filesystem\Filesystem; @@ -33,8 +33,8 @@ public function __invoke(Request $request): Response $data['image']->move($this->storageDir, $filename); $badge = new Badge(); - $badge->setLabel($data['label']); - $badge->setUrl($filename); + $badge->label = $data['label']; + $badge->url = $filename; $this->badgeRepository->save($badge); $this->addFlash('notice', 'Le badge a été ajouté'); diff --git a/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php b/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php index 13fe0d1f2..bdbdc70f0 100644 --- a/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php +++ b/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php @@ -4,30 +4,35 @@ namespace AppBundle\Controller\Admin\Members; +use AppBundle\Event\Entity\Repository\BadgeRepository; use AppBundle\Event\Entity\Repository\UserBadgeRepository; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class UserBadgeDeleteAction { - public function __construct(private readonly UserBadgeRepository $userBadgeRepository) {} + public function __construct( + private readonly BadgeRepository $badgeRepository, + private readonly UserBadgeRepository $userBadgeRepository, + ) {} public function __invoke(Request $request): RedirectResponse { - $badgeId = $request->attributes->get('badgeId'); - $userId = $request->attributes->get('userId'); - - if (!is_numeric($badgeId) || !is_numeric($userId)) { - throw new \RuntimeException('Identifiants de badge invalides.'); + $badgeId = (int) $request->attributes->get('badgeId'); + $badge = $this->badgeRepository->find($badgeId); + if (null === $badge) { + throw new NotFoundHttpException(sprintf('Badge %d inexistant', $badgeId)); } - $userBadge = $this->userBadgeRepository->find([ - 'badgeId' => (int) $badgeId, - 'userId' => (int) $userId, + $userBadge = $this->userBadgeRepository->findOneBy([ + 'userId' => (int) $request->attributes->get('userId'), + 'badge' => $badge, ]); - if ($userBadge !== null) { - $this->userBadgeRepository->delete($userBadge); + if (null === $userBadge) { + throw new NotFoundHttpException('Attribution de badge introuvable'); } + $this->userBadgeRepository->delete($userBadge); return new RedirectResponse($request->headers->get('referer')); } diff --git a/sources/AppBundle/Controller/Admin/Members/UserBadgeNewAction.php b/sources/AppBundle/Controller/Admin/Members/UserBadgeNewAction.php index 3fcd3c732..4aa9a71b2 100644 --- a/sources/AppBundle/Controller/Admin/Members/UserBadgeNewAction.php +++ b/sources/AppBundle/Controller/Admin/Members/UserBadgeNewAction.php @@ -6,17 +6,20 @@ use AppBundle\Association\Form\UserBadgeType; use AppBundle\Association\Model\Repository\UserRepository; +use AppBundle\Event\Entity\Repository\BadgeRepository; use AppBundle\Event\Entity\Repository\UserBadgeRepository; use AppBundle\Event\Entity\UserBadge; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class UserBadgeNewAction { public function __construct( private readonly UserRepository $userRepository, private readonly FormFactoryInterface $formFactory, + private readonly BadgeRepository $badgeRepository, private readonly UserBadgeRepository $userBadgeRepository, ) {} @@ -25,20 +28,22 @@ public function __invoke(Request $request): RedirectResponse $user = $this->userRepository->get($request->query->get('user_id')); $userBadgeForm = $this->formFactory->create(UserBadgeType::class, [], ['user' => $user]); $userBadgeForm->handleRequest($request); + if (!$userBadgeForm->isSubmitted() || !$userBadgeForm->isValid()) { + return new RedirectResponse($request->headers->get('referer')); + } + + /** @var array{badge: int, date: \DateTimeInterface, user: string} $data */ $data = $userBadgeForm->getData(); - if ( - !is_array($data) - || !is_numeric($data['badge'] ?? null) - || !is_numeric($data['user'] ?? null) - || !($data['date'] ?? null) instanceof \DateTimeInterface - ) { - throw new \RuntimeException('Données du formulaire d\'attribution de badge invalides.'); + $badgeId = (int) $data['badge']; + $badge = $this->badgeRepository->find($badgeId); + if (null === $badge) { + throw new NotFoundHttpException(sprintf('Badge %d inexistant', $badgeId)); } $userBadge = new UserBadge(); - $userBadge->badgeId = (int) $data['badge']; - $userBadge->issuedAt = \DateTimeImmutable::createFromInterface($data['date']); + $userBadge->badge = $badge; + $userBadge->issuedAt = $data['date']; $userBadge->userId = (int) $data['user']; $this->userBadgeRepository->save($userBadge); diff --git a/sources/AppBundle/Controller/Admin/Members/UserEditAction.php b/sources/AppBundle/Controller/Admin/Members/UserEditAction.php index 73a28cc87..a4e8f15dd 100644 --- a/sources/AppBundle/Controller/Admin/Members/UserEditAction.php +++ b/sources/AppBundle/Controller/Admin/Members/UserEditAction.php @@ -49,7 +49,7 @@ public function __invoke(Request $request): Response ]); } - $userBadges = iterator_to_array($this->userBadgeRepository->findByUserId($user->getId())); + $userBadges = $this->userBadgeRepository->findByUserId((int) $user->getId()); $userBadgeForm = $this->createForm(UserBadgeType::class, [], [ 'user' => $user, 'action' => $this->generateUrl('admin_members_user_badge_new', [ diff --git a/sources/AppBundle/Controller/Website/Badge/ImageAction.php b/sources/AppBundle/Controller/Website/Badge/ImageAction.php index 03cf3ade0..4bad94079 100644 --- a/sources/AppBundle/Controller/Website/Badge/ImageAction.php +++ b/sources/AppBundle/Controller/Website/Badge/ImageAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Website\Badge; -use AppBundle\Event\Model\Repository\BadgeRepository; +use AppBundle\Event\Entity\Repository\BadgeRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\BinaryFileResponse; @@ -20,7 +20,7 @@ public function __construct( public function __invoke(int $id): Response { - $badge = $this->badgeRepository->get($id); + $badge = $this->badgeRepository->find($id); if (null === $badge) { throw $this->createNotFoundException(); } diff --git a/sources/AppBundle/Event/Entity/Badge.php b/sources/AppBundle/Event/Entity/Badge.php new file mode 100644 index 000000000..e6f1d22e4 --- /dev/null +++ b/sources/AppBundle/Event/Entity/Badge.php @@ -0,0 +1,24 @@ + + */ +final class BadgeRepository extends EntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Badge::class); + } +} diff --git a/sources/AppBundle/Event/Entity/Repository/UserBadgeRepository.php b/sources/AppBundle/Event/Entity/Repository/UserBadgeRepository.php index 12f6a0c8e..dc87b7ade 100644 --- a/sources/AppBundle/Event/Entity/Repository/UserBadgeRepository.php +++ b/sources/AppBundle/Event/Entity/Repository/UserBadgeRepository.php @@ -5,7 +5,6 @@ namespace AppBundle\Event\Entity\Repository; use AppBundle\Doctrine\EntityRepository; -use AppBundle\Event\Entity\BadgeAttribue; use AppBundle\Event\Entity\UserBadge; use Doctrine\Persistence\ManagerRegistry; @@ -20,44 +19,20 @@ public function __construct(ManagerRegistry $registry) } /** - * Retourne les badges attribués à un membre, triés par date d'attribution. - * - * La jointure avec afup_badge se fait en DBAL : cette table est encore - * portée par l'entité Ting AppBundle\Event\Model\Badge. - * - * @return list + * @return list */ public function findByUserId(int $userId): array { - $rows = $this->getEntityManager()->getConnection()->createQueryBuilder() - ->select('badge.id AS badge_id', 'badge.label AS badge_label', 'user_badge.issued_at AS issued_at') - ->from('afup_personnes_physiques_badge', 'user_badge') - ->innerJoin('user_badge', 'afup_badge', 'badge', 'user_badge.badge_id = badge.id') - ->where('user_badge.afup_personne_physique_id = :user_id') - ->setParameter('user_id', $userId) - ->orderBy('user_badge.issued_at') - ->executeQuery() - ->fetchAllAssociative() + return $this->getEntityManager() + ->createQueryBuilder() + ->select('userBadge', 'badge') + ->from(UserBadge::class, 'userBadge') + ->innerJoin('userBadge.badge', 'badge') + ->where('userBadge.userId = :userId') + ->setParameter('userId', $userId) + ->orderBy('userBadge.issuedAt', 'ASC') + ->getQuery() + ->getResult() ; - - $badgesAttribues = []; - foreach ($rows as $row) { - $badgeId = $row['badge_id']; - $badgeLabel = $row['badge_label']; - $issuedAt = $row['issued_at']; - - if (!is_int($badgeId) || !is_string($badgeLabel) || !is_string($issuedAt)) { - throw new \RuntimeException('Données inattendues dans la table afup_personnes_physiques_badge.'); - } - - $badgesAttribues[] = new BadgeAttribue( - $userId, - $badgeId, - $badgeLabel, - new \DateTimeImmutable($issuedAt), - ); - } - - return $badgesAttribues; } } diff --git a/sources/AppBundle/Event/Entity/UserBadge.php b/sources/AppBundle/Event/Entity/UserBadge.php index e1b810e22..09f2d8aaf 100644 --- a/sources/AppBundle/Event/Entity/UserBadge.php +++ b/sources/AppBundle/Event/Entity/UserBadge.php @@ -16,9 +16,10 @@ class UserBadge public int $userId; #[ORM\Id] - #[ORM\Column(name: 'badge_id')] - public int $badgeId; + #[ORM\ManyToOne(targetEntity: Badge::class)] + #[ORM\JoinColumn(name: 'badge_id', nullable: false)] + public Badge $badge; - #[ORM\Column(name: 'issued_at', type: 'date_immutable')] - public \DateTimeImmutable $issuedAt; + #[ORM\Column(name: 'issued_at', type: 'date')] + public \DateTimeInterface $issuedAt; } diff --git a/sources/AppBundle/Event/Model/Badge.php b/sources/AppBundle/Event/Model/Badge.php deleted file mode 100644 index 7767a6607..000000000 --- a/sources/AppBundle/Event/Model/Badge.php +++ /dev/null @@ -1,58 +0,0 @@ -id; - } - - public function setId(int $id): self - { - $this->propertyChanged('id', $this->id, $id); - $this->id = $id; - - return $this; - } - - public function getLabel(): string - { - return $this->label; - } - - public function setLabel(string $label): self - { - $this->propertyChanged('label', $this->label ?? '', $label); - $this->label = $label; - - return $this; - } - - public function getUrl(): string - { - return $this->url; - } - - public function setUrl(string $url): self - { - $this->propertyChanged('url', $this->url ?? '', $url); - $this->url = $url; - - return $this; - } -} diff --git a/sources/AppBundle/Event/Model/Repository/BadgeRepository.php b/sources/AppBundle/Event/Model/Repository/BadgeRepository.php deleted file mode 100644 index 3a0aa9afb..000000000 --- a/sources/AppBundle/Event/Model/Repository/BadgeRepository.php +++ /dev/null @@ -1,55 +0,0 @@ - - */ -class BadgeRepository extends Repository implements MetadataInitializer -{ - /** - * - * @return Metadata - * - * @throws Exception - */ - public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) - { - $metadata = new Metadata($serializerFactory); - $metadata->setEntity(Badge::class); - $metadata->setConnectionName('main'); - $metadata->setDatabase($options['database']); - $metadata->setTable('afup_badge'); - - $metadata - ->addField([ - 'columnName' => 'id', - 'fieldName' => 'id', - 'primary' => true, - 'autoincrement' => true, - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'label', - 'fieldName' => 'label', - 'type' => 'string', - ]) - ->addField([ - 'columnName' => 'url', - 'fieldName' => 'url', - 'type' => 'string', - ]) - ; - - return $metadata; - } -} diff --git a/templates/admin/members/user_badge/user_badges.html.twig b/templates/admin/members/user_badge/user_badges.html.twig index f82bb3106..3e152b257 100644 --- a/templates/admin/members/user_badge/user_badges.html.twig +++ b/templates/admin/members/user_badge/user_badges.html.twig @@ -11,14 +11,14 @@ {% for user_badge in user_badges %} - {{ user_badge.badgeLabel }} + {{ user_badge.badge.label }} {{ user_badge.issuedAt|date('d/m/Y') }} - diff --git a/tests/integration/AppBundle/Event/Entity/Repository/UserBadgeRepositoryTest.php b/tests/integration/AppBundle/Event/Entity/Repository/UserBadgeRepositoryTest.php index 937749dcc..b69511b51 100644 --- a/tests/integration/AppBundle/Event/Entity/Repository/UserBadgeRepositoryTest.php +++ b/tests/integration/AppBundle/Event/Entity/Repository/UserBadgeRepositoryTest.php @@ -5,88 +5,83 @@ namespace AppBundle\IntegrationTests\Event\Entity\Repository; use Afup\Tests\Support\IntegrationTestCase; +use AppBundle\Event\Entity\Badge; +use AppBundle\Event\Entity\Repository\BadgeRepository; use AppBundle\Event\Entity\Repository\UserBadgeRepository; use AppBundle\Event\Entity\UserBadge; use Doctrine\DBAL\Connection; final class UserBadgeRepositoryTest extends IntegrationTestCase { - public function testSavePersistsTheAssociation(): void + public function testFindByUserIdReturnsBadgesJoinedAndSortedByIssueDate(): void { - $repository = self::getContainer()->get(UserBadgeRepository::class); + $userBadgeRepository = self::getContainer()->get(UserBadgeRepository::class); $connection = self::getContainer()->get(Connection::class); $this->insertBadge($connection, 1, 'Speaker 2025'); - $repository->save($this->buildUserBadge(101, 1, '2025-09-01')); + $this->insertBadge($connection, 2, 'Membre depuis 10 ans'); + $this->insertUserBadge($connection, 42, 2, '2025-01-01'); + $this->insertUserBadge($connection, 42, 1, '2026-06-15'); + $this->insertUserBadge($connection, 43, 1, '2026-01-01'); - $rows = $connection->fetchAllAssociative( - 'SELECT afup_personne_physique_id, badge_id, issued_at FROM afup_personnes_physiques_badge WHERE afup_personne_physique_id = 101', - ); + $userBadges = $userBadgeRepository->findByUserId(42); - self::assertCount(1, $rows); - self::assertSame('101', (string) $rows[0]['afup_personne_physique_id']); - self::assertSame('1', (string) $rows[0]['badge_id']); - self::assertSame('2025-09-01', (string) $rows[0]['issued_at']); + self::assertCount(2, $userBadges); + // Tri par date d'attribution croissante + self::assertSame(2, $userBadges[0]->badge->id); + self::assertSame('Membre depuis 10 ans', $userBadges[0]->badge->label); + self::assertSame('2025-01-01', $userBadges[0]->issuedAt->format('Y-m-d')); + self::assertSame(1, $userBadges[1]->badge->id); + self::assertSame(42, $userBadges[0]->userId); } - public function testFindByUserIdReturnsBadgesWithLabelOrderedByDate(): void + public function testFindByUserIdReturnsEmptyArrayForUserWithoutBadge(): void { - $repository = self::getContainer()->get(UserBadgeRepository::class); + $userBadgeRepository = self::getContainer()->get(UserBadgeRepository::class); $connection = self::getContainer()->get(Connection::class); $this->insertBadge($connection, 1, 'Speaker 2025'); - $this->insertBadge($connection, 2, 'Benevole 2024'); - $this->insertUserBadge($connection, 101, 1, '2025-01-01'); - $this->insertUserBadge($connection, 101, 2, '2024-01-01'); - - $badges = $repository->findByUserId(101); - - // Tri par date d'attribution croissante - self::assertCount(2, $badges); - self::assertSame(101, $badges[0]->userId); - self::assertSame(2, $badges[0]->badgeId); - self::assertSame('Benevole 2024', $badges[0]->badgeLabel); - self::assertSame('2024-01-01', $badges[0]->issuedAt->format('Y-m-d')); - self::assertSame(1, $badges[1]->badgeId); - self::assertSame('Speaker 2025', $badges[1]->badgeLabel); - self::assertSame('2025-01-01', $badges[1]->issuedAt->format('Y-m-d')); - } - - public function testFindByUserIdReturnsEmptyListForUnknownUser(): void - { - $repository = self::getContainer()->get(UserBadgeRepository::class); + $this->insertUserBadge($connection, 42, 1, '2025-01-01'); - self::assertSame([], $repository->findByUserId(9999)); + self::assertSame([], $userBadgeRepository->findByUserId(999)); } - public function testDeleteRemovesTheAssociation(): void + public function testSaveAndDeleteThroughTheEntityManager(): void { - $repository = self::getContainer()->get(UserBadgeRepository::class); + $userBadgeRepository = self::getContainer()->get(UserBadgeRepository::class); + $badgeRepository = self::getContainer()->get(BadgeRepository::class); $connection = self::getContainer()->get(Connection::class); - $this->insertBadge($connection, 1, 'Speaker 2025'); - $this->insertUserBadge($connection, 101, 1, '2025-01-01'); - - $userBadge = $repository->find(['badgeId' => 1, 'userId' => 101]); - self::assertInstanceOf(UserBadge::class, $userBadge); + $badge = new Badge(); + $badge->label = 'Speaker 2026'; + $badge->url = 'badge_speaker_2026.png'; + $badgeRepository->save($badge); - $repository->delete($userBadge); + $userBadge = new UserBadge(); + $userBadge->badge = $badge; + $userBadge->userId = 42; + $userBadge->issuedAt = new \DateTime('2026-09-21'); + $userBadgeRepository->save($userBadge); $rows = $connection->fetchAllAssociative( - 'SELECT badge_id FROM afup_personnes_physiques_badge WHERE afup_personne_physique_id = 101', + 'SELECT * FROM afup_personnes_physiques_badge WHERE afup_personne_physique_id = :userId', + ['userId' => 42], ); + self::assertCount(1, $rows); + self::assertSame($badge->id, (int) $rows[0]['badge_id']); + self::assertSame('2026-09-21', $rows[0]['issued_at']); - self::assertSame([], $rows); - } - - private function buildUserBadge(int $userId, int $badgeId, string $issuedAt): UserBadge - { - $userBadge = new UserBadge(); - $userBadge->userId = $userId; - $userBadge->badgeId = $badgeId; - $userBadge->issuedAt = new \DateTimeImmutable($issuedAt); + $fromDatabase = $userBadgeRepository->findOneBy([ + 'userId' => 42, + 'badge' => $badge, + ]); + self::assertNotNull($fromDatabase); - return $userBadge; + $userBadgeRepository->delete($fromDatabase); + self::assertSame( + [], + $connection->fetchAllAssociative('SELECT * FROM afup_personnes_physiques_badge WHERE afup_personne_physique_id = :userId', ['userId' => 42]), + ); } private function insertBadge(Connection $connection, int $id, string $label): void @@ -94,7 +89,7 @@ private function insertBadge(Connection $connection, int $id, string $label): vo $connection->insert('afup_badge', [ 'id' => $id, 'label' => $label, - 'url' => 'https://afup.org/images/badges/test.png', + 'url' => 'badge_' . $id . '.png', ]); } From 691e77025f9ab39727b10a2aed8cf55a84c49099 Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Tue, 6 Oct 2026 08:53:43 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Corrige=20le=20type=20du=20r=C3=A9f=C3=A9re?= =?UTF-8?q?nt=20dans=20les=20redirections=20des=20badges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le referer peut être null; RedirectResponse attend une string. On retombe sur l'URI courante (pattern déjà utilisé dans PrepareAction). Cela corrige l'erreur PHPStan argument.type et retire les entrées de baseline devenues inutiles pour UserBadgeNewAction et UserBadgeDeleteAction. --- phpstan-baseline.php | 37 +------------------ .../Association/Form/UserBadgeType.php | 1 + .../Model/Repository/UserRepository.php | 5 ++- .../Admin/Members/BadgeListAction.php | 2 +- .../Admin/Members/UserBadgeDeleteAction.php | 12 +++--- .../Admin/Members/UserBadgeNewAction.php | 4 +- .../Controller/Website/Badge/ImageAction.php | 2 +- sources/AppBundle/Event/Entity/UserBadge.php | 6 ++- .../Repository/UserBadgeRepositoryTest.php | 2 +- 9 files changed, 21 insertions(+), 50 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 430700860..a89f8fddf 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -3481,12 +3481,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/HomeAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$badgeId of method AppBundle\\\\Association\\\\Model\\\\Repository\\\\UserRepository\\:\\:loadByBadge\\(\\) expects int, int\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'image\' on mixed\\.$#', 'identifier' => 'offsetAccess.nonOffsetAccessible', @@ -3601,24 +3595,6 @@ 'count' => 2, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/GeneralMeeting/ReportsAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot cast mixed to int\\.$#', - 'identifier' => 'cast.int', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$url of class Symfony\\\\Component\\\\HttpFoundation\\\\RedirectResponse constructor expects string, string\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$url of class Symfony\\\\Component\\\\HttpFoundation\\\\RedirectResponse constructor expects string, string\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Members/UserBadgeNewAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'first\' on mixed\\.$#', 'identifier' => 'offsetAccess.nonOffsetAccessible', @@ -4519,18 +4495,7 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/ValueResolver/AdminEventSelectionValueResolver.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Binary operation "\\." between non\\-falsy\\-string and mixed results in an error\\.$#', - 'identifier' => 'binaryOp.invalid', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Badge/ImageAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method AppBundle\\\\Event\\\\Entity\\\\Badge\\:\\:getUrl\\(\\)\\.$#', - 'identifier' => 'method.notFound', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Badge/ImageAction.php', -]; + $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$code of method AppBundle\\\\Antennes\\\\AntenneRepository\\:\\:findByCode\\(\\) expects string, mixed given\\.$#', 'identifier' => 'argument.type', diff --git a/sources/AppBundle/Association/Form/UserBadgeType.php b/sources/AppBundle/Association/Form/UserBadgeType.php index 49202dfc1..3f27c0bd1 100644 --- a/sources/AppBundle/Association/Form/UserBadgeType.php +++ b/sources/AppBundle/Association/Form/UserBadgeType.php @@ -51,6 +51,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void DateType::class, [ 'label' => 'Date', + 'input' => 'datetime_immutable', ], ) ->add( diff --git a/sources/AppBundle/Association/Model/Repository/UserRepository.php b/sources/AppBundle/Association/Model/Repository/UserRepository.php index eac176d5b..ca063f525 100644 --- a/sources/AppBundle/Association/Model/Repository/UserRepository.php +++ b/sources/AppBundle/Association/Model/Repository/UserRepository.php @@ -8,6 +8,7 @@ use AppBundle\Association\MemberType; use AppBundle\Association\Model\CompanyMember; use AppBundle\Association\Model\User; +use AppBundle\Event\Entity\Badge; use Aura\SqlQuery\Common\SelectInterface; use CCMBenchmark\Ting\Driver\Mysqli\Serializer\Boolean; use CCMBenchmark\Ting\Repository\CollectionInterface; @@ -144,7 +145,7 @@ public function loadAll() ; } - public function loadByBadge(int $badgeId) + public function loadByBadge(Badge $badge) { $queryBuilder = $this->getQueryBuilderWithCompleteUser(); @@ -153,7 +154,7 @@ public function loadByBadge(int $badgeId) return $this ->getPreparedQuery($queryBuilder->getStatement()) - ->setParams(['badge_id' => $badgeId]) + ->setParams(['badge_id' => $badge->id]) ->query($this->getCollection($this->getHydratorForUser())) ; } diff --git a/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php b/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php index 0fa27f6bc..fb93c22a5 100644 --- a/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php +++ b/sources/AppBundle/Controller/Admin/Members/BadgeListAction.php @@ -24,7 +24,7 @@ public function __invoke(Request $request): Response foreach ($this->badgeRepository->findAll() as $badge) { $infos[] = [ 'badge' => $badge, - 'users' => $this->userRepository->loadByBadge($badge->id), + 'users' => $this->userRepository->loadByBadge($badge), ]; } diff --git a/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php b/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php index bdbdc70f0..ea7ae971d 100644 --- a/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php +++ b/sources/AppBundle/Controller/Admin/Members/UserBadgeDeleteAction.php @@ -4,8 +4,10 @@ namespace AppBundle\Controller\Admin\Members; +use AppBundle\Event\Entity\Badge; use AppBundle\Event\Entity\Repository\BadgeRepository; use AppBundle\Event\Entity\Repository\UserBadgeRepository; +use AppBundle\Event\Entity\UserBadge; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -19,21 +21,21 @@ public function __construct( public function __invoke(Request $request): RedirectResponse { - $badgeId = (int) $request->attributes->get('badgeId'); + $badgeId = $request->attributes->getInt('badgeId'); $badge = $this->badgeRepository->find($badgeId); - if (null === $badge) { + if (! $badge instanceof Badge) { throw new NotFoundHttpException(sprintf('Badge %d inexistant', $badgeId)); } $userBadge = $this->userBadgeRepository->findOneBy([ - 'userId' => (int) $request->attributes->get('userId'), + 'userId' => $request->attributes->getInt('userId'), 'badge' => $badge, ]); - if (null === $userBadge) { + if (! $userBadge instanceof UserBadge) { throw new NotFoundHttpException('Attribution de badge introuvable'); } $this->userBadgeRepository->delete($userBadge); - return new RedirectResponse($request->headers->get('referer')); + return new RedirectResponse($request->headers->get('referer') ?? $request->getRequestUri()); } } diff --git a/sources/AppBundle/Controller/Admin/Members/UserBadgeNewAction.php b/sources/AppBundle/Controller/Admin/Members/UserBadgeNewAction.php index 4aa9a71b2..c9ebb9834 100644 --- a/sources/AppBundle/Controller/Admin/Members/UserBadgeNewAction.php +++ b/sources/AppBundle/Controller/Admin/Members/UserBadgeNewAction.php @@ -29,7 +29,7 @@ public function __invoke(Request $request): RedirectResponse $userBadgeForm = $this->formFactory->create(UserBadgeType::class, [], ['user' => $user]); $userBadgeForm->handleRequest($request); if (!$userBadgeForm->isSubmitted() || !$userBadgeForm->isValid()) { - return new RedirectResponse($request->headers->get('referer')); + return new RedirectResponse($request->headers->get('referer') ?? $request->getRequestUri()); } /** @var array{badge: int, date: \DateTimeInterface, user: string} $data */ @@ -47,6 +47,6 @@ public function __invoke(Request $request): RedirectResponse $userBadge->userId = (int) $data['user']; $this->userBadgeRepository->save($userBadge); - return new RedirectResponse($request->headers->get('referer')); + return new RedirectResponse($request->headers->get('referer') ?? $request->getRequestUri()); } } diff --git a/sources/AppBundle/Controller/Website/Badge/ImageAction.php b/sources/AppBundle/Controller/Website/Badge/ImageAction.php index 4bad94079..bb7b7eccb 100644 --- a/sources/AppBundle/Controller/Website/Badge/ImageAction.php +++ b/sources/AppBundle/Controller/Website/Badge/ImageAction.php @@ -25,7 +25,7 @@ public function __invoke(int $id): Response throw $this->createNotFoundException(); } - $filepath = $this->storageDir . DIRECTORY_SEPARATOR . $badge->getUrl(); + $filepath = $this->storageDir . DIRECTORY_SEPARATOR . $badge->url; if (false === is_file($filepath)) { throw $this->createNotFoundException(); } diff --git a/sources/AppBundle/Event/Entity/UserBadge.php b/sources/AppBundle/Event/Entity/UserBadge.php index 09f2d8aaf..dc855c547 100644 --- a/sources/AppBundle/Event/Entity/UserBadge.php +++ b/sources/AppBundle/Event/Entity/UserBadge.php @@ -20,6 +20,8 @@ class UserBadge #[ORM\JoinColumn(name: 'badge_id', nullable: false)] public Badge $badge; - #[ORM\Column(name: 'issued_at', type: 'date')] - public \DateTimeInterface $issuedAt; + #[ORM\Column(name: 'issued_at', type: 'date_immutable', nullable: false)] + // date_immutable column hydrates DateTimeImmutable; property stays + // \DateTimeInterface by choice (DBAL rejects mutable writes anyway). + public \DateTimeInterface $issuedAt; // @phpstan-ignore doctrine.columnType } diff --git a/tests/integration/AppBundle/Event/Entity/Repository/UserBadgeRepositoryTest.php b/tests/integration/AppBundle/Event/Entity/Repository/UserBadgeRepositoryTest.php index b69511b51..003ec5dda 100644 --- a/tests/integration/AppBundle/Event/Entity/Repository/UserBadgeRepositoryTest.php +++ b/tests/integration/AppBundle/Event/Entity/Repository/UserBadgeRepositoryTest.php @@ -60,7 +60,7 @@ public function testSaveAndDeleteThroughTheEntityManager(): void $userBadge = new UserBadge(); $userBadge->badge = $badge; $userBadge->userId = 42; - $userBadge->issuedAt = new \DateTime('2026-09-21'); + $userBadge->issuedAt = new \DateTimeImmutable('2026-09-21'); $userBadgeRepository->save($userBadge); $rows = $connection->fetchAllAssociative(