├── .gitignore ├── composer.json ├── README.md ├── readme.txt └── acf-post2post.php /.gitignore: -------------------------------------------------------------------------------- 1 | _notes 2 | .DS_Store 3 | *.LCK 4 | *.svn 5 | /acf-pro 6 | /__assets 7 | *data.txt 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "hube2/acf-post2post", 4 | "type": "wordpress-plugin", 5 | "license": "MIT", 6 | "description": "Creates two way (bidirectional) relationships in Advanced Custom Fields", 7 | "homepage": "https://github.com/Hube2/acf-post2post", 8 | "support": { 9 | "issues": "https://github.com/Hube2/acf-post2post/issues", 10 | "source": "https://github.com/Hube2/acf-post2post.git" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "John A. Huebner II", 15 | "homepage": "https://github.com/Hube2" 16 | } 17 | ], 18 | "keywords": [ 19 | "acf", 20 | "advanced custom fields", 21 | "Post 2 Post" 22 | ], 23 | "prefer-stable": true, 24 | "require": { 25 | "composer/installers": "^1.0", 26 | "php": ">=5.4.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Post 2 Post for ACF 2 | 3 | Creates two way (bidirectional) relationships in Advanced Custom Fields 4 | 5 | This plugin will provide no functionality if ACF is not installed and active. 6 | 7 | This plugin does not create a new type of field or any admin interface. This plugin when used as explained below makes the 8 | existing ACF Relationship and Post Object fields work bi-directionaly, automatically updating the relationship field on 9 | the other end of the relationship. 10 | 11 | A couple of months after I created this plugin the developer posted a tutorial on how to do this using a filter. 12 | That example basically does the same thing except it seems to require the fields to have the same key as well 13 | as the same name where this plugin will let you mix fields as long as they are of a type that allows a relationship 14 | and they have the same name. 15 | 16 | [Questions? Bugs? Comments?](https://github.com/Hube2/acf-post2post/issues) 17 | 18 | ## How to use 19 | 20 | * Create a relationship or post object field. 21 | * The field must be at the top level. It cannot be a subfield of a repeater or a flexible content field. 22 | * The field name must be the same on all posts. In other words if you want to have different post types be related then you must add a field with the same field name on both post types. 23 | 24 | When you add a post to a relationship or post object field and the same field name appears on the post added to the relationship then the relationship field on the related post will be updated to include a relationship to the post being edited. 25 | 26 | If a post is removed from a relationship then the post being removed will also be updated to remove the relationship to the post being edited. 27 | 28 | ## Post Object Fields 29 | 30 | If a post object field is being used 31 | 32 | * If it allows multiple values then it will work the same way that relationship fields work. 33 | * If it does not allow multiple values and the related post already contains a value see *Overwrite Settings* 34 | 35 | ## Overwrite Settings 36 | 37 | If the field in a related post, whether it is a post object field that only allows 1 value or a relationship field that has a maximum number of related posts, if the field in the related post already has the maximum number of values allowed then, by default, a new value will not be added. You can override this default by specifying overwrite settings. 38 | 39 | How to add overwrite settings 40 | ``` 41 | add_filter('acf-post2post/overwrite-settings', 'my_overwrite_settings'); 42 | function my_overwrite_settings($settings) { 43 | $settings['field_name'] = array( 44 | 'overwrite' => true, 45 | 'type' => 'first' 46 | ); 47 | return $settings; 48 | } 49 | ``` 50 | Each element of the $settings array is an array. The index of the array is the field that you want to 51 | specify settings for. Each field can have 2 arguments. 52 | `overwrite` = true/false or 1/0. If set to true 53 | or 1 then new values will overwrite older values. The default value of this setting is false. 54 | `type` = 'first' or 'last'. Which of the existing values should be removed, the first one added or the last. The default value is 'first'. 55 | after a value is removed from the existing list the new value is added to the end of the list. 56 | 57 | ## Field Exeptions 58 | 59 | You can disable automatic bidirectional relationships for specific field keys using the filter 60 | ``` 61 | // field_XXXXXXXX = the field key of the field 62 | // you want to disable bidirectional relationships for 63 | add_filter('acf/post2post/update_relationships/key=field_XXXXXXXX', '__return_false'); 64 | ``` 65 | 66 | ## Disable All Fields 67 | You can disable bidirectional updates using the following code 68 | ``` 69 | add_filter('acf/post2post/update_relationships/default', '__return_false'); 70 | ``` 71 | When doing this the field exceptions filter works in reverse and you must use that filter to enable fields that you want bidirectionality for, like this 72 | 73 | ``` 74 | // field_XXXXXXXX = the field key of the field 75 | // you want to enable bidirectional relationships for 76 | add_filter('acf/post2post/update_relationships/key=field_XXXXXXXX', '__return_true'); 77 | ``` 78 | 79 | 80 | 81 | ## After update hooks 82 | There are two actions that can be used after a post is updated and passes a single post ID. Please make sure you see the subtle difference in these two hooks. 83 | 84 | The first is run after each related post is updated 85 | ``` 86 | add_action('acf/post2post/relationship_updated', 'my_post_updated_action'); 87 | function my_post_updated_action($post_id) { 88 | // $post_id == the post ID that was updated 89 | // do something after the related post is updated 90 | } 91 | ``` 92 | 93 | The second is run after all posts are updated and passes an array of post IDs. 94 | ``` 95 | add_action('acf/post2post/relationships_updated', 'my_post_updated_action'); 96 | function my_post_updated_action($posts) { 97 | // $posts == and array of post IDs that were updated 98 | // do something to all posts after update 99 | foreach ($posts as $post_id) { 100 | // do something to post 101 | } 102 | } 103 | ``` 104 | 105 | ## So why do this? 106 | 107 | I did not actually create this plugin to make it easier on the person that's managing a site, although that is an added side benifit. The main reason for implementing some way to make relationship and post object fields biderectional is that doing reverse relationship queries for ACF is a huge PITA. I completely understand why this is not built into ACF. If it was built in then Elliot would need to deal with relationship fields in repeaters and 108 | flex fields and nested repeaters. And then there is the problem of relationships with different post types and fields with different names. It would be a deep dark rabbit hole from which I don't think he'd return. There are too many parameters and possibilities. On the other hand, filters and actions can be created with a finite number of options to do the work that's needed. 109 | 110 | #### Automatic Updates 111 | Github updater support has been removed. This plugin has been published to WordPress.Org here 112 | https://wordpress.org/plugins/post-2-post-for-acf/. If you are having problems updating please 113 | try installing from there. 114 | 115 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === ACF Post-2-Post === 2 | Contributors: Hube2 3 | Tags: acf, advanced custom fields, add on, bidirectional, 2 way, two way, relationship 4 | Requires at least: 4.0 5 | Tested up to: 6.1 6 | Stable tag: 1.7.0 7 | License: GPLv2 or later 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | Automatic Two Way (Bidirectional) Relationships with ACF5 11 | 12 | 13 | == Description == 14 | 15 | ***This is an add on plugin for Advanced Custom Fields (ACF) >= Version 5.*** 16 | 17 | ***This plugin will not work with ACF Version 4.*** 18 | 19 | This plugin will not provide any functionality if ACF >=5 is not installed.*** 20 | 21 | This plugin does not create a new type of field or any admin interface. This plugin when used as 22 | explained below makes the existing ACF Relationship and Post Object fields work bi-directionaly, 23 | automatically updating the relationship field on the other end of the relationship. 24 | 25 | For more information see [Other Notes](https://wordpress.org/plugins/post-2-post-for-acf/) 26 | 27 | == Installation == 28 | 29 | Install like any other plugin 30 | 31 | == Screenshots == 32 | 33 | None 34 | 35 | == Frequently Asked Questions == 36 | 37 | Nothing yet 38 | 39 | == Other Notes == 40 | 41 | == Github Repository == 42 | 43 | This plugin is also on GitHub 44 | [https://github.com/Hube2/acf-post2post](https://github.com/Hube2/acf-post2post) 45 | 46 | == How To Use == 47 | * Create a relationship or post object field. 48 | * The field must be at the top level. It cannot be a subfield of a repeater or a flexible content field. 49 | * The field name must be the same on all posts. In other words if you want to have different post types be related then you must add a field with the same field name on both post types. 50 | 51 | When you add a post to a relationship or post object field and the same field name appears on the post added to the relationship then the relationship field on the related post will be updated to include a relationship to the post being edited. 52 | 53 | If a post is removed from a relationship then the post being removed will also be updated to remove the relationship to the post being edited. 54 | 55 | == Post Object Fields == 56 | If a post object field is being used 57 | 58 | * If it allows multiple values then it will work the same way that relationship fields work. 59 | * If it does not allow multiple values and the related post already contains a value see Overwrite Settings 60 | 61 | == Overwrite Settings == 62 | If the field in a related post, whether it is a post object field that only allows 1 value or a relationship field that has a maximum number of related posts, if the field in the related post already has the maximum number of values allowed, by default, a new value will not be added. You can override this default by specifying overwrite settings. 63 | 64 | How to add overwrite settings 65 | ` 66 | add_filter('acf-post2post/overwrite-settings', 'my_overwrite_settings'); 67 | function my_overwrite_settings($settings) { 68 | $settings['field_name'] = array( 69 | 'overwrite' => true, 70 | 'type' => 'first' 71 | ); 72 | return $settings; 73 | } 74 | ` 75 | 76 | Each element of the $settings array is an array. The index of the array is the field that you want to specify settings for. Each field can have 2 arguments. 77 | 78 | * overwrite: true/false or 1/0. If set to true or 1 then new values will overwrite older values. The default value of this setting is false. 79 | * type: 'first' or 'last'. Which of the existing values should be removed, the first one added or the last. The default value is 'first'. 80 | 81 | after a value is removed from the existing list the new value is added to the end of the list. 82 | 83 | 84 | 85 | == Field Exeptions == 86 | 87 | You can disable automatic bidirectional relationships for specific field keys using the filter 88 | ` 89 | // field_XXXXXXXX = the field key of the field 90 | // you want to disable bidirectional relationships for 91 | add_filter('acf/post2post/update_relationships/key=field_XXXXXXXX', '__return_false'); 92 | ` 93 | 94 | 95 | == After update hooks == 96 | There are two actions that can be used after a post is updated and passes a single post ID. Please make sure you see the subtle difference in these two hooks. 97 | 98 | The first is run after each related post is updated 99 | ` 100 | add_action('acf/post2post/relationship_updated', 'my_post_updated_action'); 101 | function my_post_updated_action($post_id) { 102 | // $post_id == the post ID that was updated 103 | // do something after the related post is updated 104 | } 105 | ` 106 | 107 | The second is run after all posts are updated and passes an array of post IDs. 108 | ` 109 | add_action('acf/post2post/relationships_updated', 'my_post_updated_action'); 110 | function my_post_updated_action($posts) { 111 | // $posts == and array of post IDs that were updated 112 | // do something to all posts after update 113 | foreach ($posts as $post_id) { 114 | // do something to post 115 | } 116 | } 117 | ` 118 | 119 | == Changelog == 120 | 121 | = 1.7.0 = 122 | * Added filter to allow disabling all fields so that specific fields can be enabled 123 | 124 | = 1.6.0 = 125 | * Added filter to allow specifying append or prepend new relationship when not overwriting 126 | 127 | = 1.5.2 = 128 | * Corrected PHP Notice: Trying to access array offset on value of type bool lines 99 & 130 129 | 130 | = 1.5.1 = 131 | * Removed Github Updater Support 132 | 133 | = 1.5.0 = 134 | * Added filter to allow specifying to append or prepend new relationship (can supercede overwrite setting) 135 | 136 | = 1.4.1 = 137 | * Corrected logic error in overwrite 138 | * add context to action hooks 139 | 140 | = 1.4.0 = 141 | * added actions after updates to related posts to allow 3rd party integrations 142 | 143 | = 1.3.2 = 144 | * removed donation nag 145 | 146 | = 1.3.1 = 147 | * Corrected bug in 1.3.0 that prevented all fields from updating correctly 148 | 149 | = 1.3.0 = 150 | * added filter to allow disabling bidirectional relationships on fields by field key 151 | 152 | = 1.2.8 = 153 | * changed from plugins_loaded to after_setup_theme for checking if ACF >= 5 is installed to allow for ACF being installed in themes 154 | 155 | = 1.2.7 = 156 | * replace php array_walk() w/array_map() to correct issue with str/int conversion of IDs 157 | 158 | = 1.2.6 = 159 | * corrected serialization of post IDs as strings instead of integers to allow correct ACF meta_key value searching of serialized ID values useing `LIKE "{ID}"` 160 | 161 | = 1.2.5 = 162 | * plugin disabled if ACF5 not installed 163 | * plugin deactivated if ACF5 not installed 164 | 165 | = 1.2.4 = 166 | * removed github updater support 167 | 168 | = 1.2.3 = 169 | * initial release to WordPress.org 170 | 171 | -------------------------------------------------------------------------------- /acf-post2post.php: -------------------------------------------------------------------------------- 1 | remove_relationship($related_id, $field_name, $post_id); 76 | $updated_posts[] = $related_id; 77 | } 78 | } 79 | } 80 | if (count($new)) { 81 | foreach ($new as $related_id) { 82 | $this->add_relationship($related_id, $field_name, $post_id); 83 | $updated_posts[] = $related_id; 84 | } 85 | } 86 | if (count($updated_posts)) { 87 | do_action('acf/post2post/relationships_updated', $updated_posts, $field_name, $new, $previous); 88 | } 89 | return $value; 90 | } // end public function update_relationship_field 91 | 92 | private function remove_relationship($post_id, $field_name, $related_id) { 93 | /* 94 | $post_id = the post id to remove the relationship from 95 | $field_name = the field name to update 96 | $related_id = the relationship to remove 97 | */ 98 | $field = $this->get_field($post_id, $field_name); 99 | if (!$field) { 100 | // field not found attached to this post 101 | return; 102 | } 103 | $array_value = true; 104 | if ($field['type'] == 'post_object') { 105 | if (!$field['multiple']) { 106 | $array_value = false; 107 | } 108 | } 109 | $values = maybe_unserialize(get_post_meta($post_id, $field_name, true)); 110 | if ($values === '') { 111 | $values = array(); 112 | } 113 | if (!is_array($values)) { 114 | $values = array($values); 115 | } 116 | if (!count($values)) { 117 | // nothing to delete 118 | return; 119 | } 120 | $values = array_map('intval', $values); 121 | $new_values = array(); 122 | foreach ($values as $value) { 123 | if ($value != $related_id) { 124 | $new_values[] = $value; 125 | } 126 | } 127 | if (!count($new_values) && !$array_value) { 128 | $new_values = ''; 129 | } elseif (!$array_value) { 130 | $new_values = $new_values[0]; 131 | } elseif (count($new_values)) { 132 | $new_values = array_map('strval', $new_values); 133 | } 134 | update_post_meta($post_id, $field_name, $new_values); 135 | update_post_meta($post_id, '_'.$field_name, $field['key']); 136 | do_action('acf/post2post/relationship_updated', $post_id, $field_name, $new_values); 137 | } // end private function remove_relationship 138 | 139 | private function add_relationship($post_id, $field_name, $related_id) { 140 | /* 141 | $post_id = the post id to add the relationship to 142 | $field_name = the field name to update 143 | $related_id = the relationship to add 144 | */ 145 | $field = $this->get_field($post_id, $field_name); 146 | if (!$field) { 147 | // field not found attached to this post 148 | return; 149 | } 150 | $max_posts = 0; 151 | $array_value = true; 152 | if ($field['type'] == 'post_object') { 153 | if (!$field['multiple']) { 154 | $max_posts = 1; 155 | $array_value = false; 156 | } 157 | } elseif ($field['type'] == 'relationship') { 158 | if ($field['max']) { 159 | $max_posts = $field['max']; 160 | } 161 | } 162 | $value = maybe_unserialize(get_post_meta($post_id, $field_name, true)); 163 | if ($value == '') { 164 | $value = array(); 165 | } 166 | if (!is_array($value)) { 167 | $value = array($value); 168 | } 169 | $value = array_map('intval', $value); 170 | if (($max_posts == 0 || count($value) < $max_posts) && 171 | !in_array($related_id, $value)) { 172 | $append_where = 'append'; 173 | $append_where = apply_filters('acf/post2post/append-prepend', $append_where); // all field 174 | $append_where = apply_filters('acf/post2post/append-prepend/name='.$field['name'], $append_where); // field name 175 | $append_where = apply_filters('acf/post2post/append-prepend/key='.$field['key'], $append_where); // field key 176 | if ($append_where == 'append') { 177 | $value[] = $related_id; 178 | } else { 179 | array_unshift($value, $related_id); 180 | } 181 | } elseif ($max_posts > 0) { 182 | $overwrite_settings = apply_filters('acf-post2post/overwrite-settings', array()); 183 | if (isset($overwrite_settings[$field_name]) && 184 | isset($overwrite_settings[$field_name]['overwrite']) && 185 | $overwrite_settings[$field_name]['overwrite']) { 186 | $type = 'first'; 187 | if (isset($overwrite_settings[$field_name]['type']) && 188 | in_array(strtolower($overwrite_settings[$field_name]['type']), array('first', 'last'))) { 189 | $type = strtolower($overwrite_settings[$field_name]['type']); 190 | } 191 | if ($type == 'first') { 192 | $remove = array_shift($value); 193 | } else { 194 | $remove = array_pop($value); 195 | } 196 | // remove this relationship from the post that was just removed 197 | $this->remove_relationship(intval($remove), $field_name, $post_id); 198 | $append_where = 'append'; 199 | $append_where = apply_filters('acf/post2post/append-prepend', $append_where); // all field 200 | $append_where = apply_filters('acf/post2post/append-prepend/name='.$field['name'], $append_where); // field name 201 | $append_where = apply_filters('acf/post2post/append-prepend/key='.$field['key'], $append_where); // field key 202 | if ($append_where == 'append') { 203 | $value[] = $related_id; 204 | } else { 205 | array_unshift($value, $related_id); 206 | } 207 | } // end field overwrite 208 | } // end if else 209 | if (!$array_value) { 210 | $value = $value[0]; 211 | } else { 212 | $value = array_map('strval', $value); 213 | } 214 | update_post_meta($post_id, $field_name, $value); 215 | update_post_meta($post_id, '_'.$field_name, $field['key']); 216 | do_action('acf/post2post/relationship_updated', $post_id, $field_name, $value); 217 | } // end private function add_relationship 218 | 219 | public function get_field($post_id, $field_name) { 220 | $field = false; 221 | $found = false; 222 | $cache_key = 'get_field-'.$post_id.'-'.$field_name; 223 | $cache = wp_cache_get($cache_key, 'acfpost2post', false, $found); 224 | if ($found) { 225 | return $cache; 226 | } 227 | $found = false; 228 | $field_groups = $this->post_field_groups($post_id); 229 | $field_group_count = count($field_groups); 230 | for ($g=0; $g<$field_group_count; $g++) { 231 | $field_count = count($field_groups[$g]['fields']); 232 | for ($f=0; $f<$field_count; $f++) { 233 | if ($field_groups[$g]['fields'][$f]['name'] == $field_name && 234 | in_array($field_groups[$g]['fields'][$f]['type'], array('relationship', 'post_object'))) { 235 | $field = $field_groups[$g]['fields'][$f]; 236 | $found == true; 237 | break; 238 | } 239 | } // end for $f 240 | if ($found) { 241 | break; 242 | } 243 | } // end for $g 244 | wp_cache_set($cache_key, $field, 'acfpost2post'); 245 | return $field; 246 | } // end public function get_field 247 | 248 | public function post_field_groups($post_id) { 249 | $found = false; 250 | $cache = wp_cache_get('post_field_groups-'.$post_id, 'acfpost2post', false, $found); 251 | if ($found) { 252 | return $cache; 253 | } 254 | $args = array('post_id' => $post_id); 255 | $field_groups = acf_get_field_groups($args); 256 | $count = count($field_groups); 257 | for ($i=0; $i<$count; $i++) { 258 | $field_groups[$i]['fields'] = acf_get_fields($field_groups[$i]['key']); 259 | } 260 | wp_cache_set('post_field_groups-'.$post_id, $field_groups, 'acfpost2post'); 261 | return $field_groups; 262 | } // end public function post_field_groups 263 | 264 | public function activate() { 265 | // just in case I need to do something to activate 266 | } // end public function activate 267 | 268 | public function deactivate() { 269 | // just in case I need to do something to deactivate 270 | } // end public function deactivate 271 | 272 | } // end class acf_post2post 273 | 274 | ?> 275 | --------------------------------------------------------------------------------