sterntours/src/AppBundle/Entity/TravelPeriodRepository.php
Kevin Adametz 4e71ddabec 12.21
2021-12-25 03:11:08 +01:00

520 lines
No EOL
22 KiB
PHP

<?php
namespace AppBundle\Entity;
use AppBundle\Util;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Query\Expr;
class TravelPeriodRepository extends \Doctrine\ORM\EntityRepository
{
private $departureRepository;
function __construct(EntityManager $em, ClassMetadata $class)
{
parent::__construct($em, $class);
$this->departureRepository = $this->getEntityManager()->getRepository('AppBundle:TravelDeparturePoint');
}
/**
* @param \DateTime $startDate
* @param \DateTime $endDate
* @param null $destinationIds
* @param bool $combi
*
* @return TravelProgram[]|array
* @throws \Exception
* @internal param null $destination
*/
public function getTravelProgramsWithTravelDatesForTimePeriod($startDate, $endDate, $destinationIds = null,
$combi = false)
{
// Idea for natural sort problem:
// Add new column sortable_name
$now = new \DateTime();
if ($startDate < $now)
{
$startDate = $now;
}
$startDateStr = $startDate->format('Y-m-d');
if ($endDate)
{
$endDateStr = $endDate->format('Y-m-d');
}
$qb = $this->getEntityManager()->createQueryBuilder()
->from('AppBundle:TravelProgram', 'tp', 'tp.id')
->addSelect('tp')
->where('tp.status > 0');
// Limit time period for seasons and travel dates
$qb->innerJoin('tp.periods', 'p');
$qb->addSelect('p');
$qb->innerJoin('p.dates', 'd');
$qb->addSelect('d');
$qb->andWhere("((p.isSeason = 0 AND d.startDate >= '$startDateStr' AND d.endDate <= '$endDateStr') OR".
" (p.isSeason = 1 AND d.endDate >= '$startDateStr' AND".
" DATE_ADD(d.startDate, tp.programDuration, 'DAY') <= '$endDateStr' AND p.status > 0))");
// Prices
// Instead of a single join to prices we add one join per price type. This reduces the execution time by
// 150ms on the development system
$priceTypes = $this->getEntityManager()->getRepository('AppBundle:TravelPeriodPriceType')->findAll();
foreach ($priceTypes as $priceType)
{
$priceTypeKey = 'price_'. $priceType->getId();
$qb->leftJoin('p.prices', $priceTypeKey, Expr\Join::WITH,
$priceTypeKey .'.priceType = '. $priceType->getId(), $priceTypeKey .'.priceTypeId');
$qb->addSelect($priceTypeKey);
}
$qb->leftJoin('p.discounts', 'discount');
$qb->addSelect('discount');
$qb->leftJoin('p.departures', 'p_dep', Expr\Join::WITH, 'tp.programType = '.
TravelProgram::MEDIATED_PROGRAM_TYPE);
$qb->addSelect('p_dep');
// Destinations
if (!empty($destinationIds) && is_array($destinationIds))
{
//$qb->innerJoin('AppBundle:TravelProgramCountry', 'tpc', Expr\Join::WITH,
// 'tpc.program = tp AND IDENTITY(tpc.country) IN ('. implode(', ', $destinationIds) .')');
$qb->innerJoin('tp.countries', 'tc', Expr\Join::WITH,
'tc.id IN ('. implode(', ', $destinationIds) .')');
if ($combi)
{
//$qb->having('COUNT(DISTINCT tpc.country) = '. count($destinationIds));
$qb->having('COUNT(DISTINCT tc) = '. count($destinationIds));
}
}
// TODO $qb->groupBy('p.id, p_dep.id, d.id');
// $qb->groupBy('p.id');
// Travel class
$qb->innerJoin('p.class', 'cls', Expr\Join::WITH, 'cls.standard = 1');
// Image
$qb->leftJoin('tp.images', 'tp_image', Expr\Join::WITH, 'tp_image.type = 2')
->addSelect('tp_image');
// Sort travel programs
// $qb->addSelect('COALESCE(tp.position, 0) as HIDDEN position_sort_key');
// $qb->orderBy('position_sort_key');
//$qb->addOrderBy('LENGTH(tp.title)'); // Emulate natural sort
$qb->addOrderBy('tp.title');
$qb->addOrderBy('tp.id');
// Sort: Real travel dates have higher priority than virtual travel dates. Sort travel programs
$qb->addOrderBy('p.isSeason, p.id');
/** @var TravelProgram[]|array $travelPrograms */
$travelPrograms = $qb->getQuery()->getResult();
if (empty($travelPrograms))
{
return $travelPrograms;
}
// Collect arrival point IDs for non mediated travel programs
$isUsedArrivalPointById = [];
foreach ($travelPrograms as $travelProgram)
{
$isUsedTravelProgramById[$travelProgram->getId()] = true;
if (!$travelProgram->getIsMediated())
{
$isUsedArrivalPointById[$travelProgram->getTravelArrivalPoint()->getId()] = true;
}
}
$usedArrivalPointIds = array_keys($isUsedArrivalPointById);
// Find flight periods and related departures
$flightPeriods = empty($isUsedArrivalPointById) ? []
: $this->getEntityManager()->getRepository('AppBundle:FlightPeriod')
->getIndexedFlightPeriodsForTimePeriod($startDate, $endDate, $usedArrivalPointIds);
// Find default departures and classify by-program or by-arrival-point
// We could've simply left joined them to get an equal result. But we're reducing the number of rows returned
// in the first travel program query above drastically with this solution. This doesn't hurt performance.
// Of course, we have to link departures of travel programs manually later.
$defDepsByArrivalPointId = [];
$qb = $this->getEntityManager()->createQueryBuilder()
->from('AppBundle:TravelDeparturePoint', 'dep')
->addSelect('dep')
->where($qb->expr()->in('IDENTITY(dep.program)', array_keys($travelPrograms)));
if (!empty($isUsedArrivalPointById))
{
$qb->orWhere($qb->expr()->in('IDENTITY(dep.travelArrivalPoint)', $usedArrivalPointIds));
}
/** @var TravelDeparturePoint[]|array $defaultDepartures */
$defaultDepartures = $qb->getQuery()->execute();
foreach ($defaultDepartures as $defaultDeparture)
{
if ($defaultDeparture->getProgram())
{
$travelProgram = $travelPrograms[$defaultDeparture->getProgram()->getId()];
if ($travelProgram->getDepartures() instanceof PersistentCollection)
{
Util::reAttachRelatedCollection($travelProgram, 'departures', $travelProgram->getDepartures()->unwrap());
}
$travelProgram->addDeparture($defaultDeparture);
}
elseif ($defaultDeparture->getTravelArrivalPoint())
{
if (!isset($defDepsByArrivalPointId[$defaultDeparture->getTravelArrivalPoint()->getId()]))
{
$defDepsByArrivalPointId[$defaultDeparture->getTravelArrivalPoint()->getId()] = [];
}
$defDepsByArrivalPointId[$defaultDeparture->getTravelArrivalPoint()->getId()][] = $defaultDeparture;
}
}
foreach ($travelPrograms as $k => $travelProgram)
{
$flightPeriod = null;
if (!$travelProgram->getIsMediated())
{
$arrivalPointId = $travelProgram->getTravelArrivalPoint()->getId();
// Manually link separately fetched default departures
$travelProgram->getTravelArrivalPoint()->__setDepartures(
isset($defDepsByArrivalPointId[$arrivalPointId])
? $defDepsByArrivalPointId[$arrivalPointId]
: []
);
}
$this->addTravelDatesToProgram($travelProgram, $travelProgram->getPeriods(), $flightPeriods,
$startDate, $endDate);
if (!$travelProgram->hasTravelDates())
{
unset($travelPrograms[$k]);
}
}
return $travelPrograms;
}
private function createTravelDateKey(\DateTime $startDate, \DateTime $endDate)
{
return $startDate->format('Y-m-d') . $endDate->format('Y-m-d');
}
const TD_QUERY_NON_VIRTUAL = 0x1;
const TD_QUERY_VIRTUAL = 0x2;
const TD_QUERY_INACTIVE = 0x4;
const TD_QUERY_OUTDATED = 0x8;
const TD_QUERY_ACP = self::TD_QUERY_INACTIVE | self::TD_QUERY_OUTDATED;
const TD_QUERY_VIRTUAL_AND_NON_VIRTUAL = self::TD_QUERY_VIRTUAL | self::TD_QUERY_NON_VIRTUAL;
/**
* @param TravelProgram $program
* @param bool $class
* @param int $flags
*
* @return TravelDate[]|array
* @todo Find a more appropriate name for this method
*/
public function getTrueTravelPeriods(TravelProgram $program, $class = false, $flags =
self::TD_QUERY_VIRTUAL_AND_NON_VIRTUAL, $start = false)
{
if (!($flags & self::TD_QUERY_VIRTUAL_AND_NON_VIRTUAL))
{
return [];
}
$doQueryVirtualAndNonVirtual = $flags & self::TD_QUERY_VIRTUAL_AND_NON_VIRTUAL ==
self::TD_QUERY_VIRTUAL_AND_NON_VIRTUAL;
/** @var TravelPeriod[] $periods */
$qb = $this->createQueryBuilder('p');
$qb
//->from('AppBundle:TravelPeriod', 'tpp', 'key')
->leftJoin('p.dates', 'd')->addSelect('d')
->leftJoin('p.prices', 'price', null, null, 'price.priceTypeId')
//->innerJoin('price.priceType', 'price_type_')
->addSelect('price')
->leftJoin('p.discounts', 'discount', Expr\Join::WITH,
'discount.start <= CURRENT_TIMESTAMP() AND discount.end >= CURRENT_TIMESTAMP()')->addSelect('discount')
->where('IDENTITY(p.program) = '. $program->getId())
;
if ($program->getIsMediated())
{
// Only mediated travel programs define departures in travelPeriods
$qb->leftJoin('p.departures', 'p_dep')->addSelect('p_dep');
}
elseif (!($flags & self::TD_QUERY_VIRTUAL))
{
// Retrieving all flight periods by join is only possible, if virtual entries are excluded
$qb->leftJoin('AppBundle:FlightPeriod', 'fp', Expr\Join::WITH, 'IDENTITY(fp.travelArrivalPoint) = '.
':travelArrivalPointId AND d.startDate = fp.startDate AND d.endDate = fp.endDate');
$qb->setParameter('travelArrivalPointId', $program->getTravelArrivalPoint()->getId());
$qb->addSelect('fp');
$qb->leftJoin('fp.departures', 'fp_dep')->addSelect('fp_dep');
}
$startDate = null;
if (!($flags & self::TD_QUERY_OUTDATED))
{
if($start && $start == 'start_week'){
$startDate = new \DateTime('+3 week');
}else{
$startDate = new \DateTime('tomorrow');
}
}
if ($class)
{
$qb->andWhere($qb->expr()->eq('IDENTITY(tpp.class)', $class));
}
if ($doQueryVirtualAndNonVirtual)
{
$qb->addOrderBy('p.isSeason');
}
else
{
$qb->andWhere($qb->expr()->eq('p.isSeason', $flags & self::TD_QUERY_VIRTUAL));
if(!($flags & self::TD_QUERY_INACTIVE))
{
// If both, non virtual and virtual entries are selected, we cannot exclude inactive entries, because
// there may be a non virtual travel date with inactive status and a matching virtual travel date with
// active status. In this case, the inactive status would get lost and the virtual travel date would
// wrongly be included in the result. Therefore we are removing inactive travel dates later in the
// TD_QUERY_VIRTUAL_AND_NON_VIRTUAL case.
$qb->andWhere($qb->expr()->gt('tpp.status', 0));
}
}
$qb
->addOrderBy('p.order', 'ASC')
->addOrderBy('d.startDate', 'ASC')
->addOrderBy('p.name', 'ASC')
;
$entities = $qb->getQuery()->execute();
$flightPeriodByKey = null;
if (!$program->getIsMediated())
{
if (!$program->getTravelArrivalPoint())
{
return [];
}
if ($flags & self::TD_QUERY_VIRTUAL)
{
// If virtual entries are included, we have to fetch all flight periods, because we don't know
// the actual dates yet
$flightPeriodByKey = $this->getEntityManager()->getRepository('AppBundle:FlightPeriod')
->getIndexedFlightPeriodsForTimePeriod($startDate, null, $program->getTravelArrivalPoint()->getId());
}
foreach ($entities as $key => $entity)
{
if ($entity == null)
{
unset($entities[$key]);
}
else if ($entity instanceof FlightPeriod)
{
// We are joining to flight period with multiple keys. Doctrine cannot handle this and returns a
// mixed list containing both, travel and flight periods at the same level. We fix that here.
$flightPeriodByKey[$entity->getStartDate()->format('Y-m-d') .'_'.
$entity->getEndDate()->format('Y-m-d')] = $entity;
unset($entities[$key]);
}
}
/** @var TravelPeriod $period */
foreach ($entities as &$period)
{
foreach ($period->getDates() as &$date)
{
$fpKey = $date->getStartDate()->format('Y-m-d') . $date->getEndDate()->format('Y-m-d') .
$program->getTravelArrivalPoint()->getId();
if (isset($flightPeriodByKey[$fpKey]))
{
// #TODO Does this cause performance problems?
$date->setFlightPeriod($flightPeriodByKey[$fpKey]);
}
}
}
}
$this->addTravelDatesToProgram($program, $entities, $flightPeriodByKey, $startDate, null, $start);
return $program->getTravelDates($start);
}
private $currencyFactor = null;
public function getCurrencyFactor()
{
if ($this->currencyFactor == null)
{
// #TODO Enable doctrine 2nd level cache instead of implementing own caching mechanism
$dollar = $this->getEntityManager()->getRepository('AppBundle:TravelSetting')->findOneBy(['key' => 'dollar']);
if (!$dollar)
{
throw new \Exception('Missing currency factor setting "dollar" in table travel_setting');
}
$this->currencyFactor = $dollar->getValue() ?? 1;
}
return $this->currencyFactor;
}
/**
* @param TravelProgram $travelProgram
* @param array|TravelPeriod[] $travelPeriods Represent seasons and travel dates
* @param array|FlightPeriod[] $flightPeriods For performance reasons, $flightPeriods must be pre-fetched
* @param \DateTime|null $startDate If not null, only add travel dates later than this value
* @param \DateTime|null $endDate If not null, only add travel dates earlier than this value
*
* @throws \Exception
*/
public function addTravelDatesToProgram(TravelProgram &$travelProgram, $travelPeriods, $flightPeriods,
\DateTime $startDate = null, \DateTime $endDate = null, $header = null)
{
$currencyFactor = $travelProgram->getNettoPricesInEuro() ? 1 : $this->getCurrencyFactor();
$counters = array();
// #TODO Consider adding travelPeriods to travelProgram in the search algorithm
//foreach ($travelProgram->getPeriods() as $travelPeriod)
$counter = 0;
foreach ($travelPeriods as $travelPeriod)
{
if ($travelPeriod->getIsSeason())
{
foreach ($travelPeriod->getDates() as $travelPeriodDate)
{
$cn = $travelPeriodDate->getId().$travelPeriod->getName();
if(empty($counters[$cn])){
$counters[$cn] = 1;
}
$seasonEndDate = clone $travelPeriodDate->getEndDate();
$seasonEndDate->modify('+'.$travelProgram->getProgramDuration().' day');
if ($endDate != null && $seasonEndDate > $endDate)
{
// Limit end date to requested latest travel end date
$seasonEndDate = clone $endDate;
}
// Subtract temporarily added days which were added above. (Also subtract if date was limited!)
// Add one day to include season end date in $dates array below (would be excluded otherwise)
$seasonEndDate->modify('-'. ($travelProgram->getProgramDuration() - 1) .' day');
$dates = new \DatePeriod($travelPeriodDate->getStartDate(),
\DateInterval::createFromDateString('1 day'), $seasonEndDate);
$doTestStartDate = $startDate != null;
/** @var \DateTime $date */
foreach ($dates as $date)
{
$isPossibleDate = $travelProgram->getIsAvailWeekday($date->format('w'));
// $doTestStartDate helps to improve performance by avoiding unnecessary (more expensive)
// "$date < $startDate" checks: As soon as $date >= $startDate the first time, it will stay
// this way until the foreach iteration has finished.
if (!($doTestStartDate && $date < $startDate))
{
$doTestStartDate = false;
if ($isPossibleDate)
{
// #TODO Do we need the travel date key?
$travelDateEnd = (clone $date)->modify('+'.$travelProgram->getProgramDuration().' day');
$travelDateKey = $this->createTravelDateKey($date, $travelDateEnd);
if (!$travelProgram->hasTravelDate($travelDateKey, $header))
{
$flightPeriod = null;
if (!$travelProgram->getIsMediated())
{
$flightPeriodKey = $travelDateKey .
$travelProgram->getTravelArrivalPoint()->getId();
$flightPeriod = $flightPeriods[$flightPeriodKey] ?? null;
}
$travelProgram->addTravelDateFromSeasonTravelPeriod(
$travelDateKey,
$travelPeriod,
$travelPeriodDate->getId() . $travelPeriod->getName() . $counters[$cn],
$date,
$travelDateEnd,
$flightPeriod,
$currencyFactor,
$header
);
}
}
}
// Also increment $i if the date is theoretically possible but excluded from the search request
if ($isPossibleDate)
{
++ $counters[$cn];
}
}
}
}
elseif (count($travelPeriod->getDates()) && $travelProgram->getIsPossibleStartDate($travelPeriod->getStartDate()))
{
$travelDateKey = $this->createTravelDateKey($travelPeriod->getStartDate(), $travelPeriod->getEndDate());
$flightPeriod = null;
if (!$travelProgram->getIsMediated())
{
$flightPeriod = $flightPeriods[$travelDateKey . $travelProgram->getTravelArrivalPoint()->getId()]
?? null;
}
// #TODO There is an error in the old backend which causes duplicates
if ($travelProgram->hasTravelDate($travelDateKey, $header) &&
$travelProgram->getTravelDate($travelDateKey, $header)->__getTravelPeriod()->getId() != $travelPeriod->getId())
{
global $kernel;
if($kernel instanceOf \AppCache) $kernel = $kernel->getKernel();
$kernel->getContainer()->get('logger')->warn('Duplicate travel period found with name "'.
$travelPeriod->getName() .'"');
}
else
{
$TravelDate = $travelProgram->addTravelDateFromNonSeasonTravelPeriod($travelDateKey, $travelPeriod, $flightPeriod,
$currencyFactor, $header);
}
}
}
}
/**
* "Default departures" are taken if a travel period date has no departure itself. For mediated travel programs
* default departures are defined at travel program level. For self-organized travel programs they are defined
* at travel-arrival-point (airport) level.
*
* @param TravelProgram $program
* @param bool $acp
*
* @return TravelDeparturePoint[]|array|\Doctrine\Common\Collections\Collection|mixed
*/
public function getDefaultDeparturesByProgram(TravelProgram $program, $acp = false)
{
if ($program->getIsMediated())
{
return $program->getDepartures();
}
if ($program->getTravelArrivalPoint() == null)
{
return [];
}
$defaultDepartures = $program->getTravelArrivalPoint()->getDepartures();
if (!$acp)
{
$defaultDepartures = $this->departureRepository->limitIndividualArrivalPriceInDepartures(
$defaultDepartures, $program->getDefaultFlightPrice());
}
return $defaultDepartures;
}
}