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; } }