125 lines
4.7 KiB
PHP
125 lines
4.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\EarlyGrowth;
|
|
|
|
use App\Services\EarlyGrowth\SpotlightEngineInterface;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
|
|
/**
|
|
* FeedBlender
|
|
*
|
|
* Blends real fresh uploads with curated older content and spotlight picks
|
|
* to make early-stage feeds feel alive and diverse without faking engagement.
|
|
*
|
|
* Rules:
|
|
* - ONLY applied to page 1 — deeper pages use the real feed untouched.
|
|
* - No fake artworks, timestamps, or metrics.
|
|
* - Duplicates removed before merging.
|
|
* - The original paginator's total / path / page-name are preserved so
|
|
* pagination links and SEO canonical/prev/next remain correct.
|
|
*
|
|
* Mode blend ratios are defined in config/early_growth.php:
|
|
* light → 60% fresh / 25% curated / 15% spotlight
|
|
* aggressive → 30% fresh / 50% curated / 20% spotlight
|
|
*/
|
|
final class FeedBlender
|
|
{
|
|
public function __construct(
|
|
private readonly SpotlightEngineInterface $spotlight,
|
|
) {}
|
|
|
|
/**
|
|
* Blend a LengthAwarePaginator of fresh artworks with curated and spotlight content.
|
|
*
|
|
* @param LengthAwarePaginator $freshResults Original fresh-upload paginator
|
|
* @param int $perPage Items per page
|
|
* @param int $page Current page number
|
|
* @return LengthAwarePaginator Blended paginator (page 1) or original (page > 1)
|
|
*/
|
|
public function blend(
|
|
LengthAwarePaginator $freshResults,
|
|
int $perPage = 24,
|
|
int $page = 1,
|
|
): LengthAwarePaginator {
|
|
// Only blend on page 1; real pagination takes over for deeper pages
|
|
if (! EarlyGrowth::enabled() || $page > 1) {
|
|
return $freshResults;
|
|
}
|
|
|
|
$ratios = EarlyGrowth::blendRatios();
|
|
|
|
if (($ratios['curated'] + $ratios['spotlight']) < 0.001) {
|
|
// Mode is effectively "fresh only" — nothing to blend
|
|
return $freshResults;
|
|
}
|
|
|
|
$fresh = $freshResults->getCollection();
|
|
$freshIds = $fresh->pluck('id')->toArray();
|
|
|
|
// Calculate absolute item counts from ratios
|
|
[$freshCount, $curatedCount, $spotlightCount] = $this->allocateCounts($ratios, $perPage);
|
|
|
|
// Fetch sources — over-fetch to account for deduplication losses
|
|
$curated = $this->spotlight
|
|
->getCurated($curatedCount + 6)
|
|
->filter(fn ($a) => ! in_array($a->id, $freshIds, true))
|
|
->take($curatedCount)
|
|
->values();
|
|
|
|
$curatedIds = $curated->pluck('id')->toArray();
|
|
|
|
$spotlightItems = $this->spotlight
|
|
->getSpotlight($spotlightCount + 6)
|
|
->filter(fn ($a) => ! in_array($a->id, $freshIds, true))
|
|
->filter(fn ($a) => ! in_array($a->id, $curatedIds, true))
|
|
->take($spotlightCount)
|
|
->values();
|
|
|
|
// Compose blended page
|
|
$blended = $fresh->take($freshCount)
|
|
->concat($curated)
|
|
->concat($spotlightItems)
|
|
->unique('id')
|
|
->values();
|
|
|
|
// Pad back to $perPage with leftover fresh items if any source ran short
|
|
if ($blended->count() < $perPage) {
|
|
$usedIds = $blended->pluck('id')->toArray();
|
|
$pad = $fresh
|
|
->filter(fn ($a) => ! in_array($a->id, $usedIds, true))
|
|
->take($perPage - $blended->count());
|
|
$blended = $blended->concat($pad)->unique('id')->values();
|
|
}
|
|
|
|
// Rebuild paginator preserving the real total so pagination links remain stable
|
|
return new LengthAwarePaginator(
|
|
$blended->take($perPage)->all(),
|
|
$freshResults->total(), // ← real total, not blended count
|
|
$perPage,
|
|
$page,
|
|
[
|
|
'path' => $freshResults->path(),
|
|
'pageName' => $freshResults->getPageName(),
|
|
]
|
|
);
|
|
}
|
|
|
|
// ─── Private helpers ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Distribute $perPage slots across fresh / curated / spotlight.
|
|
* Returns [freshCount, curatedCount, spotlightCount].
|
|
*/
|
|
private function allocateCounts(array $ratios, int $perPage): array
|
|
{
|
|
$total = max(0.001, ($ratios['fresh'] ?? 0) + ($ratios['curated'] ?? 0) + ($ratios['spotlight'] ?? 0));
|
|
$freshN = (int) round($perPage * ($ratios['fresh'] ?? 1.0) / $total);
|
|
$curatedN = (int) round($perPage * ($ratios['curated'] ?? 0.0) / $total);
|
|
$spotN = $perPage - $freshN - $curatedN;
|
|
|
|
return [max(0, $freshN), max(0, $curatedN), max(0, $spotN)];
|
|
}
|
|
}
|