520 lines
No EOL
22 KiB
PHP
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;
|
|
}
|
|
} |