Связь двух разных произвольных типов записей

5 мая 2014 г., 15:15:14
Просмотры: 20K
Голосов: 3

Надеюсь, кто-нибудь сможет помочь, так как это очень сложная задача...

Я пытаюсь создать сайт недвижимости на WordPress и хотел бы связать два произвольных типа записей -

Один будет для жилых комплексов, а другой для отдельных объектов недвижимости внутри этого жилого комплекса

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

Я прочитал похожий вопрос - Как связать разные произвольные типы записей? и ответ от Scuba Kay был почти тем, что мне нужно, но я не знаю, как запросить определенный объект недвижимости, который принадлежит определенному комплексу

Заранее спасибо!

1
Комментарии
Все ответы на вопрос 3
0
16

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

Вот класс, который при загрузке и инициализации будет:

  • создавать метабокс для типа записи 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 для связанных данных. Экономия ресурсов.
  • Можно адаптировать для связей с несколькими объектами, экономя дополнительные запросы.
  • Можно использовать для добавления колонок в админке, например показывая район для каждого объекта в списке.

Но и так уже достаточно длинно.

11 окт. 2015 г. 01:35:01
0

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

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

5 мая 2014 г. 15:59:51
2

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

5 мая 2014 г. 16:39:03
Комментарии

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

Kieran Kieran
6 мая 2014 г. 00:50:03

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

campatsky campatsky
6 мая 2014 г. 18:52:49