Da quando uso l’AI per sviluppare plugin per WordPress, temo sempre che l’agente di turno introduca bug o scriva codice troppo complesso e poco chiaro. Non che il mio codice prima dell’AI fosse impeccabile, tutt’altro. Però, visto che esistono strumenti che analizzano e validano il codice in automatico, perché non usarli?
Uno di questi è PHPStan, ovvero PHP Static Analysis Tool che fa un’analisi statica: legge il codice senza eseguirlo e trova errori che altrimenti si scoprirebbero solo a runtime, magari in produzione. Per esempio: funzioni o metodi inesistenti, tipi sbagliati, variabili non definite, valori che possono essere null, codice che non verrà mai eseguito.
Il vantaggio è che il controllo è automatico e ripetibile; si lancia con un comando, a mano o in automatico a ogni commit, e il tool esegue una serie di controlli che producono segnalazioni più o meno numerose in base al “livello di pulizia” scelto come obiettivo.
Le segnalazioni non vengono corrette automaticamente: bisogna intervenire e sistemarle una ad una.
I livelli
PHPStan non applica tutti i controlli insieme ma li organizza in livelli da 0 a 10, dal più permissivo al più rigoroso, e ogni livello include i controlli di quelli precedenti. Così su un progetto esistente si può partire basso e alzare il livello man mano che si sistema il codice.
In sintesi:
- Livelli 0–2: verificano che le classi, le funzioni, i metodi e le variabili usati nel codice esistano davvero.
- Livelli 3–5: verificano che i valori passati alle funzioni, quelli restituiti e quelli assegnati alle proprietà siano del tipo dichiarato.
- Livello 6: richiede che i parametri e i valori di ritorno delle funzioni e le proprietà delle classi abbiano sempre un tipo dichiarato.
- Livelli 7–10: richiedono che il codice gestisca ogni valore possibile, compresi i tipi multipli, il
nulle i valori di tipomixed.
Il livello si sceglie da riga di comando, ad esempio vendor/bin/phpstan analyse src --level 5, oppure nel file di configurazione phpstan.neon.

Descrizione dei livelli
Non è immediato capire cosa significhi, in pratica, raggiungere un certo livello. In sostanza, le possibili anomalie del codice sono state classificate in livelli in base al rigore dei controlli, in modo da poterle affrontare per passaggi successivi.
Di seguito riporto alcuni esempi che aiutano a capire cosa verificano effettivamente i controlli di ciascun livello, così da individuare il livello minimo da raggiungere per la propria applicazione.
Per ogni livello riporto: la domanda a cui risponde, cosa controlla e un esempio di errore (❌) con la correzione (✅).
Livello 0 – Il codice esiste?
Classi, funzioni e metodi chiamati devono esistere e devono ricevere tutti gli argomenti obbligatori. Gli argomenti in più, invece, vengono segnalati solo dal livello 1, perché PHP li accetta senza protestare.
// ❌ funzione inesistente e argomento obbligatorio mancante
$title = get_the_titel();
$color = get_option();
// ✅
$title = get_the_title();
$color = get_option('my_plugin_color');
Livello 1 – Le variabili ci sono sempre?
Segnala le variabili che potrebbero non essere definite, ad esempio quando vengono assegnate solo dentro un if.
// ❌ se $is_admin è false, $msg non esiste
if ($is_admin) {
$msg = 'Ciao admin';
}
echo $msg;
// ✅
$msg = '';
if ($is_admin) {
$msg = 'Ciao admin';
}
echo $msg;
Livello 2 – I metodi esistono su qualsiasi oggetto?
Al livello 0 vengono controllati solo i metodi chiamati su $this. Qui si controllano su qualsiasi oggetto e si verifica che i PHPDoc siano validi.
// ❌ WP_Post non ha il metodo getTitle()
function show(WP_Post $post) {
echo $post->getTitle();
}
// ✅
function show(WP_Post $post) {
echo $post->post_title;
}
Livello 3 – Restituisci il tipo promesso?
I valori restituiti e quelli assegnati alle proprietà devono corrispondere ai tipi dichiarati.
// ❌ dichiara int ma restituisce una stringa
function count_items(): int {
return '5';
}
// ✅
function count_items(): int {
return 5;
}
Livello 4 – C’è codice inutile?
Trova il codice morto: condizioni sempre vere o sempre false e codice che non verrà mai eseguito.
// ❌ $post è già un WP_Post: il controllo è sempre vero
function render(WP_Post $post) {
if ($post instanceof WP_Post) {
return $post->post_title;
}
return ''; // irraggiungibile
}
// ✅
function render(WP_Post $post) {
return $post->post_title;
}
Livello 5 – Passi argomenti del tipo giusto?
Controlla il tipo degli argomenti passati a funzioni e metodi. È il livello che trova più bug concreti.
// ❌ get_post() vuole un int o un WP_Post, non una stringa
$post = get_post('abc');
// ✅
$post = get_post(absint($_GET['id'] ?? 0));
Livello 6 – Hai dichiarato i tipi?
Segnala parametri, valori di ritorno e proprietà senza tipo, compresi gli array di cui non è descritto il contenuto.
// ❌ nessun tipo dichiarato
function get_ids($posts) {
return array_map(fn($p) => $p->ID, $posts);
}
// ✅
/**
* @param WP_Post[] $posts
* @return int[]
*/
function get_ids(array $posts): array {
return array_map(fn(WP_Post $p) => $p->ID, $posts);
}
Livello 7 – Gestisci tutti i tipi possibili?
Se un valore può avere più tipi (union type), il codice deve funzionare per tutti, non solo per alcuni.
// ❌ wp_insert_post() può restituire int oppure WP_Error
$id = wp_insert_post($data, true);
echo $id + 1;
// ✅
$id = wp_insert_post($data, true);
if (is_wp_error($id)) {
return;
}
echo $id + 1;
Livello 8 – Hai considerato il null?
Vieta di chiamare metodi o leggere proprietà su valori che potrebbero essere null.
// ❌ get_post() può restituire null
$post = get_post(42);
echo $post->post_title;
// ✅
$post = get_post(42);
if ($post === null) {
return;
}
echo $post->post_title;
Livello 9 – Usi mixed senza controllarlo?
Un valore dichiarato mixed non si può usare finché non se ne verifica il tipo. L’unica cosa consentita è passarlo a un altro mixed.
// ❌ get_option() restituisce mixed: non è detto che sia un array
$opts = get_option('my_plugin_settings');
echo $opts['color'];
// ✅
$opts = get_option('my_plugin_settings');
$color = is_array($opts) && is_string($opts['color'] ?? null)
? $opts['color']
: 'blue';
echo $color;
Livello 10 – Anche il mixed nascosto?
Applica le regole del livello 9 anche al mixed implicito, cioè ai valori senza un tipo dichiarato.
// ❌ $title non ha un tipo dichiarato: è mixed implicito
add_filter('the_title', function ($title) {
return strtoupper($title);
});
// ✅
add_filter('the_title', function ($title) {
return is_string($title) ? strtoupper($title) : $title;
});
Livelli 9 e 10 su WordPress
Su WordPress questi due livelli sono difficili da raggiungere perché molte funzioni del core restituiscono mixed, il motivo è che WordPress è nato prima che PHP avesse le dichiarazioni di tipo e qiuindi salva dati di qualsiasi forma: molte funzioni del core possono restituire, o accettare come parametri, valori di tipo diverso. Per esempio:
- opzioni e meta:
get_option(),get_post_meta(),get_transient()possono restituire una stringa, un array,false…; - filtri:
apply_filters()restituisce ciò che restituisce l’ultimo callback agganciato, anche quello di un altro plugin; - input dell’utente: ogni campo di
$_GETe$_POSTva verificato; - callback degli hook: spesso hanno argomenti senza tipo, cioè proprio il
mixedimplicito del livello 10.
La conseguenza di ciò è che ogni valore letto da WordPress va controllato prima dell’uso, come negli esempi dei livelli 9 e 10, e il codice si allunga parecchio. I controlli però non sono inutili: un’opzione corrotta o un filtro che restituisce null sono bug reali, per renderli sostenibili conviene validare i dati in un solo punto (con pochi getter tipizzati o una classe per le impostazioni) e dichiarare sempre i tipi dei callback.
Considerazioni
Per un plugin WordPress il livello 5 o 6 è di solito un buon compromesso, mentre per un plugin nuovo si può puntare all’8, che costringe a gestire i null restituiti da funzioni come get_post().
Su un progetto esistente conviene partire da un livello basso e generare una baseline (--generate-baseline), che “congela” gli errori già presenti: così PHPStan segnala solo quelli nuovi e si può alzare il livello un po’ alla volta. In ogni caso serve l’estensione phpstan-wordpress, altrimenti PHPStan non conosce le funzioni del core.
Inutile dire che questi controlli si prestano bene a procedure automatizzate di Continuous Integration e che il passaggio a un livello più alto è facilitato dalla possibilità di fare correzioni massive con gli agenti AI, che anche in questo sono molto bravi.
Magari prossimamente scriverò un post su come configurare PHPStan in un plugin WordPress, come configurare la CI su GitHub e su come aggiungere un badge che ne attesti l’uso.
Fonti e riferimenti
- PHPStan, sito ufficiale.
- Rule Levels, la documentazione dei livelli.
- Due progetti in cui lo sto usando: Design Laboratori Italia, KK Content Exporter.

