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 і перевіряйте конструктор.