: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* Adds count of children to parent count.
* Recalculates term counts by including items from child terms. Assumes all
* relevant children are already in the $terms argument.
* @global wpdb $wpdb WordPress database abstraction object.
* @param object[]|WP_Term[] $terms List of term objects (passed by reference).
* @param string $taxonomy Term context.
function _pad_term_counts( &$terms, $taxonomy ) {
// This function only works for hierarchical taxonomies like post categories.
if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
$term_hier = _get_term_hierarchy( $taxonomy );
if ( empty( $term_hier ) ) {
foreach ( (array) $terms as $key => $term ) {
$terms_by_id[ $term->term_id ] = & $terms[ $key ];
$term_ids[ $term->term_taxonomy_id ] = $term->term_id;
// Get the object and term IDs and stick them in a lookup table.
$tax_obj = get_taxonomy( $taxonomy );
$object_types = esc_sql( $tax_obj->object_type );
$results = $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships INNER JOIN $wpdb->posts ON object_id = ID WHERE term_taxonomy_id IN (" . implode( ',', array_keys( $term_ids ) ) . ") AND post_type IN ('" . implode( "', '", $object_types ) . "') AND post_status = 'publish'" );
foreach ( $results as $row ) {
$id = $term_ids[ $row->term_taxonomy_id ];
$term_items[ $id ][ $row->object_id ] = isset( $term_items[ $id ][ $row->object_id ] ) ? ++$term_items[ $id ][ $row->object_id ] : 1;
// Touch every ancestor's lookup row for each post in each term.
foreach ( $term_ids as $term_id ) {
while ( ! empty( $terms_by_id[ $child ] ) && $parent = $terms_by_id[ $child ]->parent ) {
if ( ! empty( $term_items[ $term_id ] ) ) {
foreach ( $term_items[ $term_id ] as $item_id => $touches ) {
$term_items[ $parent ][ $item_id ] = isset( $term_items[ $parent ][ $item_id ] ) ? ++$term_items[ $parent ][ $item_id ] : 1;
if ( in_array( $parent, $ancestors, true ) ) {
// Transfer the touched cells.
foreach ( (array) $term_items as $id => $items ) {
if ( isset( $terms_by_id[ $id ] ) ) {
$terms_by_id[ $id ]->count = count( $items );
* Adds any terms from the given IDs to the cache that do not already exist in cache.
* @since 6.1.0 This function is no longer marked as "private".
* @since 6.3.0 Use wp_lazyload_term_meta() for lazy-loading of term meta.
* @global wpdb $wpdb WordPress database abstraction object.
* @param array $term_ids Array of term IDs.
* @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true.
function _prime_term_caches( $term_ids, $update_meta_cache = true ) {
$non_cached_ids = _get_non_cached_ids( $term_ids, 'terms' );
if ( ! empty( $non_cached_ids ) ) {
$fresh_terms = $wpdb->get_results( sprintf( "SELECT t.*, tt.* FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE t.term_id IN (%s)", implode( ',', array_map( 'intval', $non_cached_ids ) ) ) );
update_term_cache( $fresh_terms );
if ( $update_meta_cache ) {
wp_lazyload_term_meta( $term_ids );
* Updates term count based on object types of the current taxonomy.
* Private function for the default callback for post_tag and category
* @global wpdb $wpdb WordPress database abstraction object.
* @param int[] $terms List of term taxonomy IDs.
* @param WP_Taxonomy $taxonomy Current taxonomy object of terms.
function _update_post_term_count( $terms, $taxonomy ) {
$object_types = (array) $taxonomy->object_type;
foreach ( $object_types as &$object_type ) {
list( $object_type ) = explode( ':', $object_type );
$object_types = array_unique( $object_types );
$check_attachments = array_search( 'attachment', $object_types, true );
if ( false !== $check_attachments ) {
unset( $object_types[ $check_attachments ] );
$check_attachments = true;
$object_types = esc_sql( array_filter( $object_types, 'post_type_exists' ) );
$post_statuses = array( 'publish' );
* Filters the post statuses for updating the term count.
* @param string[] $post_statuses List of post statuses to include in the count. Default is 'publish'.
* @param WP_Taxonomy $taxonomy Current taxonomy object.
$post_statuses = esc_sql( apply_filters( 'update_post_term_count_statuses', $post_statuses, $taxonomy ) );
foreach ( (array) $terms as $term ) {
// Attachments can be 'inherit' status, we need to base count off the parent's status if so.
if ( $check_attachments ) {
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
$count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts p1 WHERE p1.ID = $wpdb->term_relationships.object_id AND ( post_status IN ('" . implode( "', '", $post_statuses ) . "') OR ( post_status = 'inherit' AND post_parent > 0 AND ( SELECT post_status FROM $wpdb->posts WHERE ID = p1.post_parent ) IN ('" . implode( "', '", $post_statuses ) . "') ) ) AND post_type = 'attachment' AND term_taxonomy_id = %d", $term ) );
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
$count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts WHERE $wpdb->posts.ID = $wpdb->term_relationships.object_id AND post_status IN ('" . implode( "', '", $post_statuses ) . "') AND post_type IN ('" . implode( "', '", $object_types ) . "') AND term_taxonomy_id = %d", $term ) );
/** This action is documented in wp-includes/taxonomy.php */
do_action( 'edit_term_taxonomy', $term, $taxonomy->name );
$wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) );
/** This action is documented in wp-includes/taxonomy.php */
do_action( 'edited_term_taxonomy', $term, $taxonomy->name );
* Updates term count based on number of objects.
* Default callback for the 'link_category' taxonomy.
* @global wpdb $wpdb WordPress database abstraction object.
* @param int[] $terms List of term taxonomy IDs.
* @param WP_Taxonomy $taxonomy Current taxonomy object of terms.
function _update_generic_term_count( $terms, $taxonomy ) {
foreach ( (array) $terms as $term ) {
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $term ) );
/** This action is documented in wp-includes/taxonomy.php */
do_action( 'edit_term_taxonomy', $term, $taxonomy->name );
$wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) );
/** This action is documented in wp-includes/taxonomy.php */
do_action( 'edited_term_taxonomy', $term, $taxonomy->name );
* Creates a new term for a term_taxonomy item that currently shares its term
* with another term_taxonomy.
* @since 4.3.0 Introduced `$record` parameter. Also, `$term_id` and
* `$term_taxonomy_id` can now accept objects.
* @global wpdb $wpdb WordPress database abstraction object.
* @param int|object $term_id ID of the shared term, or the shared term object.
* @param int|object $term_taxonomy_id ID of the term_taxonomy item to receive a new term, or the term_taxonomy object
* (corresponding to a row from the term_taxonomy table).
* @param bool $record Whether to record data about the split term in the options table. The recording
* process has the potential to be resource-intensive, so during batch operations
* it can be beneficial to skip inline recording and do it just once, after the
* batch is processed. Only set this to `false` if you know what you are doing.
* @return int|WP_Error When the current term does not need to be split (or cannot be split on the current
* database schema), `$term_id` is returned. When the term is successfully split, the
* new term_id is returned. A WP_Error is returned for miscellaneous errors.
function _split_shared_term( $term_id, $term_taxonomy_id, $record = true ) {
if ( is_object( $term_id ) ) {
$term_id = (int) $shared_term->term_id;
if ( is_object( $term_taxonomy_id ) ) {
$term_taxonomy = $term_taxonomy_id;
$term_taxonomy_id = (int) $term_taxonomy->term_taxonomy_id;
// If there are no shared term_taxonomy rows, there's nothing to do here.
$shared_tt_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy tt WHERE tt.term_id = %d AND tt.term_taxonomy_id != %d", $term_id, $term_taxonomy_id ) );
if ( ! $shared_tt_count ) {
* Verify that the term_taxonomy_id passed to the function is actually associated with the term_id.
* If there's a mismatch, it may mean that the term is already split. Return the actual term_id from the db.
$check_term_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) );
if ( $check_term_id !== $term_id ) {
// Pull up data about the currently shared slug, which we'll use to populate the new one.
if ( empty( $shared_term ) ) {
$shared_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.* FROM $wpdb->terms t WHERE t.term_id = %d", $term_id ) );
'name' => $shared_term->name,
'slug' => $shared_term->slug,
'term_group' => $shared_term->term_group,
if ( false === $wpdb->insert( $wpdb->terms, $new_term_data ) ) {
return new WP_Error( 'db_insert_error', __( 'Could not split shared term.' ), $wpdb->last_error );
$new_term_id = (int) $wpdb->insert_id;
// Update the existing term_taxonomy to point to the newly created term.
array( 'term_id' => $new_term_id ),
array( 'term_taxonomy_id' => $term_taxonomy_id )
// Reassign child terms to the new parent.
if ( empty( $term_taxonomy ) ) {
$term_taxonomy = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) );
$children_tt_ids = $wpdb->get_col( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE parent = %d AND taxonomy = %s", $term_id, $term_taxonomy->taxonomy ) );
if ( ! empty( $children_tt_ids ) ) {
foreach ( $children_tt_ids as $child_tt_id ) {
array( 'parent' => $new_term_id ),
array( 'term_taxonomy_id' => $child_tt_id )
clean_term_cache( (int) $child_tt_id, '', false );
// If the term has no children, we must force its taxonomy cache to be rebuilt separately.
clean_term_cache( $new_term_id, $term_taxonomy->taxonomy, false );
clean_term_cache( $term_id, $term_taxonomy->taxonomy, false );
* Taxonomy cache clearing is delayed to avoid race conditions that may occur when
* regenerating the taxonomy's hierarchy tree.
$taxonomies_to_clean = array( $term_taxonomy->taxonomy );
// Clean the cache for term taxonomies formerly shared with the current term.
$shared_term_taxonomies = $wpdb->get_col( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) );
$taxonomies_to_clean = array_merge( $taxonomies_to_clean, $shared_term_taxonomies );
foreach ( $taxonomies_to_clean as $taxonomy_to_clean ) {
clean_taxonomy_cache( $taxonomy_to_clean );
// Keep a record of term_ids that have been split, keyed by old term_id. See wp_get_split_term().
$split_term_data = get_option( '_split_terms', array() );
if ( ! isset( $split_term_data[ $term_id ] ) ) {
$split_term_data[ $term_id ] = array();
$split_term_data[ $term_id ][ $term_taxonomy->taxonomy ] = $new_term_id;
update_option( '_split_terms', $split_term_data );
// If we've just split the final shared term, set the "finished" flag.
$shared_terms_exist = $wpdb->get_results(
"SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt
LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
if ( ! $shared_terms_exist ) {
update_option( 'finished_splitting_shared_terms', true );
* Fires after a previously shared taxonomy term is split into two separate terms.
* @param int $term_id ID of the formerly shared term.
* @param int $new_term_id ID of the new term created for the $term_taxonomy_id.
* @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split.
* @param string $taxonomy Taxonomy for the split term.
do_action( 'split_shared_term', $term_id, $new_term_id, $term_taxonomy_id, $term_taxonomy->taxonomy );
* Splits a batch of shared taxonomy terms.
* @global wpdb $wpdb WordPress database abstraction object.
function _wp_batch_split_terms() {
$lock_name = 'term_split.lock';
$lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'off') /* LOCK */", $lock_name, time() ) );
$lock_result = get_option( $lock_name );
// Bail if we were unable to create a lock, or if the existing lock is still valid.
if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) {
wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' );
// Update the lock, as by this point we've definitely got a lock, just need to fire the actions.
update_option( $lock_name, time() );
// Get a list of shared terms (those with more than one associated row in term_taxonomy).
$shared_terms = $wpdb->get_results(
"SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt
LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
// No more terms, we're done here.
update_option( 'finished_splitting_shared_terms', true );
delete_option( $lock_name );
// Shared terms found? We'll need to run this script again.
wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' );
// Rekey shared term array for faster lookups.
$_shared_terms = array();
foreach ( $shared_terms as $shared_term ) {
$term_id = (int) $shared_term->term_id;
$_shared_terms[ $term_id ] = $shared_term;
$shared_terms = $_shared_terms;
// Get term taxonomy data for all shared terms.
$shared_term_ids = implode( ',', array_keys( $shared_terms ) );
$shared_tts = $wpdb->get_results( "SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})" );
// Split term data recording is slow, so we do it just once, outside the loop.
$split_term_data = get_option( '_split_terms', array() );
$skipped_first_term = array();
foreach ( $shared_tts as $shared_tt ) {
$term_id = (int) $shared_tt->term_id;
// Don't split the first tt belonging to a given term_id.
if ( ! isset( $skipped_first_term[ $term_id ] ) ) {
$skipped_first_term[ $term_id ] = 1;
if ( ! isset( $split_term_data[ $term_id ] ) ) {
$split_term_data[ $term_id ] = array();
// Keep track of taxonomies whose hierarchies need flushing.
if ( ! isset( $taxonomies[ $shared_tt->taxonomy ] ) ) {
$taxonomies[ $shared_tt->taxonomy ] = 1;
$split_term_data[ $term_id ][ $shared_tt->taxonomy ] = _split_shared_term( $shared_terms[ $term_id ], $shared_tt, false );
// Rebuild the cached hierarchy for each affected taxonomy.
foreach ( array_keys( $taxonomies ) as $tax ) {
delete_option( "{$tax}_children" );
_get_term_hierarchy( $tax );
update_option( '_split_terms', $split_term_data );
delete_option( $lock_name );
* In order to avoid the _wp_batch_split_terms() job being accidentally removed,
* checks that it's still scheduled while we haven't finished splitting terms.
function _wp_check_for_scheduled_split_terms() {
if ( ! get_option( 'finished_splitting_shared_terms' ) && ! wp_next_scheduled( 'wp_split_shared_term_batch' ) ) {
wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_split_shared_term_batch' );
* Checks default categories when a term gets split to see if any of them need to be updated.
* @param int $term_id ID of the formerly shared term.
* @param int $new_term_id ID of the new term created for the $term_taxonomy_id.
* @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split.
* @param string $taxonomy Taxonomy for the split term.
function _wp_check_split_default_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
if ( 'category' !== $taxonomy ) {
foreach ( array( 'default_category', 'default_link_category', 'default_email_category' ) as $option ) {
if ( (int) get_option( $option, -1 ) === $term_id ) {
update_option( $option, $new_term_id );
* Checks menu items when a term gets split to see if any of them need to be updated.
* @global wpdb $wpdb WordPress database abstraction object.
* @param int $term_id ID of the formerly shared term.