Search code examples
phpwordpress

Automatically check child categories/terms when parent category is checked


I have this Continent -> Country category setup for a custom post type.

- Africa
    - Uganda
    - Zambia
    - Zimbabwe
- Asia
    - Afghanistan
    - Bahrain
    - Bangladesh
    - Bhutan

I think this could be solved by some kind of WordPress function that will auto check all the child categories (countries of a continent) if the parent category (Africa) is checked, not the other way around like this plugin.

For example, if Africa is checked, all the African Countries should be automatically checked. If only Zimbabwe is checked, the parent category Africa should not be checked.

Also when Africa is unchecked, the African Countries should be unchecked automatically as well.


Solution

  • EDIT:

    I had a relatively lengthy answer that works well for checking child terms, but not so well for unchecking them when a parent was unchecked. I've backed up that answer here: Original Answer: 60079535 (also available in the Edit History).

    New Answer:

    Okay, I'm pretty proud of this result actually. After doing some reasearch, I came across the set_object_terms hook, which is fired at the end of the wp_set_object_terms() function, located in /wp-includes/taxonomy.php.

    On that hook, it accepts 6 arguments: $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids. The important ones here for "check children" and "uncheck children" are $tt_ids and $old_tt_ids. These are arrays of the new term ids, and the old term ids, respectively.

    This allows us to compare the two arrays and see what ids were added and which were removed. This is important because you may check Africa, then later uncheck Africa and now check Asia. Here's a handy function that will allow you to see both differences:

    function array_diff_once($a1, $a2){
        foreach($a2 as $val){
            if( false !== ($pos = array_search($val, $a1)) ){
                unset($a1[$pos]);
            }
        }
        
        return array_values($a1);
    }
    

    So instead of using the save_post hook, we can instead compare the added/removed terms on the set_object_terms hook, and add/remove child terms for each one there. Note, this can also fire during inopportune times (autosaving, if the post isn't published, etc, so I've put in a few abort conditions.)

    add_action( 'set_object_terms', 'so_60079535_toggle_child_terms', 10, 6 );
    function so_60079535_toggle_child_terms( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ){
        // Abort if this is an autosave/backup
        if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
            return;
    
        // Abort if no ids are set from before or now
        if( empty($tt_ids) && empty($old_tt_ids) )
            return;
    
        // Only do things if this post is published (front facing)
        $post_status = get_post_status( $object_id );
        if( $post_status != 'publish' )
            return;
    
        // What terms where ADDED, and which were REMOVED?
        $added_terms   = array_diff_once( $tt_ids, $old_tt_ids );
        $removed_terms = array_diff_once( $old_tt_ids, $tt_ids );
    
        // Any terms ADDED this time?
        if( !empty($added_terms) ){
            foreach( $added_terms as $added_term ){
                // Do any of these added terms have children?
                if( $added_child_terms = get_term_children( $added_term, $taxonomy ) ){
                    // Append those children
                    wp_set_object_terms( $object_id, $added_child_terms, $taxonomy, true );
                }
            }
        }
    
        // Any terms REMOVED?
        if( !empty($removed_terms) ){
            foreach( $removed_terms as $removed_term ){
                // Do any of the removed terms have children?
                if( $removed_child_terms = get_term_children( $removed_term, $taxonomy ) ){
                    // Remove them all
                    wp_remove_object_terms( $object_id, $removed_child_terms, $taxonomy, true );
                }
            }
        }
    }
    

    I've actually put this code on my test site and it seems to work flawlessly, not matter how deeply (grand-child/great grand-child terms) terms are, and how many are added or removed at a time. Another neat thing is that this hook is already passed the $taxonomy parameter, so it should work for any and all taxonomies that are ever added, automatically. If that's not desired, you can always add an abort condition for certain taxonomies, post types, etc, very easily.