Кастомные поля WordPress: get_post_meta, ACF и свои метабоксы

Стандартные поля WordPress — заголовок, контент, миниатюра — покрывают далеко не все задачи. Для каталога товаров нужна цена, для портфолио — ссылка на проект, для рецептов — время приготовления. Кастомные поля (post meta) решают эту задачу. Рассмотрим три подхода: нативные функции WordPress, собственные метабоксы и плагин ACF.

Нативные функции: add, get, update, delete

WordPress хранит кастомные поля в таблице wp_postmeta. Для работы с ними есть четыре базовые функции:

// Добавление мета-поля
add_post_meta( $post_id, '_product_price', 1500 );

// Получение значения
$price = get_post_meta( $post_id, '_product_price', true );
// true — вернуть одно значение (строку)
// false — вернуть массив всех значений с этим ключом

// Обновление (создаст, если не существует)
update_post_meta( $post_id, '_product_price', 2000 );

// Удаление
delete_post_meta( $post_id, '_product_price' );

// Получение ВСЕХ мета-полей записи
$all_meta = get_post_meta( $post_id );
// Вернёт ассоциативный массив: ключ => array( значения )

Префикс _ (нижнее подчёркивание) перед ключом скрывает поле из стандартного блока “Произвольные поля” в редакторе. Используйте его для полей, которые управляются через метабоксы или код.

Разница между add_post_meta и update_post_meta: add может создать несколько записей с одинаковым ключом (если передать четвёртый параметр false), update всегда обновляет существующую или создаёт одну новую.

Создание метабокса в админке

Метабокс — это блок в редакторе записи с вашими полями. Создадим метабокс для товара с ценой и артикулом:

// Регистрация метабокса
add_action( 'add_meta_boxes', 'product_add_meta_boxes' );

function product_add_meta_boxes() {
    add_meta_box(
        'product_details',           // ID
        'Характеристики товара',     // заголовок
        'product_meta_box_callback', // функция вывода
        'post',                      // тип записи (или массив типов)
        'normal',                    // позиция: normal, side, advanced
        'high'                       // приоритет: high, core, default, low
    );
}

// Вывод HTML метабокса
function product_meta_box_callback( $post ) {
    // Nonce для безопасности
    wp_nonce_field( 'product_meta_save', 'product_meta_nonce' );

    $price = get_post_meta( $post->ID, '_product_price', true );
    $sku   = get_post_meta( $post->ID, '_product_sku', true );
    $avail = get_post_meta( $post->ID, '_product_available', true );

    echo '<table class="form-table">';

    echo '<tr>';
    echo '<th><label for="product_price">Цена (руб.)</label></th>';
    echo '<td><input type="number" id="product_price" name="product_price" value="' . esc_attr( $price ) . '" min="0" step="0.01" class="regular-text"></td>';
    echo '</tr>';

    echo '<tr>';
    echo '<th><label for="product_sku">Артикул</label></th>';
    echo '<td><input type="text" id="product_sku" name="product_sku" value="' . esc_attr( $sku ) . '" class="regular-text"></td>';
    echo '</tr>';

    echo '<tr>';
    echo '<th>В наличии</th>';
    echo '<td><label><input type="checkbox" name="product_available" value="1" ' . checked( $avail, '1', false ) . '> Да</label></td>';
    echo '</tr>';

    echo '</table>';
}

// Сохранение данных
add_action( 'save_post', 'product_save_meta', 10, 2 );

function product_save_meta( $post_id, $post ) {
    // Проверка nonce
    if ( ! isset( $_POST['product_meta_nonce'] ) ||
         ! wp_verify_nonce( $_POST['product_meta_nonce'], 'product_meta_save' ) ) {
        return;
    }

    // Проверка прав
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    // Пропускаем автосохранение
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    // Сохраняем поля
    if ( isset( $_POST['product_price'] ) ) {
        update_post_meta( $post_id, '_product_price',
            sanitize_text_field( $_POST['product_price'] )
        );
    }

    if ( isset( $_POST['product_sku'] ) ) {
        update_post_meta( $post_id, '_product_sku',
            sanitize_text_field( $_POST['product_sku'] )
        );
    }

    // Чекбокс: если не отмечен, $_POST не содержит ключ
    update_post_meta( $post_id, '_product_available',
        isset( $_POST['product_available'] ) ? '1' : '0'
    );
}

Три проверки при сохранении обязательны: nonce защищает от CSRF-атак, current_user_can — от несанкционированного доступа, DOING_AUTOSAVE — от перезаписи при автосохранении.

Вывод кастомных полей в шаблоне

После сохранения поля доступны в шаблонах через get_post_meta:

<?php
// В single.php или content-single.php
$price = get_post_meta( get_the_ID(), '_product_price', true );
$sku   = get_post_meta( get_the_ID(), '_product_sku', true );
$avail = get_post_meta( get_the_ID(), '_product_available', true );

if ( $price ) : ?>
    <div class="product-info">
        <p class="product-price">
            Цена: <strong><?php echo esc_html( number_format( $price, 0, ',', ' ' ) ); ?> руб.</strong>
        </p>

        <?php if ( $sku ) : ?>
            <p class="product-sku">Артикул: <?php echo esc_html( $sku ); ?></p>
        <?php endif; ?>

        <p class="product-availability">
            <?php echo $avail === '1' ? '✓ В наличии' : '✗ Нет в наличии'; ?>
        </p>
    </div>
<?php endif; ?>

Всегда экранируйте вывод через esc_html или esc_attr. Даже если вы сами сохраняете данные, привычка экранировать вывод предотвращает XSS-уязвимости.

Запросы по мета-полям через WP_Query

Кастомные поля можно использовать для фильтрации записей:

// Товары дешевле 5000 руб., в наличии, отсортированные по цене
$products = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => 20,
    'meta_query'     => array(
        'relation' => 'AND',
        'price_clause' => array(
            'key'     => '_product_price',
            'value'   => 5000,
            'compare' => '<=',
            'type'    => 'NUMERIC',
        ),
        array(
            'key'     => '_product_available',
            'value'   => '1',
            'compare' => '=',
        ),
    ),
    'orderby' => 'price_clause',  // сортировка по мета-полю
    'order'   => 'ASC',
) );

// Записи, у которых ЕСТЬ определённое мета-поле
$with_price = new WP_Query( array(
    'meta_key' => '_product_price',
    'meta_query' => array(
        array(
            'key'     => '_product_price',
            'compare' => 'EXISTS',
        ),
    ),
) );

Учтите, что meta_query создаёт JOIN к таблице postmeta, что может замедлить запросы при большом количестве записей. Для высоконагруженных сайтов рассмотрите таксономии вместо мета-полей для фильтрации.

ACF: Advanced Custom Fields

ACF — самый популярный плагин для работы с кастомными полями. Он предоставляет визуальный конструктор полей и удобные функции для шаблонов:

// Получение значения ACF-поля
$price = get_field( 'product_price' );           // текущая запись
$price = get_field( 'product_price', $post_id ); // конкретная запись

// Вывод значения
the_field( 'product_price' );  // echo get_field(...)

// Группа полей (Group)
$details = get_field( 'product_details' );
echo $details['color'];
echo $details['weight'];

// Repeater (повторитель) — PRO версия
if ( have_rows( 'specifications' ) ) :
    echo '<table>';
    while ( have_rows( 'specifications' ) ) : the_row();
        echo '<tr>';
        echo '<td>' . esc_html( get_sub_field( 'spec_name' ) ) . '</td>';
        echo '<td>' . esc_html( get_sub_field( 'spec_value' ) ) . '</td>';
        echo '</tr>';
    endwhile;
    echo '</table>';
endif;

// Поле-изображение (возврат как массив)
$image = get_field( 'product_photo' );
if ( $image ) :
    echo '<img src="' . esc_url( $image['sizes']['medium'] ) . '" '
       . 'alt="' . esc_attr( $image['alt'] ) . '" '
       . 'width="' . $image['sizes']['medium-width'] . '">';
endif;

// Условная логика в шаблоне
if ( get_field( 'show_banner' ) ) {
    get_template_part( 'template-parts/banner' );
}

ACF хранит данные в той же таблице wp_postmeta, что и нативные функции. Поэтому get_post_meta( $id, 'product_price', true ) вернёт то же значение, что и get_field( 'product_price', $id ). Это значит, что вы можете начать с ACF для удобства, а потом перейти на нативные функции без миграции данных.

Когда что использовать

  • Нативные функции — когда нужно 1-3 простых поля и вы не хотите зависеть от плагина. Идеально для тем и плагинов, которые распространяются.
  • Свои метабоксы — когда нужен полный контроль над интерфейсом и логикой сохранения. Подходит для сложных кастомных решений.
  • ACF — когда нужно много разных полей, повторители, гибкое содержимое. Ускоряет разработку в разы, но добавляет зависимость от плагина.

Независимо от выбранного подхода, помните о безопасности: всегда проверяйте nonce при сохранении, валидируйте и санитизируйте входные данные, экранируйте вывод. Кастомные поля — мощный инструмент, который превращает WordPress из блог-платформы в полноценную CMS для любых задач.