Связь двух разных произвольных типов записей
Надеюсь, кто-нибудь сможет помочь, так как это очень сложная задача...
Я пытаюсь создать сайт недвижимости на WordPress и хотел бы связать два произвольных типа записей -
Один будет для жилых комплексов, а другой для отдельных объектов недвижимости внутри этого жилого комплекса
Я понимаю, что могу создать произвольный тип записи 'Developments' (Комплексы) и другой 'Properties' (Объекты), но как мне связать их вместе?.. например, если я создал объект недвижимости и пытаюсь прикрепить его к комплексу, как это будет работать, если это разные произвольные типы записей?
Я прочитал похожий вопрос - Как связать разные произвольные типы записей? и ответ от Scuba Kay был почти тем, что мне нужно, но я не знаю, как запросить определенный объект недвижимости, который принадлежит определенному комплексу
Заранее спасибо!

Если я правильно понимаю, что вы делаете, вы хотите установить отношение "один ко многим" между районами застройки и объектами недвижимости.
Вот класс, который при загрузке и инициализации будет:
- создавать метабокс для типа записи development_area, содержащий список чекбоксов всех доступных объектов недвижимости, чтобы можно было легко обновлять связь между ними.
- создавать метабокс для типа записи property, содержащий выпадающий список для выбора связанного района застройки.
- обеспечивать отношение 1 ко многим между районами застройки и объектами недвижимости, то есть район застройки может быть связан со многими объектами, но каждый объект будет иметь только один связанный район.
Примечание:
Этот код демонстрирует, как это сделать. Хотя я проверил его на очевидные ошибки и дважды перепроверил логику, вам, возможно, придется что-то подправить, так как я не тестировал его в реальных условиях. Я часто так делаю, и это отлично работает.
После класса приведен пример его использования, а затем несколько примеров работы с установленными связями.
class One_To_Many_Linker {
protected $_already_saved = false; # Используется для предотвращения двойного сохранения
public function __construct() {
$this->do_initialize();
}
Настройка метабоксов и функционала сохранения
protected function do_initialize() {
add_action(
'save_post',
array( $this, 'save_meta_box_data' ),
10,
2
);
add_action(
"add_meta_boxes_for_development_area",
array( $this, 'setup_development_area_boxes' )
);
add_action(
"add_meta_boxes_for_property",
array( $this, 'setup_property_boxes' )
);
}
Создание нужного метабокса Здесь также можно легко настроить другие метабоксы.
# Тип записи района застройки
# - этот тип записи может иметь несколько связанных объектов недвижимости
public function setup_development_area_boxes( \WP_Post $post ) {
add_meta_box(
'area_related_properties_box',
__('Связанные объекты', 'language'),
array( $this, 'draw_area_related_properties_box' ),
$post->post_type,
'advanced',
'default'
);
}
Отрисовка связанных объектов Этот код выводит связанные объекты в виде списка чекбоксов.
public function draw_area_related_properties_box( \WP_Post $post ) {
$all_properties = $this->get_all_of_post_type( 'property' );
$linked_property_ids = $this->get_linked_property_ids( $post->ID );
if ( 0 == count($all_properties) ) {
$choice_block = '<p>В системе пока нет объектов.</p>';
} else {
$choices = array();
foreach ( $all_properties as $property ) {
$checked = ( in_array( $property->ID, $linked_property_ids ) ) ? ' checked="checked"' : '';
$display_name = esc_attr( $property->post_title );
$choices[] = <<<HTML
<label><input type="checkbox" name="property_ids[]" value="{$property->ID}" {$checked}/> {$display_name}</label>
HTML;
}
$choice_block = implode("\r\n", $choices);
}
# Убедимся, что пользователь намеренно это делает
wp_nonce_field(
"updating_{$post->post_type}_meta_fields",
$post->post_type . '_meta_nonce'
);
echo $choice_block;
}
Получение списка записей Этот код получает все записи определенного типа. Если вы используете закрепленные записи, вам нужно отключить флаг закрепления в аргументах.
# Получение всех записей указанного типа
# Возвращает массив объектов записей
protected function get_all_of_post_type( $type_name = '') {
$items = array();
if ( !empty( $type_name ) ) {
$args = array(
'post_type' => "{$type_name}",
'posts_per_page' => -1,
'order' => 'ASC',
'orderby' => 'title'
);
$results = new \WP_Query( $args );
if ( $results->have_posts() ) {
while ( $results->have_posts() ) {
$items[] = $results->next_post();
}
}
}
return $items;
}
Получение ID связанных объектов для района застройки
Для данного ID района застройки возвращает массив ID всех связанных объектов.
protected function get_linked_property_ids( $area_id = 0 ) {
$ids = array();
if ( 0 < $area_id ) {
$args = array(
'post_type' => 'property',
'posts_per_page' => -1,
'order' => 'ASC',
'orderby' => 'title',
'meta_query' => array(
array(
'key' => '_development_area_id',
'value' => (int)$area_id,
'type' => 'NUMERIC',
'compare' => '='
)
)
);
$results = new \WP_Query( $args );
if ( $results->have_posts() ) {
while ( $results->have_posts() ) {
$item = $results->next_post();
$ids[] = $item->ID;
}
}
}
return $ids;
}
Настройка метабоксов для объектов недвижимости
Здесь можно легко добавить дополнительные метабоксы при необходимости.
# Настройка метабоксов для типа записи
public function setup_property_boxes( \WP_Post $post ) {
add_meta_box(
'property_linked_area_box',
__('Связанный район застройки', 'language'),
array( $this, 'draw_property_linked_area_box' ),
$post->post_type,
'advanced',
'default'
);
}
Отрисовка метабокса редактирования объекта
Этот код выводит выпадающий список (select), позволяющий пользователю выбрать, с каким районом застройки связан объект. Мы используем select, чтобы гарантировать выбор только одного района, но можно использовать и список радио-кнопок.
public function draw_property_linked_area_box( \WP_Post $post ) {
$all_areas = $this->get_all_of_post_type('development_area');
$related_area_id = $this->get_property_linked_area_id( $post->ID );
if ( 0 == $all_areas ) {
$choice_block = '<p>Пока нет районов для выбора.</p>';
} else {
$choices = array();
$selected = ( 0 == $related_area_id )? ' selected="selected"':'';
$choices[] = '<option value=""' . $selected . '> -- Не выбрано -- </option>';
foreach ( $all_areas as $area ) {
$selected = ( $area->ID == (int)$related_area_id ) ? ' selected="selected"' : '';
$display_name = esc_attr( $area->post_title );
$choices[] = <<<HTML
<option value="{$area->ID}" {$selected}>{$display_name}</option>
HTML;
}
$choice_list = implode("\r\n" . $choices);
$choice_block = <<<HTML
<select name="development_area_id">
{$choice_list}
</select>
HTML;
}
wp_nonce_field(
"updating_{$post->post_type}_meta_fields",
$post->post_type . '_meta_nonce'
);
echo $choice_block;
}
Метод связывания
Обратите внимание, что мы устанавливаем связь через ключ _development_area_id
для каждого объекта.
- районы застройки могут запрашивать объекты с этим ключом для их отображения
объекты могут получать свои метаданные или иметь отфильтрованный запрос для получения данных района
protected function get_property_linked_area_id( $property_id = 0 ) { $area_id = 0; if ( 0 < $property_id ) { $area_id = (int) get_post_meta( $property_id, '_development_area_id', true ); } return $area_id; }
Сохранение метаданных
Мы стараемся сохранять только при необходимости и правильно. Смотрите комментарии в коде.
public function save_meta_box_data( $post_id = 0, \WP_Post $post = null ) {
$do_save = true;
$allowed_post_types = array(
'development_area',
'property'
);
# Не сохраняем, если уже сохранили
if ( $this->_already_saved ) {
$do_save = false;
}
# Не сохраняем, если нет ID записи или самой записи
if ( empty($post_id) || empty( $post ) ) {
$do_save = false;
} else if ( ! in_array( $post->post_type, $allowed_post_types ) ) {
$do_save = false;
}
# Не сохраняем для ревизий и автосохранений
if (
defined('DOING_AUTOSAVE')
&& (
is_int( wp_is_post_revision( $post ) )
|| is_int( wp_is_post_autosave( $post ) )
)
) {
$do_save = false;
}
# Убедимся, что работаем с правильной записью
if ( !array_key_exists('post_ID', $_POST) || $post_id != $_POST['post_ID'] ) {
$do_save = false;
}
# Убедимся, что у нас есть права на сохранение [предполагаем, что оба типа используют edit_post]
if ( ! current_user_can( 'edit_post', $post_id ) ) {
$do_save = false;
}
# Проверим nonce и реферер
$nonce_field_name = $post->post_type . '_meta_nonce';
if ( ! array_key_exists( $nonce_field_name, $_POST) ) {
$do_save = false;
} else if ( ! wp_verify_nonce( $_POST["{$nonce_field_name}"], "updating_{$post->post_type}_meta_fields" ) ) {
$do_save = false;
} else if ( ! check_admin_referer( "updating_{$post->post_type}_meta_fields", $nonce_field_name ) ) {
$do_save = false;
}
if ( $do_save ) {
switch ( $post->post_type ) {
case "development_area":
$this->handle_development_area_meta_changes( $post_id, $_POST );
break;
case "property":
$this->handle_property_meta_changes( $post_id, $_POST );
break;
default:
# Для других типов записей ничего не делаем
break;
}
# Отмечаем, что сохранили данные
$this->_already_saved = true;
}
return;
}
Обновление объектов района
Мы читаем список выбранных объектов, получаем список текущих связанных объектов и используем их для определения, что обновлять.
Важно: Здесь мы обновляем метаданные объектов недвижимости, а не редактируемого района.
# Районы могут быть связаны с несколькими объектами
# но каждый объект может быть связан только с одним районом
protected function handle_development_area_meta_changes( $post_id = 0, $data = array() ) {
# Получаем ID связанных объектов для этого района
$linked_property_ids = $this->get_linked_property_ids( $post_id );
# Получаем список ID объектов, выбранных при сохранении
if ( array_key_exists('property_ids', $data) && is_array( $data['property_ids'] ) ) {
$chosen_property_ids = $data['property_ids'];
} else {
$chosen_property_ids = array();
}
# Формируем списки объектов для связывания/разрыва связи
$to_remove = array();
$to_add = array();
if ( 0 < count( $chosen_property_ids ) ) {
# Пользователь выбрал хотя бы один объект
if ( 0 < count( $linked_property_ids ) ) {
# Уже есть связанные объекты
# Проверяем существующие и отмечаем невыбранные
foreach ( $linked_property_ids as $property_id ) {
if ( ! in_array( $property_id, $chosen_property_ids ) ) {
# Связан, но не выбран. Удаляем связь.
$to_remove[] = $property_id;
}
}
# Проверяем выбранные и отмечаем новые
foreach ( $chosen_property_ids as $property_id ) {
if ( ! in_array( $property_id, $linked_property_ids ) ) {
# Выбран, но не связан. Добавляем связь.
$to_add[] = $property_id;
}
}
} else {
# Нет ранее выбранных ID, просто добавляем все
$to_add = $chosen_property_ids;
}
} else if ( 0 < count( $linked_property_ids ) ) {
# Не выбрано ни одного объекта. Удаляем все связи.
$to_remove = $linked_property_ids;
}
if ( 0 < count($to_add) ) {
foreach ( $to_add as $property_id ) {
# Это перезапишет существующее значение ключа связывания
# гарантируя только одну связь района с объектом.
update_post_meta( $property_id, '_development_area_id', $post_id );
}
}
if ( 0 < count( $to_remove ) ) {
foreach ( $to_remove as $property_id ) {
# Удаляем все существующие связи района с объектом
# гарантируя только одну связь района с объектом
delete_post_meta( $property_id, '_development_area_id' );
}
}
}
Сохранение изменений объекта
Так как наш мета-ключ на каждом объекте, мы просто обновляем метаданные при необходимости. Поскольку чтение в MySQL почти всегда быстрее записи, мы обновляем только при необходимости.
# Объекты связаны только с одним районом
protected function handle_property_meta_changes( $post_id = 0, $data = array() ) {
# Получаем текущий связанный район
$linked_area_id = $this->get_property_linked_area_id( $post_id );
if ( empty($linked_area_id) ) {
$linked_area_id = 0;
}
if ( array_key_exists( 'development_area_id', $data ) && !empty($data['development_area_id'] ) ) {
$received_area_id = (int)$data['development_area_id'];
} else {
$received_area_id = 0;
}
if ( $received_area_id != $linked_area_id ) {
# Это перезапишет все существующие копии нашего мета-ключа
# гарантируя только одну связь объекта с районом
update_post_meta( $post_id, '_development_area_id', $received_area_id );
}
}
}
Как использовать класс
Если вы загрузили класс в functions.php вашей темы или в плагин, используйте следующее для работы:
if ( is_admin() ) {
new One_To_Many_Linker();
}
Примеры использования Ниже приведены несколько примеров для фронтенда.
- Отображение всех объектов для текущего района
- Отображение района для объекта в архиве или на странице объекта
Показать все объекты, связанные с текущим районом
global $wp_query;
$area_id = $wp_query->get_queried_object_id();
$args = array(
'post_type' => 'property',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_development_area_id',
'value' => $area_id,
'compare' => '=',
'type' => 'NUMERIC'
)
)
);
$properties = new \WP_Query( $args );
if ( $properties->have_posts() ) {
while( $properties->have_posts() ) {
$property = $properties->next_post();
# делаем что-то с объектом
$property_link = get_permalink( $property->ID );
$property_name = esc_attr( $property->post_title );
echo '<a href="' . $property_link . '">' . $property_name . '</a>';
}
}
Показать связанные районы
Способ 1: Получение метаданных, загрузка района и использование данных
- работает на страницах, где is_singular('property') истинно
- работает на страницах, где is_post_type_archive('property') истинно
global $post;
while ( have_posts() ) {
the_post();
$post_id = get_the_ID(); # можно использовать $post->ID
$dev_area_id = get_post_meta( $post_id, '_development_area_id', true);
if ( !empty( $dev_area_id ) ) {
$development_area = get_post( $dev_area_id );
# делаем что-то...
$dev_area_link = get_permalink ( $development_area->ID );
$dev_area_title = $development_area->post_title;
$dev_area_content = $development_area->post_content;
echo '<a href="' . $dev_area_link . '">' . $dev_area_title . '</a><br />' . $dev_area_content;
}
}
Способ 2: Использование фильтра запросов
- работает на страницах, где is_singular('property') истинно
- работает на страницах, где is_post_type_archive('property') истинно
Этот метод позволяет избежать получения метаданных и дополнительных запросов для данных района. Чем больше объектов на странице, тем больше экономия ресурсов.
Где разместить этот код:
- В новом плагине (рекомендуется)
- В файле functions.php вашей темы
add_filter('posts_clauses', 'do_my_maybe_modify_queries', 10, 2);
function do_my_maybe_modify_queries( $pieces, \WP_Query $wp_query ) {
if ( !is_admin() && $wp_query->is_main_query() ) {
# Мы не в админке и это основной запрос шаблона
if ( array_key_exists('post_type', $wp_query->query_vars) ) {
# Запрошен какой-то тип записи.
# Получаем его как массив указанных типов
$value = $wp_query->query_vars['post_type'];
if ( !is_array( $value ) ) {
if ( empty( $value ) ) {
$post_types = array();
} else {
$post_types = array( $value );
}
} else {
$post_types = $value;
}
if ( in_array('property', $post_types) ) {
# Запрошен объект недвижимости
if ( $wp_query->is_post_type_archive || $wp_query->is_singular ) {
# Показываем архив типа property или отдельный объект.
# Добавляем ID, название и контент района в возвращаемые поля
global $wpdb;
# Связываем район с объектом через мета-ключ
# Так как только один район на объект, это работает
$pieces['join'] .= <<<SQL
LEFT JOIN {$wpdb->prefix}postmeta AS dev_pm ON {$wpdb->prefix}posts.ID = dev_pm.post_id AND dev_pm.meta_key = '_development_area_id'
LEFT JOIN {$wpdb->prefix}posts AS dev_post ON dev_post.ID = dev_pm.meta_value
SQL;
# Добавляем нужные поля района в запрос
$pieces['fields'] .= ", IFNULL( dev_pm.meta_value, 0 ) as development_area_id";
$pieces['fields'] .= ", IFNULL( dev_post.post_title, '') as development_area_title";
$pieces['fields'] .= ", IFNULL( dev_post.post_content, '') as development_area_content";
}
}
}
}
return $pieces;
}
С этим кодом вы можете получить данные следующим образом на странице объекта или архива. Работу с таксономиями объектов оставляю читателю.
if ( have_posts() ) {
global $post;
while ( have_posts() ) {
the_post();
if ( property_exists( $post, 'development_area_id' ) ) {
$dev_area_id = $post->development_area_id;
$dev_area_title = $post->development_area_title;
$dev_area_content = $post->development_area_content;
$dev_area_link = get_permalink( $dev_area_id );
echo '<a href="' . $dev_area_link . '">' . $dev_area_title . '</a><br />' . $dev_area_content;
}
}
}
Способ 3: Фильтрация WP_Query
Аналогично способу 2, но применимо для кастомных запросов через WP_Query. Отлично подходит для шорткодов или виджетов.
Сначала создаем фильтр (аналогично способу 2)
function add_dev_data_to_wp_query( $pieces, \WP_Query $wp_query ) {
global $wpdb;
if ( !is_admin() && !$wp_query->is_main_query() ) {
# Связываем район с объектом через мета-ключ
# Так как только один район на объект, это работает
$pieces['join'] .= <<<SQL
LEFT JOIN {$wpdb->prefix}postmeta AS dev_pm ON {$wpdb->prefix}posts.ID = dev_pm.post_id AND dev_pm.meta_key = '_development_area_id'
LEFT JOIN {$wpdb->prefix}posts AS dev_post ON dev_post.ID = dev_pm.meta_value
SQL;
# Добавляем нужные поля района в запрос
$pieces['fields'] .= ", IFNULL( dev_pm.meta_value, 0 ) as development_area_id";
$pieces['fields'] .= ", IFNULL( dev_post.post_title, '') as development_area_title";
$pieces['fields'] .= ", IFNULL( dev_post.post_content, '') as development_area_content";
}
return $pieces;
}
Затем применяем фильтр перед запросом и удаляем сразу после, чтобы не влиять на другие запросы.
$args = array(
'post_type' => 'property',
'posts_per_page' => -1
);
# Применяем фильтр
add_filter('posts_clauses', 'add_dev_data_to_wp_query', 10, 2);
# Выполняем запрос
$properties = new \WP_Query( $args );
# Удаляем фильтр
remove_filter('posts_clauses', 'add_dev_data_to_wp_query', 10);
if ( $properties->have_posts() ) {
while( $properties->have_posts() ) {
$property = $properties->next_post();
# Работаем с объектами
echo '<p>Название объекта: ' . $property->post_title . '</p>';
# Работаем с районами, если данные есть
if ( property_exists( $property, 'development_area_id' ) ) {
echo '<p>Часть района: ' . $property->development_area_title . '</p>';
}
}
}
Хотя это требует больше работы на старте, дает преимущества:
- Один запрос вместо 2-3 для связанных данных. Экономия ресурсов.
- Можно адаптировать для связей с несколькими объектами, экономя дополнительные запросы.
- Можно использовать для добавления колонок в админке, например показывая район для каждого объекта в списке.
Но и так уже достаточно длинно.

Вы можете использовать замечательный и бесплатный плагин Advanced Custom Fields, который позволяет создать настраиваемое поле типа Relationship, связывающее одну запись с другой.
В любом случае, прежде всего убедитесь, что вам действительно необходимы два пользовательских типа записей, и нельзя обойтись таксономиями или просто настраиваемыми полями. Например, отдельный объект недвижимости может быть типом записи, а Район застройки, к которому он относится, может быть Таксономией или Настраиваемым полем с названием этого Района застройки... В основном это зависит от того, какой объем информации вам нужно хранить как о Районах застройки, так и об объектах недвижимости...

Я бы сделал это просто и превратил свойства, найденные в рамках застройки, в дочерние записи родительской записи застройки. Вы также можете использовать стандартную таксономию категорий и пометить каждое "под" свойство как часть более крупной застройки. Вам просто нужно будет ввести эти застройки в виде категорий в таксономии. Таким образом, все свойства остаются в одном длинном и удобном для управления списке. Вы можете включать или исключать все, что вам нужно, при вызове этого списка, используя параметр глубины или таксономию.

Спасибо за помощь. Как мне сделать свойства (объекты) внутри разработки дочерними постами родительского поста разработки?

Сначала создайте родительскую страницу/пост (разработку). Затем добавьте новую страницу, и с правой стороны редактора вы увидите блок "Атрибуты страницы", где одно из полей называется "Родительская" с выпадающим списком. Используйте его, чтобы выбрать разработку. Это должно сработать!
