В этой статье я поделись классом Kama Post Meta Box, с помощью которого можно быстро создавать метаполя для записей, просто указав их в виде массива. Получается своего рода конструктор. Этот класс автоматически очищает данные при сохранении и в некоторых случаях может защитить от взлома сайта.
Статью на эту тему я уже писал: «Блок произвольных полей в админке WordPress своими руками». Правда было это давно, но статья по-прежнему актуальна и может пригодится, когда нужно создать произвольные поля для постов, без использования плагинов. Однако в том варианте все нужно делать вручную, включая создание html каждого поля формы — это неудобно. Также, тот вариант требует определенных знаний: умение работать с хуками и т.д.
Покажу на примере, как легко можно создать произвольные поля для записей. Допустим, нам нужно создать 4 SEO поля: title, description, keywords и robots для всех типов записей. Для создания метабокса нужно вызвать класс Kama_Post_Meta_Box с передачей ему параметров:
<?php
if( class_exists( 'Kama_Post_Meta_Box' ) ){
return;
}
/**
* Creates a block of custom fields for the specified post types.
*
* Possible parameters of the class, see: `Kama_Post_Meta_Box::__construct()`.
* Possible parameters for each field, see in: `Kama_Post_Meta_Box::field()`.
*
* When saved, clears each field, via: `wp_kses()` or sanitize_text_field().
* The sanitizing function can be replaced via a hook `kpmb_save_sanitize_{id}`.
* And You can also specify the name of the sanitizing function in the `save_sanitize` parameter.
* If you specify a sanitizing function in both a parameter and a hook, then both will work!
* Both sanitizing functions gets two parameters: `$metas` (all meta-fields), `$post_id`.
*
* The block is rendered and the meta-fields are saved for users with edit current post capability only.
*
* Requires PHP: 7.2
*
* @changlog https://github.com/doiftrue/Kama_Post_Meta_Box/blob/master/changelog.md
*
* @version 1.17
*/
class Kama_Post_Meta_Box {
use Kama_Post_Meta_Box__Themes;
use Kama_Post_Meta_Box__Sanitizer;
/** @var object */
public $opt;
/** @var string */
public $id;
/** @var array */
static $instances = array();
/** @var Kama_Post_Meta_Box_Fields */
protected $fields_class;
protected const METABOX_ARGS = [
'id' => '',
'title' => '',
'desc' => '',
'post_type' => '',
'not_post_type' => '',
'post_type_feature' => '',
'post_type_options' => '',
'priority' => 'high',
'context' => 'normal',
'disable_func' => '',
'cap' => '',
'save_sanitize' => '',
'theme' => 'table',
'fields' => [
'foo' => [ 'title' => 'First meta-field' ],
'bar' => [ 'title' => 'Second meta-field' ],
],
];
/**
*
* @param array $opt {
* Опции по которым будет строиться метаблок.
*
* @type string $id Иднетификатор блока. Используется как префикс для названия метаполя.
* Начните с '_' >>> '_foo', чтобы ID не был префиксом в названии метаполей.
* @type string $title Заголовок блока.
* @type string|callback $desc Описание для метабокса (сразу под заголовком). Коллбэк получит $post.
* @type string|array $post_type Типы записей для которых добавляется блок:
* `[ 'post', 'page' ]`. По умолчанию: `''` = для всех типов записей.
* @type string|array $not_post_type Типы записей для которых метабокс не должен отображаться.
* @type string $post_type_feature Строка. Возможность которая должна быть у типа записи,
* чтобы метабокс отобразился. {@see https://wp-kama.ru/post_type_supports}.
* @type string $post_type_options Массив. Опции типа записи, которые должны быть у типа записи,
* чтобы метабокс отобразился. {@see https://wp-kama.ru/get_post_types}.
* @type string $priority Приоритет блока для показа выше или ниже остальных блоков ('high' или 'low').
* @type string $context Место где должен показываться блок ('normal', 'advanced' или 'side').
* @type callback $disable_func Функция отключения метабокса во время вызова самого метабокса.
* Если вернет что-либо кроме false/null/0/array(), то метабокс будет отключен.
* Передает объект поста.
* @type string $cap Название права пользователя, чтобы показывать метабокс.
* @type callback $save_sanitize Функция очистки сохраняемых в БД полей. Получает 2 параметра:
* $metas - все поля для очистки и $post_id.
* @type string $theme Тема оформления: `table`, `line`, `grid`.
* ИЛИ массив паттернов полей:
* css, fields_wrap, field_wrap, title_patt, field_patt, desc_before_patt.
* ЕСЛИ Массив указывается так: `[ 'desc_before_patt' => '<div>%s</div>' ]`
* (за овнову будет взята тема line).
* ЕСЛИ Массив указывается так:
* `[ 'table' => [ 'desc_before_patt' => '<div>%s</div>' ] ]`
* (за овнову будет взята тема table).
* ИЛИ изменить тему можно через фильтр 'kp_metabox_theme'
* (удобен для общего изменения темы для всех метабоксов).
* @type array $fields {
* Метаполя. Собственно, сами метаполя. Список возможных ключей массива для каждого поля.
*
* @type string $type Тип поля: textarea, select, checkbox, radio, image, wp_editor, hidden, sep_*.
* Или базовые: text, email, number, url, tel, color, password, date, month, week, range.
* 'sep' - визуальный разделитель, для него нужно указать `title` и можно
* указать `'attr'=>'style="свои стили"'`.
* 'sep' - чтобы удобнее указывать тип 'sep' начните ключ поля с
* `sep_`: 'sep_1' => [ 'title'=>'Разделитель' ].
* Для типа `image` можно указать тип сохраняемого значения в
* `options`: 'options'=>'url'. По умолчанию тип = id.
* По умолчанию 'text'.
* @type string $title Заголовок метаполя.
* @type string|callback $desc Описание для поля. Можно указать функцию/замыкание, она получит параметры:
* $post, $meta_key, $val, $name.
* @type string|callback $desc_before Алиас $desc.
* @type string|callback $desc_after Тоже что $desc, только будет выводиться внизу поля.
* @type string $placeholder Атрибут placeholder.
* @type string $id Атрибут id. По умолчанию: `{$this->opt->id}_{$key}`.
* @type string $class Атрибут class: добавляется в input, textarea, select.
* Для checkbox, radio в оборачивающий label.
* @type string $attr Любая строка. Атрибуты HTML тега элемента формы (input).
* @type string $wrap_attr Любая строка. Атрибуты HTML тега оборачивающего поле: `style="width:50%;"`.
* @type string $val Значение по умолчанию, если нет сохраненного.
* @type string $params Дополнительные параметры поля. У каждого свои (см. код метода поля).
* @type string $options массив: `array('значение'=>'название')` - варианты для типов `select`, `radio`.
* Для 'wp_editor' стенет аргументами.
* Для 'checkbox' станет значением атрибута value:
* `<input type="checkbox" value="{options}">`.
* Для 'image' определяет тип сохраняемого в метаполе значения:
* id (ID вложения), url (url вложения).
* @type callback $callback Название функции, которая отвечает за вывод поля.
* Если указана, то ни один параметр не учитывается и за вывод
* полностью отвечает указанная функция.
* Получит параметры: $args, $post, $name, $val, $rg, $var
* @type callback $sanitize_func Функция очистки данных при сохранении - название функции или Closure.
* Укажите 'none', чтобы не очищать данные...
* Работает, только если не установлен глобальный параметр 'save_sanitize'...
* Получит параметр $value - сохраняемое значение поля.
* @type callback $output_func Функция обработки значения, перед выводом в поле.
* Получит параметры: $post, $meta_key, $value - объект записи, ключ, значение метаполей.
* @type callback $update_func Функция сохранения значения в метаполя.
* Получит параметры: $post, $meta_key, $value - объект записи, ключ, значение метаполей.
* @type callback $disable_func Функция отключения поля.
* Если не false/null/0/array() - что-либо вернет, то поле не будет выведено.
* Получает парамтры: $post, $meta_key
* @type string $cap Название права пользователя, чтобы видеть и изменять поле.
* }
*
* }
*/
public function __construct( array $opt ){
// do nothing on front
if( ! is_admin() && ! defined('DOING_AJAX') ){
return;
}
$this->opt = (object) array_merge( self::METABOX_ARGS, $opt );
$this->set_fields_class();
// Init hooks hangs on the `init` action, because we need current user to be installed
add_action( 'init', [ $this, 'init_hooks' ], 20 );
}
private function set_fields_class(): void {
$fields_class = apply_filters( 'kama_post_meta_box__fields_class', '' );
if( $fields_class ){
$this->fields_class = new $fields_class();
}
else {
$this->fields_class = new Kama_Post_Meta_Box_Fields();
}
}
public function get_fields_class(): Kama_Post_Meta_Box_Fields {
return $this->fields_class;
}
public function init_hooks(): void {
// maybe the metabox is disabled by capability.
if( $this->opt->cap && ! current_user_can( $this->opt->cap ) ){
return;
}
// theme design.
add_action( 'current_screen', [ $this, '_set_theme' ], 20 );
// create a unique object ID.
$_opt = (array) clone $this->opt;
// delete all closures.
array_walk_recursive( $_opt, static function( &$val, $key ){
( $val instanceof Closure ) && $val = '';
});
$this->id = substr( md5( serialize( $_opt ) ), 0, 7 ); // ID экземпляра
// keep a reference to the instance so that it can be accessed.
self::$instances[ $this->opt->id ][ $this->id ] = & $this;
add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ], 10, 2 );
add_action( 'save_post', [ $this, 'meta_box_save' ], 1, 2 );
$this->set_value_sanitize_wp_hook();
}
public function add_meta_box( $post_type, $post ): void {
$opt = $this->opt;
if( $opt->post_type_options && is_string( $opt->post_type_options ) ){
$opt->post_type_options = [ $opt->post_type_options => 1 ];
}
/** @noinspection NotOptimalIfConditionsInspection */
if(
in_array( $post_type, [ 'comment', 'link' ], true )
|| ! current_user_can( get_post_type_object( $post_type )->cap->edit_post, $post->ID )
|| ( $opt->post_type_feature && ! post_type_supports( $post_type, $opt->post_type_feature ) )
|| ( $opt->post_type_options && ! in_array( $post_type, get_post_types( $opt->post_type_options, 'names', 'or' ), true ) )
|| ( $opt->disable_func && is_callable( $opt->disable_func ) && call_user_func( $opt->disable_func, $post ) )
|| in_array( $post_type, (array) $opt->not_post_type, true )
){
return;
}
$p_types = $opt->post_type ?: $post_type;
add_meta_box( $this->id, $opt->title, [ $this, 'meta_box_html' ], $p_types, $opt->context, $opt->priority );
// добавим css класс к метабоксу
// apply_filters( "postbox_classes_{$page}_{$id}", $classes );
add_filter( "postbox_classes_{$post_type}_{$this->id}", [ $this, 'add_metabox_css_classes' ] );
}
/**
* Displays the HTML code of the meta block.
*
* @param WP_Post $post Post object.
*/
public function meta_box_html( $post ): void {
$fields_out = '';
$hidden_out = '';
/** @var array $args For phpstan */
foreach( $this->opt->fields as $key => $args ){
// empty field
if( ! $key || ! $args ){
continue;
}
empty( $args['title_patt'] ) && ( $args['title_patt'] = $this->opt->title_patt ?? '%s' );
empty( $args['desc_before_patt'] ) && ( $args['desc_before_patt'] = $this->opt->desc_before_patt ?? '%s' );
empty( $args['field_patt'] ) && ( $args['field_patt'] = $this->opt->field_patt ?? '%s' );
$args['key'] = $key;
$field_type = $args['type'] ?? '';
$field_wrap = & $this->opt->field_wrap;
if( 'wp_editor' === $field_type ){
$field_wrap = str_replace( [ '<p ', '</p>' ], [ '<div ', '</div><br>' ], $field_wrap );
}
$Field = new Kama_Post_Meta_Box__Field_Core( $this );
$this->fields_class->set_current_field_core( $Field );
if( 'hidden' === $field_type ){
$hidden_out .= $Field->field_html( $args, $post );
}
else {
$fields_out .= sprintf( $field_wrap,
"{$key}_meta",
$Field->field_html( $args, $post ),
( $args['wrap_attr'] ?? '' )
);
}
}
$metabox_desc = '';
if( $this->opt->desc ){
$metabox_desc = is_callable( $this->opt->desc )
? call_user_func( $this->opt->desc, $post )
: '<p class="description">' . $this->opt->desc . '</p>';
}
$style = $this->opt->css ? "<style>{$this->opt->css}</style>" : '';
echo $style;
echo $metabox_desc;
echo $hidden_out;
echo sprintf( ( $this->opt->fields_wrap ?: '%s' ), $fields_out );
echo '<div class="clearfix"></div>';
}
/**
* Saving data, when saving a post.
*
* @param int $post_id Record ID.
* @param \WP_Post $post
*
* @return void False If the check is not passed.
*/
public function meta_box_save( $post_id, $post ): void {
if(
// no data
! ( $save_metadata = isset( $_POST[ $key = "{$this->id}_meta" ] ) ? $_POST[ $key ] : '' )
// Exit, if it is autosave.
|| ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
// nonce check
|| ! wp_verify_nonce( $_POST['_wpnonce'], "update-post_$post_id" )
// unsuitable post type
|| ( $this->opt->post_type && ! in_array( $post->post_type, (array) $this->opt->post_type, true ) )
){
return;
}
// leave only the fields of the current class (protection against field swapping)
$fields_data = [];
foreach( $this->opt->fields as $_key => $rg ){
$meta_key = $this->key_prefix() . $_key;
// not enough rights
if( ! empty( $rg['cap'] ) && ! current_user_can( $rg['cap'] ) ){
continue;
}
// Skip the disabled fields
if(
! empty( $rg['disable_func'] )
&& is_callable( $rg['disable_func'] )
&& call_user_func( $rg['disable_func'], $post, $meta_key )
){
continue;
}
$fields_data[ $meta_key ] = $rg;
}
$fields_names = array_keys( $fields_data );
$save_metadata = array_intersect_key( $save_metadata, array_flip( $fields_names ) );
// Sanitizing
$save_metadata = $this->maybe_run_custom_sanitize( $save_metadata, $post_id, $fields_data );
// Save
foreach( $save_metadata as $meta_key => $value ){
// If there is a save function
if(
! empty( $fields_data[ $meta_key ]['update_func'] )
&&
is_callable( $fields_data[ $meta_key ]['update_func'] )
){
call_user_func( $fields_data[ $meta_key ]['update_func'], $post, $meta_key, $value );
}
elseif( ! $value && ( $value !== '0' ) ){
delete_post_meta( $post_id, $meta_key );
}
// add_post_meta() works automatically
else{
update_post_meta( $post_id, $meta_key, $value );
}
}
}
public function add_metabox_css_classes( $classes ){
$classes[] = "kama_meta_box_{$this->opt->id}";
return $classes;
}
public function key_prefix(): string {
return ( '_' === $this->opt->id[0] ) ? '' : "{$this->opt->id}_";
}
}
/**
* Prepare single field for render.
*/
class Kama_Post_Meta_Box__Field_Core {
protected const FIELD_ARGS = [
'type' => '',
'title' => '',
'desc' => '',
'desc_before' => '',
'desc_after' => '',
'placeholder' => '',
'id' => '',
'class' => '',
'attr' => '',
'val' => '',
'options' => '',
'params' => [], // additional field options
'callback' => '',
'sanitize_func' => '',
'output_func' => '',
'update_func' => '',
'disable_func' => '',
'cap' => '',
// служебные
'key' => '', // Mandatory! Automatic
'title_patt' => '', // Mandatory! Automatic
'field_patt' => '', // Mandatory! Automatic
];
protected $rg;
protected $post;
protected $var;
/** @var Kama_Post_Meta_Box */
protected $kpmb;
public function __construct( Kama_Post_Meta_Box $kpmb ){
$this->kpmb = $kpmb;
}
/**
* Outputs individual meta field.
*
* @param array $args Field parameters.
* @param WP_Post $post The object of the current post.
*
* @return string HTML code.
*/
public function field_html( array $args, $post ): string {
$this->post = $post;
$this->rg = $this->parse_args( $args );
// no acces to the field
if( ! $this->rg ){
return '';
}
$this->var = $this->create_field_vars();
$this->standartize_rg_desc(); // !!! after var set
return $this->field_output( $args );
}
public function tpl__field( string $field ): string {
return sprintf( $this->rg->field_patt, $field );
}
public function field_desc_concat( string $field ): string{
$rg = $this->rg;
$opt = $this->kpmb->opt;
// description before field
if( $rg->desc_before ){
$desc = sprintf( $opt->desc_before_patt, $rg->desc_before );
return $desc . $field;
}
// descroption after field
if( $rg->desc_after ){
$desc = sprintf( $opt->desc_after_patt, $rg->desc_after );
return $field . $desc;
}
return $field;
}
/**
* Parse fields arguments.
*
* @return object|null Null if user can access to see meta-field
*/
private function parse_args( $args ): ?object {
$rg = (object) array_merge( self::FIELD_ARGS, $args );
if( $rg->cap && ! current_user_can( $rg->cap ) ){
return null;
}
$rg->meta_key = $this->kpmb->key_prefix() . $rg->key;
// the field is disabled
if(
$rg->disable_func
&& is_callable( $rg->disable_func )
&& call_user_func( $rg->disable_func, $this->post, $rg->meta_key )
){
return null;
}
// fix some fields $rg
$rg->id = $rg->id ?: "{$this->kpmb->opt->id}_{$rg->key}";
$rg->options = (array) $rg->options;
if( 0 === strpos( $rg->key, 'sep_' ) ){
$rg->type = 'sep';
}
if( ! $rg->type ){
$rg->type = 'text';
}
return $rg;
}
private function create_field_vars(): object {
$post = $this->post;
$rg = $this->rg;
// internal variables of this function, will be transferred to the methods
$var = new \stdClass();
$var->meta_key = $rg->meta_key;
$var->val = get_post_meta( $post->ID, $var->meta_key, true ) ?: $rg->val;
if( $rg->output_func && is_callable( $rg->output_func ) ){
$var->val = call_user_func( $rg->output_func, $post, $var->meta_key, $var->val );
}
$var->name = "{$this->kpmb->id}_meta[$var->meta_key]";
// with a table theme, the td header should always be output!
if( false !== strpos( $rg->title_patt, '<td ' ) ){
$var->title = sprintf( $rg->title_patt, $rg->title ) . ( $rg->title ? ' ' : '' );
}
else{
$var->title = $rg->title ? sprintf( $rg->title_patt, $rg->title ) . ' ' : '';
}
$var->pholder = $rg->placeholder ? ' placeholder="'. esc_attr( $rg->placeholder ) .'"' : '';
$var->class = $rg->class ? ' class="'. esc_attr( $rg->class ) .'"' : '';
return $var;
}
private function field_output( $args ): string {
$rg = & $this->rg;
$post = & $this->post;
$var = & $this->var;
// custom function
if( is_callable( $rg->callback ) ){
$out = $var->title;
$out .= $this->tpl__field(
call_user_func( $rg->callback, $args, $post, $var->name, $var->val, $rg, $var )
);
}
// custom method
// Call the method `$this->field__{FIELD}()` (to be able to extend this class)
elseif( method_exists( $this->kpmb->get_fields_class(), $rg->type ) ){
$out = $this->kpmb->get_fields_class()->{ $rg->type }( $rg, $var, $post, $args );
}
// text, email, number, url, tel, color, password, date, month, week, range
else{
$out = $this->kpmb->get_fields_class()->default( $rg, $var, $post );
}
return $out;
}
private function standartize_rg_desc(): void {
$rg = & $this->rg;
if( $rg->desc ){
$rg->desc_before = $rg->desc;
}
if( ! $rg->desc && ! $rg->desc_before && $rg->desc_after ){
$rg->desc = $rg->desc_after;
}
foreach( [ & $rg->desc, & $rg->desc_before, & $rg->desc_after ] as & $desc ){
if( is_callable( $desc ) ){
$desc = $desc( $this->post, $this->var->meta_key, $this->var->val, $this->var->name );
}
}
}
}
/**
* Separate class which contains fields.
*
* You can add your own fields by extend this class like so:
*
* add_action( 'kama_post_meta_box__fields_class', function(){
* return 'MY_Post_Meta_Box_Fields';
* } );
*
* class MY_Post_Meta_Box_Fields extends Kama_Post_Meta_Box_Fields {
*
* // create custom field `my_field`
* public function my_field( $rg, $var, $post ){
*
* $field = sprintf( '<input %s type="%s" id="%s" name="%s" value="%s" title="%s">',
* ( $rg->attr . $var->class . $var->pholder ),
* $rg->type,
* $rg->id,
* $var->name,
* esc_attr( $var->val ),
* esc_attr( $rg->title )
* );
*
* return $var->title . $this->tpl__field( $this->field_desc_concat( $field ) );
* }
*
* // override default text field
* public function text( $rg, $var, $post ){
*
* $field = sprintf( '<input %s type="%s" id="%s" name="%s" value="%s" title="%s">',
* ( $rg->attr . $var->class . $var->pholder ),
* $rg->type,
* $rg->id,
* $var->name,
* esc_attr( $var->val ),
* esc_attr( $rg->title )
* );
*
* return $var->title . $this->tpl__field(
* $this->field_desc_concat( $field )
* );
* }
*
* }
*/
class Kama_Post_Meta_Box_Fields {
/**
* Changable property. Contains class instance of single field (data) that processing now.
*
* @var Kama_Post_Meta_Box__Field_Core
*/
protected $the_field;
public function __construct(){
}
public function set_current_field_core( Kama_Post_Meta_Box__Field_Core $class ): void {
$this->the_field = $class;
}
protected function tpl__field( string $field ): string {
return $this->the_field->tpl__field( $field );
}
protected function field_desc_concat( string $field ): string {
return $this->the_field->field_desc_concat( $field );
}
/**
* Sep field.
*
* Example:
*
* 'sep_1' => [
* 'title' => 'SEO headers',
* 'desc' => fn( $post ) => 'Placeholders: ' . placeholders(),
* ],
*
* @param object $rg
* @param object $var
* @param WP_Post $post
*
* @return array|string|string[]
*/
public function sep( object $rg, object $var, $post ){
$class = [ 'kpmb__sep' ];
! $rg->title && $class[] = '--hr';
$class = implode( ' ', $class );
// table theme
if( false !== strpos( $rg->field_patt, '<td' ) ){
$field = $rg->title;
if( $rg->desc ){
$field .= sprintf( '<div class="kpmb__sep-desc">%s</div>', $rg->desc );
}
return str_replace(
'<td ',
sprintf( '<td class="%s" colspan="2" %s', $class, $rg->attr ),
$this->tpl__field( $field )
);
}
// other theme
$sep = sprintf( '<span class="%s" %s>%s</span>', $class, $rg->attr, $rg->title );
if( $rg->desc ){
$sep .= sprintf( '<span class="kpmb__sep-desc">%s</span>', $rg->desc );
}
return $sep;
}
// textarea
public function textarea( object $rg, object $var, WP_Post $post ): string {
$_style = ( false === strpos( $rg->attr, 'style=' ) ) ? ' style="width:98%;"' : '';
$field = sprintf( '<textarea %s id="%s" name="%s">%s</textarea>',
( $rg->attr . $var->class . $var->pholder . $_style ),
$rg->id,
$var->name,
esc_textarea( $var->val )
);
$field = $this->field_desc_concat( $field );
return $var->title . $this->tpl__field( $field );
}
// select
public function select( object $rg, object $var, WP_Post $post ): string {
$is_assoc = ( array_keys($rg->options) !== range(0, count($rg->options) - 1) ); // associative or not?
$_options = array();
foreach( $rg->options as $v => $l ){
$_val = $is_assoc ? $v : $l;
$_options[] = '<option value="'. esc_attr($_val) .'" '. selected($var->val, $_val, false) .'>'. $l .'</option>';
}
$field = sprintf( '<select %s id="%s" name="%s">%s</select>',
( $rg->attr . $var->class ),
$rg->id,
$var->name,
implode("\n", $_options )
);
$field = $this->field_desc_concat( $field );
return $var->title . $this->tpl__field( $field );
}
/**
* radio.
*
* Examples:
*
* 'meta_name' => [
* 'type' => 'radio',
* 'title' => 'Check me',
* 'desc' => 'mark it',
* 'options' => [ 'on' => 'Enabled', 'off' => 'Disabled' ],
* ]
*
* @param object $rg
* @param object $var
* @param WP_Post $post
*
* @return string
*/
public function radio( object $rg, object $var, WP_Post $post ): string {
$radios = [];
$patt = '
<label {attrs}>
<input type="radio" id="{id}" name="{name}" value="{value}" {checked}>
{label}
</label>
';
foreach( $rg->options as $value => $label ){
$radios[] = strtr( $patt, [
'{attrs}' => $rg->attr . $var->class,
'{name}' => $var->name,
'{id}' => $rg->id,
'{value}' => esc_attr( $value ),
'{checked}' => checked( $var->val, $value, false ),
'{label}' => $label,
] );
}
$field = '<span class="radios">'. implode( "\n", $radios ) .'</span>';
$field = $this->field_desc_concat( $field );
return $var->title . $this->tpl__field( $field );
}
/**
* Checkbox.
*
* Examples:
*
* ```
* 'meta_name' => [ 'type'=>'checkbox', 'title'=>'Check me', 'desc'=>'mark it if you want to :)' ]
* 'meta_name' => [ 'type'=>'checkbox', 'title'=>'Check me', 'options' => [ 'default' => '0' ] ]
* ```
*/
public function checkbox( object $rg, object $var, \WP_Post $post ): string {
$patt = '
<label {attrs}>
<input type="hidden" name="{name}" value="{default}">
<input type="checkbox" id="{id}" name="{name}" value="{value}" {checked}>
{desc}
</label>
';
$value = reset( $rg->options ) ?: 1;
$field = strtr( $patt, [
'{attrs}' => $rg->attr . $var->class,
'{name}' => $var->name,
'{default}' => $rg->params['default'] ?? '',
'{id}' => $rg->id,
'{value}' => esc_attr( $value ),
'{checked}' => checked( $var->val, $value, false ),
'{desc}' => $rg->desc_before ?: '',
] );
return $var->title . $this->tpl__field( $field );
}
/**
* checkbox multi
*
* Examples:
*
* [
* type => checkbox_multi,
* params => show_inline,
* options => [
* [ name => bar, val => label, desc => The checkbox ]
* [ val => label, desc => The checkbox ]
* ]
* ]
*
* @param object $rg
* @param object $var
* @param WP_Post $post
*
* @return string
*/
public function checkbox_multi( object $rg, object $var, WP_Post $post ): string {
$checkboxes = [];
$add_hidden = false;
foreach( $rg->options as $opt ){
// val
// desc
// name
$opt = (object) $opt;
if( ! isset( $opt->desc ) ){
$opt->desc = $opt->val;
}
$input_name = isset( $opt->name ) ? "{$var->name}[$opt->name]" : "{$var->name}[]";
$add_hidden = isset( $opt->name );
$input_value = $opt->val ?? 1;
// checked
$checked = '';
if( $var->val ){
if( isset( $opt->name ) ){
$checked = ! empty( $var->val[ $opt->name ] ) ? 'checked="checked"' : '';
}
else{
$var->val = array_map( fn( $val ) => str_replace( ' ', ' ', $val ), $var->val );
$checked = in_array( $opt->val, $var->val, true ) ? 'checked="checked"' : '';
}
}
$checkboxes[] = '
<label>
'.( $add_hidden ? '<input type="hidden" name="'. $input_name .'" value="">' : '' ).'
<input type="checkbox" name="'. $input_name .'" value="'. $input_value .'" '. $checked .'> '. $opt->desc .'
</label>
';
}
$sep = in_array( 'show_inline', $rg->params, true ) ? ' ' : ' <br> ';
// for the main array
$common_hidden = $add_hidden ? '' : '<input type="hidden" name="'. $var->name .'" value="">';
$field = '
<fieldset>
<div class="fieldset">'. $common_hidden . implode( "$sep\n", $checkboxes ) .'</div>
</fieldset>';
return $var->title . $this->tpl__field( $field );
}
// hidden
public function hidden( object $rg, object $var, WP_Post $post ): string {
return sprintf( '<input type="%s" id="%s" name="%s" value="%s" title="%s">',
$rg->type,
$rg->id,
$var->name,
esc_attr( $var->val ),
esc_attr( $rg->title )
);
}
// wp_editor
public function wp_editor( object $rg, object $var, WP_Post $post ): string {
$ed_args = array_merge( [
'textarea_name' => $var->name, // must be specified!
'editor_class' => $rg->class,
// changeable
'wpautop' => 1,
'textarea_rows' => 5,
'tabindex' => null,
'editor_css' => '',
'teeny' => 0,
'dfw' => 0,
'tinymce' => 1,
'quicktags' => 1,
'media_buttons' => false,
'drag_drop_upload' => false,
], $rg->options );
ob_start();
wp_editor( $var->val, $rg->id, $ed_args );
$field = ob_get_clean();
$field = $this->field_desc_concat( $field );
return $var->title . $this->tpl__field( $field );
}
// image
public function image( object $rg, object $var, WP_Post $post ): string {
wp_enqueue_media();
static $once;
if( ! $once && $once = 1 ){
add_action( 'admin_print_footer_scripts', function(){
?>
<script>
jQuery('.kmb_img_wrap').each(function(){
let $ = jQuery
let frame
let $wrap = $(this)
let $img = $wrap.find('img')
let $input = $wrap.find('input[type="hidden"]')
$wrap.on( 'click', '.set_img', function(){
let post_id = $(this).data('post_id') || null
//if( frame && frame.post_id === post_id ){
// frame.open();
// return;
//}
frame = wp.media.frames.kmbframe = wp.media({
title : '<?= __( 'Add Media' ) ?>',
// Library WordPress query arguments.
library : {
type : 'image',
uploadedTo : post_id
},
multiple: false,
button: {
text: '<?= __( 'Apply' ) ?>'
}
});
frame.on( 'select', function() {
attachment = frame.state().get('selection').first().toJSON();
$img.attr( 'src', attachment.url );
$wrap.data('usetype') === 'url' ? $input.val( attachment.url ) : $input.val( attachment.id );
});
frame.on( 'open', function(){
if( $input.val() )
frame.state().get('selection').add( wp.media.attachment( $input.val() ) );
});
frame.open();
//frame.post_id = post_id // save
});
$wrap.on( 'click', '.del_img', function(){
$img.attr( 'src', '' );
$input.val('');
});
})
</script>
<?php
}, 99 );
}
$usetype = $rg->options ? $rg->options[0] : 'id'; // может быть: id, url
if( ! $src = is_numeric( $var->val ) ? wp_get_attachment_url( $var->val ) : $var->val ){
$src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
}
ob_start();
?>
<span class="kmb_img_wrap" data-usetype="<?= esc_attr($usetype) ?>" style="display:flex; align-items:center;">
<img src="<?= esc_url($src) ?>" style="max-height:100px; max-width:100px; margin-right:1em;" alt="">
<span>
<input class="set_img button button-small" type="button" data-post_id="<?= $post->ID ?>" value="<?= __( 'Images' ) .' '. __( 'Post' ) ?>" />
<input class="set_img button button-small" type="button" value="<?= __('Set image') ?>" />
<input class="del_img button button-small" type="button" value="<?= __('Remove')?>" />
<input type="hidden" name="<?= $var->name ?>" value="<?= esc_attr($var->val) ?>">
</span>
</span>
<?php
$field = ob_get_clean();
return $var->title . $this->tpl__field( $field );
}
// text, email, number, url, tel, color, password, date, month, week, range
public function default( object $rg, object $var, WP_Post $post ): string {
$_style = ( in_array( $rg->type, [ 'text', 'url' ], true ) && false === strpos( $rg->attr, 'style=' ) )
? ' style="width:100%;"'
: '';
$field = sprintf( '<input %s type="%s" id="%s" name="%s" value="%s" title="%s">',
( $rg->attr . $var->class . $var->pholder . $_style ),
$rg->type,
$rg->id,
$var->name,
esc_attr( $var->val ),
esc_attr( $rg->title )
);
$field = $this->field_desc_concat( $field );
return $var->title . $this->tpl__field( $field );
}
}
trait Kama_Post_Meta_Box__Themes {
private function themes_settings(): array {
return [
'line' => [
// CSS styles of the whole block. For example: '.postbox .tit{ font-weight:bold; }'
'css' => '
.kpmb{ display: flex; flex-wrap: wrap; justify-content: space-between; }
.kpmb > * { width:100%; }
.kpmb__field{ box-sizing:border-box; margin-bottom:1em; }
.kpmb__tit{ display: block; margin:1em 0 .5em; font-size:115%; }
.kpmb__desc{ opacity:0.6; }
.kpmb__desc.--after{ margin-top:.5em; }
.kpmb__sep{ display: block; padding: 2em 1em 0.2em 0; font-size: 130%; font-weight: 600; }
.kpmb__sep.--hr{ padding: 0; height: 1px; background: #eee; margin: 1em -12px 0 -12px; }
',
// '%s' will be replaced by the html of all fields
'fields_wrap' => '<div class="kpmb">%s</div>',
// '%2$s' will be replaced by field HTML (along with title, field and description)
'field_wrap' => '<div class="kpmb__field %1$s" %3$s>%2$s</div>',
// '%s' will be replaced by the header
'title_patt' => '<strong class="kpmb__tit"><label>%s</label></strong>',
// '%s' will be replaced by field HTML (along with description)
'field_patt' => '%s',
// '%s' will be replaced by the description text
'desc_before_patt' => '<p class="description kpmb__desc --before">%s</p>',
'desc_after_patt' => '<p class="description kpmb__desc --after">%s</p>',
],
'table' => [
'css' => '
.kpmb-table td{ padding: .6em .5em; }
.kpmb-table tr:hover{ background: rgba(0,0,0,.03); }
.kpmb__sep{ padding: 1em .5em; font-weight: 600; }
.kpmb__sep-desc{ padding-top: .3em; font-weight: normal; opacity: .6; }
.kpmb__desc{ opacity: 0.8; }
',
'fields_wrap' => '<table class="form-table kpmb-table">%s</table>',
'field_wrap' => '<tr class="%1$s">%2$s</tr>',
'title_patt' => '<td style="width:10em;" class="tit">%s</td>',
'field_patt' => '<td class="field">%s</td>',
'desc_before_patt' => '<p class="description kpmb__desc --before">%s</p>',
'desc_after_patt' => '<p class="description kpmb__desc --after">%s</p>',
],
'grid' => [
'css' => '
.kpmb-grid{ margin: '. ( get_current_screen()->is_block_editor ? '-6px -24px -24px' : '-6px -12px -12px' ) .' }
.kpmb-grid__item{ display:grid; grid-template-columns:15em 2fr; grid-template-rows:1fr; border-bottom:1px solid rgba(0,0,0,.1) }
.kpmb-grid__item:last-child{ border-bottom:none }
.kpmb-grid__title{ padding:1.5em; background:#F9F9F9; border-right:1px solid rgba(0,0,0,.1); font-weight:600 }
.kpmb-grid__field{ align-self:center; padding:1em 1.5em }
.kpmb__sep{ grid-column: 1 / span 2; display:block; padding:1em; font-size:110%; font-weight:600; }
.kpmb__sep-desc{ grid-column: 1 / span 2; display: block; padding: 0 1em 1em 1em; opacity: .7; }
.kpmb__desc{ opacity:0.8; }
',
'fields_wrap' => '<div class="kpmb-grid">%s</div>',
'field_wrap' => '<div class="kpmb-grid__item %1$s">%2$s</div>',
'title_patt' => '<div class="kpmb-grid__title">%s</div>',
'field_patt' => '<div class="kpmb-grid__field">%s</div>',
'desc_before_patt' => '<p class="description kpmb__desc --before">%s</p>',
'desc_after_patt' => '<br><p class="description kpmb__desc --after">%s</p>',
],
];
}
public function _set_theme(): void {
$themes_settings = $this->themes_settings();
$opt_theme = & $this->opt->theme;
if( is_string( $opt_theme ) ){
$themes_settings = $themes_settings[ $opt_theme ];
}
// allows you to change individual option (field) of the theme option.
else {
$opt_theme_key = key( $opt_theme );
// theme is in the index: [ 'table' => [ 'desc_before_patt' => '<div>%s</div>' ] ]
if( isset( $themes_settings[ $opt_theme_key ] ) ){
$themes_settings = $themes_settings[ $opt_theme_key ]; // base
$opt_theme = $opt_theme[ $opt_theme_key ];
}
// not theme in the index: [ 'desc_before_patt' => '<div>%s</div>' ]
else {
$themes_settings = $themes_settings['line']; // base
}
}
$opt_theme = is_array( $opt_theme ) ? array_merge( $themes_settings, $opt_theme ) : $themes_settings;
// allows you to change the theme
$opt_theme = apply_filters( 'kp_metabox_theme', $opt_theme, $this->opt );
// Theme variables to global parameters.
// If there is already a variable in the parameters, it stays as is
// (this allows to change an individual theme element).
foreach( $opt_theme as $kk => $vv ){
if( ! isset( $this->opt->$kk ) ){
$this->opt->$kk = $vv;
}
}
}
}
trait Kama_Post_Meta_Box__Sanitizer {
/**
* Checks and run custom sanitize callback.
*/
protected function maybe_run_custom_sanitize( array $save_metadata, $post_id, $fields_data ){
// Own sanitizing.
if( is_callable( $this->opt->save_sanitize ) ){
return call_user_func( $this->opt->save_sanitize, $save_metadata, $post_id, $fields_data );
}
// Sanitizing hook.
if( has_filter( "kpmb_save_sanitize_{$this->opt->id}" ) ){
return apply_filters( "kpmb_save_sanitize_{$this->opt->id}", $save_metadata, $post_id, $fields_data );
}
/**
* INFO: Other sanitization is hanged on wp_hook.
* {@see set_value_sanitize_wp_hook()}
*/
return $save_metadata;
}
/**
* Sets wp hooks to sinitize values based on specified function or default function.
*/
private function set_value_sanitize_wp_hook(): void {
// Own sanitizing - this sanitization do only on edit post page. TODO: move it here.
if( is_callable( $this->opt->save_sanitize ) || has_filter( "kpmb_save_sanitize_{$this->opt->id}" ) ){
return;
}
foreach( $this->opt->fields as $field_key => $field ){
// empty field
if( ! $field_key || ! $field ){
continue;
}
$field_sanitize_func = $field['sanitize_func'] ?? null;
// do not clean
if( 'none' === $field_sanitize_func || 'no' === $field_sanitize_func ){
continue;
}
$meta_key = $this->key_prefix() . $field_key;
$type = $field['type'] ?? 'text';
// there is a function for cleaning a separate field
if( is_callable( $field_sanitize_func ) ){
add_filter( "sanitize_post_meta_{$meta_key}", $field_sanitize_func, 10, 1 );
}
elseif( 'number' === $type ){
add_filter( "sanitize_post_meta_{$meta_key}", [ __CLASS__, '_sanitize_val__number' ], 10, 1 );
}
elseif( 'url' === $type ){
add_filter( "sanitize_post_meta_{$meta_key}", 'sanitize_url', 10, 1 );
}
elseif( 'email' === $type ){
add_filter( "sanitize_post_meta_{$meta_key}", 'sanitize_email', 10, 1 );
}
elseif( in_array( $type, [ 'wp_editor', 'textarea' ], true ) ){
add_filter( "sanitize_post_meta_{$meta_key}", [ __CLASS__, '_sanitize_val__textarea' ], 10, 1 );
}
else {
add_filter( "sanitize_post_meta_{$meta_key}", [ __CLASS__, '_sanitize_val__default' ], 10, 1 );
}
}
}
public static function _sanitize_val__number( $value ){
return is_float( $value + 0 ) ? (float) $value : (int) $value;
}
public static function _sanitize_val__textarea( $value ){
return wp_kses_post( $value );
}
public static function _sanitize_val__default( $value ){
// do not clean - apparently it is an arbitrary field output function that saves an array
if( is_array( $value ) ){
return $value;
}
$value = sanitize_text_field( $value );
return $value;
}
}
Передаваемые параметры
Прежде чем переходить к примерам, рассмотрим все параметры, которые понимает класс:
$opt(массив)
Опции по которым будет строиться метаблок.
id(строка)
Иднетификатор блока. Используется как префикс для названия метаполя. Начните с '_' >>> '_foo', чтобы ID не был префиксом в названии метаполей.
title(строка)
Заголовок блока.
desc(строка|callback)
Описание для метабокса (сразу под заголовком). Коллбэк получит $post.
post_type(строка|массив)
Типы записей для которых добавляется блок: [ 'post', 'page' ]. По умолчанию: '' = для всех типов записей.
not_post_type(строка|массив)
Типы записей для которых метабокс не должен отображаться.
post_type_feature(строка)
Строка. Возможность которая должна быть у типа записи, чтобы метабокс отобразился. See https://wp-kama.ru/post_type_supports
post_type_options(строка)
Массив. Опции типа записи, которые должны быть у типа записи, чтобы метабокс отобразился. See перывый параметр https://wp-kama.ru/get_post_types
priority(строка)
Приоритет блока для показа выше или ниже остальных блоков ('high' или 'low').
context(строка)
Место где должен показываться блок ('normal', 'advanced' или 'side').
disable_func(callback)
Функция отключения метабокса во время вызова самого метабокса. Если вернет что-либо кроме false/null/0/array(), то метабокс будет отключен. Передает объект поста.
cap(строка)
Название права пользователя, чтобы показывать метабокс.
save_sanitize(callback)
Функция очистки сохраняемых в БД полей. Получает 2 параметра: $metas - все поля для очистки и $post_id.
theme(строка)
Тема оформления: table, line, grid. ИЛИ массив паттернов полей: css, fields_wrap, field_wrap, title_patt, field_patt, desc_before_patt. ЕСЛИ Массив указывается так: [ 'desc_before_patt' => '<div>%s</div>' ] (за овнову будет взята тема line). ЕСЛИ Массив указывается так: [ 'table' => [ 'desc_before_patt' => '<div>%s</div>' ] ] (за овнову будет взята тема table). ИЛИ изменить тему можно через фильтр 'kp_metabox_theme' (удобен для общего изменения темы для всех метабоксов).
fields(массив)
Метаполя. Собственно, сами метаполя. Список возможных ключей массива для каждого поля.
type(строка)
Тип поля: textarea, select, checkbox, radio, image, wp_editor, hidden, sep_*. Или базовые: text, email, number, url, tel, color, password, date, month, week, range. 'sep' - визуальный разделитель, для него нужно указать title и можно указать 'attr'=>'style="свои стили"'. 'sep' - чтобы удобнее указывать тип 'sep' начните ключ поля с sep_: 'sep_1' => [ 'title'=>'Разделитель' ]. Для типа image можно указать тип сохраняемого значения в options: 'options'=>'url'. По умолчанию тип = id. По умолчанию 'text'.
title(строка)
Заголовок метаполя.
desc(строка|callback)
Описание для поля. Можно указать функцию/замыкание, она получит параметры: $post, $meta_key, $val, $name.
desc_before(строка|callback)
Алиас $desc.
desc_after(строка|callback)
Тоже что $desc, только будет выводиться внизу поля.
placeholder(строка)
Атрибут placeholder.
id(строка)
Атрибут id. По умолчанию: $this->opt->id .'_'. $key.
class(строка)
Атрибут class: добавляется в input, textarea, select. Для checkbox, radio в оборачивающий label.
attr(строка)
Любая строка. Атрибуты HTML тега элемента формы (input).
wrap_attr(строка)
Любая строка. Атрибуты HTML тега оборачивающего поле: style="width:50%;".
val(строка)
Значение по умолчанию, если нет сохраненного.
options(строка)
Массив вида: [ 'значение'=>'название' ] - дополнительные параметры поля. Нужно для особенных полей:
Варианты выбора для типов: 'select', 'radio'.
Для 'wp_editor' стенет аргументами.
Для 'checkbox' станет значением атрибута value: <input type="checkbox" value="{options}">.
Для 'image' определяет тип сохраняемого в метаполе значения: id (ID вложения), url (url вложения).
callback(callback)
Название функции, которая отвечает за вывод поля.
Если указана, то ни один параметр не учитывается и за вывод полностью отвечает указанная функция.
Получит параметры: $args, $post, $name, $val, $rg, $var
sanitize_func(callback)
Функция очистки данных при сохранении. Укажите функции или Closure. Укажите 'none', чтобы не очищать данные.
Работает, только если не установлен параметр 'save_sanitize' для всего метабокса.
Получит параметр $value - сохраняемое значение поля. Должен вернуть $value обратно.
output_func(callback)
Функция обработки значения, перед выводом.
Получит параметры: $post, $meta_key, $value - объект записи, ключ, значение метаполей.
update_func(callback)
Функция сохранения значения в метаполя.
Получит параметры: $post, $meta_key, $value - объект записи, ключ, значение метаполей.
Если указано, то класс сам ничего делать не будет, вам самому нужно обработать сохранине полностью.
disable_func(callback)
Функция отключения поля.
Если не false/null/0/array() - что-либо вернет, то поле не будет выведено. Получает парамтры: $post, $meta_key
cap(строка)
Название права пользователя, чтобы видеть и изменять поле.
Заметки
Названия произвольных полей
Создаваемые произвольные поля будут иметь название/ключ, состоящий из объединения основного ID и указанного ключа метаполя: {id}_{meta_key} (смотрите пример ниже).
Самый просто способ узнать название произвольного поля — это посмотреть в исходный код. Для этого фокусируемся на нужном поле, кликаем правой кнопкой и смотрим значение атрибута name в исходном коде элемента:
Получение произвольных полей
Получать созданные поля для использования в темах и плагинах нужно стандартной функцией WordPress: get_post_meta():
// получим значение поля 'my_meta_key' у записи 25
$my_filed = get_post_meta( 25, 'my_meta_key', 1 );
echo $my_filed;
Права
Блок/метабокс будет показан только пользователям, у которых есть право редактировать текущую запись.
Сохранение метаполей будет работать только для пользователей, которые могут редактировать текущую запись.
Примеры создания различных произвольных полей
#1 Демонстрация создания всех видов метаполей
Этот пример показывает как создавать все поддерживаемые типы метаполей: 'text', 'textarea', 'select', 'checkbox', 'radio', 'wp_editor', 'hidden' и другие: 'email', 'number', 'phone', 'password' и т.д. (обрабатываются как поле 'text').
class_exists( 'Kama_Post_Meta_Box' ) && new Kama_Post_Meta_Box(
[
'id' => 'my',
'title' => 'Мои произвольные поля',
'fields' => [
'text_field' => [
'title' => 'Текстовое поле'
],
'number_field' => [
'type' => 'number',
'title' => 'Числовое поле',
'desc' => 'Число от 0 до 5',
'attr' => 'min="0" max="5"',
],
'textarea_field' => [
'type' => 'textarea',
'title' => 'Большое текстовое поле',
'desc' => 'Описание чего-либо. Можно использовать html теги.',
],
'select_field' => [
'type' => 'select',
'title' => 'Выберите значение',
'options' => [ '' => 'Ничего не выбрано', 'val_1' => 'Выбор 1', 'val_2' => 'Выбор 2' ],
],
'select_field2' => [
'type' => 'select',
'title' => 'Выберите значение 2',
'options' => [ 'Выбор 1', 'Выбор 2' ],
'desc' => 'Выбор, где не указывается значение value для тегов option',
],
'checkbox_field' => [
'type' => 'checkbox',
'title' => 'Галочка',
'desc' => 'отметьте, если хотите :)',
],
'checkbox_field2' => [
'type' => 'checkbox',
'desc' => '< только описание для галочки, без заголовка',
],
'radio_field' => [
'type' => 'radio',
'title' => 'Переключатель',
'desc' => 'Выберите одно из значений',
'options' => [ '' => 'Ничего не выбрано', 'good' => 'хорошо', 'bad' => 'плохо' ],
],
'radio_field2' => [
'type' => 'radio',
'desc' => 'Переключатель без заголовка',
'options' => [ '' => 'Не выбрано', 'good' => 'хорошо', 'bad' => 'плохо' ],
],
'wp_editor_field' => [
'type' => 'wp_editor',
'title' => 'Текстовое поле с редактором TinyMce',
],
'wp_editor_field2' => [
'type' => 'wp_editor',
'title' => 'Текстовое поле с редактором WordPress, без TinyMce',
'options' => [ 'tinymce' => 0 ], // список настроек: http://wp-kama.ru/function/wp_editor
],
'hidden_field' => [
'type' => 'hidden',
'val' => 'foo',
],
// заготовка
'' => [
'type' => 'text',
'title' => '',
],
],
]
);
В результате, появиться такой метаблок на странице редактирования любого типа записи.
А при сохранении будут созданы, такие произвольные поля:
#2 Блоки для указанных типов записей
Когда нужно создать метабокс с метаполями только для указанных типов записи, укажите параметр 'post_type' => array('post','page') в котором перечислите нужные типы записей.
Когда возможностей класса не достаточно и нужно создать поле, которое будет иметь какой-то особенный вывод, используйте параметр callback для создаваемого поля.
В этом случае можно настроить вывод поля как угодно. Для примера давайте создадим несколько полей, которые будут храниться в поле special_field в виде массива.
Все данные будут храниться в поле my_special_field в виде массива array( 'box1'=>'яблоки', 'box1'=>'апельсины', 'box1'=>'груши' ).
#4 Точные названия полей без префикса
Когда нужно, чтобы названия метаполей были точно такие же какие вы указали, добавьте в начало id нижнее подчеркивание _.
Это может понадобится, если у вас уже есть метаполя и вам нужно подстроиться под их названия. В этом случае префикс, который добавляется для названия каждого метаполя будет лишний.
Например, у вас уже есть метаполя: foo, bar, views, title и для них нужно создать метабокс:
class_exists( 'Kama_Post_Meta_Box' ) && new Kama_Post_Meta_Box(
[
'id' => '_my', // "_" - значит, что id не будет добавляться в название поля
'title' => 'Мои точные произвольные поля',
'fields' => [
'foo' => [ 'title' => 'Поле foo' ],
'bar' => [ 'title' => 'Поле bar' ],
'views' => [ 'title' => 'Поле views' ],
'title' => [ 'title' => 'Поле title' ],
],
]
);
Получим такой метабокс:
#5 Очистка значений перед сохранением
Класс автоматически очищает все поля и защищает от XSS атак. Но иногда может быть нужно, очистить определенное поле как-то особенно. В этом случае укажите навзание функции очистки в параметре 'save_sanitize' или используйте фильтр "kpmb_save_sanitize_{id}". Если указана функция или хук очистки, то класс никак не очищает сохраняемые данные, очистка всех полей должна быть в вашей функции очистки.
Допустим, мы создаем поле в котором все символы должны быть прописными, а если они указаны как строчные, то автоматом преобразуем их в прописные и сохраним:
class_exists( 'Kama_Post_Meta_Box' ) && new Kama_Post_Meta_Box(
[
'id' => 'my',
'title' => 'Мои произвольные поля',
'save_sanitize' => 'my_metabox_sanitize_function',
'fields' => [
'foo_field' => [ 'title' => 'Некое поле' ],
'for_esc' => [
'title' => 'Специальное поле',
'desc' => 'Поле которое должно содержать только прописные символы.'
],
],
]
);
// функция очистки всех полей
function my_metabox_sanitize_function( $metas, $post_id ) {
/*
$metas - сохраняемые поля в массиве
$post - ID записи
*/
// очищаем нужное поле
foreach ( $metas as $key => & $val ) {
// наше поле
if ( $key === 'my_for_esc' ) {
$val = mb_strtoupper( $val );
}
// все остальные поля
else {
$val = sanitize_text_field( $val );
}
}
return $metas;
}
В результате получим:
Еще один пример функции очистки
Такой подход используется в классе, если не указать функцию очистки. Он очищает с помощью wp_kses() и обрабатывает массив и все вложенные массивы.
#6 Темы оформления: настройка html и css каждого поля
Можно указать как должны выводиться поля в метабоксе. Для этого есть параметр theme - тема оформления.
В этом параметре можно указать строку или массив:
table (по умолчанию) - поля будут выведены в табличной форме.
line - поля будут выведены линиями. Во всю ширину метабокса, где идет заголовок поля, а под ним само поле.
grid - подя будут выведены в виде таблицы.
array() - указывая массив, вы можете сами определить все виды оборачивающих тегов для каждого элемента поля. В массиве можно указать следующие параметры:
fields_wrap — формат обёртки всего метаблока (всех полей)
field_wrap — формат обёртки поля (вместе с заголовком и полем)
title_patt — формат обёртки заголовка поля
field_patt — формат обёртки самого поля (только input...)
desc_patt — формат обёртки описания поля
class_exists( 'Kama_Post_Meta_Box' ) && new Kama_Post_Meta_Box(
[
'id' => 'my',
'title' => 'Мои произвольные поля',
'theme' => [
'css' => '.my_field_wrap{ margin-bottom:1em; } .my_field_desc{ opacity:0.5; } .my_field_tit{ font-weight:bold; margin-bottom:.3em; }',
'fields_wrap' => '%s',
'field_wrap' => '<div class="my_field_wrap %1$s">%2$s</div>', // '%2$s' будет заменено на html поля
'title_patt' => '<div class="my_field_tit">%s</div>', // '%s' будет заменено на заголовок
'field_patt' => '%s',
'desc_patt' => '<span class="my_field_desc"> %s</span>', // '%s' будет заменено на текст описания
],
'fields' => [
'text_field' => [
'title' => 'Текстовое поле'
],
'number_field' => [
'type' => 'number',
'title' => 'Числовое поле',
'desc' => 'Число от 0 до 5',
'attr' => 'min="0" max="5"'
],
'textarea_field' => [
'type' => 'textarea',
'title' => 'Большое текстовое поле',
'desc' => 'Описание чего-либо. Можно использовать html теги.'
],
'select_field' => [
'type' => 'select',
'title' => 'Выберите значение',
'options' => [ '' => 'Ничего не выбрано', 'val_1' => 'Выбор 1', 'val_2' => 'Выбор 2' ]
],
'checkbox_field' => [
'type' => 'checkbox',
'title' => 'Галочка',
'desc' => 'отметьте, если хотите :)'
],
'radio_field' => [
'type' => 'radio',
'title' => 'Переключатель',
'desc' => 'Выберите одно из значений',
'options' => [ '' => 'Ничего не выбрано', 'good' => 'хорошо', 'bad' => 'плохо' ]
],
'wp_editor_field2' => [
'type' => 'wp_editor',
'title' => 'Текстовое поле с редактором WordPress, без TinyMce',
'options' => [ 'tinymce' => 0 ] // список настроек: http://wp-kama.ru/function/wp_editor
],
// заготовка
'' => [
'type' => 'text',
'title' => ''
],
],
]
);
В результате получим такой метабокс:
#7 Как выглядит тема grid
class_exists( 'Kama_Post_Meta_Box' ) && new Kama_Post_Meta_Box( [
'id' => '_recept',
'title' => 'Рецепт (короткое описание)',
'theme' => 'grid',
'post_type' => [ 'post' ], // не выводить для типов записи
'fields' => [
'text_field' => [
'title' => 'Текстовое поле'
],
'hidden' => [
'title' => 'hiddentitle',
'type' => 'hidden'
],
'image' => [
'title' => 'Картинка',
'type' => 'image'
],
'number_field' => [
'type' => 'number',
'title' => 'Числовое поле',
'desc' => 'Число от 0 до 5',
'attr' => 'min="0" max="5"'
],
'textarea_field' => [
'type' => 'textarea',
'title' => 'Большое текстовое поле',
'desc' => 'Описание чего-либо. Можно использовать html теги.'
],
'select_field' => [
'type' => 'select',
'title' => 'Выберите значение',
'options' => [ '' => 'Ничего не выбрано', 'val_1' => 'Выбор 1', 'val_2' => 'Выбор 2' ]
],
'checkbox_field' => [
'type' => 'checkbox',
'title' => 'Галочка',
'desc' => 'отметьте, если хотите :)'
],
'radio_field' => [
'type' => 'radio',
'title' => 'Переключатель',
'desc' => 'Выберите одно из значений',
'options' => [ '' => 'Ничего не выбрано', 'good' => 'хорошо', 'bad' => 'плохо' ]
],
'sep_1' => [ 'title' => 'Длиннное предложение для разделителя' ],
'wp_editor_field2' => [
'type' => 'wp_editor',
'title' => 'Текстовое поле с редактором WordPress, без TinyMce',
'options' => [ 'tinymce' => 0 ] // список настроек: http://wp-kama.ru/function/wp_editor
],
],
] );
Получим:
#8 Блоки с одинаковыми id
Класс позволяет создавать два и более разных блока с одинаковыми id. Следующий пример создаст 2 разных блока метаполя которых будут иметь одинаковый префикс: my_:
Иногда нужно включить метабокс для одной рубрики, но не включать для другой. Для таких "поздних" проверок отключения метабокса, есть параметр: disable_func.
В этот параметр нужно передать название функции, которая будет срабатывать перед выводом метабокса и отключать его, если срабатывает описанное в функции условие. Также, вместо названия функции, можно передать анонимную функцию (замыкание). Далее, если указанная функция, что-нибудь возвращает, то метабокс будет отключен.
А теперь примеры.
1. Метабокс записи для указанной рубрики
Допустим нам нужно вывести метабокс, только если запись находится в рубрике с ID = 2, т.е. его нужно скрыть для всех рубрик кроме 2:
2. Метабокс записи для всех рубрик кроме указанной
Теперь обратный пример: допустим нужно показывать метабокс, когда запись находится в любой категории, кроме категории 2, т.е. для категории 2 его нужно отключить.
class_exists( 'Kama_Post_Meta_Box' ) && new Kama_Post_Meta_Box( [
'id' => 'mybox',
'title' => 'Виден для всех рубрик, кроме 2',
'post_type' => 'post',
'disable_func' => static function ( $post ) {
if ( in_category( 2, $post ) ) {
return 'отключить';
}
},
'fields' => [
'fname' => [ 'type' => 'text', 'title' => 'Поле-поле' ],
],
] );
Проверка может быть любой необязательно категории, это могут быть таксономии или произвольные поля или что-то еще.
#10 Создание своих полей
Для создания своих полей нужно расширить класс Kama_Post_Meta_Box_Fields и указать новый класс в качестве рабочего через хук kama_post_meta_box__fields_class: