API Сайдбаров и Виджетов

В техническом плане виджет WordPress - это PHP-объект, который наследует класс WP_Widget и выводит HTML через метод widget().

Widgets API используется для двух связанных задач:

  • создания новых виджетов;
  • регистрации областей виджетов, в которые эти виджеты можно добавить.

Код Widgets API находится в файлах:

Термины

Виджет - отдельный PHP-объект, который выводит HTML и может иметь собственные настройки.

Область виджетов или сайдбар - зарегистрированная область темы, куда пользователь может добавлять виджеты через админку WordPress.

Термин sidebar в API не обязательно означает боковую колонку. Это может быть любая область: футер, хедер, блок под записью, рекламная зона и т.д.

Разработка виджетов

Чтобы создать виджет, нужно расширить стандартный класс WP_Widget и переопределить нужные методы.

Минимально обязательный метод - widget(). Если у виджета есть настройки, также нужно переопределить методы form() и update().

class My_Widget extends WP_Widget {

	public function __construct() {
		$widget_ops = array(
			'classname'   => 'my_widget',
			'description' => 'Описание виджета.',
		);

		parent::__construct( 'my_widget', 'My Widget', $widget_ops );
	}

	public function widget( $args, $instance ) {
		// Вывод виджета на фронте.
	}

	public function form( $instance ) {
		// Форма настроек виджета в админке.
	}

	public function update( $new_instance, $old_instance ) {
		// Обработка и очистка настроек перед сохранением.
	}
}

Затем виджет нужно зарегистрировать на хуке widgets_init:

add_action( 'widgets_init', 'register_my_widget' );

function register_my_widget() {
	register_widget( 'My_Widget' );
}

Методы класса WP_Widget

__construct()

Регистрирует тип виджета и передает данные родительскому классу.

parent::__construct( $id_base, $name, $widget_options, $control_options );

Параметры:

  • $id_base - базовый ID виджета. Используется в HTML, опциях и ID экземпляров виджета.
  • $name - название виджета в интерфейсе админки.
  • $widget_options - массив настроек виджета.
  • $control_options - массив настроек формы управления виджетом.

Пример:

public function __construct() {
	$widget_ops = array(
		'classname'                   => 'foo_widget',
		'description'                 => __( 'Displays a custom text block.', 'textdomain' ),
		'customize_selective_refresh' => true,
	);

	parent::__construct(
		'foo_widget',
		__( 'Foo Widget', 'textdomain' ),
		$widget_ops
	);
}

widget()

Выводит HTML виджета на фронте.

public function widget( $args, $instance ) {}

Параметры:

  • $args - аргументы области виджетов: before_widget, after_widget, before_title, after_title.
  • $instance - настройки конкретного экземпляра виджета.

Метод должен выводить результат через echo.

public function widget( $args, $instance ) {
	$title = ! empty( $instance['title'] ) ? $instance['title'] : '';

	echo $args['before_widget'];

	if ( $title ) {
		echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
	}

	echo '<p>' . esc_html__( 'Hello, World!', 'textdomain' ) . '</p>';

	echo $args['after_widget'];
}

form()

Выводит форму настроек виджета в админке.

public function form( $instance ) {}

Параметры:

  • $instance - текущие сохраненные настройки виджета.

В полях формы нужно использовать методы:

  • $this->get_field_id( 'field_name' ) - создает уникальный id поля;
  • $this->get_field_name( 'field_name' ) - создает правильный name поля для сохранения.
public function form( $instance ) {
	$title = isset( $instance['title'] ) ? $instance['title'] : '';
	?>
	<p>
		<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
			<?php esc_html_e( 'Title:', 'textdomain' ); ?>
		</label>
		<input
			class="widefat"
			id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
			name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
			type="text"
			value="<?php echo esc_attr( $title ); ?>"
		>
	</p>
	<?php
}

Метод get_field_name() поддерживает имена в формате массива:

$this->get_field_name( 'links[0][url]' );

update()

Обрабатывает настройки виджета перед сохранением.

public function update( $new_instance, $old_instance ) {}

Параметры:

  • $new_instance - новые данные из формы.
  • $old_instance - старые сохраненные данные.

Метод должен вернуть массив данных, которые нужно сохранить. Если вернуть false, настройки не будут сохранены.

public function update( $new_instance, $old_instance ) {
	$instance = array();

	$instance['title'] = isset( $new_instance['title'] )
		? sanitize_text_field( $new_instance['title'] )
		: '';

	return $instance;
}

Полный пример виджета

Этот пример создает виджет Foo_Widget с настройкой заголовка и текста.

<?php

class Foo_Widget extends WP_Widget {

	public function __construct() {
		$widget_ops = array(
			'classname'                   => 'foo_widget',
			'description'                 => __( 'Displays custom text.', 'textdomain' ),
			'customize_selective_refresh' => true,
		);

		parent::__construct(
			'foo_widget',
			__( 'Foo Widget', 'textdomain' ),
			$widget_ops
		);
	}

	public function widget( $args, $instance ) {
		$title = isset( $instance['title'] ) ? $instance['title'] : '';
		$text  = isset( $instance['text'] ) ? $instance['text'] : '';

		echo $args['before_widget'];

		if ( $title ) {
			$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
			echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
		}

		if ( $text ) {
			echo '<div class="foo-widget__text">' . wp_kses_post( wpautop( $text ) ) . '</div>';
		}

		echo $args['after_widget'];
	}

	public function form( $instance ) {
		$title = isset( $instance['title'] ) ? $instance['title'] : '';
		$text  = isset( $instance['text'] ) ? $instance['text'] : '';
		?>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
				<?php esc_html_e( 'Title:', 'textdomain' ); ?>
			</label>
			<input
				class="widefat"
				id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
				type="text"
				value="<?php echo esc_attr( $title ); ?>"
			>
		</p>

		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>">
				<?php esc_html_e( 'Text:', 'textdomain' ); ?>
			</label>
			<textarea
				class="widefat"
				id="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'text' ) ); ?>"
				rows="5"
			><?php echo esc_textarea( $text ); ?></textarea>
		</p>
		<?php
	}

	public function update( $new_instance, $old_instance ) {
		$instance = array();

		$instance['title'] = isset( $new_instance['title'] )
			? sanitize_text_field( $new_instance['title'] )
			: '';

		$instance['text'] = isset( $new_instance['text'] )
			? wp_kses_post( $new_instance['text'] )
			: '';

		return $instance;
	}
}

Регистрация виджета:

add_action( 'widgets_init', 'register_foo_widget' );

function register_foo_widget() {
	register_widget( 'Foo_Widget' );
}

Пример с пространством имен

Если класс виджета находится в пространстве имен, при наследовании нужно указывать глобальный класс \WP_Widget.

namespace My_Plugin\Widgets;

class Foo_Widget extends \WP_Widget {

	public function __construct() {
		parent::__construct(
			'foo_widget',
			__( 'Foo Widget', 'textdomain' )
		);
	}

	public function widget( $args, $instance ) {}
}

Регистрация:

add_action( 'widgets_init', __NAMESPACE__ . '\\register_widgets' );

function register_widgets() {
	register_widget( __NAMESPACE__ . '\\Foo_Widget' );
}

Или через полное имя класса:

add_action( 'widgets_init', 'my_plugin_register_widgets' );

function my_plugin_register_widgets() {
	register_widget( 'My_Plugin\\Widgets\\Foo_Widget' );
}

Регистрация областей виджетов

Область виджетов регистрируется функцией register_sidebar() на хуке widgets_init.

add_action( 'widgets_init', 'theme_register_widget_areas' );

function theme_register_widget_areas() {
	register_sidebar(
		array(
			'name'          => __( 'Sidebar', 'textdomain' ),
			'id'            => 'sidebar-1',
			'description'   => __( 'Main sidebar area.', 'textdomain' ),
			'before_widget' => '<section id="%1$s" class="widget %2$s">',
			'after_widget'  => '</section>',
			'before_title'  => '<h2 class="widget-title">',
			'after_title'   => '</h2>',
		)
	);
}

Параметры register_sidebar():

  • name - название области в админке.
  • id - уникальный ID области. Лучше всегда задавать вручную.
  • description - описание области в админке.
  • class - дополнительный CSS-класс в интерфейсе админки.
  • before_widget - HTML перед каждым виджетом.
  • after_widget - HTML после каждого виджета.
  • before_title - HTML перед заголовком виджета.
  • after_title - HTML после заголовка виджета.
  • before_sidebar - HTML перед всей областью виджетов.
  • after_sidebar - HTML после всей области виджетов.
  • show_in_rest - показывает область в REST API.

В before_widget можно использовать плейсхолдеры:

  • %1$s - ID конкретного экземпляра виджета;
  • %2$s - CSS-классы виджета.

Пример:

'before_widget' => '<section id="%1$s" class="widget %2$s">',

WordPress может сгенерировать id автоматически, но так лучше не делать. Автоматический ID зависит от порядка регистрации областей и может измениться при смене темы или плагинов.

Регистрация нескольких областей

Для нескольких областей лучше вызвать register_sidebar() несколько раз, чтобы у каждой области были понятные id, name и описание.

add_action( 'widgets_init', 'theme_register_widget_areas' );

function theme_register_widget_areas() {
	$sidebars = array(
		'footer-1' => __( 'Footer 1', 'textdomain' ),
		'footer-2' => __( 'Footer 2', 'textdomain' ),
		'footer-3' => __( 'Footer 3', 'textdomain' ),
	);

	foreach ( $sidebars as $id => $name ) {
		register_sidebar(
			array(
				'name'          => $name,
				'id'            => $id,
				'before_widget' => '<section id="%1$s" class="widget %2$s">',
				'after_widget'  => '</section>',
				'before_title'  => '<h2 class="widget-title">',
				'after_title'   => '</h2>',
			)
		);
	}
}

Функция register_sidebars() тоже существует, но в большинстве случаев удобнее использовать несколько вызовов register_sidebar(), потому что так проще задать уникальные параметры для каждой области.

Отображение областей виджетов

Область виджетов выводится функцией dynamic_sidebar().

<?php dynamic_sidebar( 'sidebar-1' ); ?>

Функция принимает ID, имя или номер области виджетов и возвращает true, если область найдена и была выведена.

Обычно перед выводом проверяют, есть ли в области активные виджеты:

<?php if ( is_active_sidebar( 'sidebar-1' ) ) : ?>
	<aside class="site-sidebar" role="complementary">
		<?php dynamic_sidebar( 'sidebar-1' ); ?>
	</aside>
<?php endif; ?>

Так пустая HTML-обертка не будет выводиться, если пользователь не добавил виджеты.

Fallback для пустой области

Если нужно вывести запасной HTML, когда область пустая, используйте else.

<aside class="site-sidebar" role="complementary">
	<?php if ( is_active_sidebar( 'sidebar-1' ) ) : ?>
		<?php dynamic_sidebar( 'sidebar-1' ); ?>
	<?php else : ?>
		<p><?php esc_html_e( 'Add widgets to this sidebar in the admin area.', 'textdomain' ); ?></p>
	<?php endif; ?>
</aside>

Отображение одного виджета напрямую

Отдельный виджет можно вывести без области виджетов через функцию the_widget().

the_widget( 'WP_Widget_Search' );

С настройками экземпляра:

the_widget(
	'WP_Widget_Recent_Posts',
	array(
		'title'  => __( 'Recent Posts', 'textdomain' ),
		'number' => 5,
	),
	array(
		'before_widget' => '<section class="widget widget_recent_entries">',
		'after_widget'  => '</section>',
		'before_title'  => '<h2 class="widget-title">',
		'after_title'   => '</h2>',
	)
);

the_widget() полезна, когда виджет нужно вывести программно в конкретном месте шаблона, без управления через область виджетов.

Удаление виджетов и областей

Зарегистрированный виджет можно убрать функцией unregister_widget().

add_action( 'widgets_init', 'remove_default_widgets', 11 );

function remove_default_widgets() {
	unregister_widget( 'WP_Widget_Meta' );
	unregister_widget( 'WP_Widget_Calendar' );
}

Область виджетов можно убрать функцией unregister_sidebar().

add_action( 'widgets_init', 'remove_theme_sidebars', 11 );

function remove_theme_sidebars() {
	unregister_sidebar( 'sidebar-1' );
}

При удалении области сами настройки виджетов из базы данных не обязательно удаляются сразу. Они просто перестают выводиться, потому что область больше не зарегистрирована.

Где хранятся настройки

Настройки виджетов хранятся в таблице wp_options.

Основные опции:

  • sidebars_widgets - список областей виджетов и ID виджетов, которые к ним привязаны.
  • widget_{$id_base} - настройки всех экземпляров виджета с указанным $id_base.

Например, для виджета с $id_base = 'foo_widget' настройки будут храниться в опции:

widget_foo_widget

Каждый экземпляр виджета имеет свой номер. Поэтому один и тот же виджет можно использовать несколько раз с разными настройками.

Стандартные виджеты WordPress

Некоторые классы стандартных виджетов:

  • WP_Widget_Pages - страницы.
  • WP_Widget_Calendar - календарь.
  • WP_Widget_Archives - архивы.
  • WP_Widget_Media_Audio - аудио.
  • WP_Widget_Media_Image - изображение.
  • WP_Widget_Media_Gallery - галерея.
  • WP_Widget_Media_Video - видео.
  • WP_Widget_Meta - мета-ссылки.
  • WP_Widget_Search - поиск.
  • WP_Widget_Text - текст.
  • WP_Widget_Categories - рубрики.
  • WP_Widget_Recent_Posts - последние записи.
  • WP_Widget_Recent_Comments - последние комментарии.
  • WP_Widget_RSS - RSS.
  • WP_Widget_Tag_Cloud - облако тегов.
  • WP_Nav_Menu_Widget - меню навигации.

Их можно вывести через dynamic_sidebar(), добавить в область виджетов через админку или вызвать напрямую через the_widget().

Хуки Widgets API

widgets_init

Основной хук для регистрации виджетов и областей виджетов.

add_action( 'widgets_init', 'theme_widgets_init' );

widget_title

Фильтрует заголовок виджета перед выводом.

$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );

Пример:

add_filter( 'widget_title', 'theme_change_widget_title' );

function theme_change_widget_title( $title ) {
	return trim( $title );
}

register_sidebar

Срабатывает после регистрации области виджетов.

add_action( 'register_sidebar', 'theme_after_sidebar_registered' );

function theme_after_sidebar_registered( $sidebar ) {
	// $sidebar содержит итоговые параметры области.
}

dynamic_sidebar_before

Срабатывает перед выводом области виджетов.

add_action( 'dynamic_sidebar_before', 'theme_before_dynamic_sidebar' );

function theme_before_dynamic_sidebar( $index ) {
	// $index - ID, имя или номер области.
}

dynamic_sidebar_after

Срабатывает после вывода области виджетов.

add_action( 'dynamic_sidebar_after', 'theme_after_dynamic_sidebar' );

function theme_after_dynamic_sidebar( $index ) {
	// $index - ID, имя или номер области.
}

dynamic_sidebar_params

Фильтрует параметры виджета перед выводом.

add_filter( 'dynamic_sidebar_params', 'theme_filter_widget_params' );

function theme_filter_widget_params( $params ) {
	$params[0]['before_widget'] = str_replace( 'widget ', 'widget custom-widget ', $params[0]['before_widget'] );

	return $params;
}

Виджеты и редактор блоков

С WordPress 5.8 экран виджетов стал блочным. Это не отменяет классические PHP-виджеты на основе WP_Widget.

Классические виджеты могут отображаться в блочном редакторе через блок Legacy Widget. Обычно старые виджеты продолжают работать без переписывания на блоки.

Если форма виджета использует JavaScript в админке, код нужно инициализировать после события widget-added.

jQuery( document ).on( 'widget-added', function( event, widget ) {
	// Init widget admin JS here.
} );

Также часто нужно учитывать событие widget-updated, если форма виджета перерисовывается после сохранения.

jQuery( document ).on( 'widget-updated', function( event, widget ) {
	// Re-init widget admin JS here.
} );

Безопасность

Виджет должен очищать данные при сохранении и экранировать данные при выводе.

При сохранении в update() используйте подходящие функции:

  • sanitize_text_field() - для обычного текста.
  • sanitize_key() - для ключей.
  • absint() - для положительных чисел.
  • esc_url_raw() - для URL перед сохранением.
  • wp_kses_post() - для HTML, разрешенного как в постах.

При выводе в widget() используйте:

  • esc_html() - для обычного текста.
  • esc_attr() - для HTML-атрибутов.
  • esc_url() - для URL.
  • wp_kses_post() - для разрешенного HTML.

Пример:

public function update( $new_instance, $old_instance ) {
	$instance = array();

	$instance['title'] = isset( $new_instance['title'] )
		? sanitize_text_field( $new_instance['title'] )
		: '';

	$instance['url'] = isset( $new_instance['url'] )
		? esc_url_raw( $new_instance['url'] )
		: '';

	return $instance;
}
echo '<a href="' . esc_url( $instance['url'] ) . '">' . esc_html( $instance['title'] ) . '</a>';

Практические рекомендации

  • Регистрируйте виджеты и области виджетов на хуке widgets_init.
  • Всегда задавайте стабильный id для register_sidebar().
  • Используйте get_field_id() и get_field_name() для полей формы.
  • Очищайте данные в update().
  • Экранируйте данные в widget().
  • Не выводите пустые обертки, если область виджетов неактивна.
  • Не используйте PHP4-конструктор вида function My_Widget(). Используйте __construct().
  • Для новых сложных интерфейсов лучше рассмотреть блок, а не классический виджет.
  • Для простых совместимых решений WP_Widget все еще подходит.

Краткий список функций