diff --git a/Comment.php b/Comment.php
index 1293b52..d4e92dc 100755
--- a/Comment.php
+++ b/Comment.php
@@ -28,6 +28,7 @@
use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator;
use Thelia\Core\Install\Database;
use Thelia\Core\Translation\Translator;
+use Thelia\Domain\Catalog\Product\ProductRatingSourceInterface;
use Thelia\Model\ConfigQuery;
use Thelia\Model\Lang;
use Thelia\Model\LangQuery;
@@ -402,7 +403,13 @@ public static function configureServices(ServicesConfigurator $servicesConfigura
$servicesConfigurator->load(self::getModuleCode().'\\', __DIR__)
// Tests/ is part of the tree load() walks: a test double implementing an
// autoconfigured interface would otherwise be registered as a real service.
- ->exclude([__DIR__.'/I18n/*', __DIR__.'/Tests/*'])
+ ->exclude([
+ __DIR__.'/I18n/*',
+ __DIR__.'/Tests/*',
+ // The rating facet arrived with Thelia 3.3: on an older core its interface
+ // does not exist, and the class implementing it could not be loaded.
+ ...(interface_exists(ProductRatingSourceInterface::class) ? [] : [__DIR__.'/Front/RatingFacetSource.php']),
+ ])
->autowire(true)
->autoconfigure(true);
diff --git a/Config/module.xml b/Config/module.xml
index bf79ac7..7d5af5c 100755
--- a/Config/module.xml
+++ b/Config/module.xml
@@ -13,7 +13,7 @@
en_US
fr_FR
- 3.2.1
+ 3.3.0
Julien Chanséaume
diff --git a/Front/RatingFacetSource.php b/Front/RatingFacetSource.php
new file mode 100644
index 0000000..eada72f
--- /dev/null
+++ b/Front/RatingFacetSource.php
@@ -0,0 +1,59 @@
+ratingScale ?? RatingType::scaleFrom(ConfigQuery::read('comment_max_rating'));
+
+ return $this->ratings->elementIdsRatedAtLeast(
+ self::PRODUCT_REF,
+ $minimumRating * $scale / self::FACET_SCALE,
+ $amongProductIds,
+ );
+ }
+}
diff --git a/Repository/RatingMetaRepository.php b/Repository/RatingMetaRepository.php
index 67ff989..380d50f 100644
--- a/Repository/RatingMetaRepository.php
+++ b/Repository/RatingMetaRepository.php
@@ -32,4 +32,26 @@ public function clear(string $ref, int $refId): void
->filterByElementId($refId)
->delete();
}
+
+ /**
+ * Compares the stored average with the cast RatingMetaMemo::averageExpression() orders on,
+ * so that the rating facet and the best-rated order read the same figure.
+ */
+ public function elementIdsRatedAtLeast(string $ref, float $minimumAverage, ?array $amongIds = null): array
+ {
+ $query = MetaDataQuery::create()
+ ->filterByMetaKey(Comment::META_KEY_RATING)
+ ->filterByElementKey($ref)
+ ->where('CAST(MetaData.Value AS DECIMAL(10, 2)) >= ?', (string) $minimumAverage, \PDO::PARAM_STR);
+
+ if ($amongIds !== null) {
+ if ($amongIds === []) {
+ return [];
+ }
+
+ $query->filterByElementId($amongIds, Criteria::IN);
+ }
+
+ return array_map('intval', $query->select(['ElementId'])->find()->getData());
+ }
}
diff --git a/Repository/RatingMetaStorageInterface.php b/Repository/RatingMetaStorageInterface.php
index ff5c3e4..ce6dd81 100644
--- a/Repository/RatingMetaStorageInterface.php
+++ b/Repository/RatingMetaStorageInterface.php
@@ -28,4 +28,14 @@ public function store(string $ref, int $refId, float $average, int $count): void
* keeps showing an average computed from comments that are gone.
*/
public function clear(string $ref, int $refId): void;
+
+ /**
+ * The elements of a kind whose stored average reaches $minimumAverage, on the scale the
+ * module rates on.
+ *
+ * @param list|null $amongIds the elements to look at; null for every element of the kind
+ *
+ * @return list
+ */
+ public function elementIdsRatedAtLeast(string $ref, float $minimumAverage, ?array $amongIds = null): array;
}
diff --git a/Tests/Double/InMemoryRatingMetaStorage.php b/Tests/Double/InMemoryRatingMetaStorage.php
index 107e0a9..8fa602c 100644
--- a/Tests/Double/InMemoryRatingMetaStorage.php
+++ b/Tests/Double/InMemoryRatingMetaStorage.php
@@ -49,4 +49,26 @@ public function read(string $ref, int $refId): array
{
return $this->values[$ref.'|'.$refId] ?? ['average' => null, 'count' => 0];
}
+
+ public function elementIdsRatedAtLeast(string $ref, float $minimumAverage, ?array $amongIds = null): array
+ {
+ $ids = [];
+
+ foreach ($this->values as $key => $value) {
+ [$storedRef, $refId] = explode('|', $key);
+ $refId = (int) $refId;
+
+ if ($storedRef !== $ref || $value['average'] === null || $value['average'] < $minimumAverage) {
+ continue;
+ }
+
+ if ($amongIds !== null && !\in_array($refId, $amongIds, true)) {
+ continue;
+ }
+
+ $ids[] = $refId;
+ }
+
+ return $ids;
+ }
}
diff --git a/Tests/Unit/Front/RatingFacetSourceTest.php b/Tests/Unit/Front/RatingFacetSourceTest.php
new file mode 100644
index 0000000..0463fe6
--- /dev/null
+++ b/Tests/Unit/Front/RatingFacetSourceTest.php
@@ -0,0 +1,76 @@
+ratings = new InMemoryRatingMetaStorage();
+ }
+
+ public function testAThresholdKeepsTheProductsWhoseStoredAverageReachesIt(): void
+ {
+ $this->rate([1 => 4.6, 2 => 4.0, 3 => 3.99, 4 => 2.0]);
+
+ $source = new RatingFacetSource($this->ratings, ratingScale: 5);
+
+ self::assertSame([1, 2], $source->productIdsRatedAtLeast(4.0));
+ self::assertSame([1, 2, 3, 4], $source->productIdsRatedAtLeast(2.0));
+ }
+
+ public function testTheThresholdIsBroughtToTheScaleTheShopRatesOn(): void
+ {
+ // Out of ten, "4 stars & up" means an average of 8 or more.
+ $this->rate([1 => 9.0, 2 => 8.0, 3 => 7.5]);
+
+ $source = new RatingFacetSource($this->ratings, ratingScale: 10);
+
+ self::assertSame([1, 2], $source->productIdsRatedAtLeast(4.0));
+ }
+
+ public function testOnlyTheProductsAskedAboutAreAnswered(): void
+ {
+ $this->rate([1 => 5.0, 2 => 5.0, 3 => 5.0]);
+
+ $source = new RatingFacetSource($this->ratings, ratingScale: 5);
+
+ self::assertSame([1, 3], $source->productIdsRatedAtLeast(4.0, [1, 3, 9]));
+ self::assertSame([], $source->productIdsRatedAtLeast(4.0, []));
+ }
+
+ public function testTheRatingsOfOtherElementsAreNotProductRatings(): void
+ {
+ $this->ratings->store('content', 1, 5.0, 3);
+
+ self::assertSame([], (new RatingFacetSource($this->ratings, ratingScale: 5))->productIdsRatedAtLeast(2.0));
+ }
+
+ /**
+ * @param array $averages product id => stored average
+ */
+ private function rate(array $averages): void
+ {
+ foreach ($averages as $productId => $average) {
+ $this->ratings->store('product', $productId, $average, 1);
+ }
+ }
+}
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..a7dec84
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,8 @@
+# Product rating source contract of a core newer than the published one: Comment::configureServices() wires
+# Front/RatingFacetSource.php only when interface_exists() finds it. Kept out of the report so that PHPStan runs
+# against the published core; an entry the core catches up with is not reported.
+parameters:
+ # PHPStan cannot ignore a class that implements an unknown interface: the class is left out of the analysis instead (still scanned, so its name resolves).
+ excludePaths:
+ analyse:
+ - Front/RatingFacetSource.php