diff --git a/.bc-exclude.php b/.bc-exclude.php index 898559db965..93ebe167fbc 100644 --- a/.bc-exclude.php +++ b/.bc-exclude.php @@ -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', diff --git a/changelog/_unreleased/2024-04-17-product-review-loader-core.md b/changelog/_unreleased/2024-04-17-product-review-loader-core.md new file mode 100644 index 00000000000..da360c47bdd --- /dev/null +++ b/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 diff --git a/phpstan-v67-baseline.neon b/phpstan-v67-baseline.neon index 0dab8533727..799dcfe9c03 100644 --- a/phpstan-v67-baseline.neon +++ b/phpstan-v67-baseline.neon @@ -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\\\\Framework\\\\DataAbstractionLayer\\\\Search\\\\EntitySearchResult\\\\|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\\\\Framework\\\\DataAbstractionLayer\\\\Search\\\\EntitySearchResult\\\\|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 diff --git a/src/Core/Content/DependencyInjection/product.xml b/src/Core/Content/DependencyInjection/product.xml index ecc8b2d489c..3a9fbcc2a8a 100644 --- a/src/Core/Content/DependencyInjection/product.xml +++ b/src/Core/Content/DependencyInjection/product.xml @@ -236,7 +236,8 @@ - + + @@ -482,6 +483,12 @@ %shopware.cache.invalidation.product_detail_route% + + + + + + diff --git a/src/Core/Content/Product/Cms/ProductDescriptionReviewsCmsElementResolver.php b/src/Core/Content/Product/Cms/ProductDescriptionReviewsCmsElementResolver.php index db25ea39341..bccff41b8f5 100644 --- a/src/Core/Content/Product/Cms/ProductDescriptionReviewsCmsElementResolver.php +++ b/src/Core/Content/Product/Cms/ProductDescriptionReviewsCmsElementResolver.php @@ -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 @@ -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 $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([]); } } diff --git a/src/Core/Content/Product/SalesChannel/Review/AbstractProductReviewLoader.php b/src/Core/Content/Product/SalesChannel/Review/AbstractProductReviewLoader.php new file mode 100644 index 00000000000..82804f8b572 --- /dev/null +++ b/src/Core/Content/Product/SalesChannel/Review/AbstractProductReviewLoader.php @@ -0,0 +1,15 @@ +reviews; + } + + public function getSalesChannelContext(): SalesChannelContext + { + return $this->salesChannelContext; + } + + public function getContext(): Context + { + return $this->salesChannelContext->getContext(); + } + + public function getRequest(): Request + { + return $this->request; + } +} diff --git a/src/Core/Content/Product/SalesChannel/Review/ProductReviewLoader.php b/src/Core/Content/Product/SalesChannel/Review/ProductReviewLoader.php new file mode 100644 index 00000000000..18c5c35d2dd --- /dev/null +++ b/src/Core/Content/Product/SalesChannel/Review/ProductReviewLoader.php @@ -0,0 +1,183 @@ +createReviewCriteria($request, $context); + $reviews = $this->productReviewRoute + ->load($parentId ?? $productId, $request, $context, $reviewCriteria) + ->getResult(); + + $reviewResult = ProductReviewResult::createFrom($reviews); + $reviewResult->setMatrix($this->getReviewRatingMatrix($reviews)); + $reviewResult->setCustomerReview($this->getCustomerReview($productId, $context)); + $reviewResult->setTotalReviews($reviews->getTotal()); + $reviewResult->setTotalNativeReviews($this->getTotalNativeReviews($reviews)); + $reviewResult->setProductId($productId); + $reviewResult->setParentId($parentId ?? $productId); + + $this->eventDispatcher->dispatch(new ProductReviewsLoadedEvent($reviewResult, $context, $request)); + + return $reviewResult; + } + + /** + * @param EntitySearchResult $reviews + */ + protected function getReviewRatingMatrix(EntitySearchResult $reviews): RatingMatrix + { + $aggregation = $reviews->getAggregations()->get('ratingMatrix'); + + if ($aggregation instanceof TermsResult) { + return new RatingMatrix($aggregation->getBuckets()); + } + + return new RatingMatrix([]); + } + + /** + * @param EntitySearchResult $reviews + */ + protected function getTotalNativeReviews(EntitySearchResult $reviews): int + { + $aggregation = $reviews->getAggregations()->get('languageMatrix'); + + if ($aggregation instanceof TermsResult) { + $buckets = $aggregation->getBuckets(); + + return empty($buckets) ? 0 : $buckets[0]->getCount(); + } + + return $reviews->getTotal(); + } + + protected function createReviewCriteria(Request $request, SalesChannelContext $context): Criteria + { + $limit = (int) $request->get('limit', $this->systemConfigService->getInt('core.listing.reviewsPerPage', $context->getSalesChannelId())); + $page = (int) $request->get('p', 1); + $offset = $limit * ($page - 1); + + $criteria = new Criteria(); + $criteria->setLimit($limit); + $criteria->setOffset($offset); + $criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_EXACT); + + $sorting = new FieldSorting('createdAt', 'DESC'); + if ($request->get('sort', 'createdAt') === '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, $context); + + return $criteria; + } + + protected function getCustomerReview(string $productId, SalesChannelContext $context): ?ProductReviewEntity + { + $customer = $context->getCustomer(); + + if (!$customer) { + return null; + } + + $criteria = new Criteria(); + $criteria->setLimit(1); + $criteria->setOffset(0); + $criteria->addFilter(new EqualsFilter('customerId', $customer->getId())); + + $customerReviews = $this->productReviewRoute + ->load($productId, new Request(), $context, $criteria) + ->getResult()->getEntities(); + + return $customerReviews->first(); + } + + protected function handlePointsAggregation(Request $request, Criteria $criteria, SalesChannelContext $context): void + { + $reviewFilters = []; + $points = $request->get('points', []); + + if (\is_array($points) && \count($points) > 0) { + $pointFilter = []; + foreach ($points as $point) { + $pointFilter[] = new RangeFilter('points', [ + 'gte' => (int) $point - 0.5, + 'lt' => (int) $point + 0.5, + ]); + } + + $criteria->addPostFilter(new MultiFilter(MultiFilter::CONNECTION_OR, $pointFilter)); + } + + $reviewFilters[] = new EqualsFilter('status', true); + if ($context->getCustomer() !== null) { + $reviewFilters[] = new EqualsFilter('customerId', $context->getCustomer()->getId()); + } + + $criteria->addAggregation( + new FilterAggregation( + 'customer-login-filter', + new TermsAggregation('ratingMatrix', 'points'), + [ + new MultiFilter(MultiFilter::CONNECTION_OR, $reviewFilters), + ] + ), + new FilterAggregation( + 'language-filter', + new TermsAggregation('languageMatrix', 'languageId'), + [ + new EqualsFilter('languageId', $context->getContext()->getLanguageId()), + new MultiFilter(MultiFilter::CONNECTION_OR, $reviewFilters), + ] + ) + ); + } +} diff --git a/src/Core/Content/Product/SalesChannel/Review/ProductReviewResult.php b/src/Core/Content/Product/SalesChannel/Review/ProductReviewResult.php index 087f9493d63..791a77f2b9e 100644 --- a/src/Core/Content/Product/SalesChannel/Review/ProductReviewResult.php +++ b/src/Core/Content/Product/SalesChannel/Review/ProductReviewResult.php @@ -38,6 +38,11 @@ class ProductReviewResult extends EntitySearchResult */ protected $totalReviews; + /** + * @var int + */ + protected $totalNativeReviews; + public function getProductId(): string { return $this->productId; @@ -78,6 +83,16 @@ public function setTotalReviews(int $totalReviews): void $this->totalReviews = $totalReviews; } + public function getTotalNativeReviews(): int + { + return $this->totalNativeReviews; + } + + public function setTotalNativeReviews(int $totalNativeReviews): void + { + $this->totalNativeReviews = $totalNativeReviews; + } + public function getParentId(): ?string { return $this->parentId; diff --git a/src/Core/Content/Product/SalesChannel/Review/ProductReviewsWidgetLoadedHook.php b/src/Core/Content/Product/SalesChannel/Review/ProductReviewsWidgetLoadedHook.php new file mode 100644 index 00000000000..e48a7cd4be2 --- /dev/null +++ b/src/Core/Content/Product/SalesChannel/Review/ProductReviewsWidgetLoadedHook.php @@ -0,0 +1,43 @@ +getContext()); + $this->salesChannelContext = $context; + } + + public function getName(): string + { + return self::HOOK_NAME; + } + + public function getReviews(): ProductReviewResult + { + return $this->reviews; + } +} diff --git a/src/Core/DevOps/Resources/generated/script-hooks-reference.md b/src/Core/DevOps/Resources/generated/script-hooks-reference.md index f66c90aa945..9f9a88ee400 100644 --- a/src/Core/DevOps/Resources/generated/script-hooks-reference.md +++ b/src/Core/DevOps/Resources/generated/script-hooks-reference.md @@ -29,6 +29,18 @@ All available Hooks that can be used to load additional data. | **Available Services** | [repository](./data-loading-script-services-reference.md#RepositoryFacade)
[config](./miscellaneous-script-services-reference.md#SystemConfigFacade)
[store](./data-loading-script-services-reference.md#SalesChannelRepositoryFacade)
| | **Stoppable** | `false` | +#### product-reviews-widget-loaded + +| | | +|:-----------------------|:----------------------------------------| +| **Name** | product-reviews-widget-loaded | +| **Since** | 6.6.2.0 | +| **Class** | `Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewsWidgetLoadedHook` | +| **Description** | Triggered when the ProductReviewsWidget is loaded
| +| **Available Data** | reviews: [`Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewResult`](https://github.com/shopware/platform/blob/trunk/src/Core/Content/Product/SalesChannel/Review/ProductReviewResult.php)
context: [`Shopware\Core\Framework\Context`](https://github.com/shopware/platform/blob/trunk/src/Core/Framework/Context.php)
salesChannelContext: [`Shopware\Core\System\SalesChannel\SalesChannelContext`](https://github.com/shopware/platform/blob/trunk/src/Core/System/SalesChannel/SalesChannelContext.php)
| +| **Available Services** | [repository](./data-loading-script-services-reference.md#RepositoryFacade)
[config](./miscellaneous-script-services-reference.md#SystemConfigFacade)
[store](./data-loading-script-services-reference.md#SalesChannelRepositoryFacade)
[request](./miscellaneous-script-services-reference.md#RequestFacade)
| +| **Stoppable** | `false` | + #### customer-group-registration-page-loaded | | | @@ -343,6 +355,8 @@ All available Hooks that can be used to load additional data. #### product-reviews-loaded +**Deprecated:** Class "Shopware\Storefront\Page\Product\Review\ProductReviewsWidgetLoadedHook" is deprecated and will be removed in v6.7.0.0. Use "Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewsWidgetLoadedHook" instead. + | | | |:-----------------------|:----------------------------------------| | **Name** | product-reviews-loaded | diff --git a/src/Core/Migration/V6_6/Migration1711461585AddDefaultSettingConfigValueForReviewListingPerPage.php b/src/Core/Migration/V6_6/Migration1711461585AddDefaultSettingConfigValueForReviewListingPerPage.php new file mode 100644 index 00000000000..f84110d87d1 --- /dev/null +++ b/src/Core/Migration/V6_6/Migration1711461585AddDefaultSettingConfigValueForReviewListingPerPage.php @@ -0,0 +1,47 @@ +configPresent($connection)) { + return; + } + + $connection->insert('system_config', [ + 'id' => Uuid::randomBytes(), + 'configuration_key' => self::CONFIG_KEY, + 'configuration_value' => json_encode(['_value' => 10]), + 'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT), + ]); + } + + private function configPresent(Connection $connection): bool + { + return $connection->fetchOne( + 'SELECT `id` FROM `system_config` WHERE `configuration_key` = :config_key LIMIT 1;', + ['config_key' => self::CONFIG_KEY] + ) !== false; + } +} diff --git a/src/Core/System/Resources/config/listing.xml b/src/Core/System/Resources/config/listing.xml index 05c9b16bd16..5dbd346f668 100644 --- a/src/Core/System/Resources/config/listing.xml +++ b/src/Core/System/Resources/config/listing.xml @@ -38,6 +38,16 @@ Aktivieren um Bewertungen anzuzeigen + + reviewsPerPage + 10 + + + The number of reviews displayed on a product page in the storefront. + Die Anzahl der Bewertungen die auf einer Produktseite in der Storefront angezeigt werden. + 1 + + disableEmptyFilterOptions diff --git a/src/Storefront/Controller/CmsController.php b/src/Storefront/Controller/CmsController.php index 447c9bc48e9..6320317b456 100644 --- a/src/Storefront/Controller/CmsController.php +++ b/src/Storefront/Controller/CmsController.php @@ -8,13 +8,13 @@ use Shopware\Core\Content\Product\SalesChannel\Detail\AbstractProductDetailRoute; use Shopware\Core\Content\Product\SalesChannel\FindVariant\AbstractFindProductVariantRoute; use Shopware\Core\Content\Product\SalesChannel\Listing\AbstractProductListingRoute; +use Shopware\Core\Content\Product\SalesChannel\Review\AbstractProductReviewLoader; use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Routing\RoutingException; use Shopware\Core\System\SalesChannel\SalesChannelContext; use Shopware\Storefront\Event\SwitchBuyBoxVariantEvent; use Shopware\Storefront\Page\Cms\CmsPageLoadedHook; -use Shopware\Storefront\Page\Product\Review\ProductReviewLoader; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -37,7 +37,7 @@ public function __construct( private readonly AbstractCategoryRoute $categoryRoute, private readonly AbstractProductListingRoute $listingRoute, private readonly AbstractProductDetailRoute $productRoute, - private readonly ProductReviewLoader $productReviewLoader, + private readonly AbstractProductReviewLoader $productReviewLoader, private readonly AbstractFindProductVariantRoute $findVariantRoute, private readonly EventDispatcherInterface $eventDispatcher ) { @@ -146,8 +146,7 @@ public function switchBuyBoxVariant(string $productId, Request $request, SalesCh $request->request->set('parentId', $product->getParentId()); $request->request->set('productId', $product->getId()); - $reviews = $this->productReviewLoader->load($request, $context); - $reviews->setParentId($product->getParentId() ?? $product->getId()); + $reviews = $this->productReviewLoader->load($request, $context, $product->getId(), $product->getParentId()); $event = new SwitchBuyBoxVariantEvent($elementId, $product, $configurator, $request, $context); $this->eventDispatcher->dispatch($event); diff --git a/src/Storefront/Controller/ProductController.php b/src/Storefront/Controller/ProductController.php index f01686a8d36..dc14c8e62ee 100644 --- a/src/Storefront/Controller/ProductController.php +++ b/src/Storefront/Controller/ProductController.php @@ -6,8 +6,11 @@ use Shopware\Core\Content\Product\Exception\ReviewNotActiveExeption; use Shopware\Core\Content\Product\Exception\VariantNotFoundException; use Shopware\Core\Content\Product\SalesChannel\FindVariant\AbstractFindProductVariantRoute; +use Shopware\Core\Content\Product\SalesChannel\Review\AbstractProductReviewLoader; use Shopware\Core\Content\Product\SalesChannel\Review\AbstractProductReviewSaveRoute; +use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewsWidgetLoadedHook; use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface; +use Shopware\Core\Framework\Feature; use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Validation\DataBag\RequestDataBag; use Shopware\Core\Framework\Validation\Exception\ConstraintViolationException; @@ -20,7 +23,7 @@ use Shopware\Storefront\Page\Product\QuickView\MinimalQuickViewPageLoader; use Shopware\Storefront\Page\Product\QuickView\ProductQuickViewWidgetLoadedHook; use Shopware\Storefront\Page\Product\Review\ProductReviewLoader; -use Shopware\Storefront\Page\Product\Review\ProductReviewsWidgetLoadedHook; +use Shopware\Storefront\Page\Product\Review\ProductReviewsWidgetLoadedHook as StorefrontProductReviewsWidgetLoadedHook; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -43,8 +46,9 @@ public function __construct( private readonly MinimalQuickViewPageLoader $minimalQuickViewPageLoader, private readonly AbstractProductReviewSaveRoute $productReviewSaveRoute, private readonly SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler, - private readonly ProductReviewLoader $productReviewLoader, - private readonly SystemConfigService $systemConfigService + private readonly AbstractProductReviewLoader $productReviewLoader, + private readonly SystemConfigService $systemConfigService, + private readonly ProductReviewLoader $storefrontProductReviewLoader ) { } @@ -147,13 +151,19 @@ public function saveReview(string $productId, RequestDataBag $data, SalesChannel } #[Route(path: '/product/{productId}/reviews', name: 'frontend.product.reviews', defaults: ['XmlHttpRequest' => true], methods: ['GET', 'POST'])] - public function loadReviews(Request $request, SalesChannelContext $context): Response + public function loadReviews(string $productId, Request $request, SalesChannelContext $context): Response { $this->checkReviewsActive($context); - $reviews = $this->productReviewLoader->load($request, $context); + if (Feature::isActive('v6.7.0.0')) { + $reviews = $this->productReviewLoader->load($request, $context, $productId, $request->get('parentId')); - $this->hook(new ProductReviewsWidgetLoadedHook($reviews, $context)); + $this->hook(new ProductReviewsWidgetLoadedHook($reviews, $context)); + } else { + $reviews = $this->storefrontProductReviewLoader->load($request, $context); + + $this->hook(new StorefrontProductReviewsWidgetLoadedHook($reviews, $context)); + } return $this->renderStorefront('storefront/component/review/review.html.twig', [ 'reviews' => $reviews, diff --git a/src/Storefront/DependencyInjection/controller.xml b/src/Storefront/DependencyInjection/controller.xml index 687d906814e..fcccb50c97c 100644 --- a/src/Storefront/DependencyInjection/controller.xml +++ b/src/Storefront/DependencyInjection/controller.xml @@ -151,7 +151,7 @@ - + @@ -248,8 +248,9 @@ - + + @@ -327,6 +328,7 @@ + diff --git a/src/Storefront/Page/Product/Review/ProductReviewLoader.php b/src/Storefront/Page/Product/Review/ProductReviewLoader.php index 81e3ccff017..f292eda48be 100644 --- a/src/Storefront/Page/Product/Review/ProductReviewLoader.php +++ b/src/Storefront/Page/Product/Review/ProductReviewLoader.php @@ -2,10 +2,11 @@ namespace Shopware\Storefront\Page\Product\Review; +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\ProductReviewLoader as CoreProductReviewLoader; use Shopware\Core\Content\Product\SalesChannel\Review\RatingMatrix; -use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException; 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; @@ -16,25 +17,25 @@ use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting; use Shopware\Core\Framework\Feature; use Shopware\Core\Framework\Log\Package; -use Shopware\Core\Framework\Routing\Exception\MissingRequestParameterException; use Shopware\Core\Framework\Routing\RoutingException; use Shopware\Core\System\SalesChannel\SalesChannelContext; +use Shopware\Core\System\SystemConfig\SystemConfigService; use Shopware\Storefront\Framework\Page\StorefrontSearchResult; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; +/** + * @deprecated tag:v6.7.0 - Use \Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewLoader instead + */ #[Package('storefront')] class ProductReviewLoader { - private const LIMIT = 10; - private const DEFAULT_PAGE = 1; - private const FILTER_LANGUAGE = 'filter-language'; - /** * @internal */ public function __construct( - private readonly AbstractProductReviewRoute $route, + private readonly AbstractProductReviewRoute $productReviewRoute, + private readonly SystemConfigService $systemConfigService, private readonly EventDispatcherInterface $eventDispatcher ) { } @@ -44,48 +45,69 @@ public function __construct( * otherwise MissingRequestParameterException is thrown * * @throws RoutingException - * @throws InconsistentCriteriaIdsException */ public function load(Request $request, SalesChannelContext $context): ReviewLoaderResult { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewLoader::class)); + $productId = $request->get('parentId') ?? $request->get('productId'); if (!$productId) { throw RoutingException::missingRequestParameter('productId'); } - $criteria = $this->createCriteria($request, $context); - - $reviews = $this->route + $criteria = $this->createReviewCriteria($request, $context); + $reviews = $this->productReviewRoute ->load($productId, $request, $context, $criteria) ->getResult(); - - if (!Feature::isActive('v6.7.0.0')) { - $reviews = StorefrontSearchResult::createFrom($reviews); - } + $reviews = StorefrontSearchResult::createFrom($reviews); $this->eventDispatcher->dispatch(new ProductReviewsLoadedEvent($reviews, $context, $request)); $reviewResult = ReviewLoaderResult::createFrom($reviews); + $reviewResult->setMatrix($this->getReviewRatingMatrix($reviews)); + $reviewResult->setCustomerReview($this->getCustomerReview($productId, $context)); + $reviewResult->setTotalReviews($reviews->getTotal()); + $reviewResult->setTotalNativeReviews($this->getTotalNativeReviews($reviews)); $reviewResult->setProductId($request->get('productId')); - $reviewResult->setParentId($request->get('parentId')); + $reviewResult->setParentId($request->get('parentId') ?? $request->get('productId')); + + return $reviewResult; + } + /** + * @param StorefrontSearchResult $reviews + */ + private function getReviewRatingMatrix(StorefrontSearchResult $reviews): RatingMatrix + { $aggregation = $reviews->getAggregations()->get('ratingMatrix'); - $matrix = new RatingMatrix([]); if ($aggregation instanceof TermsResult) { - $matrix = new RatingMatrix($aggregation->getBuckets()); + return new RatingMatrix($aggregation->getBuckets()); } - $reviewResult->setMatrix($matrix); - $reviewResult->setCustomerReview($this->getCustomerReview($productId, $context)); - $reviewResult->setTotalReviews($matrix->getTotalReviewCount()); - return $reviewResult; + return new RatingMatrix([]); } - private function createCriteria(Request $request, SalesChannelContext $context): Criteria + /** + * @param StorefrontSearchResult $reviews + */ + private function getTotalNativeReviews(StorefrontSearchResult $reviews): int { - $limit = (int) $request->get('limit', self::LIMIT); - $page = (int) $request->get('p', self::DEFAULT_PAGE); + $aggregation = $reviews->getAggregations()->get('languageMatrix'); + + if ($aggregation instanceof TermsResult) { + $buckets = $aggregation->getBuckets(); + + return empty($buckets) ? 0 : $buckets[0]->getCount(); + } + + return $reviews->getTotal(); + } + + private function createReviewCriteria(Request $request, SalesChannelContext $context): Criteria + { + $limit = (int) $request->get('limit', $this->systemConfigService->getInt('core.listing.reviewsPerPage', $context->getSalesChannelId())); + $page = (int) $request->get('p', 1); $offset = $limit * ($page - 1); $criteria = new Criteria(); @@ -100,7 +122,7 @@ private function createCriteria(Request $request, SalesChannelContext $context): $criteria->addSorting($sorting); - if ($request->get('language') === self::FILTER_LANGUAGE) { + if ($request->get('language') === CoreProductReviewLoader::FILTER_LANGUAGE) { $criteria->addPostFilter( new EqualsFilter('languageId', $context->getContext()->getLanguageId()) ); @@ -111,13 +133,6 @@ private function createCriteria(Request $request, SalesChannelContext $context): return $criteria; } - /** - * get review by productId and customer - * a customer should only create one review per product, so if there are more than one - * review we only take one - * - * @throws InconsistentCriteriaIdsException - */ private function getCustomerReview(string $productId, SalesChannelContext $context): ?ProductReviewEntity { $customer = $context->getCustomer(); @@ -131,7 +146,7 @@ private function getCustomerReview(string $productId, SalesChannelContext $conte $criteria->setOffset(0); $criteria->addFilter(new EqualsFilter('customerId', $customer->getId())); - $customerReviews = $this->route + $customerReviews = $this->productReviewRoute ->load($productId, new Request(), $context, $criteria) ->getResult()->getEntities(); @@ -167,6 +182,14 @@ private function handlePointsAggregation(Request $request, Criteria $criteria, S [ new MultiFilter(MultiFilter::CONNECTION_OR, $reviewFilters), ] + ), + new FilterAggregation( + 'language-filter', + new TermsAggregation('languageMatrix', 'languageId'), + [ + new EqualsFilter('languageId', $context->getContext()->getLanguageId()), + new MultiFilter(MultiFilter::CONNECTION_OR, $reviewFilters), + ] ) ); } diff --git a/src/Storefront/Page/Product/Review/ProductReviewsLoadedEvent.php b/src/Storefront/Page/Product/Review/ProductReviewsLoadedEvent.php index 7d961beaab2..f65f7113d25 100644 --- a/src/Storefront/Page/Product/Review/ProductReviewsLoadedEvent.php +++ b/src/Storefront/Page/Product/Review/ProductReviewsLoadedEvent.php @@ -3,8 +3,8 @@ namespace Shopware\Storefront\Page\Product\Review; use Shopware\Core\Content\Product\Aggregate\ProductReview\ProductReviewCollection; +use Shopware\Core\Content\Product\SalesChannel\Review\Event\ProductReviewsLoadedEvent as CoreProductReviewsLoadedEvent; use Shopware\Core\Framework\Context; -use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult; use Shopware\Core\Framework\Event\NestedEvent; use Shopware\Core\Framework\Event\ShopwareSalesChannelEvent; use Shopware\Core\Framework\Feature; @@ -13,120 +13,68 @@ use Shopware\Storefront\Framework\Page\StorefrontSearchResult; use Symfony\Component\HttpFoundation\Request; -if (Feature::isActive('v6.7.0.0')) { - #[Package('storefront')] - class ProductReviewsLoadedEvent extends NestedEvent implements ShopwareSalesChannelEvent - { - /** - * @var EntitySearchResult - */ - protected EntitySearchResult $searchResult; - - /** - * @var SalesChannelContext - */ - protected $salesChannelContext; - - /** - * @var Request - */ - protected $request; - - /** - * @param EntitySearchResult $searchResult - */ - public function __construct( - EntitySearchResult $searchResult, - SalesChannelContext $salesChannelContext, - Request $request - ) { - $this->searchResult = $searchResult; - $this->salesChannelContext = $salesChannelContext; - $this->request = $request; - } - - /** - * @return EntitySearchResult - */ - public function getSearchResult(): EntitySearchResult - { - return $this->searchResult; - } - - public function getSalesChannelContext(): SalesChannelContext - { - return $this->salesChannelContext; - } - - public function getContext(): Context - { - return $this->salesChannelContext->getContext(); - } - - public function getRequest(): Request - { - return $this->request; - } +/** + * @deprecated tag:v6.7.0 - Use \Shopware\Core\Content\Product\SalesChannel\Review\Event\ProductReviewsLoadedEvent instead + */ +#[Package('storefront')] +class ProductReviewsLoadedEvent extends NestedEvent implements ShopwareSalesChannelEvent +{ + /** + * @var StorefrontSearchResult + */ + protected $searchResult; + + /** + * @var SalesChannelContext + */ + protected $salesChannelContext; + + /** + * @var Request + */ + protected $request; + + /** + * @param StorefrontSearchResult $searchResult + */ + public function __construct( + StorefrontSearchResult $searchResult, + SalesChannelContext $salesChannelContext, + Request $request + ) { + $this->searchResult = $searchResult; + $this->salesChannelContext = $salesChannelContext; + $this->request = $request; } -} else { - #[Package('storefront')] - class ProductReviewsLoadedEvent extends NestedEvent implements ShopwareSalesChannelEvent - { - /** - * @deprecated tag:v6.7.0 - Type will change to EntitySearchResult - * - * @var StorefrontSearchResult - */ - protected $searchResult; - /** - * @var SalesChannelContext - */ - protected $salesChannelContext; + /** + * @return StorefrontSearchResult + */ + public function getSearchResult(): StorefrontSearchResult + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsLoadedEvent::class)); - /** - * @var Request - */ - protected $request; + return $this->searchResult; + } - /** - * @param StorefrontSearchResult $searchResult - */ - public function __construct( - StorefrontSearchResult $searchResult, - SalesChannelContext $salesChannelContext, - Request $request - ) { - $this->searchResult = $searchResult; - $this->salesChannelContext = $salesChannelContext; - $this->request = $request; - } + public function getSalesChannelContext(): SalesChannelContext + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsLoadedEvent::class)); - /** - * @deprecated tag:v6.7.0 - Return type will change to EntitySearchResult - * - * @return StorefrontSearchResult - */ - public function getSearchResult(): StorefrontSearchResult - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', 'Return type will change to EntitySearchResult'); + return $this->salesChannelContext; + } - return $this->searchResult; - } + public function getContext(): Context + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsLoadedEvent::class)); - public function getSalesChannelContext(): SalesChannelContext - { - return $this->salesChannelContext; - } + return $this->salesChannelContext->getContext(); + } - public function getContext(): Context - { - return $this->salesChannelContext->getContext(); - } + public function getRequest(): Request + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsLoadedEvent::class)); - public function getRequest(): Request - { - return $this->request; - } + return $this->request; } } diff --git a/src/Storefront/Page/Product/Review/ProductReviewsWidgetLoadedHook.php b/src/Storefront/Page/Product/Review/ProductReviewsWidgetLoadedHook.php index f9170fad74c..6c79bac244e 100644 --- a/src/Storefront/Page/Product/Review/ProductReviewsWidgetLoadedHook.php +++ b/src/Storefront/Page/Product/Review/ProductReviewsWidgetLoadedHook.php @@ -2,8 +2,11 @@ namespace Shopware\Storefront\Page\Product\Review; +use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewsWidgetLoadedHook as CoreProductReviewsWidgetLoadedHook; +use Shopware\Core\Framework\Feature; use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Script\Execution\Awareness\SalesChannelContextAwareTrait; +use Shopware\Core\Framework\Script\Execution\DeprecatedHook; use Shopware\Core\System\SalesChannel\SalesChannelContext; use Shopware\Storefront\Page\PageLoadedHook; @@ -12,12 +15,13 @@ * * @hook-use-case data_loading * + * @deprecated tag:v6.7.0 - Use \Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewsWidgetLoadedHook instead * @since 6.4.8.0 * * @final */ #[Package('storefront')] -class ProductReviewsWidgetLoadedHook extends PageLoadedHook +class ProductReviewsWidgetLoadedHook extends PageLoadedHook implements DeprecatedHook { use SalesChannelContextAwareTrait; @@ -27,17 +31,37 @@ public function __construct( private readonly ReviewLoaderResult $reviews, SalesChannelContext $context ) { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsWidgetLoadedHook::class)); + parent::__construct($context->getContext()); $this->salesChannelContext = $context; } public function getName(): string { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsWidgetLoadedHook::class)); + return self::HOOK_NAME; } + public function getSalesChannelContext(): SalesChannelContext + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsWidgetLoadedHook::class)); + + return $this->salesChannelContext; + } + public function getReviews(): ReviewLoaderResult { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsWidgetLoadedHook::class)); + return $this->reviews; } + + public static function getDeprecationNotice(): string + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsWidgetLoadedHook::class)); + + return Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', CoreProductReviewsWidgetLoadedHook::class); + } } diff --git a/src/Storefront/Page/Product/Review/ReviewLoaderResult.php b/src/Storefront/Page/Product/Review/ReviewLoaderResult.php index 7f9e9e5a74f..bbf4c3aaee1 100644 --- a/src/Storefront/Page/Product/Review/ReviewLoaderResult.php +++ b/src/Storefront/Page/Product/Review/ReviewLoaderResult.php @@ -4,215 +4,143 @@ use Shopware\Core\Content\Product\Aggregate\ProductReview\ProductReviewCollection; use Shopware\Core\Content\Product\Aggregate\ProductReview\ProductReviewEntity; +use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewResult; use Shopware\Core\Content\Product\SalesChannel\Review\RatingMatrix; -use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult; use Shopware\Core\Framework\Feature; use Shopware\Core\Framework\Log\Package; use Shopware\Storefront\Framework\Page\StorefrontSearchResult; -if (Feature::isActive('v6.7.0.0')) { +/** + * @deprecated tag:v6.7.0 - Use \Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewResult instead + * + * @template-extends StorefrontSearchResult + */ +#[Package('storefront')] +class ReviewLoaderResult extends StorefrontSearchResult +{ /** - * @template-extends EntitySearchResult + * @var string|null */ - #[Package('storefront')] - class ReviewLoaderResult extends EntitySearchResult + protected $parentId; + + /** + * @var string + */ + protected $productId; + + /** + * @var StorefrontSearchResult + */ + protected $reviews; + + protected RatingMatrix $matrix; + + /** + * @var ProductReviewEntity|null + */ + protected $customerReview; + + /** + * @var int + */ + protected $totalReviews; + + /** + * @var int + */ + protected $totalNativeReviews; + + public function getProductId(): string + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + return $this->productId; + } + + public function setProductId(string $productId): void { - /** - * @var string|null - */ - protected $parentId; - - /** - * @var string - */ - protected $productId; - - /** - * @var EntitySearchResult - */ - protected EntitySearchResult $reviews; - - protected RatingMatrix $matrix; - - /** - * @var ProductReviewEntity|null - */ - protected $customerReview; - - /** - * @var int - */ - protected $totalReviews; - - public function getProductId(): string - { - return $this->productId; - } - - public function setProductId(string $productId): void - { - $this->productId = $productId; - } - - /** - * @return EntitySearchResult - */ - public function getReviews(): EntitySearchResult - { - return $this->reviews; - } - - public function getMatrix(): RatingMatrix - { - return $this->matrix; - } - - public function setMatrix(RatingMatrix $matrix): void - { - $this->matrix = $matrix; - } - - public function getCustomerReview(): ?ProductReviewEntity - { - return $this->customerReview; - } - - public function setCustomerReview(?ProductReviewEntity $customerReview): void - { - $this->customerReview = $customerReview; - } - - public function getTotalReviews(): int - { - return $this->totalReviews; - } - - public function setTotalReviews(int $totalReviews): void - { - $this->totalReviews = $totalReviews; - } - - public function getParentId(): ?string - { - return $this->parentId; - } - - public function setParentId(?string $parentId): void - { - $this->parentId = $parentId; - } + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + $this->productId = $productId; } -} else { + /** - * @deprecated tag:v6.7.0 - Will inherit from EntitySearchResult - * - * @template-extends StorefrontSearchResult + * @return StorefrontSearchResult */ - #[Package('storefront')] - class ReviewLoaderResult extends StorefrontSearchResult + public function getReviews(): StorefrontSearchResult + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + return $this->reviews; + } + + public function getMatrix(): RatingMatrix + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + return $this->matrix; + } + + public function setMatrix(RatingMatrix $matrix): void + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + $this->matrix = $matrix; + } + + public function getCustomerReview(): ?ProductReviewEntity + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + return $this->customerReview; + } + + public function setCustomerReview(?ProductReviewEntity $customerReview): void + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + $this->customerReview = $customerReview; + } + + public function getTotalReviews(): int + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + return $this->totalReviews; + } + + public function setTotalReviews(int $totalReviews): void + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + $this->totalReviews = $totalReviews; + } + + public function getTotalNativeReviews(): int + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + return $this->totalNativeReviews; + } + + public function setTotalNativeReviews(int $totalNativeReviews): void + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + $this->totalNativeReviews = $totalNativeReviews; + } + + public function getParentId(): ?string + { + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + return $this->parentId; + } + + public function setParentId(?string $parentId): void { - /** - * @var string|null - */ - protected $parentId; - - /** - * @var string - */ - protected $productId; - - /** - * @var StorefrontSearchResult - */ - protected $reviews; - - protected RatingMatrix $matrix; - - /** - * @var ProductReviewEntity|null - */ - protected $customerReview; - - /** - * @var int - */ - protected $totalReviews; - - public function getProductId(): string - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - - return $this->productId; - } - - public function setProductId(string $productId): void - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - $this->productId = $productId; - } - - /** - * @deprecated tag:v6.7.0 - Return type will change to EntitySearchResult - * - * @return StorefrontSearchResult - */ - public function getReviews(): StorefrontSearchResult - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - - return $this->reviews; - } - - public function getMatrix(): RatingMatrix - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - - return $this->matrix; - } - - public function setMatrix(RatingMatrix $matrix): void - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - $this->matrix = $matrix; - } - - public function getCustomerReview(): ?ProductReviewEntity - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - - return $this->customerReview; - } - - public function setCustomerReview(?ProductReviewEntity $customerReview): void - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - $this->customerReview = $customerReview; - } - - public function getTotalReviews(): int - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - - return $this->totalReviews; - } - - public function setTotalReviews(int $totalReviews): void - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - $this->totalReviews = $totalReviews; - } - - public function getParentId(): ?string - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - - return $this->parentId; - } - - public function setParentId(?string $parentId): void - { - Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', 'will extend EntitySearchResult instead StorefrontSearchResult')); - $this->parentId = $parentId; - } + Feature::triggerDeprecationOrThrow('v6.7.0.0', Feature::deprecatedClassMessage(self::class, 'v6.7.0.0', ProductReviewResult::class)); + + $this->parentId = $parentId; } } diff --git a/src/Storefront/Resources/views/storefront/component/review/review.html.twig b/src/Storefront/Resources/views/storefront/component/review/review.html.twig index 67d38a50dc1..7bdc3d00492 100644 --- a/src/Storefront/Resources/views/storefront/component/review/review.html.twig +++ b/src/Storefront/Resources/views/storefront/component/review/review.html.twig @@ -3,10 +3,12 @@ {% block utilities_offcanvas_content %} {% block component_review_container %} - {# TODO NEXT-16994 - replace items per list config value #} - {% set reviewsPerListPage = 10 %} + {% set reviewsPerListPage = config('core.listing.reviewsPerPage') %} + {% if reviewsPerListPage <= 0 %} + {% set reviewsPerListPage = 10 %} + {% endif %} - {% set currentListPage = reviews.page + 1 %} + {% set currentListPage = reviews.page %} {% set productReviewCount = reviews.totalReviews %} @@ -14,8 +16,8 @@ {% set productAvgRating = reviews.matrix.averageRating|round(2, 'common') %} {% endif %} - {# TODO NEXT-16994 - replace language flag #} - {% set foreignReviewsCount = 150 %} + {% set nativeReviewsCount = reviews.totalNativeReviews ?: reviews.totalReviews %} + {% set foreignReviewsCount = reviews.totalReviews - nativeReviewsCount %} {% set ratingSuccess = element.data.ratingSuccess %} @@ -72,15 +74,15 @@ class="collapse multi-collapse product-detail-review-list{% if ratingSuccess != -1 %} show{% endif %}"> {% block component_review_list %} {% block component_review_list_actions %} -
- {% set formAjaxSubmitOptions = { - replaceSelectors: ['.js-review-container'], - submitOnChange: true - } %} - - {% block component_review_list_action_language %} -
- {% if foreignReviewsCount > 0 %} + {% block component_review_list_action_filters %} +
+ {% set formAjaxSubmitOptions = { + replaceSelectors: ['.js-review-container'], + submitOnChange: true + } %} + + {% block component_review_list_action_language %} +
- {# TODO NEXT-16994 - set checked and disabled state #} + {% if app.request.get('language') %}checked="checked"{% endif %} + {% if foreignReviewsCount == 0 %}disabled="disabled"{% endif %}>
- {% endif %} -
- {% endblock %} - - {% block component_review_list_action_sortby %} - {% if productReviewCount > 0 %} -
- {% set formAjaxSubmitOptions = { - replaceSelectors: [ - '.js-review-info', - '.js-review-teaser', - '.js-review-content' - ], - submitOnChange: true - } %} - - {% block component_review_list_action_sortby_form %} -
- - {% if app.request.get('limit') %} - - {% endif %} - - {% if app.request.get('language') %} - - {% endif %} - - {% if app.request.get('points') %} - {% for points in app.request.get('points') %} - - {% endfor %} - {% endif %} - - {% block component_review_list_action_sortby_label %} - - {% endblock %} - - {% block component_review_list_action_sortby_select %} - - {% endblock %} -
- {% endblock %}
- {% endif %} - {% endblock %} -
+ {% endblock %} + + {% block component_review_list_action_sortby %} + {% if productReviewCount > 0 %} +
+ {% set formAjaxSubmitOptions = { + replaceSelectors: [ + '.js-review-info', + '.js-review-teaser', + '.js-review-content' + ], + submitOnChange: true + } %} + + {% block component_review_list_action_sortby_form %} +
+ + {% if app.request.get('limit') %} + + {% endif %} + + {% if app.request.get('language') %} + + {% endif %} + + {% if app.request.get('points') %} + {% for points in app.request.get('points') %} + + {% endfor %} + {% endif %} + + {% block component_review_list_action_sortby_label %} + + {% endblock %} + + {% block component_review_list_action_sortby_select %} + + {% endblock %} +
+ {% endblock %} +
+ {% endif %} + {% endblock %} +
+ {% endblock %} -
+ {% block component_review_list_counter %} +
- {# TODO NEXT-16994 - calculate reviews in current language in list #} - {% set listReviewsCount = productReviewCount - foreignReviewsCount %} - {# TODO NEXT-16994 - fix if reviews in foreign language are more than in customer language #} - {% if listReviewsCount < 0 %} - {% set listReviewsCount = 0 %} - {% endif %} + {% set listReviewsCount = reviews.total %} -

- {% if (listReviewsCount > 1 and listReviewsCount > reviewsPerListPage) %} - {{ currentListPage }} - {{ reviewsPerListPage }} {{ 'detail.reviewCountBefore'|trans|sw_sanitize }} {{ listReviewsCount }} {{ 'detail.reviewCountAfter'|trans({'%count%': listReviewsCount })|sw_sanitize }} - {% elseif listReviewsCount > 0 %} {# TODO NEXT-16994 - fix detail.reviewCountAfter snippet for listReviewsCount = 0 #} - {{ listReviewsCount }} {{ 'detail.reviewCountAfter'|trans({'%count%': listReviewsCount })|sw_sanitize }} - {% endif %} -

+

+ {% if (listReviewsCount > 1 and listReviewsCount > reviewsPerListPage) %} + {{ currentListPage }} - {{ reviewsPerListPage }} {{ 'detail.reviewCountBefore'|trans|sw_sanitize }} {{ listReviewsCount }} {{ 'detail.reviewCountAfter'|trans({'%count%': listReviewsCount })|sw_sanitize }} + {% elseif listReviewsCount > 0 %} + {{ listReviewsCount }} {{ 'detail.reviewCountAfter'|trans({'%count%': listReviewsCount })|sw_sanitize }} + {% endif %} +

+ {% endblock %} {% endblock %} {% block component_review_list_content %} diff --git a/tests/integration/Core/Content/Product/Cms/Type/ProductDescriptionReviewsTypeDataResolverTest.php b/tests/integration/Core/Content/Product/Cms/Type/ProductDescriptionReviewsTypeDataResolverTest.php index 77ebb2e851c..ec79baf6016 100644 --- a/tests/integration/Core/Content/Product/Cms/Type/ProductDescriptionReviewsTypeDataResolverTest.php +++ b/tests/integration/Core/Content/Product/Cms/Type/ProductDescriptionReviewsTypeDataResolverTest.php @@ -9,11 +9,12 @@ use Shopware\Core\Content\Cms\SalesChannel\Struct\ProductDescriptionReviewsStruct; use Shopware\Core\Content\Product\Aggregate\ProductReview\ProductReviewCollection; use Shopware\Core\Content\Product\Cms\ProductDescriptionReviewsCmsElementResolver; -use Shopware\Core\Content\Product\SalesChannel\Review\AbstractProductReviewRoute; -use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewRouteResponse; +use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewLoader; +use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewResult; use Shopware\Core\Framework\Context; use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult; +use Shopware\Core\Framework\Script\Execution\ScriptExecutor; use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; use Shopware\Core\System\SalesChannel\SalesChannelContext; use Symfony\Component\HttpFoundation\Request; @@ -29,15 +30,18 @@ class ProductDescriptionReviewsTypeDataResolverTest extends TestCase protected function setUp(): void { - $productReviewRouteMock = $this->createMock(AbstractProductReviewRoute::class); - $productReviewRouteMock->method('load')->willReturn( - new ProductReviewRouteResponse( - new EntitySearchResult('product', 0, new ProductReviewCollection(), null, new Criteria(), Context::createDefaultContext()) + $productReviewLoaderMock = $this->createMock(ProductReviewLoader::class); + $productReviewLoaderMock->method('load')->willReturn( + ProductReviewResult::createFrom( + new EntitySearchResult('product_review', 0, new ProductReviewCollection(), null, new Criteria(), Context::createDefaultContext()) ) ); + $scriptExecutorMock = $this->createMock(ScriptExecutor::class); + $this->productDescriptionReviewResolver = new ProductDescriptionReviewsCmsElementResolver( - $productReviewRouteMock + $productReviewLoaderMock, + $scriptExecutorMock ); } diff --git a/tests/unit/Core/Content/Product/SalesChannel/Review/ProductReviewLoaderTest.php b/tests/unit/Core/Content/Product/SalesChannel/Review/ProductReviewLoaderTest.php new file mode 100644 index 00000000000..ad34377c32b --- /dev/null +++ b/tests/unit/Core/Content/Product/SalesChannel/Review/ProductReviewLoaderTest.php @@ -0,0 +1,241 @@ + $productId]); + $salesChannelContext = $this->getSalesChannelContext(false); + + $review = $this->getReviewEntity($reviewId); + + $reviews = new ProductReviewCollection([ + $review, + ]); + + $productReviewLoader = $this->getProductReviewLoader($reviews, $request, $salesChannelContext); + + $result = $productReviewLoader->load($request, $salesChannelContext, $productId); + + static::assertInstanceOf(ProductReviewEntity::class, $result->first()); + static::assertEquals($result->first()->getId(), $reviewId); + static::assertCount(1, $result); + static::assertNull($result->getCustomerReview()); + } + + public function testItLoadsReviewsWithParentId(): void + { + $reviewId = Uuid::randomHex(); + $productId = Uuid::randomHex(); + $request = new Request([], [], ['productId' => $productId, 'parentId' => $productId, 'sort' => 'points', 'language' => 'filter-language']); + $salesChannelContext = $this->getSalesChannelContext(); + + $review = $this->getReviewEntity($reviewId); + + $reviews = new ProductReviewCollection([ + $review, + ]); + + $productReviewLoader = $this->getProductReviewLoader($reviews, $request, $salesChannelContext); + + $result = $productReviewLoader->load($request, $salesChannelContext, $productId, $productId); + + static::assertInstanceOf(ProductReviewEntity::class, $result->first()); + static::assertEquals($reviewId, $result->first()->getId()); + static::assertCount(1, $result); + static::assertEquals([new FieldSorting('points', 'DESC')], $result->getCriteria()->getSorting()); + static::assertNotNull($result->getCustomerReview()); + } + + public function testItLoadsReviewsWithPointsFilter(): void + { + $reviewId = Uuid::randomHex(); + $productId = Uuid::randomHex(); + $request = new Request([], [], ['productId' => $productId, 'points' => ['4', 'gg']]); + $salesChannelContext = $this->getSalesChannelContext(); + + $review = $this->getReviewEntity($reviewId); + + $reviews = new ProductReviewCollection([ + $review, + ]); + + $productReviewLoader = $this->getProductReviewLoader($reviews, $request, $salesChannelContext); + + $result = $productReviewLoader->load($request, $salesChannelContext, $productId); + + static::assertInstanceOf(ProductReviewEntity::class, $result->first()); + static::assertEquals($result->first()->getId(), $reviewId); + static::assertCount(1, $result); + } + + private function getReviewEntity(string $reviewId): ProductReviewEntity + { + $customer = new CustomerEntity(); + $customer->setId(Uuid::randomHex()); + $review = new ProductReviewEntity(); + $review->setId($reviewId); + $review->setUniqueIdentifier($reviewId); + $review->setCustomer($customer); + + return $review; + } + + private function getProductReviewLoader( + ?ProductReviewCollection $reviews, + Request $request, + SalesChannelContext $salesChannelContext + ): ProductReviewLoader { + $productReviewRouteMock = $this->createMock(ProductReviewRoute::class); + + $criteria = $this->createCriteria($request, $salesChannelContext); + + if ($reviews !== null) { + $reviewResult = new EntitySearchResult( + ProductReviewDefinition::ENTITY_NAME, + 1, + $reviews, + new AggregationResultCollection( + [ + 'ratingMatrix' => new TermsResult('ratingMatrix', []), + ] + ), + $criteria, + Context::createDefaultContext() + ); + + $productReviewRouteMock + ->method('load') + ->willReturn( + new ProductReviewRouteResponse($reviewResult) + ); + } + + return new ProductReviewLoader( + $productReviewRouteMock, + $this->createMock(SystemConfigService::class), + $this->createMock(EventDispatcherInterface::class) + ); + } + + private function getSalesChannelContext(bool $setCustomer = true): SalesChannelContext + { + $salesChannelEntity = new SalesChannelEntity(); + $salesChannelEntity->setId('salesChannelId'); + + $customer = null; + + if ($setCustomer) { + $customer = new CustomerEntity(); + $customer->setId(Uuid::randomHex()); + } + + return new SalesChannelContext( + Context::createDefaultContext(), + 'foo', + 'bar', + $salesChannelEntity, + new CurrencyEntity(), + new CustomerGroupEntity(), + new TaxCollection(), + new PaymentMethodEntity(), + new ShippingMethodEntity(), + new ShippingLocation(new CountryEntity(), null, null), + $customer, + new CashRoundingConfig(2, 0.01, true), + new CashRoundingConfig(2, 0.01, true), + [] + ); + } + + private function createCriteria(Request $request, SalesChannelContext $context): Criteria + { + $limit = (int) $request->get('limit', 10); + $page = (int) $request->get('p', 1); + $offset = $limit * ($page - 1); + + $criteria = new Criteria(); + $criteria->setLimit($limit); + $criteria->setOffset($offset); + $criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_EXACT); + + $sorting = new FieldSorting('createdAt', 'DESC'); + if ($request->get('sort', 'createdAt') === 'points') { + $sorting = new FieldSorting('points', 'DESC'); + } + + $criteria->addSorting($sorting); + + if ($request->get('language') === 'filter-language') { + $criteria->addPostFilter( + new EqualsFilter('languageId', $context->getContext()->getLanguageId()) + ); + } + + $reviewFilters[] = new EqualsFilter('status', true); + + if ($context->getCustomer() !== null) { + $reviewFilters[] = new EqualsFilter('customerId', $context->getCustomer()->getId()); + } + + $criteria->addAggregation( + new FilterAggregation( + 'customer-login-filter', + new TermsAggregation('ratingMatrix', 'points'), + [ + new MultiFilter(MultiFilter::CONNECTION_OR, $reviewFilters), + ] + ) + ); + + return $criteria; + } +} diff --git a/tests/unit/Core/Content/Product/SalesChannel/Review/ProductReviewsWidgetLoadedHookTest.php b/tests/unit/Core/Content/Product/SalesChannel/Review/ProductReviewsWidgetLoadedHookTest.php new file mode 100644 index 00000000000..c964d998487 --- /dev/null +++ b/tests/unit/Core/Content/Product/SalesChannel/Review/ProductReviewsWidgetLoadedHookTest.php @@ -0,0 +1,128 @@ +productPageLoaderMock = $this->createMock(ProductPageLoader::class); + $this->findVariantRouteMock = $this->createMock(FindProductVariantRoute::class); + $this->seoUrlPlaceholderHandlerMock = $this->createMock(SeoUrlPlaceholderHandlerInterface::class); + $this->minimalQuickViewPageLoaderMock = $this->createMock(MinimalQuickViewPageLoader::class); + $this->productReviewSaveRouteMock = $this->createMock(AbstractProductReviewSaveRoute::class); + $this->systemConfigServiceMock = $this->createMock(SystemConfigService::class); + $this->productReviewLoaderMock = $this->createMock(ProductReviewLoader::class); + $this->storefrontProductReviewLoaderMock = $this->createMock(StorefrontProductReviewLoader::class); + + $this->controller = new ProductControllerStub( + $this->productPageLoaderMock, + $this->findVariantRouteMock, + $this->minimalQuickViewPageLoaderMock, + $this->productReviewSaveRouteMock, + $this->seoUrlPlaceholderHandlerMock, + $this->productReviewLoaderMock, + $this->systemConfigServiceMock, + $this->storefrontProductReviewLoaderMock, + ); + } + + public function testHookTriggeredWhenProductReviewsWidgetIsLoaded(): void + { + Feature::skipTestIfInActive('v6.7.0.0', $this); + + $ids = new IdsCollection(); + + $this->systemConfigServiceMock->method('get')->with('core.listing.showReview')->willReturn(true); + + $productId = Uuid::randomHex(); + $parentId = Uuid::randomHex(); + + $request = new Request([ + 'test' => 'test', + 'productId' => $productId, + 'parentId' => $parentId, + ]); + + $productReview = new ProductReviewEntity(); + $productReview->setUniqueIdentifier($ids->get('productReview')); + $reviewResult = new ProductReviewResult( + 'review', + 1, + new ProductReviewCollection([$productReview]), + null, + new Criteria(), + Context::createDefaultContext() + ); + $reviewResult->setProductId($productId); + $reviewResult->setParentId($parentId); + + $this->productReviewLoaderMock->method('load')->with( + $request, + $this->createMock(SalesChannelContext::class), + $productId, + $parentId + )->willReturn($reviewResult); + + $this->controller->loadReviews( + $productId, + $request, + $this->createMock(SalesChannelContext::class) + ); + + static::assertInstanceOf(ProductReviewsWidgetLoadedHook::class, $this->controller->calledHook); + + /** @var ProductReviewsWidgetLoadedHook $productReviewsWidgetLoadedHook */ + $productReviewsWidgetLoadedHook = $this->controller->calledHook; + + static::assertEquals($reviewResult, $productReviewsWidgetLoadedHook->getReviews()); + } +} diff --git a/tests/unit/Storefront/Controller/CmsControllerTest.php b/tests/unit/Storefront/Controller/CmsControllerTest.php index 21486f0ff74..862519b1421 100644 --- a/tests/unit/Storefront/Controller/CmsControllerTest.php +++ b/tests/unit/Storefront/Controller/CmsControllerTest.php @@ -17,6 +17,7 @@ use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingResult; use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingRoute; use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingRouteResponse; +use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewLoader; use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity; use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\AggregationResultCollection; use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\CountResult; @@ -27,7 +28,6 @@ use Shopware\Core\Framework\Test\IdsCollection; use Shopware\Core\System\SalesChannel\SalesChannelContext; use Shopware\Storefront\Controller\CmsController; -use Shopware\Storefront\Page\Product\Review\ProductReviewLoader; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/tests/unit/Storefront/Controller/ProductControllerTest.php b/tests/unit/Storefront/Controller/ProductControllerTest.php index 91759a3c2de..ca87e2f0f02 100644 --- a/tests/unit/Storefront/Controller/ProductControllerTest.php +++ b/tests/unit/Storefront/Controller/ProductControllerTest.php @@ -15,6 +15,8 @@ use Shopware\Core\Content\Product\SalesChannel\FindVariant\FindProductVariantRouteResponse; use Shopware\Core\Content\Product\SalesChannel\FindVariant\FoundCombination; use Shopware\Core\Content\Product\SalesChannel\Review\AbstractProductReviewSaveRoute; +use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewLoader; +use Shopware\Core\Content\Product\SalesChannel\Review\ProductReviewResult; use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity; use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface; use Shopware\Core\Framework\Context; @@ -34,7 +36,7 @@ use Shopware\Storefront\Page\Product\QuickView\MinimalQuickViewPage; use Shopware\Storefront\Page\Product\QuickView\MinimalQuickViewPageLoader; use Shopware\Storefront\Page\Product\QuickView\ProductQuickViewWidgetLoadedHook; -use Shopware\Storefront\Page\Product\Review\ProductReviewLoader; +use Shopware\Storefront\Page\Product\Review\ProductReviewLoader as StorefrontProductReviewLoader; use Shopware\Storefront\Page\Product\Review\ReviewLoaderResult; use Shopware\Tests\Unit\Storefront\Controller\Stub\ProductControllerStub; use Symfony\Component\HttpFoundation\Request; @@ -61,6 +63,8 @@ class ProductControllerTest extends TestCase private MockObject&ProductReviewLoader $productReviewLoaderMock; + private MockObject&StorefrontProductReviewLoader $storefrontProductReviewLoaderMock; + private ProductControllerStub $controller; protected function setUp(): void @@ -72,6 +76,7 @@ protected function setUp(): void $this->productReviewSaveRouteMock = $this->createMock(AbstractProductReviewSaveRoute::class); $this->systemConfigServiceMock = $this->createMock(SystemConfigService::class); $this->productReviewLoaderMock = $this->createMock(ProductReviewLoader::class); + $this->storefrontProductReviewLoaderMock = $this->createMock(StorefrontProductReviewLoader::class); $this->controller = new ProductControllerStub( $this->productPageLoaderMock, @@ -80,7 +85,8 @@ protected function setUp(): void $this->productReviewSaveRouteMock, $this->seoUrlPlaceholderHandlerMock, $this->productReviewLoaderMock, - $this->systemConfigServiceMock + $this->systemConfigServiceMock, + $this->storefrontProductReviewLoaderMock, ); } @@ -295,11 +301,18 @@ public function testSaveReviewViolation(): void public function testLoadReview(): void { + Feature::skipTestIfActive('v6.7.0.0', $this); + $ids = new IdsCollection(); $this->systemConfigServiceMock->method('get')->with('core.listing.showReview')->willReturn(true); - $request = new Request(['test' => 'test']); + $productId = Uuid::randomHex(); + + $request = new Request([ + 'test' => 'test', + 'productId' => $productId, + ]); $productReview = new ProductReviewEntity(); $productReview->setUniqueIdentifier($ids->get('productReview')); @@ -311,12 +324,68 @@ public function testLoadReview(): void new Criteria(), Context::createDefaultContext() ); - $this->productReviewLoaderMock->method('load')->with( + $reviewResult->setProductId($productId); + $this->storefrontProductReviewLoaderMock->method('load')->with( $request, $this->createMock(SalesChannelContext::class) )->willReturn($reviewResult); $response = $this->controller->loadReviews( + $productId, + $request, + $this->createMock(SalesChannelContext::class) + ); + + static::assertSame(Response::HTTP_OK, $response->getStatusCode()); + static::assertSame('storefront/component/review/review.html.twig', $this->controller->renderStorefrontView); + static::assertEquals( + [ + 'reviews' => $reviewResult, + 'ratingSuccess' => null, + ], + $this->controller->renderStorefrontParameters + ); + } + + public function testLoadReviewResults(): void + { + Feature::skipTestIfInActive('v6.7.0.0', $this); + + $ids = new IdsCollection(); + + $this->systemConfigServiceMock->method('get')->with('core.listing.showReview')->willReturn(true); + + $productId = Uuid::randomHex(); + $parentId = Uuid::randomHex(); + + $request = new Request([ + 'test' => 'test', + 'productId' => $productId, + 'parentId' => $parentId, + ]); + + $productReview = new ProductReviewEntity(); + $productReview->setUniqueIdentifier($ids->get('productReview')); + $reviewResult = new ProductReviewResult( + 'review', + 1, + new ProductReviewCollection([$productReview]), + null, + new Criteria(), + Context::createDefaultContext() + ); + $reviewResult->setProductId($productId); + $reviewResult->setParentId($parentId); + + $this->productReviewLoaderMock->method('load')->with( + $request, + $this->createMock(SalesChannelContext::class), + $productId, + $parentId + )->willReturn($reviewResult); + + $response = $this->controller->loadReviews( + $productId, $request, $this->createMock(SalesChannelContext::class) ); diff --git a/tests/unit/Storefront/Page/Product/Review/ProductReviewLoaderTest.php b/tests/unit/Storefront/Page/Product/Review/ProductReviewLoaderTest.php index 06e314e6d14..e61675fc276 100644 --- a/tests/unit/Storefront/Page/Product/Review/ProductReviewLoaderTest.php +++ b/tests/unit/Storefront/Page/Product/Review/ProductReviewLoaderTest.php @@ -3,7 +3,6 @@ namespace Shopware\Tests\Unit\Storefront\Page\Product\Review; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\RunClassInSeparateProcess; use PHPUnit\Framework\TestCase; use Shopware\Core\Checkout\Cart\Delivery\Struct\ShippingLocation; use Shopware\Core\Checkout\Customer\Aggregate\CustomerGroup\CustomerGroupEntity; @@ -26,28 +25,30 @@ use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter; use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter; use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting; +use Shopware\Core\Framework\Feature; use Shopware\Core\Framework\Routing\RoutingException; use Shopware\Core\Framework\Uuid\Uuid; use Shopware\Core\System\Country\CountryEntity; use Shopware\Core\System\Currency\CurrencyEntity; use Shopware\Core\System\SalesChannel\SalesChannelContext; use Shopware\Core\System\SalesChannel\SalesChannelEntity; +use Shopware\Core\System\SystemConfig\SystemConfigService; use Shopware\Core\System\Tax\TaxCollection; use Shopware\Storefront\Page\Product\Review\ProductReviewLoader; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; /** - * @deprecated tag:v6.7.0 - Remove the `RunClassInSeparateProcess` attribute. - * It is only need as long as the class `Shopware\Storefront\Page\Product\Review\ReviewLoaderResult` is loaded based on a feature flag. - * Removing the attribute before 6.7 will cause flaky tests, as the class will be loaded with or without deprecations depending on which test is executed first. - * * @internal */ #[CoversClass(ProductReviewLoader::class)] -#[RunClassInSeparateProcess] class ProductReviewLoaderTest extends TestCase { + protected function setUp(): void + { + Feature::skipTestIfActive('v6.7.0.0', $this); + } + public function testExceptionWithoutProductId(): void { $request = new Request([], [], []); @@ -173,6 +174,7 @@ private function getProductReviewLoader( return new ProductReviewLoader( $productReviewRouteMock, + $this->createMock(SystemConfigService::class), $this->createMock(EventDispatcherInterface::class) ); }