Files
SkinbaseNova/app/Services/EarlyGrowth/SpotlightEngine.php

117 lines
4.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Services\EarlyGrowth;
use App\Models\Artwork;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
/**
* SpotlightEngine
*
* Selects and rotates curated spotlight artworks for use in feed blending,
* grid filling, and dedicated spotlight sections.
*
* Selection is date-seeded so the spotlight rotates daily without DB writes.
* No artwork timestamps or engagement metrics are modified — this is purely
* a read-and-present layer.
*/
final class SpotlightEngine implements SpotlightEngineInterface
{
/**
* Return spotlight artworks for the current day.
* Cached for `early_growth.cache_ttl.spotlight` seconds (default 1 hour).
* Rotates daily via a date-seeded RAND() expression.
*
* Returns empty collection when SpotlightEngine is disabled.
*/
public function getSpotlight(int $limit = 6): Collection
{
if (! EarlyGrowth::spotlightEnabled()) {
return collect();
}
$ttl = (int) config('early_growth.cache_ttl.spotlight', 3600);
$cacheKey = 'egs.spotlight.' . now()->format('Y-m-d') . ".{$limit}";
return Cache::remember($cacheKey, $ttl, fn (): Collection => $this->selectSpotlight($limit));
}
/**
* Return high-quality older artworks for feed blending ("curated" pool).
* Excludes artworks newer than $olderThanDays to keep them out of the
* "fresh" section yet available for blending.
*
* Cached per (limit, olderThanDays) tuple and rotated daily.
*/
public function getCurated(int $limit = 12, int $olderThanDays = 7): Collection
{
if (! EarlyGrowth::enabled()) {
return collect();
}
$ttl = (int) config('early_growth.cache_ttl.spotlight', 3600);
$cacheKey = 'egs.curated.' . now()->format('Y-m-d') . ".{$limit}.{$olderThanDays}";
return Cache::remember($cacheKey, $ttl, fn (): Collection => $this->selectCurated($limit, $olderThanDays));
}
// ─── Private selection logic ──────────────────────────────────────────────
/**
* Select spotlight artworks.
* Uses a date-based seed for deterministic daily rotation.
* Fetches 3× the needed count and selects the top-ranked subset.
*/
private function selectSpotlight(int $limit): Collection
{
$seed = (int) now()->format('Ymd');
// Artworks published > 7 days ago with meaningful ranking score
return Artwork::query()
->public()
->published()
->with([
'user:id,name,username',
'user.profile:user_id,avatar_hash',
'categories:id,name,slug,content_type_id,parent_id,sort_order',
])
->leftJoin('artwork_stats as _ast', '_ast.artwork_id', '=', 'artworks.id')
->select('artworks.*')
->where('artworks.published_at', '<=', now()->subDays(7))
// Blend ranking quality with daily-seeded randomness so spotlight varies
->orderByRaw("COALESCE(_ast.ranking_score, 0) * 0.6 + RAND({$seed}) * 0.4 DESC")
->limit($limit * 3)
->get()
->sortByDesc(fn ($a) => optional($a->artworkStats)->ranking_score ?? 0)
->take($limit)
->values();
}
/**
* Select curated older artworks for feed blending.
*/
private function selectCurated(int $limit, int $olderThanDays): Collection
{
$seed = (int) now()->format('Ymd');
return Artwork::query()
->public()
->published()
->with([
'user:id,name,username',
'user.profile:user_id,avatar_hash',
'categories:id,name,slug,content_type_id,parent_id,sort_order',
])
->leftJoin('artwork_stats as _ast2', '_ast2.artwork_id', '=', 'artworks.id')
->select('artworks.*')
->where('artworks.published_at', '<=', now()->subDays($olderThanDays))
->orderByRaw("COALESCE(_ast2.ranking_score, 0) * 0.7 + RAND({$seed}) * 0.3 DESC")
->limit($limit)
->get()
->values();
}
}