Skip to content

Всё для WordPress, веб-разработки — и не только

🛠 Как создать плагин виджетов для WordPress: пошаговое руководство

🛠 Как создать плагин виджетов для WordPress: пошаговое руководство

💡 Как собрать плагин виджета WordPress

  • Создайте папку my-widget-plugin в wp-content/plugins/ и файл my-widget-plugin.php с заголовком плагина, затем активируйте его в админке
  • Объявите класс, наследующий WP_Widget, и переопределите методы __construct(), form(), update() и widget()
  • Зарегистрируйте виджет функцией register_widget() на хуке widgets_init, чтобы WordPress увидел его в списке
  • В методе form() выведите поля настроек, в update() очистите и сохраните данные через sanitize_text_field()
  • В методе widget() выведите содержимое на сайте, экранируя значения через esc_html() и wp_kses_post()

Шаг 1: создаем плагин-каркас

Виджет в WordPress, это не просто строчка в functions.php. Если вы хотите, чтобы настройки сохранялись, форма в админке работала, а сам виджет не ломался при следующем обновлении темы, его нужно оформить как плагин. Это изолирует код и делает виджет независимым от смены темы.

Начните с пустой папки. Перейдите в wp-content/plugins/ и создайте директорию my-widget-plugin. Внутри, файл my-widget-plugin.php. Именно его WordPress прочитает первым при активации. Откройте файл и добавьте стандартный заголовок плагина:

1<?php
2/*
3Plugin Name: My Widget Plugin
4Plugin URI: https://www.wpexplorer.com/create-widget-plugin-wordpress/
5Description: Добавляет настраиваемый виджет с текстом, текстовой областью, чекбоксом и выпадающим списком.
6Version: 1.0
7Author: AJ Clarke
8Author URI: https://www.wpexplorer.com/
9License: GPL2
10*/

Сохраните. Теперь зайдите в админку WordPress → Плагины. Если плагин появился в списке, каркас готов. Нажмите «Активировать». Пока он не делает ничего полезного, мы только объявили о его существовании. Но WordPress уже знает, что такой плагин есть, и готов выполнять его код. Это важный принцип: сначала регистрация, потом логика.

Как устроен плагин изнутри, какие хуки срабатывают при активации и как WordPress находит ваш файл, смотрите в видео выше. А мы переходим к самому интересному: классу виджета.

Шаг 2: регистрируем виджет через WP_Widget

WordPress предоставляет готовый класс WP_Widget, он находится в ядре с версии 2.8 и с тех пор остается основой для любых кастомных виджетов. Вам не нужно писать логику сохранения, генерации полей и регистрации с нуля, достаточно унаследоваться и переопределить четыре метода.

Добавьте этот код в ваш my-widget-plugin.php сразу после заголовка плагина, перед закрывающим ?>:

1// Класс виджета
2class My_Custom_Widget extends WP_Widget {
3
4 public function __construct() {
5 parent::__construct(
6 'my_custom_widget',
7 __( 'My Custom Widget', 'text_domain' ),
8 array(
9 'customize_selective_refresh' => true,
10 )
11 );
12 }
13
14 public function form( $instance ) {
15 /* ... форма в админке ... */
16 }
17
18 public function update( $new_instance, $old_instance ) {
19 /* ... сохранение настроек ... */
20 }
21
22 public function widget( $args, $instance ) {
23 /* ... вывод на сайте ... */
24 }
25}
26
27// Регистрация виджета
28function my_register_custom_widget() {
29 register_widget( 'My_Custom_Widget' );
30}
31add_action( 'widgets_init', 'my_register_custom_widget' );

Разберем, что здесь происходит. Класс My_Custom_Widget наследует WP_Widget, это дает вам готовые методы get_field_id() и get_field_name() для генерации атрибутов полей формы. В конструкторе мы передаем родительскому классу три вещи: уникальный ID виджета (латиницей, строчными, без пробелов, my_custom_widget), его человекочитаемое имя (функция __() делает его переводимым) и массив опций. Параметр customize_selective_refresh => true разрешает обновлять виджет в кастомайзере без перезагрузки всей страницы, мелочь, которая экономит нервы при настройке.

Функция my_register_custom_widget() вызывает register_widget() на хуке widgets_init. Именно так WordPress узнает о существовании вашего виджета. Без этой строчки в админке ничего не появится.

Теперь наполним методы form(), update() и widget() реальной логикой.

Шаг 3: создаем форму виджета в админке

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

3.1. Функция form(), поля ввода

Добавьте этот код в метод form() вашего класса. Он создает пять полей: заголовок, текстовая строка, текстовая область, чекбокс и выпадающий список.

1public function form( $instance ) {
2
3 $defaults = array(
4 'title' => '',
5 'text' => '',
6 'textarea' => '',
7 'checkbox' => '',
8 'select' => '',
9 );
10
11 $args = wp_parse_args( (array) $instance, $defaults );
12 $title = $args['title'];
13 $text = $args['text'];
14 $textarea = $args['textarea'];
15 $checkbox = $args['checkbox'];
16 $select = $args['select'];
17 ?>
18
19 <p>
20 <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
21 <?php _e( 'Заголовок виджета', 'text_domain' ); ?>
22 </label>
23 <input class="widefat"
24 id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
25 name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
26 type="text"
27 value="<?php echo esc_attr( $title ); ?>" />
28 </p>
29
30 <p>
31 <label for="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>">
32 <?php _e( 'Текст:', 'text_domain' ); ?>
33 </label>
34 <input class="widefat"
35 id="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>"
36 name="<?php echo esc_attr( $this->get_field_name( 'text' ) ); ?>"
37 type="text"
38 value="<?php echo esc_attr( $text ); ?>" />
39 </p>
40
41 <p>
42 <label for="<?php echo esc_attr( $this->get_field_id( 'textarea' ) ); ?>">
43 <?php _e( 'Текстовая область:', 'text_domain' ); ?>
44 </label>
45 <textarea class="widefat"
46 id="<?php echo esc_attr( $this->get_field_id( 'textarea' ) ); ?>"
47 name="<?php echo esc_attr( $this->get_field_name( 'textarea' ) ); ?>"><?php
48 echo wp_kses_post( $textarea );
49 ?></textarea>
50 </p>
51
52 <p>
53 <input id="<?php echo esc_attr( $this->get_field_id( 'checkbox' ) ); ?>"
54 name="<?php echo esc_attr( $this->get_field_name( 'checkbox' ) ); ?>"
55 type="checkbox"
56 value="1"
57 <?php checked( '1', $checkbox ); ?> />
58 <label for="<?php echo esc_attr( $this->get_field_id( 'checkbox' ) ); ?>">
59 <?php _e( 'Показать дополнительный блок', 'text_domain' ); ?>
60 </label>
61 </p>
62
63 <p>
64 <label for="<?php echo $this->get_field_id( 'select' ); ?>">
65 <?php _e( 'Вариант отображения', 'text_domain' ); ?>
66 </label>
67 <select name="<?php echo $this->get_field_name( 'select' ); ?>"
68 id="<?php echo $this->get_field_id( 'select' ); ?>"
69 class="widefat">
70 <?php
71 $options = array(
72 '' => __( '— Выберите —', 'text_domain' ),
73 'option_1' => __( 'Вариант 1', 'text_domain' ),
74 'option_2' => __( 'Вариант 2', 'text_domain' ),
75 'option_3' => __( 'Вариант 3', 'text_domain' ),
76 );
77 foreach ( $options as $key => $name ) {
78 printf(
79 '<option value="%s" %s>%s</option>',
80 esc_attr( $key ),
81 selected( $select, $key, false ),
82 esc_html( $name )
83 );
84 }
85 ?>
86 </select>
87 </p>
88
89<?php }

Обратите внимание на две вещи. Первая: мы заменили устаревшую функцию extract() на прямое обращение к элементам массива. extract() давно исключен из стандартов кодирования WordPress, он создает переменные с именами ключей, что небезопасно и затрудняет отладку. Вторая: каждое выводимое значение пропущено через esc_attr(), wp_kses_post() или esc_html(). Это не паранойя, данные из базы могут прийти откуда угодно, и санитизация обязательна.

3.2. Функция update(), сохранение

Метод update() вызывается при нажатии кнопки «Сохранить» в форме виджета. Его задача, проверить каждое поле и вернуть очищенный массив для записи в базу.

1public function update( $new_instance, $old_instance ) {
2 $instance = $old_instance;
3
4 $instance['title'] = isset( $new_instance['title'] )
5 ? sanitize_text_field( $new_instance['title'] ) : '';
6 $instance['text'] = isset( $new_instance['text'] )
7 ? sanitize_text_field( $new_instance['text'] ) : '';
8 $instance['textarea'] = isset( $new_instance['textarea'] )
9 ? wp_kses_post( $new_instance['textarea'] ) : '';
10 $instance['checkbox'] = isset( $new_instance['checkbox'] ) ? 1 : false;
11 $instance['select'] = isset( $new_instance['select'] )
12 ? sanitize_text_field( $new_instance['select'] ) : '';
13
14 return $instance;
15}

Здесь мы используем sanitize_text_field() вместо wp_strip_all_tags(), эта функция дополнительно убирает пробельные символы и приводит строку к безопасному виду. Для текстовой области оставляем wp_kses_post(): она разрешает базовый HTML (ссылки, жирный шрифт, списки), но вырезает скрипты. Чекбокс возвращает 1 или false, в базе это будет читаемо и однозначно.

Шаг 4: выводим виджет на сайте

Функция widget(), это то, что увидит посетитель. Она получает два параметра: $args (обертка виджета, теги до и после заголовка, сайдбара) и $instance (сохраненные настройки конкретного экземпляра).

1public function widget( $args, $instance ) {
2
3 $title = isset( $instance['title'] )
4 ? apply_filters( 'widget_title', $instance['title'] ) : '';
5 $text = isset( $instance['text'] ) ? $instance['text'] : '';
6 $textarea = isset( $instance['textarea'] ) ? $instance['textarea'] : '';
7 $select = isset( $instance['select'] ) ? $instance['select'] : '';
8 $checkbox = ! empty( $instance['checkbox'] ) ? $instance['checkbox'] : false;
9
10 echo $args['before_widget'];
11
12 echo '<div class="widget-text wp_widget_plugin_box">';
13
14 if ( $title ) {
15 echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
16 }
17
18 if ( $text ) {
19 echo '<p>' . esc_html( $text ) . '</p>';
20 }
21
22 if ( $textarea ) {
23 echo '<div class="widget-textarea">' . wp_kses_post( $textarea ) . '</div>';
24 }
25
26 if ( $select ) {
27 echo '<p class="widget-select">' . esc_html( $select ) . '</p>';
28 }
29
30 if ( $checkbox ) {
31 echo '<p class="widget-checkbox-result">' . esc_html__( 'Дополнительный блок активирован', 'text_domain' ) . '</p>';
32 }
33
34 echo '</div>';
35
36 echo $args['after_widget'];
37}

Ключевой момент: мы заменили extract( $args ) на прямое обращение $args['before_widget']. Причина та же, extract() устарел и небезопасен. Также мы обернули вывод в esc_html() там, где ожидается чистый текст: заголовок, текстовая строка, значение селекта. Текстовая область выводится через wp_kses_post(), если администратор вставил ссылку или жирный шрифт, они сохранятся.

CSS-класс wp_widget_plugin_box позволяет стилизовать блок из таблицы стилей темы. Меняйте его на свой, главное, чтобы класс был уникальным и не конфликтовал с классами темы.

Полный код плагина

Соберем все вместе. Вот готовый файл my-widget-plugin.php, который можно скопировать и активировать:

1<?php
2/*
3Plugin Name: My Widget Plugin
4Plugin URI: https://www.wpexplorer.com/create-widget-plugin-wordpress/
5Description: Добавляет настраиваемый виджет с текстом, текстовой областью, чекбоксом и выпадающим списком.
6Version: 1.0
7Author: AJ Clarke
8Author URI: https://www.wpexplorer.com/
9License: GPL2
10*/
11
12class My_Custom_Widget extends WP_Widget {
13
14 public function __construct() {
15 parent::__construct(
16 'my_custom_widget',
17 __( 'My Custom Widget', 'text_domain' ),
18 array( 'customize_selective_refresh' => true )
19 );
20 }
21
22 public function form( $instance ) {
23 $defaults = array(
24 'title' => '', 'text' => '', 'textarea' => '',
25 'checkbox' => '', 'select' => ''
26 );
27 $args = wp_parse_args( (array) $instance, $defaults );
28 $title = $args['title'];
29 $text = $args['text'];
30 $textarea = $args['textarea'];
31 $checkbox = $args['checkbox'];
32 $select = $args['select'];
33 ?>
34 <p>
35 <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
36 <?php _e( 'Заголовок виджета', 'text_domain' ); ?>
37 </label>
38 <input class="widefat"
39 id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
40 name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
41 type="text" value="<?php echo esc_attr( $title ); ?>" />
42 </p>
43 <p>
44 <label for="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>">
45 <?php _e( 'Текст:', 'text_domain' ); ?>
46 </label>
47 <input class="widefat"
48 id="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>"
49 name="<?php echo esc_attr( $this->get_field_name( 'text' ) ); ?>"
50 type="text" value="<?php echo esc_attr( $text ); ?>" />
51 </p>
52 <p>
53 <label for="<?php echo esc_attr( $this->get_field_id( 'textarea' ) ); ?>">
54 <?php _e( 'Текстовая область:', 'text_domain' ); ?>
55 </label>
56 <textarea class="widefat"
57 id="<?php echo esc_attr( $this->get_field_id( 'textarea' ) ); ?>"
58 name="<?php echo esc_attr( $this->get_field_name( 'textarea' ) ); ?>"><?php
59 echo wp_kses_post( $textarea );
60 ?></textarea>
61 </p>
62 <p>
63 <input id="<?php echo esc_attr( $this->get_field_id( 'checkbox' ) ); ?>"
64 name="<?php echo esc_attr( $this->get_field_name( 'checkbox' ) ); ?>"
65 type="checkbox" value="1" <?php checked( '1', $checkbox ); ?> />
66 <label for="<?php echo esc_attr( $this->get_field_id( 'checkbox' ) ); ?>">
67 <?php _e( 'Показать дополнительный блок', 'text_domain' ); ?>
68 </label>
69 </p>
70 <p>
71 <label for="<?php echo $this->get_field_id( 'select' ); ?>">
72 <?php _e( 'Вариант отображения', 'text_domain' ); ?>
73 </label>
74 <select name="<?php echo $this->get_field_name( 'select' ); ?>"
75 id="<?php echo $this->get_field_id( 'select' ); ?>" class="widefat">
76 <?php
77 $options = array(
78 '' => __( '— Выберите —', 'text_domain' ),
79 'option_1' => __( 'Вариант 1', 'text_domain' ),
80 'option_2' => __( 'Вариант 2', 'text_domain' ),
81 'option_3' => __( 'Вариант 3', 'text_domain' ),
82 );
83 foreach ( $options as $key => $name ) {
84 printf(
85 '<option value="%s" %s>%s</option>',
86 esc_attr( $key ),
87 selected( $select, $key, false ),
88 esc_html( $name )
89 );
90 }
91 ?>
92 </select>
93 </p>
94 <?php
95 }
96
97 public function update( $new_instance, $old_instance ) {
98 $instance = $old_instance;
99 $instance['title'] = isset( $new_instance['title'] )
100 ? sanitize_text_field( $new_instance['title'] ) : '';
101 $instance['text'] = isset( $new_instance['text'] )
102 ? sanitize_text_field( $new_instance['text'] ) : '';
103 $instance['textarea'] = isset( $new_instance['textarea'] )
104 ? wp_kses_post( $new_instance['textarea'] ) : '';
105 $instance['checkbox'] = isset( $new_instance['checkbox'] ) ? 1 : false;
106 $instance['select'] = isset( $new_instance['select'] )
107 ? sanitize_text_field( $new_instance['select'] ) : '';
108 return $instance;
109 }
110
111 public function widget( $args, $instance ) {
112 $title = isset( $instance['title'] )
113 ? apply_filters( 'widget_title', $instance['title'] ) : '';
114 $text = isset( $instance['text'] ) ? $instance['text'] : '';
115 $textarea = isset( $instance['textarea'] ) ? $instance['textarea'] : '';
116 $select = isset( $instance['select'] ) ? $instance['select'] : '';
117 $checkbox = ! empty( $instance['checkbox'] ) ? $instance['checkbox'] : false;
118
119 echo $args['before_widget'];
120 echo '<div class="widget-text wp_widget_plugin_box">';
121
122 if ( $title ) {
123 echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
124 }
125 if ( $text ) {
126 echo '<p>' . esc_html( $text ) . '</p>';
127 }
128 if ( $textarea ) {
129 echo '<div class="widget-textarea">' . wp_kses_post( $textarea ) . '</div>';
130 }
131 if ( $select ) {
132 echo '<p class="widget-select">' . esc_html( $select ) . '</p>';
133 }
134 if ( $checkbox ) {
135 echo '<p class="widget-checkbox-result">'
136 . esc_html__( 'Дополнительный блок активирован', 'text_domain' ) . '</p>';
137 }
138
139 echo '</div>';
140 echo $args['after_widget'];
141 }
142}
143
144function my_register_custom_widget() {
145 register_widget( 'My_Custom_Widget' );
146}
147add_action( 'widgets_init', 'my_register_custom_widget' );

Поместите этот файл в wp-content/plugins/my-widget-plugin/, активируйте плагин и перетащите виджет в любой сайдбар через Внешний вид → Виджеты. Заполните поля, сохраните, и проверьте сайт.

Готовый код также доступен на GitHub: wpexplorer/my-widget-plugin, там же можно сравнить с оригинальной версией 2017 года и увидеть, что именно мы изменили.

⁉️🤔 Частые вопросы

Зачем выносить виджет в плагин, если можно добавить код в functions.php темы?

Код в functions.php привязан к активной теме. Сменили тему, виджет исчез. Плагин же работает независимо от темы. Плюс плагин можно активировать выборочно на разных сайтах, а код темы, нет. Если виджет решает бизнес-задачу (например, выводит форму подписки с конкретной версткой), его место в плагине.

Чем sanitize_text_field() лучше wp_strip_all_tags()?

sanitize_text_field() не только убирает HTML-теги, но и нормализует пробелы, удаляет невидимые управляющие символы и приводит строку к UTF-8. Это более полная очистка. wp_strip_all_tags() оставляет «сырой» текст со всеми пробельными артефактами. Для текстовых полей формы всегда выбирайте sanitize_text_field().

Почему вы заменили extract() на прямое обращение к массиву?

Функция extract() исключена из стандартов кодирования WordPress с версии 4.3. Она создает переменные с именами ключей массива в локальной области видимости, если ключ совпадет с существующей переменной, вы перезапишете ее и получите трудноуловимый баг. Прямое обращение $args['before_widget'] читаемо и безопасно.

Нужно ли поддерживать блочные виджеты (Gutenberg)?

Классический WP_Widget работает с блочными сайдбарами через обратную совместимость, WordPress сам оборачивает его в Legacy Widget Block. Этого достаточно для большинства сценариев. Если вы хотите создавать именно нативные блоки, смотрите Block Editor Handbook, там отдельный API. Но стартовать проще с WP_Widget: код короче, отладка быстрее, и он работает на всех версиях WordPress без плагинов-костылей.

Как отлаживать виджет, если он не появляется в админке?

Проверьте три вещи. Первое: хук widgets_init отработал? Добавьте error_log( 'Widget registered' ) в функцию регистрации и посмотрите логи. Второе: нет ли фатальной ошибки в конструкторе? Включите WP_DEBUG в wp-config.php. Третье: имя класса в register_widget() совпадает с именем класса-наследника WP_Widget? Опечатка в одной букве, и виджета нет.

Что дальше: от шаблона к своему виджету

Мы собрали работающий плагин с пятью типами полей. Это не конечный продукт, а каркас: берите его за основу и меняйте под свою задачу. Нужен виджет для формы подписки, замените текстовые поля на поля email и имени, добавьте вызов API рассылки в widget(). Нужен блок с акциями и баннерами, загружайте изображения через media uploader и выводите в верстке. Механика одна и та же: form() рисует поля, update() сохраняет, widget() выводит.

Начните с малого: скопируйте полный код выше, активируйте на тестовом сайте и поиграйте с настройками. Когда поймете, как данные текут от формы к фронтенду, добавляйте свои поля. А если виджет упал с белым экраном, возвращайтесь к шагу 2 и проверяйте конструктор.