diff --git a/CHANGELOG.md b/CHANGELOG.md index ed89625..5bad2f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ CHANGELOG for Shopware PWA =================== +### 0.3.1 + +> CMS landing pages are now supported + +**Added** +* Constant `LANDING_PAGE_ROUTE` in `SwagShopwarePwa\Pwa\Controller\PageController` +* Class `SwagShopwarePwa\Pwa\PageLoader\LandingPageLoader` +* Class `SwagShopwarePwa\Pwa\PageResult\Landing\LandingPageResult` +* Class `SwagShopwarePwa\Pwa\PageResult\Landing\LandingPageResultHydrator` +* PHPUnit test groups + * `pwa-page-product` + * `pwa-page-category` + * `pwa-page-landing` + * `pwa-page-routing` + ### 0.3.0 > PHP level has been increased to PHP 7.4 diff --git a/src/Pwa/Controller/PageController.php b/src/Pwa/Controller/PageController.php index bc18bb0..cb740fa 100644 --- a/src/Pwa/Controller/PageController.php +++ b/src/Pwa/Controller/PageController.php @@ -29,6 +29,8 @@ class PageController extends AbstractController const NAVIGATION_PAGE_ROUTE = 'frontend.navigation.page'; + const LANDING_PAGE_ROUTE = 'frontend.landing.page'; + /** * @var PageLoaderContextBuilderInterface */ @@ -79,7 +81,7 @@ public function __construct(PageLoaderContextBuilderInterface $pageLoaderContext * property="resourceType", * description="Type of page that was fetched. Indicates whether it is a product page or a category page", * type="string", - * enum={"frontend.detail.page", "frontend.navigation.page"} + * enum={"frontend.detail.page", "frontend.navigation.page", "frontend.landing.page"} * ), * @OA\Property( * property="resourceIdentifier", @@ -136,6 +138,9 @@ public function __construct(PageLoaderContextBuilderInterface $pageLoaderContext * description="The category associated with the loaded page.", * ref="#/components/schemas/category_flat" * ) + * ), + * @OA\Schema( + * description="A landing page result contains no specific fields." * ) * } * ) @@ -190,7 +195,6 @@ private function getPageLoader(PageLoaderContext $pageLoaderContext): ?PageLoade */ private function getPageResult(PageLoaderInterface $pageLoader, PageLoaderContext $pageLoaderContext): AbstractPageResult { - /** @var AbstractPageResult $pageResult */ $pageResult = $pageLoader->load($pageLoaderContext); diff --git a/src/Pwa/PageLoader/Context/PathResolver.php b/src/Pwa/PageLoader/Context/PathResolver.php index 560cc32..4a234c3 100644 --- a/src/Pwa/PageLoader/Context/PathResolver.php +++ b/src/Pwa/PageLoader/Context/PathResolver.php @@ -17,7 +17,8 @@ class PathResolver implements PathResolverInterface { private const MATCH_MAP = [ PageController::NAVIGATION_PAGE_ROUTE => '/^\/?navigation\/([a-f0-9]{32})$/', - PageController::PRODUCT_PAGE_ROUTE => '/^\/?detail\/([a-f0-9]{32})$/' + PageController::PRODUCT_PAGE_ROUTE => '/^\/?detail\/([a-f0-9]{32})$/', + PageController::LANDING_PAGE_ROUTE => '/^\/?landingPage\/([a-f0-9]{32})$/' ]; private const ROOT_ROUTE_NAME = PageController::NAVIGATION_PAGE_ROUTE; diff --git a/src/Pwa/PageLoader/LandingPageLoader.php b/src/Pwa/PageLoader/LandingPageLoader.php new file mode 100644 index 0000000..6896e0c --- /dev/null +++ b/src/Pwa/PageLoader/LandingPageLoader.php @@ -0,0 +1,61 @@ +landingPageRoute = $landingPageRoute; + $this->resultHydrator = $resultHydrator; + } + + public function getResourceType(): string + { + return self::RESOURCE_TYPE; + } + + /** + * @param PageLoaderContext $pageLoaderContext + * + * @return LandingPageResult + */ + public function load(PageLoaderContext $pageLoaderContext): LandingPageResult + { + $landingPageResult = $this->landingPageRoute->load( + $pageLoaderContext->getResourceIdentifier(), + $pageLoaderContext->getRequest(), + $pageLoaderContext->getContext() + ); + + $pageResult = $this->resultHydrator->hydrate( + $pageLoaderContext, + $landingPageResult->getLandingPage()->getCmsPage() ?? null + ); + + return $pageResult; + } +} diff --git a/src/Pwa/PageLoader/ProductPageLoader.php b/src/Pwa/PageLoader/ProductPageLoader.php index bd808d3..b5a163f 100644 --- a/src/Pwa/PageLoader/ProductPageLoader.php +++ b/src/Pwa/PageLoader/ProductPageLoader.php @@ -4,13 +4,9 @@ use Shopware\Core\Content\Product\Exception\ProductNumberNotFoundException; use Shopware\Core\Content\Product\SalesChannel\Detail\AbstractProductDetailRoute; -use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter; use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductDefinition; -use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity; use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; -use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter; use Shopware\Core\Framework\DataAbstractionLayer\Search\RequestCriteriaBuilder; -use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface; use SwagShopwarePwa\Pwa\PageLoader\Context\PageLoaderContext; use SwagShopwarePwa\Pwa\PageResult\Product\ProductPageResult; use SwagShopwarePwa\Pwa\PageResult\Product\ProductPageResultHydrator; @@ -80,11 +76,6 @@ public function load(PageLoaderContext $pageLoaderContext): ProductPageResult $pageLoaderContext->getContext()->getContext() ); - $criteria->addFilter( - new ProductAvailableFilter($pageLoaderContext->getContext()->getSalesChannel()->getId()), - new EqualsFilter('active', 1) - ); - $result = $this->productRoute->load( $pageLoaderContext->getResourceIdentifier(), $pageLoaderContext->getRequest(), diff --git a/src/Pwa/PageResult/Landing/LandingPageResult.php b/src/Pwa/PageResult/Landing/LandingPageResult.php new file mode 100644 index 0000000..56efbfb --- /dev/null +++ b/src/Pwa/PageResult/Landing/LandingPageResult.php @@ -0,0 +1,11 @@ +setCmsPage($cmsPageEntity); + $pageResult->setResourceType($pageLoaderContext->getResourceType()); + $pageResult->setResourceIdentifier($pageLoaderContext->getResourceIdentifier()); + + return $pageResult; + } +} diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 77c7544..63b739f 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -54,6 +54,12 @@ + + + + + + @@ -69,6 +75,9 @@ + + diff --git a/src/Test/Integration/PageControllerTest.php b/src/Test/Integration/PageControllerTest.php index d7e4078..0a4aff7 100644 --- a/src/Test/Integration/PageControllerTest.php +++ b/src/Test/Integration/PageControllerTest.php @@ -12,7 +12,6 @@ use Shopware\Core\Framework\Test\TestCaseBase\SalesChannelApiTestBehaviour; use Shopware\Core\Framework\Test\TestDataCollection; use Shopware\Core\Framework\Uuid\Uuid; -use Shopware\Core\PlatformRequest; class PageControllerTest extends TestCase { @@ -56,6 +55,11 @@ class PageControllerTest extends TestCase */ private $salesChannelRepository; + /** + * @var EntityRepositoryInterface + */ + private $landingPageRepository; + public function setUp(): void { $this->ids = new TestDataCollection(Context::createDefaultContext()); @@ -83,8 +87,12 @@ public function setUp(): void $this->cmsPageRepository = $this->getContainer()->get('cms_page.repository'); $this->salesChannelDomainRepository = $this->getContainer()->get('sales_channel_domain.repository'); $this->salesChannelRepository = $this->getContainer()->get('sales_channel.repository'); + $this->landingPageRepository = $this->getContainer()->get('landing_page.repository'); } + /** + * @group pwa-page-category + */ public function testResolveCategoryPageRootPath(): void { $this->createCmsPage(); @@ -109,6 +117,9 @@ public function testResolveCategoryPageRootPath(): void static::assertEquals($this->ids->get('categoryId'), $response->resourceIdentifier); } + /** + * @group pwa-page-category + */ public function testResolveCategoryPage(): void { $this->createCmsPage(); @@ -139,6 +150,9 @@ public function testResolveCategoryPage(): void static::assertNotNull($response->resourceIdentifier); } + /** + * @group pwa-page-category + */ public function testResolveCategoryBreadcrumbLink(): void { $this->createCmsPage(); @@ -163,6 +177,9 @@ public function testResolveCategoryBreadcrumbLink(): void static::assertEquals('/Home-Shoes/Children-level-2/', $response['breadcrumb'][$this->ids->get('child2CategoryId')]['path']); } + /** + * @group pwa-page-category + */ public function testResolveCategoryPageTechnicalUrl(): void { $this->createCmsPage(); @@ -188,6 +205,9 @@ public function testResolveCategoryPageTechnicalUrl(): void static::assertNotNull($response->resourceIdentifier); } + /** + * @group pwa-page-category + */ public function testResolveCategoryWithoutCmsPage(): void { $this->createCategories(false); @@ -213,7 +233,132 @@ public function testResolveCategoryWithoutCmsPage(): void static::assertNotNull($response->resourceIdentifier); } - public function testProductPage(): void + /** + * @group pwa-page-category + */ + public function testResolveCategoryPageWithIncludes(): void + { + $this->createCmsPage(); + $this->createCategories(); + $this->createSeoUrls(); + + $content = [ + 'path' => 'Home-Shoes/', + 'includes' => [ + 'pwa_page_result' => ['cmsPage'], + 'section' => ['id'] + ] + ]; + + $this->browser->request( + 'POST', + self::ENDPOINT_PAGE, + $content + ); + + $response = json_decode($this->browser->getResponse()->getContent()); + + static::assertObjectHasAttribute('cmsPage', $response); + static::assertNotNull($response->cmsPage); + static::assertObjectHasAttribute('sections', $response->cmsPage); + static::assertObjectNotHasAttribute('blocks', $response->cmsPage); + static::assertObjectNotHasAttribute('breadcrumb', $response); + } + + /** + * @group pwa-page-landing + */ + public function testResolveLandingPage(): void + { + $this->createCmsPage(); + $this->createLandingPage(true); + $this->createSeoUrls(); + + $content = [ + 'path' => 'my-landing-page/exists' + ]; + + $this->browser->request( + 'POST', + self::ENDPOINT_PAGE, + $content + ); + + $response = json_decode($this->browser->getResponse()->getContent()); + + + static::assertObjectHasAttribute('cmsPage', $response); + + static::assertEquals('shopware AG', $response->cmsPage->name); + static::assertEquals('frontend.landing.page', $response->resourceType); + static::assertObjectHasAttribute('resourceIdentifier', $response); + static::assertNotNull($response->resourceIdentifier); + } + + /** + * @group pwa-page-landing + */ + public function testResolveLandingPageTechnicalUrl(): void + { + $this->createCmsPage(); + $this->createLandingPage(true); + $this->createSeoUrls(); + + $content = [ + 'path' => 'landingPage/' . $this->ids->get('landingPageId') + ]; + + $this->browser->request( + 'POST', + self::ENDPOINT_PAGE, + $content + ); + + $response = json_decode($this->browser->getResponse()->getContent()); + + + static::assertObjectHasAttribute('cmsPage', $response); + + static::assertStringContainsString('my-landing-page', $response->canonicalPathInfo); + static::assertEquals('frontend.landing.page', $response->resourceType); + static::assertObjectHasAttribute('resourceIdentifier', $response); + static::assertNotNull($response->resourceIdentifier); + } + + /** + * @group pwa-page-landing + */ + public function testResolveLandingPageWihoutCmsPage(): void + { + $this->createLandingPage(false); + $this->createSeoUrls(); + + $content = [ + 'path' => 'my-landing-page/exists' + ]; + + $this->browser->request( + 'POST', + self::ENDPOINT_PAGE, + $content + ); + + $response = json_decode($this->browser->getResponse()->getContent()); + + + static::assertObjectHasAttribute('cmsPage', $response); + + static::assertNull($response->cmsPage); + + static::assertEquals('frontend.landing.page', $response->resourceType); + static::assertObjectHasAttribute('resourceIdentifier', $response); + static::assertNotNull($response->resourceIdentifier); + } + + /** + * @group pwa-page-product + */ + public function testResolveProductPage(): void { $this->createCategories(false); $this->createProduct(); @@ -239,7 +384,10 @@ public function testProductPage(): void static::assertNotNull($response->resourceIdentifier); } - public function testProductPageWithAssociation(): void + /** + * @group pwa-page-product + */ + public function testResolveProductPageWithAssociation(): void { $this->createCategories(false); $this->createProduct(); @@ -268,7 +416,10 @@ public function testProductPageWithAssociation(): void static::assertNotNull($response['product']['categories']); } - public function testProductPageTechnicalUrl(): void + /** + * @group pwa-page-product + */ + public function testResolveProductPageTechnicalUrl(): void { $this->createCategories(false); $this->createProduct(); @@ -293,7 +444,10 @@ public function testProductPageTechnicalUrl(): void static::assertNotNull($response->resourceIdentifier); } - public function testProductPageForInactive(): void + /** + * @group pwa-page-product + */ + public function testResolveProductPageForInactive(): void { $this->createCategories(false); $this->createProduct(); @@ -320,15 +474,21 @@ public function testProductPageForInactive(): void } - public function testProductHasBreadcrumbsLinks(): void + /** + * @group pwa-page-product + */ + public function testResolveProductPageWithCmsPage(): void { - $this->createCategories(false); - $this->createProduct(); - $this->createSalesChannelDomain(); + $this->createCmsPage(); + $this->createCategories(); + $this->createProduct(true); $this->createSeoUrls(); $content = [ - 'path' => '/foo-bar/prod-has-breadcrumb' + 'path' => '/detail/' . $this->ids->get('productActiveId'), + 'includes' => [ + 'pwa_page_result' => ['cmsPage'] + ] ]; $this->browser->request( @@ -337,16 +497,16 @@ public function testProductHasBreadcrumbsLinks(): void $content ); - $response = json_decode($this->browser->getResponse()->getContent(), true); - - static::assertArrayHasKey('breadcrumb', $response); + $response = json_decode($this->browser->getResponse()->getContent()); - static::assertEquals('/Home-Shoes/Children-canonical/', $response['breadcrumb'][$this->ids->get('childCategoryId')]['path']); - static::assertEquals('/Home-Shoes/Children-level-2/', $response['breadcrumb'][$this->ids->get('child2CategoryId')]['path']); - static::assertEquals('/navigation/' . $this->ids->get('child3CategoryId'), $response['breadcrumb'][$this->ids->get('child3CategoryId')]['path']); + static::assertObjectHasAttribute('cmsPage', $response); + static::assertNotNull($response->cmsPage); } - public function testProductHasNoBreadcrumbsLinks(): void + /** + * @group pwa-page-product + */ + public function testResolveProductHasBreadcrumbsLinks(): void { $this->createCategories(false); $this->createProduct(); @@ -354,7 +514,7 @@ public function testProductHasNoBreadcrumbsLinks(): void $this->createSeoUrls(); $content = [ - 'path' => '/detail/' . $this->ids->get('productActiveId') + 'path' => '/foo-bar/prod-has-breadcrumb' ]; $this->browser->request( @@ -366,21 +526,24 @@ public function testProductHasNoBreadcrumbsLinks(): void $response = json_decode($this->browser->getResponse()->getContent(), true); static::assertArrayHasKey('breadcrumb', $response); - static::assertNull($response['breadcrumb']); + + static::assertEquals('/Home-Shoes/Children-canonical/', $response['breadcrumb'][$this->ids->get('childCategoryId')]['path']); + static::assertEquals('/Home-Shoes/Children-level-2/', $response['breadcrumb'][$this->ids->get('child2CategoryId')]['path']); + static::assertEquals('/navigation/' . $this->ids->get('child3CategoryId'), $response['breadcrumb'][$this->ids->get('child3CategoryId')]['path']); } - public function testResolveCategoryPageWithIncludes(): void + /** + * @group pwa-page-product + */ + public function testResolveProductHasNoBreadcrumbsLinks(): void { - $this->createCmsPage(); - $this->createCategories(); + $this->createCategories(false); + $this->createProduct(); + $this->createSalesChannelDomain(); $this->createSeoUrls(); $content = [ - 'path' => 'Home-Shoes/', - 'includes' => [ - 'pwa_page_result' => ['cmsPage'], - 'section' => ['id'] - ] + 'path' => '/detail/' . $this->ids->get('productActiveId') ]; $this->browser->request( @@ -389,15 +552,15 @@ public function testResolveCategoryPageWithIncludes(): void $content ); - $response = json_decode($this->browser->getResponse()->getContent()); + $response = json_decode($this->browser->getResponse()->getContent(), true); - static::assertObjectHasAttribute('cmsPage', $response); - static::assertNotNull($response->cmsPage); - static::assertObjectHasAttribute('sections', $response->cmsPage); - static::assertObjectNotHasAttribute('blocks', $response->cmsPage); - static::assertObjectNotHasAttribute('breadcrumb', $response); + static::assertArrayHasKey('breadcrumb', $response); + static::assertNull($response['breadcrumb']); } + /** + * @group pwa-page-routing + */ public function testResolveCanonicalUrl(): void { $this->createCmsPage(); @@ -423,30 +586,20 @@ public function testResolveCanonicalUrl(): void static::assertEquals('/Home-Shoes/canonical/', $response->canonicalPathInfo); } - public function testResolveProductPageWithCmsPage(): void + /** + * @group pwa-page-routing + */ + public function testResolveInvalidUrl(): void { - $this->createCmsPage(); - $this->createCategories(); - $this->createProduct(true); - $this->createSeoUrls(); - - $content = [ - 'path' => '/detail/' . $this->ids->get('productActiveId'), - 'includes' => [ - 'pwa_page_result' => ['cmsPage'] - ] - ]; - $this->browser->request( 'POST', - self::ENDPOINT_PAGE, - $content + self::ENDPOINT_PAGE ); $response = json_decode($this->browser->getResponse()->getContent()); - static::assertObjectHasAttribute('cmsPage', $response); - static::assertNotNull($response->cmsPage); + static::assertEquals(\Symfony\Component\HttpFoundation\Response::HTTP_NOT_FOUND, $this->browser->getResponse()->getStatusCode()); + static::assertObjectHasAttribute('errors', $response); } private function createSalesChannelDomain() @@ -617,6 +770,16 @@ private function createSeoUrls() 'isValid' => true, 'isCanonical' => false, ], + [ + 'salesChannelId' => $this->ids->get('salesChannelId'), + 'languageId' => Defaults::LANGUAGE_SYSTEM, + 'routeName' => 'frontend.landing.page', + 'pathInfo' => '/landingPage/' . $this->ids->get('landingPageId'), + 'seoPathInfo' => 'my-landing-page/exists', + 'foreignKey' => $this->ids->get('landingPageId'), + 'isValid' => true, + 'isCanonical' => false, + ], ], Context::createDefaultContext()); } @@ -660,6 +823,22 @@ private function createCategories(bool $withCmsPage = true) ], Context::createDefaultContext()); } + private function createLandingPage(bool $withCmsPage = true) { + $this->landingPageRepository->create([ + [ + 'id' => $this->ids->get('landingPageId'), + 'salesChannels' => [ + [ + 'id' => $this->ids->get('salesChannelId') + ] + ], + 'name' => 'My test landing page', + 'cmsPageId' => $withCmsPage ? $this->ids->get('cmsPageId') : null, + 'url' => 'my-landing-page/exists' + ] + ], Context::createDefaultContext()); + } + private function createCmsPage() { $landingPage = [