perf: memoize PriceReliability + PriceClassification per row

Audit item #10. PriceReliability::fromUpdatedAt was being invoked
~10× per station per /api/stations response — twice in the sort
comparator (once for $a, once for $b), once in the count groupBy, and
once per resource render. PriceClassification::fromUpdatedAt was
called twice inside the resource (value + label).

The controller now computes the parsed datetime + reliability +
classification once per row and stashes them on the row. Sort,
groupBy, and StationResource read the cached values; the resource
keeps a fresh-compute fallback for callers that bypass the controller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-04-29 20:20:59 +01:00
parent 4ce5066596
commit 8e29980dfe
2 changed files with 24 additions and 18 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api;
use App\Enums\PriceClassification;
use App\Enums\PriceReliability;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\NearbyStationsRequest;
@@ -59,19 +60,23 @@ class StationController extends Controller
->where('stations.permanent_closure', false)
->get();
// Compute reliability + classification once per row. The resource and
// sort/groupBy below all read these cached attributes — without this,
// PriceReliability::fromUpdatedAt is invoked ~10× per station per
// response (sort comparator, count groupBy, resource value+label).
$all->each(function ($s): void {
$updatedAt = $s->price_effective_at ? Carbon::parse($s->price_effective_at) : null;
$s->_updated_at = $updatedAt;
$s->_reliability = PriceReliability::fromUpdatedAt($updatedAt);
$s->_classification = PriceClassification::fromUpdatedAt($updatedAt);
});
$filtered = $all->filter(fn ($s) => (float) $s->distance_km <= $radius);
$stations = $sort === 'reliable'
? $filtered
->sort(function ($a, $b) {
$weightA = PriceReliability::fromUpdatedAt(
$a->price_effective_at ? Carbon::parse($a->price_effective_at) : null
)->weight();
$weightB = PriceReliability::fromUpdatedAt(
$b->price_effective_at ? Carbon::parse($b->price_effective_at) : null
)->weight();
return $weightA <=> $weightB
return $a->_reliability->weight() <=> $b->_reliability->weight()
?: ((int) $a->price_pence <=> (int) $b->price_pence)
?: ((float) $a->distance_km <=> (float) $b->distance_km);
})
@@ -85,9 +90,7 @@ class StationController extends Controller
$prices = $stations->pluck('price_pence');
$reliabilityCounts = $stations
->groupBy(fn ($s) => PriceReliability::fromUpdatedAt(
$s->price_effective_at ? Carbon::parse($s->price_effective_at) : null
)->value)
->groupBy(fn ($s) => $s->_reliability->value)
->map->count();
Search::create([