fix: reduce snapshot payloads via serializer group and hydration

This commit is contained in:
Björn Fromme
2026-03-24 17:06:10 +01:00
parent 3b6ee90a7e
commit 6f07678f04
15 changed files with 142 additions and 105 deletions
+4 -4
View File
@@ -15,15 +15,15 @@ use Symfony\Component\Serializer\Attribute\Groups;
*/
class CrmSelection
{
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?int $id = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?string $label = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public bool $mutable = false;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public bool $selected = false;
}
+3 -3
View File
@@ -32,16 +32,16 @@ class CrmSelectionGroup
90 => 'Transport',
];
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?int $id = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?string $label = null;
/**
* @var array<int, CrmSelection>|null
*/
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?array $selections = null;
/**
+2 -2
View File
@@ -14,9 +14,9 @@ use Symfony\Component\Serializer\Attribute\Groups;
*/
class Guide
{
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?string $name = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?string $phone = null;
}
+7 -7
View File
@@ -15,24 +15,24 @@ use Symfony\Component\Serializer\Attribute\Groups;
*/
class Hotel
{
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $id = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $code = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $name = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $city = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $street = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $phone = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $country = null;
}
+9 -9
View File
@@ -17,32 +17,32 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
*/
class Pickup
{
#[Groups(['api:list'])]
#[Groups(['api:list', 'snapshot'])]
public ?int $id = null;
#[Groups(['api:list'])]
#[Groups(['api:list', 'snapshot'])]
public ?string $code = null;
#[Groups(['api:list'])]
#[Groups(['api:list', 'snapshot'])]
public ?string $city = null;
#[Groups(['api:list'])]
#[Groups(['api:list', 'snapshot'])]
public ?string $postalCode = null;
#[Groups(['api:list'])]
#[Groups(['api:list', 'snapshot'])]
public ?string $street = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i'])]
public ?\DateTimeImmutable $time = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?float $price = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?float $priceOutbound = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?float $priceInbound = null;
#[Groups(['api:booking'])]
+11 -11
View File
@@ -20,19 +20,19 @@ class Room
public const SELECTION_TYPE_BY_PAX = 'by_pax';
public const SELECTION_TYPE_BY_ROOM = 'by_room';
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $id = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $category = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $boardId = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $code = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $label = null;
#[Groups(['booking'])]
@@ -46,19 +46,19 @@ class Room
#[Groups(['booking'])]
public ?int $totalCount = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $minPax = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $maxPax = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $nights = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $status = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $available = null;
#[Groups(['booking'])]
@@ -67,7 +67,7 @@ class Room
#[Groups(['booking'])]
public array $mapping = [];
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?float $price = null;
#[Groups(['booking'])]
+25 -25
View File
@@ -17,78 +17,78 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
*/
class Service
{
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?int $id = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $category = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $status = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public bool $mandatory = false;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public bool $autoBook = false;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $label = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateFrom = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $timeFrom = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $dayTime = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateTo = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $subType = null;
#[Groups(['booking'])]
public ?int $totalCount = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public array $mapping = [];
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?float $price = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?float $totalPrice = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public array $individualPrice = [];
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?int $available = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $source = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $direction = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?int $ageFrom = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?int $ageTo = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?int $birthYearFrom = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?int $birthYearTo = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $ageConstraintType = null;
#[Groups(['api:single'])]
@@ -97,7 +97,7 @@ class Service
#[Groups(['api:single'])]
public ?string $rawAgeConstraintData = null;
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public ?string $description = null;
/**
@@ -107,6 +107,6 @@ class Service
* When false, this service's price is excluded when determining which insurance
* tier is appropriate for a participant.
*/
#[Groups(['api:single', 'api:list'])]
#[Groups(['api:single', 'api:list', 'snapshot'])]
public bool $includeInInsuranceCalculation = true;
}
+26 -26
View File
@@ -20,43 +20,43 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class Travel
{
use SortByPriceTrait;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $id = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $code = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $productCode = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $productId = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $label = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?int $hotelId = null;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?Hotel $hotel = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateFrom = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateTo = null;
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?string $type = null;
/**
* Travel booking status from BusProNet API.
* Possible values: 'Frei', 'Anfrage', 'Buchungsstop'.
*/
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?string $status = null;
/**
@@ -65,67 +65,67 @@ class Travel
*
* @var array<string>
*/
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public array $allowedBookingStatus = [];
#[Groups(['api:list', 'api:single'])]
#[Groups(['api:list', 'api:single', 'snapshot'])]
public ?float $priceFrom = null;
/**
* @var array<int, CrmSelectionGroup>
*/
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public array $selectionGroups = [];
/**
* @var array<int, Service>
*/
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public array $additionalServices = [];
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public bool $additionalServicesMutable = true;
/**
* @var array<int, Service>
*/
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public array $transportationServices = [];
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public bool $transportationServicesMutable = true;
/**
* @var array<int, Pickup>
*/
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public array $pickups = [];
/**
* @var array<int, Pickup>
*/
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public array $dropOffs = [];
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public bool $pickupsMutable = true;
/**
* @var array<int, Room>
*/
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public array $rooms = [];
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public bool $roomsMutable = true;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public bool $participantDataMutable = true;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public bool $participantCountMutable = true;
#[Groups(['api:single'])]
#[Groups(['api:single', 'snapshot'])]
public ?Guide $guide = null;
/**
+1 -1
View File
@@ -26,7 +26,7 @@ class InsuranceLoader extends AbstractLoader
{
try {
return $this->cache->get('bpn_insurances', function (ItemInterface $item) use ($filename) {
$item->expiresAfter(3 * 60 * 60);
$item->expiresAfter(24 * 60 * 60);
$crawler = $this->loadXml($filename);
-1
View File
@@ -32,7 +32,6 @@ class BpnXmlSyncCommand extends Command
'bpn_travels_mapping',
'bpn_hotels',
'bpn_pickups',
'bpn_insurances',
];
public function __construct(
+2 -2
View File
@@ -62,8 +62,8 @@ class TravelSnapshot
#[ORM\Column(type: 'date_immutable', nullable: true)]
private ?\DateTimeImmutable $dateTo = null;
/** Full JSON-serialized Travel aggregate. */
#[ORM\Column(type: 'text')]
/** Full serialized Travel aggregate stored as JSON text. */
#[ORM\Column(type: 'text', columnDefinition: 'LONGTEXT NOT NULL')]
private string $payload;
/** SHA-256 of payload; compared before every write to skip no-op flushes. */
+1 -1
View File
@@ -146,6 +146,7 @@ class TravelDataService
?? $this->getTravelDataFromXml($dateId, $hotelId);
if (null !== $travel) {
$this->patchInsuranceData($travel);
$this->hydrateInsurancePackageRelationships($travel->insurances);
}
@@ -760,7 +761,6 @@ class TravelDataService
try {
$this->pickupLoader->patchPickupsDetails($travel);
$this->hotelLoader->patchHotelDetails($travel);
$this->patchInsuranceData($travel);
} catch (\Exception $e) {
$this->logger->warning('Failed to enrich travel data', [
'travelId' => $travel->id,
+2 -2
View File
@@ -102,7 +102,7 @@ class TravelSnapshotService
return;
}
$payload = $this->serializer->serialize($travel, 'json');
$payload = $this->serializer->serialize($travel, 'json', ['groups' => ['snapshot']]);
$payloadHash = hash('sha256', $payload);
$snapshot = $this->snapshotRepository->findByDateAndHotel($travel->id, $travel->hotelId);
@@ -241,7 +241,7 @@ class TravelSnapshotService
$this->applyExtendedAvailability($travel, $response);
$payload = $this->serializer->serialize($travel, 'json');
$payload = $this->serializer->serialize($travel, 'json', ['groups' => ['snapshot']]);
$payloadHash = hash('sha256', $payload);
if ($payloadHash !== $snapshot->getPayloadHash()) {
$snapshot->setPayload($payload)->setPayloadHash($payloadHash);