diff --git a/inc/Blocks/Calendar/Pagination/PageBoundary.php b/inc/Blocks/Calendar/Pagination/PageBoundary.php index 42053484..e23ad38f 100644 --- a/inc/Blocks/Calendar/Pagination/PageBoundary.php +++ b/inc/Blocks/Calendar/Pagination/PageBoundary.php @@ -12,8 +12,6 @@ namespace DataMachineEvents\Blocks\Calendar\Pagination; -use DataMachineEvents\Blocks\Calendar\Cache\CalendarCache; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -23,170 +21,6 @@ class PageBoundary { const DAYS_PER_PAGE = 5; const MIN_EVENTS_FOR_PAGINATION = 20; - /** - * Get unique event dates for pagination calculations (cached). - * - * Multi-day events are expanded to count on each spanned date. - * - * @deprecated Use CalendarAbilities::get_unique_event_dates() instead. Moved in 0.16.0. - * - * @param array $params Query parameters. - * @return array { - * @type array $dates Ordered array of unique date strings (Y-m-d). - * @type int $total_events Total number of matching events. - * @type array $events_per_date Event counts keyed by date. - * } - */ - public static function get_unique_event_dates( array $params ): array { - $cache_key = CalendarCache::generate_key( $params, 'dates' ); - $cached = CalendarCache::get( $cache_key ); - - if ( false !== $cached ) { - return $cached; - } - - $result = self::compute_unique_event_dates( $params ); - - CalendarCache::set( $cache_key, $result, CalendarCache::TTL_DATES ); - - return $result; - } - - /** - * Compute unique event dates (uncached). - * - * Fetches start/end dates (without post IDs) and uses DATE() in SQL - * to minimize data transfer. Multi-day events are properly expanded - * to count on each spanned date. This reduces cold query time from - * ~960ms to ~500ms at 25,000 events by eliminating the ID column - * and gmdate() parsing in PHP. - * - * @deprecated Use CalendarAbilities::compute_unique_event_dates() instead. Moved in 0.16.0. - * - * @param array $params Query parameters. - * @return array Event dates data. - */ - private static function compute_unique_event_dates( array $params ): array { - global $wpdb; - - $show_past_param = $params['show_past'] ?? false; - $current_date = current_time( 'Y-m-d' ); - $ed_table = \DataMachineEvents\Core\EventDatesTable::table_name(); - - // Build WHERE clauses from params for taxonomy/location filtering. - $where_clauses = array( - "p.post_type = 'data_machine_events'", - "p.post_status = 'publish'", - ); - $join_clauses = array(); - $query_values = array(); - - if ( ! $show_past_param ) { - $where_clauses[] = 'ed.start_datetime >= %s'; - $query_values[] = $current_date . ' 00:00:00'; - } - - // Handle taxonomy archive filter (any taxonomy: artist, venue, location, etc.). - $archive_taxonomy = $params['archive_taxonomy'] ?? ''; - $archive_term_id = $params['archive_term_id'] ?? 0; - - if ( $archive_taxonomy && $archive_term_id ) { - $join_clauses[] = "INNER JOIN {$wpdb->term_relationships} tr_archive ON p.ID = tr_archive.object_id"; - $join_clauses[] = "INNER JOIN {$wpdb->term_taxonomy} tt_archive ON tr_archive.term_taxonomy_id = tt_archive.term_taxonomy_id"; - $where_clauses[] = 'tt_archive.taxonomy = %s'; - $query_values[] = $archive_taxonomy; - $where_clauses[] = 'tt_archive.term_id = %d'; - $query_values[] = (int) $archive_term_id; - } - - // Handle additional taxonomy filters from the filter bar. - $tax_filters = $params['tax_filters'] ?? array(); - $filter_index = 0; - foreach ( $tax_filters as $taxonomy_slug => $term_ids ) { - if ( empty( $term_ids ) || ! is_array( $term_ids ) ) { - continue; - } - - $alias_tr = 'tr_filter_' . $filter_index; - $alias_tt = 'tt_filter_' . $filter_index; - - $join_clauses[] = "INNER JOIN {$wpdb->term_relationships} {$alias_tr} ON p.ID = {$alias_tr}.object_id"; - $join_clauses[] = "INNER JOIN {$wpdb->term_taxonomy} {$alias_tt} ON {$alias_tr}.term_taxonomy_id = {$alias_tt}.term_taxonomy_id"; - $where_clauses[] = "{$alias_tt}.taxonomy = %s"; - $query_values[] = sanitize_key( $taxonomy_slug ); - - $placeholders = implode( ', ', array_fill( 0, count( $term_ids ), '%d' ) ); - $where_clauses[] = "{$alias_tt}.term_id IN ({$placeholders})"; - foreach ( $term_ids as $term_id ) { - $query_values[] = (int) $term_id; - } - - ++$filter_index; - } - - $joins = implode( ' ', $join_clauses ); - $where = implode( ' AND ', $where_clauses ); - - // Fetch start/end dates without IDs — DATE() in SQL avoids gmdate() in PHP. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $rows = $wpdb->get_results( - empty( $query_values ) - ? "SELECT DATE(ed.start_datetime) AS start_date, DATE(ed.end_datetime) AS end_date - FROM {$wpdb->posts} p - INNER JOIN {$ed_table} ed ON p.ID = ed.post_id - {$joins} - WHERE {$where} - ORDER BY ed.start_datetime ASC" - : $wpdb->prepare( - "SELECT DATE(ed.start_datetime) AS start_date, DATE(ed.end_datetime) AS end_date - FROM {$wpdb->posts} p - INNER JOIN {$ed_table} ed ON p.ID = ed.post_id - {$joins} - WHERE {$where} - ORDER BY ed.start_datetime ASC", - ...$query_values - ) - ); - - $total_events = count( $rows ); - $events_per_date = array(); - - foreach ( $rows as $row ) { - $events_per_date[ $row->start_date ] = ( $events_per_date[ $row->start_date ] ?? 0 ) + 1; - - // Multi-day: expand to each spanned date after the start. - if ( $row->end_date && $row->end_date > $row->start_date ) { - $current = new \DateTime( $row->start_date ); - $current->modify( '+1 day' ); - $end_dt = new \DateTime( $row->end_date ); - - while ( $current <= $end_dt ) { - $date = $current->format( 'Y-m-d' ); - - if ( ! $show_past_param && $date < $current_date ) { - $current->modify( '+1 day' ); - continue; - } - - $events_per_date[ $date ] = ( $events_per_date[ $date ] ?? 0 ) + 1; - $current->modify( '+1 day' ); - } - } - } - - if ( $show_past_param ) { - krsort( $events_per_date ); - } else { - ksort( $events_per_date ); - } - - return array( - 'dates' => array_keys( $events_per_date ), - 'total_events' => $total_events, - 'events_per_date' => $events_per_date, - ); - } - /** * Get date boundaries for a specific page. * diff --git a/inc/Blocks/Calendar/Query/UpcomingFilter.php b/inc/Blocks/Calendar/Query/UpcomingFilter.php index bcc1b0f4..55102a35 100644 --- a/inc/Blocks/Calendar/Query/UpcomingFilter.php +++ b/inc/Blocks/Calendar/Query/UpcomingFilter.php @@ -4,7 +4,7 @@ * * Single source of truth for the SQL conditions that determine whether * an event is upcoming or past. All consumers (DateFilter, EventDateQueryAbilities, - * FilterAbilities, Taxonomy\Helper) delegate here. + * FilterAbilities) delegate here. * * Definition: * upcoming = start >= $datetime OR end >= $datetime diff --git a/inc/Blocks/Calendar/Taxonomy/Helper.php b/inc/Blocks/Calendar/Taxonomy/Helper.php deleted file mode 100644 index 4f75b52b..00000000 --- a/inc/Blocks/Calendar/Taxonomy/Helper.php +++ /dev/null @@ -1,323 +0,0 @@ -name, $excluded_taxonomies, true ) || ! $taxonomy->public ) { - continue; - } - - $terms_hierarchy = self::get_taxonomy_hierarchy( $taxonomy->name, null, $date_context, $active_filters, $tax_query_override ); - - if ( ! empty( $terms_hierarchy ) ) { - $taxonomies_data[ $taxonomy->name ] = array( - 'label' => $taxonomy->label, - 'name' => $taxonomy->name, - 'hierarchical' => $taxonomy->hierarchical, - 'terms' => $terms_hierarchy, - ); - } - } - - return $taxonomies_data; - } - - /** - * Get terms in a taxonomy filtered by allowed term IDs. - * - * @deprecated Use the private get_taxonomy_hierarchy() method on FilterAbilities. - * @see \DataMachineEvents\Abilities\FilterAbilities - * - * @param string $taxonomy_slug Taxonomy to get terms for. - * @param array|null $allowed_term_ids Limit to these term IDs, or null for all. - * @param array $date_context Optional date filtering context. - * @param array $active_filters Optional active taxonomy filters for cross-filtering. - * @param array|null $tax_query_override Optional taxonomy query override. - * @return array Hierarchical term structure with event counts. - */ - public static function get_taxonomy_hierarchy( $taxonomy_slug, $allowed_term_ids = null, $date_context = array(), $active_filters = array(), $tax_query_override = null ) { - $terms = get_terms( - array( - 'taxonomy' => $taxonomy_slug, - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC', - ) - ); - - if ( is_wp_error( $terms ) || empty( $terms ) ) { - return array(); - } - - if ( null !== $allowed_term_ids && empty( $allowed_term_ids ) ) { - return array(); - } - - $term_counts = self::get_batch_term_counts( $taxonomy_slug, $date_context, $active_filters, $tax_query_override ); - - $terms_with_events = array(); - foreach ( $terms as $term ) { - if ( null !== $allowed_term_ids && ! in_array( $term->term_id, $allowed_term_ids, true ) ) { - continue; - } - - $event_count = $term_counts[ $term->term_id ] ?? 0; - if ( $event_count > 0 ) { - $term->event_count = $event_count; - $terms_with_events[] = $term; - } - } - - if ( empty( $terms_with_events ) ) { - return array(); - } - - $taxonomy_obj = get_taxonomy( $taxonomy_slug ); - if ( $taxonomy_obj && $taxonomy_obj->hierarchical ) { - return self::build_hierarchy_tree( $terms_with_events ); - } - - return array_map( - function ( $term ) { - return array( - 'term_id' => $term->term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'event_count' => $term->event_count, - 'level' => 0, - 'children' => array(), - ); - }, - $terms_with_events - ); - } - - /** - * Get event counts for all terms in a taxonomy with a single query. - * - * Always joins term_relationships → event_dates directly (no posts - * table). event_dates.post_status is authoritative and only event-typed - * posts ever get rows in that table, so filtering on - * ed.post_status = 'publish' is sufficient to guarantee correctness - * while avoiding the expensive posts join. - * - * @param string $taxonomy_slug Taxonomy to count events for. - * @param array $date_context Optional date filtering context. - * @param array $active_filters Optional active taxonomy filters for cross-filtering. - * @param array|null $tax_query_override Optional taxonomy query override. - * @return array Term ID => event count mapping. - */ - public static function get_batch_term_counts( $taxonomy_slug, $date_context = array(), $active_filters = array(), $tax_query_override = null ) { - global $wpdb; - - $ed_table = EventDatesTable::table_name(); - - // Base join: term_relationships → event_dates (no posts hop). - // ed.post_status carries authoritative status for the event post type. - $joins = "INNER JOIN {$ed_table} ed ON tr.object_id = ed.post_id"; - $where_clauses = " AND ed.post_status = 'publish'"; - $params = array( $taxonomy_slug ); - - if ( ! empty( $date_context ) ) { - $date_start = $date_context['date_start'] ?? ''; - $date_end = $date_context['date_end'] ?? ''; - $show_past = ! empty( $date_context['past'] ) && '1' === $date_context['past']; - $current_datetime = current_time( 'mysql' ); - - if ( ! empty( $date_start ) && ! empty( $date_end ) ) { - $where_clauses .= ' AND (ed.start_datetime >= %s AND ed.start_datetime <= %s)'; - $params[] = $date_start . ' 00:00:00'; - $params[] = $date_end . ' 23:59:59'; - } elseif ( $show_past ) { - $where_clauses .= ' AND (ed.start_datetime < %s AND (ed.end_datetime < %s OR ed.end_datetime IS NULL))'; - $params[] = $current_datetime; - $params[] = $current_datetime; - } else { - $where_clauses .= ' AND (ed.start_datetime >= %s OR ed.end_datetime >= %s)'; - $params[] = $current_datetime; - $params[] = $current_datetime; - } - } - - if ( ! empty( $tax_query_override ) && is_array( $tax_query_override ) ) { - $base_join_index = 0; - foreach ( $tax_query_override as $clause ) { - $base_taxonomy = sanitize_key( $clause['taxonomy'] ?? '' ); - $base_terms = array_map( 'absint', (array) ( $clause['terms'] ?? array() ) ); - - if ( ! $base_taxonomy || empty( $base_terms ) ) { - continue; - } - - $placeholders = implode( ',', array_fill( 0, count( $base_terms ), '%d' ) ); - $alias_tr = "base_tr_{$base_join_index}"; - $alias_tt = "base_tt_{$base_join_index}"; - - $joins .= " INNER JOIN {$wpdb->term_relationships} {$alias_tr} ON tr.object_id = {$alias_tr}.object_id"; - $joins .= " INNER JOIN {$wpdb->term_taxonomy} {$alias_tt} ON {$alias_tr}.term_taxonomy_id = {$alias_tt}.term_taxonomy_id"; - - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $where_clauses .= " AND {$alias_tt}.taxonomy = %s AND {$alias_tt}.term_id IN ($placeholders)"; - $params[] = $base_taxonomy; - $params = array_merge( $params, $base_terms ); - - ++$base_join_index; - } - } - - // Cross-taxonomy filtering (exclude current taxonomy from cross-filter). - $cross_filters = array_diff_key( $active_filters, array( $taxonomy_slug => true ) ); - $join_index = 0; - foreach ( $cross_filters as $filter_taxonomy => $term_ids ) { - if ( empty( $term_ids ) ) { - continue; - } - - $term_ids = array_map( 'intval', (array) $term_ids ); - $placeholders = implode( ',', array_fill( 0, count( $term_ids ), '%d' ) ); - - $alias_tr = "cross_tr_{$join_index}"; - $alias_tt = "cross_tt_{$join_index}"; - - $joins .= " INNER JOIN {$wpdb->term_relationships} {$alias_tr} ON tr.object_id = {$alias_tr}.object_id"; - $joins .= " INNER JOIN {$wpdb->term_taxonomy} {$alias_tt} ON {$alias_tr}.term_taxonomy_id = {$alias_tt}.term_taxonomy_id"; - - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $where_clauses .= " AND {$alias_tt}.taxonomy = %s AND {$alias_tt}.term_id IN ($placeholders)"; - $params[] = $filter_taxonomy; - $params = array_merge( $params, $term_ids ); - - ++$join_index; - } - - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $query = $wpdb->prepare( - "SELECT tt.term_id, COUNT(DISTINCT tr.object_id) as event_count - FROM {$wpdb->term_relationships} tr - INNER JOIN {$wpdb->term_taxonomy} tt - ON tr.term_taxonomy_id = tt.term_taxonomy_id - {$joins} - WHERE tt.taxonomy = %s - {$where_clauses} - GROUP BY tt.term_id", - $params - ); - - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $results = $wpdb->get_results( $query ); - - $counts = array(); - foreach ( $results as $row ) { - $counts[ (int) $row->term_id ] = (int) $row->event_count; - } - - return $counts; - } - - /** - * @param array $terms Flat array of term objects - * @param int $parent_id Parent term ID for current level - * @param int $level Current nesting level - * @return array Nested tree structure - */ - public static function build_hierarchy_tree( $terms, $parent_id = 0, $level = 0 ) { - $tree = array(); - - $term_ids = array_map( - function ( $t ) { - return $t->term_id; - }, - $terms - ); - - foreach ( $terms as $term ) { - $effective_parent = $term->parent; - while ( 0 !== $effective_parent && ! in_array( $effective_parent, $term_ids, true ) ) { - $parent_term = get_term( $effective_parent ); - $effective_parent = $parent_term && ! is_wp_error( $parent_term ) ? $parent_term->parent : 0; - } - - if ( $effective_parent == $parent_id ) { - $term_data = array( - 'term_id' => $term->term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'event_count' => $term->event_count, - 'level' => $level, - 'children' => array(), - ); - - $children = self::build_hierarchy_tree( $terms, $term->term_id, $level + 1 ); - if ( ! empty( $children ) ) { - $term_data['children'] = $children; - } - - $tree[] = $term_data; - } - } - - return $tree; - } - - /** - * @param array $terms_hierarchy Nested term structure - * @return array Flattened term array maintaining level information - */ - public static function flatten_hierarchy( $terms_hierarchy ) { - $flattened = array(); - - foreach ( $terms_hierarchy as $term ) { - $flattened[] = $term; - - if ( ! empty( $term['children'] ) ) { - $flattened = array_merge( $flattened, self::flatten_hierarchy( $term['children'] ) ); - } - } - - return $flattened; - } -}