Skip to content

Commit

Permalink
Add basic implementation with hardcoded products and default filters
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiasGraml11 committed Oct 17, 2023
1 parent 22dd907 commit 9b964df
Show file tree
Hide file tree
Showing 26 changed files with 1,676 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Nosto\NostoIntegration\Core\Content\Product\SalesChannel\Listing;

use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductListingResultEvent;
use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductSearchResultEvent;
use Shopware\Core\Content\Product\Events\ProductSuggestCriteriaEvent;
use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingFeaturesSubscriber as ShopwareProductListingFeaturesSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ProductListingFeaturesSubscriber implements EventSubscriberInterface
{
public function __construct(
protected readonly ShopwareProductListingFeaturesSubscriber $decorated
) {
}

public static function getSubscribedEvents(): array
{
return [
ProductListingCriteriaEvent::class => 'prepare',
ProductSuggestCriteriaEvent::class => 'prepare',
ProductSearchCriteriaEvent::class => [
['handleSearchRequest', 100],
],
ProductListingResultEvent::class => 'process',
ProductSearchResultEvent::class => 'process',
];
}

public function prepare(ProductListingCriteriaEvent $event): void
{
$this->decorated->prepare($event);
}

public function process(ProductListingResultEvent $event): void
{
$this->decorated->process($event);
}

public function handleSearchRequest(ProductSearchCriteriaEvent $event): void
{
$limit = $event->getCriteria()->getLimit();
$this->decorated->prepare($event);

$limitOverride = $limit ?? $event->getCriteria()->getLimit();

$event->getCriteria()->setIds(['2a88d9b59d474c7e869d8071649be43c', '11dc680240b04f469ccba354cbf0b967']);

// $this->findologicSearchService->doSearch($event, $limitOverride);
}

public function __call($method, $args)
{
if (!method_exists($this->decorated, $method)) {
return;
}

return $this->decorated->{$method}(...$args);
}
}
108 changes: 108 additions & 0 deletions src/Core/Content/Product/SalesChannel/Search/ProductSearchRoute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace Nosto\NostoIntegration\Core\Content\Product\SalesChannel\Search;

use Nosto\NostoIntegration\Model\ConfigProvider;
use Nosto\NostoIntegration\Traits\SearchResultHelper;
use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductSearchResultEvent;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingResult;
use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter;
use Shopware\Core\Content\Product\SalesChannel\Search\AbstractProductSearchRoute;
use Shopware\Core\Content\Product\SalesChannel\Search\ProductSearchRouteResponse;
use Shopware\Core\Content\Product\SearchKeyword\ProductSearchBuilderInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\RequestCriteriaBuilder;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class ProductSearchRoute extends AbstractProductSearchRoute
{
use SearchResultHelper;

public function __construct(
private readonly AbstractProductSearchRoute $decorated,
private readonly ProductSearchBuilderInterface $searchBuilder,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly SalesChannelRepository $salesChannelProductRepository,
private readonly ProductDefinition $definition,
private readonly RequestCriteriaBuilder $criteriaBuilder,
private readonly ConfigProvider $configProvider
) {
}

public function getDecorated(): AbstractProductSearchRoute
{
return $this->decorated;
}

public function load(
Request $request,
SalesChannelContext $context,
?Criteria $criteria = null
): ProductSearchRouteResponse {
$this->addElasticSearchContext($context);

$criteria ??= $this->criteriaBuilder->handleRequest(
$request,
new Criteria(),
$this->definition,
$context->getContext()
);

$shouldHandleRequest = $this->configProvider->isSearchEnabled();

$criteria->addFilter(
new ProductAvailableFilter(
$context->getSalesChannel()->getId(),
ProductVisibilityDefinition::VISIBILITY_SEARCH
)
);

$this->searchBuilder->build($request, $criteria, $context);

$this->eventDispatcher->dispatch(
new ProductSearchCriteriaEvent($request, $criteria, $context)
);

if (!$shouldHandleRequest) {
return $this->decorated->load($request, $context, $criteria);
}

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

$this->eventDispatcher->dispatch(
new ProductSearchResultEvent($request, $result, $context)
);

return new ProductSearchRouteResponse($result);
}

protected function doSearch(
Criteria $criteria,
SalesChannelContext $salesChannelContext,
?string $query
): EntitySearchResult {
$this->assignPaginationToCriteria($criteria);
$this->addOptionsGroupAssociation($criteria);

if (empty($criteria->getIds())) {
return $this->createEmptySearchResult($criteria, $salesChannelContext->getContext());
}

return $this->fetchProducts($criteria, $salesChannelContext, $query);
}

public function addElasticSearchContext(SalesChannelContext $context): void
{
$context->getContext()->addState(Criteria::STATE_ELASTICSEARCH_AWARE);
}
}
89 changes: 89 additions & 0 deletions src/Core/Content/Product/SearchKeyword/ProductSearchBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace Nosto\NostoIntegration\Core\Content\Product\SearchKeyword;

use Shopware\Core\Content\Product\SearchKeyword\ProductSearchBuilderInterface;
use Shopware\Core\Content\Product\SearchKeyword\ProductSearchTermInterpreterInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\AndFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Query\ScoreQuery;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Term\SearchPattern;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\Request;

/**
* @see \Shopware\Core\Content\Product\SearchKeyword\ProductSearchBuilder
*/
class ProductSearchBuilder implements ProductSearchBuilderInterface
{
public function __construct(
private readonly ProductSearchTermInterpreterInterface $interpreter,
private readonly ProductSearchBuilderInterface $decorated
) {
}

public function build(Request $request, Criteria $criteria, SalesChannelContext $context): void
{
if ($request->getPathInfo() === '/suggest') {
$this->buildParent($request, $criteria, $context);
return;
}

$this->doBuild($request, $criteria, $context);
}

public function buildParent(Request $request, Criteria $criteria, SalesChannelContext $context): void
{
$this->decorated->build($request, $criteria, $context);
}

public function doBuild(Request $request, Criteria $criteria, SalesChannelContext $context): void
{
$search = $request->get('search');

if (is_array($search)) {
$term = implode(' ', $search);
} else {
$term = (string) $search;
}

$term = trim($term);
$pattern = $this->interpreter->interpret($term, $context->getContext());

foreach ($pattern->getTerms() as $searchTerm) {
$criteria->addQuery(
new ScoreQuery(
new EqualsFilter('product.searchKeywords.keyword', $searchTerm->getTerm()),
$searchTerm->getScore(),
'product.searchKeywords.ranking'
)
);
}
$criteria->addQuery(
new ScoreQuery(
new ContainsFilter('product.searchKeywords.keyword', $pattern->getOriginal()->getTerm()),
$pattern->getOriginal()->getScore(),
'product.searchKeywords.ranking'
)
);

if ($pattern->getBooleanClause() !== SearchPattern::BOOLEAN_CLAUSE_AND) {
$criteria->addFilter(new AndFilter([
new EqualsAnyFilter('product.searchKeywords.keyword', array_values($pattern->getAllTerms())),
new EqualsFilter('product.searchKeywords.languageId', $context->getContext()->getLanguageId()),
]));

return;
}

foreach ($pattern->getTokenTerms() as $terms) {
$criteria->addFilter(new AndFilter([
new EqualsFilter('product.searchKeywords.languageId', $context->getContext()->getLanguageId()),
new EqualsAnyFilter('product.searchKeywords.keyword', $terms),
]));
}
}
}
48 changes: 48 additions & 0 deletions src/Elasticsearch/Product/ProductSearchBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Nosto\NostoIntegration\Elasticsearch\Product;

use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Content\Product\SearchKeyword\ProductSearchBuilderInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Elasticsearch\Framework\ElasticsearchHelper;
use Symfony\Component\HttpFoundation\Request;

/**
* @see \Shopware\Elasticsearch\Product\ProductSearchBuilder
*/
class ProductSearchBuilder implements ProductSearchBuilderInterface
{
public function __construct(
private readonly ProductSearchBuilderInterface $decorated,
private readonly ElasticsearchHelper $helper,
private readonly ProductDefinition $productDefinition
) {
}

public function build(Request $request, Criteria $criteria, SalesChannelContext $context): void
{
if (!$this->helper->allowSearch($this->productDefinition, $context->getContext(), $criteria)) {
$this->decorated->build($request, $criteria, $context);

return;
}

$search = $request->get('search');

if (\is_array($search)) {
$term = implode(' ', $search);
} else {
$term = (string) $search;
}

$term = trim($term);

// reset queries and set term to criteria.
$criteria->resetQueries();

// elasticsearch will interpret this on demand
$criteria->setTerm($term);
}
}
14 changes: 14 additions & 0 deletions src/Model/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ class ConfigProvider

public const ENABLE_PRODUCT_LABELLING_SYNC = 'config.enableLabelling';

public const ENABLE_SEARCH = 'config.enableSearch';

public const ENABLE_NAVIGATION = 'config.enableNavigation';

public function __construct(SystemConfigService $systemConfig)
{
$this->systemConfig = $systemConfig;
Expand Down Expand Up @@ -199,4 +203,14 @@ public function getProductIdentifier($channelId = null): string
$value = $this->systemConfig->get($this->path(self::PRODUCT_IDENTIFIER_FIELD), $channelId);
return is_string($value) ? $value : 'product-id';
}

public function isSearchEnabled($channelId = null): bool
{
return $this->systemConfig->getBool($this->path(self::ENABLE_SEARCH), $channelId);
}

public function isNavigationEnabled($channelId = null): bool
{
return $this->systemConfig->getBool($this->path(self::ENABLE_SEARCH), $channelId);
}
}
Loading

0 comments on commit 9b964df

Please sign in to comment.