Skip to content

Commit

Permalink
Move ProductReviewLoader logic to core
Browse files Browse the repository at this point in the history
  • Loading branch information
akf-bw committed Apr 25, 2024
1 parent 8688475 commit 4545703
Show file tree
Hide file tree
Showing 27 changed files with 1,266 additions and 603 deletions.
7 changes: 7 additions & 0 deletions .bc-exclude.php
Expand Up @@ -32,6 +32,13 @@
'Shopware\\\\Core\\\\Framework\\\\App\\\\Payment\\\\Payload\\\\Struct\\\\SyncPayPayload#__construct()',
'Shopware\\\\Core\\\\Framework\\\\Api\\\\Sync\\\\FkReference#__construct\(\)',

// Moved ProductReviewLoader to core, can be removed after 6.7.0.0 release
'Type of property Shopware\\\\Storefront\\\\Page\\\\Product\\\\Review\\\\ReviewLoaderResult#\\$.+ changed from .+ to having no type',
'The return type of Shopware\\\\Storefront\\\\Page\\\\Product\\\\Review\\\\ReviewLoaderResult#.+ changed from .+ to .+',
'Type of property Shopware\\\\Storefront\\\\Page\\\\Product\\\\Review\\\\ProductReviewsLoadedEvent#\\$.+ changed from .+ to having no type',
'The return type of Shopware\\\\Storefront\\\\Page\\\\Product\\\\Review\\\\ProductReviewsLoadedEvent#.+ changed from .+ to .+',
'The parameter .+ of Shopware\\\\Storefront\\\\Page\\\\Product\\\\Review\\\\ProductReviewsLoadedEvent#.+ changed from .+ to .+',

// Removed boot method from Bundle
'Shopware\\\\Core\\\\Framework\\\\Bundle#boot',

Expand Down
33 changes: 33 additions & 0 deletions changelog/_unreleased/2024-04-17-product-review-loader-core.md
@@ -0,0 +1,33 @@
---
title: Product review loader core
issue: NEXT-00000
author: Benjamin Wittwer
author_email: dev@a-k-f.de
author_github: akf-bw
---
# Core
* Changed `ProductDescriptionReviewsCmsElementResolver` to use the `AbstractProductReviewLoader` and removed the duplicate functions
* Changed `ProductDescriptionReviewsCmsElementResolver` to now execute the Core `ProductReviewsWidgetLoadedHook` hook
* Added `AbstractProductReviewLoader` to allow overwriting product review load logic
* Added `ProductReviewLoader` based on the Storefront `ProductReviewLoader`
* Changed `ProductReviewResult` to include the `totalNativeReviews` field
* Added `ProductReviewsWidgetLoadedHook` based on the Storefront `ProductReviewsWidgetLoadedHook`
* Added `ProductReviewsLoadedEvent` based on the Storefront `ProductReviewsLoadedEvent`
* Added `Migration1711461585AddDefaultSettingConfigValueForReviewListingPerPage` to include the new config option
* Added `core.listing.reviewsPerPage` to config `listing` with default value `10`
* Changed `ProductDescriptionReviewsTypeDataResolverTest` to match Core changes
* Added `ProductReviewLoaderTest` to match core changes
___
# Storefront
* Changed `CmsController` to use `AbstractProductReviewLoader`
* Changed `ProductController` to use `AbstractProductReviewLoader`
* Changed `ProductReviewLoader` to `@deprecated` and copy logic from Core `ProductReviewLoader`
* Changed `ProductReviewsLoadedEvent` to `@deprecated`
* Changed `ProductReviewsWidgetLoadedHook` to `@deprecated`
* Changed `ReviewLoaderResult` to `@deprecated`
* Changed `review.html.twig` template to include the new `core.listing.reviewsPerPage` to config
* Changed `review.html.twig` template to include missing `nativeReviewsCount` and `foreignReviewsCount` variables
* Changed `review.html.twig` by including additional `component_review_list_action_filters` and `component_review_list_counter` blocks
* Changed `CmsControllerTest` to match Storefront changes
* Changed `ProductControllerTest` to match Storefront changes
* Changed `ProductReviewLoaderTest` to match Storefront changes
15 changes: 0 additions & 15 deletions phpstan-v67-baseline.neon
Expand Up @@ -9,18 +9,3 @@ parameters:
message: "#^Parameter \\#1 \\$orders of method Shopware\\\\Storefront\\\\Page\\\\Account\\\\Order\\\\AccountOrderPage\\:\\:setOrders\\(\\) expects Shopware\\\\Core\\\\Framework\\\\DataAbstractionLayer\\\\Search\\\\EntitySearchResult\\<Shopware\\\\Core\\\\Checkout\\\\Order\\\\OrderCollection\\>, Shopware\\\\Core\\\\Framework\\\\DataAbstractionLayer\\\\Search\\\\EntitySearchResult\\<Shopware\\\\Core\\\\Checkout\\\\Order\\\\OrderCollection\\>\\|Shopware\\\\Storefront\\\\Framework\\\\Page\\\\StorefrontSearchResult given\\.$#"
count: 1
path: src/Storefront/Page/Account/Order/AccountOrderPageLoader.php

-
message: "#^Parameter \\#1 \\$searchResult of class Shopware\\\\Storefront\\\\Page\\\\Product\\\\Review\\\\ProductReviewsLoadedEvent constructor expects Shopware\\\\Core\\\\Framework\\\\DataAbstractionLayer\\\\Search\\\\EntitySearchResult\\<Shopware\\\\Core\\\\Content\\\\Product\\\\Aggregate\\\\ProductReview\\\\ProductReviewCollection\\>, Shopware\\\\Core\\\\Framework\\\\DataAbstractionLayer\\\\Search\\\\EntitySearchResult\\<Shopware\\\\Core\\\\Content\\\\Product\\\\Aggregate\\\\ProductReview\\\\ProductReviewCollection\\>\\|Shopware\\\\Storefront\\\\Framework\\\\Page\\\\StorefrontSearchResult given\\.$#"
count: 1
path: src/Storefront/Page/Product/Review/ProductReviewLoader.php

-
message: "#^Multiple class/interface/trait is not allowed in single file$#"
count: 1
path: src/Storefront/Page/Product/Review/ProductReviewsLoadedEvent.php

-
message: "#^Multiple class/interface/trait is not allowed in single file$#"
count: 1
path: src/Storefront/Page/Product/Review/ReviewLoaderResult.php
9 changes: 8 additions & 1 deletion src/Core/Content/DependencyInjection/product.xml
Expand Up @@ -236,7 +236,8 @@
</service>

<service id="Shopware\Core\Content\Product\Cms\ProductDescriptionReviewsCmsElementResolver">
<argument type="service" id="Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewRoute"/>
<argument type="service" id="Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewLoader"/>
<argument type="service" id="Shopware\Core\Framework\Script\Execution\ScriptExecutor"/>
<tag name="shopware.cms.data_resolver"/>
</service>

Expand Down Expand Up @@ -482,6 +483,12 @@
<argument>%shopware.cache.invalidation.product_detail_route%</argument>
</service>

<service id="Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewLoader">
<argument type="service" id="Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewRoute"/>
<argument type="service" id="Shopware\Core\System\SystemConfig\SystemConfigService"/>
<argument type="service" id="event_dispatcher"/>
</service>

<service id="Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewRoute" public="true">
<argument type="service" id="product_review.repository"/>
</service>
Expand Down
Expand Up @@ -7,38 +7,24 @@
use Shopware\Core\Content\Cms\DataResolver\ResolverContext\EntityResolverContext;
use Shopware\Core\Content\Cms\DataResolver\ResolverContext\ResolverContext;
use Shopware\Core\Content\Cms\SalesChannel\Struct\ProductDescriptionReviewsStruct;
use Shopware\Core\Content\Product\Aggregate\ProductReview\ProductReviewCollection;
use Shopware\Core\Content\Product\Aggregate\ProductReview\ProductReviewEntity;
use Shopware\Core\Content\Product\SalesChannel\Review\AbstractProductReviewRoute;
use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewResult;
use Shopware\Core\Content\Product\SalesChannel\Review\RatingMatrix;
use Shopware\Core\Content\Product\SalesChannel\Review\AbstractProductReviewLoader;
use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewsWidgetLoadedHook;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\FilterAggregation;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Bucket\TermsResult;
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\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\Request;
use Shopware\Core\Framework\Script\Execution\ScriptExecutor;

#[Package('inventory')]
class ProductDescriptionReviewsCmsElementResolver extends AbstractProductDetailCmsElementResolver
{
final public const TYPE = 'product-description-reviews';
private const LIMIT = 10;
private const DEFAULT_PAGE = 1;
private const FILTER_LANGUAGE = 'filter-language';

/**
* @internal
*/
public function __construct(private readonly AbstractProductReviewRoute $productReviewRoute)
{
public function __construct(
private readonly AbstractProductReviewLoader $productReviewLoader,
private readonly ScriptExecutor $scriptExecutor
) {
}

public function getType(): string
Expand Down Expand Up @@ -72,113 +58,12 @@ public function enrich(CmsSlotEntity $slot, ResolverContext $resolverContext, El

/** @var SalesChannelProductEntity|null $product */
if ($product !== null) {
$data->setProduct($product);
$data->setReviews($this->loadProductReviews($product, $request, $resolverContext->getSalesChannelContext()));
}
}

private function loadProductReviews(SalesChannelProductEntity $product, Request $request, SalesChannelContext $context): ProductReviewResult
{
$reviewCriteria = $this->createReviewCriteria($request, $context);
$reviews = $this->productReviewRoute
->load($product->getParentId() ?? $product->getId(), $request, $context, $reviewCriteria)
->getResult();

$matrix = $this->getReviewRatingMatrix($reviews);

$reviewResult = ProductReviewResult::createFrom($reviews);
$reviewResult->setMatrix($matrix);
$reviewResult->setProductId($product->getId());
$reviewResult->setCustomerReview($this->getCustomerReview($product->getId(), $context));
$reviewResult->setTotalReviews($matrix->getTotalReviewCount());
$reviewResult->setProductId($product->getId());
$reviewResult->setParentId($product->getParentId() ?? $product->getId());

return $reviewResult;
}

private function createReviewCriteria(Request $request, SalesChannelContext $context): Criteria
{
$limit = (int) $request->get('limit', self::LIMIT);
$page = (int) $request->get('p', self::DEFAULT_PAGE);
$offset = $limit * ($page - 1);

$criteria = new Criteria();
$criteria->setLimit($limit);
$criteria->setOffset($offset);

$sorting = new FieldSorting('createdAt', 'DESC');
if ($request->get('sort', 'points') === 'points') {
$sorting = new FieldSorting('points', 'DESC');
}

$criteria->addSorting($sorting);

if ($request->get('language') === self::FILTER_LANGUAGE) {
$criteria->addPostFilter(
new EqualsFilter('languageId', $context->getContext()->getLanguageId())
);
}

$this->handlePointsAggregation($request, $criteria);

return $criteria;
}

private function handlePointsAggregation(Request $request, Criteria $criteria): void
{
$points = $request->get('points', []);

if (\is_array($points) && \count($points) > 0) {
$pointFilter = [];
foreach ($points as $point) {
$pointFilter[] = new RangeFilter('points', [
'gte' => $point - 0.5,
'lt' => $point + 0.5,
]);
}

$criteria->addPostFilter(new MultiFilter(MultiFilter::CONNECTION_OR, $pointFilter));
}

$criteria->addAggregation(
new FilterAggregation(
'status-filter',
new TermsAggregation('ratingMatrix', 'points'),
[new EqualsFilter('status', 1)]
)
);
}

private function getCustomerReview(string $productId, SalesChannelContext $context): ?ProductReviewEntity
{
$customer = $context->getCustomer();
$reviews = $this->productReviewLoader->load($request, $resolverContext->getSalesChannelContext(), $product->getId(), $product->getParentId());

if (!$customer) {
return null;
}

$criteria = new Criteria();
$criteria->setLimit(1);
$criteria->setOffset(0);
$criteria->addFilter(new EqualsFilter('customerId', $customer->getId()));

return $this->productReviewRoute
->load($productId, new Request(), $context, $criteria)
->getResult()->getEntities()->first();
}

/**
* @param EntitySearchResult<ProductReviewCollection> $reviews
*/
private function getReviewRatingMatrix(EntitySearchResult $reviews): RatingMatrix
{
$aggregation = $reviews->getAggregations()->get('ratingMatrix');
$this->scriptExecutor->execute(new ProductReviewsWidgetLoadedHook($reviews, $resolverContext->getSalesChannelContext()));

if ($aggregation instanceof TermsResult) {
return new RatingMatrix($aggregation->getBuckets());
$data->setProduct($product);
$data->setReviews($reviews);
}

return new RatingMatrix([]);
}
}
@@ -0,0 +1,15 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Content\Product\SalesChannel\Review;

use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\Request;

#[Package('inventory')]
abstract class AbstractProductReviewLoader
{
abstract public function getDecorated(): AbstractProductReviewLoader;

abstract public function load(Request $request, SalesChannelContext $context, string $productId, ?string $parentId = null): ProductReviewResult;
}
@@ -0,0 +1,42 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Content\Product\SalesChannel\Review\Event;

use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewResult;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Event\NestedEvent;
use Shopware\Core\Framework\Event\ShopwareSalesChannelEvent;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\Request;

#[Package('content')]
final class ProductReviewsLoadedEvent extends NestedEvent implements ShopwareSalesChannelEvent
{
public function __construct(
protected ProductReviewResult $reviews,
protected SalesChannelContext $salesChannelContext,
protected Request $request
) {
}

public function getReviews(): ProductReviewResult
{
return $this->reviews;
}

public function getSalesChannelContext(): SalesChannelContext
{
return $this->salesChannelContext;
}

public function getContext(): Context
{
return $this->salesChannelContext->getContext();
}

public function getRequest(): Request
{
return $this->request;
}
}

0 comments on commit 4545703

Please sign in to comment.