Cómo escribir un plugin de WordPress desde cero
Crea un plugin de WordPress desde cero con hooks, activación y desactivación, página de ajustes, sanitización, escape y guardado seguro.
Un plugin de WordPress es, en su forma más simple, una carpeta dentro de wp-content/plugins que contiene un archivo PHP que comienza con un comentario de cabecera con el nombre del plugin. Guarda ese archivo y WordPress mostrará el plugin en la pantalla de Plugins.
Si has estado agregando fragmentos de código al functions.php de un tema, ya conoces el inconveniente: si cambias de tema, el código se va con él. Trasladar ese código a un plugin significa que la funcionalidad sobrevive a un cambio de tema, que es la razón principal por la que el comportamiento del sitio pertenece a los plugins y la presentación a los temas.
Este artículo construye un pequeño plugin de principio a fin: un aviso del sitio que se imprime en el pie de página, editable desde una página de ajustes que guarda los datos de forma segura. Cada bloque de código es un estado válido del plugin, así que puedes detenerte después de cualquier sección y activar lo que tengas hasta ese momento.
Puntos clave
- Un plugin es una carpeta en
wp-content/pluginscon un archivo PHP cuyo comentario de cabecera indica el nombre del plugin;Plugin Namees el único campo obligatorio de la cabecera. - El código de un plugin no hace nada hasta que enlazas una función a un hook con
add_action()oadd_filter(). - Un callback de filtro debe devolver el valor que recibe; si no devuelve nada, el valor filtrado queda vacío.
- Una página de ajustes segura combina cuatro elementos: una comprobación de capacidad, un nonce, saneamiento en la entrada y escapado en la salida.
register_activation_hook()sirve para los valores por defecto, la desactivación para la limpieza temporal, y la eliminación permanente de datos corresponde a la desinstalación.
Cómo crear el archivo y la cabecera de un plugin de WordPress
Crea la carpeta wp-content/plugins/site-notice/ y, dentro de ella, el archivo site-notice.php. WordPress construye la pantalla de Plugins leyendo los archivos PHP de la carpeta de plugins y seleccionando aquellos que comienzan con una cabecera de plugin. Cada archivo que incluya una cabecera cuenta como un plugin independiente, así que coloca la cabecera en un solo archivo.
<?php
/**
* Plugin Name: Site Notice
* Description: Shows a short notice in the site footer, editable from Settings.
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: OpenReplay Team
* License: GPL-2.0-or-later
* Text Domain: site-notice
*/
La página de requisitos de la cabecera enumera todos los campos reconocidos, incluidos Update URI y Requires Plugins. Los que usarás con más frecuencia:
| Campo | Qué hace WordPress con él | Obligatorio |
|---|---|---|
| Plugin Name | Lo muestra en la lista de Plugins | Sí |
| Description | Lo muestra debajo del nombre | No |
| Version | La muestra; determina las comparaciones de actualización | No |
| Requires at least / Requires PHP | Impide la activación en entornos antiguos | No |
| Author, License, Text Domain | Atribución, licencia y slug de traducción | No |
Guarda el archivo y el plugin aparecerá en la pantalla de Plugins. Se activa sin problemas y no hace nada.
¿Qué hacen los hooks de activación y desactivación?
register_activation_hook() ejecuta tu callback una sola vez, en el momento en que alguien activa el plugin, lo que lo convierte en un buen lugar para escribir los valores iniciales de tus opciones en la base de datos. register_deactivation_hook() sirve para descartar todo aquello que el plugin solo necesitaba mientras estaba en funcionamiento; una caché es el ejemplo habitual. Eliminar cosas de forma definitiva, incluidas opciones y tablas personalizadas, es tarea de la desinstalación, ya que a menudo se desactiva un plugin con la intención de volver a activarlo más adelante.
Añade debajo de la cabecera:
function orp_activate() {
add_option( 'orp_notice_text', 'Welcome to the site.' );
}
register_activation_hook( __FILE__, 'orp_activate' );
function orp_deactivate() {
delete_transient( 'orp_notice_cache' );
}
register_deactivation_hook( __FILE__, 'orp_deactivate' );
El primer argumento, __FILE__, apunta al archivo principal del plugin. Nuestro plugin todavía no almacena nada en caché, pero el callback de desactivación muestra la estructura: limpia los artefactos temporales y deja intacta la opción guardada. El prefijo orp_ en cada nombre de función y de opción evita colisiones con el núcleo y con otros plugins.
¿Por qué no se ejecuta el código de mi plugin?
El código de un archivo de plugin no se ejecuta por sí solo. WordPress solo ejecuta una función después de que la enlaces a un hook, y hasta entonces el archivo permanece inerte. add_action() recibe el nombre de un hook y un callable, con una prioridad opcional cuyo valor por defecto es 10.
function orp_print_notice() {
$notice = get_option( 'orp_notice_text' );
if ( $notice ) {
echo '<p class="orp-notice">' . esc_html( $notice ) . '</p>';
}
}
add_action( 'wp_footer', 'orp_print_notice' );
Cuando el tema dispara wp_footer, WordPress llama a todas las funciones enlazadas a ese hook, incluida la nuestra. Carga cualquier página del front-end y el aviso aparecerá justo encima de la etiqueta de cierre del body.
Añadir un filtro: modificar un valor y devolverlo
Una acción permite que tu función se ejecute en un punto concreto del ciclo de vida de WordPress; un filtro entrega un valor a tu función y espera recibir de vuelta el valor modificado. Un callback de filtro debe devolver un valor. Si no devuelve nada, PHP devuelve null, WordPress propaga ese null y aquello que estuvieras filtrando desaparece del sitio. Si olvidas el return en un callback de body_class, el elemento body pierde sus clases; si lo olvidas en the_content, todas las entradas se renderizan vacías.
function orp_body_class( $classes ) {
if ( get_option( 'orp_notice_text' ) ) {
$classes[] = 'orp-has-notice';
}
return $classes;
}
add_filter( 'body_class', 'orp_body_class' );
Esto recibe el array de clases del body, añade una cuando hay un aviso configurado y devuelve el array, de modo que los temas puedan aplicar estilos distintos a las páginas mientras el aviso esté activo.
¿Cómo se construye una página de ajustes en WordPress?
Una página de ajustes son tres registros: una página de menú en el hook admin_menu, y un ajuste junto con su sección y su campo en el hook admin_init, tal como expone el capítulo sobre la Settings API. add_options_page() coloca la página bajo Ajustes; register_setting() nombra la opción y, sobre todo, su sanitize_callback.
function orp_settings_menu() {
add_options_page( 'Site Notice', 'Site Notice', 'manage_options',
'orp-site-notice', 'orp_settings_page_html' );
}
add_action( 'admin_menu', 'orp_settings_menu' );
function orp_settings_init() {
register_setting( 'orp_settings', 'orp_notice_text', array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
) );
add_settings_section( 'orp_main', 'Notice', '__return_false', 'orp-site-notice' );
add_settings_field( 'orp_notice_text', 'Notice text',
'orp_notice_field_html', 'orp-site-notice', 'orp_main' );
}
add_action( 'admin_init', 'orp_settings_init' );
function orp_notice_field_html() {
$value = get_option( 'orp_notice_text', '' );
echo '<input type="text" name="orp_notice_text" value="'
. esc_attr( $value ) . '" class="regular-text">';
}
function orp_settings_page_html() {
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( 'orp_settings' );
do_settings_sections( 'orp-site-notice' );
submit_button( 'Save Notice' );
?>
</form>
</div>
<?php
}
El formulario envía los datos a options.php y el núcleo se encarga del guardado. Ten en cuenta que register_setting() también acepta un argumento default; nosotros establecemos el nuestro en el hook de activación, así que elige un único mecanismo y mantén la coherencia.
¿Cómo se gestiona de forma segura el valor enviado?
Una comprobación de capacidad responde a si este usuario tiene permiso para guardar los ajustes; un nonce responde a si este usuario realmente tenía la intención de enviar este formulario. Una página de ajustes que quede en el sitio de un cliente necesita ambos, y los nonces nunca deben sustituir a la comprobación de capacidad.
Aquí, la Settings API cubre la mayor parte: settings_fields() imprime el nonce y el núcleo lo verifica cuando el formulario se envía de vuelta, y el núcleo bloquea el guardado a menos que el usuario actual disponga de manage_options, la capacidad que wp-admin/options.php aplica por defecto a las páginas de ajustes. Si alguna vez escribes un manejador de formularios propio, imprime el nonce con wp_nonce_field() y verifícalo con check_admin_referer(). Añade la comprobación explícita al callback de la página, siguiendo el ejemplo del propio manual:
function orp_settings_page_html() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// ... form as above ...
}
Comprueba una capacidad, nunca un nombre de rol como administrator. Las dos capas restantes ya están implementadas: sanear a la entrada de la base de datos (sanitize_text_field como sanitize_callback) y escapar a la salida (esc_attr() en el campo, esc_html() en el pie de página). Ambas operaciones no son intercambiables, y omitir cualquiera de ellas es la forma en que una opción almacenada se convierte en un vector de inyección.
Actívalo y comprueba que funciona
Activa Site Notice en la pantalla de Plugins, carga el front-end y comprueba que aparece el aviso en el pie de página. Cambia el texto en Ajustes → Site Notice y recarga. Si no ocurre nada, repasa esta lista en orden:
- ¿El plugin está realmente activado, no solo presente?
- ¿Cada cadena pasada a
add_action()oadd_filter()coincide exactamente con el nombre de una función definida? - ¿Hay espacios en blanco o salida antes de
<?php? WordPress informa de “unexpected output” durante la activación cuando los hay. - Activa
WP_DEBUGenwp-config.php, conWP_DEBUG_LOGescribiendo los errores enwp-content/debug.log, y lee el error real de PHP en lugar de adivinar.
Ahora tienes un plugin con una opción por defecto, una acción, un filtro que devuelve su valor y una página de ajustes que guarda mediante un nonce, una comprobación de capacidad y un saneador. Esa es la base que merece la pena replicar para cada fragmento que siga viviendo en functions.php: trasládalo, ponle un prefijo, engánchalo a un hook y mantén intacto el par sanear-a-la-entrada, escapar-a-la-salida.
Preguntas frecuentes
¿Cuál es la diferencia entre wp_verify_nonce y check_admin_referer?
check_admin_referer() verifica tanto el nonce como el referente para formularios y URLs en las pantallas de administración, y detiene la petición con un 403 cuando la verificación falla. wp_verify_nonce() comprueba únicamente el nonce y devuelve un resultado que tú mismo gestionas, lo que resulta adecuado para manejadores Ajax y otros contextos personalizados. Ninguna de las dos sustituye a una comprobación de capacidad: los nonces confirman que el usuario tenía la intención de realizar la acción, current_user_can() confirma que el usuario tiene permiso para llevarla a cabo.
¿Qué ocurre con las opciones guardadas de un plugin cuando se desactiva?
Nada: la desactivación deja las opciones en la base de datos, de modo que los ajustes permanecen intactos cuando el usuario vuelve a activar el plugin. Un callback de desactivación debería eliminar únicamente elementos de vida corta, como transients o archivos en caché. La limpieza permanente corresponde a la desinstalación, implementada con register_uninstall_hook() o con un archivo uninstall.php en la carpeta del plugin. Si usas uninstall.php, debe comprobar que la constante WP_UNINSTALL_PLUGIN esté definida antes de eliminar nada.
¿Puede un plugin de WordPress tener más de un archivo PHP?
Sí, pero solo el archivo principal debe contener el comentario de cabecera del plugin. WordPress encuentra los plugins leyendo los archivos PHP del directorio de plugins en busca de esa cabecera, y cada archivo que la incluya aparece como un plugin independiente. Carga los archivos adicionales desde el archivo principal con require_once, y mantén las llamadas como register_activation_hook() apuntando al archivo principal del plugin, ya que su primer parámetro debe hacer referencia a ese archivo.
¿Debe establecerse una opción por defecto en register_setting o en el hook de activación?
Usa un solo mecanismo, no ambos. register_setting() acepta un argumento default que se devuelve cuando no existe ningún valor en la base de datos, mientras que add_option() en un hook de activación escribe una fila real una única vez durante la activación. El enfoque de la activación garantiza que el valor exista en cada petición, incluidas las del front-end; el default de register_setting() solo se aplica allí donde se haya ejecutado el código de registro. Mezclar ambos crea dos fuentes de verdad para la misma opción.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k