From 86e8f63d39186b92d7c3ccd61bed43a6392d2009 Mon Sep 17 00:00:00 2001 From: RomMad Date: Tue, 2 Jun 2026 18:39:47 +0200 Subject: [PATCH 1/3] feat: Add functionality to create surf sessions from trip page and update related templates and translations --- .../SurfSession/NewSurfSessionController.php | 25 ++++++++-- src/Controller/Trip/EditTripController.php | 5 -- src/Controller/Trip/ShowTripController.php | 5 -- .../SurfSessionWriteModelFactory.php | 48 +++++++++++++++++++ src/Service/Trip/TripReadModelProvider.php | 13 ++++- templates/trip/edit.html.twig | 11 ++++- templates/trip/show.html.twig | 16 +++++-- .../NewSurfSessionControllerTest.php | 33 +++++++++++++ .../Trip/ShowTripControllerTest.php | 2 + translations/messages+intl-icu.en.yaml | 1 + translations/messages+intl-icu.fr.yaml | 1 + 11 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 src/Service/SurfSession/SurfSessionWriteModelFactory.php diff --git a/src/Controller/SurfSession/NewSurfSessionController.php b/src/Controller/SurfSession/NewSurfSessionController.php index 1c5ca4b..e6caf8e 100644 --- a/src/Controller/SurfSession/NewSurfSessionController.php +++ b/src/Controller/SurfSession/NewSurfSessionController.php @@ -8,14 +8,17 @@ use App\Entity\SurfSession; use App\Entity\User; use App\Enum\User\UserRole; -use App\Form\Model\SurfSession\SurfSessionWriteModel; use App\Form\SurfSession\SurfSessionFormType; use App\Repository\SurfSessionRepository; +use App\Security\Voter\TripVoter; +use App\Service\SurfSession\SurfSessionWriteModelFactory; +use App\Service\Trip\TripReadModelProvider; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\ObjectMapper\ObjectMapperInterface; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\Requirement\Requirement; use Symfony\Component\Security\Http\Attribute\CurrentUser; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -25,6 +28,8 @@ public function __construct( private readonly ObjectMapperInterface $objectMapper, private readonly SurfSessionRepository $surfSessionRepository, private readonly SurfSessionCacheInvalidator $surfSessionCacheInvalidator, + private readonly TripReadModelProvider $tripReadModelProvider, + private readonly SurfSessionWriteModelFactory $surfSessionWriteModelFactory, ) {} #[Route( @@ -32,10 +37,24 @@ public function __construct( name: 'app.surf_session.new', methods: [Request::METHOD_GET, Request::METHOD_POST], )] + #[Route( + path: '/trip/{tripId}/sessions/new', + name: 'app.trip.surf_session.new', + requirements: ['tripId' => Requirement::POSITIVE_INT], + methods: [Request::METHOD_GET, Request::METHOD_POST], + )] #[IsGranted(UserRole::USER)] - public function __invoke(Request $request, #[CurrentUser()] User $currentUser): Response + public function __invoke(Request $request, #[CurrentUser()] User $currentUser, ?int $tripId = null): Response { - $surfSessionWriteModel = new SurfSessionWriteModel(); + $trip = null; + + if (null !== $tripId) { + $trip = $this->tripReadModelProvider->getById($tripId); + + $this->denyAccessUnlessGranted(TripVoter::EDIT, $trip); + } + + $surfSessionWriteModel = $this->surfSessionWriteModelFactory->create($trip); $form = $this->createForm(SurfSessionFormType::class, $surfSessionWriteModel); $form->handleRequest($request); diff --git a/src/Controller/Trip/EditTripController.php b/src/Controller/Trip/EditTripController.php index 64a56e6..56c39c5 100644 --- a/src/Controller/Trip/EditTripController.php +++ b/src/Controller/Trip/EditTripController.php @@ -4,7 +4,6 @@ namespace App\Controller\Trip; -use App\Exception\TripNotFoundHttpException; use App\Form\Model\Trip\TripWriteModel; use App\Form\Trip\TripFormType; use App\Security\Voter\TripVoter; @@ -40,10 +39,6 @@ public function __invoke(Request $request, int $id, string $slug): Response { $trip = $this->tripReadModelProvider->getById($id); - if (null === $trip) { - throw new TripNotFoundHttpException($id); - } - $this->denyAccessUnlessGranted(TripVoter::EDIT, $trip); if ($trip->slug->value !== $slug) { diff --git a/src/Controller/Trip/ShowTripController.php b/src/Controller/Trip/ShowTripController.php index 47f0c39..6c76981 100644 --- a/src/Controller/Trip/ShowTripController.php +++ b/src/Controller/Trip/ShowTripController.php @@ -4,7 +4,6 @@ namespace App\Controller\Trip; -use App\Exception\TripNotFoundHttpException; use App\Service\Trip\TripReadModelProvider; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -33,10 +32,6 @@ public function __invoke(int $id, string $slug): Response { $trip = $this->tripReadModelProvider->getById($id); - if (null === $trip) { - throw new TripNotFoundHttpException($id); - } - if ($trip->slug->value !== $slug) { return $this->redirectToRoute(self::ROUTE, [ 'id' => $trip->id, diff --git a/src/Service/SurfSession/SurfSessionWriteModelFactory.php b/src/Service/SurfSession/SurfSessionWriteModelFactory.php new file mode 100644 index 0000000..4d4dbed --- /dev/null +++ b/src/Service/SurfSession/SurfSessionWriteModelFactory.php @@ -0,0 +1,48 @@ +resolveStartAt($trip); + + if (null !== $trip) { + $trip = new TripSelectReadModel( + id: $trip->id, + title: $trip->title->value, + location: $trip->location->value, + ); + } + + $surfSessionWriteModel->trip = $trip; + $surfSessionWriteModel->spot = $trip?->location; + $surfSessionWriteModel->startAt = $startAt; + $surfSessionWriteModel->endAt = $startAt->modify(sprintf('+%d hours', self::DEFAULT_SESSION_DURATION_HOURS)); + + return $surfSessionWriteModel; + } + + private function resolveStartAt(?AbstractTripReadModel $trip): \DateTimeImmutable + { + $now = new \DateTimeImmutable(); + $currentHourStart = $now->setTime((int) $now->format('H'), 0); + + return match (true) { + null === $trip => $currentHourStart, + $now < $trip->startAt => $trip->startAt->setTime(8, 0), + $now > $trip->endAt => $trip->endAt, + default => $currentHourStart, + }; + } +} diff --git a/src/Service/Trip/TripReadModelProvider.php b/src/Service/Trip/TripReadModelProvider.php index d4381c3..628c0f4 100644 --- a/src/Service/Trip/TripReadModelProvider.php +++ b/src/Service/Trip/TripReadModelProvider.php @@ -5,6 +5,7 @@ namespace App\Service\Trip; use App\Cache\Trip\TripCacheKeys; +use App\Exception\TripNotFoundHttpException; use App\ReadModel\Trip\TripShowReadModel; use App\Repository\TripRepository; use Symfony\Contracts\Cache\ItemInterface; @@ -19,9 +20,9 @@ public function __construct( private TagAwareCacheInterface $cache, ) {} - public function getById(int $id): ?TripShowReadModel + public function getById(int $id): TripShowReadModel { - return $this->cache->get( + $trip = $this->cache->get( TripCacheKeys::readModel($id), function (ItemInterface $item) use ($id): ?TripShowReadModel { $item->expiresAfter(new \DateInterval(self::CACHE_TTL)); @@ -29,5 +30,13 @@ function (ItemInterface $item) use ($id): ?TripShowReadModel { return $this->tripRepository->findShowReadModelById($id); }, ); + + if (null === $trip) { + $this->cache->delete(TripCacheKeys::readModel($id)); + + throw new TripNotFoundHttpException($id); + } + + return $trip; } } diff --git a/templates/trip/edit.html.twig b/templates/trip/edit.html.twig index 0f2e658..16f807f 100644 --- a/templates/trip/edit.html.twig +++ b/templates/trip/edit.html.twig @@ -3,7 +3,16 @@ {% set title = 'trip.edit.label'|trans %} {% block body %} - +
+ + {% if is_granted('EDIT', trip) %} + + {% endif %} +
+ diff --git a/templates/trip/show.html.twig b/templates/trip/show.html.twig index b722a70..8c466b9 100644 --- a/templates/trip/show.html.twig +++ b/templates/trip/show.html.twig @@ -1,16 +1,26 @@ {% extends 'base.html.twig' %} {% set title = 'trip.label'|trans %} +{% set is_granted_edit = is_granted('EDIT', trip) %} {% block body %} - +
+ + {% if is_granted_edit %} + + {% endif %} +
+ - {% if is_granted('EDIT', trip) %} + {% if is_granted_edit %} {% endif %}
- {% if is_granted('EDIT', trip) %} + {% if is_granted_edit %} {% endif %} diff --git a/tests/Functional/Controller/SurfSession/NewSurfSessionControllerTest.php b/tests/Functional/Controller/SurfSession/NewSurfSessionControllerTest.php index 4f4cbe4..7e9a6bc 100644 --- a/tests/Functional/Controller/SurfSession/NewSurfSessionControllerTest.php +++ b/tests/Functional/Controller/SurfSession/NewSurfSessionControllerTest.php @@ -23,6 +23,7 @@ final class NewSurfSessionControllerTest extends CustomWebTestCase // Paths private const string PATH_INDEX = '/en/sessions'; private const string PATH_NEW = '/en/sessions/new'; + private const string PATH_NEW_FROM_TRIP = '/en/trip/%d/sessions/new'; // Selectors private const string FORM = 'form[name="surf_session"]'; private const string FIRST_CARD = '.app-card'; @@ -89,4 +90,36 @@ public function testCreateSurfSessionIsSuccessful(): void $this->assertSelectorTextContains(self::FIRST_CARD, self::SESSION_SPOT); $this->assertSelectorTextContains(self::FIRST_CARD, self::SESSION_BOARD); } + + public function testNewSurfSessionWithTrip(): void + { + $trip = TripFactory::find(['title' => Title::from(TripStory::CURRENT_TRIP_TITLE)]); + $now = new \DateTimeImmutable(); + + $this->client->request(Request::METHOD_GET, sprintf(self::PATH_NEW_FROM_TRIP, $trip->id)); + + $startAt = $this->parseDateTimeFromInput('#surf_session_startAt'); + $endAt = $this->parseDateTimeFromInput('#surf_session_endAt'); + + $this->assertResponseIsSuccessful(); + $this->assertSame($trip->location->value, $this->getInputValue('#surf_session_spot')); + $this->assertSame((string) $trip->id, $this->getInputValue('#surf_session_trip option[selected]')); + $this->assertSame($now->format('Y-m-d\TH'), $startAt->format('Y-m-d\TH')); + $this->assertSame($startAt->modify('+2 hours')->format('Y-m-d\TH'), $endAt->format('Y-m-d\TH')); + } + + private function parseDateTimeFromInput(string $selector): \DateTimeImmutable + { + $value = $this->getInputValue($selector); + $parsedDate = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i', $value); + + $this->assertInstanceOf(\DateTimeImmutable::class, $parsedDate); + + return $parsedDate; + } + + private function getInputValue(string $selector): string + { + return (string) $this->client->getCrawler()->filter($selector)->attr('value'); + } } diff --git a/tests/Functional/Controller/Trip/ShowTripControllerTest.php b/tests/Functional/Controller/Trip/ShowTripControllerTest.php index bffca5f..2c4e575 100644 --- a/tests/Functional/Controller/Trip/ShowTripControllerTest.php +++ b/tests/Functional/Controller/Trip/ShowTripControllerTest.php @@ -24,6 +24,7 @@ final class ShowTripControllerTest extends CustomWebTestCase private const string PATH = '/en/trip/%d/%s'; private const string TITLE = 'Trip'; + private const string ADD_SESSION_LABEL = 'Add session'; private ?Trip $trip = null; @@ -45,6 +46,7 @@ public function testShowTripPageIsDisplayed(): void $this->assertSelectorTextSame(self::TITLE_H1, self::TITLE); $this->assertSelectorExists(self::TABLE); $this->assertSelectorTextContains(self::TABLE, $this->trip->title->value); + $this->assertCount(1, $this->client->getCrawler()->selectLink(self::ADD_SESSION_LABEL)); } public function testShowTripPageRedirectsWhenSlugIsInvalid(): void diff --git a/translations/messages+intl-icu.en.yaml b/translations/messages+intl-icu.en.yaml index 0b3f55e..509fa39 100644 --- a/translations/messages+intl-icu.en.yaml +++ b/translations/messages+intl-icu.en.yaml @@ -103,6 +103,7 @@ surf_session: created_successfully: The session has been created. updated_successfully: The session has been updated. deleted_successfully: The session has been deleted. + add.label: Add session new.label: New session edit.label: Edit session delete.confirm: Are you sure you want to delete this session? diff --git a/translations/messages+intl-icu.fr.yaml b/translations/messages+intl-icu.fr.yaml index 79cb17f..fa115b1 100644 --- a/translations/messages+intl-icu.fr.yaml +++ b/translations/messages+intl-icu.fr.yaml @@ -103,6 +103,7 @@ surf_session: created_successfully: La session a été créée. updated_successfully: La session a été mise à jour. deleted_successfully: La session a été supprimée. + add.label: Ajouter une session new.label: Nouvelle session edit.label: Édition de la session delete.confirm: Es-tu sûr de vouloir supprimer cette session ? From 3339177b514755cf2ccfc6084e049fe7cfebb08b Mon Sep 17 00:00:00 2001 From: RomMad Date: Tue, 2 Jun 2026 19:04:02 +0200 Subject: [PATCH 2/3] fix: Adjust session start time logic and update related test to reflect new default time --- .../SurfSession/SurfSessionWriteModelFactory.php | 11 ++++++----- .../SurfSession/NewSurfSessionControllerTest.php | 7 ++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Service/SurfSession/SurfSessionWriteModelFactory.php b/src/Service/SurfSession/SurfSessionWriteModelFactory.php index 4d4dbed..97a4a8a 100644 --- a/src/Service/SurfSession/SurfSessionWriteModelFactory.php +++ b/src/Service/SurfSession/SurfSessionWriteModelFactory.php @@ -36,13 +36,14 @@ public function create(?AbstractTripReadModel $trip = null): SurfSessionWriteMod private function resolveStartAt(?AbstractTripReadModel $trip): \DateTimeImmutable { $now = new \DateTimeImmutable(); - $currentHourStart = $now->setTime((int) $now->format('H'), 0); - return match (true) { - null === $trip => $currentHourStart, - $now < $trip->startAt => $trip->startAt->setTime(8, 0), + $startAt = match (true) { + null === $trip => $now, + $now < $trip->startAt => $trip->startAt, $now > $trip->endAt => $trip->endAt, - default => $currentHourStart, + default => $now, }; + + return $startAt->setTime(10, 0); } } diff --git a/tests/Functional/Controller/SurfSession/NewSurfSessionControllerTest.php b/tests/Functional/Controller/SurfSession/NewSurfSessionControllerTest.php index 7e9a6bc..d7faea6 100644 --- a/tests/Functional/Controller/SurfSession/NewSurfSessionControllerTest.php +++ b/tests/Functional/Controller/SurfSession/NewSurfSessionControllerTest.php @@ -94,7 +94,7 @@ public function testCreateSurfSessionIsSuccessful(): void public function testNewSurfSessionWithTrip(): void { $trip = TripFactory::find(['title' => Title::from(TripStory::CURRENT_TRIP_TITLE)]); - $now = new \DateTimeImmutable(); + $now = new \DateTimeImmutable()->setTime(10, 0); $this->client->request(Request::METHOD_GET, sprintf(self::PATH_NEW_FROM_TRIP, $trip->id)); @@ -111,11 +111,8 @@ public function testNewSurfSessionWithTrip(): void private function parseDateTimeFromInput(string $selector): \DateTimeImmutable { $value = $this->getInputValue($selector); - $parsedDate = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i', $value); - $this->assertInstanceOf(\DateTimeImmutable::class, $parsedDate); - - return $parsedDate; + return \DateTimeImmutable::createFromFormat('Y-m-d\TH:i', $value); } private function getInputValue(string $selector): string From 514a7fc968c7b7b7ad3ed0be53e2f08e61601660 Mon Sep 17 00:00:00 2001 From: RomMad Date: Wed, 3 Jun 2026 11:42:28 +0200 Subject: [PATCH 3/3] refactor: Update trip handling in SurfSessionWriteModelFactory to improve clarity and consistency --- src/Service/SurfSession/SurfSessionWriteModelFactory.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Service/SurfSession/SurfSessionWriteModelFactory.php b/src/Service/SurfSession/SurfSessionWriteModelFactory.php index 97a4a8a..33ee169 100644 --- a/src/Service/SurfSession/SurfSessionWriteModelFactory.php +++ b/src/Service/SurfSession/SurfSessionWriteModelFactory.php @@ -16,24 +16,25 @@ public function create(?AbstractTripReadModel $trip = null): SurfSessionWriteMod { $surfSessionWriteModel = new SurfSessionWriteModel(); $startAt = $this->resolveStartAt($trip); + $tripSelect = null; if (null !== $trip) { - $trip = new TripSelectReadModel( + $tripSelect = new TripSelectReadModel( id: $trip->id, title: $trip->title->value, location: $trip->location->value, ); } - $surfSessionWriteModel->trip = $trip; - $surfSessionWriteModel->spot = $trip?->location; + $surfSessionWriteModel->trip = $tripSelect; + $surfSessionWriteModel->spot = $tripSelect?->location; $surfSessionWriteModel->startAt = $startAt; $surfSessionWriteModel->endAt = $startAt->modify(sprintf('+%d hours', self::DEFAULT_SESSION_DURATION_HOURS)); return $surfSessionWriteModel; } - private function resolveStartAt(?AbstractTripReadModel $trip): \DateTimeImmutable + private function resolveStartAt(?AbstractTripReadModel $trip = null): \DateTimeImmutable { $now = new \DateTimeImmutable();