
🛠 Как создать плагин виджетов для 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 /* 3 Plugin Name: My Widget Plugin 4 Plugin URI: https://www.wpexplorer.com/create-widget-plugin-wordpress/ 5 Description: Добавляет настраиваемый виджет с текстом, текстовой областью, чекбоксом и выпадающим списком. 6 Version: 1.0 7 Author: AJ Clarke 8 Author URI: https://www.wpexplorer.com/ 9 License: GPL2 10 */
Сохраните. Теперь зайдите в админку WordPress → Плагины. Если плагин появился в списке, каркас готов. Нажмите «Активировать». Пока он не делает ничего полезного, мы только объявили о его существовании. Но WordPress уже знает, что такой плагин есть, и готов выполнять его код. Это важный принцип: сначала регистрация, потом логика.
Как устроен плагин изнутри, какие хуки срабатывают при активации и как WordPress находит ваш файл, смотрите в видео выше. А мы переходим к самому интересному: классу виджета.
Шаг 2: регистрируем виджет через WP_Widget
WordPress предоставляет готовый класс WP_Widget, он находится в ядре с версии 2.8 и с тех пор остается основой для любых кастомных виджетов. Вам не нужно писать логику сохранения, генерации полей и регистрации с нуля, достаточно унаследоваться и переопределить четыре метода.
Добавьте этот код в ваш my-widget-plugin.php сразу после заголовка плагина, перед закрывающим ?>:
1 // Класс виджета 2 class 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 // Регистрация виджета 28 function my_register_custom_widget() { 29 register_widget( 'My_Custom_Widget' ); 30 } 31 add_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() вашего класса. Он создает пять полей: заголовок, текстовая строка, текстовая область, чекбокс и выпадающий список.
1 public 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() вызывается при нажатии кнопки «Сохранить» в форме виджета. Его задача, проверить каждое поле и вернуть очищенный массив для записи в базу.
1 public 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 (сохраненные настройки конкретного экземпляра).
1 public 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 /* 3 Plugin Name: My Widget Plugin 4 Plugin URI: https://www.wpexplorer.com/create-widget-plugin-wordpress/ 5 Description: Добавляет настраиваемый виджет с текстом, текстовой областью, чекбоксом и выпадающим списком. 6 Version: 1.0 7 Author: AJ Clarke 8 Author URI: https://www.wpexplorer.com/ 9 License: GPL2 10 */ 11 12 class 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 144 function my_register_custom_widget() { 145 register_widget( 'My_Custom_Widget' ); 146 } 147 add_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 и проверяйте конструктор.



