Perché un solo plugin può buttare giù un intero sito
Heckgalerie 1.2.5 e Leadlotse 1.4.3, due dei nostri plugin della directory di WordPress, insieme causavano un errore fatale su ogni pagina di un sito. Nessun plugin era guasto da solo. Il problema stava in una libreria che cinque dei nostri plugin includono e, in buona parte, in due particolarità di Composer e WordPress che ci erano sfuggite. Riguarda chiunque condivida codice tra plugin.
Il messaggio di errore è breve. Eccolo, riprodotto in WordPress Playground:
PHP Fatal error: Uncaught Error: Call to undefined method
Hafenstudios\Core\Admin\DesignSystem::action_button()
in /wordpress/wp-content/plugins/heckgalerie/heckgalerie.php:163
La cosa strana: il metodo esiste. Heckgalerie lo porta nella propria copia della libreria. PHP ha comunque caricato una copia più vecchia da un altro plugin, Leadlotse, che non lo aveva ancora.
L’errore è stato nostro. Due dei suoi tre strati, però, stanno nel modo in cui Composer e WordPress interagiscono, e riguardano chiunque condivida codice tra plugin. Per questo li mettiamo per iscritto.
Una libreria, cinque copie
Heckgalerie, Kurato, Leadlotse, Wellenbrecher e Wimpel condividono una libreria, l’Hafenstudios Core (da non confondere con il nostro plugin Hafen Core): impostazioni, eventi, invio di email, registro dei consensi, i mattoni delle schermate di amministrazione. Attraverso di essa i plugin lavorano insieme. Quando un modulo di Leadlotse segnala una nuova richiesta, Kurato ascolta lo stesso evento e crea un contatto.
Ogni plugin porta la propria copia, tramite Composer in vendor/hafenstudios/core. Deve essere così: un plugin della directory deve funzionare anche da solo. E siccome ogni plugin ha il suo ritmo di rilascio, su un sito si ritrovano presto più versioni affiancate:
| Plugin | nella directory fino all’11/10/2026 | Core incluso | dal 12/10/2026 | Core incluso |
|---|---|---|---|---|
| Heckgalerie | 1.2.5 | 1.4.0 | 1.2.6 | 1.4.4 |
| Leadlotse | 1.4.3 | 1.3.0 | 1.5.1 | 1.4.4 |
| Wellenbrecher | 1.3.2 | 1.4.0 | 1.3.3 | 1.4.4 |
| Wimpel | 1.2.0 | 1.4.1 | 1.3.1 | 1.4.4 |
In una richiesta PHP conosce una sola classe con un dato nome. Chi carica per primo Hafenstudios\Core\Core la fissa per tutti. La risposta abituale arriva dal mondo WooCommerce: Action Scheduler viene incluso da molti plugin, ogni copia registra la propria versione e sull’hook plugins_loaded viene caricata solo la più alta. È esattamente ciò che avevamo costruito.
La scelta si regge su due presupposti a cui si pensa di rado. Ogni copia deve registrarsi. E la copia scelta deve davvero fornire le classi. Nessuno dei due era soddisfatto.
Prima scoperta: Composer si mette davanti
Composer genera un autoloader distinto per ogni plugin. Si registra con l’opzione prepend, cioè in testa alla coda che PHP interroga per le classi sconosciute. Lo dice il file vendor/composer/autoload_real.php generato:
$loader->register(true);
WordPress carica i plugin attivi nell’ordine della lista active_plugins, che ordena alfabeticamente all’attivazione. Ogni plugin in più mette il suo loader Composer davanti ai precedenti. Alla fine, il primo della coda è quello del plugin la cui cartella viene per ultima nell’alfabeto.
La nostra scelta di versione girava dopo, su plugins_loaded, e aggiungeva il loader della copia scelta in fondo. Quando una ricerca arrivava lì, un loader Composer aveva già risposto da un pezzo. Lo abbiamo riprodotto senza WordPress, con i veri pacchetti della directory di Heckgalerie 1.2.5 e Leadlotse 1.4.3, nell’ordine in cui WordPress li carica. Il risultato, in sintesi:
Voci in __composer_autoload_files: 1
Registrate per la scelta: Core 1.4.0 (Heckgalerie)
Scelta: Core 1.4.0 da Heckgalerie
DesignSystem fornito da: Leadlotse (Core 1.3.0)
action_button() presente: no
La scelta indicava 1.4.0, il codice eseguito era 1.3.0. Il nostro controllo di versione leggeva solo la costante impostata dalla scelta, e quindi non ha visto nulla.
Quanto fosse casuale il risultato lo mostra una prova in Playground con tutti e quattro i plugin vecchi: il sito funzionava. Wellenbrecher viene dopo Leadlotse nell’alfabeto, quindi il suo loader stava davanti, e la sua copia (1.4.0) conosce il metodo. Wimpel viene ancora più tardi nell’alfabeto, ma la versione 1.2.0 porta un proprio loader al posto di quello di Composer, che si mette in fondo alla coda. Che un sito funzionasse dipendeva dall’ordine dei nomi dei plugin.
La prima correzione, Core 1.4.2 del 6 ottobre, registra anche il loader della copia scelta con prepend. Inoltre, le nuove versioni dei nostri plugin intercettano gli errori all’avvio e li segnalano nella bacheca, invece di trascinare giù il sito. Questa correzione è stata rilasciata solo il 12 ottobre, insieme alle due successive.
Seconda scoperta: il file di avvio gira una volta sola
Per il codice che deve girare subito al caricamento, come funzioni o una registrazione, Composer offre l’autoload files. La nostra libreria lo usa per il suo bootstrap.php, in cui ogni copia si iscrive alla scelta. Nel composer.json della libreria c’è scritto, tra l’altro:
"autoload": {
"files": [ "bootstrap.php" ]
}
Da qui Composer scrive questo blocco in ogni autoloader:
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
Composer costruisce l’identificativo $fileIdentifier come md5 del nome del pacchetto e del percorso. Per il nostro file di avvio è md5('hafenstudios/core:bootstrap.php'), cioè b6716d317db73fd5f2cd1d7c8ab2ffd8, lo stesso valore in tutti i plugin. E $GLOBALS vale per l’intera richiesta, non per un singolo plugin. Appena il file di avvio del primo plugin è stato eseguito, l’identificativo è annotato e Composer salta il file in tutti gli altri.
In un singolo progetto è il comportamento giusto: un file di funzioni non gira due volte e PHP non si ferma con «Cannot redeclare». Su un sito WordPress con più cartelle vendor, lo stesso blocco colpisce copie diverse in versioni diverse.
Per la nostra scelta di versione significava che, di regola, si registrava solo la copia del plugin che WordPress carica per primo, da noi quasi sempre Heckgalerie. Nella riproduzione qui sopra c’è esattamente una voce annotata, e Leadlotse non compare tra le copie registrate, anche se porta la sua. La scelta aveva un solo candidato. Ce ne siamo accorti solo l’11 ottobre, costruendo Wimpel 1.3.1 in Playground.
Non è una particolarità della nostra libreria. symfony/polyfill-mbstring carica il suo bootstrap.php per la stessa via, e Guzzle il suo file di funzioni. Se due plugin includono lo stesso pacchetto senza cambiarne il namespace, sul sito gira il file del plugin caricato per primo, qualunque versione porti l’altro.
La correzione in Core 1.4.4: ogni plugin include anche da sé il proprio file di avvio, direttamente nel file principale. In forma semplificata:
require_once __DIR__ . '/vendor/autoload.php';
// Composer esegue bootstrap.php solo per il primo plugin caricato.
require_once __DIR__ . '/vendor/hafenstudios/core/bootstrap.php';
require_once ricorda i file in base al percorso. La copia già eseguita da Composer non gira una seconda volta, tutte le altre esattamente una. Action Scheduler fa lo stesso, tra l’altro: secondo le sue istruzioni ogni plugin include direttamente il file di avvio. Inoltre, dalla 1.4.4 ogni copia tiene una voce sotto la propria cartella, così due copie con lo stesso numero restano entrambe visibili.
Terza scoperta: la facciata tagliata
Il terzo strato è stato colpa nostra. I nostri pacchetti per la directory di WordPress sono più snelli di quelli che distribuiamo direttamente: le parti della libreria che a un plugin non servono restano fuori, in Wimpel 1.2.0 per esempio l’invio di email, il registro dei consensi e gli strumenti per la privacy. Lo script di build modificava anche la classe centrale Core.php, attraverso cui tutti i plugin accedono a questi servizi, e ne toglieva metodi come consent(), mailer() e privacy().
Nella prova in Playground con tutti e quattro i plugin vecchi questo è rimasto senza conseguenze. Lì era scelta la copia tagliata di Wimpel 1.2.0, Core 1.4.1, allora il numero più alto nella directory. Si era registrata nonostante il blocco perché il loader proprio di Wimpel 1.2.0 include da sé il file di avvio con require_once. Le classi caricate arrivavano comunque tutte da Wellenbrecher. Dalla prima correzione, però, il loader della copia scelta sta in testa. Se allora una copia tagliata vince la scelta, questi metodi mancano a tutti gli altri plugin, e ogni invio di Leadlotse fallisce. Ce ne siamo accorti costruendo le nuove versioni, prima di rilasciarne una.
La correzione ha due parti. Dalla 1.4.4 Core.php resta completo in ogni pacchetto, e all’avvio la libreria verifica con class_exists se un servizio è disponibile. Inoltre il loader della copia scelta colma da sé le lacune: se gli manca un file, lo cerca in tutte le copie registrate del sito, in ordine di versione decrescente. Solo se nessuna lo ha entrano in gioco i loader Composer dei plugin.
Il 12 ottobre il test di integrazione ha portato alla luce un altro caso. In una versione intermedia di 1.4.4, ancora prima del rilascio, lo script di build continuava a tagliare ai() da Core.php nei pacchetti senza funzioni di IA. Questa copia tagliata del pacchetto di directory di Leadlotse e quella completa di Kurato avevano entrambe il numero 1.4.4. A parità di numero vinceva l’ultima caricata, cioè quella tagliata, e Kurato si è bloccato su una funzione di IA. Da allora resta anche ai(), e a parità di numero nella lista della scelta entra la copia più completa. Un numero di versione dovrebbe corrispondere a un solo contenuto. Dove non è possibile, la scelta ha bisogno di un secondo criterio.
Misurato in WordPress Playground
Prima del rilascio i pacchetti hanno girato in undici combinazioni in WordPress Playground, con WordPress 7.1.3 e PHP 8.3, richieste esterne bloccate. Il giorno del rilascio abbiamo misurato di nuovo, alle stesse condizioni, cosa cambia un solo plugin nuovo nella vecchia combinazione che andava in errore. I risultati principali:
| Combinazione | Risultato |
|---|---|
| Heckgalerie 1.2.5 e Leadlotse 1.4.3 dalla directory | errore fatale in heckgalerie.php:163, il messaggio qui sopra |
| Solo Heckgalerie 1.2.6 nuovo; Leadlotse 1.4.3 vecchio | funziona, DesignSystem da Heckgalerie (Core 1.4.4) |
| Solo Leadlotse 1.5.1 nuovo; Heckgalerie 1.2.5 vecchio | funziona, DesignSystem da Leadlotse (Core 1.4.4) |
| Solo Wimpel 1.3.1 nuovo; Heckgalerie e Leadlotse vecchi | funziona, modulo inviato, email con dieresi corrette |
| Solo Kurato 1.9.3 nuovo; Heckgalerie 1.2.5 e Leadlotse 1.4.3 vecchi | funziona, DesignSystem da Kurato (Core 1.4.4) |
| Heckgalerie, Leadlotse, Wellenbrecher e Wimpel nuovi dalla directory | funziona, tutte e quattro le copie registrate |
| Kurato 1.9.3 dalla distribuzione diretta; Heckgalerie, Leadlotse e Wellenbrecher nuovi dalla directory | funziona, tutte e quattro le copie registrate |
| Dal vecchio al nuovo, un plugin alla volta sostituito tramite upload, con una galleria e una voce di modulo | dopo ogni scambio tutte le pagine controllate con stato 200, contenuti conservati |
La riproduzione senza WordPress mostra lo stesso in piccolo. Con Heckgalerie 1.2.6 al posto di 1.2.5, DesignSystem arriva da Heckgalerie (Core 1.4.4); con Leadlotse 1.5.1 al posto di 1.4.3, da Leadlotse (Core 1.4.4). In entrambi i casi il metodo c’è.
Cosa il test non copre: clic in un browser vero, multisito, l’aggiornamento con il pulsante «Aggiorna ora», aggiornamenti automatici via cron, altre versioni di PHP e WordPress e, nel passaggio dal vecchio al nuovo, solo uno dei 24 ordini possibili.
Cosa puoi fare se gestisci un sito
- Aggiorna insieme i plugin dello stesso produttore. Quando dei plugin condividono una libreria, lo stato misto è quello rischioso. Per i nostri significa: Heckgalerie 1.2.6, Leadlotse 1.5.1, Wellenbrecher 1.3.3 e Wimpel 1.3.1 sono nella directory di WordPress dal 12 ottobre 2026, e Kurato 1.9.3 arriva dal nostro canale di aggiornamento.
- Se c’è fretta, uno aiuta già. Ognuno di questi pacchetti include Core 1.4.4. Quella copia vince la scelta e, siccome il suo loader sta in testa, è lei a fornire le classi, anche ai plugin più vecchi. In Playground la vecchia combinazione che andava in errore, Heckgalerie 1.2.5 e Leadlotse 1.4.3, funzionava senza errore fatale non appena uno dei due era aggiornato. Lo stesso valeva se invece si aggiungeva Wimpel 1.3.1 o Kurato 1.9.3. Non abbiamo testato tutte le combinazioni miste; il lavoro è finito solo quando sono tutti aggiornati.
- Se il sito è già giù, usa il link alla modalità di ripristino che WordPress invia all’indirizzo dell’amministratore dopo un errore grave. Niente email? Rinomina, via SFTP o con il file manager dell’hosting, la cartella dell’ultimo plugin che hai attivato o aggiornato in
wp-content/plugins/. - Leggi con attenzione il messaggio di errore. Se nomina un metodo che non esisterebbe in una classe di un namespace incluso da più dei tuoi plugin, un conflitto di versioni è più probabile di un errore di programmazione nel plugin citato. Per capire quale plugin è coinvolto, disattiva i plugin uno alla volta. Senza conseguenze per i visitatori puoi farlo con il plugin Health Check & Troubleshooting: la sua modalità di risoluzione dei problemi disattiva tutti i plugin solo per la tua sessione, e tu li riattivi uno alla volta.
Come organizzare gli aggiornamenti perché un’interruzione non ti colga di sorpresa lo spieghiamo in aggiornare i plugin di WordPress.
Cosa possono portarsi a casa gli sviluppatori di plugin
- Non affidare la registrazione all’autoload files. Ciò che ogni copia deve fare va nel file principale, con
require_once. L’autoloadfilesesegue un file una volta per richiesta, non una volta per plugin. - Registra con prepend il loader della copia scelta. Altrimenti rispondono i loader Composer dei plugin, e vince l’ultimo caricato:
spl_autoload_register( $loader, true, true ); // terzo argomento: prepend - Non tagliare mai la facciata per pacchetto. Lascia fuori dei file se serve, ma non i metodi della classe che tutti i plugin chiamano. Se un servizio è disponibile lo verifica
class_existsin fase di esecuzione. - Un numero di versione, un contenuto. Se una copia tagliata e una completa hanno lo stesso numero, la scelta non può distinguerle. Serve allora un secondo criterio; il nostro è la completezza.
- Controlla l’origine, non la costante. Cosa ha caricato davvero PHP lo rivela il file dietro la classe:
$file = ( new \ReflectionClass( \Hafenstudios\Core\Admin\DesignSystem::class ) )->getFileName(); - Testa accanto a versioni vecchie. Il difetto si vede solo quando un plugin nuovo gira accanto a uno vecchio, mai da solo. La directory di WordPress continua a offrire le versioni precedenti in ZIP.
- Se non devi condividere nulla, usa i prefissi. Strumenti come PHP-Scoper o Strauss danno a ogni plugin il proprio namespace, così non esiste una classe comune da contendersi. Noi condividiamo di proposito, perché i nostri plugin comunicano tra loro attraverso la libreria. Se non ti serve, i prefissi sono più semplici.
Il sito è fermo?
Schermata bianca, errore fatale dopo un aggiornamento, un modulo che non invia più nulla: con la nostra assistenza urgente WordPress iniziamo entro quattro ore nei giorni lavorativi, senza contratto e senza pagamento anticipato. 185 € l’ora.
Domande frequenti
Perché un solo plugin può buttare giù un intero sito WordPress?
Tutti i plugin attivi girano nello stesso processo PHP. Se uno si ferma con un errore fatale, si ferma l’intera richiesta. Quando più plugin includono la stessa libreria, si aggiunge una seconda via: PHP conosce una sola classe per nome, e la copia di un plugin può sostituire quella di un altro, anche se è più vecchia.
Che cos’è l’autoload files di Composer?
Una voce del composer.json con cui Composer esegue dei file appena l’autoloader viene caricato, per esempio per funzioni o registrazioni. Ogni file riceve un identificativo formato dall’md5 del nome del pacchetto e del percorso, e Composer lo esegue una sola volta per richiesta. Se più plugin includono lo stesso pacchetto, i file hanno lo stesso identificativo e gira solo quello del plugin caricato per primo.
Come scopro da quale plugin è stata caricata una classe?
Con la reflection di PHP: getFileName() su una ReflectionClass della classe restituisce il percorso del file da cui PHP l’ha caricata. Se punta alla cartella vendor di un altro plugin, la classe arriva da una copia estranea. Una costante di versione non lo dimostra, dice solo quale copia è stata scelta.
Devo aggiornare tutti i plugin Hafenstudios contemporaneamente?
Sì, meglio tutti insieme. Heckgalerie 1.2.6, Leadlotse 1.5.1, Wellenbrecher 1.3.3, Wimpel 1.3.1 e Kurato 1.9.3 includono Core 1.4.4. In WordPress Playground la vecchia combinazione che andava in errore, Heckgalerie 1.2.5 e Leadlotse 1.4.3, funzionava già senza errore fatale non appena Heckgalerie, Leadlotse, Wimpel o Kurato era presente nella nuova versione. Non abbiamo però testato tutte le combinazioni miste. L’installazione è completa solo quando sono tutti aggiornati.
Come evitano gli sviluppatori i conflitti tra librerie condivise?
Se non ti serve uno stato condiviso, aggiungi un prefisso ai namespace di ogni plugin, per esempio con PHP-Scoper o Strauss. Se condividi una libreria di proposito, registra tu stesso ogni copia con require_once, registra il loader della copia scelta con prepend, non tagliare mai la classe condivisa per pacchetto e usa ogni numero di versione per un solo contenuto.