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:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Enums\PriceClassification;
|
||||||
use App\Enums\PriceReliability;
|
use App\Enums\PriceReliability;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Api\NearbyStationsRequest;
|
use App\Http\Requests\Api\NearbyStationsRequest;
|
||||||
@@ -59,19 +60,23 @@ class StationController extends Controller
|
|||||||
->where('stations.permanent_closure', false)
|
->where('stations.permanent_closure', false)
|
||||||
->get();
|
->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);
|
$filtered = $all->filter(fn ($s) => (float) $s->distance_km <= $radius);
|
||||||
|
|
||||||
$stations = $sort === 'reliable'
|
$stations = $sort === 'reliable'
|
||||||
? $filtered
|
? $filtered
|
||||||
->sort(function ($a, $b) {
|
->sort(function ($a, $b) {
|
||||||
$weightA = PriceReliability::fromUpdatedAt(
|
return $a->_reliability->weight() <=> $b->_reliability->weight()
|
||||||
$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
|
|
||||||
?: ((int) $a->price_pence <=> (int) $b->price_pence)
|
?: ((int) $a->price_pence <=> (int) $b->price_pence)
|
||||||
?: ((float) $a->distance_km <=> (float) $b->distance_km);
|
?: ((float) $a->distance_km <=> (float) $b->distance_km);
|
||||||
})
|
})
|
||||||
@@ -85,9 +90,7 @@ class StationController extends Controller
|
|||||||
$prices = $stations->pluck('price_pence');
|
$prices = $stations->pluck('price_pence');
|
||||||
|
|
||||||
$reliabilityCounts = $stations
|
$reliabilityCounts = $stations
|
||||||
->groupBy(fn ($s) => PriceReliability::fromUpdatedAt(
|
->groupBy(fn ($s) => $s->_reliability->value)
|
||||||
$s->price_effective_at ? Carbon::parse($s->price_effective_at) : null
|
|
||||||
)->value)
|
|
||||||
->map->count();
|
->map->count();
|
||||||
|
|
||||||
Search::create([
|
Search::create([
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ class StationResource extends JsonResource
|
|||||||
{
|
{
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
$updatedAt = $this->price_effective_at ? Carbon::parse($this->price_effective_at) : null;
|
// The controller pre-computes _updated_at / _reliability / _classification
|
||||||
$reliability = PriceReliability::fromUpdatedAt($updatedAt);
|
// 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 [
|
return [
|
||||||
'station_id' => $this->node_id,
|
'station_id' => $this->node_id,
|
||||||
@@ -32,11 +37,9 @@ class StationResource extends JsonResource
|
|||||||
'open_today' => $this->openTodayPayload(),
|
'open_today' => $this->openTodayPayload(),
|
||||||
'price_pence' => (int) $this->price_pence,
|
'price_pence' => (int) $this->price_pence,
|
||||||
'price' => round((int) $this->price_pence / 100, 2),
|
'price' => round((int) $this->price_pence / 100, 2),
|
||||||
'price_updated_at' => $this->price_effective_at
|
'price_updated_at' => $updatedAt?->toISOString(),
|
||||||
? Carbon::parse($this->price_effective_at)->toISOString()
|
'price_classification' => $classification->value,
|
||||||
: null,
|
'price_classification_label' => $classification->label(),
|
||||||
'price_classification' => PriceClassification::fromUpdatedAt($updatedAt)->value,
|
|
||||||
'price_classification_label' => PriceClassification::fromUpdatedAt($updatedAt)->label(),
|
|
||||||
'reliability' => $reliability->value,
|
'reliability' => $reliability->value,
|
||||||
'reliability_label' => $reliability->label(),
|
'reliability_label' => $reliability->label(),
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user