Custom Taxonomy: Parent still counting deleted Child

I have this weird situation when I deleted two child taxonomies but they still seem to be assigned to their parent.

I need a overview site for all our products with the taxonomy title ahead. If there is a child taxonomy the products should display under the child, and not under the parent taxonomy.

Read More

the code

$args = array(
    'type'                     => 'anco_produkt',
    'orderby'                  => 'menu_order',
    'order'                    => 'ASC',
    'taxonomy'                 => 'produkt_category'
); 
$taxonomies = get_categories($args);

foreach ($taxonomies as $taxonomy) {
  if($taxonomy->parent == 0) {
    echo '<h3 class="alignnone">'.$taxonomy->name.'</h3>';
  } else {
    echo '<h4>'.$taxonomy->name.'</h4>';
  }

  // Get the children of current taxonomy
  $tax_children = get_term_children($taxonomy->term_id , 'produkt_category');

  if(count($tax_children) == 0) { // Are there children?
    // Do something with the posts
  }

It works great!

But I have a taxonomy with two former child-taxonomies. I deleted them and assigned all the affected posts to the parent category, but the posts don’t show up under the parent category.

I tested if I get any children with:

print_r($tax_children);
echo count($tax_children);

And voilà, I get their IDs (19 and 22) and a count of 2 children for this “childrenless” parent. In the DB are no taxonomies/categories with these IDs. I tried wp_delete_term() and wp_delete_category(), this parent just can’t let go of it’s children :((

the problem in action:
http://ch-de.ancotech.com/produkte/

the taxonomies
the taxonomies I currently use

There should be 15 products under ‘Spezialbewehrungen’…

Has someone a idea?

Ps. Should I send the parent to therapy to accept the loss of its children?

Related posts

1 comment

  1. The problem

    There’s on (imho) serious issue with WordPress and Taxonomies and their term hierarchy (and children): They aren’t really fetched from the actual state, but someone who thought (s)he might be really “smart” stuffed that into the *_options table.

    Just take a look at the source of get_term_children(): It makes a call to _get_term_hierarchy(). And in there the following is used to retrieve the children of a taxonomy:

    get_option( "{$taxonomy}_children" )
    

    Now it might sound smart at first to leverage a static array instead of a real count, but at the long term it’s a maintenance nightmare. WordPress has to keep that value updated and in case someone forgets to trigger that update programmatically, it gets out of sync and fails.

    Real world example

    To get a proof for that, you can do a plain DB call in (for e.g.) phpMyAdmin (or whatever you use): Just replace {$wpdb->prefix} with your DB table prefix.

    SELECT * 
    FROM  `{$wpdb->prefix}_options` 
    WHERE  `option_name` LIKE  '%children'
    ORDER BY  `{$wpdb->prefix}_options`.`option_name` ASC 
    LIMIT 0 , 1000
    

    The result will be something like the following – a serialized array:

    a:2:{i:23;a:5:{i:0;i:38;i:1;i:39;i:2;i:40;i:3;i:41;i:4;i:42;}i:40;a:1:{i:0;i:43;}}
    

    Or when deserialized:

    array (
      23 => 
      array (
        0 => 38,
        1 => 39,
        2 => 40,
        3 => 41,
        4 => 42,
      ),
      40 => 
      array (
        0 => 43,
      ),
    )
    

    So the keys are the parent terms, while the sub arrays are the associated child terms.

    What’s wrong?

    First off: I’m not completely sure about this, as WP uses plenty of plain functions and DB calls and tracing all routes is quite tough – maybe I got lost in some rabbit hole.

    When I look at wp_delete_term() it seems that there’s nowhere an update of the actual option happening. And wp_delete_category() is just a wrapper for that. So in order to keep your children in sync, you’ll have to manually update that option.

    I’ve written an importer once, where I stumbled upon the same problem and had to update the option manually as well. So unless no one smarter than me enters the stage, I think this is the only way to go: update_option():

    $taxonomy = 'your_current_taxonomy';
    $children = get_option( "{$taxonomy}_children" );
    // Merge here
    update_option( "{$taxonomy}_children", $your_new_value );
    

    EDIT (1)

    One more thing for everyone who wants to reproduce that or get some proof: There’s no way to actually see that the children are out of synch, unless you take a look at the taxonomy list table screen in admin or comparing the option return value manually. Everything else will just work as expected and look ok.

    EDIT (2)

    As @MannyFleurmond just commented, there might be another option: Deleting the option and let WP regenerate it for you. I searched core and looked into wp_delete_term() in ~/wp-includes/taxonomy.php… and found something: clean_term_cacheSource really has a part that does this:

        if ( $clean_taxonomy ) {
            wp_cache_delete('all_ids', $taxonomy);
            wp_cache_delete('get', $taxonomy);
            delete_option("{$taxonomy}_children");
            // Regenerate {$taxonomy}_children
            _get_term_hierarchy($taxonomy);
        }
    

    Point still is, that it doesn’t trigger. The reason is simple: The 3rd argument is true per default. And therefore – see source – it won’t trigger an update.

    A possible solution might be the following: Use a hook and clean it manually to trigger an update:

    // The last hook before everything happens in core:
    do_action( 'deleted_term_taxonomy', $tt_id );
    

    So we could use that:

    <?php
    defined( 'ABSPATH' ) OR exit;
    /** Plugin Name: (#117373) Fix Child term relationships */
    
    add_action( 'deleted_term_taxonomy', 'wpse117374UpdateTaxChildCount' );
    function wpse117374UpdateTaxChildCount( $termID )
    {
        $tax = get_current_screen()->taxonomy;
        delete_option( "{$tax}_children" );
        # OR: ... alternate solution
        // clear_term_cache( $termID, get_current_screen()->taxonomy, false );
    }
    

    Keep in mind that above plugin isn’t tested. But if it works, please leave a comment so people can simply use this as mu-plugin to automatically fix this crap 🙂

Comments are closed.