WordPress. Фильтр записей по произвольным полям. Часть 1 из 3

30.09.2019

Теги: CMSWeb-разработкаWordPressКаталогТоваровМетаБоксМетаДанныеПлагинТаксономия

Давайте напишем плагин, который позволит фильтровать пользовательский тип записи по произвольным полям. За основу возьмем плагин «Каталог товаров», чтобы не начинать с полного нуля. Добавим для этого плагина страницу настроек в панели управления, где можно будет создавать фильтры для товаров. А для страницы редактирования записи типа product создадим метабокс, позволяющий задать значения фильтров.

<?php
/*
Plugin Name: Каталог товаров
Plugin URI: https://tokmakov.msk.ru
Description: Позволяет создать на сайте простой каталог товаров с категориями и фильтами.
Version: 1.0
Author: Евгений Токмаков
Author URI: https://tokmakov.msk.ru
*/

register_activation_hook(__FILE__, function() {
    // проверяем права пользователя на установку плагинов
    if (!current_user_can('activate_plugins')) {
        return;
    }
});

register_deactivation_hook(__FILE__, function() {
    // проверяем права пользователя на деактивацию плагинов
    if (!current_user_can('deactivate_plugins')) {
        return;
    }
});
/*
 * Регистрируем пользовательский тип записи product
 */
add_action('init', function () {
    $labels = [
        'name' => 'Товары',
        'menu_name' => 'Товары',
        'singular_name' => 'Товар',
        'add_new' => 'Добавить товар',
        'add_new_item' => 'Добавить новый товар',
        'edit_item' => 'Редактировать товар',
        'new_item' => 'Новый товар',
        'all_items' => 'Все товары',
        'view_item' => 'Посмотреть товар',
        'search_items' => 'Найти товары',
        'not_found' =>  'Ничего не найдено',
        'not_found_in_trash' => 'В корзине не найдено'
    ];
    $args = [
        'labels' => $labels,
        'description' => 'Коллекции женской, мужской и детской обуви на все времена года.',
        'public' => true,
        'publicly_queryable' => true,
        'show_ui' => true,
        'show_in_menu' => true,
        'query_var' => true,
        'capability_type' => 'post',
        'has_archive' => true,
        'rewrite' => true,
        'hierarchical' => false,
        'menu_position' => null,
        'supports' => [
            'title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields'
        ],
        'taxonomies' => ['group'],
    ];
    register_post_type('product', $args);
});
/*
 * Регистрируем иерархическую таксономию (разделы каталога)
 */
add_action('init', function () {
    $labels = array(
        'name'          => 'Категории',
        'singular_name' => 'Категория',
        'menu_name'     => 'Категории' ,
        'all_items'     => 'Все категории',
        'edit_item'     => 'Редактировать категорию',
        'view_item'     => 'Посмотреть категорию',
        'update_item'   => 'Сохранить категорию',
        'add_new_item'  => 'Добавить новую категорию',
        'parent_item'   => 'Родительская категория',
        'search_items'  => 'Поиск по категориям',
        'back_to_items' => 'Назад на страницу категорий',
        'most_used'     => 'Популярные категории',
    );
    $args = array(
        'labels'            => $labels,
        'show_admin_column' => true,
        'hierarchical'      => true,
    );
    register_taxonomy('group', ['product'], $args);
});
/*
 * Добавляем страницу настроек плагина
 */
add_action('admin_menu', function () {
    add_menu_page(
        // содержимое тега title этой страницы
        'Плагин «Товары»',
        // название пункта меню для этой страницы
        'Плагин «Товары»',
        // права доступа, чтобы был показан этот пункт меню
        'manage_options',
        // уникальный идентификатор меню (страницы)
        'tokmakov_catalog_settings',
        // функция выводит содержимое этой страницы
        function () {
            ?>
            <div class="wrap">
                <h1>Настройки плагина «Каталог товаров»</h1>
                <form method="post" action="options.php">
                    <?php
                    settings_fields('tokmakov_catalog_settings');
                    do_settings_sections('tokmakov_catalog_settings');
                    submit_button();
                    ?>
                </form>
            </div>
            <?php
        },
        // иконка для страницы настроек плагина
        'dashicons-admin-settings',
        // позиция страницы, в конце списка
        90
    );
});

Все фильтры будем хранить в опции tokmakov_catalog_filters, в таблице БД wp_options она будет сохраняться в виде сериализованного массива:

Array
(
    [size] => Array
        (
            [text] => Размер
            [value] => Array
                (
                    [small] => маленький
                    [medium] => средний
                    [large] => большой
                )
        )
    [color] => Array
        (
            [text] => Цвет
            [value] => Array
                (
                    [white] => белый
                    [black] => черный
                )
        )
)
/*
 * 1. Регистрируем опцию для плагина
 * 2. Добавляем секцию для этих опции
 * 3. Добавляем поля формы для опции
 */
add_action('admin_init', function () {
    /*
     * 1. Регистрируем опцию для плагина
     */
    register_setting(
        // страница меню, куда будет добавлена эта опция
        'tokmakov_catalog_settings',
        // уникальный идентификатор этой опции (slug)
        'tokmakov_catalog_filters',
        /*
         * Эта функция нужна, чтобы обработать опцию перед сохранением.
         */
        function ($option) {
            /*
             * $filters = array(
             *     'size' => array(
             *         'text' = 'Размер',
             *         'value' => array(
             *             'small' => 'маленький',
             *             'medium' => 'средний',
             *             'large' => 'большой'
             *         )
             *     ),
             *     'color' => array(
             *         'text' = 'Цвет',
             *         'value' => array(
             *             'white' => 'белый',
             *             'black' => 'черный'
             *         )
             *     ),
             * );
             */
            // сюда будем сохранять все данные
            $filters = [];
            foreach ($_POST['tokmakov_catalog_filter_name_slug'] as $i => $slug) {
                $slug = trim($slug);
                // проверяем slug названия фильтра (латиница)
                if (!preg_match('~^[a-z][_a-z0-9]*$~i', $slug)) {
                    continue;
                }
                // slug названия фильтра должен быть уникальным
                if (isset($filters[$slug])) {
                    continue;
                }
                // название фильтра (кириллица)
                if (!empty($_POST['tokmakov_catalog_filter_name_text'][$i])) {
                    $text = $_POST['tokmakov_catalog_filter_name_text'][$i];
                } else {
                    $text = $slug;
                }
                $filters[$slug] = ['text' => $text];
                foreach ($_POST['tokmakov_catalog_filter_value_slug'][$i] as $j => $value) {
                    $value = trim($value);
                    // проверяем slug значения фильтра (латиница)
                    if (!preg_match('~^[a-z][_a-z0-9]*$~i', $value)) {
                        continue;
                    }
                    // slug значения фильтра должен быть уникальным
                    if (isset($filters[$slug]['value'][$value])) {
                        continue;
                    }
                    // название значения фильтра (кириллица)
                    if (!empty($_POST['tokmakov_catalog_filter_value_text'][$i][$j])) {
                        $text = $_POST['tokmakov_catalog_filter_value_text'][$i][$j];
                    } else {
                        $text = $value;
                    }
                    $filters[$slug]['value'][$value] = $text;
                }
                // нужно проверить, что у фильтра есть хотя бы одно значение
                if (empty($filters[$slug]['value'])) {
                    array_pop($filters);
                }
            }

            /*
             * При редактировании могли быть удалены какие-то фильтры или их
             * значения. Значит, мы должны почистить таблицу БД `wp_postmeta`
             * от старых записей.
             */
            global $wpdb;
            $query = "SELECT
                          `".$wpdb->postmeta."`.`meta_id` AS `id`,
                          SUBSTRING(`".$wpdb->postmeta."`.`meta_key`, 26) AS `slug`,
                          CASE
                              LEFT(`".$wpdb->postmeta."`.`meta_value`, 1) WHEN '—'
                          THEN
                              SUBSTRING(`".$wpdb->postmeta."`.`meta_value`, 2)
                          ELSE
                              `".$wpdb->postmeta."`.`meta_value`
                          END
                          AS `value`
                      FROM
                          `".$wpdb->postmeta."`
                      WHERE
                          LEFT(`".$wpdb->postmeta."`.`meta_key`, 25)
                          =
                          '_tokmakov_catalog_filter_'";
            $result = $wpdb->get_results($query);
            foreach ($result as $item) {
                // если фильтр существует
                if (isset($filters[$item->slug])) {
                    // и значение существует
                    if (isset($filters[$item->slug]['value'][$item->value])) {
                        // тогда ничего не делаем
                        continue;
                    }
                }
                // а иначе — удаляем запись
                $wpdb->delete(
                    $wpdb->postmeta,
                    ['meta_id' => $item->id]
                );
            }
            return $filters;
        }
    );

    /*
     * 2. Регистрируем новую секцию для опций
     */
    add_settings_section(
        // уникальный идентификатор секции
        'tokmakov_catalog_filters',
        // заголовок секции нам не нужен
        '',
        // функция выводит описание секции
        function () {
            ?>
            <h2>Фильтры для каталога</h2>
            <p>
                Создайте новые фильтры или отредактируйте уже существующие. Для каждого фильтра
                задаются два названия — кириллицей и латиницей. Аналогично задаются значения
                фильтров — кириллицей и латиницей. Названия фильтров латиницей должны быть
                уникальными и содержать только буквы, цифры и подчеркивание. Аналогично, значения
                фильтров латиницей должны быть уникальнвми внутри каждого фильтра. И содержать
                только буквы, цифры и подчеркивание.
            </p>
            <?php
        },
        // страница, куда будет добавлена эта секция
        'tokmakov_catalog_settings'
    );

    /*
     * 3.1. Добавляем поле формы для редактирования первой опции
     */
    add_settings_field(
        'tokmakov_catalog_filters', // идентификатор поля формы
        'Фильтры для каталога', // заголовок поля формы
        function () { // выводит html-код поля формы
            $filters = get_option('tokmakov_catalog_filters');
            /*
             * $filters = array(
             *     'size' => array(
             *         'text' = 'Размер',
             *         'value' => array(
             *             'small' => 'маленький',
             *             'medium' => 'средний',
             *             'large' => 'большой'
             *         )
             *     ),
             *     'color' => array(
             *         'text' = 'Цвет',
             *         'value' => array(
             *             'white' => 'белый',
             *             'black' => 'черный'
             *         )
             *     ),
             * );
             */
            ?>
            <div id="tokmakov-catalog-filters">
            <?php $i = 0; ?>
            <?php if (!empty($filters)): ?>
                <h4>Существующие фильтры</h4>
                <?php foreach ($filters as $slug => $filter): ?>
                    <fieldset>
                        <legend>Фильтр</legend>
                        <p>Название фильтра (латиница + кириллица)</p>
                        <p>
                            <input
                                type="text"
                                name="tokmakov_catalog_filter_name_slug[<?= $i; ?>]"
                                value="<?= esc_attr($slug); ?>"
                            />
                            <input
                                type="text"
                                name="tokmakov_catalog_filter_name_text[<?= $i; ?>]"
                                value="<?= esc_attr($filter['text']); ?>"
                            />
                        </p>
                        <p>Значения фильтра (латиница + кириллица)</p>
                        <?php foreach ($filter['value'] as $key => $value): ?>
                            <p>
                                <input
                                    type="text"
                                    name="tokmakov_catalog_filter_value_slug[<?= $i; ?>][]"
                                    value="<?= esc_attr($key); ?>"
                                />
                                <input
                                    type="text"
                                    name="tokmakov_catalog_filter_value_text[<?= $i; ?>][]"
                                    value="<?= esc_attr($value); ?>"
                                />
                            </p>
                        <?php endforeach; ?>
                        <p>
                            <input
                                type="text"
                                name="tokmakov_catalog_filter_value_slug[<?= $i; ?>][]"
                                value=""
                                placeholder="Значение фильтра (лат)"
                            />
                            <input
                                type="text"
                                name="tokmakov_catalog_filter_value_text[<?= $i; ?>][]"
                                value=""
                                placeholder="Значение фильтра (кир)"
                            />
                        </p>
                    </fieldset>
                    <?php $i++; ?>
                <?php endforeach; ?>
            <?php endif; ?>
            <h4>Добавить новый фильтр</h4>
            <fieldset>
                <legend>Фильтр</legend>
                <p>Название фильтра (латиница + кириллица)</p>
                <p>
                    <input type="text" name="tokmakov_catalog_filter_name_slug[<?= $i; ?>]"
                           value="" placeholder="Название фильтра (лат)" />
                    <input type="text" name="tokmakov_catalog_filter_name_text[<?= $i; ?>]"
                           value="" placeholder="Название фильтра (кир)" />
                </p>
                <p>Значения фильтра (латиница + кириллица)</p>
                <p>
                    <input type="text" name="tokmakov_catalog_filter_value_slug[<?= $i; ?>][]"
                           value="" placeholder="Значение фильтра (лат)" />
                    <input type="text" name="tokmakov_catalog_filter_value_text[<?= $i; ?>][]"
                           value="" placeholder="Значение фильтра (кир)" />
                </p>
                <p>
                    <input type="text" name="tokmakov_catalog_filter_value_slug[<?= $i; ?>][]"
                           value="" placeholder="Значение фильтра (лат)" />
                    <input type="text" name="tokmakov_catalog_filter_value_text[<?= $i; ?>][]"
                           value="" placeholder="Значение фильтра (кир)" />
                </p>
                <p>
                    <input type="text" name="tokmakov_catalog_filter_value_slug[<?= $i; ?>][]"
                           value="" placeholder="Значение фильтра (лат)" />
                    <input type="text" name="tokmakov_catalog_filter_value_text[<?= $i; ?>][]"
                           value="" placeholder="Значение фильтра (кир)" />
                </p>
            </fieldset>
            </div>
            <?php
        },
        'tokmakov_catalog_settings', // страница меню
        'tokmakov_catalog_filters' // идентификатор секции
    );
});

Форма для добавления и редактирования фильтров имеет вид:

<div id="tokmakov-catalog-filters">
    <h4>Существующие фильтры</h4>
    <fieldset>
        <legend>Фильтр</legend>
        <p>Название фильтра (латиница + кириллица)</p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_name_slug[0]" value="size" />
            <input type="text" name="tokmakov_catalog_filter_name_text[0]" value="Размер" />
        </p>
        <p>Значения фильтра (латиница + кириллица)</p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[0][]" value="small" />
            <input type="text" name="tokmakov_catalog_filter_value_text[0][]" value="маленький" />
        </p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[0][]" value="medium" />
            <input type="text" name="tokmakov_catalog_filter_value_text[0][]" value="средний" />
        </p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[0][]" value="large" />
            <input type="text" name="tokmakov_catalog_filter_value_text[0][]" value="большой" />
        </p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[0][]" value="" placeholder="Значение фильтра (лат)" />
            <input type="text" name="tokmakov_catalog_filter_value_text[0][]" value="" placeholder="Значение фильтра (кир)" />
        </p>
    </fieldset>
    <fieldset>
        <legend>Фильтр</legend>
        <p>Название фильтра (латиница + кириллица)</p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_name_slug[1]" value="color" />
            <input type="text" name="tokmakov_catalog_filter_name_text[1]" value="Цвет" />
        </p>
        <p>Значения фильтра (латиница + кириллица)</p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[1][]" value="white" />
            <input type="text" name="tokmakov_catalog_filter_value_text[1][]" value="белый" />
        </p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[1][]" value="black" />
            <input type="text" name="tokmakov_catalog_filter_value_text[1][]" value="черный" />
        </p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[1][]" value="" placeholder="Значение фильтра (лат)" />
            <input type="text" name="tokmakov_catalog_filter_value_text[1][]" value="" placeholder="Значение фильтра (кир)" />
        </p>
    </fieldset>
    <h4>Добавить новый фильтр</h4>
    <fieldset>
        <legend>Фильтр</legend>
        <p>Название фильтра (латиница + кириллица)</p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_name_slug[2]" value="" placeholder="Название фильтра (лат)" />
            <input type="text" name="tokmakov_catalog_filter_name_text[2]" value="" placeholder="Название фильтра (кир)" />
        </p>
        <p>Значения фильтра (латиница + кириллица)</p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[2][]" value="" placeholder="Значение фильтра (лат)" />
            <input type="text" name="tokmakov_catalog_filter_value_text[2][]" value="" placeholder="Значение фильтра (кир)" />
        </p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[2][]" value="" placeholder="Значение фильтра (лат)" />
            <input type="text" name="tokmakov_catalog_filter_value_text[2][]" value="" placeholder="Значение фильтра (кир)" />
        </p>
        <p>
            <input type="text" name="tokmakov_catalog_filter_value_slug[2][]" value="" placeholder="Значение фильтра (лат)" />
            <input type="text" name="tokmakov_catalog_filter_value_text[2][]" value="" placeholder="Значение фильтра (кир)" />
        </p>
    </fieldset>
</div>

Массив $_POST, который надо обработать и записать в опцию, имеет вид:

Array
(
    [tokmakov_catalog_filter_name_slug] => Array
        (
            [0] => size
            [1] => color
            [2] => 
        )
    [tokmakov_catalog_filter_name_text] => Array
        (
            [0] => Размер
            [1] => Цвет
            [2] => 
        )
    [tokmakov_catalog_filter_value_slug] => Array
        (
            [0] => Array
                (
                    [0] => small
                    [1] => medium
                    [2] => large
                    [3] => 
                )
            [1] => Array
                (
                    [0] => white
                    [1] => black
                    [2] => 
                )
            [2] => Array
                (
                    [0] => 
                    [1] => 
                    [2] => 
                )
        )
    [tokmakov_catalog_filter_value_text] => Array
        (
            [0] => Array
                (
                    [0] => маленький
                    [1] => средний
                    [2] => большой
                    [3] => 
                )
            [1] => Array
                (
                    [0] => белый
                    [1] => черный
                    [2] => 
                )
            [2] => Array
                (
                    [0] => 
                    [1] => 
                    [2] => 
                )
        )
    [submit] => Сохранить изменения
)
/*
 * Подключаем файл стилей для панели управления, чтобы оформить форму фильтров
 */
add_action('admin_enqueue_scripts', function () {
    wp_enqueue_style(
        'tokmakov-catalog',
        plugin_dir_url(__FILE__) . 'backend-style.css'
    );
});
#tokmakov-catalog-filters {

}
    #tokmakov-catalog-filters h4 {
        margin: 7px 0;
    }
    #tokmakov-catalog-filters fieldset {
        border: 1px solid #ddd;
        padding: 10px;
        margin-bottom: 10px;
        background: #fff;
    }
        #tokmakov-catalog-filters fieldset legend {
            border: 1px solid #ddd;
            padding: 1px 10px 2px 10px;
            border-radius: 12px;
            background: #fff;
        }

/*
 * Добавляем метабокс на страницу редактирования товара
 */
add_action('add_meta_boxes', function () {
    add_meta_box(
        'tokmakov-catalog-filters',
        'Значения фильтров',
        function ($post) {
            $filters = get_option('tokmakov_catalog_filters');
            if (empty($filters)) {
                return 'Создайте фильтры на странице настроек плагина';
            }
            ?>
            <?php foreach ($filters as $slug => $filter): ?>
                <?php
                // значения текущего фильтра для этого товара
                $values = get_post_meta(
                    $post->ID,
                    '_tokmakov_catalog_filter_' . $slug
                );
                ?>
                <fieldset>
                    <legend><?= $filter['text']; ?></legend>
                    <?php foreach ($filter['value'] as $value => $text): ?>
                        <?php $checked = in_array($value, $values); ?>
                        <input type="checkbox"
                               name="tokmakov_catalog_filter_<?= $slug; ?>[]"
                               value="<?= $value; ?>"
                               <?php checked($checked); ?> />
                        <?= $text; ?>
                    <?php endforeach; ?>
                </fieldset>
            <?php endforeach; ?>
            <input type="hidden" name="tokmakov_catalog_filters" value="1" />
            <?php
        },
        ['product'],
        'normal',
        'high'
    );
});
/*
 * Сохраняем данные из формы добавления-редактирования товара
 */
add_action('save_post', function ($post_id) {

    // проверяем права пользователя
    if(!current_user_can('edit_post', $post_id)) {
        return;
    }
    // если это автосохранение, то ничего не делаем
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    // тип записи должен быть product
    $post = get_post($post_id);
    if ($post->post_type != 'product') {
        return;
    }
    // только при отправке формы, которую мы изменили
    if (!isset($_POST['tokmakov_catalog_filters'])) {
        return;
    }
    // фильтры еще не созданы, ничего не делаем
    $filters = get_option('tokmakov_catalog_filters');
    if (empty($filters)) {
        return;
    }

    /*
     * Сначала в качестве значений всех фильтров записываем специальное
     * значение, означающее «пустое значение», например «—small» или
     * «—red». Потом обрабатываем POST-данные и записываем реальные
     * значения фильтров для товара, например «small» или «red».
     */
    foreach ($filters as $slug => $filter) {
        foreach ($filter['value'] as $value => $text) {
            $meta = get_post_meta(
                $post_id,
                '_tokmakov_catalog_filter_' . $slug,
                false
            );
            if (!in_array($value, $meta) && !in_array('—'.$value, $meta)) {
                /*
                 * Если такого значения фильтра еще нет, то создаем и записываем
                 * «пустое значение», которое чуть ниже (возможно) перезапишем
                 */
                add_post_meta(
                    $post_id,
                    '_tokmakov_catalog_filter_' . $slug,
                    '—' . $value
                );
            } else {
                /*
                 * Если такое значение фильтра есть, то запишем «пустое значение»,
                 * которое чуть ниже (возможно) перезапишем
                 */
                update_post_meta(
                    $post_id,
                    '_tokmakov_catalog_filter_' . $slug,
                    '—' . $value,
                    $value
                );
            }
        }
    }
    foreach ($filters as $slug => $filter) {
        foreach ($filter['value'] as $value => $text) {
            if (isset($_POST['tokmakov_catalog_filter'][$slug][$value])) {
                update_post_meta(
                    $post_id,
                    '_tokmakov_catalog_filter_' . $slug,
                    $value,
                    '—'.$value
                );
            }
        }
    }
});

Массив $_POST, который нужно обработать при сохранении значений фильтров для товара:

Array
(
    /*...*/
    [tokmakov_catalog_filter] => Array
        (
            [size] => Array
                (
                    [small] => 1
                    [large] => 1
                )

            [color] => Array
                (
                    [white] => 1
                    [black] => 1
                )

        )
    [tokmakov_catalog_filters] => 1
    /*...*/
)

Поиск: CMS • Web-разработка • WordPress • Каталог товаров • Мета бокс • Мета данные • Плагин • Таксономия

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.