Метаданные и поля материала
У плагина появились четыре карточки курса, но количество уроков пока существует только в редакционной таблице. Добавим поле pw_lessons, чтобы JavaScript имел значение 20, HTML и CSS — 16, Markdown и производительность — по 12. Результат этой главы — одно структурированное значение с понятным чтением и двумя границами изменения: собственная редакторская форма и зарегистрированная REST-схема.
Метаданные расширяют объект дополнительными значениями. Это не замена содержимого: аудитория и объяснение курса остаются в тексте, количество хранится отдельным числом. register_post_meta сообщает тип, одиночность и REST-представление поля; его контракт описан в справочнике функции. Регистрация не создаёт красивую панель редактирования автоматически. В нашем примере добавим небольшой metabox и явно обозначим его ограничения.
Схема числового поля
Продолжаем файл Tools после урока 17. Добавьте регистрацию поля на init, после регистрации типа по приоритету. Ниже полный новый фрагмент, остальной код плагина сохраняется:
function pw_workshop_register_course_meta(): void {
register_post_meta('pw_course', 'pw_lessons', [
'type' => 'integer',
'single' => true,
'default' => 0,
'show_in_rest' => [
'schema' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 500],
],
'auth_callback' => static function ($allowed, $key, $object_id): bool {
return current_user_can('edit_post', (int) $object_id);
},
]);
}
add_action('init', 'pw_workshop_register_course_meta', 20);
Ноль означает «количество ещё не указано», а не обещание пустого законченного курса. Для подтверждённых карточек мы запишем известные числа. single означает одно значение данного ключа для объекта в этом представлении. Предел 500 выбран для учебного договора, не для универсальной модели школы. Если курс вырастет больше, сначала изменяется схема и потребители, а не только HTML-атрибут формы.
Auth callback участвует в проверке изменения метаданных. Он спрашивает право редактировать конкретный объект, не сравнивает строковое название роли. Публичное чтение выбранного REST-поля опубликованного курса допустимо в нашей модели: количество уроков не является приватным свойством. Не используйте этот пример для секретных данных, предполагая, что callback автоматически скроет их от всех читателей. Для публичного ответа надо заранее выбрать безопасные поля.
Редакторская форма
Следующий фрагмент целиком добавляет metabox с одним полем. Тип WP_Post определяет текущий объект WordPress, а значения экранируются при включении в атрибут HTML:
function pw_workshop_add_course_box(): void {
add_meta_box('pw-course-lessons', 'Количество уроков', 'pw_workshop_course_box', 'pw_course', 'side');
}
function pw_workshop_course_box(WP_Post $post): void {
wp_nonce_field('pw_course_lessons', 'pw_course_lessons_nonce');
$value = (int) get_post_meta($post->ID, 'pw_lessons', true);
echo '<label for="pw_course_lessons">От 0 до 500; 0 — пока не указано</label>';
printf('<input id="pw_course_lessons" name="pw_course_lessons" type="number" min="0" max="500" value="%s">', esc_attr((string) $value));
}
add_action('add_meta_boxes_pw_course', 'pw_workshop_add_course_box');
Атрибуты min и max помогают обычному взаимодействию, но не являются серверной защитой. Посетитель может отправить другое тело запроса, а браузерные ограничения можно обойти. Поэтому ниже отдельно обрабатываем сохранение. Nonce подтверждает контекст намерения, а capability подтверждает полномочия. Документация nonces прямо отделяет их от аутентификации и авторизации; в собственном обработчике нужны обе проверки.
Полный обработчик сохранения добавляется в тот же файл:
function pw_workshop_save_course_lessons(int $post_id, WP_Post $post, bool $update): void {
if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
$nonce = $_POST['pw_course_lessons_nonce'] ?? null;
$raw = $_POST['pw_course_lessons'] ?? null;
if (!is_string($nonce) || !is_string($raw)) {
return;
}
if (!wp_verify_nonce(wp_unslash($nonce), 'pw_course_lessons')) {
return;
}
$value = filter_var(wp_unslash($raw), FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 500],
]);
if ($value === false) {
return;
}
update_post_meta($post_id, 'pw_lessons', $value);
}
add_action('save_post_pw_course', 'pw_workshop_save_course_lessons', 10, 3);
Проверка === false не путает допустимый ноль с отказом валидации. При массиве вместо строки, отсутствии nonce или числе за пределами обработчик сохраняет прежнее значение. Он не выводит собственного административного сообщения об ошибке. Это ограничение учебной формы, а не рекомендуемый полный UX для редакции: рабочая версия должна понятно сообщать, почему число не принято. Остальная запись при этом может сохраниться своим обычным процессом.
Где действуют ограничения
REST-схема проверяет тип и границы REST-запроса. Наш обработчик проверяет собственную форму. Произвольный другой код, вызывающий update_post_meta, обязан соблюдать тот же предметный договор; регистрация не превращает базу в универсальный числовой constraint. Поэтому не заявляем абсолютный инвариант для любых SQL-изменений или сторонних расширений. Для проекта с несколькими писателями потребуется единый сервис изменения или другой контролируемый путь.
Тип pw_course в предыдущем уроке включает поддержку custom-fields, необходимую для ожидаемой работы метаданных через REST. Однако это не создаёт полноценную современную боковую панель блочного редактора с индивидуальным интерфейсом поля. Наш metabox является отдельным учебным представлением. Совместимость его размещения и сохранения с конкретной версией редактора надо проверить самостоятельно. В этой задаче такая проверка не выполнялась, и снимки отмечены not_run.
Снятие экранирующих слешей, валидация и экранирование вывода решают разные задачи. wp_unslash восстанавливает полученную строку из принятого WordPress представления входа. filter_var определяет, является ли эта строка допустимым целым в нашем диапазоне. esc_attr подготавливает уже известное значение для HTML-атрибута. Ни одна из этих операций не назначает пользователю права. Такое разделение помогает не заменять проверку полномочий словом «данные очищены», когда запрос вообще не должен менять выбранный курс.
Отдельно рассмотрите сохранение страницы без нашей формы, например через REST. Если nonce и поле metabox отсутствуют, обработчик просто возвращается и не перезаписывает pw_lessons нулём. У REST есть собственная аутентификация, право обновить объект и зарегистрированная схема метаданных. Нельзя требовать от публичного GET nonce редакторской формы: чтение числа и изменение числа имеют разные контракты. Урок 19 покажет только чтение, поэтому не передаст администраторские cookies или токен из кода браузера в неизвестный сервис.
Ревизия текста курса также не является автоматически подтверждённой ревизией этого поля. В нашем коде не включена отдельная поддержка восстановления метаданных вместе с редакционными ревизиями. Если число изменили ошибочно, его можно исправить по источнику редакционных данных; для полноценного возврата состояния нужен документированный снимок базы. Рабочий проект может расширить поддержку ревизий поля, но должен проверить, какие значения возвращаются при восстановлении. Не обещаем эту возможность по наличию обычной истории изменений тела записи.
Значения четырёх курсов
На стенде откройте каждый курс, запишите 20, 16, 12 и 12, сохраните и повторно откройте. Ожидается прежнее число в поле. Для JavaScript попробуйте 501 и отрицательное значение: собственный обработчик не должен перезаписать корректные 20. Затем передайте ноль: это допустимое «не указано», поэтому наблюдаемое значение отличается от ошибочного отказа. После эксперимента верните подтверждённое 20, чтобы следующая глава читала исходные данные серии.
Количество является редакционным утверждением, а не вычисленным числом связанных уроков. Если текст говорит «21 глава», а поле хранит 20, надо исправить источник данных и процесс, не скрывать одну из цифр. Следующий урок прочитает опубликованные карточки через REST API и покажет это число в клиентском представлении. Там особенно важно сохранить границу: API передаёт зарегистрированное значение, но не доказывает существование двадцати учебных страниц.