declare(strict_types=1) es una línea que se coloca al inicio de un archivo PHP para que, en ese archivo, las llamadas a funciones respeten al pie de la letra los tipos declarados. Sin ella, PHP intenta convertir los valores por su cuenta. Con ella, si una función espera un int y le pasas el texto "5", recibes un TypeError en lugar de una conversión silenciosa.

Tipos y strict_types en PHP 8
Foto: Markus Spiske markusspiske — CC0, vía Wikimedia Commons

Para entender por qué existe esa línea y cuándo te conviene, primero hay que repasar cómo maneja PHP las variables y los tipos. Este artículo va de lo básico a lo práctico, con ejemplos que puedes ejecutar en cualquier versión de PHP 8.

Variables: lo esencial en cinco reglas

Una variable en PHP es un nombre que apunta a un valor. Las reglas son pocas, pero conviene tenerlas claras:

  1. Empiezan con $ y siguen con una letra o guion bajo: $total, $_temporal, $usuario2.
  2. Distinguen mayúsculas y minúsculas: $Nombre y $nombre son dos variables distintas.
  3. No se declaran con un tipo: el tipo lo define el valor que guardan en cada momento.
  4. Dentro de una función solo existen las variables locales y los parámetros. Las de fuera no se ven, salvo que uses global (mejor evitarlo) o las pases como argumento.
  5. Leer una variable que no existe genera un aviso de tipo Warning desde PHP 8 («Undefined variable») y el valor resultante es null.
<?php
$precio = 100;          // ahora $precio contiene un int
$precio = '100.50';     // ahora contiene un string: PHP no se queja
$precio = 100.5;        // y ahora un float

// get_debug_type() (PHP 8.0+) devuelve el nombre del tipo de forma legible
echo get_debug_type($precio); // float

function aplicarIva(float $monto): float
{
    // $precio no existe aquí: cada función tiene su propio ámbito
    return $monto * 1.19;
}

echo aplicarIva($precio); // 119.595

Los tipos que maneja PHP 8

PHP tiene tipos escalares (valores simples), compuestos y especiales. Además, puedes usarlos en declaraciones de parámetros, valores de retorno y propiedades de clase.

TipoEjemplo de valorComentario
int42, -7, 0x1AEntero con signo, normalmente de 64 bits
float3.14, 1e3Decimal de coma flotante; no sirve para dinero exacto
string'hola', "hola $nombre"Secuencia de bytes; las comillas dobles interpolan
booltrue, falseResultado de comparaciones
array[1, 2], ['a' => 1]Lista y diccionario a la vez
objectnew DateTime()Instancia de una clase
nullnullAusencia de valor
callable'strlen', fn($x) => $xAlgo que se puede invocar
mixedcualquieraDesde PHP 8.0; acepta todo, incluido null

Y para las firmas de funciones hay tipos que solo tienen sentido ahí: void (no devuelve nada), never (la función nunca retorna, porque lanza una excepción o termina el script; PHP 8.1), static como tipo de retorno (PHP 8.0) y los literales true, false y null como tipos independientes (PHP 8.2).

Combinar tipos

Desde PHP 8.0 existen los tipos unión, que aceptan varias opciones: int|string. El atajo ?int equivale a int|null. PHP 8.1 añadió los tipos intersección (Countable&Iterator, el valor debe cumplir ambos) y PHP 8.2 permitió mezclarlos en forma normal disyuntiva, como (Countable&Iterator)|null.

<?php
declare(strict_types=1);

// Acepta un id numérico o un slug de texto
function buscarProducto(int|string $clave): ?array
{
    // ?array: devuelve un array o null si no lo encuentra
    return is_int($clave)
        ? buscarPorId($clave)
        : buscarPorSlug($clave);
}

// never: esta función siempre interrumpe el flujo
function abortar(string $mensaje): never
{
    throw new RuntimeException($mensaje);
}

Un detalle que conviene saber si mantienes código antiguo: escribir function f(int $x = null) hace que el parámetro sea anulable de forma implícita. Desde PHP 8.4 eso genera un aviso de obsolescencia; la forma correcta es declararlo explícitamente: function f(?int $x = null).

Modo coercitivo: lo que PHP hace sin pedirte permiso

Por defecto, PHP trabaja en modo coercitivo. Si una función declara int y recibe otro tipo, PHP intenta convertirlo. Algunas conversiones funcionan, otras fallan y otras funcionan con un aviso. Esto es lo que ocurre en PHP 8 con una función function f(int $n) sin strict_types:

LlamadaResultado en modo coercitivo
f("5")Recibe int(5)
f("5 manzanas")TypeError: el texto no es numérico
f("manzanas")TypeError
f(2.0)Recibe int(2)
f(1.5)Recibe int(1) con aviso Deprecated (desde PHP 8.1) por pérdida de precisión
f(true)Recibe int(1)
f(null)TypeError (para funciones de usuario)

El modo coercitivo es cómodo cuando trabajas con datos que llegan como texto, como los de un formulario. El problema es que oculta errores: si por un fallo en otra parte del código llega true en vez de un número, tu función recibe un 1 y sigue adelante como si nada.

Modo estricto: qué cambia con declare(strict_types=1)

Al añadir declare(strict_types=1); como primera instrucción del archivo, las llamadas hechas desde ese archivo dejan de convertir tipos. La única excepción permitida es pasar un int donde se espera float, porque esa conversión nunca pierde información.

<?php
declare(strict_types=1); // Debe ir antes de cualquier otra instrucción

function porcentaje(int $parte, int $total): float
{
    return $total === 0 ? 0.0 : ($parte / $total) * 100;
}

echo porcentaje(25, 200);   // 12.5

// Los datos de $_GET siempre son strings: aquí hay que convertir a mano
$parte = (int) ($_GET['parte'] ?? 0);
echo porcentaje($parte, 200);

echo porcentaje('25', 200); // TypeError: Argument #1 ($parte) must be of type int, string given

Las tres reglas que suelen confundir

  • Se aplica por archivo, no por proyecto. Cada archivo decide. No existe una opción global en php.ini.
  • Afecta a las llamadas que salen del archivo, no a las funciones definidas en él. Si el archivo A (estricto) llama a una función del archivo B (sin la línea), la llamada se comprueba en modo estricto. Si un archivo C sin la línea llama a esa misma función, PHP convierte tipos.
  • Los valores de retorno se comprueban según el archivo donde está definida la función. Si una función estricta declara : int y devuelve "3", falla aunque quien la llame no sea estricto.

También afecta a las funciones internas de PHP. En un archivo estricto, strlen(123) lanza TypeError porque strlen() espera un string.

Comparaciones: == frente a ===

El otro gran foco de errores con tipos no está en las funciones, sino en las comparaciones. El operador == compara valores después de convertir tipos; === exige que coincidan valor y tipo. PHP 8 corrigió el caso más absurdo (antes 0 == "a" era verdadero; ahora es falso), pero quedan resultados que sorprenden:

<?php
var_dump("1" == "01");     // true: ambos parecen números, se comparan como tales
var_dump("10" == "1e1");   // true: "1e1" es notación científica de 10
var_dump(100 == "1e2");    // true
var_dump(null == false);   // true
var_dump([] == false);     // true: un array vacío se considera falso
var_dump("abc" == 0);      // false en PHP 8 (era true en PHP 7)

var_dump("1" === "01");    // false: mismos tipos, distinto contenido

La recomendación práctica es usar === y !== por defecto, y reservar == para casos donde de verdad quieras comparar «por valor aproximado». Lo mismo aplica a funciones con parámetro de modo estricto: in_array($valor, $lista, true) y array_search($valor, $lista, true).

isset, empty y el operador ??

Tres herramientas sirven para lidiar con valores que pueden no existir, y cada una responde a una pregunta distinta:

  • isset($x): ¿la variable existe y no es null?
  • empty($x): ¿no existe o es «falsa» (0, "0", "", [], null, false)? Ojo: empty("0") es true, lo que puede descartar un valor válido.
  • $x ?? 'defecto': devuelve $x si existe y no es null; si no, el valor por defecto. No genera avisos.
<?php
// Leer un parámetro opcional de la URL sin avisos
$pagina = (int) ($_GET['pagina'] ?? 1);

// Asignar solo si la clave no existe o es null (PHP 7.4+)
$config['idioma'] ??= 'es';

Propiedades tipadas y constantes tipadas

Los tipos no se limitan a funciones. Desde PHP 7.4 las propiedades de clase pueden tener tipo, y desde PHP 8.3 también las constantes de clase:

<?php
declare(strict_types=1);

final class Pedido
{
    public const string MONEDA = 'COP'; // constante tipada (PHP 8.3+)

    public function __construct(
        public readonly int $id,      // promoción de propiedad (PHP 8.0) + readonly (8.1)
        private float $total = 0.0,
    ) {}

    public function sumar(float $importe): void
    {
        $this->total += $importe;
    }
}

$p = new Pedido(10);
$p->sumar(15000);      // int a float: permitido incluso en modo estricto
// $p->id = 11;        // Error: no se puede modificar una propiedad readonly

Una propiedad tipada sin valor inicial queda en estado «no inicializado». Leerla antes de asignarla lanza un Error, a diferencia de las propiedades sin tipo, que valen null. Esto ayuda a detectar objetos construidos a medias.

¿Conviene activar strict_types en todo el proyecto?

En código nuevo, sí. La mayoría de librerías modernas lo usan, y herramientas como PHP-CS-Fixer pueden añadir la línea automáticamente a cada archivo. Su ventaja principal no es el rendimiento, sino que los errores aparecen donde se originan, con un mensaje claro, en vez de propagarse como datos raros.

En un proyecto heredado conviene ir archivo por archivo, empezando por las clases con más pruebas automatizadas. Activarlo en todo de golpe suele destapar decenas de llamadas que dependían de conversiones implícitas, sobre todo con datos de formularios y de bases de datos (los drivers suelen devolver números como strings según la configuración).

Lo que debes llevarte:

  • Declara tipos en parámetros, retornos y propiedades siempre que puedas.
  • Pon declare(strict_types=1); al inicio de cada archivo nuevo.
  • Convierte explícitamente los datos externos ((int), (float), filter_var()) justo en el punto donde entran.
  • Compara con === y usa el tercer parámetro true en in_array().

Leave a comment