Si administrás sitios WordPress con el tema Betheme y ya migraste (o estás migrando) a PHP 8, es muy probable que en algún momento te haya llegado un correo de Wordfence anunciando un error fatal justo después de actualizar plugins. El sitio sigue funcionando, pero la actualización queda a mitad de camino y el correo de alerta se vuelve un ritual incómodo cada vez que tocás el panel de plugins.
Esto no es un problema de tu instalación, ni de un plugin mal configurado: es un bug de compatibilidad dentro del propio updater de Betheme, que durante años funcionó «por accidente» gracias a que PHP 7 era permisivo con errores que PHP 8 ya no tolera.
A continuación, el recorrido completo: qué dice exactamente el error, por qué pasa, cómo se confirma la causa sin adivinar, y el fix aplicado vía child theme — con la verificación real hecha por línea de comandos para confirmar que quedó resuelto.
En una frase: Betheme asume que un dato interno de WordPress (el
transientde actualizaciones de temas) siempre va a existir como objeto. Cuando llega vacío, PHP 8 lo trata como error fatal en lugar de ignorarlo como hacía PHP 7.
Paso 1: el síntoma — el correo que llega después de cada actualización
El disparador siempre era el mismo: actualizar varios plugins a la vez desde el panel de WordPress. Minutos después, Wordfence enviaba una alerta con este mensaje:
Wordfence Alert — mundogeek.com.ar
Se ha producido un error del tipo E_ERROR en la línea 55 del archivo
/srv/.../themes/betheme/functions/admin/class-mfn-update.php
Uncaught Error: Attempt to modify property «response» on null
in class-mfn-update.php:55
Stack trace:
#0 class-wp-hook.php(343): Mfn_Update->pre_set_site_transient_update_themes()
#1 plugin.php(205): WP_Hook->apply_filters()
#2 option.php(2652): apply_filters()
#3 element-pack-base.php(88): set_site_transient()
#7 class-plugin-upgrader.php(412): do_action()
#8 update.php(51): Plugin_Upgrader->bulk_upgrade()
Lo primero que hice fue descartar lo obvio: revisar las tres personalizaciones propias que tiene el child theme del sitio (webhook.php, email-fixes.php, security.php). Ninguna de las tres toca actualizaciones de temas ni transients — quedaban fuera de sospecha desde el primer vistazo al stack trace.
Paso 2: el diagnóstico — por qué PHP 8 lo convierte en fatal
2.1 — Identificar el hook responsable
El stack trace señala con precisión la función Mfn_Update->pre_set_site_transient_update_themes(), colgada del filtro nativo de WordPress pre_set_site_transient_update_themes. Es el mecanismo que Betheme usa para chequear si hay una nueva versión del tema disponible.
2.2 — Encontrar el supuesto del código
Dentro de esa función, Betheme da por sentado que el valor que recibe (el transient) ya es un objeto, y escribe directamente sobre una de sus propiedades:
class-mfn-update.php
// Lógica aproximada dentro de class-mfn-update.php:55
$transient->response[$theme_slug] = $theme_data;
Si $transient llega como null, esa línea intenta escribir una propiedad sobre algo que no es un objeto.
2.3 — El cambio de comportamiento entre versiones de PHP
Acá está la raíz real del problema: en PHP 7, escribir una propiedad sobre null generaba apenas un warning silencioso que no interrumpía la ejecución. En PHP 8, ese mismo intento se convirtió en Error no capturado — y un error no capturado es fatal. El código de Betheme nunca cambió; el comportamiento de PHP debajo de él, sí.
2.4 — Identificar el disparador
Quedaba una pregunta: ¿por qué el transient llegaba vacío? La respuesta estaba más arriba en el mismo stack trace: el plugin bdthemes-element-pack ejecuta set_site_transient() durante su propia inicialización. Esa llamada dispara el filtro de Betheme en un momento en que WordPress todavía no había poblado el transient de actualizaciones de temas — especialmente probable durante una actualización masiva (bulk upgrade) de varios plugins en simultáneo.
Paso 3: la solución — interceptar el valor antes de que llegue a Betheme
Editar class-mfn-update.php directamente no es una opción real: cualquier actualización del tema sobrescribe el archivo y el fix desaparece. La alternativa correcta es un patrón clásico de compatibility shim desde el child theme: enganchar el mismo filtro con una prioridad más baja, para «sanear» el valor antes de que Betheme lo reciba.
includes/compat-fixes.php
includes/compat-fixes.php
<?php
/**
* Compat Fixes — Parches de compatibilidad con bugs de plugins/temas de terceros
*
* Betheme asume que el transient 'update_themes' siempre es un objeto al
* enganchar 'pre_set_site_transient_update_themes'. Si llega null, en PHP 8+
* esto genera Fatal Error: "Attempt to modify property «response» on null"
*
* Este filtro corre con prioridad 1 (ANTES que el de Betheme) y garantiza
* que el valor nunca llegue como null.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_filter( 'pre_set_site_transient_update_themes', 'cab_fix_betheme_null_transient', 1 );
function cab_fix_betheme_null_transient( $value ) {
if ( is_null( $value ) ) {
// Nunca null → Betheme ya no explota al hacer $value->response[...]
return new stdClass();
}
return $value;
}
La clave de todo el fix es un solo número: la prioridad 1. WordPress ejecuta los callbacks de un mismo hook en orden de prioridad ascendente, así que este filtro corre antes que el de Betheme y entrega un objeto vacío en lugar de null — exactamente lo que el código del tema espera recibir.
Y en functions.php, sumado a los includes existentes:
functions.php
$child_includes = array(
'includes/webhook.php',
'includes/email-fixes.php',
'includes/security.php',
'includes/compat-fixes.php',
);
Paso 4: la verificación — no asumir, confirmar
Un parche que «debería funcionar» no es lo mismo que un parche confirmado. Estos fueron los tres chequeos, en orden, antes de dar el problema por cerrado:
SSH — Server-Multitask
root@Server-Multitask:~/betheme-child# php -l includes/compat-fixes.php
No syntax errors detected in includes/compat-fixes.php
root@Server-Multitask:~/betheme-child# wp eval 'var_dump( has_filter(
"pre_set_site_transient_update_themes",
"cab_fix_betheme_null_transient"
) );' --allow-root
int(1)
| Chequeo | Qué confirma | Resultado |
|---|---|---|
php -l |
Sintaxis del archivo correcta | ✓ Sin errores |
wp eval has_filter() |
El filtro está registrado en runtime | ✓ int(1) — prioridad correcta |
| Actualización real de plugins | El fatal error ya no ocurre | ✓ Confirmado en producción |
El int(1) no es un detalle menor: confirma que el filtro quedó activo con exactamente la prioridad que necesitaba para ejecutarse antes que Betheme. Sin ese chequeo, el fix podía estar «cargado» en el archivo pero no necesariamente operativo si hubiera algún conflicto de prioridades con otro plugin.
La lección de fondo
Este tipo de errores —que «siempre funcionaron» y de repente rompen tras una migración de versión de PHP— casi nunca son culpa de quien administra el sitio. Son incompatibilidades silenciosas que durante años vivieron escondidas detrás de un comportamiento permisivo del lenguaje, hasta que una versión más estricta las deja en evidencia.
El patrón de solución, en cambio, sí es una decisión de quien administra: parchar desde el child theme, nunca desde el core; enganchar el filtro con la prioridad exacta que el problema requiere; y verificar con herramientas reales (php -l, wp-cli) en lugar de confiar en que «ya debería estar funcionando».