php
61 lines · 9 steps
Building a cached daily leaderboard in Laravel
A service that ranks the day's top scorers and caches the result until midnight.
Explained by
highlit
1<?php
2
3namespace App\Services;
4
5use App\Models\Score;
6use Carbon\CarbonInterval;
7use Illuminate\Support\Carbon;
8use Illuminate\Support\Collection;
9use Illuminate\Support\Facades\Cache;
10
11class DailyLeaderboard
12{
13 public function __construct(private readonly int $limit = 25)
14 {
15 }
16
17 public function top(?Carbon $date = null): Collection
18 {
19 $date = ($date ?? now())->startOfDay();
20
21 return Cache::remember(
22 $this->cacheKey($date),
23 $this->ttlUntilMidnight($date),
24 fn () => $this->rank($date),
25 );
26 }
27
28 public function forget(?Carbon $date = null): void
29 {
30 Cache::forget($this->cacheKey($date ?? now()));
31 }
32
33 private function rank(Carbon $date): Collection
34 {
35 return Score::query()
36 ->select('user_id')
37 ->selectRaw('SUM(points) AS total_points')
38 ->whereBetween('created_at', [$date, $date->copy()->endOfDay()])
39 ->groupBy('user_id')
40 ->orderByDesc('total_points')
41 ->with('user:id,name,avatar_url')
42 ->limit($this->limit)
43 ->get()
44 ->values()
45 ->map(fn (Score $score, int $index) => [
46 'rank' => $index + 1,
47 'user' => $score->user,
48 'points' => (int) $score->total_points,
49 ]);
50 }
51
52 private function cacheKey(Carbon $date): string
53 {
54 return "leaderboard:daily:{$date->toDateString()}:{$this->limit}";
55 }
56
57 private function ttlUntilMidnight(Carbon $date): CarbonInterval
58 {
59 return now()->diffAsCarbonInterval($date->copy()->addDay());
60 }
61}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Scoping a cache TTL to a natural boundary like midnight keeps derived data fresh without manual invalidation.
- 2Pushing aggregation into SQL with grouping and ordering avoids loading and summing rows in PHP.
- 3Encoding every query variable into the cache key prevents different configurations from colliding.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-cached-daily-leaderboard-in-laravel-explained-php-e129/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.