From e572fcc1f1e00ff01568938a4248925b5c458568 Mon Sep 17 00:00:00 2001 From: Robin Morel Date: Tue, 30 Jun 2026 11:01:36 +0200 Subject: [PATCH 1/4] fix form hydratation --- Controller/SelectionUpdateController.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Controller/SelectionUpdateController.php b/Controller/SelectionUpdateController.php index 7e2edec..a110fed 100644 --- a/Controller/SelectionUpdateController.php +++ b/Controller/SelectionUpdateController.php @@ -11,6 +11,7 @@ use Selection\Model\Selection as SelectionModel; use Selection\Model\SelectionContainerAssociatedSelection; use Selection\Model\SelectionContentQuery; +use Selection\Model\SelectionI18nQuery; use Selection\Model\SelectionProductQuery; use Selection\Model\SelectionQuery; use Selection\Selection; @@ -349,7 +350,7 @@ protected function getObjectFromEvent($event): mixed protected function getExistingObject(): ?\Propel\Runtime\ActiveRecord\ActiveRecordInterface { $selection = SelectionQuery::create() - ->findPk($this->getRequest()->request->get('selectionId', $this->getRequest()->query->get('selectionId', 0))); + ->findPk($this->getRequest()->attributes->get('selectionId')); if (null !== $selection) { $selection->setLocale($this->getCurrentEditionLocale()); @@ -393,12 +394,13 @@ protected function renderListTemplate($currentOrder): Response protected function renderEditionTemplate(): Response { $request = $this->getRequest(); - $selectionId = $request->query->get('selectionId', $request->request->get('selectionId')); - $currentTab = $request->query->get('current_tab', $request->request->get('current_tab')); + $selectionId = $request->attributes->get('selectionId'); + $currentTab = $request->attributes->get('current_tab'); + $locale = $this->getCurrentEditionLocale(); + + $selection = $this->getExistingObject(); - $selection = SelectionQuery::create()->findPk($selectionId); $form = $this->hydrateObjectForm($this->getParserContext(), $selection); - $locale = $this->getCurrentEditionLocale(); return $this->renderTwig('selection-edit.html.twig', [ 'selection_id' => $selectionId, @@ -543,8 +545,7 @@ public function processUpdateSeoAction( Request $request, ParserContext $parserContext, EventDispatcherInterface $eventDispatcher - ): Response - { + ): Response { $selectionId = $request->query->get('current_id'); $request->request->set("selectionId", $selectionId); return parent::processUpdateSeoAction($request, $parserContext, $eventDispatcher); From e668962ef1a1fb9d16ae3b8bb848920280da2897 Mon Sep 17 00:00:00 2001 From: Robin Morel Date: Tue, 30 Jun 2026 12:02:09 +0200 Subject: [PATCH 2/4] feat: add front API resources and Twig helpers for selections --- .../SelectionContainerCodeExtension.php | 49 +++++ Api/Resource/Selection.php | 187 ++++++++++++++++++ Api/Resource/SelectionContainer.php | 158 +++++++++++++++ Api/Resource/SelectionContainerI18n.php | 116 +++++++++++ Api/Resource/SelectionContent.php | 84 ++++++++ Api/Resource/SelectionI18n.php | 116 +++++++++++ Readme.md | 67 ++++++- Twig/SelectionExtention.php | 45 +++++ 8 files changed, 821 insertions(+), 1 deletion(-) create mode 100644 Api/Extension/SelectionContainerCodeExtension.php create mode 100644 Api/Resource/Selection.php create mode 100644 Api/Resource/SelectionContainer.php create mode 100644 Api/Resource/SelectionContainerI18n.php create mode 100644 Api/Resource/SelectionContent.php create mode 100644 Api/Resource/SelectionI18n.php create mode 100644 Twig/SelectionExtention.php diff --git a/Api/Extension/SelectionContainerCodeExtension.php b/Api/Extension/SelectionContainerCodeExtension.php new file mode 100644 index 0000000..a3333ad --- /dev/null +++ b/Api/Extension/SelectionContainerCodeExtension.php @@ -0,0 +1,49 @@ +requestStack->getCurrentRequest()?->query->get('container_code'); + + if (null === $containerCode || '' === $containerCode) { + return; + } + + $query + ->useSelectionContainerAssociatedSelectionQuery() + ->useSelectionContainerQuery() + ->filterByCode($containerCode) + ->endUse() + ->endUse(); + } +} diff --git a/Api/Resource/Selection.php b/Api/Resource/Selection.php new file mode 100644 index 0000000..59c2a24 --- /dev/null +++ b/Api/Resource/Selection.php @@ -0,0 +1,187 @@ + [self::GROUP_FRONT_READ, self::GROUP_FRONT_READ_SINGLE, Content::GROUP_FRONT_READ]], + ), + ], + normalizationContext: ['groups' => [self::GROUP_FRONT_READ, Content::GROUP_FRONT_READ]], +)] +#[ApiFilter( + filterClass: SearchFilter::class, + properties: [ + 'id', + 'code', + ], +)] +#[ApiFilter( + filterClass: BooleanFilter::class, + properties: [ + 'visible', + ], +)] +#[ApiFilter( + filterClass: OrderFilter::class, + properties: [ + 'position', + ], +)] +class Selection extends AbstractTranslatableResource +{ + public const GROUP_FRONT_READ = 'front:selection:read'; + public const GROUP_FRONT_READ_SINGLE = 'front:selection:read:single'; + + #[Groups([self::GROUP_FRONT_READ])] + public ?int $id = null; + + #[Groups([self::GROUP_FRONT_READ])] + public ?string $code = null; + + #[Groups([self::GROUP_FRONT_READ])] + public bool $visible; + + #[Groups([self::GROUP_FRONT_READ])] + public ?int $position = null; + + #[Groups([self::GROUP_FRONT_READ])] + public ?\DateTime $createdAt = null; + + #[Groups([self::GROUP_FRONT_READ])] + public ?\DateTime $updatedAt = null; + + #[Groups([self::GROUP_FRONT_READ])] + public I18nCollection $i18ns; + + /** + * Related contents, through the selection_content join table. + * + * @var SelectionContent[] + */ + #[Relation(targetResource: SelectionContent::class)] + #[Groups([self::GROUP_FRONT_READ])] + public array $selectionContents = []; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(?int $id): self + { + $this->id = $id; + + return $this; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(?string $code): self + { + $this->code = $code; + + return $this; + } + + public function isVisible(): bool + { + return $this->visible; + } + + public function setVisible(bool $visible): self + { + $this->visible = $visible; + + return $this; + } + + public function getPosition(): ?int + { + return $this->position; + } + + public function setPosition(?int $position): self + { + $this->position = $position; + + return $this; + } + + public function getCreatedAt(): ?\DateTime + { + return $this->createdAt; + } + + public function setCreatedAt(?\DateTime $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getUpdatedAt(): ?\DateTime + { + return $this->updatedAt; + } + + public function setUpdatedAt(?\DateTime $updatedAt): self + { + $this->updatedAt = $updatedAt; + + return $this; + } + + /** + * @return SelectionContent[] + */ + public function getSelectionContents(): array + { + return $this->selectionContents; + } + + /** + * @param SelectionContent[] $selectionContents + */ + public function setSelectionContents(array $selectionContents): self + { + $this->selectionContents = $selectionContents; + + return $this; + } + + public static function getPropelRelatedTableMap(): ?TableMap + { + return new SelectionTableMap(); + } + + public static function getI18nResourceClass(): string + { + return SelectionI18n::class; + } +} diff --git a/Api/Resource/SelectionContainer.php b/Api/Resource/SelectionContainer.php new file mode 100644 index 0000000..72a226a --- /dev/null +++ b/Api/Resource/SelectionContainer.php @@ -0,0 +1,158 @@ + [self::GROUP_FRONT_READ, self::GROUP_FRONT_READ_SINGLE]], + ), + ], + normalizationContext: ['groups' => [self::GROUP_FRONT_READ]], +)] +#[ApiFilter( + filterClass: SearchFilter::class, + properties: [ + 'id', + 'code', + ], +)] +#[ApiFilter( + filterClass: BooleanFilter::class, + properties: [ + 'visible', + ], +)] +#[ApiFilter( + filterClass: OrderFilter::class, + properties: [ + 'position', + ], +)] +class SelectionContainer extends AbstractTranslatableResource +{ + public const GROUP_FRONT_READ = 'front:selection_container:read'; + public const GROUP_FRONT_READ_SINGLE = 'front:selection_container:read:single'; + + #[Groups([self::GROUP_FRONT_READ])] + public ?int $id = null; + + #[Groups([self::GROUP_FRONT_READ])] + public ?string $code = null; + + #[Groups([self::GROUP_FRONT_READ])] + public bool $visible; + + #[Groups([self::GROUP_FRONT_READ])] + public ?int $position = null; + + #[Groups([self::GROUP_FRONT_READ])] + public ?\DateTime $createdAt = null; + + #[Groups([self::GROUP_FRONT_READ])] + public ?\DateTime $updatedAt = null; + + #[Groups([self::GROUP_FRONT_READ])] + public I18nCollection $i18ns; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(?int $id): self + { + $this->id = $id; + + return $this; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(?string $code): self + { + $this->code = $code; + + return $this; + } + + public function isVisible(): bool + { + return $this->visible; + } + + public function setVisible(bool $visible): self + { + $this->visible = $visible; + + return $this; + } + + public function getPosition(): ?int + { + return $this->position; + } + + public function setPosition(?int $position): self + { + $this->position = $position; + + return $this; + } + + public function getCreatedAt(): ?\DateTime + { + return $this->createdAt; + } + + public function setCreatedAt(?\DateTime $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getUpdatedAt(): ?\DateTime + { + return $this->updatedAt; + } + + public function setUpdatedAt(?\DateTime $updatedAt): self + { + $this->updatedAt = $updatedAt; + + return $this; + } + + public static function getPropelRelatedTableMap(): ?TableMap + { + return new SelectionContainerTableMap(); + } + + public static function getI18nResourceClass(): string + { + return SelectionContainerI18n::class; + } +} diff --git a/Api/Resource/SelectionContainerI18n.php b/Api/Resource/SelectionContainerI18n.php new file mode 100644 index 0000000..91593cf --- /dev/null +++ b/Api/Resource/SelectionContainerI18n.php @@ -0,0 +1,116 @@ +title; + } + + public function setTitle(?string $title): self + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): self + { + $this->description = $description; + + return $this; + } + + public function getChapo(): ?string + { + return $this->chapo; + } + + public function setChapo(?string $chapo): self + { + $this->chapo = $chapo; + + return $this; + } + + public function getPostscriptum(): ?string + { + return $this->postscriptum; + } + + public function setPostscriptum(?string $postscriptum): self + { + $this->postscriptum = $postscriptum; + + return $this; + } + + public function getMetaTitle(): ?string + { + return $this->metaTitle; + } + + public function setMetaTitle(?string $metaTitle): self + { + $this->metaTitle = $metaTitle; + + return $this; + } + + public function getMetaDescription(): ?string + { + return $this->metaDescription; + } + + public function setMetaDescription(?string $metaDescription): self + { + $this->metaDescription = $metaDescription; + + return $this; + } + + public function getMetaKeywords(): ?string + { + return $this->metaKeywords; + } + + public function setMetaKeywords(?string $metaKeywords): self + { + $this->metaKeywords = $metaKeywords; + + return $this; + } +} diff --git a/Api/Resource/SelectionContent.php b/Api/Resource/SelectionContent.php new file mode 100644 index 0000000..8a1338d --- /dev/null +++ b/Api/Resource/SelectionContent.php @@ -0,0 +1,84 @@ + [self::GROUP_FRONT_READ, Content::GROUP_FRONT_READ]], +)] +#[CompositeIdentifiers(['selection', 'content'])] +class SelectionContent implements PropelResourceInterface +{ + use PropelResourceTrait; + + public const GROUP_FRONT_READ = 'front:selection_content:read'; + + #[Relation(targetResource: Selection::class)] + #[Groups([self::GROUP_FRONT_READ])] + public Selection $selection; + + #[Relation(targetResource: Content::class)] + #[Groups([self::GROUP_FRONT_READ, Selection::GROUP_FRONT_READ])] + public Content $content; + + #[Groups([self::GROUP_FRONT_READ, Selection::GROUP_FRONT_READ])] + public ?int $position = null; + + public function getSelection(): Selection + { + return $this->selection; + } + + public function setSelection(Selection $selection): self + { + $this->selection = $selection; + + return $this; + } + + public function getContent(): Content + { + return $this->content; + } + + public function setContent(Content $content): self + { + $this->content = $content; + + return $this; + } + + public function getPosition(): ?int + { + return $this->position; + } + + public function setPosition(?int $position): self + { + $this->position = $position; + + return $this; + } + + public static function getPropelRelatedTableMap(): ?TableMap + { + return new SelectionContentTableMap(); + } +} diff --git a/Api/Resource/SelectionI18n.php b/Api/Resource/SelectionI18n.php new file mode 100644 index 0000000..1d5ff64 --- /dev/null +++ b/Api/Resource/SelectionI18n.php @@ -0,0 +1,116 @@ +title; + } + + public function setTitle(?string $title): self + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): self + { + $this->description = $description; + + return $this; + } + + public function getChapo(): ?string + { + return $this->chapo; + } + + public function setChapo(?string $chapo): self + { + $this->chapo = $chapo; + + return $this; + } + + public function getPostscriptum(): ?string + { + return $this->postscriptum; + } + + public function setPostscriptum(?string $postscriptum): self + { + $this->postscriptum = $postscriptum; + + return $this; + } + + public function getMetaTitle(): ?string + { + return $this->metaTitle; + } + + public function setMetaTitle(?string $metaTitle): self + { + $this->metaTitle = $metaTitle; + + return $this; + } + + public function getMetaDescription(): ?string + { + return $this->metaDescription; + } + + public function setMetaDescription(?string $metaDescription): self + { + $this->metaDescription = $metaDescription; + + return $this; + } + + public function getMetaKeywords(): ?string + { + return $this->metaKeywords; + } + + public function setMetaKeywords(?string $metaKeywords): self + { + $this->metaKeywords = $metaKeywords; + + return $this; + } +} diff --git a/Readme.md b/Readme.md index 2068442..5be2c4b 100644 --- a/Readme.md +++ b/Readme.md @@ -36,7 +36,72 @@ front of the selection you wish to make visible or invisible. of the selection you wish to edit. - Delete a selection by clicking on the cog button then on the trash button in front of the selection you wish to delete. -You may then display your selection on your website by calling the selection_list loop. +You may then display your selection on your website by calling the selection_list loop, + +## REST API (API Platform) + +The module exposes two read-only front resources. They return the i18n content of every +available locale, and the selections also embed their related contents. + +| Method | URL | Description | +|---|---|---| +| GET | `/api/front/selections` | List of selections (with related contents) | +| GET | `/api/front/selections/{id}` | A single selection | +| GET | `/api/front/selection-containers` | List of selection containers | +| GET | `/api/front/selection-containers/{id}` | A single selection container | + +### Filters + +Both collections accept: + +| Parameter | Example | Description | +|---|---|---| +| `id` | `?id=1` | Filter by id (exact) | +| `code` | `?code=HOME` | Filter by code (exact) | +| `visible` | `?visible=true` | Filter by visibility | +| `order[position]` | `?order[position]=asc` | Order by position | + +The selections collection additionally accepts: + +| Parameter | Example | Description | +|---|---|---| +| `container_code` | `?container_code=MAIN` | Keep only the selections belonging to the container with this code | + +```http +GET /api/front/selections?container_code=MAIN&order[position]=asc +``` + +## Twig functions + +The module registers two Twig functions (available in any active front theme) that query +the API front endpoints above through Thelia's `DataAccessService`. They accept the same +filters as an associative array. + +| Function | Endpoint | +|---|---| +| `getSelection(params = [])` | `/api/front/selections` | +| `getSelectionContainer(params = [])` | `/api/front/selection-containers` | + +```twig +{# All selections, with their i18n content and related contents #} +{% set selections = getSelection() %} + +{# Filtered by code #} +{% set home = getSelection({code: 'HOME'}) %} + +{# Selections of a given container #} +{% set mainSelections = getSelection({container_code: 'MAIN'}) %} + +{# Selection containers #} +{% set containers = getSelectionContainer() %} + +{% for selection in selections %} +

{{ selection.i18ns.fr_FR.title }}

+ {% for selectionContent in selection.selectionContents %} +
{{ selectionContent.content.i18ns.fr_FR.title }}
+ {% endfor %} +{% endfor %} +``` ## Hook diff --git a/Twig/SelectionExtention.php b/Twig/SelectionExtention.php new file mode 100644 index 0000000..de46684 --- /dev/null +++ b/Twig/SelectionExtention.php @@ -0,0 +1,45 @@ + $params + */ + public function getSelection(array $params = []): array|object|null + { + return $this->dataAccessService->resources('/api/front/selections', $params); + } + + public function getSelectionContainer(array $params = []): array|object|null + { + return $this->dataAccessService->resources('/api/front/selection-containers', $params); + } +} From 6b5b6a3246945c84e9d3f42e360430ae6c55e5b3 Mon Sep 17 00:00:00 2001 From: Robin Morel Date: Tue, 30 Jun 2026 12:16:09 +0200 Subject: [PATCH 3/4] fix bad spelling --- Twig/{SelectionExtention.php => SelectionExtension.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Twig/{SelectionExtention.php => SelectionExtension.php} (95%) diff --git a/Twig/SelectionExtention.php b/Twig/SelectionExtension.php similarity index 95% rename from Twig/SelectionExtention.php rename to Twig/SelectionExtension.php index de46684..4d98aa3 100644 --- a/Twig/SelectionExtention.php +++ b/Twig/SelectionExtension.php @@ -8,7 +8,7 @@ use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -final class SelectionExtention extends AbstractExtension +final class SelectionExtension extends AbstractExtension { public function __construct( private readonly DataAccessService $dataAccessService, From 51a4f11bc83323f5d0cd192cb623e82867f3db4a Mon Sep 17 00:00:00 2001 From: Robin Morel Date: Tue, 30 Jun 2026 13:28:51 +0200 Subject: [PATCH 4/4] fix i18n configuration service --- Selection.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Selection.php b/Selection.php index 7cf25cc..f4c0d9f 100644 --- a/Selection.php +++ b/Selection.php @@ -1,4 +1,5 @@ load(self::getModuleCode().'\\', __DIR__) - ->exclude([THELIA_MODULE_DIR.ucfirst(self::getModuleCode()).'/I18n/*']) + ->exclude(['/I18n/*']) ->autowire(true) ->autoconfigure(true); }