feat: improved contingents api performance with db backed snapshots

This commit is contained in:
Björn Fromme
2026-08-20 11:46:03 +02:00
parent d5f9f4ef10
commit 351fbab498
20 changed files with 1521 additions and 109 deletions
@@ -60,4 +60,20 @@ class AccommodationRepository extends ServiceEntityRepository
->getOneOrNullResult()
;
}
/**
* Every accommodation that is addressable by a calendar hotel code, ordered for stable output.
*
* @return Accommodation[]
*/
public function findAllWithCalendarCode(): array
{
return $this->createQueryBuilder('a')
->andWhere('a.calendarCode IS NOT NULL')
->andWhere("a.calendarCode != ''")
->orderBy('a.calendarCode', 'ASC')
->getQuery()
->getResult()
;
}
}
@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Repository\Groups;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\ContingentDay;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ContingentDay>
*/
class ContingentDayRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ContingentDay::class);
}
/**
* Returns the snapshot days within [dateFrom, dateTo] for the accommodation
* identified by the given hotel code, keyed by Y-m-d.
*
* @return array<string, ContingentDay>
*/
public function findByHotelCodeAndDateRange(
string $hotelCode,
\DateTimeImmutable $dateFrom,
\DateTimeImmutable $dateTo,
): array {
/** @var ContingentDay[] $days */
$days = $this->createQueryBuilder('cd')
->join('cd.accommodation', 'a')
->where('a.calendarCode = :hotelCode')
->andWhere('cd.date >= :dateFrom')
->andWhere('cd.date <= :dateTo')
->setParameter('hotelCode', $hotelCode)
->setParameter('dateFrom', $dateFrom)
->setParameter('dateTo', $dateTo)
->orderBy('cd.date', 'ASC')
->getQuery()
->getResult();
return $this->indexByDate($days);
}
/**
* Same as above but for a known accommodation; used by the sync to diff against.
*
* @return array<string, ContingentDay>
*/
public function findByAccommodationAndDateRange(
Accommodation $accommodation,
\DateTimeImmutable $dateFrom,
\DateTimeImmutable $dateTo,
): array {
/** @var ContingentDay[] $days */
$days = $this->createQueryBuilder('cd')
->where('cd.accommodation = :accommodation')
->andWhere('cd.date >= :dateFrom')
->andWhere('cd.date <= :dateTo')
->setParameter('accommodation', $accommodation)
->setParameter('dateFrom', $dateFrom)
->setParameter('dateTo', $dateTo)
->orderBy('cd.date', 'ASC')
->getQuery()
->getResult();
return $this->indexByDate($days);
}
/**
* Returns the accommodation's complete stored snapshot as an ordered "Y-m-d:STATUS" list.
*
* Scalar hydration keeps the fingerprint cheap: the entities themselves are of no interest here.
*
* @return list<string>
*/
public function findStatusFingerprintParts(Accommodation $accommodation): array
{
/** @var array<int, array{date: \DateTimeImmutable, status: \App\BpnConnect\Model\ContingentStatus}> $rows */
$rows = $this->createQueryBuilder('cd')
->select('cd.date', 'cd.status')
->where('cd.accommodation = :accommodation')
->setParameter('accommodation', $accommodation)
->orderBy('cd.date', 'ASC')
->getQuery()
->getArrayResult();
return array_map(
static fn (array $row) => $row['date']->format('Y-m-d').':'.$row['status']->value,
$rows,
);
}
/**
* Retention: drops snapshot days that are in the past and can no longer be requested.
*/
public function deleteBefore(\DateTimeImmutable $date): int
{
return (int) $this->createQueryBuilder('cd')
->delete()
->where('cd.date < :date')
->setParameter('date', $date)
->getQuery()
->execute();
}
/**
* @param ContingentDay[] $days
*
* @return array<string, ContingentDay>
*/
private function indexByDate(array $days): array
{
$indexed = [];
foreach ($days as $day) {
$indexed[$day->getDate()->format('Y-m-d')] = $day;
}
return $indexed;
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Repository\Groups;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\ContingentSyncState;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ContingentSyncState>
*/
class ContingentSyncStateRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ContingentSyncState::class);
}
public function findOneByAccommodation(Accommodation $accommodation): ?ContingentSyncState
{
return $this->findOneBy(['accommodation' => $accommodation]);
}
/**
* Returns every sync state keyed by accommodation id, for the revisions endpoint.
*
* @return array<int, ContingentSyncState>
*/
public function findAllIndexedByAccommodationId(): array
{
$indexed = [];
foreach ($this->findAll() as $state) {
$accommodation = $state->getAccommodation();
if (null === $accommodation || null === $accommodation->getId()) {
continue;
}
$indexed[$accommodation->getId()] = $state;
}
return $indexed;
}
}