$query */ public static function matchingStart(string $alias, array $query): string { global $wpdb; $where = ["ds.event_id={$alias}.event_id", "ds.revision={$alias}.schedule_revision"]; $params = []; if (! empty($query['schedule_local_start'])) { $where[] = 'ds.end_local > %s'; $params[] = $query['schedule_local_start']; } elseif (! empty($query['start'])) { $where[] = 'ds.end_utc > %s'; $params[] = self::utc((string) $query['start']); } if (! empty($query['schedule_local_end'])) { $where[] = 'ds.start_local <= %s'; $params[] = $query['schedule_local_end']; } elseif (! empty($query['end'])) { $where[] = 'ds.start_utc <= %s'; $params[] = self::utc((string) $query['end']); } if (isset($query['past_events']) && ! $query['past_events']) { $where[] = 'ds.end_utc > %s'; $params[] = gmdate('Y-m-d H:i:s'); } if (($query['weekday'] ?? -1) >= 0) { $where[] = 'WEEKDAY(ds.start_local) = %d'; $params[] = (int) $query['weekday']; } foreach (['time_from' => '>=', 'time_to' => '<='] as $key => $operator) { if (! empty($query[$key])) { $where[] = "TIME(ds.start_local) {$operator} %s"; $params[] = $query[$key] . ($key === 'time_to' ? ':59' : ':00'); } } $sql = '(SELECT MIN(ds.start_utc) FROM ' . $wpdb->prefix . 'pcwp_daily_intervals ds WHERE ' . implode(' AND ', $where) . ')'; return $params === [] ? $sql : $wpdb->prepare($sql, ...$params); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- All values are parameterized; identifiers are controlled by the caller and WordPress table prefix. } private static function utc(string $value): string { try { return (new DateTimeImmutable($value))->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s'); } catch (\Exception) { return gmdate('Y-m-d H:i:s'); } } /** Expand only the already-selected events, with one batched projection read. * @param list $occurrences * @param array $query * @return list */ public static function calendarIntervals(array $occurrences, array $query): array { if ($occurrences === []) { return []; } global $wpdb; $ids = array_map(static fn (OccurrenceData $item): int => $item->eventId, $occurrences); $placeholders = implode(',', array_fill(0, count($ids), '%d')); $rows = $wpdb->get_results($wpdb->prepare("SELECT ds.* FROM {$wpdb->prefix}pcwp_daily_intervals ds INNER JOIN {$wpdb->prefix}pcwp_occurrences o ON o.event_id=ds.event_id AND o.schedule_revision=ds.revision WHERE ds.event_id IN ({$placeholders}) ORDER BY ds.start_utc,ds.slot_id", ...$ids)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Only WordPress identifiers and generated placeholders are interpolated. $slotsByEvent = []; foreach ($rows as $row) { $slotsByEvent[(int) $row->event_id][] = $row; } $result = []; foreach ($occurrences as $occurrence) { if (! isset($slotsByEvent[$occurrence->eventId])) { $result[] = $occurrence; continue; } foreach ($slotsByEvent[$occurrence->eventId] as $slot) { $zone = new DateTimeZone($occurrence->timezone); $time = new EventTime(new DateTimeImmutable($slot->start_utc, new DateTimeZone('UTC')), new DateTimeImmutable($slot->end_utc, new DateTimeZone('UTC')), $occurrence->timezone); $time = new EventTime($time->start->setTimezone($zone), $time->end->setTimezone($zone), $occurrence->timezone); $beforeRange = ! empty($query['schedule_local_start']) ? $time->end->format('Y-m-d H:i:s') <= $query['schedule_local_start'] : (! empty($query['start']) && $time->endUtc()->format('Y-m-d H:i:s') <= self::utc($query['start'])); $afterRange = ! empty($query['schedule_local_end']) ? $time->start->format('Y-m-d H:i:s') > $query['schedule_local_end'] : (! empty($query['end']) && $time->startUtc()->format('Y-m-d H:i:s') > self::utc($query['end'])); if ( $beforeRange || $afterRange || (isset($query['past_events']) && ! $query['past_events'] && $time->endUtc()->getTimestamp() <= time()) || (($query['weekday'] ?? -1) >= 0 && (int) $time->start->format('N') - 1 !== (int) $query['weekday']) || (! empty($query['time_from']) && $time->start->format('H:i') < $query['time_from']) || (! empty($query['time_to']) && $time->start->format('H:i') > $query['time_to']) ) { continue; } $result[] = new OccurrenceData($occurrence->id, $occurrence->eventId, $occurrence->seriesId, $time->startUtc(), $time->endUtc(), $time->start, $time->end, $occurrence->timezone, false, $occurrence->status, $slot->slot_id); } } usort($result, static fn (OccurrenceData $a, OccurrenceData $b): int => $a->startUtc <=> $b->startUtc); return $result; } /** @param list $occurrences * @return array> */ public static function byDay(array $occurrences, DateTimeImmutable $from, DateTimeImmutable $until): array { $days = []; foreach ($occurrences as $item) { if ($item->slotId === '') { $days[$item->startLocal->format('Y-m-d')][] = $item; continue; } for ($date = $item->startLocal->setTime(0, 0); $date < $item->endLocal; $date = $date->modify('+1 day')) { $key = $date->format('Y-m-d'); if ($key >= $from->format('Y-m-d') && $key <= $until->format('Y-m-d')) { $days[$key][] = $item; } } } return $days; } }