From 8e29980dfee9e4e95b6b3d33d99c7266d4dae061 Mon Sep 17 00:00:00 2001 From: Ovidiu U Date: Wed, 29 Apr 2026 20:20:59 +0100 Subject: [PATCH] perf: memoize PriceReliability + PriceClassification per row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Controllers/Api/StationController.php | 25 +++++++++++-------- app/Http/Resources/Api/StationResource.php | 17 +++++++------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app/Http/Controllers/Api/StationController.php b/app/Http/Controllers/Api/StationController.php index e792863..9cba67d 100644 --- a/app/Http/Controllers/Api/StationController.php +++ b/app/Http/Controllers/Api/StationController.php @@ -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([ diff --git a/app/Http/Resources/Api/StationResource.php b/app/Http/Resources/Api/StationResource.php index 366e2c5..c19d727 100644 --- a/app/Http/Resources/Api/StationResource.php +++ b/app/Http/Resources/Api/StationResource.php @@ -12,8 +12,13 @@ class StationResource extends JsonResource { public function toArray(Request $request): array { - $updatedAt = $this->price_effective_at ? Carbon::parse($this->price_effective_at) : null; - $reliability = PriceReliability::fromUpdatedAt($updatedAt); + // The controller pre-computes _updated_at / _reliability / _classification + // per row. Falling back to fresh computation keeps the resource usable + // outside that path (e.g. tests or future callers). + $updatedAt = $this->_updated_at + ?? ($this->price_effective_at ? Carbon::parse($this->price_effective_at) : null); + $reliability = $this->_reliability ?? PriceReliability::fromUpdatedAt($updatedAt); + $classification = $this->_classification ?? PriceClassification::fromUpdatedAt($updatedAt); return [ 'station_id' => $this->node_id, @@ -32,11 +37,9 @@ class StationResource extends JsonResource 'open_today' => $this->openTodayPayload(), 'price_pence' => (int) $this->price_pence, 'price' => round((int) $this->price_pence / 100, 2), - 'price_updated_at' => $this->price_effective_at - ? Carbon::parse($this->price_effective_at)->toISOString() - : null, - 'price_classification' => PriceClassification::fromUpdatedAt($updatedAt)->value, - 'price_classification_label' => PriceClassification::fromUpdatedAt($updatedAt)->label(), + 'price_updated_at' => $updatedAt?->toISOString(), + 'price_classification' => $classification->value, + 'price_classification_label' => $classification->label(), 'reliability' => $reliability->value, 'reliability_label' => $reliability->label(), ];