From c73e64eb5e590b6093d7bfa6a8b79df099297840 Mon Sep 17 00:00:00 2001 From: Tetrakern <26898880+Tetrakern@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:40:29 +0200 Subject: [PATCH] Boost performance of story data query (x15) --- CUSTOMIZE.md | 43 ++++++++++++++++++ FILTERS.md | 38 +++++++++++++++- includes/functions/_utility.php | 79 ++++++++++++++++++++++++--------- 3 files changed, 137 insertions(+), 23 deletions(-) diff --git a/CUSTOMIZE.md b/CUSTOMIZE.md index c44990ce..b92dba2a 100644 --- a/CUSTOMIZE.md +++ b/CUSTOMIZE.md @@ -358,6 +358,23 @@ If the "Next Chapter" note above the chapter list is not enough and you want to * Filter: [fictioneer_filter_story_chapter_posts_query](FILTERS.md#apply_filters-fictioneer_filter_story_chapter_posts_query-query_args-story_id-chapter_ids-) ```php +/** + * Allows scheduled (future) chapters to be queried + * + * @since x.x.x + * + * @param array $statuses Statuses that are queried. Default ['publish]. + * + * @return array The updated array of statuses. + */ + +function child_query_scheduled_chapters( $statuses ) { + $statuses[] = 'future'; + + return $statuses; +} +add_filter( 'fictioneer_filter_get_story_data_queried_chapter_statuses', 'child_query_scheduled_chapters' ); + /** * Shows scheduled (future) chapter in story chapter list * @@ -405,6 +422,32 @@ function child_consider_scheduled_chapters_as_update( $statuses ) { return $statuses; } add_action( 'fictioneer_filter_chapters_added_statuses', 'child_consider_scheduled_chapters_as_update' ); + +// If you want scheduled chapters to be treated like published ones; +// you still need a function or plugin to make them accessible or +// they will lead to a 404 error page. Yes, that's a lot of filters. + +/** + * Adds the 'future' post status to an allowed statuses array + * + * @since x.x.x + * + * @param array $statuses Statuses that are queried. Default ['publish]. + * + * @return array The updated array of statuses. + */ + +function child_treat_scheduled_chapters_as_published( $statuses ) { + $statuses[] = 'future'; + + return $statuses; +} +add_filter( 'fictioneer_filter_chapter_list_statuses', 'child_treat_scheduled_chapters_as_published' ); +add_filter( 'fictioneer_filter_chapter_index_popup_menu_statuses', 'child_treat_scheduled_chapters_as_published' ); +add_filter( 'fictioneer_filter_chapter_nav_buttons_allowed_statuses', 'child_treat_scheduled_chapters_as_published' ); +add_filter( 'fictioneer_filter_get_story_data_indexed_chapter_statuses', 'child_treat_scheduled_chapters_as_published' ); +add_filter( 'fictioneer_filter_allowed_chapter_permalinks', 'child_treat_scheduled_chapters_as_published' ); + ``` ## Modify or remove items from card footers diff --git a/FILTERS.md b/FILTERS.md index 92887655..947bfe75 100644 --- a/FILTERS.md +++ b/FILTERS.md @@ -405,11 +405,45 @@ Filters the array of support links returned for the current post (or post ID if --- -### `apply_filters( 'fictioneer_filter_get_story_data_indexed_chapter_statuses', $statuses )` -Filters the array of chapter statuses that can be appended to a story’s `indexed_chapter_ids` array in the `fictioneer_get_story_data()` function. By default, the statuses are `['publish']`. +### `apply_filters( 'fictioneer_filter_get_story_data_indexed_chapter_statuses', $statuses, $story_id )` +Filters the array of chapter statuses that can be appended to a story’s `indexed_chapter_ids` array in the `fictioneer_get_story_data()` function. These chapters are a subset of the queried chapters, which need to be filtered separately. By default, the statuses are `['publish']`. **Parameters:** * $statuses (array) – Array of chapter statuses. +* $story_id (int) – The story post ID. + +**Example:** +```php +// Adds 'future' chapters to the indexed list in fictioneer_get_story_data() + +function child_index_scheduled_chapters( $statuses ) { + $statuses[] = 'future'; + + return $statuses; +} +add_filter( 'fictioneer_filter_get_story_data_indexed_chapter_statuses', 'child_index_scheduled_chapters' ); +``` + +--- + +### `apply_filters( 'fictioneer_filter_get_story_data_queried_chapter_statuses', $statuses, $story_id )` +Filters the array of queried chapter statuses in the `fictioneer_get_story_data()` function. These chapters may appear in lists but cannot necessarily be navigated to (for example, `'future'` chapters). By default, the statuses are `['publish']`. + +**Parameters:** +* $statuses (array) – Array of chapter statuses. +* $story_id (int) – The story post ID. + +**Example:** +```php +// Adds 'future' chapters to the query and visible list in fictioneer_get_story_data() + +function child_query_scheduled_chapters( $statuses ) { + $statuses[] = 'future'; + + return $statuses; +} +add_filter( 'fictioneer_filter_get_story_data_queried_chapter_statuses', 'child_query_scheduled_chapters' ); +``` --- diff --git a/includes/functions/_utility.php b/includes/functions/_utility.php index 801bb698..aaff674a 100644 --- a/includes/functions/_utility.php +++ b/includes/functions/_utility.php @@ -344,6 +344,7 @@ if ( ! function_exists( 'fictioneer_get_story_data' ) ) { * Get collection of a story's data * * @since 4.3.0 + * @since 5.25.0 - Refactored with custom SQL query. * * @param int $story_id ID of the story. * @param boolean $show_comments Optional. Whether the comment count is needed. @@ -354,6 +355,8 @@ if ( ! function_exists( 'fictioneer_get_story_data' ) ) { */ function fictioneer_get_story_data( $story_id, $show_comments = true, $args = [] ) { + global $wpdb; + $story_id = fictioneer_validate_id( $story_id, 'fcn_story' ); $meta_cache = null; @@ -402,7 +405,6 @@ if ( ! function_exists( 'fictioneer_get_story_data' ) ) { } // Setup - $chapters = fictioneer_get_story_chapter_posts( $story_id ); $tags = get_the_tags( $story_id ); $fandoms = get_the_terms( $story_id, 'fcn_fandom' ); $characters = get_the_terms( $story_id, 'fcn_character' ); @@ -416,8 +418,6 @@ if ( ! function_exists( 'fictioneer_get_story_data' ) ) { $visible_chapter_ids = []; $indexed_chapter_ids = []; - $allowed_indexed_statuses = apply_filters( 'fictioneer_filter_get_story_data_indexed_chapter_statuses', ['publish'] ); - // Assign correct icon if ( $status != 'Ongoing' ) { switch ( $status ) { @@ -436,29 +436,66 @@ if ( ! function_exists( 'fictioneer_get_story_data' ) ) { } } - // Count chapters, words, comments, etc. - if ( ! empty( $chapters ) ) { - foreach ( $chapters as $chapter ) { - // This is about 50 times faster than using a meta query lol - if ( ! get_post_meta( $chapter->ID, 'fictioneer_chapter_hidden', true ) ) { - // Do not count non-chapters... - if ( ! get_post_meta( $chapter->ID, 'fictioneer_chapter_no_chapter', true ) ) { - $chapter_count += 1; - $word_count += get_post_meta( $chapter->ID, '_word_count', true ); - } + // Custom SQL query to count chapters, words, comments, etc. + // This significantly faster than WP_Query (up to 15 times with 500 chapters) + $queried_statuses = apply_filters( 'fictioneer_filter_get_story_data_queried_chapter_statuses', ['publish'], $story_id ); + $indexed_statuses = apply_filters( 'fictioneer_filter_get_story_data_indexed_chapter_statuses', ['publish'], $story_id ); + $chapter_ids = fictioneer_get_story_chapter_ids( $story_id ); + $chapters = []; - // ... but they are still listed! - $visible_chapter_ids[] = $chapter->ID; + if ( ! empty( $chapter_ids ) ) { + $chapter_ids_placeholder = implode( ',', array_fill( 0, count( $chapter_ids ), '%d' ) ); - // Indexed chapters (accounts for custom filters) - if ( in_array( $chapter->post_status, $allowed_indexed_statuses ) ) { - $indexed_chapter_ids[] = $chapter->ID; - } + $status_list = array_map( function( $status ) use ( $wpdb ) { + return $wpdb->prepare( '%s', $status ); + }, $queried_statuses ); + + $status_list = implode( ',', $status_list ); + + $query = $wpdb->prepare( + "SELECT + c.ID as chapter_id, + c.comment_count, + SUM(CASE WHEN pm.meta_key = '_word_count' THEN CAST(pm.meta_value AS UNSIGNED) ELSE 0 END) AS word_count, + MAX(CASE WHEN pm.meta_key = 'fictioneer_chapter_hidden' THEN pm.meta_value ELSE '' END) AS is_hidden, + MAX(CASE WHEN pm.meta_key = 'fictioneer_chapter_no_chapter' THEN pm.meta_value ELSE '' END) AS is_no_chapter, + c.post_status + FROM {$wpdb->posts} c + LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = c.ID + WHERE c.ID IN ($chapter_ids_placeholder) + AND c.post_status IN ($status_list) + GROUP BY c.ID", + ...$chapter_ids // WHERE clause + ); + + $chapters = $wpdb->get_results( $query ); + + usort( $chapters, function( $a, $b ) use ( $chapter_ids ) { + $position_a = array_search( $a->chapter_id, $chapter_ids ); + $position_b = array_search( $b->chapter_id, $chapter_ids ); + return $position_a - $position_b; + }); + } + + foreach ( $chapters as $chapter ) { + if ( empty( $chapter->is_hidden ) ) { + // Do not count non-chapters... + if ( empty( $chapter->is_no_chapter ) ) { + $chapter_count++; + $word_count += intval( $chapter->word_count ); } - // Count ALL comments - $comment_count += $chapter->comment_count; + // ... but they are still listed! + $visible_chapter_ids[] = $chapter->chapter_id; + + // Indexed chapters (accounts for custom filters) + if ( in_array( $chapter->post_status, $indexed_statuses ) ) { + $indexed_chapter_ids[] = $chapter->chapter_id; + } } + + // Count ALL comments + $comment_count += intval( $chapter->comment_count ); } // Add story word count