130 lines
4.0 KiB
PHP
130 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Repository;
|
|
|
|
use App\Entity\TravelSnapshot;
|
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
use Doctrine\Persistence\ManagerRegistry;
|
|
|
|
/**
|
|
* Repository for querying and maintaining persisted travel snapshots.
|
|
*
|
|
* Provides focused lookup methods used by DB-primary travel loading,
|
|
* mapping generation, extended refresh scheduling, and retention cleanup.
|
|
*
|
|
* @extends ServiceEntityRepository<TravelSnapshot>
|
|
*/
|
|
class TravelSnapshotRepository extends ServiceEntityRepository
|
|
{
|
|
public function __construct(ManagerRegistry $registry)
|
|
{
|
|
parent::__construct($registry, TravelSnapshot::class);
|
|
}
|
|
|
|
/**
|
|
* Returns the exact snapshot for a travel date and hotel combination.
|
|
*/
|
|
public function findByDateAndHotel(int $dateId, int $hotelId): ?TravelSnapshot
|
|
{
|
|
return $this->findOneBy([
|
|
'dateId' => $dateId,
|
|
'hotelId' => $hotelId,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Returns a fallback snapshot for the given date when hotel-specific lookup misses.
|
|
*/
|
|
public function findFirstByDateId(int $dateId): ?TravelSnapshot
|
|
{
|
|
return $this->createQueryBuilder('s')
|
|
->where('s.dateId = :dateId')
|
|
->setParameter('dateId', $dateId)
|
|
->orderBy('s.id', 'ASC')
|
|
->setMaxResults(1)
|
|
->getQuery()
|
|
->getOneOrNullResult();
|
|
}
|
|
|
|
/**
|
|
* Returns the first known product ID for a travel date, if available.
|
|
*/
|
|
public function findProductIdByDateId(int $dateId): ?int
|
|
{
|
|
$value = $this->createQueryBuilder('s')
|
|
->select('s.productId')
|
|
->where('s.dateId = :dateId')
|
|
->andWhere('s.productId IS NOT NULL')
|
|
->setParameter('dateId', $dateId)
|
|
->setMaxResults(1)
|
|
->getQuery()
|
|
->getOneOrNullResult();
|
|
|
|
if (null === $value) {
|
|
return null;
|
|
}
|
|
|
|
return (int) $value['productId'];
|
|
}
|
|
|
|
/**
|
|
* Returns all snapshots sorted deterministically for mapping aggregation.
|
|
*
|
|
* @return array<int, TravelSnapshot>
|
|
*/
|
|
public function findAllForMapping(?int $limit = null): array
|
|
{
|
|
$qb = $this->createQueryBuilder('s')
|
|
->orderBy('s.dateId', 'ASC')
|
|
->addOrderBy('s.hotelId', 'ASC');
|
|
|
|
if (null !== $limit) {
|
|
$qb->setMaxResults($limit);
|
|
}
|
|
|
|
return $qb->getQuery()->getResult();
|
|
}
|
|
|
|
/**
|
|
* Returns snapshots eligible for extended availability refresh.
|
|
*
|
|
* @return array<int, TravelSnapshot>
|
|
*/
|
|
public function findRefreshCandidates(
|
|
\DateTimeImmutable $dateToThreshold,
|
|
\DateTimeImmutable $refreshBefore,
|
|
int $limit,
|
|
): array {
|
|
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')
|
|
// extendedRefreshedAt IS NULL: never-refreshed snapshots rank as oldest.
|
|
->andWhere('s.extendedRefreshedAt IS NULL OR s.extendedRefreshedAt < :refreshBefore')
|
|
->setParameter('dateToThreshold', $dateToThreshold)
|
|
->setParameter('refreshBefore', $refreshBefore)
|
|
->orderBy('s.extendedRefreshedAt', 'ASC')
|
|
->addOrderBy('s.id', 'ASC')
|
|
->setMaxResults($limit)
|
|
->getQuery()
|
|
->getResult();
|
|
}
|
|
|
|
/**
|
|
* Deletes snapshots whose travel end date is older than the given threshold.
|
|
*/
|
|
public function deleteExpiredSnapshots(\DateTimeImmutable $beforeDate): int
|
|
{
|
|
return (int) $this->createQueryBuilder('s')
|
|
->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')
|
|
->andWhere('s.dateTo < :beforeDate')
|
|
->setParameter('beforeDate', $beforeDate)
|
|
->getQuery()
|
|
->execute();
|
|
}
|
|
}
|