WordPress.org

Make WordPress Core

Ticket #40351: 40351.2.diff

File 40351.2.diff, 34.7 KB (added by peterwilsoncc, 10 months ago)
  • src/wp-includes/class-wp-taxonomy.php

    diff --git a/src/wp-includes/class-wp-taxonomy.php b/src/wp-includes/class-wp-taxonomy.php
    index c34a03c38e..21f3d4aec9 100644
    a b final class WP_Taxonomy { 
    180180         */
    181181        public $update_count_callback;
    182182
     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;
     190
    183191        /**
    184192         * Whether this taxonomy should appear in the REST API.
    185193         *
    public function set_props( $object_type, $args ) { 
    277285                $args = apply_filters( 'register_taxonomy_args', $args, $this->name, (array) $object_type );
    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
    304313                $args = array_merge( $defaults, $args );
    public function set_props( $object_type, $args ) { 
    411420                        );
    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;
    416436                }
  • src/wp-includes/post.php

    diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php
    index 9456bec763..988c7ae79f 100644
    a b function wp_insert_post( $postarr, $wp_error = false ) { 
    40354035                clean_post_cache( $post_ID );
    40364036        }
    40374037
     4038        // Allow term counts to be handled by transitioning post type.
     4039        _wp_prevent_term_counting( true );
    40384040        if ( is_object_in_taxonomy( $post_type, 'category' ) ) {
    40394041                wp_set_post_categories( $post_ID, $post_category );
    40404042        }
    function wp_insert_post( $postarr, $wp_error = false ) { 
    40914093                        }
    40924094                }
    40934095        }
     4096        // Restore term counting.
     4097        _wp_prevent_term_counting( false );
    40944098
    40954099        if ( ! empty( $postarr['meta_input'] ) ) {
    40964100                foreach ( $postarr['meta_input'] as $field => $value ) {
    function wp_publish_post( $post ) { 
    43994403                if ( ! $default_term_id ) {
    44004404                        continue;
    44014405                }
     4406                _wp_prevent_term_counting( true );
    44024407                wp_set_post_terms( $post->ID, array( $default_term_id ), $taxonomy );
     4408                _wp_prevent_term_counting( false );
    44034409        }
    44044410
    44054411        $wpdb->update( $wpdb->posts, array( 'post_status' => 'publish' ), array( 'ID' => $post->ID ) );
    function wp_queue_posts_for_term_meta_lazyload( $posts ) { 
    72657271 * @param WP_Post $post       Post object.
    72667272 */
    72677273function _update_term_count_on_transition_post_status( $new_status, $old_status, $post ) {
    7268         // Update counts for the post's terms.
     7274        if ( 'inherit' === $new_status ) {
     7275                $new_status = get_post_status( $post->post_parent );
     7276        }
     7277
     7278        if ( 'inherit' === $old_status ) {
     7279                $old_status = get_post_status( $post->post_parent );
     7280        }
     7281
     7282        $count_new = 'publish' === $new_status;
     7283        $count_old = 'publish' === $old_status;
     7284
     7285        if ( $count_new === $count_old ) {
     7286                // Nothing to do.
     7287                return;
     7288        }
     7289
     7290        /*
     7291         * Update counts for the post's terms.
     7292         *
     7293         * Term counting is deferred while incrementing/decrementing the counts to
     7294         * reduce the number of database queries required. Once the counts are
     7295         * complete the updates are performed if term counting wasn't previously
     7296         * deferred.
     7297         */
     7298        $previous_deferred_setting = wp_defer_term_counting();
     7299        wp_defer_term_counting( true );
    72697300        foreach ( (array) get_object_taxonomies( $post->post_type ) as $taxonomy ) {
    72707301                $tt_ids = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'tt_ids' ) );
    7271                 wp_update_term_count( $tt_ids, $taxonomy );
     7302
     7303                if ( empty( $tt_ids ) ) {
     7304                        // No terms for this taxonomy on object.
     7305                        continue;
     7306                }
     7307
     7308                $object_types = (array) get_taxonomy( $taxonomy )->object_type;
     7309
     7310                foreach ( $object_types as &$object_type ) {
     7311                        list( $object_type ) = explode( ':', $object_type );
     7312                }
     7313
     7314                $object_types = array_unique( $object_types );
     7315
     7316                if ( ! in_array( $post->post_type, $object_types, true ) ) {
     7317                        $modify_by = 0;
     7318                } elseif ( $count_new && ! $count_old ) {
     7319                        $modify_by = 1;
     7320                } elseif ( $count_old && ! $count_new ) {
     7321                        $modify_by = -1;
     7322                }
     7323
     7324                if ( 'attachment' === $post->post_type ) {
     7325                        wp_modify_term_count_by( $tt_ids, $taxonomy, $modify_by );
     7326                        continue;
     7327                }
     7328
     7329                $check_attachments = array_search( 'attachment', $object_types, true );
     7330                if ( false !== $check_attachments ) {
     7331                        unset( $object_types[ $check_attachments ] );
     7332                        $check_attachments = true;
     7333                }
     7334
     7335                wp_modify_term_count_by( $tt_ids, $taxonomy, $modify_by );
     7336                if ( ! $check_attachments ) {
     7337                        continue;
     7338                }
     7339
     7340                /*
     7341                 * For non-attachments, check if there are any attachment children
     7342                 * with 'inherited' post status -- if so those will need to be counted.
     7343                 */
     7344                $attachments = get_children(
     7345                        array(
     7346                                'post_parent'            => $post->ID,
     7347                                'post_status'            => 'inherit',
     7348                                'post_type'              => 'attachment',
     7349                                'update_post_meta_cache' => false,
     7350                                'update_post_term_cache' => true,
     7351                        )
     7352                );
     7353
     7354                foreach ( $attachments as $attachment ) {
     7355                        _update_term_count_on_transition_post_status( $new_status, $old_status, $attachment );
     7356                }
    72727357        }
     7358        wp_defer_term_counting( $previous_deferred_setting );
    72737359}
    72747360
    72757361/**
  • src/wp-includes/taxonomy.php

    diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php
    index cc491a87dd..cfdb09b626 100644
    a b function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) { 
    25612561                return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
    25622562        }
    25632563
     2564        $taxonomy_object = get_taxonomy( $taxonomy );
     2565
     2566        $object_types = (array) $taxonomy_object->object_type;
     2567        foreach ( $object_types as &$object_type ) {
     2568                if ( 0 === strpos( $object_type, 'attachment:' ) ) {
     2569                        list( $object_type ) = explode( ':', $object_type );
     2570                }
     2571        }
     2572
     2573        if ( array_filter( $object_types, 'post_type_exists' ) !== $object_types ) {
     2574                // This taxonomy applies to non-posts, count changes now.
     2575                $do_recount = ! _wp_prevent_term_counting();
     2576        } elseif ( 'publish' === get_post_status( $object_id ) ) {
     2577                // Published post, count changes now.
     2578                $do_recount = ! _wp_prevent_term_counting();
     2579        } else {
     2580                $do_recount = false;
     2581        }
     2582
    25642583        if ( ! is_array( $terms ) ) {
    25652584                $terms = array( $terms );
    25662585        }
    function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) { 
    26462665                $new_tt_ids[] = $tt_id;
    26472666        }
    26482667
    2649         if ( $new_tt_ids ) {
    2650                 wp_update_term_count( $new_tt_ids, $taxonomy );
     2668        if ( $new_tt_ids && $do_recount ) {
     2669                wp_increment_term_count( $new_tt_ids, $taxonomy );
    26512670        }
    26522671
    26532672        if ( ! $append ) {
    function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) { 
    26652684                }
    26662685        }
    26672686
    2668         $t = get_taxonomy( $taxonomy );
    2669 
    2670         if ( ! $append && isset( $t->sort ) && $t->sort ) {
     2687        if ( ! $append && isset( $taxonomy_object->sort ) && $taxonomy_object->sort ) {
    26712688                $values     = array();
    26722689                $term_order = 0;
    26732690
    function wp_remove_object_terms( $object_id, $terms, $taxonomy ) { 
    27482765                return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
    27492766        }
    27502767
     2768        $taxonomy_object = get_taxonomy( $taxonomy );
     2769
     2770        $object_types = (array) $taxonomy_object->object_type;
     2771        foreach ( $object_types as &$object_type ) {
     2772                if ( 0 === strpos( $object_type, 'attachment:' ) ) {
     2773                        list( $object_type ) = explode( ':', $object_type );
     2774                }
     2775        }
     2776
     2777        if ( array_filter( $object_types, 'post_type_exists' ) !== $object_types ) {
     2778                // This taxonomy applies to non-posts, count changes now.
     2779                $do_recount = ! _wp_prevent_term_counting();
     2780        } elseif (
     2781                'publish' === get_post_status( $object_id ) ||
     2782                (
     2783                        'inherit' === get_post_status( $object_id ) &&
     2784                        'publish' === get_post_status( wp_get_post_parent_id( $object_id ) )
     2785                )
     2786        ) {
     2787                // Published post, count changes now.
     2788                $do_recount = ! _wp_prevent_term_counting();
     2789        } else {
     2790                $do_recount = false;
     2791        }
     2792
    27512793        if ( ! is_array( $terms ) ) {
    27522794                $terms = array( $terms );
    27532795        }
    function wp_remove_object_terms( $object_id, $terms, $taxonomy ) { 
    28062848                 */
    28072849                do_action( 'deleted_term_relationships', $object_id, $tt_ids, $taxonomy );
    28082850
    2809                 wp_update_term_count( $tt_ids, $taxonomy );
     2851                if ( $do_recount ) {
     2852                        wp_decrement_term_count( $tt_ids, $taxonomy );
     2853                }
    28102854
    28112855                return (bool) $deleted;
    28122856        }
    function wp_defer_term_counting( $defer = null ) { 
    32263270                $_defer = $defer;
    32273271                // Flush any deferred counts.
    32283272                if ( ! $defer ) {
     3273                        wp_modify_term_count_by( null, null, null, true );
    32293274                        wp_update_term_count( null, null, true );
    32303275                }
    32313276        }
    function wp_defer_term_counting( $defer = null ) { 
    32333278        return $_defer;
    32343279}
    32353280
     3281/**
     3282 * Prevents add/removing a term from modifying a term count.
     3283 *
     3284 * This is used by functions calling wp_transition_post_status() to indicate the
     3285 * term count will be handled during the post's transition.
     3286 *
     3287 * @private
     3288 * @since 5.6.0
     3289 *
     3290 * @param bool $new_setting The new setting for preventing term counts.
     3291 * @return bool Whether term count prevention is enabled or disabled.
     3292 */
     3293function _wp_prevent_term_counting( $new_setting = null ) {
     3294        static $prevent = false;
     3295
     3296        if ( is_bool( $new_setting ) ) {
     3297                $prevent = $new_setting;
     3298        }
     3299
     3300        return $prevent;
     3301}
     3302
     3303/**
     3304 * Increments the amount of terms in taxonomy.
     3305 *
     3306 * If there is a taxonomy callback applied, then it will be called for updating
     3307 * the count.
     3308 *
     3309 * The default action is to increment the count by one and update the database.
     3310 *
     3311 * @since 5.6.0
     3312 *
     3313 * @param int|array $tt_ids       The term_taxonomy_id of the terms.
     3314 * @param string    $taxonomy     The context of the term.
     3315 * @param int       $increment_by By how many the term count is to be incremented. Default 1.
     3316 * @param bool      $do_deferred  Whether to flush the deferred term counts too. Default false.
     3317 * @return bool If no terms will return false, and if successful will return true.
     3318 */
     3319function wp_increment_term_count( $tt_ids, $taxonomy, $increment_by = 1, $do_deferred = false ) {
     3320        return wp_modify_term_count_by( $tt_ids, $taxonomy, $increment_by, $do_deferred );
     3321}
     3322
     3323/**
     3324 * Decrements the amount of terms in taxonomy.
     3325 *
     3326 * If there is a taxonomy callback applied, then it will be called for updating
     3327 * the count.
     3328 *
     3329 * The default action is to decrement the count by one and update the database.
     3330 *
     3331 * @since 5.6.0
     3332 *
     3333 * @param int|array $tt_ids       The term_taxonomy_id of the terms.
     3334 * @param string    $taxonomy     The context of the term.
     3335 * @param int       $decrement_by By how many the term count is to be decremented. Default 1.
     3336 * @param bool      $do_deferred  Whether to flush the deferred term counts too. Default false.
     3337 * @return bool If no terms will return false, and if successful will return true.
     3338 */
     3339function wp_decrement_term_count( $tt_ids, $taxonomy, $decrement_by = 1, $do_deferred = false ) {
     3340        return wp_modify_term_count_by( $tt_ids, $taxonomy, $decrement_by * -1, $do_deferred );
     3341}
     3342
     3343/**
     3344 * Modifies the amount of terms in taxonomy.
     3345 *
     3346 * If there is a taxonomy callback applied, then it will be called for updating
     3347 * the count.
     3348 *
     3349 * The default action is to decrement the count by one and update the database.
     3350 *
     3351 * @since 5.6.0
     3352 *
     3353 * @param int|array $tt_ids      The term_taxonomy_id of the terms.
     3354 * @param string    $taxonomy    The context of the term.
     3355 * @param int       $modify_by   By how many the term count is to be modified.
     3356 * @param bool      $do_deferred Whether to flush the deferred term counts too. Default false.
     3357 * @return bool If no terms will return false, and if successful will return true.
     3358 */
     3359function wp_modify_term_count_by( $tt_ids, $taxonomy, $modify_by, $do_deferred = false ) {
     3360        static $_deferred = array();
     3361
     3362        if ( $do_deferred ) {
     3363                foreach ( (array) $_deferred as $taxonomy_name => $modifications ) {
     3364                        $tax_by_count = array_reduce(
     3365                                array_keys( $modifications ),
     3366                                function( $by_count, $tt_id ) use ( $modifications ) {
     3367                                        if ( ! isset( $by_count[ $modifications[ $tt_id ] ] ) ) {
     3368                                                $by_count[ $modifications[ $tt_id ] ] = array();
     3369                                        }
     3370                                        $by_count[ $modifications[ $tt_id ] ][] = $tt_id;
     3371                                        return $by_count;
     3372                                },
     3373                                array()
     3374                        );
     3375
     3376                        foreach ( $tax_by_count as $_modify_by => $_tt_ids ) {
     3377                                wp_modify_term_count_by_now( $_tt_ids, $taxonomy_name, $_modify_by );
     3378                        }
     3379                        unset( $_deferred[ $taxonomy_name ] );
     3380                }
     3381        }
     3382
     3383        if ( empty( $tt_ids ) ) {
     3384                return false;
     3385        }
     3386
     3387        if ( ! is_array( $tt_ids ) ) {
     3388                $tt_ids = array( $tt_ids );
     3389        }
     3390
     3391        if ( wp_defer_term_counting() ) {
     3392                foreach ( $tt_ids as $tt_id ) {
     3393                        if ( ! isset( $_deferred[ $taxonomy ][ $tt_id ] ) ) {
     3394                                $_deferred[ $taxonomy ][ $tt_id ] = 0;
     3395                        }
     3396                        $_deferred[ $taxonomy ][ $tt_id ] += $modify_by;
     3397                }
     3398                return true;
     3399        }
     3400
     3401        return wp_modify_term_count_by_now( $tt_ids, $taxonomy, $modify_by );
     3402}
     3403
     3404/**
     3405 * Modifies the amount of terms in taxonomy immediately
     3406 *
     3407 * If there is a taxonomy callback applied, then it will be called for updating
     3408 * the count.
     3409 *
     3410 * The default action is to decrement the count by one and update the database.
     3411 *
     3412 * @since 5.6.0
     3413 *
     3414 * @param int|array $tt_ids      The term_taxonomy_id of the terms.
     3415 * @param string    $taxonomy    The context of the term.
     3416 * @param int       $modify_by   By how many the term count is to be modified.
     3417 * @return bool If no terms will return false, and if successful will return true.
     3418 */
     3419function wp_modify_term_count_by_now( $tt_ids, $taxonomy, $modify_by ) {
     3420        global $wpdb;
     3421
     3422        if ( 0 === $modify_by ) {
     3423                return false;
     3424        }
     3425
     3426        $tt_ids = array_filter( array_map( 'intval', (array) $tt_ids ) );
     3427
     3428        if ( empty( $tt_ids ) ) {
     3429                return false;
     3430        }
     3431
     3432        $taxonomy = get_taxonomy( $taxonomy );
     3433        if ( ! empty( $taxonomy->update_count_by_callback ) ) {
     3434                call_user_func( $taxonomy->update_count_by_callback, $tt_ids, $taxonomy, $modify_by );
     3435                clean_term_cache( $tt_ids, '', false );
     3436                return true;
     3437        }
     3438
     3439        $tt_ids_string = '(' . implode( ',', $tt_ids ) . ')';
     3440
     3441        foreach ( $tt_ids as $tt_id ) {
     3442                /** This action is documented in wp-includes/taxonomy.php */
     3443                do_action( 'edit_term_taxonomy', $tt_id, $taxonomy );
     3444        }
     3445
     3446        $result = $wpdb->query(
     3447                $wpdb->prepare(
     3448                        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     3449                        "UPDATE {$wpdb->term_taxonomy} AS tt SET tt.count = GREATEST( 0, tt.count + %d ) WHERE tt.term_taxonomy_id IN $tt_ids_string",
     3450                        $modify_by
     3451                )
     3452        );
     3453
     3454        if ( ! $result ) {
     3455                return false;
     3456        }
     3457
     3458        foreach ( $tt_ids as $tt_id ) {
     3459                /** This action is documented in wp-includes/taxonomy.php */
     3460                do_action( 'edited_term_taxonomy', $tt_id, $taxonomy );
     3461        }
     3462
     3463        clean_term_cache( $tt_ids, '', false );
     3464
     3465        return true;
     3466}
     3467
    32363468/**
    32373469 * Updates the amount of terms in taxonomy.
    32383470 *
  • tests/phpunit/tests/term/termCounts.php

    diff --git a/tests/phpunit/tests/term/termCounts.php b/tests/phpunit/tests/term/termCounts.php
    index 284d5f90ea..6e197c5d07 100644
    a b class Tests_Term_termCount extends WP_UnitTestCase { 
    1919         */
    2020        public static $post_ids;
    2121
     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;
     42
    2243        /**
    2344         * Create shared fixtures.
    2445         *
    public static function wpSetUpBeforeClass( $factory ) { 
    3051                        self::$post_ids[ $status ] = $factory->post->create( array( 'post_status' => $status ) );
    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();
     67                self::register_taxonomies();
     68        }
    3969
     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
    43121        /**
    44122         * Term counts increments correctly when post status becomes published.
    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
    50128         * @ticket 51292
    function data_term_counts_incremented_on_publish() { 
    83161                );
    84162        }
    85163
     164        /**
     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
    86279        /**
    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
    92286         * @ticket 51292
    function data_term_counts_incremented_on_publish_with_attachments() { 
    134328                );
    135329        }
    136330
     331        /**
     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
    137454        /**
    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
    142460         * @ticket 40351
    function data_term_counts_incremented_on_publish_with_untermed_attachments() { 
    183501                        array( 'private', 1 ),
    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}