From b7612c925a7bd80c3ce9ad8ba81fc0183452a6b3 Mon Sep 17 00:00:00 2001 From: axel-paillaud Date: Sun, 29 Mar 2026 14:31:32 +0200 Subject: [PATCH 1/8] add GET and CREATE cms page endpoint --- src/ApiPlatform/Resources/CmsPage/CmsPage.php | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/ApiPlatform/Resources/CmsPage/CmsPage.php diff --git a/src/ApiPlatform/Resources/CmsPage/CmsPage.php b/src/ApiPlatform/Resources/CmsPage/CmsPage.php new file mode 100644 index 00000000..3a422444 --- /dev/null +++ b/src/ApiPlatform/Resources/CmsPage/CmsPage.php @@ -0,0 +1,135 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\CmsPage; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\ConstraintValidator\Constraints\CleanHtml; +use PrestaShop\PrestaShop\Core\ConstraintValidator\Constraints\DefaultLanguage; +use PrestaShop\PrestaShop\Core\ConstraintValidator\Constraints\IsUrlRewrite; +use PrestaShop\PrestaShop\Core\ConstraintValidator\Constraints\TypedRegex; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Command\AddCmsPageCommand; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Exception\CannotAddCmsPageException; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Exception\CmsPageNotFoundException; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Query\GetCmsPageForEditing; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSCreate; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use PrestaShopBundle\ApiPlatform\Metadata\LocalizedValue; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + operations: [ + new CQRSGet( + uriTemplate: '/cms-pages/{cmsPageId}', + CQRSQuery: GetCmsPageForEditing::class, + scopes: ['cms_page_read'], + CQRSQueryMapping: self::QUERY_MAPPING, + ), + new CQRSCreate( + uriTemplate: '/cms-pages', + validationContext: ['groups' => ['Default', 'Create']], + CQRSCommand: AddCmsPageCommand::class, + CQRSQuery: GetCmsPageForEditing::class, + scopes: ['cms_page_write'], + CQRSQueryMapping: self::QUERY_MAPPING, + CQRSCommandMapping: self::COMMAND_MAPPING, + ), + ], + exceptionToStatus: [ + CmsPageNotFoundException::class => Response::HTTP_NOT_FOUND, + CannotAddCmsPageException::class => Response::HTTP_UNPROCESSABLE_ENTITY, + ], +)] +class CmsPage +{ + #[ApiProperty(identifier: true)] + public int $cmsPageId; + + public int $cmsPageCategoryId; + + #[LocalizedValue] + #[DefaultLanguage(groups: ['Create'], fieldName: 'titles')] + #[Assert\All(constraints: [ + new TypedRegex(['type' => TypedRegex::TYPE_GENERIC_NAME]), + new Assert\Length(['max' => 255]), + ])] + public array $titles; + + #[LocalizedValue] + #[Assert\All(constraints: [ + new TypedRegex(['type' => TypedRegex::TYPE_GENERIC_NAME]), + new Assert\Length(['max' => 255]), + ])] + public array $metaTitles; + + #[LocalizedValue] + #[Assert\All(constraints: [ + new TypedRegex(['type' => TypedRegex::TYPE_GENERIC_NAME]), + new Assert\Length(['max' => 512]), + ])] + public array $metaDescriptions; + + #[LocalizedValue] + #[DefaultLanguage(groups: ['Create'], fieldName: 'friendlyUrls')] + #[Assert\All(constraints: [ + new IsUrlRewrite(), + new Assert\Length(['max' => 128]), + ])] + public array $friendlyUrls; + + #[LocalizedValue] + #[Assert\All(constraints: [ + new CleanHtml(), + ])] + public array $contents; + + public bool $indexedForSearch; + + public bool $enabled; + + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] + #[Assert\NotBlank(allowNull: true)] + public array $shopIds; + + public const QUERY_MAPPING = [ + '[localizedTitle]' => '[titles]', + '[localizedMetaTitle]' => '[metaTitles]', + '[localizedMetaDescription]' => '[metaDescriptions]', + '[localizedFriendlyUrl]' => '[friendlyUrls]', + '[localizedContent]' => '[contents]', + '[indexedForSearch]' => '[indexedForSearch]', + '[displayed]' => '[enabled]', + '[shopAssociation]' => '[shopIds]', + ]; + + public const COMMAND_MAPPING = [ + '[titles]' => '[localizedTitle]', + '[metaTitles]' => '[localizedMetaTitle]', + '[metaDescriptions]' => '[localizedMetaDescription]', + '[friendlyUrls]' => '[localizedFriendlyUrl]', + '[contents]' => '[localizedContent]', + '[enabled]' => '[displayed]', + '[shopIds]' => '[shopAssociation]', + ]; +} From 1746d94779a2df414c3813bb2c1dfd0627bd5c4c Mon Sep 17 00:00:00 2001 From: axel-paillaud Date: Sun, 29 Mar 2026 14:36:36 +0200 Subject: [PATCH 2/8] add test for CmsPage endpoint --- .../ApiPlatform/CmsPageEndpointTest.php | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 tests/Integration/ApiPlatform/CmsPageEndpointTest.php diff --git a/tests/Integration/ApiPlatform/CmsPageEndpointTest.php b/tests/Integration/ApiPlatform/CmsPageEndpointTest.php new file mode 100644 index 00000000..ba53793b --- /dev/null +++ b/tests/Integration/ApiPlatform/CmsPageEndpointTest.php @@ -0,0 +1,185 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PsApiResourcesTest\Integration\ApiPlatform; + +use Symfony\Component\HttpFoundation\Response; +use Tests\Resources\DatabaseDump; +use Tests\Resources\Resetter\LanguageResetter; + +class CmsPageEndpointTest extends ApiTestCase +{ + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + LanguageResetter::resetLanguages(); + self::addLanguageByLocale('fr-FR'); + self::resetTables(); + self::createApiClient(['cms_page_read', 'cms_page_write']); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + LanguageResetter::resetLanguages(); + self::resetTables(); + } + + protected static function resetTables(): void + { + DatabaseDump::restoreTables([ + 'cms', + 'cms_lang', + 'cms_shop', + ]); + } + + public static function getProtectedEndpoints(): iterable + { + yield 'get endpoint' => ['GET', '/cms-pages/1']; + yield 'create endpoint' => ['POST', '/cms-pages']; + } + + public function testAddCmsPage(): int + { + $postData = [ + 'cmsPageCategoryId' => 1, + 'titles' => [ + 'en-US' => 'Test CMS Page', + 'fr-FR' => 'Page CMS de test', + ], + 'metaTitles' => [ + 'en-US' => 'Meta Title EN', + 'fr-FR' => 'Meta Title FR', + ], + 'metaDescriptions' => [ + 'en-US' => 'Meta description EN', + 'fr-FR' => 'Meta description FR', + ], + 'friendlyUrls' => [ + 'en-US' => 'test-cms-page', + 'fr-FR' => 'page-cms-de-test', + ], + 'contents' => [ + 'en-US' => '

Content EN

', + 'fr-FR' => '

Contenu FR

', + ], + 'indexedForSearch' => true, + 'enabled' => true, + 'shopIds' => [1], + ]; + + $response = $this->createItem('/cms-pages', $postData, ['cms_page_write']); + $this->assertArrayHasKey('cmsPageId', $response); + $cmsPageId = $response['cmsPageId']; + + $this->assertEquals( + ['cmsPageId' => $cmsPageId] + $postData, + $response + ); + + return $cmsPageId; + } + + /** @depends testAddCmsPage */ + public function testGetCmsPage(int $cmsPageId): int + { + $response = $this->getItem('/cms-pages/' . $cmsPageId, ['cms_page_read']); + $this->assertEquals([ + 'cmsPageId' => $cmsPageId, + 'cmsPageCategoryId' => 1, + 'titles' => [ + 'en-US' => 'Test CMS Page', + 'fr-FR' => 'Page CMS de test', + ], + 'metaTitles' => [ + 'en-US' => 'Meta Title EN', + 'fr-FR' => 'Meta Title FR', + ], + 'metaDescriptions' => [ + 'en-US' => 'Meta description EN', + 'fr-FR' => 'Meta description FR', + ], + 'friendlyUrls' => [ + 'en-US' => 'test-cms-page', + 'fr-FR' => 'page-cms-de-test', + ], + 'contents' => [ + 'en-US' => '

Content EN

', + 'fr-FR' => '

Contenu FR

', + ], + 'indexedForSearch' => true, + 'enabled' => true, + 'shopIds' => [1], + ], $response); + + return $cmsPageId; + } + + public function testInvalidCmsPage(): void + { + // Missing required titles (default language) and friendlyUrls + $invalidData = [ + 'cmsPageCategoryId' => 1, + 'titles' => [ + // en-US (default language) is missing + 'fr-FR' => 'Page CMS<', + ], + 'friendlyUrls' => [ + // en-US (default language) is missing + 'fr-FR' => 'invalid friendly url with spaces', + ], + 'metaTitles' => [], + 'metaDescriptions' => [], + 'contents' => [], + 'indexedForSearch' => true, + 'enabled' => true, + 'shopIds' => [1], + ]; + + $response = $this->createItem( + '/cms-pages', + $invalidData, + ['cms_page_write'], + Response::HTTP_UNPROCESSABLE_ENTITY + ); + + $this->assertValidationErrors([ + [ + 'propertyPath' => 'titles', + 'message' => 'The field titles is required at least in your default language.', + ], + [ + 'propertyPath' => 'titles[fr-FR]', + 'message' => '"Page CMS<" is invalid', + ], + [ + 'propertyPath' => 'friendlyUrls', + 'message' => 'The field friendlyUrls is required at least in your default language.', + ], + [ + 'propertyPath' => 'friendlyUrls[fr-FR]', + 'message' => '"invalid friendly url with spaces" is invalid', + ], + ], $response); + } +} From 561dfe7e26712ae728b3e24910f7e0da7d1205bd Mon Sep 17 00:00:00 2001 From: axel-paillaud Date: Sun, 29 Mar 2026 15:02:05 +0200 Subject: [PATCH 3/8] remove useless mapping --- src/ApiPlatform/Resources/CmsPage/CmsPage.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ApiPlatform/Resources/CmsPage/CmsPage.php b/src/ApiPlatform/Resources/CmsPage/CmsPage.php index 3a422444..10406f92 100644 --- a/src/ApiPlatform/Resources/CmsPage/CmsPage.php +++ b/src/ApiPlatform/Resources/CmsPage/CmsPage.php @@ -118,7 +118,6 @@ class CmsPage '[localizedMetaDescription]' => '[metaDescriptions]', '[localizedFriendlyUrl]' => '[friendlyUrls]', '[localizedContent]' => '[contents]', - '[indexedForSearch]' => '[indexedForSearch]', '[displayed]' => '[enabled]', '[shopAssociation]' => '[shopIds]', ]; From 28e6be98205f3989a3d6766c132d93d9230d7661 Mon Sep 17 00:00:00 2001 From: axel-paillaud Date: Sun, 29 Mar 2026 15:29:07 +0200 Subject: [PATCH 4/8] send fr-FR valid for friendlyUrls --- tests/Integration/ApiPlatform/CmsPageEndpointTest.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/Integration/ApiPlatform/CmsPageEndpointTest.php b/tests/Integration/ApiPlatform/CmsPageEndpointTest.php index ba53793b..c50e324a 100644 --- a/tests/Integration/ApiPlatform/CmsPageEndpointTest.php +++ b/tests/Integration/ApiPlatform/CmsPageEndpointTest.php @@ -146,7 +146,7 @@ public function testInvalidCmsPage(): void ], 'friendlyUrls' => [ // en-US (default language) is missing - 'fr-FR' => 'invalid friendly url with spaces', + 'fr-FR' => 'valid-friendly-url', ], 'metaTitles' => [], 'metaDescriptions' => [], @@ -176,10 +176,6 @@ public function testInvalidCmsPage(): void 'propertyPath' => 'friendlyUrls', 'message' => 'The field friendlyUrls is required at least in your default language.', ], - [ - 'propertyPath' => 'friendlyUrls[fr-FR]', - 'message' => '"invalid friendly url with spaces" is invalid', - ], ], $response); } } From 9fad92faddc381e41e74dbc1e5a244d804e02f55 Mon Sep 17 00:00:00 2001 From: axel-paillaud Date: Wed, 1 Apr 2026 06:55:00 +0200 Subject: [PATCH 5/8] add delete, toggle, patch endpoint for CmsPage --- src/ApiPlatform/Resources/CmsPage/CmsPage.php | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/ApiPlatform/Resources/CmsPage/CmsPage.php b/src/ApiPlatform/Resources/CmsPage/CmsPage.php index 10406f92..f02f72f3 100644 --- a/src/ApiPlatform/Resources/CmsPage/CmsPage.php +++ b/src/ApiPlatform/Resources/CmsPage/CmsPage.php @@ -29,11 +29,19 @@ use PrestaShop\PrestaShop\Core\ConstraintValidator\Constraints\IsUrlRewrite; use PrestaShop\PrestaShop\Core\ConstraintValidator\Constraints\TypedRegex; use PrestaShop\PrestaShop\Core\Domain\CmsPage\Command\AddCmsPageCommand; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Command\DeleteCmsPageCommand; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Command\EditCmsPageCommand; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Command\ToggleCmsPageStatusCommand; use PrestaShop\PrestaShop\Core\Domain\CmsPage\Exception\CannotAddCmsPageException; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Exception\CannotDeleteCmsPageException; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Exception\CannotEditCmsPageException; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Exception\CannotToggleCmsPageException; use PrestaShop\PrestaShop\Core\Domain\CmsPage\Exception\CmsPageNotFoundException; use PrestaShop\PrestaShop\Core\Domain\CmsPage\Query\GetCmsPageForEditing; use PrestaShopBundle\ApiPlatform\Metadata\CQRSCreate; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSDelete; use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSPartialUpdate; use PrestaShopBundle\ApiPlatform\Metadata\LocalizedValue; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Validator\Constraints as Assert; @@ -53,12 +61,36 @@ CQRSQuery: GetCmsPageForEditing::class, scopes: ['cms_page_write'], CQRSQueryMapping: self::QUERY_MAPPING, - CQRSCommandMapping: self::COMMAND_MAPPING, + CQRSCommandMapping: self::CREATE_COMMAND_MAPPING, + ), + new CQRSPartialUpdate( + uriTemplate: '/cms-pages/{cmsPageId}', + validationContext: ['groups' => ['Default', 'Update']], + CQRSCommand: EditCmsPageCommand::class, + CQRSQuery: GetCmsPageForEditing::class, + scopes: ['cms_page_write'], + CQRSQueryMapping: self::QUERY_MAPPING, + CQRSCommandMapping: self::UPDATE_COMMAND_MAPPING, + ), + new CQRSPartialUpdate( + uriTemplate: '/cms-pages/{cmsPageId}/toggle-status', + CQRSCommand: ToggleCmsPageStatusCommand::class, + CQRSQuery: GetCmsPageForEditing::class, + scopes: ['cms_page_write'], + CQRSQueryMapping: self::QUERY_MAPPING, + ), + new CQRSDelete( + uriTemplate: '/cms-pages/{cmsPageId}', + CQRSCommand: DeleteCmsPageCommand::class, + scopes: ['cms_page_write'], ), ], exceptionToStatus: [ CmsPageNotFoundException::class => Response::HTTP_NOT_FOUND, CannotAddCmsPageException::class => Response::HTTP_UNPROCESSABLE_ENTITY, + CannotEditCmsPageException::class => Response::HTTP_UNPROCESSABLE_ENTITY, + CannotDeleteCmsPageException::class => Response::HTTP_UNPROCESSABLE_ENTITY, + CannotToggleCmsPageException::class => Response::HTTP_UNPROCESSABLE_ENTITY, ], )] class CmsPage @@ -70,6 +102,7 @@ class CmsPage #[LocalizedValue] #[DefaultLanguage(groups: ['Create'], fieldName: 'titles')] + #[DefaultLanguage(groups: ['Update'], fieldName: 'titles', allowNull: true)] #[Assert\All(constraints: [ new TypedRegex(['type' => TypedRegex::TYPE_GENERIC_NAME]), new Assert\Length(['max' => 255]), @@ -92,6 +125,7 @@ class CmsPage #[LocalizedValue] #[DefaultLanguage(groups: ['Create'], fieldName: 'friendlyUrls')] + #[DefaultLanguage(groups: ['Update'], fieldName: 'friendlyUrls', allowNull: true)] #[Assert\All(constraints: [ new IsUrlRewrite(), new Assert\Length(['max' => 128]), @@ -118,11 +152,12 @@ class CmsPage '[localizedMetaDescription]' => '[metaDescriptions]', '[localizedFriendlyUrl]' => '[friendlyUrls]', '[localizedContent]' => '[contents]', + '[indexedForSearch]' => '[indexedForSearch]', '[displayed]' => '[enabled]', '[shopAssociation]' => '[shopIds]', ]; - public const COMMAND_MAPPING = [ + public const CREATE_COMMAND_MAPPING = [ '[titles]' => '[localizedTitle]', '[metaTitles]' => '[localizedMetaTitle]', '[metaDescriptions]' => '[localizedMetaDescription]', @@ -131,4 +166,15 @@ class CmsPage '[enabled]' => '[displayed]', '[shopIds]' => '[shopAssociation]', ]; + + public const UPDATE_COMMAND_MAPPING = [ + '[titles]' => '[localizedTitle]', + '[metaTitles]' => '[localizedMetaTitle]', + '[metaDescriptions]' => '[localizedMetaDescription]', + '[friendlyUrls]' => '[localizedFriendlyUrl]', + '[contents]' => '[localizedContent]', + '[indexedForSearch]' => '[isIndexedForSearch]', + '[enabled]' => '[isDisplayed]', + '[shopIds]' => '[shopAssociation]', + ]; } From 3e52abeb9eac6d2061b18c244647015e92425029 Mon Sep 17 00:00:00 2001 From: axel-paillaud Date: Wed, 1 Apr 2026 06:59:29 +0200 Subject: [PATCH 6/8] add bulk delete endpoint --- .../Resources/CmsPage/BulkDeleteCmsPages.php | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/ApiPlatform/Resources/CmsPage/BulkDeleteCmsPages.php diff --git a/src/ApiPlatform/Resources/CmsPage/BulkDeleteCmsPages.php b/src/ApiPlatform/Resources/CmsPage/BulkDeleteCmsPages.php new file mode 100644 index 00000000..02fd9d27 --- /dev/null +++ b/src/ApiPlatform/Resources/CmsPage/BulkDeleteCmsPages.php @@ -0,0 +1,54 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\CmsPage; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Command\BulkDeleteCmsPageCommand; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Exception\CmsPageNotFoundException; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSDelete; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + operations: [ + new CQRSDelete( + uriTemplate: '/cms-pages/bulk-delete', + CQRSCommand: BulkDeleteCmsPageCommand::class, + scopes: ['cms_page_write'], + allowEmptyBody: false, + ), + ], + exceptionToStatus: [ + CmsPageNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class BulkDeleteCmsPages +{ + /** + * @var int[] + */ + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] + #[Assert\NotBlank] + public array $cmsPageIds; +} From b9f1ad53bcc9fe27ffe173633d6662d70b10560e Mon Sep 17 00:00:00 2001 From: axel-paillaud Date: Wed, 1 Apr 2026 07:02:28 +0200 Subject: [PATCH 7/8] add bulk enable / disable endpoint --- .../CmsPage/BulkUpdateStatusCmsPages.php | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/ApiPlatform/Resources/CmsPage/BulkUpdateStatusCmsPages.php diff --git a/src/ApiPlatform/Resources/CmsPage/BulkUpdateStatusCmsPages.php b/src/ApiPlatform/Resources/CmsPage/BulkUpdateStatusCmsPages.php new file mode 100644 index 00000000..f78ffff1 --- /dev/null +++ b/src/ApiPlatform/Resources/CmsPage/BulkUpdateStatusCmsPages.php @@ -0,0 +1,61 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\CmsPage; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Command\BulkDisableCmsPageCommand; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Command\BulkEnableCmsPageCommand; +use PrestaShop\PrestaShop\Core\Domain\CmsPage\Exception\CmsPageNotFoundException; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSUpdate; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + operations: [ + new CQRSUpdate( + uriTemplate: '/cms-pages/bulk-enable', + output: false, + CQRSCommand: BulkEnableCmsPageCommand::class, + scopes: ['cms_page_write'], + ), + new CQRSUpdate( + uriTemplate: '/cms-pages/bulk-disable', + output: false, + CQRSCommand: BulkDisableCmsPageCommand::class, + scopes: ['cms_page_write'], + ), + ], + exceptionToStatus: [ + CmsPageNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class BulkUpdateStatusCmsPages +{ + /** + * @var int[] + */ + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] + #[Assert\NotBlank] + public array $cmsPageIds; +} From 460f501b90f9def57ff79bc5cc2207ed6ed8f435 Mon Sep 17 00:00:00 2001 From: axel-paillaud Date: Wed, 1 Apr 2026 07:50:43 +0200 Subject: [PATCH 8/8] fix: align toggle-status endpoint with CQRSUpdate pattern (output: false, allowEmptyBody: true) --- src/ApiPlatform/Resources/CmsPage/CmsPage.php | 7 +- .../ApiPlatform/CmsPageEndpointTest.php | 128 +++++++++++++++++- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/src/ApiPlatform/Resources/CmsPage/CmsPage.php b/src/ApiPlatform/Resources/CmsPage/CmsPage.php index f02f72f3..6fadfba5 100644 --- a/src/ApiPlatform/Resources/CmsPage/CmsPage.php +++ b/src/ApiPlatform/Resources/CmsPage/CmsPage.php @@ -42,6 +42,7 @@ use PrestaShopBundle\ApiPlatform\Metadata\CQRSDelete; use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; use PrestaShopBundle\ApiPlatform\Metadata\CQRSPartialUpdate; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSUpdate; use PrestaShopBundle\ApiPlatform\Metadata\LocalizedValue; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Validator\Constraints as Assert; @@ -72,12 +73,12 @@ CQRSQueryMapping: self::QUERY_MAPPING, CQRSCommandMapping: self::UPDATE_COMMAND_MAPPING, ), - new CQRSPartialUpdate( + new CQRSUpdate( uriTemplate: '/cms-pages/{cmsPageId}/toggle-status', + output: false, + allowEmptyBody: true, CQRSCommand: ToggleCmsPageStatusCommand::class, - CQRSQuery: GetCmsPageForEditing::class, scopes: ['cms_page_write'], - CQRSQueryMapping: self::QUERY_MAPPING, ), new CQRSDelete( uriTemplate: '/cms-pages/{cmsPageId}', diff --git a/tests/Integration/ApiPlatform/CmsPageEndpointTest.php b/tests/Integration/ApiPlatform/CmsPageEndpointTest.php index c50e324a..4851d4c3 100644 --- a/tests/Integration/ApiPlatform/CmsPageEndpointTest.php +++ b/tests/Integration/ApiPlatform/CmsPageEndpointTest.php @@ -57,6 +57,12 @@ public static function getProtectedEndpoints(): iterable { yield 'get endpoint' => ['GET', '/cms-pages/1']; yield 'create endpoint' => ['POST', '/cms-pages']; + yield 'patch endpoint' => ['PATCH', '/cms-pages/1']; + yield 'toggle status endpoint' => ['PUT', '/cms-pages/1/toggle-status']; + yield 'delete endpoint' => ['DELETE', '/cms-pages/1']; + yield 'bulk delete endpoint' => ['DELETE', '/cms-pages/bulk-delete']; + yield 'bulk enable endpoint' => ['PUT', '/cms-pages/bulk-enable']; + yield 'bulk disable endpoint' => ['PUT', '/cms-pages/bulk-disable']; } public function testAddCmsPage(): int @@ -135,9 +141,129 @@ public function testGetCmsPage(int $cmsPageId): int return $cmsPageId; } + /** @depends testGetCmsPage */ + public function testPartialUpdateCmsPage(int $cmsPageId): int + { + $patchData = [ + 'titles' => [ + 'en-US' => 'Updated CMS Page', + 'fr-FR' => 'Page CMS mise à jour', + ], + 'enabled' => false, + ]; + + $updated = $this->partialUpdateItem('/cms-pages/' . $cmsPageId, $patchData, ['cms_page_write']); + $this->assertEquals([ + 'cmsPageId' => $cmsPageId, + 'cmsPageCategoryId' => 1, + 'titles' => [ + 'en-US' => 'Updated CMS Page', + 'fr-FR' => 'Page CMS mise à jour', + ], + 'metaTitles' => [ + 'en-US' => 'Meta Title EN', + 'fr-FR' => 'Meta Title FR', + ], + 'metaDescriptions' => [ + 'en-US' => 'Meta description EN', + 'fr-FR' => 'Meta description FR', + ], + 'friendlyUrls' => [ + 'en-US' => 'test-cms-page', + 'fr-FR' => 'page-cms-de-test', + ], + 'contents' => [ + 'en-US' => '

Content EN

', + 'fr-FR' => '

Contenu FR

', + ], + 'indexedForSearch' => true, + 'enabled' => false, + 'shopIds' => [1], + ], $updated); + + // Verify GET reflects the changes + $fetched = $this->getItem('/cms-pages/' . $cmsPageId, ['cms_page_read']); + $this->assertEquals($updated, $fetched); + + return $cmsPageId; + } + + /** @depends testPartialUpdateCmsPage */ + public function testToggleCmsPageStatus(int $cmsPageId): int + { + // Page is currently disabled, toggle should enable it + $this->updateItem('/cms-pages/' . $cmsPageId . '/toggle-status', [], ['cms_page_write'], Response::HTTP_NO_CONTENT); + $response = $this->getItem('/cms-pages/' . $cmsPageId, ['cms_page_read']); + $this->assertEquals(true, $response['enabled']); + + // Toggle again should disable it + $this->updateItem('/cms-pages/' . $cmsPageId . '/toggle-status', [], ['cms_page_write'], Response::HTTP_NO_CONTENT); + $response = $this->getItem('/cms-pages/' . $cmsPageId, ['cms_page_read']); + $this->assertEquals(false, $response['enabled']); + + return $cmsPageId; + } + + /** @depends testToggleCmsPageStatus */ + public function testBulkUpdateStatusCmsPages(int $cmsPageId): int + { + // Bulk enable + $this->updateItem('/cms-pages/bulk-enable', ['cmsPageIds' => [$cmsPageId]], ['cms_page_write'], Response::HTTP_NO_CONTENT); + $response = $this->getItem('/cms-pages/' . $cmsPageId, ['cms_page_read']); + $this->assertEquals(true, $response['enabled']); + + // Bulk disable + $this->updateItem('/cms-pages/bulk-disable', ['cmsPageIds' => [$cmsPageId]], ['cms_page_write'], Response::HTTP_NO_CONTENT); + $response = $this->getItem('/cms-pages/' . $cmsPageId, ['cms_page_read']); + $this->assertEquals(false, $response['enabled']); + + return $cmsPageId; + } + + /** @depends testBulkUpdateStatusCmsPages */ + public function testDeleteCmsPage(int $cmsPageId): void + { + $this->deleteItem('/cms-pages/' . $cmsPageId, ['cms_page_write']); + $this->getItem('/cms-pages/' . $cmsPageId, ['cms_page_read'], Response::HTTP_NOT_FOUND); + } + + public function testBulkDeleteCmsPages(): void + { + // Create two pages to bulk delete + $page1 = $this->createItem('/cms-pages', [ + 'cmsPageCategoryId' => 1, + 'titles' => ['en-US' => 'Bulk Page 1'], + 'friendlyUrls' => ['en-US' => 'bulk-page-1'], + 'metaTitles' => [], + 'metaDescriptions' => [], + 'contents' => [], + 'indexedForSearch' => false, + 'enabled' => false, + 'shopIds' => [1], + ], ['cms_page_write']); + + $page2 = $this->createItem('/cms-pages', [ + 'cmsPageCategoryId' => 1, + 'titles' => ['en-US' => 'Bulk Page 2'], + 'friendlyUrls' => ['en-US' => 'bulk-page-2'], + 'metaTitles' => [], + 'metaDescriptions' => [], + 'contents' => [], + 'indexedForSearch' => false, + 'enabled' => false, + 'shopIds' => [1], + ], ['cms_page_write']); + + $this->deleteItem('/cms-pages/bulk-delete', ['cms_page_write'], Response::HTTP_NO_CONTENT, [ + 'json' => ['cmsPageIds' => [$page1['cmsPageId'], $page2['cmsPageId']]], + ]); + + $this->getItem('/cms-pages/' . $page1['cmsPageId'], ['cms_page_read'], Response::HTTP_NOT_FOUND); + $this->getItem('/cms-pages/' . $page2['cmsPageId'], ['cms_page_read'], Response::HTTP_NOT_FOUND); + } + public function testInvalidCmsPage(): void { - // Missing required titles (default language) and friendlyUrls $invalidData = [ 'cmsPageCategoryId' => 1, 'titles' => [