Bidirectional Relationships

Last updated May 3, 2018

Overview

This tutorial will cover the concept and code for creating a bidirectional relationship between posts using only the Advanced Custom Fields plugin.

This tutorial uses a Relationship field to select posts, however a Post Object field with multiple selections enabled would work just the same as both field types save data as an array of post ID’s.

Code

The following snippet adds a function to hook into the acf/update_value filter (run before a value is saved). It will update the custom field value of each selected post to include the current post’s ID as well as removing the current post’s ID from previously selected posts (which are no longer selected).

This function does not contain any hard-coded field names so it will work with any relationship field. The only change required is within the add_filter() parameter. It contains the name of the relationship field which in the example below is called ‘related_posts’.

functions.php

function bidirectional_acf_update_value( $value, $post_id, $field  ) {
    
    // vars
    $field_name = $field['name'];
    $field_key = $field['key'];
    $global_name = 'is_updating_' . $field_name;
    
    
    // bail early if this filter was triggered from the update_field() function called within the loop below
    // - this prevents an inifinte loop
    if( !empty($GLOBALS[ $global_name ]) ) return $value;
    
    
    // set global variable to avoid inifite loop
    // - could also remove_filter() then add_filter() again, but this is simpler
    $GLOBALS[ $global_name ] = 1;
    
    
    // loop over selected posts and add this $post_id
    if( is_array($value) ) {
    
        foreach( $value as $post_id2 ) {
            
            // load existing related posts
            $value2 = get_field($field_name, $post_id2, false);
            
            
            // allow for selected posts to not contain a value
            if( empty($value2) ) {
                
                $value2 = array();
                
            }
            
            
            // bail early if the current $post_id is already found in selected post's $value2
            if( in_array($post_id, $value2) ) continue;
            
            
            // append the current $post_id to the selected post's 'related_posts' value
            $value2[] = $post_id;
            
            
            // update the selected post's value (use field's key for performance)
            update_field($field_key, $value2, $post_id2);
            
        }
    
    }
    
    
    // find posts which have been removed
    $old_value = get_field($field_name, $post_id, false);
    
    if( is_array($old_value) ) {
        
        foreach( $old_value as $post_id2 ) {
            
            // bail early if this value has not been removed
            if( is_array($value) && in_array($post_id2, $value) ) continue;
            
            
            // load existing related posts
            $value2 = get_field($field_name, $post_id2, false);
            
            
            // bail early if no value
            if( empty($value2) ) continue;
            
            
            // find the position of $post_id within $value2 so we can remove it
            $pos = array_search($post_id, $value2);
            
            
            // remove
            unset( $value2[ $pos] );
            
            
            // update the un-selected post's value (use field's key for performance)
            update_field($field_key, $value2, $post_id2);
            
        }
        
    }
    
    
    // reset global varibale to allow this filter to function as per normal
    $GLOBALS[ $global_name ] = 0;
    
    
    // return
    return $value;
    
}

add_filter('acf/update_value/name=related_posts', 'bidirectional_acf_update_value', 10, 3);

Example

In this example, we have a field group which contains a relationship field called ‘related_posts’. This field group will appear on all ‘post’ edit screens.

acf-bidirectional-posts-1

The posts

There are 4 posts named clearly to illustrate the functionality of this tutorial.

acf-bidirectional-posts-2

Edit Post 1

Let’s now edit ‘Post 1’ and select all the available posts.

acf-bidirectional-posts-3

Edit Post 2

Next, lets edit ‘Post 2’ and see that the ‘Related Posts’ already contains a value with ‘Post 1’ selected. This is due to the above code appending ‘Post 1’ to newly selected posts.

acf-bidirectional-posts-4

Next, let’s un-select ‘Post 1’ from the ‘Related Posts’ field and update ‘Post 2’.

acf-bidirectional-posts-5

Edit Post 1

Lastly, let’s edit ‘Post 1’ again and see that ‘Post 2’ has been removed from the selected ‘Related Posts’ custom field. This is due to the above code removing ‘Post 2’ from previously selected posts.

acf-bidirectional-posts-6

Conclusion

Pretty neat huh? This functionality may find it’s way into the plugin core at some time, but for now, please use the above code to add bidirectional functionality to your relationship fields. The main benefit for such functionality is for clients to clearly see post relationships, however, this also allows developers to query posts more efficiently.