Skip to content

Commit

Permalink
Merge pull request #2 from Nosto/ADS-4194_implement_navigation
Browse files Browse the repository at this point in the history
ADS-4194 Implement navigation
  • Loading branch information
TobiasGraml11 authored Nov 7, 2023
2 parents ae2d43c + 688e310 commit 3752b8a
Show file tree
Hide file tree
Showing 18 changed files with 493 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Nosto\NostoIntegration\Model\ConfigProvider;
use Nosto\NostoIntegration\Search\Api\SearchService;
use Nosto\NostoIntegration\Utils\SearchHelper;
use Shopware\Core\Content\Product\SalesChannel\Listing\Processor\AbstractListingProcessor;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
Expand All @@ -27,8 +28,16 @@ public function getDecorated(): AbstractListingProcessor

public function prepare(Request $request, Criteria $criteria, SalesChannelContext $context): void
{
if ($this->configProvider->isSearchEnabled()) {
$this->searchService->doSearch($request, $criteria, $context);
if (SearchHelper::shouldHandleRequest(
$context->getContext(),
$this->configProvider,
SearchHelper::isNavigationPage($request)
)) {
if (SearchHelper::isSearchPage($request)) {
$this->searchService->doSearch($request, $criteria, $context);
} elseif (SearchHelper::isNavigationPage($request)) {
$this->searchService->doNavigation($request, $criteria, $context);
}
}
}
}
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ public function load(

$query = $request->query->get('search');
$result = $this->fetchProductsById($criteria, $context, $query);
$result = ProductListingResult::createFrom($result);
$result->addCurrentFilter('search', $query);
$productListing = ProductListingResult::createFrom($result);
$productListing->addCurrentFilter('search', $query);

$this->listingProcessor->process($request, $result, $context);
$this->listingProcessor->process($request, $productListing, $context);

return new ProductSearchRouteResponse($result);
return new ProductSearchRouteResponse($productListing);
}

private function fetchProductsById(
Expand Down
111 changes: 111 additions & 0 deletions src/Decorator/Storefront/Controller/CmsController.php
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);
}
}
16 changes: 16 additions & 0 deletions src/Model/Nosto/Entity/Product/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Shopware\Core\Checkout\Cart\Price\NetPriceCalculator;
use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
use Shopware\Core\Content\Category\CategoryCollection;
use Shopware\Core\Content\Category\CategoryEntity;
use Shopware\Core\Content\Product\Aggregate\ProductMedia\ProductMediaEntity;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
Expand Down Expand Up @@ -110,6 +112,11 @@ public function build(SalesChannelProductEntity $product, SalesChannelContext $c
$nostoProduct->setCategories($nostoCategoryNames);
}

$categoryIds = $this->getCategoryIds($product->getCategoriesRo());
if (!empty($categoryIds)) {
$nostoProduct->setCategoryIds($categoryIds);
}

if ($ratingAvg = $product->getRatingAverage()) {
$nostoProduct->setRatingValue(round($ratingAvg, 1));
$nostoProduct->setReviewCount($this->productHelper->getReviewsCount($product, $context));
Expand Down Expand Up @@ -290,4 +297,13 @@ private function loadTags(Context $context): TagCollection
$criteria = new Criteria();
return $this->tagRepository->search($criteria, $context)->getEntities();
}

private function getCategoryIds(CategoryCollection $categoriesRo): array
{
return array_values(
array_map(function (CategoryEntity $category) {
return $category->getId();
}, $categoriesRo->getElements())
);
}
}
Loading

0 comments on commit 3752b8a

Please sign in to comment.