BEAR - WooCommerce Bulk Editor and Products Manager Professional

Conflictos entre plugins de WordPress y un admin de WooCommerce lento: aislar plugins durante la edición masiva

Un conflicto entre plugins de WordPress rara vez se anuncia. En una tienda con sesenta plugins activos, el síntoma es un admin de WooCommerce lento y una operación masiva que se comporta de forma distinta a una instalación limpia: se atasca, a veces se detiene a medio camino, ocasionalmente escribe parte de los productos y da un error en el resto. El editor masivo rara vez es la causa. Cada plugin del sitio engancha sus propios callbacks a save_post y woocommerce_update_product, y cuando BEAR guarda tres mil productos, todos esos callbacks se ejecutan tres mil veces cada uno. Un maquetador visual regenerando su caché, un plugin de SEO recalculando puntuaciones, un plugin de búsqueda reindexando, un plugin de sincronización llamando a una API externa: ninguno de ellos se escribió pensando en una escritura masiva.

Tener demasiados plugins en un sitio de WordPress no es un problema que se resuelva desactivándolos, porque los necesitas el resto del día. Es un problema que se resuelve petición a petición.

WordPress tiene un único punto donde esto se puede controlar, y está antes de lo que la mayoría espera.

Por qué un conflicto entre plugins de WordPress no se puede arreglar desde functions.php

El consejo habitual ante un conflicto entre plugins de WordPress es desactivarlos uno a uno hasta que el síntoma desaparezca. Eso encuentra al culpable y te deja con una decisión que no quieres tomar, porque el plugin que tienes que mantener desactivado es también un plugin que la tienda necesita. WordPress carga los plugins antes de cargar el tema. Para cuando se ejecuta functions.php, ya se ha incluido cada archivo de plugin y ya está registrado cada hook. Quitar callbacks en ese punto significa perseguirlos uno a uno con remove_action, y siempre se te escapará alguno: los closures no se pueden quitar por nombre, las prioridades varían, y un plugin que registra sus hooks de forma perezosa los volverá a añadir después de que termines. El resultado es un plugin medio desactivado, que es peor que uno completamente cargado.

Hay un hook que se ejecuta antes de que se carguen los plugins. WordPress lee la lista de plugins activos de la opción active_plugins e incluye cada archivo de esa lista. La opción pasa primero por el filtro option_active_plugins. Devuelve ahí un array más corto y los demás archivos de plugin nunca llegan a leerse. No desactivados, no silenciados: jamás cargados.

El código que debe ejecutarse antes de que se carguen los plugins no puede vivir dentro de un plugin. Vive en wp-content/mu-plugins/. Los plugins must-use de WordPress se cargan antes que cualquier cosa de la lista de plugins normal, no se pueden desactivar desde el admin, y cada archivo PHP colocado directamente en esa carpeta se ejecuta automáticamente. Eso es lo bastante temprano para que el filtro anterior siga siendo útil.

Los plugins must-use de WordPress se cargan primero

Guarda esto como wp-content/mu-plugins/bear-isolation.php. Si la carpeta de mu-plugins de WordPress todavía no existe en tu sitio, créala: es un directorio normal dentro de wp-content, y cada archivo PHP que haya en ella se carga automáticamente. Después no hay nada que activar.

<?php
/**
 * Plugin Name: BEAR Plugin Isolation
 * Description: Loads a reduced set of plugins during BEAR bulk editor and MCP requests.
 * Version: 1.0
 *
 * Place this file into wp-content/mu-plugins/
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Mode:
 *   'disable'   - load everything EXCEPT the plugins listed below. Safer, start here.
 *   'keep_only' - load ONLY the plugins listed below. Fastest, needs testing.
 */
define( 'BEAR_ISOLATION_MODE', 'disable' );

/**
 * The list the mode above applies to. Paths are exactly as they appear
 * in the active_plugins option: folder/file.php
 */
function bear_isolation_list() {

	$list = array(
		// 'elementor/elementor.php',
		// 'js_composer/js_composer.php',
		// 'contact-form-7/wp-contact-form-7.php',
		// 'wp-smushit/wp-smush.php',
	);

	return apply_filters( 'bear_isolation_list', $list );
}

/**
 * Plugins that stay loaded whatever the mode and the list say.
 * Without WooCommerce and BEAR itself the request cannot work at all.
 */
function bear_isolation_always_keep() {

	$keep = array(
		'woocommerce/woocommerce.php',
		'woo-bulk-editor/index.php',   // same folder for the free and the paid build
	);

	return apply_filters( 'bear_isolation_always_keep', $keep );
}

/**
 * Is this a request the isolation should apply to?
 *
 * REST routing has not run yet at this point, so the raw URI is the only
 * signal available for the MCP endpoint.
 */
function bear_isolation_is_target_request() {

	$uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : '';

	// The MCP endpoint: pretty permalinks and the plain query form.
	if ( strpos( $uri, '/woobe/v1/mcp' ) !== false
		|| strpos( $uri, 'rest_route=/woobe/v1/mcp' ) !== false ) {
		return true;
	}

	// The bulk editor admin page.
	if ( isset( $_GET['page'] ) && strpos( (string) $_GET['page'], 'woobe' ) === 0 ) {
		return true;
	}

	// The editor's own ajax calls: woobe_get_products, woobe_bulk_edit and the rest.
	if ( isset( $_REQUEST['action'] ) && strpos( (string) $_REQUEST['action'], 'woobe' ) === 0 ) {
		return true;
	}

	return false;
}

/**
 * Regular single-site activation list: values are plugin paths.
 */
function bear_isolation_filter_plugins( $plugins ) {

	if ( ! is_array( $plugins ) || ! bear_isolation_is_target_request() ) {
		return $plugins;
	}

	$keep = bear_isolation_always_keep();
	$list = bear_isolation_list();

	if ( BEAR_ISOLATION_MODE === 'keep_only' ) {
		$allowed = array_merge( $keep, $list );
		return array_values( array_intersect( $plugins, $allowed ) );
	}

	// 'disable': drop the listed ones, but never the ones that must stay.
	$drop = array_diff( $list, $keep );
	return array_values( array_diff( $plugins, $drop ) );
}

add_filter( 'option_active_plugins', 'bear_isolation_filter_plugins', 1 );

/**
 * Multisite: network-activated plugins live in another option, and there the
 * plugin path is the KEY, not the value. Hence the _key variants.
 */
function bear_isolation_filter_network_plugins( $plugins ) {

	if ( ! is_array( $plugins ) || ! bear_isolation_is_target_request() ) {
		return $plugins;
	}

	$keep = bear_isolation_always_keep();
	$list = bear_isolation_list();

	if ( BEAR_ISOLATION_MODE === 'keep_only' ) {
		$allowed = array_flip( array_merge( $keep, $list ) );
		return array_intersect_key( $plugins, $allowed );
	}

	$drop = array_flip( array_diff( $list, $keep ) );
	return array_diff_key( $plugins, $drop );
}

add_filter( 'site_option_active_sitewide_plugins', 'bear_isolation_filter_network_plugins', 1 );

Los dos modos

disable es el que conviene usar para empezar. Todo se carga como de costumbre excepto los plugins que nombres. Añade los pesados que no tienen nada que hacer en una escritura de productos: maquetadores visuales, sliders, galerías, plugins de formularios, analítica, widgets de chat, optimizadores de imágenes, plugins de copia de seguridad. Cada uno que añadas es un conjunto menos de callbacks ejecutándose por producto.

keep_only es lo contrario y el más rápido de los dos: no se carga nada salvo WooCommerce, BEAR y lo que listes. En un sitio con sesenta plugins, esto convierte una escritura masiva en algo parecido a una instalación limpia. También quita plugins que puedas necesitar de verdad durante la escritura, así que trata la lista como algo que se construye probando, no adivinando.

Ambos modos protegen WooCommerce y BEAR mediante bear_isolation_always_keep(), así que un error tipográfico en la lista no puede romper la petición. La carpeta del plugin es woo-bulk-editor tanto para la versión gratuita como para la de pago, así que la ruta de arriba es la misma sea cual sea la que uses. Si no estás seguro de la ruta de algún otro plugin, es visible en Plugins en wp-admin, o en la opción active_plugins.

Qué cubre el aislamiento

Tres tipos de petición, y puedes acotarlos editando bear_isolation_is_target_request():

  • El endpoint MCP, /wp-json/woobe/v1/mcp y su forma ?rest_route=. Aquí es donde un asistente de IA escribe productos, y donde un conflicto entre plugins es más difícil de notar, porque nadie está mirando la pantalla.
  • La propia página del editor masivo, edit.php?post_type=product&page=woobe. Es la que carga visiblemente más rápido: el admin ya no arrastra cada metabox ni cada script de admin de cada plugin.
  • Las llamadas ajax del editor, cada acción que empieza por woobe. Aquí es donde ocurre la escritura real, así que aquí es donde se habrían disparado los callbacks.

Si quieres el aislamiento solo para MCP y no para la página del editor, elimina la segunda y la tercera comprobación. El resto del archivo sigue funcionando.

Qué pierdes, y esta es la parte en la que hay que pensar

Un plugin que no está cargado no reacciona a que se guarde un producto. La mayoría de las veces, ese es justo el objetivo. A veces es un problema, y es mejor saber cuál antes de ejecutar una operación sobre todo el catálogo.

Búsqueda y sincronización. Relevanssi, ElasticPress, Algolia, generadores de feeds, conectores ERP: actualizan su índice o envían datos en save_post. Aíslalos y tres mil productos cambiarán en la base de datos mientras el índice sigue describiendo los antiguos. Reconstruye el índice después, o deja cargados estos plugins.

Multilingüe. WPML y Polylang crean y enlazan traducciones cuando se guarda un producto. Sin ellos, la escritura solo llega al idioma de origen y las conexiones entre traducciones pueden desincronizarse.

Campos definidos por otros plugins. ACF, Subscriptions, Bookings, metaboxes personalizados: si el plugin no está cargado, sus campos no se registran, y BEAR no los muestra como columnas. La operación no corromperá nada, pero un campo que esperabas editar no estará ahí.

Cachés. Los plugins de caché invalidan páginas al guardar. Aislados, no lo harán, y la tienda seguirá sirviendo los precios antiguos hasta que la caché expire o la purgues a mano.

Plugins de seguridad. Un plugin de firewall en la lista de disable es un firewall que no se ejecuta en esa petición. El endpoint MCP tiene su propio token y, con la conexión de dos factores activada, su propia segunda comprobación, pero esta es una decisión que conviene tomar de forma deliberada y no por accidente.

Hay dos cosas que este método no puede desactivar en absoluto: otros mu-plugins, y el tema activo. Ambos se cargan fuera de active_plugins. Si el functions.php de tu tema lleva el código más pesado del sitio, el aislamiento no ayudará con esa parte.

Probarlo antes de confiar en él

Trabaja primero en una copia de staging, con un filtro pequeño en lugar de todo el catálogo.

  1. Instala el archivo con una lista vacía y confirma que nada cambia. El sitio debe comportarse exactamente igual que antes.
  2. Añade un plugin a la lista, abre la página del editor masivo y comprueba que las columnas que usas normalmente siguen ahí.
  3. Ejecuta una operación masiva sobre diez productos y compara el resultado con lo que esperas: precios escritos, stock escrito, la tienda mostrando los nuevos valores tras purgar la caché.
  4. Añade el siguiente plugin y repite. Cuando algo desaparezca o deje de actualizarse, el último plugin que añadiste es el motivo.

Para eliminar el aislamiento por completo, borra el archivo. No hay nada que desactivar ni nada que quede en la base de datos.

Cuándo merece la pena hacer esto: un admin de WooCommerce lento, y cuándo no

En una tienda con una docena de plugins, no. WordPress se las arregla bien con eso y la ganancia no compensa el archivo. Se gana su lugar donde la lista de plugins es larga, donde una operación masiva sobre miles de productos agota el tiempo de espera o termina a medias, o donde un asistente de IA escribe a través del servidor MCP y nadie está mirando la pantalla para notar que un plugin de sincronización ha disparado ocho mil llamadas a una API en segundo plano.

Es también la respuesta honesta a "cómo acelero el admin de WooCommerce" cuando la razón real es el número de plugins y no el servidor: en lugar de perseguir al plugin culpable, decides cuáles tienen algo que hacer cargados mientras se escriben los productos.