WordPress wp_insert_term and wp_update_term: Manage Taxonomy Terms Programmatically

Taxonomy terms in WordPress are usually created by editors through the admin UI — but plugins that import data, run migrations, or set up initial site structure often need to create and update terms programmatically. WordPress provides wp_insert_term() for creating new terms, wp_update_term() for modifying existing ones, and wp_delete_term() for removal. These functions handle slug generation, hierarchy (parent terms), taxonomy validation, and term-taxonomy relationship management. They return the term ID and taxonomy ID on success, or a WP_Error on failure — including the specific case where a term with the same name already exists, which must be handled gracefully in import routines.

Problem: An import script reads product categories from a CSV and needs to create them in WordPress if they don't exist, update their descriptions if they do exist, and set up a two-level hierarchy (parent → child). Running the script twice should be idempotent — no duplicate terms.

Solution: Check for existence with term_exists() before inserting. On WP_Error with code term_exists, retrieve the existing ID and call wp_update_term() instead.

<?php
// ── Idempotent: create or update a term ────────────────────────────────
function upsert_term( string $name, string $taxonomy, array $args = [] ): int|WP_Error {
    $existing = term_exists( $name, $taxonomy, $args['parent'] ?? 0 );

    if ( $existing ) {
        // Term exists — update it
        $term_id = is_array( $existing ) ? (int) $existing['term_id'] : (int) $existing;
        $result  = wp_update_term( $term_id, $taxonomy, $args );
        return is_wp_error( $result ) ? $result : $term_id;
    }

    // Term does not exist — create it
    $result = wp_insert_term( $name, $taxonomy, $args );
    if ( is_wp_error( $result ) ) {
        // Handle race condition: another request created it between our check and insert
        if ( $result->get_error_code() === 'term_exists' ) {
            return (int) $result->get_error_data( 'term_exists' );
        }
        return $result;
    }
    return (int) $result['term_id'];
}

// ── Create a parent term ───────────────────────────────────────────────
$electronics_id = upsert_term( 'Electronics', 'product_cat', [
    'slug'        => 'electronics',
    'description' => 'All electronic products',
] );

if ( is_wp_error( $electronics_id ) ) {
    error_log( 'Failed to create Electronics: ' . $electronics_id->get_error_message() );
    return;
}

// ── Create a child term ────────────────────────────────────────────────
$phones_id = upsert_term( 'Smartphones', 'product_cat', [
    'slug'        => 'smartphones',
    'description' => 'Mobile phones and accessories',
    'parent'      => $electronics_id,    // set parent here
] );

// ── wp_insert_term() result structure ─────────────────────────────────
// On success: [ 'term_id' => 42, 'term_taxonomy_id' => 55 ]
// On failure: WP_Error
$result = wp_insert_term( 'Laptops', 'product_cat', [
    'slug'   => 'laptops',
    'parent' => $electronics_id,
] );
if ( ! is_wp_error( $result ) ) {
    $term_id          = $result['term_id'];
    $term_taxonomy_id = $result['term_taxonomy_id'];
}

// ── Update an existing term ────────────────────────────────────────────
wp_update_term( $phones_id, 'product_cat', [
    'name'        => 'Smartphones & Tablets',
    'description' => 'Updated description',
] );

// ── Delete a term ──────────────────────────────────────────────────────
// $default = term ID to reassign posts to (optional)
wp_delete_term( $phones_id, 'product_cat', [ 'default' => $electronics_id ] );

// ── Assign a term to a post ────────────────────────────────────────────
// wp_set_post_terms: replaces all terms. wp_set_object_terms: lower-level
wp_set_object_terms( $post_id, [ $electronics_id, $phones_id ], 'product_cat' );

NOTE: WordPress enforces term name uniqueness per taxonomy, not globally — so a term named "Electronics" in category and another named "Electronics" in product_cat are completely separate. However, within the same taxonomy, uniqueness is enforced only at the parent level: two child terms can have the same name if they have different parent terms, and this is by design for hierarchical taxonomies like categories. Always call clean_term_cache() and clean_taxonomy_cache() after a batch import to ensure query results are fresh — WordPress caches taxonomy data aggressively and will serve stale counts otherwise.