Files
SkinbaseNova/app/Services/NovaCards/NovaCardRisingService.php
2026-03-28 19:15:39 +01:00

82 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\NovaCards;
use App\Models\NovaCard;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
/**
* Surfaces "rising" cards — recently published cards that are gaining
* engagement faster than average, weighted to balance novelty creators.
*/
class NovaCardRisingService
{
/** Number of hours a card is eligible for "rising" feed. */
private const WINDOW_HOURS = 96;
/** Cache TTL in seconds. */
private const CACHE_TTL = 300;
public function risingCards(int $limit = 18, bool $cached = true): Collection
{
if ($cached) {
return Cache::remember(
'nova_cards.rising.' . $limit,
self::CACHE_TTL,
fn () => $this->queryRising($limit),
);
}
return $this->queryRising($limit);
}
public function invalidateCache(): void
{
foreach ([6, 18, 24, 36] as $limit) {
Cache::forget('nova_cards.rising.' . $limit);
}
}
private function queryRising(int $limit): Collection
{
$cutoff = Carbon::now()->subHours(self::WINDOW_HOURS);
$isSqlite = DB::connection()->getDriverName() === 'sqlite';
$ageHoursExpression = $isSqlite
? "CASE WHEN ((julianday('now') - julianday(published_at)) * 24.0) < 1 THEN 1 ELSE ((julianday('now') - julianday(published_at)) * 24.0) END"
: 'GREATEST(1, TIMESTAMPDIFF(HOUR, published_at, NOW()))';
$decayExpression = $isSqlite
? $ageHoursExpression
: 'POWER(' . $ageHoursExpression . ', 0.7)';
$risingMomentumExpression = '(
(saves_count * 5.0 + remixes_count * 6.0 + likes_count * 4.0 + favorites_count * 2.5 + comments_count * 2.0 + challenge_entries_count * 4.0)
/ ' . $decayExpression . '
) AS rising_momentum';
return NovaCard::query()
->publiclyVisible()
->where('published_at', '>=', $cutoff)
// Must have at least one meaningful engagement signal.
->where(function (Builder $q): void {
$q->where('saves_count', '>', 0)
->orWhere('remixes_count', '>', 0)
->orWhere('likes_count', '>', 1);
})
->select([
'nova_cards.*',
// Rising score: weight recent engagement, penalise by sqrt(age hours) to let novelty show
DB::raw($risingMomentumExpression),
])
->orderByDesc('rising_momentum')
->orderByDesc('published_at')
->limit($limit)
->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags'])
->get();
}
}