Создание функциональных модулей WordPress с помощью PHP

Раздел: -> WordPress

Основные подходы к разработке под WordPress на PHP

Как создать собственный тип записи с метабоксами и сохранением данных?

Наиболее эффективное решение для расширения функциональности WordPress - это создание произвольного типа записи (Custom Post Type) с дополнительными полями. Это позволяет организовать данные с уникальной структурой и вывести их на сайт.

Регистрация типа записи

Используется функция register_post_type(), которую нужно поместить в файл functions.php темы или в отдельный плагин. Пример регистрации типа записи "Книга":


function my_register_book() {
    $labels = array(
        'name'               => 'Книги',
        'singular_name'      => 'Книга',
        'add_new'            => 'Добавить новую',
        'add_new_item'       => 'Добавить новую книгу',
        'edit_item'          => 'Редактировать книгу',
        'view_item'          => 'Просмотреть книгу',
        'search_items'       => 'Искать книги',
        'not_found'          => 'Книги не найдены',
        'not_found_in_trash' => 'В корзине книг нет'
    );
    $args = array(
        'labels'        => $labels,
        'public'        => true,
        'has_archive'   => true,
        'supports'      => array('title', 'editor', 'thumbnail'),
        'menu_icon'     => 'dashicons-book'
    );
    register_post_type('book', $args);
}
add_action('init', 'my_register_book');
  

После активации кода в админ-меню появится пункт "Книги". Проблемы могут возникнуть при конфликте названий - следует выбирать уникальные префиксы для функций и названий типов записей.

Типичная ошибка - вызов register_post_type() слишком рано, до хука init. Решение: всегда подключать регистрацию через add_action('init', ...).

Добавление метабоксов

Для хранения дополнительных данных (например, автор книги) создаются метабоксы с помощью add_meta_box() и сохранения через save_post.


function my_add_book_metabox() {
    add_meta_box(
        'book_author',
        'Автор книги',
        'my_book_author_callback',
        'book',
        'normal',
        'high'
    );
}
add_action('add_meta_boxes', 'my_add_book_metabox');

function my_book_author_callback($post) {
    wp_nonce_field('book_author_save', 'book_author_nonce');
    $value = get_post_meta($post->ID, '_book_author', true);
    echo '<input type="text" name="book_author" value="' . esc_attr($value) . '" style="width:100%" />';
}

function my_save_book_meta($post_id) {
    if (!isset($_POST['book_author_nonce']) || !wp_verify_nonce($_POST['book_author_nonce'], 'book_author_save')) {
        return;
    }
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    if (!current_user_can('edit_post', $post_id)) {
        return;
    }
    if (isset($_POST['book_author'])) {
        update_post_meta($post_id, '_book_author', sanitize_text_field($_POST['book_author']));
    }
}
add_action('save_post', 'my_save_book_meta');
  

Важно: ключи мета-полей начинаются с подчёркивания, чтобы скрыть их из произвольного вывода. Проблема: отсутствие nonce-проверки делает сохранение уязвимым. Всегда используется wp_nonce_field() и wp_verify_nonce().

Вариант 1. Использование WP_Query для кастомных запросов

Как вывести записи произвольного типа с фильтрацией по мета-полям?

Класс WP_Query позволяет строить сложные выборки. Пример вывода всех книг с автором "Толстой":


$query = new WP_Query(array(
    'post_type' => 'book',
    'meta_query' => array(
        array(
            'key'     => '_book_author',
            'value'   => 'Толстой',
            'compare' => '='
        )
    )
));
if($query->have_posts()) {
    while($query->have_posts()) {
        $query->the_post();
        echo '<h2>' . get_the_title() . '</h2>';
    }
    wp_reset_postdata();
}
  

Ошибка: забытый wp_reset_postdata() может повлиять на глобальный объект $post после цикла. Решение: всегда вызывать сброс после кастомного запроса.

Вариант 2. Создание шорткодов

Как добавить произвольный PHP-вывод в контент записи?

Шорткоды позволяют вставлять динамический контент. Пример шорткода [book_info id="123"]:


function my_book_info_shortcode($atts) {
    $atts = shortcode_atts(array('id' => 0), $atts);
    $post_id = intval($atts['id']);
    if (!$post_id) return 'Неверный ID';
    $author = get_post_meta($post_id, '_book_author', true);
    $title  = get_the_title($post_id);
    return '<div class="book-info">' . esc_html($title) . ' - ' . esc_html($author) . '</div>';
}
add_shortcode('book_info', 'my_book_info_shortcode');
  

Проблема: вывод без esc_html() приводит к XSS-уязвимости. Решение: всегда экранировать данные перед выводом.

Вариант 3. Использование хуков (actions и filters)

Как изменить стандартное поведение WordPress без изменения ядра?

Хуки позволяют перехватывать выполнение кода. Пример - добавление текста после содержимого каждой книги:


function my_add_book_footer($content) {
    if (get_post_type() === 'book') {
        $content .= '<p class="book-footer">Спасибо за чтение!</p>';
    }
    return $content;
}
add_filter('the_content', 'my_add_book_footer');
  

Ошибка: изменение содержимого без проверки типа записи может затронуть все страницы. Решение: использовать условные теги (get_post_type()).

Вариант 4. Создание виджета

Как разместить произвольный PHP-вывод в боковой панели?

Виджеты регистрируются через расширение класса WP_Widget. Пример виджета для показа случайной книги:


class Random_Book_Widget extends WP_Widget {
    function __construct() {
        parent::__construct('random_book', 'Случайная книга');
    }
    function widget($args, $instance) {
        echo $args['before_widget'];
        echo $args['before_title'] . 'Рекомендуем' . $args['after_title'];
        $books = get_posts(array('post_type' => 'book', 'posts_per_page' => 1, 'orderby' => 'rand'));
        if ($books) {
            foreach ($books as $b) {
                echo '<p>' . get_the_title($b) . '</p>';
            }
        }
        echo $args['after_widget'];
    }
    function form($instance) { /* форма настроек */ }
    function update($new_instance, $old_instance) { return $new_instance; }
}
function my_register_widget() {
    register_widget('Random_Book_Widget');
}
add_action('widgets_init', 'my_register_widget');
  

Проблема: виджет использует get_posts без кэширования. При большом числе книг запрос может быть медленным. Решение: применить WP_Query с кэшированием или transient API.

Вариант 5. Использование REST API

Как предоставить данные произвольного типа записи через API для внешних приложений?

WordPress REST API автоматически включает стандартные типы записей. Для кастомных типов нужно указать 'show_in_rest' => true при регистрации. Также можно добавить поля:


function my_register_book_rest() {
    register_rest_field('book', 'book_author', array(
        'get_callback' => function($post) {
            return get_post_meta($post['id'], '_book_author', true);
        }
    ));
}
add_action('rest_api_init', 'my_register_book_rest');
  

Теперь по адресу /wp-json/wp/v2/book/1 будет возвращаться JSON с полем book_author.

Ошибка: в кастомных полях не возвращаются приватные мета (начинающиеся с _). Чтобы включить их, нужно задать 'show_in_rest' => true для мета-ключа или использовать register_rest_field.

Дополнительные примеры и нестандартные решения

Сложный WP_Query с сортировкой и пагинацией

Пример запроса книг, авторами которых являются "Толстой" или "Достоевский", отсортированных по дате в обратном порядке, с постраничной навигацией:

Пример

$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
$args = array(
    'post_type'      => 'book',
    'posts_per_page' => 5,
    'paged'          => $paged,
    'meta_query'     => array(
        'relation' => 'OR',
        array(
            'key'   => '_book_author',
            'value' => 'Толстой'
        ),
        array(
            'key'   => '_book_author',
            'value' => 'Достоевский'
        )
    ),
    'orderby'        => 'date',
    'order'          => 'DESC'
);
$query = new WP_Query($args);
if ($query->have_posts()) {
    while ($query->have_posts()) {
        $query->the_post();
        echo '<h2>' . get_the_title() . '</h2>';
        echo '<p>Автор: ' . get_post_meta(get_the_ID(), '_book_author', true) . '</p>';
    }
    // Пагинация
    echo paginate_links(array(
        'total'     => $query->max_num_pages,
        'current'   => max(1, get_query_var('paged')),
        'prev_text' => 'Назад',
        'next_text' => 'Вперёд'
    ));
    wp_reset_postdata();
} else {
    echo '<p>Книги не найдены.</p>';
}
Результат: список из 5 книг (например, "Война и мир", "Преступление и наказание") со ссылками на следующие страницы.

Шорткод с атрибутами и кэшированием через Transients

Шорткод выводит список книг по заданному автору, кэшируя результат на час:

Пример

function my_cached_books_shortcode($atts) {
    $atts = shortcode_atts(array('author' => ''), $atts);
    $author = sanitize_text_field($atts['author']);
    if (empty($author)) return 'Укажите автора.';

    $cache_key = 'books_author_' . md5($author);
    $output = get_transient($cache_key);
    if ($output === false) {
        $query = new WP_Query(array(
            'post_type'  => 'book',
            'meta_key'   => '_book_author',
            'meta_value' => $author
        ));
        $output = '<ul>';
        if ($query->have_posts()) {
            while ($query->have_posts()) {
                $query->the_post();
                $output .= '<li>' . get_the_title() . '</li>';
            }
            wp_reset_postdata();
        } else {
            $output .= '<li>Нет книг этого автора.</li>';
        }
        $output .= '</ul>';
        set_transient($cache_key, $output, HOUR_IN_SECONDS);
    }
    return $output;
}
add_shortcode('cached_books', 'my_cached_books_shortcode');
Использование: [cached_books author="Толстой"] - выводит кэшированный список, который обновляется раз в час.

Добавление собственной колонки в админ-список записей

Для удобства можно показывать мета-поле "автор" в таблице всех книг в админке:

Пример

function my_add_book_columns($columns) {
    $columns['book_author'] = 'Автор';
    return $columns;
}
add_filter('manage_book_posts_columns', 'my_add_book_columns');

function my_show_book_author_column($column, $post_id) {
    if ($column === 'book_author') {
        $value = get_post_meta($post_id, '_book_author', true);
        echo esc_html($value ?: '-');
    }
}
add_action('manage_book_posts_custom_column', 'my_show_book_author_column', 10, 2);
В админ-таблице книг появится столбец "Автор" с соответствующими значениями.

REST API с кастомными маршрутами

Создание собственного эндпоинта для получения книг по автору:

Пример

function my_books_by_author_route() {
    register_rest_route('my/v1', '/books-by-author/(?P<author>[a-zA-Zа-я\s]+)', array(
        'methods'  => 'GET',
        'callback' => 'my_get_books_by_author',
        'args'     => array(
            'author' => array(
                'required' => true,
                'sanitize_callback' => 'sanitize_text_field'
            )
        )
    ));
}
add_action('rest_api_init', 'my_books_by_author_route');

function my_get_books_by_author($request) {
    $author = $request->get_param('author');
    $posts = get_posts(array(
        'post_type'  => 'book',
        'meta_key'   => '_book_author',
        'meta_value' => $author,
        'numberposts' => -1
    ));
    $data = array();
    foreach ($posts as $post) {
        $data[] = array(
            'id'    => $post->ID,
            'title' => get_the_title($post),
            'link'  => get_permalink($post)
        );
    }
    return new WP_REST_Response($data, 200);
}
GET /wp-json/my/v1/books-by-author/Толстой - вернёт JSON со списком книг.

Виджет с настройками и кэшированием

Усложнённый виджет, позволяющий выбирать количество книг и сортировку:

Пример

class Advanced_Book_Widget extends WP_Widget {
    function __construct() {
        parent::__construct('advanced_book', 'Продвинутые книги');
    }
    function widget($args, $instance) {
        $count  = !empty($instance['count']) ? intval($instance['count']) : 3;
        $order  = !empty($instance['order']) ? $instance['order'] : 'rand';
        $cache_key = 'widget_books_' . $this->id . '_' . $count . '_' . $order;
        $output = get_transient($cache_key);
        if ($output === false) {
            $query = new WP_Query(array(
                'post_type'      => 'book',
                'posts_per_page' => $count,
                'orderby'        => $order,
                'no_found_rows'  => true,
                'update_post_meta_cache' => false,
                'update_post_term_cache' => false
            ));
            $output = $args['before_widget'];
            $output .= $args['before_title'] . 'Книги' . $args['after_title'];
            $output .= '<ul>';
            while ($query->have_posts()) {
                $query->the_post();
                $output .= '<li><a href="' . get_permalink() . '">' . get_the_title() . '</a></li>';
            }
            wp_reset_postdata();
            $output .= '</ul>';
            $output .= $args['after_widget'];
            set_transient($cache_key, $output, HOUR_IN_SECONDS);
        }
        echo $output;
    }
    function form($instance) {
        $count = isset($instance['count']) ? intval($instance['count']) : 3;
        $order = isset($instance['order']) ? $instance['order'] : 'rand';
        ?>
        <p>
            <label for="<?php echo $this->get_field_id('count'); ?>">Количество:</label>
            <input id="<?php echo $this->get_field_id('count'); ?>" name="<?php echo $this->get_field_name('count'); ?>" type="number" value="<?php echo $count; ?>" min="1" max="10" />
        </p>
        <p>
            <label for="<?php echo $this->get_field_id('order'); ?>">Сортировка:</label>
            <select id="<?php echo $this->get_field_id('order'); ?>" name="<?php echo $this->get_field_name('order'); ?>">
                <option value="rand" <?php selected($order, 'rand'); ?>>Случайно</option>
                <option value="date" <?php selected($order, 'date'); ?>>По дате</option>
                <option value="title" <?php selected($order, 'title'); ?>>По названию</option>
            </select>
        </p>
        <?php
    }
    function update($new_instance, $old_instance) {
        $instance = array();
        $instance['count'] = intval($new_instance['count']);
        $instance['order'] = sanitize_text_field($new_instance['order']);
        return $instance;
    }
}
Виджет отображает заданное число книг (кэшируется) и позволяет настраивать сортировку через админку.

Разработка под WordPress на PHP - comments

En
Wp php (php)