2026-09-01 12:06:29 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\Core\Catalog\Services;
|
|
|
|
|
|
2026-09-01 13:03:39 +03:00
|
|
|
use Illuminate\Database\Eloquent\Collection;
|
2026-09-01 12:06:29 +03:00
|
|
|
use Lunar\Models\Product;
|
|
|
|
|
use Modules\Core\Catalog\Contracts\RecommendationRule;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Runs each rule in config('catalog.recommendation_rules'), in order,
|
|
|
|
|
* topping up from each successive rule until $limit distinct products are
|
|
|
|
|
* collected or every rule is exhausted — e.g. 3 from SameCategoryRule
|
|
|
|
|
* (the product's category only has 3 other products) + 1 from RandomRule.
|
|
|
|
|
* No rule is special-cased as "the fallback" here; a store gets fallback
|
|
|
|
|
* behaviour purely by how it orders its own config (e.g. SameCategoryRule
|
|
|
|
|
* before RandomRule). Never returns the same product twice even if two
|
|
|
|
|
* rules would both suggest it (see RecommendationRule's $exclude), and
|
|
|
|
|
* never returns fewer than $limit unless the store genuinely doesn't have
|
|
|
|
|
* that many other products at all.
|
|
|
|
|
*/
|
|
|
|
|
class RecommendationService
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* @return Collection<int, Product>
|
|
|
|
|
*/
|
|
|
|
|
public function recommend(Product $product, int $limit = 4): Collection
|
|
|
|
|
{
|
2026-09-01 13:03:39 +03:00
|
|
|
$recommendations = new Collection();
|
2026-09-01 12:06:29 +03:00
|
|
|
|
|
|
|
|
foreach (config('catalog.recommendation_rules', []) as $ruleClass) {
|
|
|
|
|
if ($recommendations->count() >= $limit) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$exclude = [$product->id, ...$recommendations->pluck('id')];
|
|
|
|
|
$remaining = $limit - $recommendations->count();
|
|
|
|
|
|
|
|
|
|
/** @var RecommendationRule $rule */
|
|
|
|
|
$rule = app($ruleClass);
|
|
|
|
|
$recommendations = $recommendations->merge(
|
|
|
|
|
$rule->recommend($product, $remaining, $exclude)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $recommendations->take($limit)->values();
|
|
|
|
|
}
|
|
|
|
|
}
|