-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from Nosto/ADS-4194_implement_navigation
ADS-4194 Implement navigation
- Loading branch information
Showing
18 changed files
with
493 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
src/Decorator/Core/Content/Product/SalesChannel/Listing/ProductListingRoute.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Nosto\NostoIntegration\Decorator\Core\Content\Product\SalesChannel\Listing; | ||
|
||
use Nosto\NostoIntegration\Model\ConfigProvider; | ||
use Nosto\NostoIntegration\Traits\SearchResultHelper; | ||
use Nosto\NostoIntegration\Utils\SearchHelper; | ||
use Shopware\Core\Content\Category\CategoryDefinition; | ||
use Shopware\Core\Content\Category\CategoryEntity; | ||
use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition; | ||
use Shopware\Core\Content\Product\SalesChannel\Listing\AbstractProductListingRoute; | ||
use Shopware\Core\Content\Product\SalesChannel\Listing\Processor\CompositeListingProcessor; | ||
use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingResult; | ||
use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingRouteResponse; | ||
use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter; | ||
use Shopware\Core\Content\ProductStream\Service\ProductStreamBuilderInterface; | ||
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; | ||
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; | ||
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult; | ||
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter; | ||
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository; | ||
use Shopware\Core\System\SalesChannel\SalesChannelContext; | ||
use Symfony\Component\HttpFoundation\Request; | ||
|
||
class ProductListingRoute extends AbstractProductListingRoute | ||
{ | ||
use SearchResultHelper; | ||
|
||
public function __construct( | ||
private readonly AbstractProductListingRoute $decorated, | ||
private readonly SalesChannelRepository $salesChannelProductRepository, | ||
private readonly EntityRepository $categoryRepository, | ||
private readonly ProductStreamBuilderInterface $productStreamBuilder, | ||
private readonly CompositeListingProcessor $listingProcessor, | ||
private readonly ConfigProvider $configProvider, | ||
) { | ||
} | ||
|
||
public function getDecorated(): AbstractProductListingRoute | ||
{ | ||
return $this->decorated; | ||
} | ||
|
||
public function load( | ||
string $categoryId, | ||
Request $request, | ||
SalesChannelContext $context, | ||
Criteria $criteria = null | ||
): ProductListingRouteResponse { | ||
$shouldHandleRequest = SearchHelper::shouldHandleRequest( | ||
$context->getContext(), | ||
$this->configProvider, | ||
true | ||
); | ||
|
||
$isDefaultCategory = $categoryId === $context->getSalesChannel()->getNavigationCategoryId(); | ||
if (!$shouldHandleRequest || $isDefaultCategory || !$this->isRouteSupported($request)) { | ||
SearchHelper::disableNostoWhenEnabled($context); | ||
|
||
return $this->decorated->load($categoryId, $request, $context, $criteria); | ||
} | ||
|
||
$criteria->addFilter( | ||
new ProductAvailableFilter( | ||
$context->getSalesChannel()->getId(), | ||
ProductVisibilityDefinition::VISIBILITY_ALL | ||
) | ||
); | ||
|
||
/** @var CategoryEntity $category */ | ||
$category = $this->categoryRepository->search( | ||
new Criteria([$categoryId]), | ||
$context->getContext() | ||
)->first(); | ||
|
||
$streamId = $this->extendCriteria($context, $criteria, $category); | ||
|
||
$this->listingProcessor->prepare($request, $criteria, $context); | ||
|
||
$result = $this->fetchProductsById($criteria, $context); | ||
$productListing = ProductListingResult::createFrom($result); | ||
$productListing->addCurrentFilter('navigationId', $categoryId); | ||
$productListing->setStreamId($streamId); | ||
|
||
$this->listingProcessor->process($request, $productListing, $context); | ||
|
||
return new ProductListingRouteResponse($productListing); | ||
} | ||
|
||
private function isRouteSupported(Request $request): bool | ||
{ | ||
// Nosto should never trigger on home page, even if there are categories that would allow it. | ||
if ($this->isHomePage($request)) { | ||
return false; | ||
} | ||
|
||
// In case request came from the home page, Nosto should not trigger on those. | ||
if ($this->isRequestFromHomePage($request)) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private function isHomePage(Request $request): bool | ||
{ | ||
return $request->getPathInfo() === '/'; | ||
} | ||
|
||
private function isRequestFromHomePage(Request $request): bool | ||
{ | ||
if (!$request->isXmlHttpRequest()) { | ||
return false; | ||
} | ||
|
||
$referer = $request->headers->get('referer'); | ||
if (!$referer || !is_string($referer)) { | ||
return false; | ||
} | ||
|
||
$refererPath = parse_url($request->headers->get('referer'), PHP_URL_PATH); | ||
$path = ltrim($refererPath, $request->getBasePath()); | ||
|
||
return $path === '' || $path === '/'; | ||
} | ||
|
||
private function extendCriteria( | ||
SalesChannelContext $salesChannelContext, | ||
Criteria $criteria, | ||
CategoryEntity $category | ||
): ?string { | ||
$supportsProductStreams = defined( | ||
'\Shopware\Core\Content\Category\CategoryDefinition::PRODUCT_ASSIGNMENT_TYPE_PRODUCT_STREAM' | ||
); | ||
$isProductStream = $supportsProductStreams && | ||
$category->getProductAssignmentType() === CategoryDefinition::PRODUCT_ASSIGNMENT_TYPE_PRODUCT_STREAM; | ||
if ($isProductStream && $category->getProductStreamId() !== null) { | ||
$filters = $this->productStreamBuilder->buildFilters( | ||
$category->getProductStreamId(), | ||
$salesChannelContext->getContext() | ||
); | ||
|
||
$criteria->addFilter(...$filters); | ||
|
||
return $category->getProductStreamId(); | ||
} | ||
|
||
$criteria->addFilter( | ||
new EqualsFilter('product.categoriesRo.id', $category->getId()) | ||
); | ||
|
||
return null; | ||
} | ||
|
||
private function fetchProductsById( | ||
Criteria $criteria, | ||
SalesChannelContext $salesChannelContext | ||
): EntitySearchResult { | ||
if (empty($criteria->getIds())) { | ||
return $this->createEmptySearchResult($criteria, $salesChannelContext->getContext()); | ||
} | ||
|
||
return $this->fetchProducts($criteria, $salesChannelContext); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Nosto\NostoIntegration\Decorator\Storefront\Controller; | ||
|
||
use Nosto\NostoIntegration\Model\ConfigProvider; | ||
use Nosto\NostoIntegration\Search\Api\SearchService; | ||
use Nosto\NostoIntegration\Search\Request\Handler\FilterHandler; | ||
use Nosto\NostoIntegration\Utils\SearchHelper; | ||
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; | ||
use Shopware\Core\System\SalesChannel\SalesChannelContext; | ||
use Shopware\Storefront\Controller\CmsController as ShopwareCmsController; | ||
use Shopware\Storefront\Controller\StorefrontController; | ||
use Symfony\Component\HttpFoundation\JsonResponse; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\Routing\Annotation\Route; | ||
|
||
#[Route( | ||
defaults: [ | ||
'_routeScope' => ['storefront'], | ||
] | ||
)] | ||
class CmsController extends StorefrontController | ||
{ | ||
public function __construct( | ||
private readonly ShopwareCmsController $decorated, | ||
private readonly FilterHandler $filterHandler, | ||
private readonly SearchService $searchService, | ||
private readonly ConfigProvider $configProvider, | ||
) { | ||
} | ||
|
||
#[Route( | ||
path: '/widgets/cms/{id}', | ||
name: 'frontend.cms.page', | ||
defaults: [ | ||
'id' => null, | ||
'XmlHttpRequest' => true, | ||
'_httpCache' => true, | ||
], | ||
methods: ['GET', 'POST'] | ||
)] | ||
public function page(?string $id, Request $request, SalesChannelContext $salesChannelContext): Response | ||
{ | ||
return $this->decorated->page($id, $request, $salesChannelContext); | ||
} | ||
|
||
#[Route( | ||
path: '/widgets/cms/navigation/{navigationId}', | ||
name: 'frontend.cms.navigation.page', | ||
defaults: [ | ||
'navigationId' => null, | ||
'XmlHttpRequest' => true, | ||
], | ||
methods: ['GET', 'POST'] | ||
)] | ||
public function category(?string $navigationId, Request $request, SalesChannelContext $salesChannelContext): Response | ||
{ | ||
return $this->decorated->category($navigationId, $request, $salesChannelContext); | ||
} | ||
|
||
#[Route( | ||
path: '/widgets/cms/navigation/{navigationId}/filter', | ||
name: 'frontend.cms.navigation.filter', | ||
defaults: [ | ||
'XmlHttpRequest' => true, | ||
'_routeScope' => ['storefront'], | ||
'_httpCache' => true, | ||
], | ||
methods: ['GET', 'POST'] | ||
)] | ||
public function filter(string $navigationId, Request $request, SalesChannelContext $salesChannelContext): Response | ||
{ | ||
if (!SearchHelper::shouldHandleRequest( | ||
$salesChannelContext->getContext(), | ||
$this->configProvider, | ||
true | ||
)) { | ||
return $this->decorated->filter($navigationId, $request, $salesChannelContext); | ||
} | ||
|
||
$criteria = new Criteria(); | ||
$this->searchService->doFilter($request, $criteria, $salesChannelContext); | ||
|
||
if (!$criteria->hasExtension('nostoAvailableFilters')) { | ||
return $this->decorated->filter($navigationId, $request, $salesChannelContext); | ||
} | ||
|
||
return new JsonResponse( | ||
$this->filterHandler->handleAvailableFilters($criteria) | ||
); | ||
} | ||
|
||
#[Route( | ||
path: '/widgets/cms/buybox/{productId}/switch', | ||
name: 'frontend.cms.buybox.switch', | ||
defaults: [ | ||
'productId' => null, | ||
'XmlHttpRequest' => true, | ||
'_routeScope' => ['storefront'], | ||
'_httpCache' => true, | ||
], | ||
methods: ['GET'] | ||
)] | ||
public function switchBuyBoxVariant(string $productId, Request $request, SalesChannelContext $context): Response | ||
{ | ||
return $this->decorated->switchBuyBoxVariant($productId, $request, $context); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.