Módulo 02: Sintaxis, variables y tipos

Objetivos del módulo

  • Usar correctamente las etiquetas de PHP y saber cuándo omitir la de cierre
  • Escribir instrucciones y comentarios (incluidos los bloques PHPDoc)
  • Mostrar información con echo y print, y mezclar PHP con HTML
  • Crear variables siguiendo las reglas y convenciones de nombres
  • Distinguir la asignación por valor de la asignación por referencia
  • Definir constantes con define() y const, y usar las constantes mágicas
  • Conocer todos los tipos de datos de PHP y cómo inspeccionarlos
  • Entender las conversiones de tipo automáticas y explícitas (y sus sorpresas)
  • Activar declare(strict_types=1) y usar declaraciones de tipo modernas
  • Diferenciar isset(), empty() e is_null()
  • Comprender el ámbito de las variables

1. Las etiquetas de PHP

Un fichero .php puede contener HTML normal y bloques de código PHP. El intérprete solo ejecuta lo que está entre las etiquetas de PHP; todo lo demás lo envía tal cual a la salida.

<p>Esto es HTML y sale tal cual.</p>
<?php
    echo '<p>Esto lo genera PHP.</p>';
?>
<p>Y esto vuelve a ser HTML.</p>
Etiqueta Significado ¿Usarla?
<?php ... ?> Bloque de código PHP ✅ Siempre
<?= expresión ?> Atajo de <?php echo expresión; ?> ✅ En plantillas HTML
<? ... ?> Etiqueta corta (short open tag) ❌ Depende de la configuración short_open_tag; no es portable

La etiqueta de impresión <?=

Es la forma más limpia de insertar un valor en HTML:

<?php $usuario = 'Lucía'; ?>

<!-- Forma larga -->
<p>Bienvenida, <?php echo $usuario; ?></p>

<!-- Forma corta, equivalente -->
<p>Bienvenida, <?= $usuario ?></p>

💡 Consejo: Dentro de <?= ?> no hace falta punto y coma: la etiqueta de cierre ya termina la instrucción.

Ficheros que solo contienen PHP: sin etiqueta de cierre

Cuando un fichero contiene únicamente código PHP (una clase, funciones, configuración…), la norma profesional es no poner la etiqueta de cierre ?>:

<?php

declare(strict_types=1);

function saludar(string $nombre): string
{
    return "Hola, $nombre";
}

// Fin del fichero: SIN ?>

⚠️ Importante: ¿Por qué omitirla? Cualquier espacio o salto de línea que quede después de ?> se envía al navegador como salida. Si después intentas enviar una cabecera HTTP (con header(), al iniciar una sesión o al poner una cookie), PHP dará el error “Cannot modify header information - headers already sent”. Sin ?> el problema desaparece. Lo recomiendan los estándares PSR y lo hacen todos los frameworks.


2. Instrucciones y mayúsculas

El punto y coma

Cada instrucción termina en punto y coma ;. Olvidarlo es el error más común al empezar:

<?php

$precio = 10      // ← falta el ;
echo $precio;
PHP Parse error:  syntax error, unexpected token "echo" in index.php on line 4

Fíjate en que PHP señala la línea 4, aunque el error está en la 3: el intérprete descubre el fallo al encontrar algo inesperado después. Cuando un error de sintaxis no tenga sentido en la línea indicada, mira la línea anterior.

¿Distingue mayúsculas y minúsculas?

Elemento ¿Distingue mayúsculas? Ejemplo
Variables ✅ Sí $nombre y $Nombre son variables distintas
Constantes ✅ Sí IVA e iva son distintas
Palabras clave ❌ No echo, ECHO y Echo funcionan igual
Funciones ❌ No strlen() y STRLEN() llaman a la misma función
Clases ❌ No DateTime y datetime son la misma clase

💡 Consejo: Aunque PHP te deje escribir ECHO o StrLen(), escribe siempre en minúsculas las palabras clave y las funciones nativas, y respeta el nombre exacto de clases y funciones. Es lo que esperan los demás programadores (y las herramientas de análisis).


3. Comentarios

Los comentarios son texto que el intérprete ignora. Sirven para explicar el porqué del código.

<?php

// Comentario de una línea (el más habitual)

# También es un comentario de una línea (estilo shell, poco usado)

/*
 * Comentario de varias líneas.
 * Útil para explicaciones largas o para desactivar
 * temporalmente un bloque de código.
 */

$total = 100; // También pueden ir al final de una línea

⚠️ Importante: Desde PHP 8, #[ inicia un atributo (como #[\Override] o #[\NoDiscard]), no un comentario. Nunca empieces un comentario con #[.

Comentarios de documentación: PHPDoc

Un bloque que empieza por /** (dos asteriscos) es un bloque PHPDoc. Lo leen los editores (Intelephense), los generadores de documentación y las herramientas de análisis estático como PHPStan:

<?php

declare(strict_types=1);

/**
 * Calcula el precio final aplicando el IVA.
 *
 * @param float $base    Precio sin impuestos.
 * @param float $tipoIva Tipo de IVA en tanto por uno (0.21 = 21 %).
 *
 * @return float Precio con IVA, redondeado a 2 decimales.
 */
function precioConIva(float $base, float $tipoIva = 0.21): float
{
    return round($base * (1 + $tipoIva), 2);
}

/** @var list<string> $ciudades Lista de ciudades sin claves */
$ciudades = ['Madrid', 'Sevilla', 'Bilbao'];

💡 Consejo: Con las declaraciones de tipo modernas, muchos @param y @return sobran. Usa PHPDoc para lo que el tipo no dice: qué significa el valor, sus unidades, o tipos más precisos como list<string> o array<string, int> que PHP todavía no puede expresar por sí mismo.


4. Mostrar información: echo y print

echo

echo envía uno o varios valores a la salida. No es una función, sino una construcción del lenguaje, por eso no necesita paréntesis:

<?php

echo 'Hola';
echo 'Hola', ' ', 'mundo', PHP_EOL;   // Varios argumentos separados por comas
echo 'Total: ' . 42 . PHP_EOL;        // Concatenando con el operador .

print

print hace casi lo mismo, pero solo admite un argumento y devuelve siempre 1, lo que le permite usarse dentro de expresiones (algo que en la práctica casi nunca se hace):

<?php

print 'Hola mundo' . PHP_EOL;
$resultado = print '';   // $resultado vale 1
Característica echo print
Número de argumentos Uno o varios (separados por comas) Solo uno
Valor de retorno Ninguno Siempre 1
¿Se puede usar en una expresión? No Sí
Velocidad Ligeramente más rápido Prácticamente igual
Uso habitual ✅ El estándar Poco frecuente

💡 Consejo: Cuando uses var_dump() o print_r() en el navegador, envuélvelos en <pre> para que se respeten los saltos de línea: echo '<pre>'; var_dump($datos); echo '</pre>';. Con Xdebug en modo develop, var_dump() ya sale formateado.

Saltos de línea: PHP_EOL frente a <br>

  • En la terminal, un salto de línea es "\n" (o PHP_EOL).
  • En el navegador, los saltos de línea del código se ignoran: necesitas <br> o etiquetas de bloque como <p>.

Mezclar PHP y HTML

La gran fortaleza de PHP es poder “entrar y salir” del HTML. Patrón recomendado: primero la lógica (arriba, en PHP) y después la presentación (abajo, HTML con <?= ?>):

<?php
declare(strict_types=1);

// 1. Lógica: preparamos los datos
$tienda    = 'Librería Cervantes';
$producto  = 'El Quijote (edición ilustrada)';
$precio    = 24.90;
$enOferta  = true;
$precioFinal = $enOferta ? $precio * 0.8 : $precio;
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title><?= $tienda ?></title>
</head>
<body>
    <!-- 2. Presentación: solo mostramos -->
    <h1><?= $tienda ?></h1>
    <article>
        <h2><?= $producto ?></h2>
        <p>Precio: <?= number_format($precioFinal, 2, ',', '.') ?> €</p>
        <?php if ($enOferta): ?>
            <p><strong>¡20 % de descuento!</strong></p>
        <?php endif; ?>
    </article>
</body>
</html>

⚠️ Importante: En este ejemplo los datos los escribimos nosotros. Cuando un dato venga del usuario (un formulario, la URL), habrá que escaparlo con htmlspecialchars() antes de mostrarlo para evitar ataques XSS. Lo verás en los módulos 06, 08 y 16.


5. Variables

📘 Concepto: Una variable es una caja con nombre donde guardas un valor para usarlo después. En PHP, el nombre de toda variable empieza por el símbolo $. No hace falta declararla antes: la variable nace la primera vez que le asignas un valor.

<?php

$nombre = 'Carlos';     // Texto (string)
$edad = 34;             // Número entero (int)
$altura = 1.78;         // Número decimal (float)
$esSocio = true;        // Booleano (bool)

echo "$nombre tiene $edad años" . PHP_EOL;   // Carlos tiene 34 años

$edad = 35;             // Se puede cambiar el valor...
$edad = 'treinta y cinco'; // ...e incluso el tipo (tipado dinámico)

Reglas para los nombres

Regla Válido No válido
Empieza por $ $total total
Después de $, una letra o _ $_temporal, $precio $1precio
Luego letras, números o _ $precio2, $precio_final $precio-final, $precio final
Distingue mayúsculas $total ≠ $Total —
$this está reservada — $this = 5;

💡 Consejo: PHP admite tildes y eñes en los nombres ($año funciona), pero evítalas: pueden dar problemas con codificaciones y herramientas, y dificultan la colaboración internacional. Usa $anio o $ejercicio.

Convenciones de nombres

Estilo Ejemplo Dónde se usa en PHP
camelCase $precioFinal, calcularTotal() Variables, funciones de usuario, métodos (lo usaremos en el curso)
PascalCase CarritoCompra Clases, interfaces, enums
snake_case $precio_final, str_replace() Funciones nativas de PHP, algunos proyectos (WordPress)
MAYÚSCULAS_CON_GUIONES IVA_GENERAL, MAX_INTENTOS Constantes

💡 Consejo: Lo importante es ser coherente dentro del proyecto. Y elige nombres que expliquen el contenido: $precioConIva es infinitamente mejor que $p2 o $x.

Asignación por valor

Por defecto, al asignar una variable a otra se copia el valor. Las dos variables son independientes:

<?php

$original = 10;
$copia = $original;   // Se copia el valor 10

$copia = 99;

echo $original;  // 10  ← no ha cambiado
echo $copia;     // 99

Asignación por referencia

Con & haces que dos nombres apunten al mismo valor. Es como poner dos etiquetas en la misma caja:

<?php

$original = 10;
$alias = &$original;  // $alias es otro nombre para la misma caja

$alias = 99;

echo $original;  // 99  ← ¡ha cambiado!
echo $alias;     // 99

unset($alias);   // Quita la etiqueta $alias; la caja sigue existiendo
echo $original;  // 99

💡 Consejo: Las referencias tienen usos legítimos (modificar un array dentro de un foreach, parámetros que una función debe modificar), pero hacen el código más difícil de seguir. Úsalas solo cuando sea necesario. Los objetos, como verás en el módulo 11, ya se comportan de forma parecida sin necesidad de &.


6. Constantes

📘 Concepto: Una constante es un nombre asociado a un valor que no puede cambiar durante la ejecución. Úsalas para valores fijos: el tipo de IVA, el número máximo de intentos de login, la versión de tu aplicación. No llevan $.

const y define()

<?php

declare(strict_types=1);

// Con const (en tiempo de compilación) - la forma recomendada
const IVA_GENERAL = 0.21;
const NOMBRE_APP = 'Mi Tienda';
const DIAS_SEMANA = ['lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'];

// Con define() (en tiempo de ejecución)
define('MAX_INTENTOS', 3);

echo NOMBRE_APP . PHP_EOL;            // Mi Tienda
echo 100 * (1 + IVA_GENERAL) . PHP_EOL; // 121
echo DIAS_SEMANA[0] . PHP_EOL;        // lunes

// IVA_GENERAL = 0.10;  // ❌ Error de sintaxis: no se puede reasignar
Característica const define()
Momento Al compilar Al ejecutar
¿Dentro de un if o función? ❌ No (solo en el nivel superior o en clases) ✅ Sí
¿Nombre calculado? ❌ No ✅ Sí: define('CLAVE_' . $x, 1)
¿En clases? ✅ Sí (constantes de clase) ❌ No
Respeta namespaces ✅ Sí Hay que indicar el nombre completo
Recomendación ✅ La opción por defecto Solo si necesitas definirla condicionalmente

Constantes predefinidas útiles

Constante Valor de ejemplo Significado
PHP_VERSION "8.5.3" Versión de PHP
PHP_OS_FAMILY "Windows", "Linux", "Darwin" Familia del sistema operativo
PHP_EOL "\n" o "\r\n" Salto de línea del sistema
PHP_INT_MAX 9223372036854775807 Mayor entero posible (64 bits)
PHP_INT_MIN -9223372036854775808 Menor entero posible
PHP_FLOAT_EPSILON 2.220446049250313E-16 Menor diferencia distinguible entre floats
M_PI 3.141592653589793 El número π
E_ALL — Nivel de error “todos los errores”

Constantes mágicas

Las constantes mágicas cambian de valor según dónde se usen. Empiezan y terminan con dos guiones bajos:

Constante Valor
__LINE__ Número de línea actual
__FILE__ Ruta completa del fichero actual
__DIR__ Carpeta del fichero actual (equivale a dirname(__FILE__))
__FUNCTION__ Nombre de la función actual
__CLASS__ Nombre de la clase actual
__METHOD__ Nombre del método actual (Clase::metodo)
__NAMESPACE__ Namespace actual
__TRAIT__ Nombre del trait actual
__PROPERTY__ Nombre de la propiedad, dentro de un property hook (PHP 8.4)
<?php

echo 'Estás en la línea ' . __LINE__ . PHP_EOL;     // Estás en la línea 3
echo 'Fichero: ' . __FILE__ . PHP_EOL;              // Fichero: C:\curso-php\scripts\magicas.php
echo 'Carpeta: ' . __DIR__ . PHP_EOL;               // Carpeta: C:\curso-php\scripts

function depurar(): void
{
    echo 'Dentro de ' . __FUNCTION__ . PHP_EOL;     // Dentro de depurar
}

depurar();

💡 Consejo: __DIR__ es imprescindible para incluir ficheros de forma fiable: require __DIR__ . '/config.php'; funciona siempre, se ejecute el script desde donde se ejecute. Una ruta relativa como require 'config.php'; depende de la carpeta de trabajo y puede fallar.


7. Tipos de datos

PHP tiene estos tipos:

Categoría Tipo Ejemplo Descripción
Escalares bool true, false Verdadero o falso
  int 42, -7, 0x1F Número entero
  float 3.14, 1.5e3 Número decimal (coma flotante)
  string 'Hola', "Adiós" Cadena de texto
Compuestos array [1, 2, 3], ['a' => 1] Colección ordenada de pares clave-valor
  object new DateTime() Instancia de una clase
  callable 'strlen', fn($x) => $x * 2 Algo que se puede llamar como función
  iterable Un array o un objeto recorrible Cualquier cosa que admita foreach
Especiales null null Ausencia de valor
  resource Un fichero abierto con fopen() Referencia a un recurso externo (tipo heredado)
Pseudotipo mixed Cualquiera Acepta cualquier valor (solo en declaraciones de tipo)

int: enteros

<?php

$decimal     = 255;
$negativo    = -42;
$hexadecimal = 0xFF;        // 255 en base 16
$octal       = 0o377;       // 255 en base 8 (prefijo 0o desde PHP 8.1)
$binario     = 0b11111111;  // 255 en base 2
$legible     = 1_000_000;   // Separador de miles (PHP 7.4): vale 1000000

var_dump(PHP_INT_MAX);      // int(9223372036854775807) en sistemas de 64 bits
var_dump(PHP_INT_MAX + 1);  // float(9.2233720368547758E+18)  ← ¡se convierte en float!

📘 Concepto: Si un entero supera PHP_INT_MAX, PHP no da error: lo convierte silenciosamente en float (perdiendo precisión). Para números realmente grandes (criptografía, finanzas de altísima precisión) existen las extensiones BCMath y GMP.

float: números decimales

<?php

$precio = 19.99;
$cientifica = 1.5e3;   // 1500.0
$pequeno = 2.5E-4;     // 0.00025

var_dump(0.1 + 0.2);          // float(0.30000000000000004)
var_dump(0.1 + 0.2 == 0.3);   // bool(false)  ← ¡sorpresa!

⚠️ Importante: Los float se guardan en binario y muchos decimales (como 0.1) no tienen representación exacta, igual que 1/3 no la tiene en decimal (0,3333…). Nunca compares floats con ==. Compara con una tolerancia o redondea:

<?php

$a = 0.1 + 0.2;
$b = 0.3;

var_dump(abs($a - $b) < PHP_FLOAT_EPSILON);  // bool(true)
var_dump(round($a, 2) === round($b, 2));     // bool(true)

💡 Consejo: Para dinero, la práctica profesional es trabajar en céntimos con enteros (1999 en lugar de 19.99) o usar una librería de dinero/BCMath. Así evitas errores de redondeo acumulados.

null: ausencia de valor

null es el único valor del tipo null y significa “no hay valor”. Una variable vale null si se le asigna explícitamente o si se ha eliminado con unset():

<?php

$fechaBaja = null;   // El cliente no se ha dado de baja
var_dump($fechaBaja); // NULL

8. Inspeccionar tipos

var_dump(): la navaja suiza

<?php

var_dump(42);           // int(42)
var_dump(3.14);         // float(3.14)
var_dump('Hola');       // string(4) "Hola"
var_dump(true);         // bool(true)
var_dump(null);         // NULL

📘 Concepto: En string(4) "Hola", el número es la longitud en bytes, no en caracteres. Por eso var_dump('ñu') muestra string(3): la ñ ocupa 2 bytes en UTF-8. Lo verás en el módulo de cadenas.

gettype() frente a get_debug_type()

Valor gettype() get_debug_type() (PHP 8.0)
42 "integer" "int"
3.14 "double" "float"
'hola' "string" "string"
true "boolean" "bool"
null "NULL" "null"
[1, 2] "array" "array"
new DateTime() "object" "DateTime"
fn() => 1 "object" "Closure"

💡 Consejo: Usa get_debug_type(): devuelve los mismos nombres que usas en las declaraciones de tipo (int, float, bool) y, para objetos, el nombre de la clase. gettype() mantiene nombres históricos confusos como "double".


9. Conversiones de tipo

PHP convierte tipos de dos formas: explícitamente (tú lo pides) o automáticamente (type juggling, “malabarismo de tipos”).

Conversión explícita: casting

Se escribe el tipo entre paréntesis delante del valor:

<?php

var_dump((int) '42');         // int(42)
var_dump((int) '42.9');       // int(42)   - trunca, no redondea
var_dump((int) 3.99);         // int(3)
var_dump((int) -3.99);        // int(-3)
var_dump((int) '12abc');      // int(12)   - lee hasta donde puede
var_dump((int) 'abc');        // int(0)
var_dump((float) '3.14xyz');  // float(3.14)
var_dump((float) '1,5');      // float(1)  - ¡la coma no es separador decimal!
var_dump((string) 42);        // string(2) "42"
var_dump((string) true);      // string(1) "1"
var_dump((string) false);     // string(0) ""
var_dump((bool) 'false');     // bool(true) - ¡cualquier cadena no vacía salvo "0"!
var_dump((array) 'hola');     // array(1) { [0]=> string(4) "hola" }

⚠️ Importante: PHP 8.5 declara obsoletos los nombres alternativos de los casts: (integer), (boolean), (double) y (binary). Usa siempre las formas canónicas (int), (bool), (float) y (string). Además, PHP 8.5 avisa cuando conviertes a int un float que no cabe en un entero (como 1e20 o NAN).

¿Qué es “verdadero” y qué es “falso”?

Cuando PHP necesita un booleano (en un if, por ejemplo), convierte el valor. Estos valores se consideran falsos (falsy); todo lo demás es verdadero:

Valor (bool)
false false
0 y -0 false
0.0 false
'' (cadena vacía) false
'0' (la cadena con un cero) false
[] (array vacío) false
null false
'0.0', ' ', 'false' ⚠️ true (¡son cadenas no vacías y distintas de '0'!)
[0], [false] ⚠️ true (el array no está vacío)

Conversión automática: type juggling

En modo no estricto, PHP convierte los operandos según la operación:

<?php

var_dump('10' + 5);       // int(15)    - la cadena se convierte en número
var_dump('3.5' * 2);      // float(7)
var_dump('5' . 5);        // string(2) "55" - el . convierte a cadena
var_dump(true + true);    // int(2)
var_dump(null + 5);       // int(5)
var_dump(10 / 4);         // float(2.5) - la división puede dar float
var_dump(10 / 2);         // int(5)
var_dump('12abc' + 1);    // int(13) + Warning: A non-numeric value encountered
var_dump('abc' + 1);      // TypeError: Unsupported operand types: string + int

Tabla de sorpresas

Expresión Resultado Por qué
'10' + '5' 15 + siempre es suma numérica
'10' . '5' '105' . siempre es concatenación
0.1 + 0.2 == 0.3 false Imprecisión de los float
(int) '1e3' 1000 '1e3' es notación científica
(int) '0x1A' 0 Las cadenas hexadecimales no se interpretan
(bool) '0' false La cadena '0' es el único texto no vacío falso
(bool) '0.0' true En cambio, '0.0' es verdadero
(float) '1,5' 1.0 PHP usa el punto como separador decimal
'5' * '4' 20 (int) Las cadenas numéricas se convierten en número al operar
'abc' == 0 false en PHP 8 (true en PHP 7) PHP 8 cambió la comparación cadena-número
PHP_INT_MAX + 1 float Desbordamiento a float
7 / 2 3.5 La división no es entera; usa intdiv(7, 2) → 3

💡 Consejo: Las conversiones automáticas son la fuente de muchos errores difíciles de encontrar. La solución moderna es combinar declaraciones de tipo, strict_types y la comparación estricta === (módulo 03).


10. Tipado estricto y declaraciones de tipo

Declaraciones de tipo

Puedes indicar el tipo de los parámetros y del valor de retorno de una función. PHP lo comprobará en cada llamada:

<?php

function doble(int $numero): int
{
    return $numero * 2;
}

echo doble(21);   // 42

Modo coercitivo (por defecto) frente a modo estricto

Sin strict_types, PHP intenta convertir el argumento al tipo esperado:

<?php
// SIN declare(strict_types=1)

function doble(int $numero): int
{
    return $numero * 2;
}

echo doble('5');     // 10  ← convierte '5' en 5 sin avisar
echo doble('5.0');   // 10
echo doble('cinco'); // TypeError: doble(): Argument #1 ($numero) must be of type int, string given

Con strict_types, el tipo debe coincidir exactamente:

<?php

declare(strict_types=1);  // Debe ser la PRIMERA instrucción del fichero

function doble(int $numero): int
{
    return $numero * 2;
}

echo doble(5);     // 10
echo doble('5');   // TypeError: doble(): Argument #1 ($numero) must be of type int, string given

📘 Concepto: declare(strict_types=1) afecta a las llamadas hechas desde ese fichero (y a los return de las funciones definidas en él). Por eso debe estar en todos los ficheros. Hay una única excepción a la rigidez: se permite pasar un int donde se espera un float (5 se convierte en 5.0), porque no se pierde información.

💡 Consejo: En este curso todos los ejemplos usan declare(strict_types=1);. Es la práctica habitual en proyectos modernos, Symfony y Laravel. Los errores de tipo aparecen al instante y en la línea exacta, en lugar de convertirse en datos corruptos que descubres semanas después.

Tipos disponibles en declaraciones

Declaración Significado Desde
int, float, string, bool Escalares PHP 7.0
array, callable, iterable, object Compuestos PHP 7.x
NombreClase Un objeto de esa clase (o hija) PHP 5
?string Nullable: string o null PHP 7.1
void La función no devuelve nada (solo retorno) PHP 7.1
int\|float Unión: uno u otro PHP 8.0
mixed Cualquier valor PHP 8.0
static La clase actual (solo retorno, en métodos) PHP 8.0
never La función nunca termina normalmente (solo retorno) PHP 8.1
A&B Intersección: cumple ambas interfaces PHP 8.1
null, false, true Como tipos por sí mismos PHP 8.2
(A&B)\|null Tipos DNF (combinación de unión e intersección) PHP 8.2

Tipos unión

<?php

declare(strict_types=1);

function formatearPrecio(int|float $precio): string
{
    return number_format($precio, 2, ',', '.') . ' €';
}

echo formatearPrecio(10) . PHP_EOL;      // 10,00 €
echo formatearPrecio(1234.5) . PHP_EOL;  // 1.234,50 €

Tipos nullable

?string es una forma abreviada de string|null. Úsalo cuando un valor puede faltar:

<?php

declare(strict_types=1);

function saludar(?string $nombre = null): string
{
    // ?? devuelve 'invitado' si $nombre es null (módulo 03)
    return 'Hola, ' . ($nombre ?? 'invitado');
}

echo saludar('Marta') . PHP_EOL;  // Hola, Marta
echo saludar() . PHP_EOL;         // Hola, invitado

void y never

<?php

declare(strict_types=1);

// void: hace algo, pero no devuelve ningún valor
function registrar(string $mensaje): void
{
    echo '[' . date('H:i:s') . "] $mensaje" . PHP_EOL;
    // return 5;  ❌ Error: una función void no puede devolver un valor
}

// never: la función NUNCA vuelve (lanza una excepción o termina el script)
function abortar(string $motivo): never
{
    throw new RuntimeException($motivo);
}

registrar('Inicio del proceso');

false y true como tipos

Muchas funciones nativas antiguas devuelven un valor o false si fallan (por ejemplo, strpos()). Con PHP 8 puedes expresarlo con precisión:

<?php

declare(strict_types=1);

/** Devuelve la posición del primer dígito o false si no hay ninguno. */
function posicionPrimerDigito(string $texto): int|false
{
    for ($i = 0; $i < strlen($texto); $i++) {
        if (ctype_digit($texto[$i])) {
            return $i;
        }
    }
    return false;
}

var_dump(posicionPrimerDigito('abc7d'));  // int(3)
var_dump(posicionPrimerDigito('abcd'));   // bool(false)

💡 Consejo: false como tipo se usa casi siempre en uniones (int|false) para reflejar el comportamiento de funciones heredadas. En tu código nuevo, para indicar “no hay resultado” suele ser más claro devolver null (?int) o lanzar una excepción.


11. isset(), empty() e is_null()

Tres funciones que parecen similares pero responden preguntas distintas:

Función Pregunta que responde
isset($v) ¿Existe la variable y no es null?
empty($v) ¿No existe o su valor es “falso”?
is_null($v) ¿Su valor es null? (avisa si la variable no existe)

Tabla comparativa

Valor de $v isset($v) empty($v) is_null($v) (bool) $v
No definida false true true + Warning false + Warning
null false true true false
0 true true false false
'0' true true false false
'' true true false false
[] true true false false
false true true false false
'0.0' true false false true
'hola' true false false true
42 true false false true
<?php

$usuario = ['nombre' => 'Pablo', 'telefono' => null, 'hijos' => 0];

var_dump(isset($usuario['nombre']));    // bool(true)
var_dump(isset($usuario['telefono']));  // bool(false) - existe la clave pero vale null
var_dump(isset($usuario['email']));     // bool(false) - no existe (sin aviso)
var_dump(empty($usuario['hijos']));     // bool(true)  - ¡0 cuenta como vacío!

// Para saber si la clave existe aunque valga null:
var_dump(array_key_exists('telefono', $usuario));  // bool(true)

⚠️ Importante: Ten cuidado con empty(): considera vacíos el 0 y la cadena '0'. Si un usuario escribe “0” en el campo “número de hijos” de un formulario, empty() dirá que no ha rellenado el campo. Para validar formularios, compara explícitamente: $valor === '' o $valor === null.


12. Variables de variables (y por qué evitarlas)

PHP permite usar el valor de una variable como nombre de otra, con $$:

<?php

$campo = 'ciudad';
$$campo = 'Valencia';   // Crea la variable $ciudad

echo $ciudad;           // Valencia
echo ${$campo};         // Valencia (forma con llaves)

Parece ingenioso, pero es una mala práctica:

Problema Explicación
Ilegible Nadie sabe qué variables existen leyendo el código
Sin ayuda del editor Intelephense no puede autocompletar ni detectar errores
Inseguro Si el nombre viene del usuario, puede sobrescribir variables importantes ($esAdmin)
Difícil de depurar Las variables “aparecen” sin una asignación visible

La alternativa correcta casi siempre es un array asociativo:

<?php

// ❌ Con variables de variables
foreach (['nombre', 'email'] as $campo) {
    $$campo = $_POST[$campo] ?? '';
}

// ✅ Con un array
$datos = [];
foreach (['nombre', 'email'] as $campo) {
    $datos[$campo] = $_POST[$campo] ?? '';
}

echo $datos['nombre'];

13. Ámbito de las variables

📘 Concepto: El ámbito (scope) de una variable es la zona del código donde existe y se puede usar. En PHP es sencillo: una variable creada fuera de las funciones vive en el ámbito global; una creada dentro de una función vive solo en esa función (ámbito local).

Las funciones no ven las variables de fuera

A diferencia de JavaScript, en PHP una función no tiene acceso automático a las variables globales:

<?php

declare(strict_types=1);

$iva = 0.21;

function precioFinal(float $base): float
{
    return $base * (1 + $iva);   // Warning: Undefined variable $iva
}

global y $GLOBALS (desaconsejados)

Existen formas de acceder a las variables globales, pero crean dependencias ocultas y hacen el código difícil de probar:

<?php

$contadorVisitas = 10;

function incrementar(): void
{
    global $contadorVisitas;   // Importa la variable global
    $contadorVisitas++;
}

function mostrar(): void
{
    echo $GLOBALS['contadorVisitas'];   // Acceso a través del array $GLOBALS
}

incrementar();
mostrar();   // 11

⚠️ Importante: Evita global y $GLOBALS en tu código. Si una función necesita un dato, pásaselo como parámetro; si un valor no cambia nunca, usa una constante.

Variables static

Una variable static dentro de una función conserva su valor entre llamadas:

<?php

declare(strict_types=1);

function siguienteNumeroFactura(): int
{
    static $numero = 0;   // Solo se inicializa la primera vez
    return ++$numero;
}

echo siguienteNumeroFactura() . PHP_EOL;  // 1
echo siguienteNumeroFactura() . PHP_EOL;  // 2
echo siguienteNumeroFactura() . PHP_EOL;  // 3

📘 Concepto: Recuerda el modelo share nothing del módulo 00: una variable static solo vive durante una petición. En la siguiente petición vuelve a empezar en 0. Para numerar facturas de verdad, el contador tendría que estar en una base de datos.


14. Ejercicios

Ejercicio 1: Ficha personal

Crea ficha.php con declare(strict_types=1); y variables para tu nombre, edad, altura en metros, ciudad y si tienes carné de conducir. Muestra en la terminal, usando echo y PHP_EOL, una ficha como esta:

Nombre: Laura Martín
Edad: 29 años
Altura: 1.68 m
Ciudad: Zaragoza
Carné de conducir: sí

Pista: para el carné, usa $tieneCarne ? 'sí' : 'no'.

Ejercicio 2: Constantes de una tienda

Define con const las constantes NOMBRE_TIENDA, IVA_GENERAL (0.21), IVA_REDUCIDO (0.10) y GASTOS_ENVIO (4.95). Calcula y muestra el total de un pedido de 3 libros de 18,50 € (IVA reducido) más gastos de envío. Muestra también la línea en la que se hace el cálculo con __LINE__.

Ejercicio 3: Detective de tipos

Para cada uno de estos valores, muestra en una tabla HTML su valor con var_export(), el resultado de get_debug_type(), de (bool) y de (int): '0', '0.0', '', '12 monos', 3.99, -0.5, null, [], [0], true. Antes de ejecutarlo, apunta tus predicciones y compáralas.

Ejercicio 4: Modo estricto en acción

Escribe una función calcularMedia(float $a, float $b, float $c): float. Llámala con (5, 7, 9), con (5.5, '7', 9) y con (5, 7, null). Ejecútala primero sin strict_types y luego con él. Anota qué llamadas funcionan en cada caso y explica por qué 5 (int) se acepta como float incluso en modo estricto.

Ejercicio 5: Referencias

Predice la salida de este código y después compruébala:

<?php

$a = 1;
$b = $a;
$c = &$a;

$b = 2;
$c = 3;

echo "$a $b $c" . PHP_EOL;

unset($c);
$c = 4;

echo "$a $b $c" . PHP_EOL;

Ejercicio 6: Validación con isset y empty

Simula los datos de un formulario con el array $form = ['nombre' => 'Iván', 'edad' => '0', 'email' => '', 'telefono' => null];. Para cada campo (nombre, edad, email, telefono, direccion), muestra si “existe” (isset), si “está vacío” (empty) y si la clave está presente (array_key_exists). Explica por qué edad sale como vacía y cómo lo validarías correctamente.


Resumen

Concepto Sintaxis / ejemplo
Etiquetas <?php ... ?>, <?= $valor ?>
Fichero solo PHP Sin ?> al final
Comentarios // línea, /* bloque */, /** PHPDoc */
Salida echo 'a', 'b';, print 'a';
Variable $nombreVariable = valor;
Por referencia $alias = &$original;
Constantes const IVA = 0.21;, define('IVA', 0.21);
Constantes mágicas __LINE__, __FILE__, __DIR__, __FUNCTION__
Tipos escalares bool, int, float, string
Tipos compuestos array, object, callable, iterable
Especiales null, mixed
Ver el tipo get_debug_type($v) (mejor que gettype())
Casting (int), (float), (string), (bool), (array)
Modo estricto declare(strict_types=1); (primera línea)
Tipos en funciones function f(int $a): string
Unión / nullable int\|float, ?string
Retornos especiales void, never, int\|false, true
Comprobaciones isset(), empty(), is_null(), array_key_exists()
Ámbito Local en funciones; static conserva valor; evitar global