La arquitectura de un plugin empresarial moderno en WordPress 6.7–7.0+ («Armstrong») dista enormemente de los archivos procedimentales monolíticos de la era de PHP 5/7. El estándar industrial para la construcción de plugins (Enterprise Plugin Boilerplate) se fundamenta en cinco invariantes arquitectónicos:
Requires Plugins:): Mecanismo nativo de WordPress (introducido en WP 6.5 y estandarizado en WP 7.0), que permite especificar en la cabecera del archivo principal la lista de plugins obligatorios (Requires Plugins: woocommerce, advanced-custom-fields). El núcleo bloquea automáticamente la activación del plugin hijo y la desactivación del padre sin requerir comprobaciones manuales con is_plugin_active().PHP-Scoper / Strauss): La base de código se estructura en el directorio src/ conforme al estándar Composer PSR-4, mientras que todas las librerías de terceros en vendor/ se compilan con un prefijo de espacio de nombres único (AgenticLab\Vendor\...), eliminando por completo colisiones fatales de versiones entre plugins distintos.dbDelta(): La creación y actualización de tablas personalizadas se ejecuta estrictamente durante la activación o actualización de versión del plugin (SCHEMA_VERSION en wp_options), respetando las rigurosas reglas de sintaxis SQL requeridas por el analizador de dbDelta.Activation $\neq$ Deactivation $\neq$ Uninstall):
register_activation_hook: verificación del entorno, migración de tablas con dbDelta, asignación de permisos a roles y regeneración suave de enlaces permanentes con flush_rewrite_rules(false).register_deactivation_hook: cancelación de tareas de Action Scheduler / WP-Cron y purga de transitorios temporales (¡sin eliminar jamás datos de usuario!).uninstall.php: destructor aislado, ejecutado en un entorno limpio al eliminar por completo el plugin (defined('WP_UNINSTALL_PLUGIN')) para purgar tablas personalizadas, opciones y metadatos.El ciclo de vida de un plugin debe entenderse como una máquina de estados finitos determinista entre los estados de instalación, ejecución regular y desinstalación, donde cada fase posee límites estrictos sobre los efectos secundarios permitidos.
Requires Plugins Header PHP-Scoper / Strauss dbDelta Idempotent Activation Isolated uninstall.php[1. Instalación ZIP / Git Deploy]
│
▼
[2. Verificación de Cabeceras por el Núcleo WP 7.0+]
├── Requires PHP: 8.4
├── Requires at least: 6.7
└── Requires Plugins: woocommerce ──► Si falta: ¡Bloqueo de activación en UI!
│
▼
[3. Fase de Activación: register_activation_hook]
├── Ejecución de migraciones de tablas vía dbDelta()
├── Guardado de versión del esquema: update_option('agentic_db_version', '2.1.0')
└── flush_rewrite_rules(false)
│
▼
[4. Operación Regular: hook plugins_loaded]
├── Carga de vendor/scoper-autoload.php (PSR-4)
└── Verificación de versión de BD (Auto-Upgrade en despliegues Git/FTP)
│
▼
[5. Desactivación: register_deactivation_hook]
├── wp_clear_scheduled_hook('agentic_cron_sync')
└── ¡Los datos y tablas del usuario SE PRESERVAN!
│
▼
[6. Eliminación Completa: uninstall.php]
├── Verificación if (!defined('WP_UNINSTALL_PLUGIN')) exit;
└── DROP TABLE + delete_option + purga de cachéValidación de cabeceras Requires PHP, Requires at least y Requires Plugins por el núcleo antes de ejecutar código.
Carga de clases desde src/ y dependencias aisladas desde build/vendor/ sin contaminar el ámbito global.
Análisis del esquema actual de MySQL, creación o adición de columnas/índices vía dbDelta y fijación de versión.
Detención de tareas en segundo plano WP-Cron y purga de caché transitoria preservando íntegros los datos del cliente.
Ejecución aislada del script de purga al borrar el plugin: eliminación de tablas personalizadas, opciones y metadatos.
PHP-Scoper): Aislar librerías (Guzzle, Symfony, Monolog, Carbon) en un sub-espacio de nombres propio permite que tu plugin coexista de forma robusta con decenas de otros plugins en cualquier versión.register_activation_hook no se dispara, verificar la constante SCHEMA_VERSION en plugins_loaded ejecuta automáticamente dbDelta() al detectar nuevo código.{$wpdb->prefix}agentic_telemetry con clave primaria e índices B-Tree optimizados.is_multisite()): Inicialización y limpieza correcta de tablas para cada sub-sitio de la red mediante get_sites(['fields' => 'ids']) y switch_to_blog().Comparemos la organización obsoleta de un plugin frente a la arquitectura modular moderna de WordPress 7.0+:
dbDelta()#La función dbDelta() en wp-admin/includes/upgrade.php utiliza expresiones regulares para analizar la cadena SQL y compararla con la salida de DESCRIBE table_name. Infringir cualquiera de estas reglas provocará que la migración sea ignorada silenciosamente:
PRIMARY KEY y el paréntesis de apertura ( DEBE haber exactamente dos espacios: PRIMARY KEY (id).KEY en lugar del sinónimo INDEX, especificando siempre el nombre del índice: KEY status_created_idx (status, created_at).IF NOT EXISTS dentro de CREATE TABLE ni comillas invertidas (backticks `) alrededor de los nombres de columnas.A continuación se presenta la estructura canónica del archivo principal y desinstalador en PHP 8.4+ con cargador PSR-4 integrado, migrador versionado dbDelta y soporte completo para WordPress Multisite.
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Core Infrastructure Plugin
* Plugin URI: https://agenticlab.dev/lab
* Description: Plugin de infraestructura modular con migraciones idempotentes dbDelta y arquitectura PSR-4.
* Version: 2.1.0
* Requires at least: 6.7
* Requires PHP: 8.4
* Requires Plugins:
* Author: Artur Yusupov (Fullstack Agentic Lab)
* License: GPL-2.0-or-later
* Text Domain: agentic-lab-core
*/
declare(strict_types=1);
namespace AgenticLab\Plugin;
if (!defined('ABSPATH')) {
exit;
}
final class PluginBootstrap
{
public const VERSION = '2.1.0';
public const DB_VERSION_OPTION = 'agentic_lab_db_schema_version';
public const CRON_HOOK = 'agentic_lab_hourly_telemetry_cleanup';
public static function register(): void
{
// 1. Registro del autocargador de clases PSR-4
spl_autoload_register(self::autoloadClass(...));
// 2. Hooks del ciclo de vida para activación y desactivación
register_activation_hook(__FILE__, self::onActivate(...));
register_deactivation_hook(__FILE__, self::onDeactivate(...));
// 3. Verificación de cambios de esquema en despliegues Git/CI-CD (donde activation_hook no se dispara)
add_action('plugins_loaded', self::maybeUpgradeDatabaseSchema(...), 5);
}
public static function autoloadClass(string $class): void
{
$prefix = 'AgenticLab\\Plugin\\';
if (!str_starts_with($class, $prefix)) {
return;
}
$relativeClass = substr($class, strlen($prefix));
$file = plugin_dir_path(__FILE__) . 'src/' . str_replace('\\', '/', $relativeClass) . '.php';
if (is_readable($file)) {
require_once $file;
}
}
public static function onActivate(bool $networkWide = false): void
{
if (is_multisite() && $networkWide) {
/** @var list<int> $siteIds */
$siteIds = get_sites(['fields' => 'ids']);
foreach ($siteIds as $siteId) {
switch_to_blog((int) $siteId);
self::installSingleSite();
restore_current_blog();
}
return;
}
self::installSingleSite();
}
private static function installSingleSite(): void
{
self::runDbDeltaMigration();
if (!wp_next_scheduled(self::CRON_HOOK)) {
wp_schedule_event(time(), 'hourly', self::CRON_HOOK);
}
flush_rewrite_rules(false);
}
public static function maybeUpgradeDatabaseSchema(): void
{
$installedVersion = (string) get_option(self::DB_VERSION_OPTION, '0.0.0');
if (version_compare($installedVersion, self::VERSION, '<')) {
self::runDbDeltaMigration();
}
}
/**
* Migración idempotente de tabla personalizada cumpliendo las reglas estrictas de dbDelta().
*/
public static function runDbDeltaMigration(): void
{
global $wpdb;
$tableName = $wpdb->prefix . 'agentic_telemetry_logs';
$charsetCollate = $wpdb->get_charset_collate();
// IMPORTANTE: 2 espacios después de PRIMARY KEY y sin backticks para un diff correcto en dbDelta.
$sql = "CREATE TABLE {$tableName} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
event_type varchar(64) NOT NULL,
payload longtext NOT NULL,
execution_ms float unsigned NOT NULL DEFAULT 0,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY event_created_idx (event_type, created_at)
) {$charsetCollate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
update_option(self::DB_VERSION_OPTION, self::VERSION, true);
}
/**
* Desactivación segura: cancela tareas WP-Cron sin eliminar tablas ni configuración de usuario.
*/
public static function onDeactivate(): void
{
wp_clear_scheduled_hook(self::CRON_HOOK);
flush_rewrite_rules(false);
}
}
PluginBootstrap::register();
/*
* ============================================================================
* Archivo aislado de desinstalación: wp-content/plugins/agentic-lab-core/uninstall.php
* (Invocado por el núcleo automáticamente solo al borrar por completo el plugin)
* ============================================================================
*
* <?php
* declare(strict_types=1);
*
* if (!defined('WP_UNINSTALL_PLUGIN')) {
* exit;
* }
*
* global $wpdb;
* $tableName = $wpdb->prefix . 'agentic_telemetry_logs';
* $wpdb->query("DROP TABLE IF EXISTS {$tableName}");
*
* delete_option('agentic_lab_db_schema_version');
* wp_cache_flush();
*/En qué consiste el error: Ejecutar DROP TABLE o delete_option() dentro del hook de desactivación del plugin (register_deactivation_hook).
Por qué lo cometen los desarrolladores: Confusión fundamental entre «desactivar temporalmente un plugin» y «desinstalar el plugin del servidor».
Qué ocurre en producción: El administrador desactiva el plugin durante 30 segundos para aislar un conflicto o realizar una actualización y pierde de forma irreversible toda la base de datos de pedidos, ajustes y registros.
La solución correcta: En register_deactivation_hook solo se permite cancelar eventos programados (wp_clear_scheduled_hook) y purgar transitorios. La destrucción de datos debe relegarse con exclusividad al archivo uninstall.php.
En qué consiste el error: Colocar la invocación de dbDelta() exclusivamente en el callback de register_activation_hook(__FILE__, ...).
Por qué lo cometen los desarrolladores: La suposición errónea de que al actualizar el plugin a una nueva versión el hook de activación volverá a ejecutarse.
Qué ocurre en producción: Durante actualizaciones desatendidas vía Git, Composer, WP-CLI o el repositorio de WordPress, el núcleo NO ejecuta register_activation_hook. El nuevo código PHP intentará consultar columnas que aún no existen en MySQL, desencadenando fallos fatales en producción.
La solución correcta: Almacenar la versión del esquema en wp_options (agentic_lab_db_schema_version) y compararla con la constante self::VERSION en el hook plugins_loaded, disparando dbDelta() de forma automática ante cualquier discrepancia.
En qué consiste el error: Utilizar CREATE TABLE IF NOT EXISTS, un solo espacio en PRIMARY KEY (id) o la palabra clave INDEX en vez de KEY al enviar sentencias a dbDelta().
Por qué lo cometen los desarrolladores: Costumbre de escribir sintaxis MySQL estándar, válida para $wpdb->query(), pero incompatible con las expresiones regulares internas del analizador de WordPress.
Qué ocurre en producción: En la instalación inicial la tabla puede crearse, pero en futuras actualizaciones con nuevas columnas, dbDelta() no logrará calcular el diff estructural e ignorará por completo la adición de nuevos campos e índices.
La solución correcta: Respetar estrictamente el formato exigido por dbDelta: dos espacios exactos en PRIMARY KEY (id), palabra clave KEY para índices, cada campo en una línea separada y omitir IF NOT EXISTS.
En qué consiste el error: Escribir consultas DROP TABLE en uninstall.php sin verificar if (!defined('WP_UNINSTALL_PLUGIN')) exit; en la primera línea ejecutable.
Por qué lo cometen los desarrolladores: Olvidar que uninstall.php reside en una ruta públicamente accesible del servidor web (/wp-content/plugins/my-plugin/uninstall.php).
Qué ocurre en producción: Aunque sin cargar WordPress el objeto $wpdb no existirá (produciendo un Fatal Error que expone la ruta completa del sistema), configuraciones atípicas o scripts externos podrían provocar la ejecución indebida de operaciones destructivas ante peticiones HTTP maliciosas.
La solución correcta: La primera instrucción de uninstall.php debe ser siempre if (!defined('WP_UNINSTALL_PLUGIN')) { exit; }.
El núcleo de WordPress 7.0+ analiza las cabeceras Requires PHP: 8.4 y Requires Plugins antes de habilitar la activación.
Previene fallos fatales por incompatibilidad de entorno antes de ejecutar una sola línea de código.
Creación de tablas vía dbDelta(), programación de tareas WP-Cron y vaciado suave de reglas de reescritura.
En activaciones para toda la red (Multisite), el proceso itera por cada sitio mediante switch_to_blog().
Comparación de la versión persistida en wp_options frente a la constante VERSION del código. Ejecuta dbDelta() si hay cambios.
Asegura la actualización de tablas en despliegues con Git y CI/CD sin requerir reactivaciones manuales.
Cancelación de eventos recurrentes mediante wp_clear_scheduled_hook() y purga de cachés transitorias.
Todas las tablas de base de datos y configuraciones del cliente permanecen completamente intactas.
Validación de la constante WP_UNINSTALL_PLUGIN y purga definitiva de tablas, opciones y metadatos del plugin.
Se ejecuta única y exclusivamente cuando el administrador elimina explícitamente un plugin inactivo.