Skip to content

Instantly share code, notes, and snippets.

@EarthlingDavey
Last active March 6, 2025 10:43
Show Gist options
  • Save EarthlingDavey/8361f504f4df7303ff8dc41f1b7400e3 to your computer and use it in GitHub Desktop.
Save EarthlingDavey/8361f504f4df7303ff8dc41f1b7400e3 to your computer and use it in GitHub Desktop.
An ACF Relationship field with multisite support. Tested with ACF 6.3.6
<?php
/**
* An ACF Relationship field with multisite support. Tested with ACF 6.3.6
*
* This aims to be the lightest possible implementation of a multisite compatible relationship field.
* For the php class we extend the existing ACF Relationship field and override the necessary methods.
*
* For the JavaScript, we copy the existing ACF Relationship field and modify only the necessary properties.
*
* There are minor CSS changes to the field type.
*
* This code should be added to your theme's functions.php file.
*
* add_action( 'acf/include_field_types', 'include_acf_relationship_multisite' );
* function include_acf_relationship_multisite() {
* include_once('acf-relationship-multisite.php');
* }
*
* TODO
* - Taxonomy filter only works for the current subsite. These should update as the blog_id filter changes.
* - Comment where parts of php and js functions are different to the original relationship field.
* - Document the returned field values.
*/
if (! class_exists('acf_field_relationship_multisite')) :
class acf_field_relationship_multisite extends acf_field_relationship
{
/**
* This function will setup the field type data
*
* @type function
* @date 5/03/2014
* @since 5.0.0
*/
public function initialize()
{
$this->name = 'relationship_multisite';
$this->label = __('Relationship (multisite)', 'acf');
$this->category = 'relational';
$this->description = __('A multisite compatible variation of the relationship field.', 'acf');
$this->preview_image = acf_get_url() . '/assets/images/field-type-previews/field-preview-relationship.png';
$this->doc_url = null;
$this->defaults = array(
'post_type' => array(),
'taxonomy' => array(),
'min' => 0,
'max' => 0,
'filters' => array('search', 'post_type', 'taxonomy', 'blog_id'),
'elements' => array(),
'return_format' => 'object',
'bidirectional_target' => array(),
);
add_filter('acf/conditional_logic/choices', array($this, 'render_field_relation_conditional_choices'), 10, 3);
// extra
add_action('wp_ajax_acf/fields/relationship_multisite/query', array($this, 'ajax_query'));
add_action('wp_ajax_nopriv_acf/fields/relationship_multisite/query', array($this, 'ajax_query'));
}
/**
* Returns a list of pretty subsites.
*
* This function is similar to acf_get_pretty_post_types() and acf_get_pretty_taxonomies().
*
* @param array $blog_ids
* @return array $r - an associative array of blog_id => blogname
*/
private function acf_get_pretty_subsites($blog_ids = array())
{
$blogs = get_sites();
$r = array();
foreach ($blogs as $blog) {
if (!empty($blog_ids) && !in_array($blog->blog_id, $blog_ids)) {
continue;
}
$r[$blog->blog_id] = $blog->blogname;
}
return $r;
}
/**
* Filters choices in relation conditions.
*
* @since 6.3
*
* @param array $choices The selected choice.
* @param array $conditional_field The conditional field settings object.
* @param string $rule_value The rule value.
* @return array
*/
public function render_field_relation_conditional_choices($choices, $conditional_field, $rule_value)
{
if (! is_array($conditional_field) || $conditional_field['type'] !== 'relationship_multisite') {
return $choices;
}
if (! empty($rule_value)) {
$post_title = get_the_title($rule_value);
$choices = array($rule_value => $post_title);
}
return $choices;
}
/**
* This function will return an array of data formatted for use in a select2 AJAX response
*
* @since 5.0.9
*
* @param array $options An array of options for the query.
* @return array
*/
public function get_ajax_query($options = array())
{
// defaults
$options = wp_parse_args(
$options,
array(
'post_id' => 0,
's' => '',
'field_key' => '',
'paged' => 1,
'post_type' => '',
'include' => '',
'taxonomy' => '',
'blog_id' => '',
)
);
// load field
$field = acf_get_field($options['field_key']);
if (! $field) {
return false;
}
// vars
$results = array();
$args = array();
$s = false;
$is_search = false;
// paged
$args['posts_per_page'] = 20;
$args['paged'] = intval($options['paged']);
// search
if ($options['s'] !== '' && empty($options['include'])) {
// strip slashes (search may be integer)
$s = wp_unslash(strval($options['s']));
// update vars
$args['s'] = $s;
$is_search = true;
}
// post_type
if (! empty($options['post_type'])) {
$args['post_type'] = acf_get_array($options['post_type']);
} elseif (! empty($field['post_type'])) {
$args['post_type'] = acf_get_array($field['post_type']);
} else {
$args['post_type'] = acf_get_post_types();
}
// post status
if (! empty($options['post_status'])) {
$args['post_status'] = acf_get_array($options['post_status']);
} elseif (! empty($field['post_status'])) {
$args['post_status'] = acf_get_array($field['post_status']);
}
// blog id
if (! empty($options['blog_id'])) {
// Add the blog_id to the args array - so that it can be filtered.
$args['blog_id'] = $options['blog_id'];
} elseif (! empty($field['blog_id'])) {
$args['blog_id'] = $field['blog_id'];
}
// taxonomy
if (! empty($options['taxonomy'])) {
// vars
$term = acf_decode_taxonomy_term($options['taxonomy']);
// tax query
$args['tax_query'] = array();
// append
$args['tax_query'][] = array(
'taxonomy' => $term['taxonomy'],
'field' => 'slug',
'terms' => $term['term'],
);
} elseif (! empty($field['taxonomy'])) {
// vars
$terms = acf_decode_taxonomy_terms($field['taxonomy']);
// append to $args
$args['tax_query'] = array(
'relation' => 'OR',
);
// now create the tax queries
foreach ($terms as $k => $v) {
$args['tax_query'][] = array(
'taxonomy' => $k,
'field' => 'slug',
'terms' => $v,
);
}
}
if (! empty($options['include'])) {
// If we have an include, we need to return only the selected posts.
$args['post__in'] = array($options['include']);
}
// filters
$args = apply_filters('acf/fields/relationship_multisite/query', $args, $field, $options['post_id']);
$args = apply_filters('acf/fields/relationship_multisite/query/name=' . $field['name'], $args, $field, $options['post_id']);
$args = apply_filters('acf/fields/relationship_multisite/query/key=' . $field['key'], $args, $field, $options['post_id']);
// Get the blog id from the args array.
$blog_id = $args['blog_id'] && (int) $args['blog_id'] > 0 ? (int) $args['blog_id'] : 0;
// Remove the blog_id from the args array - so that it doesn't interfere with the query.
unset($args['blog_id']);
// If blog id is set, switch to that blog
$blog_id && switch_to_blog($blog_id);
// get posts grouped by post type
$groups = acf_get_grouped_posts($args);
// bail early if no posts
if (empty($groups)) {
// Restore the current blog, since we switched earlier.
$blog_id && restore_current_blog();
return false;
}
// loop
foreach (array_keys($groups) as $group_title) {
// vars
$posts = acf_extract_var($groups, $group_title);
// data
$data = array(
'text' => $group_title,
'children' => array(),
);
// convert post objects to post titles
foreach (array_keys($posts) as $post_id) {
$posts[$post_id] = $this->get_post_title($posts[$post_id], $field, $options['post_id']);
}
// order posts by search
if ($is_search && empty($args['orderby']) && isset($args['s'])) {
$posts = acf_order_by_search($posts, $args['s']);
}
// append to $data
foreach (array_keys($posts) as $post_id) {
$compound_id = $blog_id ? $post_id . ',' . $blog_id : $post_id;
$data['children'][] = $this->get_post_result($compound_id, $posts[$post_id]);
}
// append to $results
$results[] = $data;
}
// Restore the current blog, since we switched earlier.
$blog_id && restore_current_blog();
// add as optgroup or results
if (count($args['post_type']) == 1) {
$results = $results[0]['children'];
}
// vars
$response = array(
'results' => $results,
'limit' => $args['posts_per_page'],
);
// return
return $response;
}
private function postAndBlogToId($post_id, $blog_id)
{
return $blog_id ? $post_id . ',' . $blog_id : $post_id;
}
private function idToPostAndBlog($id)
{
$parts = explode(',', $id);
return array(
'post_id' => $parts[0],
'blog_id' => count($parts) > 1 ? $parts[1] : 0
);
}
/**
* This function returns the HTML for a result
*
* @type function
* @date 1/11/2013
* @since 5.0.0
*
* @param $post (object)
* @param $field (array)
* @param $post_id (int) the post_id to which this value is saved to
* @return (string)
*/
function get_post_title($post, $field, $post_id = 0, $is_search = 0)
{
// get post_id
if (! $post_id) {
$post_id = acf_get_form_data('post_id');
}
// vars
$title = acf_get_post_title($post, $is_search);
// featured_image
if (acf_in_array('featured_image', $field['elements'])) {
// vars
$class = 'thumbnail';
$thumbnail = acf_get_post_thumbnail($post->ID, array(17, 17));
// icon
if ($thumbnail['type'] == 'icon') {
$class .= ' -' . $thumbnail['type'];
}
// append
$title = '<div class="' . $class . '">' . $thumbnail['html'] . '</div>' . $title;
}
// filters
$title = apply_filters('acf/fields/relationship_multisite/result', $title, $post, $field, $post_id);
$title = apply_filters('acf/fields/relationship_multisite/result/name=' . $field['_name'], $title, $post, $field, $post_id);
$title = apply_filters('acf/fields/relationship_multisite/result/key=' . $field['key'], $title, $post, $field, $post_id);
// return
return $title;
}
/**
* Create the HTML interface for your field
*
* @param $field - an array holding all the field's data
*
* @type action
* @since 3.6
* @date 23/01/13
*/
function render_field($field)
{
// vars
$post_type = acf_get_array($field['post_type']);
$taxonomy = acf_get_array($field['taxonomy']);
$blog_id = acf_get_array($field['blog_id']);
$filters = acf_get_array($field['filters']);
// filters
$filter_count = count($filters);
$filter_post_type_choices = array();
$filter_taxonomy_choices = array();
$filter_blog_id_choices = array();
// post_type filter
if (in_array('post_type', $filters)) {
$filter_post_type_choices = array(
'' => __('Select post type', 'acf'),
) + acf_get_pretty_post_types($post_type);
}
// blog_id filter
if (in_array('blog_id', $filters)) {
// Get all blogs
$pretty_blogs = $this->acf_get_pretty_subsites($blog_id);
// Get this blog id
$current_blog_id = get_current_blog_id();
// Initialize the choices array
$filter_blog_id_choices = array();
// If the current blog is in the list
if (isset($pretty_blogs[$current_blog_id])) {
// Add the current blog to the top of the list
$filter_blog_id_choices[$current_blog_id] = $pretty_blogs[$current_blog_id];
// Remove the current blog from the list
unset($pretty_blogs[$current_blog_id]);
}
// Add the rest of the blogs to the list
$filter_blog_id_choices = $filter_blog_id_choices + $pretty_blogs;
}
// taxonomy filter
if (in_array('taxonomy', $filters)) {
$term_choices = array();
$filter_taxonomy_choices = array(
'' => __('Select taxonomy', 'acf'),
);
// check for specific taxonomy setting
if ($taxonomy) {
$terms = acf_get_encoded_terms($taxonomy);
$term_choices = acf_get_choices_from_terms($terms, 'slug');
// if no terms were specified, find all terms
} else {
// restrict taxonomies by the post_type selected
$term_args = array();
if ($post_type) {
$term_args['taxonomy'] = acf_get_taxonomies(
array(
'post_type' => $post_type,
)
);
}
// get terms
$terms = acf_get_grouped_terms($term_args);
$term_choices = acf_get_choices_from_grouped_terms($terms, 'slug');
}
// append term choices
$filter_taxonomy_choices = $filter_taxonomy_choices + $term_choices;
}
// div attributes
$atts = array(
'id' => $field['id'],
// Keep the acf-relationship class for styling compatibility
// Add the acf-relationship-multisite class for custom styling nd js targeting
'class' => "acf-relationship acf-relationship-multisite {$field['class']}",
'data-min' => $field['min'],
'data-max' => $field['max'],
'data-s' => '',
'data-paged' => 1,
'data-post_type' => '',
'data-taxonomy' => '',
'data-blog_id' => '',
'data-nonce' => wp_create_nonce($field['key']),
);
?>
<div <?php echo acf_esc_attrs($atts); ?>>
<?php
acf_hidden_input(
array(
'name' => $field['name'],
'value' => '',
)
);
?>
<?php
/* filters */
if ($filter_count) :
?>
<div class="filters -f<?php echo esc_attr($filter_count); ?>">
<?php
/* search */
if (in_array('search', $filters)) :
?>
<div class="filter -search">
<?php
acf_text_input(
array(
'placeholder' => __('Search...', 'acf'),
'data-filter' => 's',
)
);
?>
</div>
<?php
endif;
/* blog_id */
if (in_array('blog_id', $filters)) :
?>
<div class="filter -blog_id">
<?php
acf_select_input(
array(
'choices' => $filter_blog_id_choices,
'data-filter' => 'blog_id',
'value' => array_keys($filter_blog_id_choices)[0]
)
);
?>
</div>
<?php
endif;
/* post_type */
if (in_array('post_type', $filters)) :
?>
<div class="filter -post_type">
<?php
acf_select_input(
array(
'choices' => $filter_post_type_choices,
'data-filter' => 'post_type',
)
);
?>
</div>
<?php
endif;
/* post_type */
if (in_array('taxonomy', $filters)) :
?>
<div class="filter -taxonomy">
<?php
acf_select_input(
array(
'choices' => $filter_taxonomy_choices,
'data-filter' => 'taxonomy',
)
);
?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="selection">
<div class="choices">
<ul class="acf-bl list choices-list"></ul>
</div>
<div class="values">
<ul class="acf-bl list values-list">
<?php
if (! empty($field['value'])) :
// loop
foreach ($field['value'] as $compound_id) :
// Get the post_id and blog_id from the compound id
$ids = $this->idToPostAndBlog($compound_id);
$post_title_suffix = '';
// Is blog_id current blog? Then set it to 0
if (absint($ids['blog_id']) === get_current_blog_id()) {
$ids['blog_id'] = 0;
}
if ($ids['blog_id']) {
switch_to_blog($ids['blog_id']);
$post_title_suffix = ' (' . get_bloginfo('name') . ')';
}
// get post
$post = get_post($ids['post_id']);
// validate
if (! $post) {
error_log('no post');
if ($ids['blog_id']) {
restore_current_blog();
}
continue;
}
?>
<li>
<?php
acf_hidden_input(
array(
'name' => $field['name'] . '[]',
'value' => $compound_id,
)
);
?>
<span tabindex="0" data-id="<?php echo $compound_id; ?>" class="acf-rel-item acf-rel-item-remove">
<?php echo acf_esc_html($this->get_post_title($post, $field)) . $post_title_suffix; ?>
<a href="#" class="acf-icon -minus small dark" data-name="remove_item"></a>
</span>
</li>
<?php
if ($ids['blog_id']) {
restore_current_blog();
}
?>
<?php endforeach; ?>
<?php endif; ?>
</ul>
</div>
</div>
</div>
<?php
}
/**
* Create extra options for your field. This is rendered when editing a field.
* The value of $field['name'] can be used (like bellow) to save extra data to the $field
*
* @type action
* @since 3.6
* @date 23/01/13
*
* @param $field - an array holding all the field's data
*/
function render_field_settings($field)
{
acf_render_field_setting(
$field,
array(
'label' => __('Filter by Post Type', 'acf'),
'instructions' => '',
'type' => 'select',
'name' => 'post_type',
'choices' => acf_get_pretty_post_types(),
'multiple' => 1,
'ui' => 1,
'allow_null' => 1,
'placeholder' => __('All post types', 'acf'),
)
);
acf_render_field_setting(
$field,
array(
'label' => __('Filter by Post Status', 'acf'),
'instructions' => '',
'type' => 'select',
'name' => 'post_status',
'choices' => acf_get_pretty_post_statuses(),
'multiple' => 1,
'ui' => 1,
'allow_null' => 1,
'placeholder' => __('Any post status', 'acf'),
)
);
acf_render_field_setting(
$field,
array(
'label' => __('Filter by Taxonomy', 'acf'),
'instructions' => '',
'type' => 'select',
'name' => 'taxonomy',
'choices' => acf_get_taxonomy_terms(),
'multiple' => 1,
'ui' => 1,
'allow_null' => 1,
'placeholder' => __('All taxonomies', 'acf'),
)
);
acf_render_field_setting(
$field,
array(
'label' => __('Filter by Subsite', 'acf'),
'instructions' => '',
'type' => 'select',
'name' => 'blog_id',
'choices' => $this->acf_get_pretty_subsites(),
'multiple' => 1,
'ui' => 1,
'allow_null' => 1,
'placeholder' => __('All subsites', 'acf'),
)
);
acf_render_field_setting(
$field,
array(
'label' => __('Filters', 'acf'),
'instructions' => '',
'type' => 'checkbox',
'name' => 'filters',
'choices' => array(
'search' => __('Search', 'acf'),
'post_type' => __('Post Type', 'acf'),
'taxonomy' => __('Taxonomy', 'acf'),
'blog_id' => __('Subsite', 'acf'),
),
)
);
acf_render_field_setting(
$field,
array(
'label' => __('Return Format', 'acf'),
'instructions' => '',
'type' => 'radio',
'name' => 'return_format',
'choices' => array(
'object' => __('Post Object', 'acf'),
'id' => __('Post ID and Blog ID', 'acf'),
),
'layout' => 'horizontal',
)
);
}
/**
* This filter is applied to the $value after it is loaded from the db and before it is returned to the template
*
* @type filter
* @since 3.6
* @date 23/01/13
*
* @param $value (mixed) the value which was loaded from the database
* @param $post_id (mixed) the post_id from which the value was loaded
* @param $field (array) the field array holding all the field options
*
* @return $value (mixed) the modified value
*/
function format_value($value, $post_id, $field)
{
// bail early if no value
if (empty($value)) {
return $value;
}
// force value to array
$value = acf_get_array($value);
$value = array_map(function ($v) {
$ids = $this->idToPostAndBlog($v);
$ids['post_id'] = intval($ids['post_id']);
$ids['blog_id'] = intval($ids['blog_id']);
return $ids;
}, $value);
// load posts if needed
if ($field['return_format'] == 'object') {
// get posts
$value = array_map(function ($v) use ($field) {
if($v['blog_id'] === 0) {
return get_post($v['post_id']);
}
switch_to_blog($v['blog_id']);
$post = get_post($v['post_id']);
// Check the post type
if (!empty($field['post_type']) && !in_array($post->post_type, $field['post_type'])) {
$post = null;
}
restore_current_blog();
if($post) {
$post->blog_id = $v['blog_id'];
}
return $post;
}, $value);
}
// return
return $value;
}
/**
* Filters the field value before it is saved into the database.
*
* @since 3.6
*
* @param mixed $value The value which will be saved in the database.
* @param integer $post_id The post_id of which the value will be saved.
* @param array $field The field array holding all the field options.
*
* @return mixed $value The modified value.
*/
public function update_value($value, $post_id, $field)
{
// Bail early if no value.
if (empty($value)) {
acf_update_bidirectional_values(array(), $post_id, $field);
return $value;
}
// Format array of values.
// - ensure each value is an id, or in the pattern blog_id:id.
// - Parse each id as string for SQL LIKE queries.
if (acf_is_sequential_array($value)) {
$value = array_map(function ($v) {
$ids = $this->idToPostAndBlog($v);
$ids['post_id'] = acf_idval($ids['post_id']);
$ids['post_id'] = strval($ids['post_id']);
return $this->postAndBlogToId($ids['post_id'], $ids['blog_id']);
}, $value);
// Parse single value for id.
} else {
$ids = $this->idToPostAndBlog($value);
$ids['post_id'] = acf_idval($ids['post_id']);
}
acf_update_bidirectional_values(acf_get_array($value), $post_id, $field);
// Return value.
return $value;
}
/**
* Return the schema array for the REST API.
*
* @param array $field
* @return array
*/
public function get_rest_schema(array $field)
{
$schema = array(
'type' => array('string', 'array', 'null'),
'required' => ! empty($field['required']),
'items' => array(
'type' => 'string',
),
);
if (empty($field['allow_null'])) {
$schema['minItems'] = 1;
}
if (! empty($field['min'])) {
$schema['minItems'] = (int) $field['min'];
}
if (! empty($field['max'])) {
$schema['maxItems'] = (int) $field['max'];
}
return $schema;
}
}
// initialize
acf_register_field_type('acf_field_relationship_multisite');
endif; // class_exists check
/**
* JavaScript to register the field type
*
* Copy the way that ACF registers its field types in it's js file:
* advanced-custom-fields-pro/assets/build/js/acf-input.js
*
* But, instead of writing the field group from scratch,
* we can copy the existing relationship field and modify it.
*/
$acf_relationship_multisite_script = <<<JS
(function($, undefined) {
var FieldCopy = acf.models["RelationshipField"].prototype;
var RelationshipMultisiteField = acf.Field.extend({
type: "relationship_multisite",
events: FieldCopy.events,
\$control: function () {
return this.$('.acf-relationship-multisite');
},
\$list: FieldCopy.\$list,
\$listItems: FieldCopy.\$listItems,
\$listItem: FieldCopy.\$listItem,
getValue: FieldCopy.getValue,
newChoice: FieldCopy.newChoice,
newValue: FieldCopy.newValue,
initialize: function () {
// Get the initial filter value for blog_id
var \$el = this.\$('.filter.-blog_id select');
var val = \$el.val();
var filter = \$el.data('filter');
// update attr
this.set(filter, val);
// Run the original initialize function
FieldCopy.initialize.call(this);
},
onScrollChoices: FieldCopy.onScrollChoices,
onKeypressFilter: FieldCopy.onKeypressFilter,
onChangeFilter: FieldCopy.onChangeFilter,
onClickAdd: FieldCopy.onClickAdd,
onClickRemove: FieldCopy.onClickRemove,
onTouchStartValues: FieldCopy.onTouchStartValues,
maybeFetch: FieldCopy.maybeFetch,
getAjaxData: function () {
// load data based on element attributes
var ajaxData = this.\$control().data();
for (var name in ajaxData) {
ajaxData[name] = this.get(name);
}
// extra
ajaxData.action = 'acf/fields/relationship_multisite/query';
ajaxData.field_key = this.get('key');
ajaxData.nonce = this.get('nonce');
// Filter.
ajaxData = acf.applyFilters('relationship_multisite_ajax_data', ajaxData, this);
// return
return ajaxData;
},
fetch: FieldCopy.fetch,
walkChoices: FieldCopy.walkChoices,
});
acf.registerFieldType(RelationshipMultisiteField);
})(jQuery);
JS;
/**
* Add inline script to the footer
*/
add_action(
'acf/input/admin_print_footer_scripts',
function () use ($acf_relationship_multisite_script) {
echo "<script type='text/javascript'>{$acf_relationship_multisite_script}</script>";
}
);
/**
* CSS related to the field type
*
* 1. Centre the field type label - this element is shown in the modal when 'Browse Fields' is clicked.
* 2. If there are 4 filters on the field, make the filter width 25%.
*/
$acf_relationship_multisite_styles = <<<CSS
.acf-field-type[data-field-type="relationship_multisite"] .field-type-label {
text-align: center;
}
.acf-relationship-multisite .filters.-f4 .filter {
width: 25%;
}
CSS;
/**
* Add inline styles to the header
*/
add_action(
'acf/input/admin_print_scripts',
function () use ($acf_relationship_multisite_styles) {
echo "<style type='text/css'>{$acf_relationship_multisite_styles}</style>";
}
);
@EarthlingDavey
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment