WordPress.org

Make WordPress Core

Changeset 49141


Ignore:
Timestamp:
10/14/2020 12:49:52 AM (10 months ago)
Author:
peterwilsoncc
Message:

Taxonomy: Improve performance of term recounting database queries.

When modifying terms assigned to an object, replace full term recounts with incrementing/decrementing the count as appropriate. This provides a significant performance boost on sites with a high number of term/object relationships and/or posts.

Introduces the functions wp_increment_term_count(), wp_decrement_term_count(), wp_modify_term_count_by() and wp_modify_term_count_by_now() for updating the term count.

Introduces the function _wp_prevent_term_counting() for preventing double counting on posts that are about to transition.

Adds the parameter update_count_by_callback to register_taxonomy() to allow developers to use a custom callback for incrementing or decrementing a term count.

Props boonebgorges, davidbaumwald, hellofromTonya, johnbillion, lcyh78, mattoperry, peterwilsoncc, rebasaurus, whyisjake.
Fixes #40351.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-taxonomy.php

    r48356 r49141  
    180180     */
    181181    public $update_count_callback;
     182
     183    /**
     184     * Function that will be called when the count is modified by an amount.
     185     *
     186     * @since 5.6.0
     187     * @var callable
     188     */
     189    public $update_count_by_callback;
    182190
    183191    /**
     
    278286
    279287        $defaults = array(
    280             'labels'                => array(),
    281             'description'           => '',
    282             'public'                => true,
    283             'publicly_queryable'    => null,
    284             'hierarchical'          => false,
    285             'show_ui'               => null,
    286             'show_in_menu'          => null,
    287             'show_in_nav_menus'     => null,
    288             'show_tagcloud'         => null,
    289             'show_in_quick_edit'    => null,
    290             'show_admin_column'     => false,
    291             'meta_box_cb'           => null,
    292             'meta_box_sanitize_cb'  => null,
    293             'capabilities'          => array(),
    294             'rewrite'               => true,
    295             'query_var'             => $this->name,
    296             'update_count_callback' => '',
    297             'show_in_rest'          => false,
    298             'rest_base'             => false,
    299             'rest_controller_class' => false,
    300             'default_term'          => null,
    301             '_builtin'              => false,
     288            'labels'                   => array(),
     289            'description'              => '',
     290            'public'                   => true,
     291            'publicly_queryable'       => null,
     292            'hierarchical'             => false,
     293            'show_ui'                  => null,
     294            'show_in_menu'             => null,
     295            'show_in_nav_menus'        => null,
     296            'show_tagcloud'            => null,
     297            'show_in_quick_edit'       => null,
     298            'show_admin_column'        => false,
     299            'meta_box_cb'              => null,
     300            'meta_box_sanitize_cb'     => null,
     301            'capabilities'             => array(),
     302            'rewrite'                  => true,
     303            'query_var'                => $this->name,
     304            'update_count_callback'    => '',
     305            'update_count_by_callback' => '',
     306            'show_in_rest'             => false,
     307            'rest_base'                => false,
     308            'rest_controller_class'    => false,
     309            'default_term'             => null,
     310            '_builtin'                 => false,
    302311        );
    303312
     
    412421        }
    413422
     423        // If generic update callback is defined but increment/decrement callback is not.
     424        if (
     425            ! empty( $args['update_count_callback'] ) &&
     426            is_callable( $args['update_count_callback'] ) &&
     427            empty( $args['update_count_by_callback'] )
     428        ) {
     429            $args['update_count_by_callback'] = function( $tt_ids, $taxonomy, $modify_by ) {
     430                return call_user_func( $args['update_count_callback'], $tt_ids, $taxonomy );
     431            };
     432        }
     433
    414434        foreach ( $args as $property_name => $property_value ) {
    415435            $this->$property_name = $property_value;
  • trunk/src/wp-includes/post.php

    r49125 r49141  
    40724072    }
    40734073
     4074    // Allow term counts to be handled by transitioning post type.
     4075    _wp_prevent_term_counting( true );
    40744076    if ( is_object_in_taxonomy( $post_type, 'category' ) ) {
    40754077        wp_set_post_categories( $post_ID, $post_category );
     
    41284130        }
    41294131    }
     4132    // Restore term counting.
     4133    _wp_prevent_term_counting( false );
    41304134
    41314135    if ( ! empty( $postarr['meta_input'] ) ) {
     
    44374441            continue;
    44384442        }
     4443        _wp_prevent_term_counting( true );
    44394444        wp_set_post_terms( $post->ID, array( $default_term_id ), $taxonomy );
     4445        _wp_prevent_term_counting( false );
    44404446    }
    44414447
     
    73137319 */
    73147320function _update_term_count_on_transition_post_status( $new_status, $old_status, $post ) {
    7315     // Update counts for the post's terms.
     7321    if ( 'inherit' === $new_status ) {
     7322        $new_status = get_post_status( $post->post_parent );
     7323    }
     7324
     7325    if ( 'inherit' === $old_status ) {
     7326        $old_status = get_post_status( $post->post_parent );
     7327    }
     7328
     7329    $count_new = 'publish' === $new_status;
     7330    $count_old = 'publish' === $old_status;
     7331
     7332    if ( $count_new === $count_old ) {
     7333        // Nothing to do.
     7334        return;
     7335    }
     7336
     7337    /*
     7338     * Update counts for the post's terms.
     7339     *
     7340     * Term counting is deferred while incrementing/decrementing the counts to
     7341     * reduce the number of database queries required. Once the counts are
     7342     * complete the updates are performed if term counting wasn't previously
     7343     * deferred.
     7344     */
     7345    $previous_deferred_setting = wp_defer_term_counting();
     7346    wp_defer_term_counting( true );
    73167347    foreach ( (array) get_object_taxonomies( $post->post_type ) as $taxonomy ) {
    73177348        $tt_ids = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'tt_ids' ) );
    7318         wp_update_term_count( $tt_ids, $taxonomy );
    7319     }
     7349
     7350        if ( empty( $tt_ids ) ) {
     7351            // No terms for this taxonomy on object.
     7352            continue;
     7353        }
     7354
     7355        $object_types = (array) get_taxonomy( $taxonomy )->object_type;
     7356
     7357        foreach ( $object_types as &$object_type ) {
     7358            list( $object_type ) = explode( ':', $object_type );
     7359        }
     7360
     7361        $object_types = array_unique( $object_types );
     7362
     7363        if ( ! in_array( $post->post_type, $object_types, true ) ) {
     7364            $modify_by = 0;
     7365        } elseif ( $count_new && ! $count_old ) {
     7366            $modify_by = 1;
     7367        } elseif ( $count_old && ! $count_new ) {
     7368            $modify_by = -1;
     7369        }
     7370
     7371        if ( 'attachment' === $post->post_type ) {
     7372            wp_modify_term_count_by( $tt_ids, $taxonomy, $modify_by );
     7373            continue;
     7374        }
     7375
     7376        $check_attachments = array_search( 'attachment', $object_types, true );
     7377        if ( false !== $check_attachments ) {
     7378            unset( $object_types[ $check_attachments ] );
     7379            $check_attachments = true;
     7380        }
     7381
     7382        wp_modify_term_count_by( $tt_ids, $taxonomy, $modify_by );
     7383        if ( ! $check_attachments ) {
     7384            continue;
     7385        }
     7386
     7387        /*
     7388         * For non-attachments, check if there are any attachment children
     7389         * with 'inherited' post status -- if so those will need to be counted.
     7390         */
     7391        $attachments = get_children(
     7392            array(
     7393                'post_parent'            => $post->ID,
     7394                'post_status'            => 'inherit',
     7395                'post_type'              => 'attachment',
     7396                'update_post_meta_cache' => false,
     7397                'update_post_term_cache' => true,
     7398            )
     7399        );
     7400
     7401        foreach ( $attachments as $attachment ) {
     7402            _update_term_count_on_transition_post_status( $new_status, $old_status, $attachment );
     7403        }
     7404    }
     7405    wp_defer_term_counting( $previous_deferred_setting );
    73207406}
    73217407
  • trunk/src/wp-includes/taxonomy.php

    r49108 r49141  
    337337 * @since 5.4.0 Added the registered taxonomy object as a return value.
    338338 * @since 5.5.0 Introduced `default_term` argument.
     339 * @since 5.6.0 Introduced `update_count_by_callback` argument.
    339340 *
    340341 * @global array $wp_taxonomies Registered taxonomies.
     
    345346 *     Optional. Array or query string of arguments for registering a taxonomy.
    346347 *
    347  *     @type array         $labels                An array of labels for this taxonomy. By default, Tag labels are
    348  *                                                used for non-hierarchical taxonomies, and Category labels are used
    349  *                                                for hierarchical taxonomies. See accepted values in
    350  *                                                get_taxonomy_labels(). Default empty array.
    351  *     @type string        $description           A short descriptive summary of what the taxonomy is for. Default empty.
    352  *     @type bool          $public                Whether a taxonomy is intended for use publicly either via
    353  *                                                the admin interface or by front-end users. The default settings
    354  *                                                of `$publicly_queryable`, `$show_ui`, and `$show_in_nav_menus`
    355  *                                                are inherited from `$public`.
    356  *     @type bool          $publicly_queryable    Whether the taxonomy is publicly queryable.
    357  *                                                If not set, the default is inherited from `$public`
    358  *     @type bool          $hierarchical          Whether the taxonomy is hierarchical. Default false.
    359  *     @type bool          $show_ui               Whether to generate and allow a UI for managing terms in this taxonomy in
    360  *                                                the admin. If not set, the default is inherited from `$public`
    361  *                                                (default true).
    362  *     @type bool          $show_in_menu          Whether to show the taxonomy in the admin menu. If true, the taxonomy is
    363  *                                                shown as a submenu of the object type menu. If false, no menu is shown.
    364  *                                                `$show_ui` must be true. If not set, default is inherited from `$show_ui`
    365  *                                                (default true).
    366  *     @type bool          $show_in_nav_menus     Makes this taxonomy available for selection in navigation menus. If not
    367  *                                                set, the default is inherited from `$public` (default true).
    368  *     @type bool          $show_in_rest          Whether to include the taxonomy in the REST API. Set this to true
    369  *                                                for the taxonomy to be available in the block editor.
    370  *     @type string        $rest_base             To change the base url of REST API route. Default is $taxonomy.
    371  *     @type string        $rest_controller_class REST API Controller class name. Default is 'WP_REST_Terms_Controller'.
    372  *     @type bool          $show_tagcloud         Whether to list the taxonomy in the Tag Cloud Widget controls. If not set,
    373  *                                                the default is inherited from `$show_ui` (default true).
    374  *     @type bool          $show_in_quick_edit    Whether to show the taxonomy in the quick/bulk edit panel. It not set,
    375  *                                                the default is inherited from `$show_ui` (default true).
    376  *     @type bool          $show_admin_column     Whether to display a column for the taxonomy on its post type listing
    377  *                                                screens. Default false.
    378  *     @type bool|callable $meta_box_cb           Provide a callback function for the meta box display. If not set,
    379  *                                                post_categories_meta_box() is used for hierarchical taxonomies, and
    380  *                                                post_tags_meta_box() is used for non-hierarchical. If false, no meta
    381  *                                                box is shown.
    382  *     @type callable      $meta_box_sanitize_cb  Callback function for sanitizing taxonomy data saved from a meta
    383  *                                                box. If no callback is defined, an appropriate one is determined
    384  *                                                based on the value of `$meta_box_cb`.
     348 *     @type array         $labels                   An array of labels for this taxonomy. By default, Tag labels are
     349 *                                                   used for non-hierarchical taxonomies, and Category labels are used
     350 *                                                   for hierarchical taxonomies. See accepted values in
     351 *                                                   get_taxonomy_labels(). Default empty array.
     352 *     @type string        $description              A short descriptive summary of what the taxonomy is for. Default empty.
     353 *     @type bool          $public                   Whether a taxonomy is intended for use publicly either via
     354 *                                                   the admin interface or by front-end users. The default settings
     355 *                                                   of `$publicly_queryable`, `$show_ui`, and `$show_in_nav_menus`
     356 *                                                   are inherited from `$public`.
     357 *     @type bool          $publicly_queryable       Whether the taxonomy is publicly queryable.
     358 *                                                   If not set, the default is inherited from `$public`
     359 *     @type bool          $hierarchical             Whether the taxonomy is hierarchical. Default false.
     360 *     @type bool          $show_ui                  Whether to generate and allow a UI for managing terms in this taxonomy in
     361 *                                                   the admin. If not set, the default is inherited from `$public`
     362 *                                                   (default true).
     363 *     @type bool          $show_in_menu             Whether to show the taxonomy in the admin menu. If true, the taxonomy is
     364 *                                                   shown as a submenu of the object type menu. If false, no menu is shown.
     365 *                                                   `$show_ui` must be true. If not set, default is inherited from `$show_ui`
     366 *                                                   (default true).
     367 *     @type bool          $show_in_nav_menus        Makes this taxonomy available for selection in navigation menus. If not
     368 *                                                   set, the default is inherited from `$public` (default true).
     369 *     @type bool          $show_in_rest             Whether to include the taxonomy in the REST API. Set this to true
     370 *                                                   for the taxonomy to be available in the block editor.
     371 *     @type string        $rest_base                To change the base url of REST API route. Default is $taxonomy.
     372 *     @type string        $rest_controller_class    REST API Controller class name. Default is 'WP_REST_Terms_Controller'.
     373 *     @type bool          $show_tagcloud            Whether to list the taxonomy in the Tag Cloud Widget controls. If not set,
     374 *                                                   the default is inherited from `$show_ui` (default true).
     375 *     @type bool          $show_in_quick_edit       Whether to show the taxonomy in the quick/bulk edit panel. It not set,
     376 *                                                   the default is inherited from `$show_ui` (default true).
     377 *     @type bool          $show_admin_column        Whether to display a column for the taxonomy on its post type listing
     378 *                                                   screens. Default false.
     379 *     @type bool|callable $meta_box_cb              Provide a callback function for the meta box display. If not set,
     380 *                                                   post_categories_meta_box() is used for hierarchical taxonomies, and
     381 *                                                   post_tags_meta_box() is used for non-hierarchical. If false, no meta
     382 *                                                   box is shown.
     383 *     @type callable      $meta_box_sanitize_cb     Callback function for sanitizing taxonomy data saved from a meta
     384 *                                                   box. If no callback is defined, an appropriate one is determined
     385 *                                                   based on the value of `$meta_box_cb`.
    385386 *     @type array         $capabilities {
    386387 *         Array of capabilities for this taxonomy.
     
    400401 *         @type int    $ep_mask      Assign an endpoint mask. Default `EP_NONE`.
    401402 *     }
    402  *     @type string|bool   $query_var             Sets the query var key for this taxonomy. Default `$taxonomy` key. If
    403  *                                                false, a taxonomy cannot be loaded at `?{query_var}={term_slug}`. If a
    404  *                                                string, the query `?{query_var}={term_slug}` will be valid.
    405  *     @type callable      $update_count_callback Works much like a hook, in that it will be called when the count is
    406  *                                                updated. Default _update_post_term_count() for taxonomies attached
    407  *                                                to post types, which confirms that the objects are published before
    408  *                                                counting them. Default _update_generic_term_count() for taxonomies
    409  *                                                attached to other object types, such as users.
     403 *     @type string|bool   $query_var                Sets the query var key for this taxonomy. Default `$taxonomy` key. If
     404 *                                                   false, a taxonomy cannot be loaded at `?{query_var}={term_slug}`. If a
     405 *                                                   string, the query `?{query_var}={term_slug}` will be valid.
     406 *     @type callable      $update_count_callback    Works much like a hook, in that it will be called when the count is
     407 *                                                   updated. Default _update_post_term_count() for taxonomies attached
     408 *                                                   to post types, which confirms that the objects are published before
     409 *                                                   counting them. Default _update_generic_term_count() for taxonomies
     410 *                                                   attached to other object types, such as users.
     411 *     @type callable      $update_count_by_callback Works much like a hook, in that it will be called when the count is
     412 *                                                   incremented or decremented. Defaults to the value of `$update_count_callback` if
     413 *                                                   a custom callack is defined, otherwise uses wp_modify_term_count_by().
    410414 *     @type string|array  $default_term {
    411415 *         Default term to be used for the taxonomy.
     
    415419 *         @type string $description  Description for default term. Default empty.
    416420 *     }
    417  *     @type bool          $_builtin              This taxonomy is a "built-in" taxonomy. INTERNAL USE ONLY!
    418  *                                                Default false.
     421 *     @type bool          $_builtin                 This taxonomy is a "built-in" taxonomy. INTERNAL USE ONLY!
     422 *                                                   Default false.
    419423 * }
    420424 * @return WP_Taxonomy|WP_Error The registered taxonomy object on success, WP_Error object on failure.
     
    25622566    }
    25632567
     2568    $taxonomy_object = get_taxonomy( $taxonomy );
     2569
     2570    $object_types = (array) $taxonomy_object->object_type;
     2571    foreach ( $object_types as &$object_type ) {
     2572        if ( 0 === strpos( $object_type, 'attachment:' ) ) {
     2573            list( $object_type ) = explode( ':', $object_type );
     2574        }
     2575    }
     2576
     2577    if ( array_filter( $object_types, 'post_type_exists' ) !== $object_types ) {
     2578        // This taxonomy applies to non-posts, count changes now.
     2579        $do_recount = ! _wp_prevent_term_counting();
     2580    } elseif ( 'publish' === get_post_status( $object_id ) ) {
     2581        // Published post, count changes now.
     2582        $do_recount = ! _wp_prevent_term_counting();
     2583    } else {
     2584        $do_recount = false;
     2585    }
     2586
    25642587    if ( ! is_array( $terms ) ) {
    25652588        $terms = array( $terms );
     
    26472670    }
    26482671
    2649     if ( $new_tt_ids ) {
    2650         wp_update_term_count( $new_tt_ids, $taxonomy );
     2672    if ( $new_tt_ids && $do_recount ) {
     2673        wp_increment_term_count( $new_tt_ids, $taxonomy );
    26512674    }
    26522675
     
    26662689    }
    26672690
    2668     $t = get_taxonomy( $taxonomy );
    2669 
    2670     if ( ! $append && isset( $t->sort ) && $t->sort ) {
     2691    if ( ! $append && isset( $taxonomy_object->sort ) && $taxonomy_object->sort ) {
    26712692        $values     = array();
    26722693        $term_order = 0;
     
    27472768    if ( ! taxonomy_exists( $taxonomy ) ) {
    27482769        return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
     2770    }
     2771
     2772    $taxonomy_object = get_taxonomy( $taxonomy );
     2773
     2774    $object_types = (array) $taxonomy_object->object_type;
     2775    foreach ( $object_types as &$object_type ) {
     2776        if ( 0 === strpos( $object_type, 'attachment:' ) ) {
     2777            list( $object_type ) = explode( ':', $object_type );
     2778        }
     2779    }
     2780
     2781    if ( array_filter( $object_types, 'post_type_exists' ) !== $object_types ) {
     2782        // This taxonomy applies to non-posts, count changes now.
     2783        $do_recount = ! _wp_prevent_term_counting();
     2784    } elseif (
     2785        'publish' === get_post_status( $object_id ) ||
     2786        (
     2787            'inherit' === get_post_status( $object_id ) &&
     2788            'publish' === get_post_status( wp_get_post_parent_id( $object_id ) )
     2789        )
     2790    ) {
     2791        // Published post, count changes now.
     2792        $do_recount = ! _wp_prevent_term_counting();
     2793    } else {
     2794        $do_recount = false;
    27492795    }
    27502796
     
    28072853        do_action( 'deleted_term_relationships', $object_id, $tt_ids, $taxonomy );
    28082854
    2809         wp_update_term_count( $tt_ids, $taxonomy );
     2855        if ( $do_recount ) {
     2856            wp_decrement_term_count( $tt_ids, $taxonomy );
     2857        }
    28102858
    28112859        return (bool) $deleted;
     
    32273275        // Flush any deferred counts.
    32283276        if ( ! $defer ) {
     3277            wp_modify_term_count_by( null, null, null, true );
    32293278            wp_update_term_count( null, null, true );
    32303279        }
     
    32323281
    32333282    return $_defer;
     3283}
     3284
     3285/**
     3286 * Prevents add/removing a term from modifying a term count.
     3287 *
     3288 * This is used by functions calling wp_transition_post_status() to indicate the
     3289 * term count will be handled during the post's transition.
     3290 *
     3291 * @private
     3292 * @since 5.6.0
     3293 *
     3294 * @param bool $new_setting The new setting for preventing term counts.
     3295 * @return bool Whether term count prevention is enabled or disabled.
     3296 */
     3297function _wp_prevent_term_counting( $new_setting = null ) {
     3298    static $prevent = false;
     3299
     3300    if ( is_bool( $new_setting ) ) {
     3301        $prevent = $new_setting;
     3302    }
     3303
     3304    return $prevent;
     3305}
     3306
     3307/**
     3308 * Increments the amount of terms in taxonomy.
     3309 *
     3310 * If there is a taxonomy callback applied, then it will be called for updating
     3311 * the count.
     3312 *
     3313 * The default action is to increment the count by one and update the database.
     3314 *
     3315 * @since 5.6.0
     3316 *
     3317 * @param int|array $tt_ids       The term_taxonomy_id of the terms.
     3318 * @param string    $taxonomy     The context of the term.
     3319 * @param int       $increment_by By how many the term count is to be incremented. Default 1.
     3320 * @param bool      $do_deferred  Whether to flush the deferred term counts too. Default false.
     3321 * @return bool If no terms will return false, and if successful will return true.
     3322 */
     3323function wp_increment_term_count( $tt_ids, $taxonomy, $increment_by = 1, $do_deferred = false ) {
     3324    return wp_modify_term_count_by( $tt_ids, $taxonomy, $increment_by, $do_deferred );
     3325}
     3326
     3327/**
     3328 * Decrements the amount of terms in taxonomy.
     3329 *
     3330 * If there is a taxonomy callback applied, then it will be called for updating
     3331 * the count.
     3332 *
     3333 * The default action is to decrement the count by one and update the database.
     3334 *
     3335 * @since 5.6.0
     3336 *
     3337 * @param int|array $tt_ids       The term_taxonomy_id of the terms.
     3338 * @param string    $taxonomy     The context of the term.
     3339 * @param int       $decrement_by By how many the term count is to be decremented. Default 1.
     3340 * @param bool      $do_deferred  Whether to flush the deferred term counts too. Default false.
     3341 * @return bool If no terms will return false, and if successful will return true.
     3342 */
     3343function wp_decrement_term_count( $tt_ids, $taxonomy, $decrement_by = 1, $do_deferred = false ) {
     3344    return wp_modify_term_count_by( $tt_ids, $taxonomy, $decrement_by * -1, $do_deferred );
     3345}
     3346
     3347/**
     3348 * Modifies the amount of terms in taxonomy.
     3349 *
     3350 * If there is a taxonomy callback applied, then it will be called for updating
     3351 * the count.
     3352 *
     3353 * The default action is to decrement the count by one and update the database.
     3354 *
     3355 * @since 5.6.0
     3356 *
     3357 * @param int|array $tt_ids      The term_taxonomy_id of the terms.
     3358 * @param string    $taxonomy    The context of the term.
     3359 * @param int       $modify_by   By how many the term count is to be modified.
     3360 * @param bool      $do_deferred Whether to flush the deferred term counts too. Default false.
     3361 * @return bool If no terms will return false, and if successful will return true.
     3362 */
     3363function wp_modify_term_count_by( $tt_ids, $taxonomy, $modify_by, $do_deferred = false ) {
     3364    static $_deferred = array();
     3365
     3366    if ( $do_deferred ) {
     3367        foreach ( (array) $_deferred as $taxonomy_name => $modifications ) {
     3368            $tax_by_count = array_reduce(
     3369                array_keys( $modifications ),
     3370                function( $by_count, $tt_id ) use ( $modifications ) {
     3371                    if ( ! isset( $by_count[ $modifications[ $tt_id ] ] ) ) {
     3372                        $by_count[ $modifications[ $tt_id ] ] = array();
     3373                    }
     3374                    $by_count[ $modifications[ $tt_id ] ][] = $tt_id;
     3375                    return $by_count;
     3376                },
     3377                array()
     3378            );
     3379
     3380            foreach ( $tax_by_count as $_modify_by => $_tt_ids ) {
     3381                wp_modify_term_count_by_now( $_tt_ids, $taxonomy_name, $_modify_by );
     3382            }
     3383            unset( $_deferred[ $taxonomy_name ] );
     3384        }
     3385    }
     3386
     3387    if ( empty( $tt_ids ) ) {
     3388        return false;
     3389    }
     3390
     3391    if ( ! is_array( $tt_ids ) ) {
     3392        $tt_ids = array( $tt_ids );
     3393    }
     3394
     3395    if ( wp_defer_term_counting() ) {
     3396        foreach ( $tt_ids as $tt_id ) {
     3397            if ( ! isset( $_deferred[ $taxonomy ][ $tt_id ] ) ) {
     3398                $_deferred[ $taxonomy ][ $tt_id ] = 0;
     3399            }
     3400            $_deferred[ $taxonomy ][ $tt_id ] += $modify_by;
     3401        }
     3402        return true;
     3403    }
     3404
     3405    return wp_modify_term_count_by_now( $tt_ids, $taxonomy, $modify_by );
     3406}
     3407
     3408/**
     3409 * Modifies the amount of terms in taxonomy immediately
     3410 *
     3411 * If there is a taxonomy callback applied, then it will be called for updating
     3412 * the count.
     3413 *
     3414 * The default action is to decrement the count by one and update the database.
     3415 *
     3416 * @since 5.6.0
     3417 *
     3418 * @param int|array $tt_ids      The term_taxonomy_id of the terms.
     3419 * @param string    $taxonomy    The context of the term.
     3420 * @param int       $modify_by   By how many the term count is to be modified.
     3421 * @return bool If no terms will return false, and if successful will return true.
     3422 */
     3423function wp_modify_term_count_by_now( $tt_ids, $taxonomy, $modify_by ) {
     3424    global $wpdb;
     3425
     3426    if ( 0 === $modify_by ) {
     3427        return false;
     3428    }
     3429
     3430    $tt_ids = array_filter( array_map( 'intval', (array) $tt_ids ) );
     3431
     3432    if ( empty( $tt_ids ) ) {
     3433        return false;
     3434    }
     3435
     3436    $taxonomy = get_taxonomy( $taxonomy );
     3437    if ( ! empty( $taxonomy->update_count_by_callback ) ) {
     3438        call_user_func( $taxonomy->update_count_by_callback, $tt_ids, $taxonomy, $modify_by );
     3439        clean_term_cache( $tt_ids, '', false );
     3440        return true;
     3441    }
     3442
     3443    $tt_ids_string = '(' . implode( ',', $tt_ids ) . ')';
     3444
     3445    foreach ( $tt_ids as $tt_id ) {
     3446        /** This action is documented in wp-includes/taxonomy.php */
     3447        do_action( 'edit_term_taxonomy', $tt_id, $taxonomy );
     3448    }
     3449
     3450    $result = $wpdb->query(
     3451        $wpdb->prepare(
     3452            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     3453            "UPDATE {$wpdb->term_taxonomy} AS tt SET tt.count = GREATEST( 0, tt.count + %d ) WHERE tt.term_taxonomy_id IN $tt_ids_string",
     3454            $modify_by
     3455        )
     3456    );
     3457
     3458    if ( ! $result ) {
     3459        return false;
     3460    }
     3461
     3462    foreach ( $tt_ids as $tt_id ) {
     3463        /** This action is documented in wp-includes/taxonomy.php */
     3464        do_action( 'edited_term_taxonomy', $tt_id, $taxonomy );
     3465    }
     3466
     3467    clean_term_cache( $tt_ids, '', false );
     3468
     3469    return true;
    32343470}
    32353471
  • trunk/tests/phpunit/tests/term/termCounts.php

    r49000 r49141  
    1919     */
    2020    public static $post_ids;
     21
     22    /**
     23     * Array of tag IDs.
     24     *
     25     * @var int[]
     26     */
     27    public static $tag_ids;
     28
     29    /**
     30     * Term ID for testing user counts.
     31     *
     32     * @var int
     33     */
     34    public static $user_term;
     35
     36    /**
     37     * User ID for testing user counts.
     38     *
     39     * @var int
     40     */
     41    public static $user_id;
    2142
    2243    /**
     
    3152        }
    3253
    33         register_taxonomy( 'wp_test_tax_counts', array( 'post', 'attachment' ) );
     54        // Extra published post.
     55        self::$post_ids['publish_two'] = $factory->post->create( array( 'post_status' => 'publish' ) );
     56
     57        self::$user_id = $factory->user->create( array( 'role' => 'author' ) );
     58
     59        self::register_taxonomies();
    3460        self::$attachment_term = $factory->term->create( array( 'taxonomy' => 'wp_test_tax_counts' ) );
     61        self::$user_term       = $factory->term->create( array( 'taxonomy' => 'wp_test_user_tax_counts' ) );
     62        self::$tag_ids         = $factory->term->create_many( 5 );
    3563    }
    3664
    3765    public function setUp() {
    3866        parent::setUp();
    39 
     67        self::register_taxonomies();
     68    }
     69
     70    /**
     71     * Register taxonomies used by tests.
     72     *
     73     * This is called both before class and before each test as the global is
     74     * reset in each test's tearDown.
     75     */
     76    public static function register_taxonomies() {
    4077        register_taxonomy( 'wp_test_tax_counts', array( 'post', 'attachment' ) );
     78        register_taxonomy( 'wp_test_user_tax_counts', 'user' );
     79    }
     80
     81    /**
     82     * Term counts are not double incremented when post created.
     83     *
     84     * @covers wp_modify_term_count_by
     85     * @dataProvider data_term_count_changes_for_post_statuses
     86     * @ticket 40351
     87     *
     88     * @param string $post_status New post status.
     89     * @param int    $change      Expected change.
     90     */
     91    public function test_term_count_changes_for_post_statuses( $post_status, $change ) {
     92        $term_count = get_term( get_option( 'default_category' ) )->count;
     93        // Do not use shared fixture for this test as it relies on a new post.
     94        $post_id = $this->factory()->post->create( array( 'post_status' => $post_status ) );
     95
     96        $expected = $term_count + $change;
     97        $this->assertSame( $expected, get_term( get_option( 'default_category' ) )->count );
     98    }
     99
     100    /**
     101     * Data provider for test_term_count_changes_for_post_statuses.
     102     *
     103     * @return array[] {
     104     *     @type string $post_status New post status.
     105     *     @type int    $change      Expected change.
     106     * }
     107     */
     108    function data_term_count_changes_for_post_statuses() {
     109        return array(
     110            // 0. Published post
     111            array( 'publish', 1 ),
     112            // 1. Auto draft
     113            array( 'auto-draft', 0 ),
     114            // 2. Draft
     115            array( 'draft', 0 ),
     116            // 3. Private post
     117            array( 'private', 0 ),
     118        );
    41119    }
    42120
     
    45123     *
    46124     * @covers wp_publish_post
    47      * @covers wp_count_terms
     125     * @covers wp_modify_term_count_by
    48126     * @dataProvider data_term_counts_incremented_on_publish
    49127     * @ticket 40351
     
    85163
    86164    /**
     165     * Test post status transition update term counts correctly.
     166     *
     167     * @covers wp_modify_term_count_by
     168     * @dataProvider data_term_count_transitions_update_term_counts
     169     * @ticket 40351
     170     *
     171     * @param string $original_post_status Post status upon create.
     172     * @param string $new_post_status      Post status after update.
     173     * @param int    $change               Expected change upon publish.
     174     */
     175    function test_term_count_transitions_update_term_counts( $original_post_status, $new_post_status, $change ) {
     176        $post_id    = self::$post_ids[ $original_post_status ];
     177        $term_count = get_term( get_option( 'default_category' ) )->count;
     178
     179        wp_update_post(
     180            array(
     181                'ID'          => $post_id,
     182                'post_status' => $new_post_status,
     183            )
     184        );
     185
     186        $expected = $term_count + $change;
     187        $this->assertSame( $expected, get_term( get_option( 'default_category' ) )->count );
     188    }
     189
     190    /**
     191     * Data provider for test_term_count_transitions_update_term_counts.
     192     *
     193     * @return array[] {
     194     *     @type string $original_post_status Post status upon create.
     195     *     @type string $new_post_status      Post status after update.
     196     *     @type int    $change               Expected change upon publish.
     197     * }
     198     */
     199    function data_term_count_transitions_update_term_counts() {
     200        return array(
     201            // 0. Draft -> published post
     202            array( 'draft', 'publish', 1 ),
     203            // 1. Auto draft -> published post
     204            array( 'auto-draft', 'publish', 1 ),
     205            // 2. Private -> published post
     206            array( 'private', 'publish', 1 ),
     207            // 3. Published -> published post
     208            array( 'publish', 'publish', 0 ),
     209
     210            // 4. Draft -> private post
     211            array( 'draft', 'private', 0 ),
     212            // 5. Auto draft -> private post
     213            array( 'auto-draft', 'private', 0 ),
     214            // 6. Private -> private post
     215            array( 'private', 'private', 0 ),
     216            // 7. Published -> private post
     217            array( 'publish', 'private', -1 ),
     218
     219            // 8. Draft -> draft post
     220            array( 'draft', 'draft', 0 ),
     221            // 9. Auto draft -> draft post
     222            array( 'auto-draft', 'draft', 0 ),
     223            // 10. Private -> draft post
     224            array( 'private', 'draft', 0 ),
     225            // 11. Published -> draft post
     226            array( 'publish', 'draft', -1 ),
     227        );
     228    }
     229
     230    /**
     231     * Term counts are not double incremented when post created.
     232     *
     233     * @covers wp_modify_term_count_by
     234     * @dataProvider data_term_count_changes_for_post_statuses_with_attachments
     235     * @ticket 40351
     236     *
     237     * @param string $post_status New post status.
     238     * @param int    $change      Expected change.
     239     */
     240    public function test_term_count_changes_for_post_statuses_with_attachments( $post_status, $change ) {
     241        $term_count = get_term( self::$attachment_term )->count;
     242        // Do not use shared fixture for this test as it relies on a new post.
     243        $post_id = $this->factory()->post->create( array( 'post_status' => $post_status ) );
     244        wp_add_object_terms( $post_id, self::$attachment_term, 'wp_test_tax_counts' );
     245        $attachment_id = self::factory()->attachment->create_object(
     246            array(
     247                'file'        => 'image.jpg',
     248                'post_parent' => $post_id,
     249                'post_status' => 'inherit',
     250            )
     251        );
     252        wp_add_object_terms( $attachment_id, self::$attachment_term, 'wp_test_tax_counts' );
     253
     254        $expected = $term_count + $change;
     255        $this->assertSame( $expected, get_term( self::$attachment_term )->count );
     256    }
     257
     258    /**
     259     * Data provider for test_term_count_changes_for_post_statuses_with_attachments.
     260     *
     261     * @return array[] {
     262     *     @type string $post_status New post status.
     263     *     @type int    $change      Expected change.
     264     * }
     265     */
     266    function data_term_count_changes_for_post_statuses_with_attachments() {
     267        return array(
     268            // 0. Published post
     269            array( 'publish', 2 ),
     270            // 1. Auto draft
     271            array( 'auto-draft', 0 ),
     272            // 2. Draft
     273            array( 'draft', 0 ),
     274            // 3. Private post
     275            array( 'private', 0 ),
     276        );
     277    }
     278
     279    /**
    87280     * Term counts increments correctly when post status becomes published.
    88281     *
    89282     * @covers wp_publish_post
     283     * @covers wp_modify_term_count_by
    90284     * @dataProvider data_term_counts_incremented_on_publish_with_attachments
    91285     * @ticket 40351
     
    136330
    137331    /**
     332     * Test post status transition update term counts correctly.
     333     *
     334     * @covers wp_modify_term_count_by
     335     * @dataProvider data_term_count_transitions_update_term_counts_with_attachments
     336     * @ticket 40351
     337     *
     338     * @param string $original_post_status Post status upon create.
     339     * @param string $new_post_status      Post status after update.
     340     * @param int    $change               Expected change upon publish.
     341     */
     342    function test_term_count_transitions_update_term_counts_with_attachments( $original_post_status, $new_post_status, $change ) {
     343        $post_id = self::$post_ids[ $original_post_status ];
     344        wp_add_object_terms( $post_id, self::$attachment_term, 'wp_test_tax_counts' );
     345        $attachment_id = self::factory()->attachment->create_object(
     346            array(
     347                'file'        => 'image.jpg',
     348                'post_parent' => $post_id,
     349                'post_status' => 'inherit',
     350            )
     351        );
     352        wp_add_object_terms( $attachment_id, self::$attachment_term, 'wp_test_tax_counts' );
     353        $term_count = get_term( self::$attachment_term )->count;
     354
     355        wp_update_post(
     356            array(
     357                'ID'          => $post_id,
     358                'post_status' => $new_post_status,
     359            )
     360        );
     361
     362        $expected = $term_count + $change;
     363        $this->assertSame( $expected, get_term( self::$attachment_term )->count );
     364    }
     365
     366    /**
     367     * Data provider for test_term_count_transitions_update_term_counts_with_attachments.
     368     *
     369     * @return array[] {
     370     *     @type string $original_post_status Post status upon create.
     371     *     @type string $new_post_status      Post status after update.
     372     *     @type int    $change               Expected change upon publish.
     373     * }
     374     */
     375    function data_term_count_transitions_update_term_counts_with_attachments() {
     376        return array(
     377            // 0. Draft -> published post
     378            array( 'draft', 'publish', 2 ),
     379            // 1. Auto draft -> published post
     380            array( 'auto-draft', 'publish', 2 ),
     381            // 2. Private -> published post
     382            array( 'private', 'publish', 2 ),
     383            // 3. Published -> published post
     384            array( 'publish', 'publish', 0 ),
     385
     386            // 4. Draft -> private post
     387            array( 'draft', 'private', 0 ),
     388            // 5. Auto draft -> private post
     389            array( 'auto-draft', 'private', 0 ),
     390            // 6. Private -> private post
     391            array( 'private', 'private', 0 ),
     392            // 7. Published -> private post
     393            array( 'publish', 'private', -2 ),
     394
     395            // 8. Draft -> draft post
     396            array( 'draft', 'draft', 0 ),
     397            // 9. Auto draft -> draft post
     398            array( 'auto-draft', 'draft', 0 ),
     399            // 10. Private -> draft post
     400            array( 'private', 'draft', 0 ),
     401            // 11. Published -> draft post
     402            array( 'publish', 'draft', -2 ),
     403        );
     404    }
     405
     406    /**
     407     * Term counts are not double incremented when post created.
     408     *
     409     * @covers wp_modify_term_count_by
     410     * @dataProvider data_term_count_changes_for_post_statuses_with_untermed_attachments
     411     * @ticket 40351
     412     *
     413     * @param string $post_status New post status.
     414     * @param int    $change      Expected change.
     415     */
     416    public function test_term_count_changes_for_post_statuses_with_untermed_attachments( $post_status, $change ) {
     417        $term_count = get_term( self::$attachment_term )->count;
     418        // Do not use shared fixture for this test as it relies on a new post.
     419        $post_id = $this->factory()->post->create( array( 'post_status' => $post_status ) );
     420        wp_add_object_terms( $post_id, self::$attachment_term, 'wp_test_tax_counts' );
     421        $attachment_id = self::factory()->attachment->create_object(
     422            array(
     423                'file'        => 'image.jpg',
     424                'post_parent' => $post_id,
     425                'post_status' => 'inherit',
     426            )
     427        );
     428
     429        $expected = $term_count + $change;
     430        $this->assertSame( $expected, get_term( self::$attachment_term )->count );
     431    }
     432
     433    /**
     434     * Data provider for test_term_count_changes_for_post_statuses_with_untermed_attachments.
     435     *
     436     * @return array[] {
     437     *     @type string $post_status New post status.
     438     *     @type int    $change      Expected change.
     439     * }
     440     */
     441    function data_term_count_changes_for_post_statuses_with_untermed_attachments() {
     442        return array(
     443            // 0. Published post
     444            array( 'publish', 1 ),
     445            // 1. Auto draft
     446            array( 'auto-draft', 0 ),
     447            // 2. Draft
     448            array( 'draft', 0 ),
     449            // 3. Private post
     450            array( 'private', 0 ),
     451        );
     452    }
     453
     454    /**
    138455     * Term counts increments correctly when post status becomes published.
    139456     *
     457     * @covers wp_modify_term_count_by
    140458     * @covers wp_publish_post
    141459     * @dataProvider data_term_counts_incremented_on_publish_with_untermed_attachments
     
    184502        );
    185503    }
     504
     505    /**
     506     * Test post status transition update term counts correctly.
     507     *
     508     * @covers wp_modify_term_count_by
     509     * @dataProvider data_term_count_transitions_update_term_counts_with_untermed_attachments
     510     * @ticket 40351
     511     *
     512     * @param string $original_post_status Post status upon create.
     513     * @param string $new_post_status      Post status after update.
     514     * @param int    $change               Expected change upon publish.
     515     */
     516    function test_term_count_transitions_update_term_counts_with_untermed_attachments( $original_post_status, $new_post_status, $change ) {
     517        $post_id = self::$post_ids[ $original_post_status ];
     518        wp_add_object_terms( $post_id, self::$attachment_term, 'wp_test_tax_counts' );
     519        $attachment_id = self::factory()->attachment->create_object(
     520            array(
     521                'file'        => 'image.jpg',
     522                'post_parent' => $post_id,
     523                'post_status' => 'inherit',
     524            )
     525        );
     526        $term_count    = get_term( self::$attachment_term )->count;
     527
     528        wp_update_post(
     529            array(
     530                'ID'          => $post_id,
     531                'post_status' => $new_post_status,
     532            )
     533        );
     534
     535        $expected = $term_count + $change;
     536        $this->assertSame( $expected, get_term( self::$attachment_term )->count );
     537    }
     538
     539    /**
     540     * Data provider for test_term_count_transitions_update_term_counts_with_untermed_attachments.
     541     *
     542     * @return array[] {
     543     *     @type string $original_post_status Post status upon create.
     544     *     @type string $new_post_status      Post status after update.
     545     *     @type int    $change               Expected change upon publish.
     546     * }
     547     */
     548    function data_term_count_transitions_update_term_counts_with_untermed_attachments() {
     549        return array(
     550            // 0. Draft -> published post
     551            array( 'draft', 'publish', 1 ),
     552            // 1. Auto draft -> published post
     553            array( 'auto-draft', 'publish', 1 ),
     554            // 2. Private -> published post
     555            array( 'private', 'publish', 1 ),
     556            // 3. Published -> published post
     557            array( 'publish', 'publish', 0 ),
     558
     559            // 4. Draft -> private post
     560            array( 'draft', 'private', 0 ),
     561            // 5. Auto draft -> private post
     562            array( 'auto-draft', 'private', 0 ),
     563            // 6. Private -> private post
     564            array( 'private', 'private', 0 ),
     565            // 7. Published -> private post
     566            array( 'publish', 'private', -1 ),
     567
     568            // 8. Draft -> draft post
     569            array( 'draft', 'draft', 0 ),
     570            // 9. Auto draft -> draft post
     571            array( 'auto-draft', 'draft', 0 ),
     572            // 10. Private -> draft post
     573            array( 'private', 'draft', 0 ),
     574            // 11. Published -> draft post
     575            array( 'publish', 'draft', -1 ),
     576        );
     577    }
     578
     579    /**
     580     * User taxonomy term counts increments when added to an account.
     581     *
     582     * @covers wp_modify_term_count_by
     583     * @ticket 51292
     584     */
     585    public function test_term_counts_user_adding_term() {
     586        $term_count = get_term( self::$user_term )->count;
     587        wp_add_object_terms( self::$user_id, self::$user_term, 'wp_test_user_tax_counts' );
     588
     589        $expected = $term_count + 1;
     590        $this->assertSame( $expected, get_term( self::$user_term )->count );
     591    }
     592
     593    /**
     594     * User taxonomy term counts decrement when term deleted from user.
     595     *
     596     * @covers wp_modify_term_count_by
     597     * @ticket 51292
     598     */
     599    public function test_term_counts_user_removing_term() {
     600        wp_add_object_terms( self::$user_id, self::$user_term, 'wp_test_user_tax_counts' );
     601        $term_count = get_term( self::$user_term )->count;
     602
     603        wp_remove_object_terms( self::$user_id, self::$user_term, 'wp_test_user_tax_counts' );
     604        $expected = $term_count - 1;
     605        $this->assertSame( $expected, get_term( self::$user_term )->count );
     606    }
     607
     608    /**
     609     * Ensure DB queries for deferred counts are nullified for net zero gain.
     610     *
     611     * @covers wp_modify_term_count_by
     612     * @covers wp_defer_term_counting
     613     * @ticket 51292
     614     */
     615    public function test_counts_after_deferral_net_zero() {
     616        $post_one = self::$post_ids['publish'];
     617        $post_two = self::$post_ids['publish_two'];
     618        $terms    = self::$tag_ids;
     619
     620        wp_set_object_terms( $post_one, $terms[0], 'post_tag', true );
     621
     622        // Net gain 0;
     623        wp_defer_term_counting( true );
     624        wp_remove_object_terms( $post_one, $terms[0], 'post_tag' );
     625        wp_set_object_terms( $post_two, $terms[0], 'post_tag', true );
     626        $num_queries = get_num_queries();
     627        wp_defer_term_counting( false );
     628        // Ensure number of queries unchanged.
     629        $this->assertSame( $num_queries, get_num_queries() );
     630    }
     631
     632    /**
     633     * Ensure full recounts follow modify by X recounts to avoid miscounts.
     634     *
     635     * @covers wp_modify_term_count_by
     636     * @covers wp_update_term_count
     637     * @covers wp_defer_term_counting
     638     * @ticket 51292
     639     */
     640    public function test_counts_after_deferral_full_before_partial() {
     641        $post_one   = self::$post_ids['publish'];
     642        $terms      = self::$tag_ids;
     643        $term_count = get_term( $terms[0] )->count;
     644
     645        // Net gain 1;
     646        wp_defer_term_counting( true );
     647        wp_set_object_terms( $post_one, $terms[0], 'post_tag', true );
     648        wp_update_term_count( get_term( $terms[0] )->term_taxonomy_id, 'post_tag' );
     649        wp_defer_term_counting( false );
     650
     651        // Ensure term count is correct.
     652        $expected = $term_count + 1;
     653        $this->assertSame( $expected, get_term( $terms[0] )->count );
     654    }
     655
     656    /**
     657     * Ensure DB queries for deferred counts are combined.
     658     *
     659     * @covers wp_modify_term_count_by
     660     * @covers wp_defer_term_counting
     661     * @ticket 51292
     662     */
     663    public function test_counts_after_deferral_matching_changes() {
     664        $post_one = self::$post_ids['publish'];
     665        $post_two = self::$post_ids['publish_two'];
     666        $terms    = self::$tag_ids;
     667
     668        wp_set_object_terms( $post_one, $terms[0], 'post_tag', true );
     669
     670        // Net gain 0:
     671        wp_defer_term_counting( true );
     672        wp_remove_object_terms( $post_one, $terms[0], 'post_tag' );
     673        wp_set_object_terms( $post_two, $terms[0], 'post_tag', true );
     674
     675        // Net gain 1:
     676        wp_set_object_terms( $post_one, $terms[1], 'post_tag', true );
     677        wp_set_object_terms( $post_two, $terms[2], 'post_tag', true );
     678
     679        // Net gain 2:
     680        wp_set_object_terms( $post_one, array( $terms[3], $terms[4] ), 'post_tag', true );
     681        wp_set_object_terms( $post_two, array( $terms[3], $terms[4] ), 'post_tag', true );
     682
     683        $num_queries = get_num_queries();
     684        wp_defer_term_counting( false );
     685
     686        /*
     687         * Each count is expected to produce two queries:
     688         * 1) The count update
     689         * 2) The SELECT in `clean_term_cache()`.
     690         */
     691        $expected = $num_queries + ( 2 * 2 );
     692        // Ensure number of queries correct.
     693        $this->assertSame( $expected, get_num_queries() );
     694    }
    186695}
Note: See TracChangeset for help on using the changeset viewer.