Escribir código PHP que funciona no es difícil. Escribir código PHP que no va a fallar en producción con un tipo inesperado, una propiedad no inicializada o un array al que le falta una clave: eso es otra historia.
PHP te deja hacer cosas que no debería. Mezclar tipos, acceder a propiedades que quizá no existen, llamar métodos sobre null sin pestañear. Y el día que algo falla en un sitio con tráfico real, el error no aparece en desarrollo, aparece en el debug.log a las 3 de la mañana con un stack trace que no te esperabas.
PHPStan es la herramienta que te dice antes de desplegar lo que PHP te diría después de romper. Y ejecutarlo a nivel 9 (la máxima severidad) no es una exageración. Es la única forma de saber que tu código es sólido de verdad.
Qué es PHPStan y qué no es
PHPStan es un analizador estático. No ejecuta tu código, lo lee, lo analiza y te dice qué puede fallar basándose en los tipos, las llamadas, las estructuras de control y las anotaciones que has escrito.
No es un sustituto de PHPUnit. PHPUnit te dice si tu lógica funciona. PHPStan te dice si tu código tiene sentido. Son capas distintas y complementarias.
Y no es PHPCS. PHPCS te dice si sigues los estándares de formato y estilo de WordPress. PHPStan te dice si estás llamando a un método que no existe, pasando un string donde esperabas un int, o accediendo a una propiedad que puede ser null.
Las tres herramientas juntas, PHPCS + PHPStan + PHPUnit, son el trípode sobre el que se sostiene un plugin profesional.
Los niveles: por qué el 9 no es una exageración
PHPStan tiene 10 niveles, del 0 al 9. Cada uno añade comprobaciones más estrictas:
- Nivel 0: básico — clases que no existen, métodos que no existen.
- Nivel 5: tipos de retorno, tipos de parámetros.
- Nivel 8: propiedades no inicializadas, tipos union.
- Nivel 9: todo lo anterior + análisis de dead code, tipos condicionales, y el nivel más exhaustivo posible.
¿Por qué nivel 9? Porque cualquier error que PHPStan pueda detectar estáticamente a nivel 9 es un error que no vas a descubrir hasta que alguien lo provoca en producción. Y si lo puedes prevenir automáticamente, lo previenes.
Empezar a nivel 5 y subir gradualmente es perfectamente válido si vienes de un proyecto sin análisis estático. Pero el objetivo tiene que ser el 9. Si lo declaras en tu phpstan.neon y pasa, sabes que tu código no tiene las trampas más comunes de PHP.
Instalación: por proyecto, nunca global
Igual que PHPCS, PHPStan se instala por proyecto con Composer:
composer require --dev phpstan/phpstan:"^2.1"
composer require --dev szepeviktor/phpstan-wordpress:"^2.0"
La segunda dependencia es clave: phpstan-wordpress le enseña a PHPStan a entender las funciones, los hooks y las convenciones de WordPress. Sin ella, PHPStan no sabe qué es get_post_meta(), qué devuelve apply_filters(), o qué tipos maneja wp_remote_get().
La configuración mínima en phpstan.neon:
parameters:
level: 9
paths:
- .
excludePaths:
- vendor
- tests
- node_modules
Y se ejecuta siempre vía el binario local:
vendor/bin/phpstan analyse
Nunca global. Nunca con una instalación del sistema. Lo mismo que con PHPCS: si no está en vendor/bin/, no existe.
Lo que PHPStan detecta (y lo que tú no verías)
Algunos ejemplos reales de lo que nivel 9 atrapa que pasaría desapercibido:
Tipos incompatibles en parámetros
function render_panel( string $title, array $options ): void {
// ...
}
// PHPStan detecta esto:
render_panel( 123, 'not-an-array' );
PHP lo ejecutaría con un warning silencioso (o con conversión implícita). PHPStan lo bloquea antes.
Propiedades no inicializadas
class Settings_Manager {
private string $option_name;
private string $cache_group;
public function __construct( string $option_name ) {
$this->option_name = $option_name;
// $cache_group nunca se inicializa
}
public function get_cache_key(): string {
return $this->cache_group . '_' . $this->option_name;
// TypeError en PHP 8.x, silencioso en 7.x
}
}
Nivel 9 te dice exactamente que $cache_group no está inicializada en el constructor. En producción, esto es un TypeError en PHP 8 o un valor vacío silencioso en PHP 7, y ambos son fallos.
Retornos inalcanzables o inconsistentes
function get_user_role( int $user_id ): string {
$user = get_userdata( $user_id );
if ( ! $user ) {
return; // void, no string
}
return $user->roles[0] ?? '';
}
PHPStan ve que la función declara string como retorno, pero puede devolver void. PHP no te avisa hasta que alguien llama a la función con un usuario que no existe.
Arrays con claves inconsistentes
/** @return array<string, int> */
function get_counts(): array {
return [
'posts' => 5,
'pages' => 3,
'comments' => 'many', // string, no int
];
}
La anotación dice array<string, int> pero hay un string colado. PHPStan lo pilla. En runtime, el error se manifiesta cuando algo intenta hacer aritmética con 'many'.
phpstan-wordpress: entender el ecosistema
WordPress tiene convenciones que PHP estándar no entiende. apply_filters() devuelve tipos dinámicos. get_post_meta() puede devolver un valor o un array. Los hooks se registran como strings.
phpstan-wordpress resuelve esto con stub files y extensiones que enseñan a PHPStan:
- Los tipos de retorno de las funciones de la API de WordPress.
- Los patrones de hooks (
add_action,add_filter,apply_filters,do_action). - Las convenciones de tipos en
wp-includes. - Las constantes globales de WordPress.
Sin esta extensión, tendrías que silenciar cientos de false positives con @phpstan-ignore, lo cual derrota el propósito. Con ella, PHPStan entiende el contexto y solo te avisa de los problemas reales.
Anotaciones PHPDoc: el contrato que PHPStan lee
PHP no tiene tipos de retorno para todo, especialmente en WordPress, donde mucha API devuelve mixed. Las anotaciones PHPDoc son el contrato que PHPStan usa cuando el lenguaje no puede expresar la restricción:
/**
* Get the plugin settings.
*
* @since 1.0.0
* @return array{enabled: bool, threshold: int, label: string}
*/
public function get_settings(): array {
return get_option( 'my_plugin_settings', $this->defaults );
}
La anotación array{enabled: bool, threshold: int, label: string} le dice a PHPStan exactamente qué estructura esperar. Si algo accede a $settings['theshold'] (typo), nivel 9 lo detecta. Si algo espera un string en threshold, lo detecta.
Esto no es burocracia, es documentación ejecutable. Cada anotación que escribes es una promesa que PHPStan verifica.
Cuando PHPStan se equivoca: extensiones y supresiones
A veces PHPStan emite un false positive, un error que no lo es. Esto pasa especialmente con código WordPress que usa patrones dinámicos (hooks, metadatos, opciones serializadas).
La primera opción es escribir una extensión que enseñe a PHPStan el comportamiento específico. Esto es lo correcto si el patrón se repite.
La segunda opción es suprimir con @phpstan-ignore, pero solo con justificación documentada:
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $html; // @phpstan-ignore echo.nonString — Escaped in render_template()
La tercera opción (ignorar el error) no es una opción. Si PHPStan dice que algo puede fallar y tú no lo resuelves, estás asumiendo un riesgo que tendrás que depurar a las 3 de la mañana.
En el flujo de trabajo: antes del commit, siempre
PHPStan no es algo que ejecutas una vez al mes. Es parte del ciclo:
En cada commit:
vendor/bin/phpcspasa sin errores.vendor/bin/phpstan analysepasa nivel 9 sin errores.- No hay notices en
debug.log.
Antes del deploy:
- PHPStan nivel 9 en todos los archivos modificados.
- PHPCompatibility para el rango declarado.
- AI audit como capa final de revisión semántica.
Si algo falla en PHPStan, el commit no sale. Si el deploy se ejecuta con errores de PHPStan sin resolver, estás desplegando código que sabes que tiene puntos ciegos.
Lo que realmente cambia
La diferencia entre un proyecto con PHPStan nivel 9 y uno sin análisis estático no se nota el primer día. Se nota tres meses después, cuando añades una feature y PHPStan te avisa de que el nuevo método espera un WP_Post, pero le estás pasando un int. O cuando refactorizas y te dice que hay diecisiete sitios donde el tipo de retorno cambió y tres ya no cuadran.
PHPStan te da algo que PHP por sí solo no puede darte: certeza antes del runtime. Y en producción, con decenas de sitios dependiendo de tu código, la certeza no es un lujo, es la línea entre dormir tranquilo y no dormir.
Nivel 9 no es perfeccionismo. Es profesionalismo.





Deja una respuesta