chore: add inline documentation
This commit is contained in:
@@ -13,6 +13,18 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
#[ORM\Index(name: 'idx_snapshot_product_id', columns: ['product_id'])]
|
#[ORM\Index(name: 'idx_snapshot_product_id', columns: ['product_id'])]
|
||||||
#[ORM\Index(name: 'idx_snapshot_date_to', columns: ['date_to'])]
|
#[ORM\Index(name: 'idx_snapshot_date_to', columns: ['date_to'])]
|
||||||
#[ORM\Index(name: 'idx_snapshot_extended_refreshed_at', columns: ['extended_refreshed_at'])]
|
#[ORM\Index(name: 'idx_snapshot_extended_refreshed_at', columns: ['extended_refreshed_at'])]
|
||||||
|
/**
|
||||||
|
* Persisted copy of a Travel aggregate for travels whose source XML may no longer be available.
|
||||||
|
*
|
||||||
|
* Each row stores the full serialized Travel graph as a JSON payload alongside a set of
|
||||||
|
* denormalized metadata columns (code, label, dates, hotel) that allow filtering and
|
||||||
|
* mapping lookups without deserializing the payload.
|
||||||
|
*
|
||||||
|
* Three timestamps track the lifecycle of the snapshot:
|
||||||
|
* - capturedAt: when the snapshot was first written
|
||||||
|
* - updatedAt: when the payload or metadata was last modified
|
||||||
|
* - extendedRefreshedAt: when extended availability (VERFUEGBARKEIT2) was last applied
|
||||||
|
*/
|
||||||
class TravelSnapshot
|
class TravelSnapshot
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -50,9 +62,11 @@ class TravelSnapshot
|
|||||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||||
private ?\DateTimeImmutable $dateTo = null;
|
private ?\DateTimeImmutable $dateTo = null;
|
||||||
|
|
||||||
|
/** Full JSON-serialized Travel aggregate. */
|
||||||
#[ORM\Column(type: 'text')]
|
#[ORM\Column(type: 'text')]
|
||||||
private string $payload;
|
private string $payload;
|
||||||
|
|
||||||
|
/** SHA-256 of payload; compared before every write to skip no-op flushes. */
|
||||||
#[ORM\Column(type: 'string', length: 64)]
|
#[ORM\Column(type: 'string', length: 64)]
|
||||||
private string $payloadHash;
|
private string $payloadHash;
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,9 @@ class TravelSnapshotRepository extends ServiceEntityRepository
|
|||||||
int $limit,
|
int $limit,
|
||||||
): array {
|
): array {
|
||||||
return $this->createQueryBuilder('s')
|
return $this->createQueryBuilder('s')
|
||||||
|
// dateTo IS NULL: snapshots without a known end date are always considered active.
|
||||||
->where('s.dateTo IS NULL OR s.dateTo >= :dateToThreshold')
|
->where('s.dateTo IS NULL OR s.dateTo >= :dateToThreshold')
|
||||||
|
// extendedRefreshedAt IS NULL: never-refreshed snapshots rank as oldest.
|
||||||
->andWhere('s.extendedRefreshedAt IS NULL OR s.extendedRefreshedAt < :refreshBefore')
|
->andWhere('s.extendedRefreshedAt IS NULL OR s.extendedRefreshedAt < :refreshBefore')
|
||||||
->setParameter('dateToThreshold', $dateToThreshold)
|
->setParameter('dateToThreshold', $dateToThreshold)
|
||||||
->setParameter('refreshBefore', $refreshBefore)
|
->setParameter('refreshBefore', $refreshBefore)
|
||||||
@@ -116,6 +118,8 @@ class TravelSnapshotRepository extends ServiceEntityRepository
|
|||||||
{
|
{
|
||||||
return (int) $this->createQueryBuilder('s')
|
return (int) $this->createQueryBuilder('s')
|
||||||
->delete()
|
->delete()
|
||||||
|
// Snapshots with no known end date are excluded from purging as a safety net;
|
||||||
|
// they must be cleaned up manually if they become stale.
|
||||||
->where('s.dateTo IS NOT NULL')
|
->where('s.dateTo IS NOT NULL')
|
||||||
->andWhere('s.dateTo < :beforeDate')
|
->andWhere('s.dateTo < :beforeDate')
|
||||||
->setParameter('beforeDate', $beforeDate)
|
->setParameter('beforeDate', $beforeDate)
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ class TravelSnapshotService
|
|||||||
*/
|
*/
|
||||||
public function upsertFromTravel(Travel $travel): void
|
public function upsertFromTravel(Travel $travel): void
|
||||||
{
|
{
|
||||||
|
// Both fields form the composite unique key; nothing to persist without them.
|
||||||
if (null === $travel->id || null === $travel->hotelId) {
|
if (null === $travel->id || null === $travel->hotelId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -191,6 +192,10 @@ class TravelSnapshotService
|
|||||||
$extendedResponseByDateId = [];
|
$extendedResponseByDateId = [];
|
||||||
|
|
||||||
foreach ($candidates as $snapshot) {
|
foreach ($candidates as $snapshot) {
|
||||||
|
// Skip travels whose XML is still live; their snapshot is kept authoritative
|
||||||
|
// by the XML load path (getTravelDataFromXml → upsertFromTravel). An empty
|
||||||
|
// $xmlAvailableDateIds means the file map could not be loaded, so fall back
|
||||||
|
// to refreshing everything rather than skipping all candidates.
|
||||||
if ([] !== $xmlAvailableDateIds && in_array($snapshot->getDateId(), $xmlAvailableDateIds, true)) {
|
if ([] !== $xmlAvailableDateIds && in_array($snapshot->getDateId(), $xmlAvailableDateIds, true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -203,6 +208,8 @@ class TravelSnapshotService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the API response by dateId: multiple hotel snapshots share one travel
|
||||||
|
// date, so a single API call covers all of them within the same batch.
|
||||||
if (false === array_key_exists($dateId, $extendedResponseByDateId)) {
|
if (false === array_key_exists($dateId, $extendedResponseByDateId)) {
|
||||||
$response = null;
|
$response = null;
|
||||||
try {
|
try {
|
||||||
@@ -215,6 +222,8 @@ class TravelSnapshotService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Notification response signals a business-level "no data" answer
|
||||||
|
// from the API rather than an error; treat it the same as a null response.
|
||||||
if (null !== $response && false === $response instanceof Notification) {
|
if (null !== $response && false === $response instanceof Notification) {
|
||||||
$extendedResponseByDateId[$dateId] = $response;
|
$extendedResponseByDateId[$dateId] = $response;
|
||||||
} elseif (false === isset($extendedResponseByDateId[$dateId])) {
|
} elseif (false === isset($extendedResponseByDateId[$dateId])) {
|
||||||
@@ -236,6 +245,8 @@ class TravelSnapshotService
|
|||||||
$snapshot->setPayload($payload)->setPayloadHash($payloadHash);
|
$snapshot->setPayload($payload)->setPayloadHash($payloadHash);
|
||||||
$this->applyTravelMetadata($snapshot, $travel);
|
$this->applyTravelMetadata($snapshot, $travel);
|
||||||
}
|
}
|
||||||
|
// Always stamp the refresh timestamp even when the payload was unchanged,
|
||||||
|
// so the candidate query does not re-select this snapshot on the next run.
|
||||||
$snapshot->setExtendedRefreshedAt(new \DateTimeImmutable())->touchUpdatedAt();
|
$snapshot->setExtendedRefreshedAt(new \DateTimeImmutable())->touchUpdatedAt();
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user