feat: performance improvements
This commit is contained in:
@@ -21,7 +21,6 @@
|
|||||||
"nesbot/carbon": "^3.8",
|
"nesbot/carbon": "^3.8",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.0",
|
"phpstan/phpdoc-parser": "^2.0",
|
||||||
"spatie/blink": "^1.4",
|
|
||||||
"spatie/crypto": "^2.1",
|
"spatie/crypto": "^2.1",
|
||||||
"spatie/ray": "^1.43",
|
"spatie/ray": "^1.43",
|
||||||
"symfony/apache-pack": "^1.0",
|
"symfony/apache-pack": "^1.0",
|
||||||
|
|||||||
Generated
+1
-63
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "a298068a9461dc19cb71f53ea0a7093b",
|
"content-hash": "01564d521df6bf616616fcbba9454877",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -4012,68 +4012,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-08-26T08:22:30+00:00"
|
"time": "2025-08-26T08:22:30+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "spatie/blink",
|
|
||||||
"version": "1.4.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/spatie/blink.git",
|
|
||||||
"reference": "d2c12b84ba04d4c5b53d701cc09810bf7e5d546f"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/spatie/blink/zipball/d2c12b84ba04d4c5b53d701cc09810bf7e5d546f",
|
|
||||||
"reference": "d2c12b84ba04d4c5b53d701cc09810bf7e5d546f",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^9.5"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Spatie\\Blink\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Freek Van der Herten",
|
|
||||||
"email": "[email protected]",
|
|
||||||
"homepage": "https://spatie.be",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Cache that expires in the blink of an eye",
|
|
||||||
"homepage": "https://github.com/spatie/blink",
|
|
||||||
"keywords": [
|
|
||||||
"Blink",
|
|
||||||
"cache",
|
|
||||||
"caching",
|
|
||||||
"spatie"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/spatie/blink/issues",
|
|
||||||
"source": "https://github.com/spatie/blink/tree/1.4.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://spatie.be/open-source/support-us",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/spatie",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2023-07-19T18:28:44+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "spatie/crypto",
|
"name": "spatie/crypto",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ use App\Service\ServiceAvailabilityCalculator;
|
|||||||
*/
|
*/
|
||||||
class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||||
{
|
{
|
||||||
|
/** @var array<string, array<string, mixed>> Request-scoped cache for field options */
|
||||||
|
private array $optionsCache = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the provider with required dependencies.
|
* Initializes the provider with required dependencies.
|
||||||
*
|
*
|
||||||
@@ -57,6 +60,39 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves form field options with request-scoped caching.
|
||||||
|
*
|
||||||
|
* Overrides parent to add instance-level caching, preventing redundant
|
||||||
|
* field option calculations when the same field is accessed multiple times
|
||||||
|
* during form building (happens ~16 times per participant × 50 participants = 800 calls).
|
||||||
|
*
|
||||||
|
* Cache key includes: field name, participant index, mode, and date of birth.
|
||||||
|
* The cache is automatically cleared between requests since the service is request-scoped.
|
||||||
|
*
|
||||||
|
* @param string $fieldName The name of the field to configure
|
||||||
|
* @param BookingDto $bookingDto The current booking data for context
|
||||||
|
* @param int $participantIndex The index of the participant being configured
|
||||||
|
* @param array $options Additional options to customize field behavior
|
||||||
|
*
|
||||||
|
* @return array<string, mixed> Symfony form field options, or empty array if field not supported
|
||||||
|
*/
|
||||||
|
public function getFieldOptions(string $fieldName, BookingDto $bookingDto, int $participantIndex, array $options = []): array
|
||||||
|
{
|
||||||
|
// Generate cache key based on factors that affect field options
|
||||||
|
$participant = $bookingDto->getParticipant($participantIndex);
|
||||||
|
$cacheKey = sprintf(
|
||||||
|
'%s_%d_%s_%s',
|
||||||
|
$fieldName,
|
||||||
|
$participantIndex,
|
||||||
|
$bookingDto->getMode(),
|
||||||
|
$participant?->dateOfBirth?->format('Y-m-d') ?? 'no_dob'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return cached result if available, otherwise compute and cache
|
||||||
|
return $this->optionsCache[$cacheKey] ??= parent::getFieldOptions($fieldName, $bookingDto, $participantIndex, $options);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers all field option providers during service initialization.
|
* Registers all field option providers during service initialization.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ use App\Form\Model\ParticipantDto;
|
|||||||
*/
|
*/
|
||||||
class BookingPriceCalculatorService
|
class BookingPriceCalculatorService
|
||||||
{
|
{
|
||||||
|
/** @var array<string, float> Request-scoped cache for participant prices */
|
||||||
|
private array $participantPriceCache = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ParticipantEligibilityService $participantEligibilityService,
|
private readonly ParticipantEligibilityService $participantEligibilityService,
|
||||||
private readonly InsuranceService $insuranceService,
|
private readonly InsuranceService $insuranceService,
|
||||||
@@ -324,6 +327,28 @@ class BookingPriceCalculatorService
|
|||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate cache key based on participant state that affects pricing
|
||||||
|
$stateComponents = [
|
||||||
|
'room' => $participant->assignedRoomId ?? 'none',
|
||||||
|
'skiPass' => $participant->skiPass?->id ?? 'none',
|
||||||
|
'rentalInsurance' => $participant->rentalInsurance?->id ?? 'none',
|
||||||
|
'transportationOut' => $participant->transportationOutbound?->id ?? 'none',
|
||||||
|
'transportationIn' => $participant->transportationInbound?->id ?? 'none',
|
||||||
|
'pickup' => $participant->pickup?->id ?? 'none',
|
||||||
|
'parking' => $participant->parkingService?->id ?? 'none',
|
||||||
|
'courses' => implode('_', array_map(fn ($s) => $s->id, $participant->courses ?? [])),
|
||||||
|
'additionalServices' => implode('_', array_map(fn ($s) => $s->id, $participant->additionalServices ?? [])),
|
||||||
|
'board' => implode('_', array_map(fn ($s) => $s->id, $participant->board ?? [])),
|
||||||
|
'rentals' => implode('_', array_map(fn ($s) => $s->id, $participant->rentals ?? [])),
|
||||||
|
];
|
||||||
|
|
||||||
|
$cacheKey = sprintf(
|
||||||
|
'participant_price_%d_%s',
|
||||||
|
$participantIndex,
|
||||||
|
md5(json_encode($stateComponents))
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->participantPriceCache[$cacheKey] ??= (function () use ($bookingDto, $participant) {
|
||||||
$totalPrice = 0.0;
|
$totalPrice = 0.0;
|
||||||
|
|
||||||
// Add room price if participant is assigned to a room
|
// Add room price if participant is assigned to a room
|
||||||
@@ -340,6 +365,7 @@ class BookingPriceCalculatorService
|
|||||||
$totalPrice += $this->calculateParticipantServiceTotal($participant, false, null, true);
|
$totalPrice += $this->calculateParticipantServiceTotal($participant, false, null, true);
|
||||||
|
|
||||||
return $totalPrice;
|
return $totalPrice;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,20 +10,23 @@ use App\BusProNet\Traits\SortByPriceTrait;
|
|||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Spatie\Blink\Blink;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consolidated service for all insurance-related operations.
|
* Consolidated service for all insurance-related operations.
|
||||||
*
|
*
|
||||||
* Handles insurance eligibility evaluation, filtering, caching, and assignment logic.
|
* Handles insurance eligibility evaluation, filtering, caching, and assignment logic.
|
||||||
* Uses request-scoped caching via Blink to optimize performance for bookings with many participants.
|
* Uses request-scoped instance-level caching to optimize performance for bookings with many participants.
|
||||||
* This service is stateless and has no dependencies to avoid circular dependency issues.
|
* This service is stateless and has no dependencies to avoid circular dependency issues.
|
||||||
*/
|
*/
|
||||||
class InsuranceService
|
class InsuranceService
|
||||||
{
|
{
|
||||||
use SortByPriceTrait;
|
use SortByPriceTrait;
|
||||||
|
|
||||||
private const CACHE_KEY_PREFIX = 'selectable_insurances_';
|
/** @var array<string, array<Insurance>> Request-scoped cache for selectable insurances */
|
||||||
|
private array $selectableInsurancesCache = [];
|
||||||
|
|
||||||
|
/** @var array<string, array<Insurance>> Request-scoped cache for eligible insurances */
|
||||||
|
private array $eligibleInsurancesCache = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns selectable (non-complementary) insurances for a travel with request-scoped caching.
|
* Returns selectable (non-complementary) insurances for a travel with request-scoped caching.
|
||||||
@@ -38,11 +41,9 @@ class InsuranceService
|
|||||||
*/
|
*/
|
||||||
public function getSelectableInsurances(Travel $travel): array
|
public function getSelectableInsurances(Travel $travel): array
|
||||||
{
|
{
|
||||||
$cacheKey = self::CACHE_KEY_PREFIX.$travel->id;
|
$cacheKey = 'selectable_insurances_'.$travel->id;
|
||||||
|
|
||||||
return Blink::global()->once($cacheKey, function () use ($travel) {
|
return $this->selectableInsurancesCache[$cacheKey] ??= $this->filterNonComplementary($travel->insurances ?? []);
|
||||||
return $this->filterNonComplementary($travel->insurances ?? []);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,6 +69,23 @@ class InsuranceService
|
|||||||
return []; // Cannot evaluate without travel dates
|
return []; // Cannot evaluate without travel dates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate cache key based on all eligibility criteria
|
||||||
|
// Use spl_object_id() for insurance identification (not $insurance->id):
|
||||||
|
// - Works with test fixtures where IDs may be null
|
||||||
|
// - Negligible overhead (~0.002ms for 20 insurances vs ~50ms saved by caching)
|
||||||
|
// - Insurance objects are stable within a single request (loaded from session)
|
||||||
|
$insuranceHashes = array_map(fn (Insurance $i) => spl_object_id($i), $insurances);
|
||||||
|
$cacheKey = sprintf(
|
||||||
|
'eligible_insurances_%d_%s_%s_%s_%s_%s',
|
||||||
|
$participant->index,
|
||||||
|
number_format(round($travelPrice, 2), 2, '.', ''),
|
||||||
|
$booking->getMode(),
|
||||||
|
$travelStartDate->format('Y-m-d'),
|
||||||
|
$participant->dateOfBirth?->format('Y-m-d') ?? 'no_dob',
|
||||||
|
md5(implode('_', $insuranceHashes))
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->eligibleInsurancesCache[$cacheKey] ??= (function () use ($insurances, $participant, $booking, $travelStartDate, $travelEndDate, $travelPrice) {
|
||||||
$bookingDate = Carbon::now()->toDateTimeImmutable();
|
$bookingDate = Carbon::now()->toDateTimeImmutable();
|
||||||
$travelDurationDays = $travelStartDate->diff($travelEndDate)->days;
|
$travelDurationDays = $travelStartDate->diff($travelEndDate)->days;
|
||||||
|
|
||||||
@@ -86,6 +104,7 @@ class InsuranceService
|
|||||||
);
|
);
|
||||||
|
|
||||||
return $this->sortByPrice($eligibleInsurances);
|
return $this->sortByPrice($eligibleInsurances);
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use App\BusProNet\Constants;
|
|||||||
use App\BusProNet\Model\Service;
|
use App\BusProNet\Model\Service;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Spatie\Blink\Blink;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for evaluating participant eligibility for booking.
|
* Service for evaluating participant eligibility for booking.
|
||||||
@@ -18,11 +17,13 @@ use Spatie\Blink\Blink;
|
|||||||
* for eligibility checks used by both the conditional field state system and
|
* for eligibility checks used by both the conditional field state system and
|
||||||
* the view layer via Twig extension.
|
* the view layer via Twig extension.
|
||||||
*
|
*
|
||||||
* Results are cached per request using Blink to avoid redundant calculations
|
* Results are cached per request using instance-level arrays to avoid redundant calculations
|
||||||
* when checking the same participant multiple times.
|
* when checking the same participant multiple times.
|
||||||
*/
|
*/
|
||||||
class ParticipantEligibilityService
|
class ParticipantEligibilityService
|
||||||
{
|
{
|
||||||
|
/** @var array<string, bool> Request-scoped cache for participant eligibility */
|
||||||
|
private array $eligibilityCache = [];
|
||||||
/**
|
/**
|
||||||
* Checks if a participant is eligible for booking.
|
* Checks if a participant is eligible for booking.
|
||||||
*
|
*
|
||||||
@@ -52,7 +53,7 @@ class ParticipantEligibilityService
|
|||||||
$participantIndex
|
$participantIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
return Blink::global()->once($cacheKey, function () use ($bookingDto, $participantIndex) {
|
return $this->eligibilityCache[$cacheKey] ??= (function () use ($bookingDto, $participantIndex) {
|
||||||
$allSkiPasses = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true, true);
|
$allSkiPasses = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true, true);
|
||||||
$availableSkiPasses = array_filter(
|
$availableSkiPasses = array_filter(
|
||||||
$allSkiPasses,
|
$allSkiPasses,
|
||||||
@@ -60,7 +61,7 @@ class ParticipantEligibilityService
|
|||||||
);
|
);
|
||||||
|
|
||||||
return !empty($availableSkiPasses);
|
return !empty($availableSkiPasses);
|
||||||
});
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user