From 6458a6948a8a453dba4e3deda3f797339592946b Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Mon, 21 Sep 2026 19:41:17 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20migration=20de=20l'entit=C3=A9=20Pl?= =?UTF-8?q?anning=20de=20Ting=20vers=20Doctrine=20(#2383)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/packages/twig.yaml | 2 +- phpstan-baseline.php | 150 ------------------ .../Calendar/IcsPlanningGenerator.php | 18 ++- .../Calendar/JsonPlanningGenerator.php | 14 +- .../AppBundle/Command/VideosDataCommand.php | 8 +- .../Event/Session/CalendarAjaxAction.php | 12 +- .../Admin/Event/Session/DeleteAction.php | 6 +- .../Admin/Event/Session/EditAction.php | 25 +-- .../Admin/Event/Session/IndexAction.php | 16 +- .../Controller/Event/Blog/PlanningAction.php | 14 +- .../Event/Blog/TalkWidgetAction.php | 8 + .../Controller/Website/Talks/ShowAction.php | 7 +- sources/AppBundle/Event/Entity/Planning.php | 51 ++++++ .../Entity/Repository/PlanningRepository.php | 108 +++++++++++++ sources/AppBundle/Event/JsonLd.php | 10 +- sources/AppBundle/Event/Model/Planning.php | 130 --------------- .../Model/Repository/PlanningRepository.php | 131 --------------- .../Event/Model/Repository/TalkRepository.php | 16 +- .../AppBundle/Event/Model/TalkAggregate.php | 2 + .../AppBundle/Event/Speaker/SpeakerPage.php | 8 +- .../AppBundle/Event/Talk/ExportGenerator.php | 16 +- sources/AppBundle/Indexation/Talks/Runner.php | 17 +- .../Indexation/Talks/Transformer.php | 4 +- .../OpenfeedbackJsonGenerator.php | 10 +- sources/AppBundle/VideoNotifier/Engine.php | 6 +- .../Repository/PlanningRepositoryTest.php | 94 +++++++++++ .../Indexation/Talks/TransformerTest.php | 12 +- 27 files changed, 402 insertions(+), 493 deletions(-) create mode 100644 sources/AppBundle/Event/Entity/Planning.php create mode 100644 sources/AppBundle/Event/Entity/Repository/PlanningRepository.php delete mode 100644 sources/AppBundle/Event/Model/Planning.php delete mode 100644 sources/AppBundle/Event/Model/Repository/PlanningRepository.php create mode 100644 tests/integration/AppBundle/Event/Entity/Repository/PlanningRepositoryTest.php diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 6720b9be7..b537358e4 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -4,6 +4,6 @@ twig: globals: photo_storage: '@AppBundle\CFP\PhotoStorage' global_menu_event_label: '%env(AFUP_GLOBAL_MENU_EVENT_LABEL)%' - planning_timezone: !php/const AppBundle\Event\Model\Planning::TIMEZONE + planning_timezone: !php/const AppBundle\Event\Entity\Planning::TIMEZONE form_themes: ['form_theme.html.twig'] default_path: "%kernel.project_dir%/templates" diff --git a/phpstan-baseline.php b/phpstan-baseline.php index f85ead6d8..f376a09bb 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -1993,42 +1993,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/CFP/ViewModel/EventTalkList.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getEnd\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Calendar/IcsPlanningGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getName\\(\\) on AppBundle\\\\Event\\\\Model\\\\Room\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Calendar/IcsPlanningGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getStart\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Calendar/IcsPlanningGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getEnd\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Calendar/JsonPlanningGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getName\\(\\) on AppBundle\\\\Event\\\\Model\\\\Room\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Calendar/JsonPlanningGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getStart\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Calendar/JsonPlanningGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method setTimezone\\(\\) on DateTime\\|null\\.$#', 'identifier' => 'method.nonObject', @@ -2239,18 +2203,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Command/UpdateCompanyMemberStateCommand.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method format\\(\\) on DateTime\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Command/VideosDataCommand.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getStart\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Command/VideosDataCommand.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$messages of method Symfony\\\\Component\\\\Console\\\\Output\\\\OutputInterface\\:\\:writeln\\(\\) expects iterable\\\\|string, string\\|false given\\.$#', 'identifier' => 'argument.type', @@ -3103,30 +3055,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/DeleteAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getEnd\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getId\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getStart\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method setRoomId\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Method AppBundle\\\\Controller\\\\Admin\\\\Event\\\\Session\\\\EditAction\\:\\:getForm\\(\\) has parameter \\$roomChoices with no value type specified in iterable type array\\.$#', 'identifier' => 'missingType.iterableValue', @@ -3145,30 +3073,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$data of method AppBundle\\\\Controller\\\\Admin\\\\Event\\\\Session\\\\EditAction\\:\\:getForm\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Planning, AppBundle\\\\Event\\\\Model\\\\Planning\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$entity of method CCMBenchmark\\\\Ting\\\\Repository\\\\Repository\\\\:\\:save\\(\\) expects AppBundle\\\\Event\\\\Model\\\\Planning, AppBundle\\\\Event\\\\Model\\\\Planning\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$eventId of method AppBundle\\\\Event\\\\Model\\\\Planning\\:\\:setEventId\\(\\) expects int, int\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$talkId of method AppBundle\\\\Event\\\\Model\\\\Planning\\:\\:setTalkId\\(\\) expects int, int\\|null given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Possibly invalid array key type mixed\\.$#', 'identifier' => 'offsetAccess.invalidOffset', @@ -5239,12 +5143,6 @@ 'count' => 2, 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Talks/ListAction.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot call method getEventId\\(\\) on AppBundle\\\\Event\\\\Model\\\\Planning\\|null\\.$#', - 'identifier' => 'method.nonObject', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Controller/Website/Talks/ShowAction.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|T of object, string given\\.$#', 'identifier' => 'argument.type', @@ -6067,18 +5965,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Lead.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot cast mixed to int\\.$#', - 'identifier' => 'cast.int', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Planning.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Planning\\:\\:setId\\(\\) has parameter \\$id with no type specified\\.$#', - 'identifier' => 'missingType.parameter', - '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', @@ -6499,30 +6385,6 @@ 'count' => 1, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\PlanningRepository\\:\\:getByTalk\\(\\) should return AppBundle\\\\Event\\\\Model\\\\Planning\\|null but returns mixed\\.$#', - 'identifier' => 'return.type', - 'count' => 1, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/PlanningRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\PlanningRepository\\:\\: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/PlanningRepository.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\PlanningRepository\\:\\: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/PlanningRepository.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/PlanningRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Method AppBundle\\\\Event\\\\Model\\\\Repository\\\\RoomRepository\\:\\:initMetadata\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#', 'identifier' => 'missingType.iterableValue', @@ -6733,12 +6595,6 @@ 'count' => 2, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'planning\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'room\' on mixed\\.$#', 'identifier' => 'offsetAccess.nonOffsetAccessible', @@ -6895,12 +6751,6 @@ 'count' => 2, 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#4 \\$planning of class AppBundle\\\\Event\\\\Model\\\\TalkAggregate constructor expects AppBundle\\\\Event\\\\Model\\\\Planning\\|null, mixed given\\.$#', - 'identifier' => 'argument.type', - 'count' => 2, - 'path' => __DIR__ . '/sources/AppBundle/Event/Model/Repository/TalkRepository.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot access offset \'count\' on mixed\\.$#', 'identifier' => 'offsetAccess.nonOffsetAccessible', diff --git a/sources/AppBundle/Calendar/IcsPlanningGenerator.php b/sources/AppBundle/Calendar/IcsPlanningGenerator.php index f5adb2838..46db64d67 100644 --- a/sources/AppBundle/Calendar/IcsPlanningGenerator.php +++ b/sources/AppBundle/Calendar/IcsPlanningGenerator.php @@ -4,27 +4,37 @@ namespace AppBundle\Calendar; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\TalkRepository; use Sabre\VObject\Component\VCalendar; class IcsPlanningGenerator { - public function __construct(private readonly TalkRepository $talkRepository) {} + public function __construct( + private readonly TalkRepository $talkRepository, + private readonly PlanningRepository $planningRepository, + ) {} public function generateForEvent(Event $event): string { - $talkAggregates = $this->talkRepository->getByEventWithSpeakers($event); + $talkAggregates = $this->planningRepository->enrichTalkAggregates( + $this->talkRepository->getByEventWithSpeakers($event), + ); $events = []; $vcalendar = new VCalendar($events); $vcalendar->add('X-WR-CALNAME', $event->getTitle()); foreach ($talkAggregates as $talkAggregate) { + if ($talkAggregate->planning === null || $talkAggregate->room === null) { + continue; + } + $vcalendar->add('VEVENT', [ 'SUMMARY' => $talkAggregate->talk->getTitle(), - 'DTSTART' => $talkAggregate->planning->getStart(), - 'DTEND' => $talkAggregate->planning->getEnd(), + 'DTSTART' => $talkAggregate->planning->start, + 'DTEND' => $talkAggregate->planning->end, 'DESCRIPTION' => strip_tags(html_entity_decode($talkAggregate->talk->getAbstract())), 'LOCATION' => $talkAggregate->room->getName(), ]); diff --git a/sources/AppBundle/Calendar/JsonPlanningGenerator.php b/sources/AppBundle/Calendar/JsonPlanningGenerator.php index 5c6df5538..5f5a287f0 100644 --- a/sources/AppBundle/Calendar/JsonPlanningGenerator.php +++ b/sources/AppBundle/Calendar/JsonPlanningGenerator.php @@ -5,6 +5,7 @@ namespace AppBundle\Calendar; use AppBundle\CFP\PhotoStorage; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\TalkRepository; @@ -12,6 +13,7 @@ class JsonPlanningGenerator { public function __construct( private readonly TalkRepository $talkRepository, + private readonly PlanningRepository $planningRepository, private readonly PhotoStorage $photoStorage, ) {} @@ -20,11 +22,17 @@ public function __construct( */ public function generate(Event $event): array { - $talkAggregates = $this->talkRepository->getByEventWithSpeakers($event); + $talkAggregates = $this->planningRepository->enrichTalkAggregates( + $this->talkRepository->getByEventWithSpeakers($event), + ); $data = []; foreach ($talkAggregates as $talkAggregate) { + if ($talkAggregate->planning === null || $talkAggregate->room === null) { + continue; + } + $conferenciers = []; foreach ($talkAggregate->speakers as $speaker) { $conferenciers[] = [ @@ -35,8 +43,8 @@ public function generate(Event $event): array } $timeZone = new \DateTimeZone("Europe/Paris"); - $start = $talkAggregate->planning->getStart()->setTimezone($timeZone); - $end = $talkAggregate->planning->getEnd()->setTimezone($timeZone); + $start = $talkAggregate->planning->start->setTimezone($timeZone); + $end = $talkAggregate->planning->end->setTimezone($timeZone); $data[] = [ 'conferenciers' => $conferenciers, diff --git a/sources/AppBundle/Command/VideosDataCommand.php b/sources/AppBundle/Command/VideosDataCommand.php index c69e295b6..a11b9df1d 100644 --- a/sources/AppBundle/Command/VideosDataCommand.php +++ b/sources/AppBundle/Command/VideosDataCommand.php @@ -4,6 +4,7 @@ namespace AppBundle\Command; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Repository\EventRepository; use AppBundle\Event\Model\Repository\TalkRepository; use Symfony\Component\Console\Command\Command; @@ -15,6 +16,7 @@ class VideosDataCommand extends Command { public function __construct( private readonly TalkRepository $talkRepository, + private readonly PlanningRepository $planningRepository, private readonly EventRepository $eventRepository, ) { parent::__construct(); @@ -36,7 +38,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new \InvalidArgumentException("Event not found"); } - $talkAggregates = $this->talkRepository->getByEventWithSpeakers($event); + $talkAggregates = $this->planningRepository->enrichTalkAggregates( + $this->talkRepository->getByEventWithSpeakers($event), + ); $data = []; @@ -51,7 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'title' => sprintf("%s - %s - %s", $talkAggregate->talk->getTitle(), implode(',', $speakersNames), $event->getTitle()), "language" => $talkAggregate->talk->getLanguageCode(), 'url' => "https://afup.org/talks/" . $talkAggregate->talk->getUrlKey(), - 'recording_date' => $talkAggregate->planning->getStart()->format(\Datetime::ISO8601), + 'recording_date' => $talkAggregate->planning?->start?->format(\Datetime::ISO8601), ]; } diff --git a/sources/AppBundle/Controller/Admin/Event/Session/CalendarAjaxAction.php b/sources/AppBundle/Controller/Admin/Event/Session/CalendarAjaxAction.php index 89772f6e8..fcce0cf69 100644 --- a/sources/AppBundle/Controller/Admin/Event/Session/CalendarAjaxAction.php +++ b/sources/AppBundle/Controller/Admin/Event/Session/CalendarAjaxAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Admin\Event\Session; -use AppBundle\Event\Model\Planning; -use AppBundle\Event\Model\Repository\PlanningRepository; +use AppBundle\Event\Entity\Planning; +use AppBundle\Event\Entity\Repository\PlanningRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -16,7 +16,7 @@ public function __construct(private readonly PlanningRepository $planningReposit public function __invoke(int $id, Request $request): Response { - $planning = $this->planningRepository->get($id); + $planning = $this->planningRepository->find($id); if (!$planning) { throw $this->createNotFoundException('Planning not found: ' . $id); } @@ -26,9 +26,9 @@ public function __invoke(int $id, Request $request): Response // c'est donc l'heure locale de l'événement, pas celle du navigateur. $timezone = new \DateTimeZone(Planning::TIMEZONE); - $planning->setStart(new \DateTime($data['start'], $timezone)); - $planning->setEnd(new \DateTime($data['end'], $timezone)); - $planning->setRoomId((int) $data['roomId']); + $planning->start = new \DateTime($data['start'], $timezone); + $planning->end = new \DateTime($data['end'], $timezone); + $planning->roomId = (int) $data['roomId']; $this->planningRepository->save($planning); diff --git a/sources/AppBundle/Controller/Admin/Event/Session/DeleteAction.php b/sources/AppBundle/Controller/Admin/Event/Session/DeleteAction.php index ef200956d..e6bdc1c46 100644 --- a/sources/AppBundle/Controller/Admin/Event/Session/DeleteAction.php +++ b/sources/AppBundle/Controller/Admin/Event/Session/DeleteAction.php @@ -5,8 +5,8 @@ namespace AppBundle\Controller\Admin\Event\Session; use AppBundle\AuditLog\Audit; -use AppBundle\Event\Model\Planning; -use AppBundle\Event\Model\Repository\PlanningRepository; +use AppBundle\Event\Entity\Planning; +use AppBundle\Event\Entity\Repository\PlanningRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -21,7 +21,7 @@ public function __construct( public function __invoke(Request $request): Response { $id = $request->attributes->get('id'); - $planning = $this->planningRepository->get($id); + $planning = $this->planningRepository->find($id); if (!$planning instanceof Planning) { throw $this->createNotFoundException(sprintf('Planning not found with id "%s".', $id)); } diff --git a/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php b/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php index f49806c72..1204d31dc 100644 --- a/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php +++ b/sources/AppBundle/Controller/Admin/Event/Session/EditAction.php @@ -5,10 +5,10 @@ namespace AppBundle\Controller\Admin\Event\Session; use AppBundle\AuditLog\Audit; +use AppBundle\Event\Entity\Planning; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Event; -use AppBundle\Event\Model\Planning; use AppBundle\Event\Model\Repository\EventRepository; -use AppBundle\Event\Model\Repository\PlanningRepository; use AppBundle\Event\Model\Repository\RoomRepository; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Event\Model\Room; @@ -43,21 +43,24 @@ public function __invoke(Request $request): Response $roomChoices = $this->roomChoices($event); if ($request->attributes->get('sessionId')) { - $planning = $this->planningRepository->get($request->attributes->get('sessionId')); + $planning = $this->planningRepository->find($request->attributes->get('sessionId')); + if (!$planning instanceof Planning) { + throw $this->createNotFoundException('Planning not found'); + } } else { $planning = new Planning(); - $planning->setTalkId($talk->getId()); - $planning->setEventId($event->getId()); - $planning->setStart($this->firstDayOfEvent($event)); - $planning->setEnd($this->firstDayOfEvent($event)); + $planning->talkId = $talk->getId(); + $planning->eventId = $event->getId(); + $planning->start = $this->firstDayOfEvent($event); + $planning->end = $this->firstDayOfEvent($event); } $form = $this->getForm($planning, $roomChoices); if ($request->query->get('mode') === 'add') { - $planning->getStart()?->setTime(9, 0); - $planning->getEnd()?->setTime(9, 40); - $planning->setRoomId(array_first($roomChoices)); + $planning->start?->setTime(9, 0); + $planning->end?->setTime(9, 40); + $planning->roomId = array_first($roomChoices); $this->planningRepository->save($planning); @@ -66,7 +69,7 @@ public function __invoke(Request $request): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $isNew = !$planning->getId(); + $isNew = $planning->id === null; $this->planningRepository->save($planning); diff --git a/sources/AppBundle/Controller/Admin/Event/Session/IndexAction.php b/sources/AppBundle/Controller/Admin/Event/Session/IndexAction.php index 4960342b6..3da3ace62 100644 --- a/sources/AppBundle/Controller/Admin/Event/Session/IndexAction.php +++ b/sources/AppBundle/Controller/Admin/Event/Session/IndexAction.php @@ -5,8 +5,9 @@ namespace AppBundle\Controller\Admin\Event\Session; use AppBundle\Event\AdminEventSelection; +use AppBundle\Event\Entity\Planning; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Event; -use AppBundle\Event\Model\Planning; use AppBundle\Event\Model\Repository\RoomRepository; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Event\Model\Room; @@ -21,13 +22,16 @@ final class IndexAction extends AbstractController { public function __construct( private readonly TalkRepository $talkRepository, + private readonly PlanningRepository $planningRepository, private readonly RoomRepository $roomRepository, ) {} public function __invoke(Request $request, AdminEventSelection $eventSelection): Response { $event = $eventSelection->event; - $sessions = $this->talkRepository->getByEventWithSpeakers($event, false); + $sessions = $this->planningRepository->enrichTalkAggregates( + $this->talkRepository->getByEventWithSpeakers($event, false), + ); return $this->render('event/session/index.html.twig', [ 'event' => $event, @@ -73,14 +77,14 @@ private function calendarEvents(array $sessions): array $events = []; foreach ($sessions as $session) { - if (!$session->planning || !$session->room || !$session->planning->getStart() || !$session->planning->getEnd()) { + if (!$session->planning || !$session->room || !$session->planning->start || !$session->planning->end) { continue; } $events[] = new CalendarEvent( - $session->planning->getId(), + $session->planning->id, $session->talk->getTitle(), - $this->formatForCalendar($session->planning->getStart(), $timezone), - $this->formatForCalendar($session->planning->getEnd(), $timezone), + $this->formatForCalendar($session->planning->start, $timezone), + $this->formatForCalendar($session->planning->end, $timezone), $session->room->getId(), ); } diff --git a/sources/AppBundle/Controller/Event/Blog/PlanningAction.php b/sources/AppBundle/Controller/Event/Blog/PlanningAction.php index ec3eb090e..730d5b44f 100644 --- a/sources/AppBundle/Controller/Event/Blog/PlanningAction.php +++ b/sources/AppBundle/Controller/Event/Blog/PlanningAction.php @@ -5,6 +5,7 @@ namespace AppBundle\Controller\Event\Blog; use AppBundle\Controller\Event\EventActionHelper; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\JsonLd; use AppBundle\Event\Model\Repository\TalkRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -17,6 +18,7 @@ public function __construct( private readonly JsonLd $jsonLd, private readonly EventActionHelper $eventActionHelper, private readonly TalkRepository $talkRepository, + private readonly PlanningRepository $planningRepository, ) {} public function __invoke(Request $request, string $eventSlug): Response @@ -30,7 +32,9 @@ public function __invoke(Request $request, string $eventSlug): Response $applyPublicationDateFilters = $request->query->getBoolean('apply-publication-date-filters', true); - $talkAggregates = $this->talkRepository->getByEventsWithSpeakers($events, $applyPublicationDateFilters); + $talkAggregates = $this->planningRepository->enrichTalkAggregates( + $this->talkRepository->getByEventsWithSpeakers($events, $applyPublicationDateFilters), + ); $jsonld = []; foreach ($events as $event) { @@ -52,14 +56,14 @@ public function __invoke(Request $request, string $eventSlug): Response continue; } - $startDay = $planning->getStart()->format('d/m/Y'); + $startDay = $planning->start->format('d/m/Y'); if (isset($eventPlanning[$startDay]) === false) { $eventPlanning[$startDay] = []; } - $dateStart = $planning->getStart()->setTimezone(new \DateTimeZone('Europe/Paris')); + $dateStart = $planning->start->setTimezone(new \DateTimeZone('Europe/Paris')); $start = $dateStart->format('d/m/Y H:i'); - $dateEnd = $planning->getEnd()->setTimezone(new \DateTimeZone('Europe/Paris')); + $dateEnd = $planning->end->setTimezone(new \DateTimeZone('Europe/Paris')); if ($dateStart->format('H') < $hourMin || $hourMin === null) { $hourMin = $dateStart->format('H'); @@ -73,7 +77,7 @@ public function __invoke(Request $request, string $eventSlug): Response $eventPlanning[$startDay][$start] = []; } - $interval = $planning->getEnd()->diff($planning->getStart()); + $interval = $planning->end->diff($planning->start); $defaultProgramPagePrefix = '/'; if (isset($events[$talk->getForumId()])) { diff --git a/sources/AppBundle/Controller/Event/Blog/TalkWidgetAction.php b/sources/AppBundle/Controller/Event/Blog/TalkWidgetAction.php index 8c56d9d91..34a00e055 100644 --- a/sources/AppBundle/Controller/Event/Blog/TalkWidgetAction.php +++ b/sources/AppBundle/Controller/Event/Blog/TalkWidgetAction.php @@ -5,6 +5,7 @@ namespace AppBundle\Controller\Event\Blog; use AppBundle\Event\Entity\Repository\InterviewRepository; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Event\Model\Speaker; @@ -17,6 +18,7 @@ final class TalkWidgetAction extends AbstractController public function __construct( private readonly TalkRepository $talkRepository, private readonly InterviewRepository $interviewRepository, + private readonly PlanningRepository $planningRepository, ) {} public function __invoke(Request $request): Response @@ -24,11 +26,17 @@ public function __invoke(Request $request): Response $widgetType = $request->query->get('type', 'all'); $talks = $this->talkRepository->getBy(['id' => explode(',', (string) $request->query->get('ids'))]); + $talkIds = array_map(static fn($talk) => (int) $talk->getId(), iterator_to_array($talks, false)); + $plannings = $this->planningRepository->findIndexedByTalkIds($talkIds); + $event = null; $speakers = []; $talksInfos = []; foreach ($talks as $talk) { foreach ($this->talkRepository->getByTalkWithSpeakers($talk) as $row) { + // Les plannings ne sont plus hydratés par Ting : ils sont rechargés + // via Doctrine avec PlanningRepository::findIndexedByTalkIds(). + $row['planning'] = $talk->getId() !== null ? ($plannings[$talk->getId()] ?? null) : null; if (($row['event'] ?? null) instanceof Event) { $event = $row['event']; } diff --git a/sources/AppBundle/Controller/Website/Talks/ShowAction.php b/sources/AppBundle/Controller/Website/Talks/ShowAction.php index f92e23c76..deda565b9 100644 --- a/sources/AppBundle/Controller/Website/Talks/ShowAction.php +++ b/sources/AppBundle/Controller/Website/Talks/ShowAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Website\Talks; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Repository\EventRepository; -use AppBundle\Event\Model\Repository\PlanningRepository; use AppBundle\Event\Model\Repository\SpeakerRepository; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Joindin\JoindinComments; @@ -35,7 +35,10 @@ public function __invoke(int $id, string $slug): Response $speakers = $this->speakerRepository->getSpeakersByTalk($talk); $planning = $this->planningRepository->getByTalk($talk); - $event = $this->eventRepository->get($planning->getEventId()); + if ($planning === null) { + throw $this->createNotFoundException(); + } + $event = $this->eventRepository->get($planning->eventId); $comments = $this->joindinComments->getCommentsFromTalk($talk); $parser = new Parser(); diff --git a/sources/AppBundle/Event/Entity/Planning.php b/sources/AppBundle/Event/Entity/Planning.php new file mode 100644 index 000000000..5dbcaea02 --- /dev/null +++ b/sources/AppBundle/Event/Entity/Planning.php @@ -0,0 +1,51 @@ + 0])] + public bool $isKeynote = false; +} diff --git a/sources/AppBundle/Event/Entity/Repository/PlanningRepository.php b/sources/AppBundle/Event/Entity/Repository/PlanningRepository.php new file mode 100644 index 000000000..fb5e31d7b --- /dev/null +++ b/sources/AppBundle/Event/Entity/Repository/PlanningRepository.php @@ -0,0 +1,108 @@ + + */ +final class PlanningRepository extends EntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Planning::class); + } + + public function getByTalk(Talk $talk): ?Planning + { + $talkId = $talk->getId(); + Assert::notNull($talkId); + + return $this->findOneBy(['talkId' => $talkId]); + } + + /** + * @return list + */ + public function findNonKeynotesBetween(\DateTimeInterface $since, \DateTimeInterface $until): array + { + return $this->createQueryBuilder('p') + ->where('p.isKeynote = :keynote') + ->andWhere('p.start >= :since') + ->andWhere('p.start < :until') + ->setParameter('keynote', false) + ->setParameter('since', $since, UnixTimestampType::NAME) + ->setParameter('until', $until, UnixTimestampType::NAME) + ->getQuery() + ->getResult(); + } + + /** + * Récupère le premier planning (par date de début) de chacune des sessions demandées. + * + * @param list $talkIds + * + * @return array + */ + public function findIndexedByTalkIds(array $talkIds): array + { + if ($talkIds === []) { + return []; + } + + $plannings = []; + foreach ($this->findBy(['talkId' => $talkIds], ['start' => 'ASC']) as $planning) { + $talkId = $planning->talkId; + if ($talkId === null) { + continue; + } + $plannings[$talkId] ??= $planning; + } + + return $plannings; + } + + /** + * Complète les agrégats session/conférenciers/salle produits par TalkRepository (Ting) + * avec les plannings correspondants, rechargés via Doctrine. + * + * @param array $talkAggregates + * + * @return array + */ + public function enrichTalkAggregates(array $talkAggregates): array + { + $talkIds = []; + foreach ($talkAggregates as $talkAggregate) { + $talkId = $talkAggregate->talk->getId(); + if ($talkId !== null) { + $talkIds[] = $talkId; + } + } + + $plannings = $this->findIndexedByTalkIds($talkIds); + + $enriched = []; + foreach ($talkAggregates as $talkAggregate) { + $talkId = $talkAggregate->talk->getId(); + $enriched[] = new TalkAggregate( + $talkAggregate->talk, + $talkAggregate->speakers, + $talkAggregate->room, + $talkId !== null ? ($plannings[$talkId] ?? null) : null, + $talkAggregate->vote, + ); + } + + return $enriched; + } +} diff --git a/sources/AppBundle/Event/JsonLd.php b/sources/AppBundle/Event/JsonLd.php index 75f629c82..931437af5 100644 --- a/sources/AppBundle/Event/JsonLd.php +++ b/sources/AppBundle/Event/JsonLd.php @@ -5,6 +5,7 @@ namespace AppBundle\Event; use AppBundle\CFP\PhotoStorage; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Event\Model\Repository\TicketEventTypeRepository; @@ -16,6 +17,7 @@ { public function __construct( private TalkRepository $talkRepository, + private PlanningRepository $planningRepository, private TicketEventTypeRepository $ticketEventTypeRepository, private TicketTypeAvailability $ticketTypeAvailability, private Packages $packages, @@ -24,7 +26,9 @@ public function __construct( public function getDataForEvent(Event $event): array { - $talkAggregates = $this->talkRepository->getByEventWithSpeakers($event); + $talkAggregates = $this->planningRepository->enrichTalkAggregates( + $this->talkRepository->getByEventWithSpeakers($event), + ); $subEvents = []; foreach ($talkAggregates as $talkAggregate) { @@ -68,8 +72,8 @@ public function getDataForEvent(Event $event): array ]; if ($talkAggregate->planning && $event->isPlanningDisplayable()) { - $subEvent['startDate'] = $talkAggregate->planning->getStart()->format('c'); - $subEvent['endDate'] = $talkAggregate->planning->getEnd()->format('c'); + $subEvent['startDate'] = $talkAggregate->planning->start->format('c'); + $subEvent['endDate'] = $talkAggregate->planning->end->format('c'); } $subEvents[] = $subEvent; diff --git a/sources/AppBundle/Event/Model/Planning.php b/sources/AppBundle/Event/Model/Planning.php deleted file mode 100644 index c46a90b9c..000000000 --- a/sources/AppBundle/Event/Model/Planning.php +++ /dev/null @@ -1,130 +0,0 @@ -id; - } - - public function setId($id): self - { - $id = (int) $id; - $this->propertyChanged('id', $this->id, $id); - $this->id = $id; - return $this; - } - - public function getTalkId(): ?int - { - return $this->talkId; - } - - public function setTalkId(int $talkId): self - { - $this->propertyChanged('talkId', $this->talkId, $talkId); - $this->talkId = $talkId; - - return $this; - } - - public function getStart(): ?\DateTime - { - return $this->start; - } - - public function setStart(\DateTime $start): self - { - $this->propertyChanged('start', $this->start, $start); - $this->start = $start; - return $this; - } - - public function getEnd(): ?\DateTime - { - return $this->end; - } - - public function setEnd(\DateTime $end): self - { - $this->propertyChanged('end', $this->end, $end); - $this->end = $end; - return $this; - } - - public function getEventId(): ?int - { - return $this->eventId; - } - - public function setEventId(int $eventId): self - { - $this->propertyChanged('eventId', $this->eventId, $eventId); - $this->eventId = $eventId; - - return $this; - } - - public function setIsKeynote(bool $isKeynote): self - { - $this->propertyChanged('isKeynote', $this->isKeynote, $isKeynote); - $this->isKeynote = $isKeynote; - - return $this; - } - - public function getIsKeynote(): bool - { - return $this->isKeynote; - } - - public function setRoomId(?int $roomId): self - { - $this->propertyChanged('roomId', $this->roomId, $roomId); - $this->roomId = $roomId; - - return $this; - } - - public function getRoomId(): ?int - { - return $this->roomId; - } -} diff --git a/sources/AppBundle/Event/Model/Repository/PlanningRepository.php b/sources/AppBundle/Event/Model/Repository/PlanningRepository.php deleted file mode 100644 index 1503c7ca2..000000000 --- a/sources/AppBundle/Event/Model/Repository/PlanningRepository.php +++ /dev/null @@ -1,131 +0,0 @@ - - */ -class PlanningRepository extends Repository implements MetadataInitializer -{ - /** - * @return Planning|null - */ - public function getByTalk(Talk $talk) - { - $query = $this - ->getQuery('SELECT * FROM afup_forum_planning WHERE id_session= :id_session LIMIT 1') - ; - - $query->setParams(['id_session' => $talk->getId()]); - - $plannings = $query->query($this->getCollection(new HydratorSingleObject())); - if ($plannings->count() === 0) { - return null; - } - - return $plannings->first(); - } - - /** - * @return CollectionInterface - */ - public function findNonKeynotesBetween(\DateTimeInterface $since, \DateTimeInterface $until): CollectionInterface - { - /** @var SelectInterface $qb */ - $qb = $this->getQueryBuilder(self::QUERY_SELECT); - - $qb->from('afup_forum_planning') - ->cols(['*']) - ->where('keynote = 0') - ->where('debut >= :since') - ->where('debut < :until') - ; - - return $this - ->getPreparedQuery($qb->getStatement()) - ->setParams([ - 'since' => $since->getTimestamp(), - 'until' => $until->getTimestamp(), - ]) - ->query($this->getCollection(new HydratorSingleObject())); - } - - /** - * @return Metadata - */ - public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) - { - $metadata = new Metadata($serializerFactory); - $metadata->setEntity(Planning::class); - $metadata->setConnectionName('main'); - $metadata->setDatabase($options['database']); - $metadata->setTable('afup_forum_planning'); - - $metadata - ->addField([ - 'columnName' => 'id', - 'fieldName' => 'id', - 'primary' => true, - 'autoincrement' => true, - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'id_session', - 'fieldName' => 'talkId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'id_forum', - 'fieldName' => 'eventId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'debut', - 'fieldName' => 'start', - 'type' => 'datetime', - 'serializer' => DateTimeWithTimeZoneSerializer::class, - 'serializer_options' => [ - 'unserialize' => ['unSerializeUseFormat' => true, 'format' => 'U'], - 'serialize' => ['serializeUseFormat' => true, 'format' => 'U'], - ], - ]) - ->addField([ - 'columnName' => 'fin', - 'fieldName' => 'end', - 'type' => 'datetime', - 'serializer' => DateTimeWithTimeZoneSerializer::class, - 'serializer_options' => [ - 'unserialize' => ['unSerializeUseFormat' => true, 'format' => 'U'], - 'serialize' => ['serializeUseFormat' => true, 'format' => 'U'], - ], - ]) - ->addField([ - 'columnName' => 'keynote', - 'fieldName' => 'isKeynote', - 'type' => 'bool', - 'serializer' => Boolean::class, - ]) - ->addField([ - 'columnName' => 'id_salle', - 'fieldName' => 'roomId', - 'type' => 'int', - ]) - ; - - return $metadata; - } -} diff --git a/sources/AppBundle/Event/Model/Repository/TalkRepository.php b/sources/AppBundle/Event/Model/Repository/TalkRepository.php index 29db1ee8e..a0476b29e 100644 --- a/sources/AppBundle/Event/Model/Repository/TalkRepository.php +++ b/sources/AppBundle/Event/Model/Repository/TalkRepository.php @@ -217,9 +217,11 @@ public function getByTalkWithSpeakers(Talk $talk) $hydrator->aggregateOn('talk', 'speaker', 'getId'); $query = $this->getPreparedQuery( + // Les plannings ne sont plus hydratés par Ting : ils sont rechargés + // via Doctrine avec PlanningRepository::findIndexedByTalkIds(). 'SELECT talk.session_id, talk.titre, skill, talk.genre, abstract, talk.plannifie, speaker.conferencier_id, speaker.nom, speaker.prenom, speaker.id_forum, speaker.photo, speaker.societe, speaker.biographie, - planning.debut, planning.fin, room.id, room.nom, event.date_annonce_planning, event.path, event.interviews_intro, event.interviews_cta_text + room.id, room.nom, event.date_annonce_planning, event.path, event.interviews_intro, event.interviews_cta_text FROM afup_sessions AS talk LEFT JOIN afup_conferenciers_sessions acs ON acs.session_id = talk.session_id LEFT JOIN afup_conferenciers speaker ON speaker.conferencier_id = acs.conferencier_id @@ -285,7 +287,7 @@ public function getByEventsWithSpeakers(array $events, bool $applyPublicationdat sprintf('SELECT talk.id_forum, talk.session_id, titre, skill, talk.genre, abstract, talk.plannifie, talk.language_code, talk.joindin, talk.theme, talk.position, speaker.conferencier_id, speaker.nom, speaker.prenom, speaker.id_forum, speaker.photo, speaker.societe, - planning.id, planning.debut, planning.fin, room.id, room.nom + room.id, room.nom FROM afup_sessions AS talk LEFT JOIN afup_conferenciers_sessions acs ON acs.session_id = talk.session_id LEFT JOIN afup_conferenciers speaker ON speaker.conferencier_id = acs.conferencier_id @@ -304,7 +306,9 @@ public function getByEventsWithSpeakers(array $events, bool $applyPublicationdat $row['talk'], $row['.aggregation']['speaker'], $row['room'] ?? null, - $row['planning'] ?? null, + // Les plannings ne sont plus hydratés par Ting : ils sont rechargés + // via Doctrine avec PlanningRepository::enrichTalkAggregates(). + null, ); } @@ -322,7 +326,7 @@ public function getByEventWithSpeakersAndVotes(Event $event, string $search = '' talk.joindin, talk.youtube_id, talk.slides_url, talk.interview_url, talk.blog_post_url, talk.needs_mentoring, talk.date_soumission, speaker.conferencier_id, speaker.nom, speaker.prenom, speaker.id_forum, speaker.photo, speaker.societe, - planning.id, planning.debut, planning.fin, room.id, room.nom, + room.id, room.nom, (SELECT AVG(vote) FROM afup_sessions_vote_github asvg WHERE asvg.session_id = talk.session_id) AS vote_note, (SELECT COUNT(vote) FROM afup_sessions_vote_github asvg WHERE asvg.session_id = talk.session_id) AS vote_total FROM afup_sessions AS talk @@ -365,7 +369,9 @@ public function getByEventWithSpeakersAndVotes(Event $event, string $search = '' $row['talk'], $row['.aggregation']['speaker'], $row['room'] ?? null, - $row['planning'] ?? null, + // Les plannings ne sont plus hydratés par Ting : ils sont rechargés + // via Doctrine avec PlanningRepository::enrichTalkAggregates(). + null, $row[0]->vote_note ? new TalkAggregateVote($row[0]->vote_note, $row[0]->vote_total) : null, ); } diff --git a/sources/AppBundle/Event/Model/TalkAggregate.php b/sources/AppBundle/Event/Model/TalkAggregate.php index 4f400891f..0d9d17b53 100644 --- a/sources/AppBundle/Event/Model/TalkAggregate.php +++ b/sources/AppBundle/Event/Model/TalkAggregate.php @@ -4,6 +4,8 @@ namespace AppBundle\Event\Model; +use AppBundle\Event\Entity\Planning; + final readonly class TalkAggregate { public function __construct( diff --git a/sources/AppBundle/Event/Speaker/SpeakerPage.php b/sources/AppBundle/Event/Speaker/SpeakerPage.php index 0e31aff1f..ada1f56bc 100644 --- a/sources/AppBundle/Event/Speaker/SpeakerPage.php +++ b/sources/AppBundle/Event/Speaker/SpeakerPage.php @@ -4,8 +4,9 @@ namespace AppBundle\Event\Speaker; +use AppBundle\Event\Entity\Planning; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Event; -use AppBundle\Event\Model\Planning; use AppBundle\Event\Model\Repository\SpeakerRepository; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Event\Model\Room; @@ -29,6 +30,7 @@ class SpeakerPage extends AbstractController { public function __construct( private readonly TalkRepository $talkRepository, + private readonly PlanningRepository $planningRepository, private readonly SpeakerRepository $speakerRepository, private readonly SpeakersExpensesStorage $speakersExpensesStorage, ) {} @@ -204,7 +206,9 @@ public function handleRequest(Request $request, Event $event, Speaker $speaker): */ protected function addTalkInfos(Event $event, array $talks): array { - $talkAggregates = $this->talkRepository->getByEventWithSpeakers($event, false); + $talkAggregates = $this->planningRepository->enrichTalkAggregates( + $this->talkRepository->getByEventWithSpeakers($event, false), + ); $allTalksById = []; foreach ($talkAggregates as $talkAggregate) { $allTalksById[$talkAggregate->talk->getId()] = [ diff --git a/sources/AppBundle/Event/Talk/ExportGenerator.php b/sources/AppBundle/Event/Talk/ExportGenerator.php index 067d1a524..0c741fd2b 100644 --- a/sources/AppBundle/Event/Talk/ExportGenerator.php +++ b/sources/AppBundle/Event/Talk/ExportGenerator.php @@ -4,8 +4,9 @@ namespace AppBundle\Event\Talk; +use AppBundle\Event\Entity\Planning; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Event; -use AppBundle\Event\Model\Planning; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Event\Model\Speaker; use AppBundle\Event\Model\Talk; @@ -13,7 +14,10 @@ class ExportGenerator { - public function __construct(private readonly TalkRepository $talkRepository) {} + public function __construct( + private readonly TalkRepository $talkRepository, + private readonly PlanningRepository $planningRepository, + ) {} /** * @@ -53,7 +57,9 @@ public function export(Event $event, \SplFileObject $toFile): void public function exportJoindIn(Event $event, \SplFileObject $toFile): void { // Récupération des données - $talkAggregates = $this->talkRepository->getByEventWithSpeakers($event); + $talkAggregates = $this->planningRepository->enrichTalkAggregates( + $this->talkRepository->getByEventWithSpeakers($event), + ); $toFile->fputcsv(['Title','Description','Speaker','Date','Time','Type'], escape: '\\'); @@ -77,11 +83,11 @@ public function exportJoindIn(Event $event, \SplFileObject $toFile): void // Gestion des horaires : stockés en timestamp, ils sont exportés dans la // timezone de l'événement et non dans celle du serveur. - $start = $talkAggregate->planning?->getStart(); + $start = $talkAggregate->planning?->start; $start = $start === null ? null : \DateTimeImmutable::createFromInterface($start)->setTimezone($timezone); // Gestion du type de conférence - if ($talkAggregate->planning?->getIsKeynote()) { + if ($talkAggregate->planning?->isKeynote) { $type = 'Keynote'; } elseif (Talk::TYPE_WORKSHOP === $talkAggregate->talk->getType()) { $type = 'Workshop'; diff --git a/sources/AppBundle/Indexation/Talks/Runner.php b/sources/AppBundle/Indexation/Talks/Runner.php index 160d03d41..764b198cf 100644 --- a/sources/AppBundle/Indexation/Talks/Runner.php +++ b/sources/AppBundle/Indexation/Talks/Runner.php @@ -6,12 +6,11 @@ use Algolia\AlgoliaSearch\SearchClient; use Algolia\AlgoliaSearch\SearchIndex; -use AppBundle\Event\Model\Planning; +use AppBundle\Event\Entity\Planning; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Repository\EventRepository; -use AppBundle\Event\Model\Repository\PlanningRepository; use AppBundle\Event\Model\Repository\SpeakerRepository; use AppBundle\Event\Model\Repository\TalkRepository; -use CCMBenchmark\Ting\Repository\CollectionInterface; class Runner { @@ -80,26 +79,26 @@ protected function initIndex(): SearchIndex } /** - * @return CollectionInterface + * @return list */ - protected function getAllPlannings() + protected function getAllPlannings(): array { - return $this->planningRepository->getAll(); + return $this->planningRepository->findAll(); } protected function prepareObject(Planning $planning): ?array { - if ($planning->getStart() > new \DateTime()) { + if ($planning->start > new \DateTime()) { return null; } - $talk = $this->talkRepository->get($planning->getTalkId()); + $talk = $this->talkRepository->get($planning->talkId); if (null === $talk || !$talk->isDisplayedOnHistory()) { return null; } - $event = $this->eventRepository->get($planning->getEventId()); + $event = $this->eventRepository->get($planning->eventId); if (null === $event) { return null; diff --git a/sources/AppBundle/Indexation/Talks/Transformer.php b/sources/AppBundle/Indexation/Talks/Transformer.php index be526dc35..3b7cc44bd 100644 --- a/sources/AppBundle/Indexation/Talks/Transformer.php +++ b/sources/AppBundle/Indexation/Talks/Transformer.php @@ -4,8 +4,8 @@ namespace AppBundle\Indexation\Talks; +use AppBundle\Event\Entity\Planning; use AppBundle\Event\Model\Event; -use AppBundle\Event\Model\Planning; use AppBundle\Event\Model\Speaker; use AppBundle\Event\Model\Talk; @@ -18,7 +18,7 @@ class Transformer public function transform(Planning $planning, Talk $talk, Event $event, \Traversable $speakers): array { $item = [ - 'planning_id' => $planning->getId(), + 'planning_id' => $planning->id, 'talk_id' => $talk->getId(), 'url_key' => $talk->getUrlKey(), 'title' => $talk->getTitle(), diff --git a/sources/AppBundle/Openfeedback/OpenfeedbackJsonGenerator.php b/sources/AppBundle/Openfeedback/OpenfeedbackJsonGenerator.php index b63336fee..e3e35dad4 100644 --- a/sources/AppBundle/Openfeedback/OpenfeedbackJsonGenerator.php +++ b/sources/AppBundle/Openfeedback/OpenfeedbackJsonGenerator.php @@ -5,6 +5,7 @@ namespace AppBundle\Openfeedback; use AppBundle\CFP\PhotoStorage; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Repository\TalkRepository; @@ -12,12 +13,15 @@ class OpenfeedbackJsonGenerator { public function __construct( private readonly TalkRepository $talkRepository, + private readonly PlanningRepository $planningRepository, private readonly PhotoStorage $photoStorage, ) {} public function generate(Event $event): array { - $talkAggregates = $this->talkRepository->getByEventWithSpeakers($event); + $talkAggregates = $this->planningRepository->enrichTalkAggregates( + $this->talkRepository->getByEventWithSpeakers($event), + ); $data = []; foreach ($talkAggregates as $talkAggregate) { $speakersFormatted = []; @@ -46,8 +50,8 @@ public function generate(Event $event): array ]; if (null !== $talkAggregate->planning) { - $talkFormatted['startTime'] = $this->getOpenfeedbackFormat($talkAggregate->planning->getStart()); - $talkFormatted['endTime'] = $this->getOpenfeedbackFormat($talkAggregate->planning->getEnd()); + $talkFormatted['startTime'] = $this->getOpenfeedbackFormat($talkAggregate->planning->start); + $talkFormatted['endTime'] = $this->getOpenfeedbackFormat($talkAggregate->planning->end); } $data['sessions'][$talkAggregate->talk->getId()] = $talkFormatted; diff --git a/sources/AppBundle/VideoNotifier/Engine.php b/sources/AppBundle/VideoNotifier/Engine.php index cc69c5152..546f9a158 100644 --- a/sources/AppBundle/VideoNotifier/Engine.php +++ b/sources/AppBundle/VideoNotifier/Engine.php @@ -4,8 +4,7 @@ namespace AppBundle\VideoNotifier; -use AppBundle\Event\Model\Planning; -use AppBundle\Event\Model\Repository\PlanningRepository; +use AppBundle\Event\Entity\Repository\PlanningRepository; use AppBundle\Event\Model\Repository\SpeakerRepository; use AppBundle\Event\Model\Repository\TalkRepository; use AppBundle\Event\Model\Talk; @@ -92,9 +91,8 @@ private function pickRandomTalk(): ?Talk $talkIds = []; - /** @var Planning $planning */ foreach ($plannings as $planning) { - $talkIds[] = $planning->getTalkId(); + $talkIds[] = $planning->talkId; } if (empty($talkIds)) { diff --git a/tests/integration/AppBundle/Event/Entity/Repository/PlanningRepositoryTest.php b/tests/integration/AppBundle/Event/Entity/Repository/PlanningRepositoryTest.php new file mode 100644 index 000000000..65763d738 --- /dev/null +++ b/tests/integration/AppBundle/Event/Entity/Repository/PlanningRepositoryTest.php @@ -0,0 +1,94 @@ +get(PlanningRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $this->insertPlanning($connection, '1007', '1451606400', '1451608800', '1', '10', '0'); + + $talk = new Talk()->setId(1007); + + $planning = $planningRepository->getByTalk($talk); + self::assertInstanceOf(Planning::class, $planning); + self::assertSame(1007, $planning->talkId); + self::assertSame(10, $planning->eventId); + self::assertSame(1, $planning->roomId); + self::assertFalse($planning->isKeynote); + // Les horaires sont stockés en timestamp : la valeur convertie doit être équivalente + self::assertSame(1451606400, $planning->start->getTimestamp()); + self::assertSame(1451608800, $planning->end->getTimestamp()); + + self::assertNull($planningRepository->getByTalk(new Talk()->setId(9999))); + + // CRUD via EntityRepository + $planning = new Planning(); + $planning->talkId = 1007; + $planning->eventId = 10; + $planning->start = new \DateTime('@1451606400'); + $planning->end = new \DateTime('@1451608800'); + $planning->roomId = 1; + $planningRepository->save($planning); + self::assertNotNull($planning->id); + + $saved = $planningRepository->find($planning->id); + self::assertInstanceOf(Planning::class, $saved); + self::assertSame(1007, $saved->talkId); + + $id = $planning->id; + $planningRepository->delete($planning); + self::assertNull($planningRepository->find($id)); + } + + public function testFindNonKeynotesBetween(): void + { + $planningRepository = self::getContainer()->get(PlanningRepository::class); + $connection = self::getContainer()->get(Connection::class); + + $this->insertPlanning($connection, '1001', '1451606401', '1451608800', '1', '10', '0'); + // En dessous de la borne inférieure + $this->insertPlanning($connection, '1002', '1451606399', '1451608800', '1', '10', '0'); + // Au dessus de la borne supérieure (exclusive) + $this->insertPlanning($connection, '1003', '1451692800', '1451695200', '1', '10', '0'); + // Keynote : exclue même dans la plage + $this->insertPlanning($connection, '1004', '1451606401', '1451608800', '1', '10', '1'); + + $since = (new \DateTime())->setTimestamp(1451606400); + $until = (new \DateTime())->setTimestamp(1451692800); + + $plannings = $planningRepository->findNonKeynotesBetween($since, $until); + + self::assertCount(1, $plannings); + $planning = $plannings[0]; + self::assertInstanceOf(Planning::class, $planning); + self::assertSame(1001, $planning->talkId); + self::assertFalse($planning->isKeynote); + } + + /** + * @return void + */ + private function insertPlanning(Connection $connection, string $talkId, string $debut, string $fin, string $salle, string $forum, string $keynote): void + { + $connection->insert('afup_forum_planning', [ + 'id_session' => $talkId, + 'debut' => $debut, + 'fin' => $fin, + 'id_salle' => $salle, + 'id_forum' => $forum, + 'keynote' => $keynote, + ]); + } +} diff --git a/tests/unit/AppBundle/Indexation/Talks/TransformerTest.php b/tests/unit/AppBundle/Indexation/Talks/TransformerTest.php index 83be4ab70..d8b9e60be 100644 --- a/tests/unit/AppBundle/Indexation/Talks/TransformerTest.php +++ b/tests/unit/AppBundle/Indexation/Talks/TransformerTest.php @@ -5,8 +5,8 @@ namespace AppBundle\Tests\Indexation\Talks; use AppBundle\Association\Genre; +use AppBundle\Event\Entity\Planning; use AppBundle\Event\Model\Event; -use AppBundle\Event\Model\Planning; use AppBundle\Event\Model\Speaker; use AppBundle\Event\Model\Talk; use AppBundle\Indexation\Talks\Transformer; @@ -32,11 +32,11 @@ public function testTransform(): void ->setBlogPostUrl('http://tapoueh.org/confs/2014/06/23-PHPTour-Lyon-2014') ->setLanguageCode('fr'); - ($planning = new Planning()) - ->setId(266) - ->setTalkId(1007) - ->setEventId(10) - ->setIsKeynote(false); + $planning = new Planning(); + $planning->id = 266; + $planning->talkId = 1007; + $planning->eventId = 10; + $planning->isKeynote = false; ($event = new Event()) ->setId(10)