Skip to content

Todo para WordPress, el desarrollo web — y mucho más

🛠 Cómo crear un plugin de widget para WordPress: una guía paso a paso

🛠 Cómo crear un plugin de widget para WordPress: una guía paso a paso

💡 Cómo crear un plugin de widget en WordPress

  • Cree una carpeta my-widget-plugin dentro de wp-content/plugins/ y un archivo my-widget-plugin.php con la cabecera del plugin, luego actívelo en el panel de administración
  • Declare una clase que extienda WP_Widget y sobrescriba los métodos __construct(), form(), update() y widget()
  • Registre el widget con la función register_widget() en el hook widgets_init para que WordPress pueda verlo en la lista
  • En el método form(), renderice los campos de configuración; en update(), sanitice y guarde los datos mediante sanitize_text_field()
  • En el método widget(), emita el contenido en el front-end, escapando los valores con esc_html() y wp_kses_post()

Paso 1: crear el andamiaje del plugin

Un widget en WordPress no es solo una línea en functions.php. Si quiere que la configuración persista, que el formulario de administración funcione y que el widget sobreviva a la próxima actualización del tema, necesita empaquetarlo como un plugin. Esto aísla el código y hace que el widget sea independiente de cualquier cambio de tema.

Comience con una carpeta vacía. Navegue a wp-content/plugins/ y cree un directorio llamado my-widget-plugin. Dentro de él, cree un archivo llamado my-widget-plugin.php. Este es el archivo que WordPress leerá primero al activarse. Abra el archivo y añada la cabecera estándar del plugin:

1<?php
2/*
3Plugin Name: My Widget Plugin
4Plugin URI: https://www.wpexplorer.com/create-widget-plugin-wordpress/
5Description: Adds a customizable widget with text, textarea, checkbox, and dropdown.
6Version: 1.0
7Author: AJ Clarke
8Author URI: https://www.wpexplorer.com/
9License: GPL2
10*/

Guarde el archivo. Ahora vaya al panel de administración de WordPress → Plugins. Si el plugin aparece en la lista, el andamiaje está listo. Haga clic en «Activar». Todavía no hace nada útil; solo hemos anunciado su existencia. Pero WordPress ya sabe que este plugin existe y está listo para ejecutar su código. Este es un principio importante: primero el registro, después la lógica.

En el video anterior se explica cómo funciona el plugin internamente, qué hooks se disparan en la activación y cómo WordPress localiza su archivo. Ahora pasemos a la parte más interesante: la clase del widget.

Paso 2: registrar el widget mediante WP_Widget

WordPress proporciona una clase integrada llamada WP_Widget; ha sido parte del núcleo desde la versión 2.8 y sigue siendo la base para cualquier widget personalizado. No necesita escribir la lógica de guardado, la generación de campos ni el registro desde cero: solo extienda la clase y sobrescriba cuatro métodos.

Añada este código a su my-widget-plugin.php justo después de la cabecera del plugin, antes del cierre ?>:

1// Widget class
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 /* ... admin form ... */
16 }
17
18 public function update( $new_instance, $old_instance ) {
19 /* ... saving settings ... */
20 }
21
22 public function widget( $args, $instance ) {
23 /* ... frontend output ... */
24 }
25}
26
27// Widget registration
28function my_register_custom_widget() {
29 register_widget( 'My_Custom_Widget' );
30}
31add_action( 'widgets_init', 'my_register_custom_widget' );

Analicemos lo que está sucediendo aquí. La clase My_Custom_Widget extiende WP_Widget, lo que le proporciona métodos ya preparados como get_field_id() y get_field_name() para generar atributos de campos de formulario. En el constructor pasamos tres cosas a la clase padre: un ID de widget único (caracteres latinos en minúscula, sin espacios, my_custom_widget), su nombre legible (la función __() lo hace traducible) y un array de opciones. El parámetro customize_selective_refresh => true permite que el widget se refresque en el Personalizador sin recargar toda la página, un pequeño detalle que ahorra mucha frustración durante la configuración.

La función my_register_custom_widget() llama a register_widget() en el hook widgets_init. Así es como WordPress se entera de su widget. Sin esta línea, no aparecerá nada en el panel de administración.

Ahora llenemos los métodos form(), update() y widget() con lógica real.

Paso 3: crear el formulario del widget en el panel de administración

El formulario es lo que ve el administrador al arrastrar el widget a una barra lateral. Consiste en campos: entradas de texto, listas desplegables, casillas de verificación. Cada campo debe poder guardar su valor y mostrar el actual al reabrirse.

3.1. La función form() y los campos de entrada

Añada este código al método form() de su clase. Crea cinco campos: un título, una entrada de texto, un área de texto, una casilla de verificación y una lista desplegable.

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( 'Widget Title', '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:', '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( 'Textarea:', '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( 'Show additional block', 'text_domain' ); ?>
60 </label>
61 </p>
62
63 <p>
64 <label for="<?php echo $this->get_field_id( 'select' ); ?>">
65 <?php _e( 'Display variant', '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 '' => __( '— Select —', 'text_domain' ),
73 'option_1' => __( 'Option 1', 'text_domain' ),
74 'option_2' => __( 'Option 2', 'text_domain' ),
75 'option_3' => __( 'Option 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 }

Preste atención a dos cosas. Primero, reemplazamos la función obsoleta extract() por acceso directo al array. extract() ha sido excluida de los estándares de codificación de WordPress durante mucho tiempo; crea variables con los nombres de las claves del array, lo cual es inseguro y dificulta la depuración. Segundo, cada valor de salida se pasa por esc_attr(), wp_kses_post() o esc_html(). Esto no es paranoia: los datos de la base de datos pueden venir de cualquier parte, y la sanitización es obligatoria.

3.2. La función update() para guardar

El método update() se llama cuando se hace clic en el botón «Guardar» en el formulario del widget. Su trabajo es validar cada campo y devolver un array sanitizado para escribirlo en la base de datos.

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}

Aquí usamos sanitize_text_field() en lugar de wp_strip_all_tags() porque también normaliza los espacios en blanco, elimina caracteres de control invisibles y convierte la cadena a UTF-8 seguro. Esta es una limpieza más exhaustiva. wp_strip_all_tags() deja el texto «en bruto» con todos los artefactos de espacio en blanco intactos. Para los campos de formulario de texto, elija siempre sanitize_text_field().

El área de texto conserva wp_kses_post(): permite HTML básico (enlaces, texto en negrita, listas) pero elimina los scripts. La casilla de verificación devuelve 1 o false, lo cual es legible e inequívoco en la base de datos.

Paso 4: renderizar el widget en el front-end

La función widget() es lo que ve el visitante. Recibe dos parámetros: $args (el envoltorio del widget, etiquetas antes y después del título y la barra lateral) y $instance (la configuración guardada para esta instancia en particular).

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__( 'Additional block activated', 'text_domain' ) . '</p>';
32 }
33
34 echo '</div>';
35
36 echo $args['after_widget'];
37}

El punto clave aquí: reemplazamos extract( $args ) por acceso directo mediante $args['before_widget']. La razón es la misma: extract() está obsoleto y es inseguro. También envolvimos la salida en esc_html() donde se espera texto plano (el título, la cadena de texto, el valor del select). El área de texto se renderiza a través de wp_kses_post(), por lo que si el administrador insertó un enlace o texto en negrita, se conservarán.

La clase CSS wp_widget_plugin_box le permite estilizar el bloque desde la hoja de estilos del tema. Siéntase libre de renombrarla, solo asegúrese de que la clase sea única y no entre en conflicto con las clases del tema.

Código completo del plugin

Vamos a reunirlo todo. Aquí está el archivo completo my-widget-plugin.php, listo para copiar y activar:

1<?php
2/*
3Plugin Name: My Widget Plugin
4Plugin URI: https://www.wpexplorer.com/create-widget-plugin-wordpress/
5Description: Adds a customizable widget with text, textarea, checkbox, and dropdown.
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( 'Widget Title', '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:', '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( 'Textarea:', '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( 'Show additional block', 'text_domain' ); ?>
68 </label>
69 </p>
70 <p>
71 <label for="<?php echo $this->get_field_id( 'select' ); ?>">
72 <?php _e( 'Display variant', '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 '' => __( '— Select —', 'text_domain' ),
79 'option_1' => __( 'Option 1', 'text_domain' ),
80 'option_2' => __( 'Option 2', 'text_domain' ),
81 'option_3' => __( 'Option 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__( 'Additional block activated', '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' );

Coloque este archivo en wp-content/plugins/my-widget-plugin/, active el plugin y arrastre el widget a cualquier barra lateral a través de Apariencia → Widgets. Rellene los campos, guarde y compruebe su sitio.

El código terminado también está disponible en GitHub: wpexplorer/my-widget-plugin, donde puede compararlo con la versión original de 2017 y ver exactamente qué cambiamos.

⁉️🤔 Preguntas frecuentes

¿Por qué poner un widget en un plugin cuando se puede simplemente añadir código al functions.php del tema?

El código en functions.php está vinculado al tema activo. Cambie de tema y el widget desaparece. Un plugin funciona independientemente del tema. Además, un plugin puede activarse selectivamente en diferentes sitios, mientras que el código del tema no. Si el widget resuelve una necesidad de negocio (por ejemplo, mostrar un formulario de suscripción con un diseño específico), pertenece a un plugin.

¿Por qué es mejor sanitize_text_field() que wp_strip_all_tags()?

sanitize_text_field() no solo elimina las etiquetas HTML; también normaliza los espacios en blanco, elimina caracteres de control invisibles y convierte la cadena a UTF-8. Esta es una limpieza más exhaustiva. wp_strip_all_tags() deja el texto «en bruto» con todos los artefactos de espacio en blanco intactos. Para los campos de formulario de texto, elija siempre sanitize_text_field().

¿Por qué reemplazó extract() por acceso directo al array?

La función extract() ha sido excluida de los estándares de codificación de WordPress desde la versión 4.3. Crea variables con los nombres de las claves del array en el ámbito local; si una clave coincide con una variable existente, la sobrescribirá y terminará con un error difícil de encontrar. El acceso directo como $args['before_widget'] es legible y seguro.

¿Necesito soportar widgets de bloque (Gutenberg)?

El WP_Widget clásico funciona con barras laterales basadas en bloques mediante retrocompatibilidad: WordPress lo envuelve automáticamente en un Bloque de Widget Heredado. Esto es suficiente para la mayoría de los escenarios. Si quiere crear bloques verdaderamente nativos, consulte el Manual del Editor de Bloques, que tiene una API separada. Pero empezar con WP_Widget es más fácil: el código es más corto, la depuración es más rápida y funciona en todas las versiones de WordPress sin plugins de compatibilidad adicionales.

¿Cómo depuro un widget que no aparece en el panel de administración?

Compruebe tres cosas. Primero: ¿se disparó el hook widgets_init? Añada error_log( 'Widget registered' ) a la función de registro y revise los registros. Segundo: ¿hay un error fatal en el constructor? Active WP_DEBUG en wp-config.php. Tercero: ¿coincide el nombre de la clase en register_widget() con el nombre de la clase que extiende WP_Widget? Un solo error tipográfico y el widget no aparecerá.

¿Qué sigue? De la plantilla a su propio widget

Hemos construido un plugin funcional con cinco tipos de campo. Esto no es un producto terminado; es un andamiaje. Tómelo como su base y adáptelo a sus necesidades. ¿Necesita un widget de formulario de suscripción? Reemplace los campos de texto por campos de correo electrónico y nombre, y añada una llamada a la API del servicio de mailing dentro de widget(). ¿Necesita un bloque con promociones y banners? Cargue imágenes a través del cargador de medios y renderícelas en su marcado. La mecánica es siempre la misma: form() dibuja los campos, update() los guarda, widget() renderiza la salida.

Empiece poco a poco: copie el código completo de arriba, actívelo en un sitio de pruebas y experimente con la configuración. Una vez que entienda cómo fluyen los datos del formulario al front-end, empiece a añadir sus propios campos. Y si el widget falla con una pantalla en blanco, vuelva al paso 2 y revise el constructor.