ACF Two Way Relationships

ACF Two Way Relationships

How to modify the WordPress ACF plugin to allow for two way relationships.

The ACF Relationship field is great for creating advanced relationships between posts. If we want to create a relationship from post A to post B we add it on the post edit page. acf-relationship-field This is great, but what you might be after is a two-way relationship. When you add the relationship from A→B, you would also like there to be a relationship from B→A. If you go check out post B, you will see that there is no such relationship (yet). You can add it yourself, but this becomes unmanageable and inefficient at some point. All we need is the following:
  • When we add a relationship from post A→B, create the inverse (B→A).
  • When we remove one of the relationships (A→B or B→A), remove the inverse.
The first step is to add the following to your functions.php file within your theme directory. The acf/save_post hook lets us handle things before any ACF fields are actually updated. The third parameter (priority) lets us hook in before the save occurs. We have to handle before so that we can check for any dropped relationships.
function acf_pre_save_hook( $post_id ) {
    // TODO
}
add_action( 'acf/save_post', 'acf_pre_save_hook', 1 );  // run before ACF saves the $_POST['acf'] data
And here is what the overall code looks like:
// ============================== ACF pre-save hook =============================
// 
// We needed this to simulate a two-way relationship (acf doesnt do this apparently)
function acf_pre_save_hook( $post_id ) {
    // bail early if no ACF data
    if ( empty( $_POST['acf'] ) ) {
        return;
    }
    $fields = $_POST['acf'];
    $relationship_field = 'field_111806f5651b7';  // This is the id of the acf relationship field
    if ( isset( $fields[ $relationship_field ] ) ) {    	
    	$post_id = $_POST[ 'post_ID' ];
    	$previous_relations = get_field( $relationship_field, $post_id, false );

    	// 1. Attach the opposite relations
    	$target_relationships = $fields[ $relationship_field ];
    	if ( !is_array( $target_relationships ) )
    		$target_relationships = array();  // there were no values, lets just assign empty array
    	for ($i = 0; $i < count( $target_relationships ); $i++) {
    		$existing_relationships = get_field( $relationship_field, $target_relationships[ $i ], false );  // false means just return the array of ids
    		if ( $existing_relationships == null )
    			$existing_relationships = array();
    		array_push( $existing_relationships, $post_id );
    		update_field( $relationship_field, $existing_relationships, $target_relationships[ $i ] );
	}

	// 2. If any relationships were dropped, we need to update those other posts with this info (ie: remove that relation on other end).
	$removed_relationships = array();
	for ($i = 0; $i < count( $previous_relations ); $i++) { 
		if ( !in_array( $previous_relations[ $i ], $target_relationships ) ) {
			array_push( $removed_relationships, $previous_relations[ $i ] );
		}
	}
	for ($i = 0; $i < count( $removed_relationships ); $i++) { 
		// Fetch the relationship field for the target post
		$product_groups = get_field( $relationship_field, $removed_relationships[ $i ], false );
		if ( is_array( $product_groups ) ) {
			// Remove the current post from that array
			if ( ( $key = array_search( $post_id, $product_groups ) ) !== false ) {
			    unset( $product_groups[ $key ] );
			}
			// Update that field with subset
			update_field( $relationship_field, $product_groups, $removed_relationships[ $i ] );
		}
	}
    }
}
add_action( 'acf/save_post', 'acf_pre_save_hook', 1 );  // run before ACF saves the $_POST['acf'] data
The logic is pretty straight-forward on this one, it just takes a bit of work. We used this particular solution to group posts together via ACF. Keep in mind that we are simply updating meta data, so this should work across different post types and pages. If you spot any areas for improvement on this one let us know! Let us know if you’re looking for web development help!