Блог
Назад к статьям

Сущности WordPress, свои поля и роль functions.php: нормальная модель данных без плагинов

Произвольные поля (post meta) для Custom Post Type не сохраняются или пустые после обновления. Решение через register_post_meta, add_meta_box и save_post с nonce — код, диагностика и типичные ошибки.

24 апреля 2026 г.
11
WordPresswordpressbackendcustom-post-typespost-metaregister_post_metafunctions-php
Сущности WordPress, свои поля и роль functions.php: нормальная модель данных без плагинов
Сущности WordPress, свои поля и роль functions.php

Запрос wordpress custom fields functions php или произвольные поля не сохраняются часто приводит к ситуации: вы завели Custom Post Type (CPT), добавили метабокс с полем в админке, при сохранении записи значение не попадает в базу — getpostmeta($postid, 'mykey', true) возвращает пустую строку. Ниже — одна конкретная проблема: почему meta для своего типа записи не пишется в wppostmeta и как это исправить кодом в теме без плагинов (registerpostmeta, nonce, приоритет хука). Если у вас ACF и поле не сохраняется, отдельный сценарий разобран в статье blank" rel="noopener noreferrer">ACF поле не сохраняется в WordPress.

В чём проблема

Симптомы:

  • В админке у записи CPT есть метабокс с полем, вы вводите значение и нажимаете «Обновить».
  • После перезагрузки страницы редактирования поле пустое.
  • В базе в таблице wppostmeta для этого postid нет строки с нужным meta_key или значение не обновилось.
  • В шаблоне или в коде getpostmeta($postid, 'repo_url', true) возвращает пусто.

Пример типичной ошибки в коде: сохранение без проверки nonce и без отсечения autosave/revision — в итоге meta перезаписывается пустым при автосохранении или ревизии:

// ❌ Плохо: срабатывает на автосохранении и ревизиях, перетирает meta
add_action('save_post', function ($post_id) {
    update_post_meta($post_id, '_repo_url', $_POST['project_repo_url'] ?? '');
});

Почему возникает:

  1. Не проверяется nonce — запрос может быть подделан; кроме того, без nonce форма могла не отправить поле.
  2. Хук savepost срабатывает на автосохранении и ревизиях — WordPress передаёт в хук ID автосохранения или ревизии; запись в meta идёт «не в тот» пост или перезаписывается пустым $POST.
  3. Ключ meta не зарегистрирован через registerpostmeta — в современных версиях WordPress для корректной работы REST и админки meta для CPT лучше регистрировать.
  4. Логика в functions.php без разделения — десятки хуков и полей в одном файле приводят к конфликтам приоритетов и случайному перезаписыванию.

Итог: одна статья = одна задача — сделать так, чтобы произвольное поле для CPT стабильно сохранялось и читалось, с путём к файлам и проверкой результата.

Как быстро проверить: откройте запись CPT в админке, введите значение в своё поле и нажмите «Обновить». Перезагрузите страницу редактирования. Если поле пустое — проблема в сохранении (хук, nonce или autosave). Если в админке значение есть, а в шаблоне пусто — проверьте ключ в getpostmeta() и то, что вы читаете тот же postid (в цикле используйте getthe_ID()).

Рабочее решение

Используем один CPT (например, project) и одно метаполе (URL репозитория). Код разнесён по файлам темы, чтобы не превращать functions.php в свалку.

Стек: WordPress 6.x, PHP 7.4+, тема с доступом к functions.php и папке inc/.

Путь к файлам:

theme/
├── functions.php
├── inc/
│   ├── cpt.php
│   ├── meta.php
1) Регистрация CPT

Файл: inc/cpt.php

<?php
add_action('init', function () {
    register_post_type('project', [
        'label'       => 'Проекты',
        'public'      => true,
        'supports'    => ['title', 'editor'],
        'has_archive' => true,
        'rewrite'     => ['slug' => 'projects'],
    ]);
});
2) Регистрация meta и метабокс

Файл: inc/meta.php

Регистрируем meta для типа project через registerpostmeta, чтобы ключ был известен ядру (REST, админка). Ключ с подчёркиванием в начале (repourl) скрывает поле из блока «Произвольные поля» в редакторе.

<?php
add_action('init', function () {
    register_post_meta('project', '_repo_url', [
        'type'              => 'string',
        'single'            => true,
        'sanitize_callback' => 'esc_url_raw',
        'auth_callback'     => function ($allowed, $meta_key, $post_id) {
            return current_user_can('edit_post', $post_id);
        },
    ]);
});

add_action('add_meta_boxes', function () {
    add_meta_box(
        'project_repo',
        'Репозиторий',
        'render_project_repo_box',
        'project'
    );
});

function render_project_repo_box($post) {
    wp_nonce_field('project_repo_save', 'project_repo_nonce');
    $value = get_post_meta($post->ID, '_repo_url', true);
    ?>
    <input
        type="url"
        name="project_repo_url"
        value="<?php echo esc_attr($value); ?>"
        style="width:100%;"
    />
    <?php
}
3) Сохранение с проверкой nonce и без autosave/revision

Тот же файл: inc/meta.php

add_action('save_post_project', function ($post_id) {
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    if (wp_is_post_revision($post_id)) {
        return;
    }
    if (!isset($_POST['project_repo_nonce'])
        || !wp_verify_nonce($_POST['project_repo_nonce'], 'project_repo_save')) {
        return;
    }
    if (!current_user_can('edit_post', $post_id)) {
        return;
    }
    if (!isset($_POST['project_repo_url'])) {
        return;
    }

    update_post_meta(
        $post_id,
        '_repo_url',
        esc_url_raw($_POST['project_repo_url'])
    );
});
4) Подключение в теме

Файл: functions.php

<?php
require_once __DIR__ . '/inc/cpt.php';
require_once __DIR__ . '/inc/meta.php';
5) Вывод в шаблоне
<?php
$repo_url = get_post_meta(get_the_ID(), '_repo_url', true);
if ($repo_url) {
    printf(
        '<a href="%s" target="_blank" rel="noopener">Репозиторий</a>',
        esc_url($repo_url)
    );
}

Этого достаточно, чтобы поле стабильно сохранялось и отображалось. Дополнительные варианты (OOP, таксономия): blank" rel="noopener noreferrer">сниппет CPT и таксономия (OOP), blank" rel="noopener noreferrer">сниппет метабокс с безопасным сохранением (OOP).

Почему ключ с подчёркиванием: в редакторе WordPress блок «Произвольные поля» выводит все meta записи. Ключи, начинающиеся с , в этом блоке не показываются — так служебные поля не путаются с теми, что редактор может случайно изменить. Для полей, которые заполняет только ваш код или метабокс, используйте префикс . Третий параметр true в getpostmeta($id, 'repourl', true) означает «вернуть одно значение»; без него вернётся массив (нужно, если по одному ключу хранится несколько значений).

Проверка результата В админке
  1. Открыть запись типа «Проекты».
  2. Ввести URL в поле «Репозиторий», нажать «Обновить».
  3. Обновить страницу редактирования — значение должно остаться.
Через WP-CLI

Подставьте свой post_id (например, 123):

wp post meta get 123 _repo_url

Ожидаемый вывод: строка с URL, например https://github.com/user/repo. Пустой вывод означает, что meta не записалось — тогда смотрите раздел «Типичные ошибки».

В коде (временная проверка)

В шаблоне single для project или в цикле:

<?php
$value = get_post_meta(get_the_ID(), '_repo_url', true);
var_dump($value); // должно вывести сохранённый URL

После проверки var_dump уберите.

Типичные ошибки

1. Хук на save_post без типа поста и без отсечения autosave/revision

Срабатывает на всех типах записей и на автосохранении — meta перетирается. Используйте savepostproject (или свой тип) и в начале обработчика проверяйте DOINGAUTOSAVE и wpispostrevision($post_id).

2. Не проверяется nonce

Без wpverifynonce и проверки currentusercan('editpost', $postid) возможна подмена запроса и запись meta от имени другого поста. Всегда проверяйте nonce и права.

3. Ключ meta без подчёркивания показывается в «Произвольных полях»

Редактор выводит все meta в блоке «Произвольные поля»; ключи с префиксом там не показываются. Для служебных полей используйте repourl, а не repourl.

4. Логика в одном большом functions.php

Сотни строк в functions.php усложняют отладку и порядок хуков. Выносите регистрацию CPT и meta в отдельные файлы и подключайте через require_once, как в блоке выше.

5. Читаете meta по другому ключу

Убедитесь, что в getpostmeta($postid, 'repourl', true) ключ совпадает с тем, что в updatepostmeta. Третий параметр true у getpost_meta возвращает одно значение (строку).

Если после правок meta по-прежнему не сохраняется: убедитесь, что хук именно savepostproject (с суффиксом типа записи), а не общий savepost. Проверьте, что в форме есть wpnoncefield() с тем же action, что и в wpverifynonce(). Временно отключите другие плагины и переключитесь на тему Twenty Twenty-Four — если поле начнёт сохраняться, конфликт в теме или плагине. Для отладки добавьте в начало обработчика savepostproject строку errorlog('savepostproject: ' . $post_id); и убедитесь, что хук срабатывает при нажатии «Обновить», а не только при автосохранении.

Где применять
  • Prod — решение предназначено для боевого сайта: регистрация meta, nonce и проверка прав обязательны.
  • Docker / локальный стенд — те же файлы; удобно проверить через WP-CLI и отключить лишние плагины, чтобы исключить конфликт хуков.
  • CI/CD — можно добавить smoke-тест: создание поста типа project, запись meta через WP-CLI или код, чтение обратно и проверка значения.

Резюме: одна проблема — meta для CPT не сохраняется. Решение — регистрация meta через registerpostmeta, метабокс с nonce, хук savepost{posttype} с отсечением autosave и revision и проверкой прав. Структура темы: functions.php только подключает inc/cpt.php и inc/meta.php. Проверка — админка, WP-CLI, при необходимости временный vardump в шаблоне. Типичные ошибки — общий savepost, отсутствие nonce, ключ без , один огромный functions.php, несовпадение ключа при чтении. Для нескольких метаполей повторите блок registerpostmeta, метабокс и ветку в обработчике savepostproject для каждого ключа. Если CPT зарегистрирован в теме и вы планируете менять тему позже — данные в БД останутся, но тип записей в админке пропадёт, пока не подключите плагин или mu-plugin с той же регистрацией; для долгоживущих проектов регистрацию CPT и meta часто выносят в плагин. В REST API зарегистрированные через registerpostmeta поля автоматически доступны для чтения и записи при корректных authcallback и sanitizecallback.

Дополнительно по теме: blank" rel="noopener noreferrer">WordPress Core Web Vitals 2026: ускорение без магии, blank" rel="noopener noreferrer">Migrations Plugin: управление миграциями БД WordPress.