setup_hooks(); } /** * Set up hooks for various purposes. * * This method adds hooks for different purposes as needed. * * @since 1.0.0 * * @return void */ protected function setup_hooks(): void { add_action( 'pre_get_posts', array( $this, 'prepare_event_query_before_execution' ) ); add_filter( 'posts_clauses', array( $this, 'adjust_admin_event_sorting' ), 10, 2 ); } /** * Retrieve upcoming events. * * Retrieves a list of upcoming events with optional filtering by the maximum number to display. * * @since 1.0.0 * * @param int $number Maximum number of upcoming events to retrieve. * @return WP_Query A WordPress query object containing the list of upcoming events. */ public function get_upcoming_events( int $number = 5 ): WP_Query { return $this->get_events_list( 'upcoming', $number ); } /** * Retrieve past events. * * Retrieves a list of past events with optional filtering by the maximum number to display. * * @since 1.0.0 * * @param int $number Maximum number of past events to retrieve. * @return WP_Query A WordPress query object containing the list of past events. */ public function get_past_events( int $number = 5 ): WP_Query { return $this->get_events_list( 'past', $number ); } /** * Retrieve a list of events based on specified criteria. * * This method queries and returns a list of events based on the event list type (upcoming or past), * maximum number to display, optional topics, and venues for filtering. The results are returned as * a WordPress query object. * * @since 1.0.0 * * @param string $event_list_type Type of event list: 'upcoming' or 'past'. * @param int $number Maximum number of events to retrieve. * @param array $topics Array of topic slugs for additional filtering. * @param array $venues Array of venue slugs for additional filtering. * @return WP_Query A WordPress query object containing the list of events. */ public function get_events_list( string $event_list_type = '', int $number = 5, array $topics = array(), array $venues = array() ): WP_Query { // Past events should be ordered DESC (most recent first), // upcoming events should be ordered ASC (soonest first). $order = ( 'past' === $event_list_type ) ? 'DESC' : 'ASC'; $args = array( 'post_type' => Event::POST_TYPE, 'fields' => 'ids', 'no_found_rows' => true, 'posts_per_page' => $number, self::EVENT_QUERY_PARAM => $event_list_type, 'order' => $order, ); $tax_query = array(); if ( ! empty( $venues ) && ! empty( $topics ) ) { $tax_query[] = array( 'relation' => 'AND', array( 'taxonomy' => Topic::TAXONOMY, 'field' => 'slug', 'terms' => $topics, ), array( 'taxonomy' => Venue::TAXONOMY, 'field' => 'slug', 'terms' => $venues, ), ); } elseif ( ! empty( $topics ) ) { $tax_query[] = array( 'taxonomy' => Topic::TAXONOMY, 'field' => 'slug', 'terms' => $topics, ); } elseif ( ! empty( $venues ) ) { $tax_query[] = array( 'taxonomy' => Venue::TAXONOMY, 'field' => 'slug', 'terms' => $venues, ); } $args['tax_query'] = $tax_query; //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query return new WP_Query( $args ); } /** * Set event query and order adjustments before a query is executed. * * This method prepares and adjusts the event query based on specified criteria before it is executed. * It primarily handles adjustments for event archive pages, such as changing the post type, ordering, * and filtering. This method is typically hooked into the 'pre_get_posts' action. * * @since 1.0.0 * * @param WP_Query $query An instance of WP_Query representing the event query. * @return void */ public function prepare_event_query_before_execution( WP_Query $query ): void { $events_query = $query->get( self::EVENT_QUERY_PARAM ); if ( ! is_admin() && $query->is_main_query() ) { $general = get_option( Utility::prefix_key( 'general' ) ); if ( ! is_array( $general ) ) { return; } $pages = $general['pages'] ?? ''; if ( empty( $pages ) || ! is_array( $pages ) ) { return; } $archive_pages = array( 'past' => json_decode( $pages['past_events'] ), 'upcoming' => json_decode( $pages['upcoming_events'] ), ); foreach ( $archive_pages as $key => $value ) { if ( ! empty( $value ) && is_array( $value ) ) { $page = $value[0]; if ( $page->id === $query->queried_object_id ) { $page_id = $query->queried_object_id; $events_query = $key; $query->set( 'post_type', 'gatherpress_event' ); $query->set( self::EVENT_QUERY_PARAM, $key ); $query->is_page = false; $query->is_singular = false; $query->is_archive = true; $query->is_post_type_archive = true; // This will force a page to behave like an archive page. Use -1 as that is not a valid ID. $query->queried_object_id = -1; // Option adjustments for page_for_posts and show_on_front to force archive page. add_filter( 'pre_option', static function ( $pre, $option ) { if ( 'page_for_posts' === $option ) { return -1; } if ( 'show_on_front' === $option ) { return 'page'; } return $pre; }, 10, 2 ); // Pass original page title as archive title. add_filter( 'get_the_archive_title', static function () use ( $page_id ) { return get_the_title( $page_id ); } ); } } } } switch ( $events_query ) { case 'upcoming': remove_filter( 'posts_clauses', array( $this, 'adjust_sorting_for_past_events' ) ); add_filter( 'posts_clauses', array( $this, 'adjust_sorting_for_upcoming_events' ), 10, 2 ); break; case 'past': add_filter( 'posts_clauses', array( $this, 'adjust_sorting_for_past_events' ), 10, 2 ); remove_filter( 'posts_clauses', array( $this, 'adjust_sorting_for_upcoming_events' ) ); break; default: remove_filter( 'posts_clauses', array( $this, 'adjust_sorting_for_past_events' ) ); remove_filter( 'posts_clauses', array( $this, 'adjust_sorting_for_upcoming_events' ) ); } } /** * Adjust the sorting criteria for upcoming events in a query. * * This method modifies the SQL query pieces, including join, where, orderby, etc., to adjust the sorting criteria * for upcoming events in the query. It ensures that events are ordered by their start datetime in ascending order. * * @see https://developer.wordpress.org/reference/hooks/posts_clauses/ * * @since 1.0.0 * * @param array $query_pieces An array containing pieces of the SQL query. * @param WP_Query $query The WP_Query instance (passed by reference). * @return array The modified SQL query pieces with adjusted sorting criteria for upcoming events. */ public function adjust_sorting_for_upcoming_events( array $query_pieces, WP_Query $query ): array { $include_unfinished = $query->get( 'include_unfinished' ); // Default to true if not explicitly set to maintain backward compatibility. $inclusive = ( '' === $include_unfinished ) ? true : (bool) $include_unfinished; return $this->adjust_event_sql( $query_pieces, 'upcoming', $query->get( 'order' ), $query->get( 'orderby' ), $inclusive ); } /** * Adjust the sorting criteria for past events in a query. * * This method modifies the SQL query pieces, including join, where, orderby, etc., to adjust the sorting criteria * for past events in the query. It ensures that events are ordered by their start datetime in the desired order. * * @since 1.0.0 * * @param array $query_pieces An array containing pieces of the SQL query. * @param WP_Query $query The WP_Query instance (passed by reference). * @return array The modified SQL query pieces with adjusted sorting criteria for past events. */ public function adjust_sorting_for_past_events( array $query_pieces, WP_Query $query ): array { $include_unfinished = $query->get( 'include_unfinished' ); // For past events, default to false (exclude currently running events). // This shows only truly finished events unless explicitly requested otherwise. $inclusive = ( '' === $include_unfinished ) ? false : (bool) $include_unfinished; return $this->adjust_event_sql( $query_pieces, 'past', $query->get( 'order' ), $query->get( 'orderby' ), $inclusive ); } /** * Adjust event sorting criteria for the WordPress admin panel. * * This method modifies the SQL query pieces, including join, where, orderby, etc., to adjust the sorting criteria * for events when viewing them in the WordPress admin panel. It specifically handles sorting by event datetime. * * @since 1.0.0 * * @param array $query_pieces An array containing pieces of the SQL query. * @param WP_Query $wp_query The WP_Query instance (passed by reference). * @return array The modified SQL query pieces with adjusted sorting criteria. */ public function adjust_admin_event_sorting( array $query_pieces, WP_Query $wp_query ): array { if ( ! is_admin() ) { return $query_pieces; } /** * Run only for Event post listings. * * First checks whether the get_current_screen function exists, * because it is loaded only after the 'admin_init' hook. * * @see https://developer.wordpress.org/reference/functions/get_current_screen/#comment-5424 * * This sanity check was added after it's been reported that some admin screens may not have $wp_query set. * @see https://wordpress.org/support/topic/gatherpress-has-critical-error-when-i-access-wpforms-payment-settings/ */ $current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $current_screen || 'edit-gatherpress_event' !== $current_screen->id ) { return $query_pieces; } if ( 'datetime' === $wp_query->get( 'orderby' ) ) { $query_pieces = $this->adjust_event_sql( $query_pieces, 'all', $wp_query->get( 'order' ) ); } return $query_pieces; } /** * Adjust SQL clauses for Event queries to join on the gatherpress_events table. * * This method adjusts various SQL clauses (e.g., join, where, orderby) for Event queries to include * the `gatherpress_events` table in the database join. It allows querying events based on different * criteria such as upcoming or past events and specifying the event order (DESC or ASC). * * @see https://developer.wordpress.org/reference/hooks/posts_join/ * @see https://developer.wordpress.org/reference/hooks/posts_orderby/ * @see https://developer.wordpress.org/reference/hooks/posts_where/ * * @since 1.0.0 * * @param array $pieces An array of query pieces, including join, where, orderby, * and more. * @param string $type The type of events to query (options: 'all', 'upcoming', 'past') * (Default: 'all'). * @param string $order The event order ('DESC' for descending or 'ASC' for ascending) * (Default: 'DESC'). * @param string[]|string $order_by List or singular string of ORDERBY statement(s) (Default: ['datetime']). * @param bool $inclusive Whether to include currently running events in the query (Default: true). * @return array An array containing adjusted SQL clauses for the Event query. */ public function adjust_event_sql( array $pieces, string $type = 'all', string $order = 'DESC', $order_by = array( 'datetime' ), bool $inclusive = true ): array { global $wpdb; $defaults = array( 'where' => '', 'groupby' => '', 'join' => '', 'orderby' => '', 'distinct' => '', 'fields' => '', 'limits' => '', ); $pieces = array_merge( $defaults, $pieces ); $table = sprintf( Event::TABLE_FORMAT, $wpdb->prefix ); $pieces['join'] .= ' LEFT JOIN ' . esc_sql( $table ) . ' ON ' . esc_sql( $wpdb->posts ) . '.ID=' . esc_sql( $table ) . '.post_id'; $order = strtoupper( $order ); if ( in_array( $order, array( 'DESC', 'ASC' ), true ) ) { // ORDERBY is an array, which allows to orderby multiple values. // Currently, it is only allowed to order events by ONE value. $order_by = ( is_array( $order_by ) ) ? $order_by[0] : $order_by; switch ( strtolower( $order_by ) ) { case 'id': $pieces['orderby'] = sprintf( esc_sql( $wpdb->posts ) . '.ID %s', esc_sql( $order ) ); break; case 'title': $pieces['orderby'] = sprintf( esc_sql( $wpdb->posts ) . '.post_name %s', esc_sql( $order ) ); break; case 'modified': $pieces['orderby'] = sprintf( esc_sql( $wpdb->posts ) . '.post_modified_gmt %s', esc_sql( $order ) ); break; case 'rand': $pieces['orderby'] = esc_sql( 'RAND()' ); break; case 'datetime': default: $pieces['orderby'] = sprintf( esc_sql( $table ) . '.datetime_start_gmt %s', esc_sql( $order ) ); break; } } if ( 'all' === $type ) { return $pieces; } $current = gmdate( Event::DATETIME_FORMAT, time() ); $column = $this->get_datetime_comparison_column( $type, $inclusive ); // Appends a date-based condition to the WHERE clause of the SQL query, // filtering events as either upcoming or past. if ( 'upcoming' === $type ) { // Include only events starting on or after the current date/time (upcoming). $pieces['where'] .= $wpdb->prepare( ' AND %i.%i >= %s', $table, $column, $current ); } elseif ( 'past' === $type ) { // Include only events starting before the current date/time (past). $pieces['where'] .= $wpdb->prepare( ' AND %i.%i < %s', $table, $column, $current ); } return $pieces; } /** * Determine which db column to compare against, * based on the type of event query (either upcoming or past) * and if started but unfinished events should be included. * * @param string $type The type of events to query (options: 'all', 'upcoming', 'past') * (Cannot be 'all' anymore). * @param bool $inclusive Whether to include currently running events in the query. * * @return string Name of the DB column, which content to compare against the current time. */ protected function get_datetime_comparison_column( string $type, bool $inclusive ): string { if ( // Upcoming events, including ones that are running. ( $inclusive && 'upcoming' === $type ) || // Past events, that are finished already. ( ! $inclusive && 'past' === $type ) ) { return 'datetime_end_gmt'; } // All others, means: // - Upcoming events, without running events. // - Past events, that are still running. return 'datetime_start_gmt'; } }