Módulo 04: Estructuras de control

Objetivos del módulo

  • Tomar decisiones con if, elseif y else
  • Usar la sintaxis alternativa (if: ... endif;) en plantillas HTML
  • Comprender switch y su comportamiento de “caída” (fallthrough)
  • Dominar la expresión match de PHP 8 y saber cuándo preferirla a switch
  • Repetir código con while, do-while, for y foreach
  • Recorrer arrays con foreach por clave y por referencia, evitando la referencia colgante
  • Controlar los bucles con break y continue, incluidos los niveles
  • Construir programas completos: tabla de multiplicar, calculadora, FizzBuzz y un menú de consola

1. ¿Qué son las estructuras de control?

📘 Concepto: Por defecto, PHP ejecuta las instrucciones una detrás de otra, de arriba abajo. Las estructuras de control permiten cambiar ese flujo: elegir qué código ejecutar según una condición (estructuras condicionales) o repetir código varias veces (bucles).

Piensa en un GPS: normalmente sigue recto (secuencia), pero en cada cruce decide hacia dónde girar (condicional), y en una rotonda puede dar vueltas hasta encontrar la salida (bucle).


2. if, elseif y else

if simple

Ejecuta un bloque solo si la condición es verdadera:

<?php

declare(strict_types=1);

$temperatura = 32;

if ($temperatura > 30) {
    echo 'Hace mucho calor. ¡Bebe agua!' . PHP_EOL;
}

if / else

<?php

declare(strict_types=1);

$edad = 16;

if ($edad >= 18) {
    echo 'Puedes votar.' . PHP_EOL;
} else {
    $faltan = 18 - $edad;
    echo "Podrás votar dentro de $faltan años." . PHP_EOL;
}

if / elseif / else

Cuando hay varias opciones, PHP comprueba las condiciones en orden y ejecuta solo la primera que se cumple:

<?php

declare(strict_types=1);

$nota = 7.8;

if ($nota >= 9) {
    $calificacion = 'Sobresaliente';
} elseif ($nota >= 7) {
    $calificacion = 'Notable';
} elseif ($nota >= 6) {
    $calificacion = 'Bien';
} elseif ($nota >= 5) {
    $calificacion = 'Suficiente';
} else {
    $calificacion = 'Insuficiente';
}

echo "Nota $nota: $calificacion" . PHP_EOL;   // Nota 7.8: Notable

⚠️ Importante: El orden de las condiciones importa. Si pusieras primero $nota >= 5, una nota de 9,5 entraría ahí y saldría “Suficiente”. Ordena de la condición más restrictiva a la más general.

💡 Consejo: En PHP puedes escribir elseif (junto) o else if (separado) y funcionan igual con llaves. Usa elseif: es la forma recomendada por el estándar PER y la única válida en la sintaxis alternativa.

Llaves siempre

PHP permite omitir las llaves si el bloque tiene una única instrucción, pero es una fuente clásica de errores:

<?php

$stock = 0;

// ❌ Peligroso: parece que las dos líneas dependen del if, pero solo la primera
if ($stock === 0)
    echo 'Agotado';
    echo 'Avisaremos al proveedor';   // ¡Se ejecuta SIEMPRE!

// ✅ Con llaves no hay ambigüedad
if ($stock === 0) {
    echo 'Agotado';
    echo 'Avisaremos al proveedor';
}

Cláusulas de guarda: menos anidamiento

Cuando hay muchas condiciones anidadas, el código se convierte en una “flecha” difícil de leer. La técnica de las cláusulas de guarda consiste en descartar primero los casos inválidos y salir pronto:

<?php

declare(strict_types=1);

// Cada problema se descarta y se sale; el caso normal queda al final
function puedeComprarMejor(?array $usuario, float $importe): string
{
    if ($usuario === null) {
        return 'Debes iniciar sesión';
    }

    if (!$usuario['activo']) {
        return 'Cuenta desactivada';
    }

    if ($usuario['saldo'] < $importe) {
        return 'Saldo insuficiente';
    }

    return 'Compra realizada';
}

echo puedeComprarMejor(['activo' => true, 'saldo' => 50.0], 30.0) . PHP_EOL;  // Compra realizada

3. Sintaxis alternativa para plantillas HTML

Cuando mezclas PHP con mucho HTML, las llaves { } se pierden entre las etiquetas. PHP ofrece una sintaxis alternativa que sustituye la llave de apertura por : y la de cierre por una palabra clave (endif, endforeach, endfor, endwhile, endswitch):

<?php
declare(strict_types=1);

$usuario = ['nombre' => 'Rocío', 'rol' => 'admin'];
$notificaciones = 3;
?>
<nav>
    <?php if ($usuario === null): ?>
        <a href="/login">Iniciar sesión</a>
    <?php elseif ($usuario['rol'] === 'admin'): ?>
        <a href="/admin">Panel de administración</a>
        <span>Hola, <?= $usuario['nombre'] ?></span>
    <?php else: ?>
        <span>Hola, <?= $usuario['nombre'] ?></span>
    <?php endif; ?>

    <?php if ($notificaciones > 0): ?>
        <span class="badge"><?= $notificaciones ?></span>
    <?php endif; ?>
</nav>
Con llaves Sintaxis alternativa
if (...) { ... } if (...): ... endif;
} elseif (...) { elseif (...):
} else { else:
foreach (...) { ... } foreach (...): ... endforeach;
for (...) { ... } for (...): ... endfor;
while (...) { ... } while (...): ... endwhile;
switch (...) { ... } switch (...): ... endswitch;

💡 Consejo: Usa la sintaxis alternativa solo en las plantillas (ficheros donde predomina el HTML) y las llaves en el código PHP puro. Es la convención de WordPress y de muchas aplicaciones. Cuando tu endif; está a 40 líneas de su if, se agradece saber qué estructura cierra.

⚠️ Importante: No mezcles estilos en la misma estructura: un if (...): debe cerrarse con endif;, nunca con }.


4. switch

switch compara una expresión con varios valores (case) y ejecuta el código del que coincida:

<?php

declare(strict_types=1);

$dia = 'martes';

switch ($dia) {
    case 'lunes':
        echo 'Reunión de equipo a las 9:00';
        break;
    case 'martes':
        echo 'Revisión de código';
        break;
    case 'viernes':
        echo 'Despliegue semanal';
        break;
    default:
        echo 'Día de desarrollo normal';
}
// Revisión de código
  • case valor: marca un punto de entrada.
  • break sale del switch.
  • default: se ejecuta si ningún case coincide (es opcional).

La “caída” (fallthrough)

Si olvidas el break, PHP sigue ejecutando los case siguientes aunque no coincidan:

<?php

$nivel = 2;

switch ($nivel) {
    case 1:
        echo 'Nivel 1 ';
    case 2:
        echo 'Nivel 2 ';
    case 3:
        echo 'Nivel 3 ';
        break;
    case 4:
        echo 'Nivel 4 ';
}
// Nivel 2 Nivel 3

A veces se usa a propósito para agrupar casos:

<?php

declare(strict_types=1);

$mes = 4;

switch ($mes) {
    case 12:
    case 1:
    case 2:
        $estacion = 'Invierno';
        break;
    case 3:
    case 4:
    case 5:
        $estacion = 'Primavera';
        break;
    case 6:
    case 7:
    case 8:
        $estacion = 'Verano';
        break;
    default:
        $estacion = 'Otoño';
}

echo $estacion;   // Primavera

⚠️ Importante: switch compara con == (igualdad débil), con todas las sorpresas que viste en el módulo 03. Por ejemplo, si $x = '1', entrará en case 1:. Esto, junto con la caída accidental por olvidar un break, es la razón por la que PHP 8 introdujo match.


5. match (PHP 8)

📘 Concepto: match es una expresión que compara un valor con varias alternativas de forma estricta (===) y devuelve el resultado de la que coincide. Es como una máquina expendedora: introduces un código y te devuelve exactamente un producto.

<?php

declare(strict_types=1);

$codigoHttp = 404;

$mensaje = match ($codigoHttp) {
    200 => 'OK',
    201 => 'Creado',
    301, 302 => 'Redirección',          // Varios valores separados por comas
    404 => 'No encontrado',
    500 => 'Error interno del servidor',
    default => 'Código desconocido',
};

echo $mensaje;   // No encontrado

Características de match

Característica Explicación
Devuelve un valor Se puede asignar a una variable, pasar a una función o usar en return
Comparación estricta Usa ===: '1' no coincide con 1
Sin break Cada rama ejecuta una sola expresión y termina; no hay caída
Varios valores por rama 301, 302 => ...
Exhaustivo Si ningún valor coincide y no hay default, lanza UnhandledMatchError
Coma final permitida Puedes dejar , tras la última rama (recomendado)

UnhandledMatchError

<?php

declare(strict_types=1);

$talla = 'XXL';

$precio = match ($talla) {
    'S', 'M' => 15.0,
    'L', 'XL' => 17.5,
};
// Fatal error: Uncaught UnhandledMatchError: Unhandled match case 'XXL'

Esto es una ventaja: en lugar de continuar con un valor incorrecto (como pasaría con un switch sin default), el programa avisa del caso que no has contemplado. Puedes incluso lanzar tu propia excepción en el default:

<?php

declare(strict_types=1);

$talla = 'XXL';

$precio = match ($talla) {
    'S', 'M' => 15.0,
    'L', 'XL' => 17.5,
    default => throw new InvalidArgumentException("Talla no válida: $talla"),
};

match(true): condiciones en lugar de valores

Si pasas true como valor, cada rama puede ser una condición. Se ejecuta la primera que valga true. Es la forma moderna de sustituir una cadena de if/elseif que asigna un valor:

<?php

declare(strict_types=1);

$nota = 7.8;

$calificacion = match (true) {
    $nota >= 9 => 'Sobresaliente',
    $nota >= 7 => 'Notable',
    $nota >= 6 => 'Bien',
    $nota >= 5 => 'Suficiente',
    default => 'Insuficiente',
};

echo $calificacion;   // Notable
<?php

declare(strict_types=1);

$imc = 27.3;

$categoria = match (true) {
    $imc < 18.5 => 'Bajo peso',
    $imc < 25 => 'Peso normal',
    $imc < 30 => 'Sobrepeso',
    default => 'Obesidad',
};

⚠️ Importante: En match(true) las condiciones deben devolver exactamente true (comparación estricta). Una expresión que devuelva 1 o una cadena no vacía no coincidirá. Usa comparaciones (>=, ===) o funciones que devuelvan booleanos.

Tabla comparativa: switch frente a match

Aspecto switch match
Tipo Instrucción Expresión (devuelve valor)
Comparación == (débil) === (estricta)
break Necesario (si no, cae al siguiente caso) No existe; nunca hay caída
Varios valores Varios case seguidos valor1, valor2 =>
Cuerpo de cada rama Varias instrucciones Una expresión
Sin coincidencia No hace nada (o default) UnhandledMatchError
Disponible desde Siempre PHP 8.0
Úsalo cuando… Cada caso necesita varias instrucciones Quieres obtener un valor a partir de otro

💡 Consejo: En código nuevo, empieza pensando en match. Si cada rama necesita varias instrucciones, extrae ese código a funciones ('pdf' => generarPdf($datos)) o usa if/elseif. switch sigue siendo válido, pero ya no es la primera opción.


6. El bucle while

📘 Concepto: Un bucle repite un bloque de código. while (“mientras”) repite el bloque mientras la condición sea verdadera. Comprueba la condición antes de cada vuelta, así que puede no ejecutarse ninguna vez.

Úsalo cuando no sabes cuántas vueltas darás. Por ejemplo, cuánto tarda un capital en duplicarse:

<?php

declare(strict_types=1);

$capital = 10000.0;
$objetivo = $capital * 2;
$interesAnual = 0.035;
$anios = 0;

while ($capital < $objetivo) {
    $capital *= 1 + $interesAnual;
    $anios++;
}

printf("Tardarás %d años. Capital final: %.2f €\n", $anios, $capital);
// Tardarás 21 años. Capital final: 20594.31 €

⚠️ Importante: Si la condición nunca se vuelve falsa, tienes un bucle infinito y el script no termina (en el navegador acabará con el error Maximum execution time exceeded tras 30 segundos por defecto). Asegúrate de que algo dentro del bucle hace avanzar la condición hacia su fin.


7. El bucle do-while

Igual que while, pero comprueba la condición después de cada vuelta. Por tanto, el bloque se ejecuta al menos una vez:

<?php

declare(strict_types=1);

$i = 10;

do {
    echo "Vuelta con i = $i" . PHP_EOL;
    $i++;
} while ($i < 5);

// Vuelta con i = 10   ← se ejecuta una vez aunque la condición sea falsa

Su uso típico es repetir hasta que algo sea válido, como pedir datos al usuario:

<?php

declare(strict_types=1);

do {
    $entrada = readline('Introduce un número del 1 al 10: ');
    $numero = filter_var($entrada, FILTER_VALIDATE_INT);
} while ($numero === false || $numero < 1 || $numero > 10);

echo "Has elegido el $numero" . PHP_EOL;

8. El bucle for

for es ideal cuando sabes cuántas vueltas quieres dar. Reúne en una línea las tres partes del control del bucle:

<?php

for (inicialización; condición; actualización) {
    // cuerpo
}
<?php

declare(strict_types=1);

for ($i = 1; $i <= 5; $i++) {
    echo "Vuelta número $i" . PHP_EOL;
}
flowchart TD
    A["1. Inicialización: $i = 1"] --> B{"2. ¿$i <= 5?"}
    B -->|Sí| C["3. Ejecuta el cuerpo"]
    C --> D["4. Actualización: $i++"]
    D --> B
    B -->|No| E[Fin del bucle]

💡 Consejo: Si recorres un array con for, calcula su tamaño una sola vez fuera del bucle: $total = count($datos); for ($i = 0; $i < $total; $i++). Aunque, para recorrer arrays, casi siempre es mejor foreach.


9. El bucle foreach

foreach es el bucle más usado en PHP. Recorre todos los elementos de un array (o de un objeto iterable) sin preocuparte de índices ni tamaños.

Solo valores

<?php

declare(strict_types=1);

$frutas = ['manzana', 'pera', 'naranja'];

foreach ($frutas as $fruta) {
    echo "Me gusta la $fruta" . PHP_EOL;
}

Clave y valor

<?php

declare(strict_types=1);

$precios = [
    'café' => 1.20,
    'té' => 1.50,
    'zumo' => 2.75,
];

foreach ($precios as $producto => $precio) {
    echo "$producto: $precio €" . PHP_EOL;
}
// café: 1.2 €
// té: 1.5 €
// zumo: 2.75 €

foreach en plantillas HTML

<?php
declare(strict_types=1);

$menu = ['Inicio' => '/', 'Productos' => '/productos', 'Contacto' => '/contacto'];
$actual = '/productos';
?>
<ul class="menu">
    <?php foreach ($menu as $texto => $url): ?>
        <li class="<?= $url === $actual ? 'activo' : '' ?>">
            <a href="<?= $url ?>"><?= $texto ?></a>
        </li>
    <?php endforeach; ?>
</ul>

Modificar elementos: foreach por referencia

Dentro de un foreach normal, $valor es una copia. Modificarla no cambia el array:

<?php

$precios = [10, 20, 30];

foreach ($precios as $precio) {
    $precio *= 2;   // Solo cambia la copia
}

print_r($precios);   // [10, 20, 30]  ← sin cambios

Con & trabajas sobre el elemento original:

<?php

$precios = [10, 20, 30];

foreach ($precios as &$precio) {
    $precio *= 2;
}
unset($precio);   // ⚠️ ¡Imprescindible! (ver a continuación)

print_r($precios);   // [20, 40, 60]

El peligro de la referencia colgante

Tras un foreach por referencia, la variable $precio sigue siendo una referencia al último elemento. Si la reutilizas, modificarás el array sin querer:

<?php

$numeros = [1, 2, 3];

foreach ($numeros as &$n) {
    $n *= 10;
}
// Aquí $n sigue apuntando a $numeros[2]

foreach ($numeros as $n) {   // ¡Cada vuelta ASIGNA un valor a $numeros[2]!
    // nada
}

print_r($numeros);
// Array ( [0] => 10 [1] => 20 [2] => 20 )   ← ¡el 30 ha desaparecido!

Qué ocurre en el segundo bucle:

Vuelta $n recibe Como $n es $numeros[2], el array queda
1 10 [10, 20, 10]
2 20 [10, 20, 20]
3 20 (el valor actual de $numeros[2]) [10, 20, 20]

⚠️ Importante: Después de un foreach por referencia, escribe siempre unset($variable); inmediatamente después del bucle. O mejor aún: evita las referencias y crea un array nuevo (con array_map(), módulo 05) o modifica por clave: foreach ($precios as $i => $precio) { $precios[$i] = $precio * 2; }.


10. break y continue

Instrucción Efecto
break Sale del bucle por completo
continue Salta el resto de la vuelta actual y pasa a la siguiente

break: salir del bucle

<?php

declare(strict_types=1);

$pedidos = [120, 85, 0, 230, 45];

foreach ($pedidos as $posicion => $importe) {
    if ($importe === 0) {
        echo "Pedido vacío en la posición $posicion. Se detiene el proceso." . PHP_EOL;
        break;
    }
    echo "Procesando pedido de $importe €" . PHP_EOL;
}
// Procesando pedido de 120 €
// Procesando pedido de 85 €
// Pedido vacío en la posición 2. Se detiene el proceso.

continue: saltar a la siguiente vuelta

<?php

declare(strict_types=1);

$usuarios = [
    ['nombre' => 'Ana', 'activo' => true],
    ['nombre' => 'Pedro', 'activo' => false],
    ['nombre' => 'Lucía', 'activo' => true],
];

foreach ($usuarios as $usuario) {
    if (!$usuario['activo']) {
        continue;   // Ignora a los inactivos
    }
    echo "Enviando boletín a {$usuario['nombre']}" . PHP_EOL;
}
// Enviando boletín a Ana
// Enviando boletín a Lucía

Niveles: break 2 y continue 2

Con bucles anidados, break y continue afectan solo al bucle más interno. Puedes indicar un número de niveles:

<?php

declare(strict_types=1);

// Buscar la primera butaca libre en un cine (filas x asientos)
$sala = [
    [true, true, true],    // Fila 1: todas ocupadas
    [true, false, true],   // Fila 2: el asiento 2 está libre
    [false, false, false], // Fila 3: libres
];

$encontrada = null;

foreach ($sala as $fila => $asientos) {
    foreach ($asientos as $asiento => $ocupado) {
        if (!$ocupado) {
            $encontrada = 'Fila ' . ($fila + 1) . ', asiento ' . ($asiento + 1);
            break 2;   // Sale de LOS DOS bucles
        }
    }
}

echo $encontrada ?? 'Sala completa';   // Fila 2, asiento 2
<?php

// continue 2: pasa a la siguiente vuelta del bucle EXTERIOR
for ($i = 1; $i <= 3; $i++) {
    for ($j = 1; $j <= 3; $j++) {
        if ($j === 2) {
            continue 2;
        }
        echo "$i-$j ";
    }
}
// 1-1 2-1 3-1

💡 Consejo: break 2 es útil, pero si te encuentras con break 3 o bucles muy anidados, probablemente el código mejoraría extrayendo la búsqueda a una función y usando return para salir (lo verás en el módulo 07).


11. Ejemplos completos

Ejemplo 1: Tabla de multiplicar en HTML

<?php
declare(strict_types=1);

// Número recibido por la URL (?n=7), por defecto 5 y limitado entre 1 y 20
$n = (int) ($_GET['n'] ?? 5);
$n = max(1, min(20, $n));
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Tabla del <?= $n ?></title>
    <style>
        table { border-collapse: collapse; font-family: system-ui, sans-serif; }
        td, th { border: 1px solid #ccc; padding: .4rem 1rem; text-align: right; }
        tr:nth-child(even) { background: #f3f3f9; }
        .destacado { background: #777bb3; color: white; }
    </style>
</head>
<body>
    <h1>Tabla de multiplicar del <?= $n ?></h1>

    <form method="get">
        <label>Número: <input type="number" name="n" min="1" max="20" value="<?= $n ?>"></label>
        <button>Ver tabla</button>
    </form>

    <table>
        <tr><th>Operación</th><th>Resultado</th></tr>
        <?php for ($i = 1; $i <= 10; $i++): ?>
            <tr class="<?= $i === 10 ? 'destacado' : '' ?>">
                <td><?= $n ?> × <?= $i ?></td>
                <td><?= $n * $i ?></td>
            </tr>
        <?php endfor; ?>
    </table>
</body>
</html>

💡 Consejo: Fíjate en la validación: (int) convierte la entrada en entero (si alguien escribe ?n=abc, obtiene 0) y max(1, min(20, $n)) la limita al rango permitido. Nunca confíes en lo que llega por la URL.

Ejemplo 2: Calculadora con match

<?php

declare(strict_types=1);

function calcular(float $a, string $operador, float $b): float
{
    return match ($operador) {
        '+' => $a + $b,
        '-' => $a - $b,
        '*', 'x' => $a * $b,
        '/' => $b != 0.0
            ? $a / $b
            : throw new DivisionByZeroError('No se puede dividir entre cero'),
        '%' => fmod($a, $b),
        '^' => $a ** $b,
        default => throw new InvalidArgumentException("Operador desconocido: $operador"),
    };
}

$operaciones = [
    [12, '+', 30],
    [7, '/', 2],
    [2, '^', 10],
    [5, '*', 1.5],
];

foreach ($operaciones as [$a, $op, $b]) {
    echo "$a $op $b = " . calcular($a, $op, $b) . PHP_EOL;
}
// 12 + 30 = 42
// 7 / 2 = 3.5
// 2 ^ 10 = 1024
// 5 * 1.5 = 7.5

📘 Concepto: foreach ($operaciones as [$a, $op, $b]) desestructura cada sub-array en tres variables. Lo verás en detalle en el módulo 05.

Ejemplo 3: FizzBuzz

Un clásico de las entrevistas de trabajo: imprime los números del 1 al 100, pero los múltiplos de 3 se sustituyen por “Fizz”, los de 5 por “Buzz” y los de ambos por “FizzBuzz”.

Con match(true) la solución queda muy compacta. Fíjate en que el múltiplo de 15 (de 3 y de 5 a la vez) debe ir primero, porque match se queda con la primera condición que se cumple:

<?php

declare(strict_types=1);

for ($i = 1; $i <= 100; $i++) {
    echo match (true) {
        $i % 15 === 0 => 'FizzBuzz',
        $i % 3 === 0 => 'Fizz',
        $i % 5 === 0 => 'Buzz',
        default => $i,
    } . PHP_EOL;
}

Ejemplo 4: Menú interactivo en la consola

readline() muestra un mensaje en la terminal y devuelve lo que escribe el usuario. Con un bucle do-while y un switch construimos un menú:

<?php

declare(strict_types=1);

// gestor-tareas.php - ejecútalo con: php gestor-tareas.php
$tareas = [];

do {
    echo PHP_EOL . '=== GESTOR DE TAREAS ===' . PHP_EOL;
    echo '1. Añadir tarea' . PHP_EOL;
    echo '2. Listar tareas' . PHP_EOL;
    echo '3. Completar tarea' . PHP_EOL;
    echo '0. Salir' . PHP_EOL;

    $entrada = readline('Elige una opción: ');
    // readline() devuelve false si se cierra la entrada (Ctrl+D / Ctrl+Z): salimos
    $opcion = $entrada === false ? '0' : trim($entrada);

    switch ($opcion) {
        case '1':
            $texto = trim((string) readline('Descripción: '));
            if ($texto === '') {
                echo '⚠ La descripción no puede estar vacía.' . PHP_EOL;
                break;
            }
            $tareas[] = ['texto' => $texto, 'hecha' => false];
            echo '✔ Tarea añadida.' . PHP_EOL;
            break;

        case '2':
            if (count($tareas) === 0) {
                echo 'No hay tareas.' . PHP_EOL;
                break;
            }
            foreach ($tareas as $i => $tarea) {
                $marca = $tarea['hecha'] ? '[x]' : '[ ]';
                echo ($i + 1) . ". $marca {$tarea['texto']}" . PHP_EOL;
            }
            break;

        case '3':
            $numero = (int) readline('Número de tarea: ');
            if (!isset($tareas[$numero - 1])) {
                echo '⚠ Esa tarea no existe.' . PHP_EOL;
                break;
            }
            $tareas[$numero - 1]['hecha'] = true;
            echo '✔ Tarea completada.' . PHP_EOL;
            break;

        case '0':
            echo '¡Hasta pronto!' . PHP_EOL;
            break;

        default:
            echo '⚠ Opción no válida.' . PHP_EOL;
    }
} while ($opcion !== '0');

📘 Concepto: Aquí usamos switch a propósito: cada opción ejecuta varias instrucciones y no devuelve un valor, que es justo el caso en que switch sigue siendo adecuado. Los break dentro de los if salen del switch (no del do-while), lo que nos permite “abandonar” una opción a mitad.

💡 Consejo: readline() solo funciona en la línea de comandos y requiere la extensión readline (incluida en las compilaciones habituales de Windows, Linux y Docker). Si no estuviera disponible, puedes usar fgets(STDIN) como alternativa: echo 'Opción: '; $opcion = trim((string) fgets(STDIN));.


12. Buenas prácticas y errores comunes

Error Ejemplo Solución
Asignar en lugar de comparar if ($rol = 'admin') (¡siempre verdadero!) if ($rol === 'admin')
Olvidar break en switch Se ejecutan varios case Añade break o usa match
Bucle infinito while ($i < 10) { echo $i; } sin $i++ Asegura que la condición avanza
Error por uno (off-by-one) for ($i = 0; $i <= count($a); $i++) Usa < o, mejor, foreach
Referencia colgante Reutilizar $v tras foreach (... as &$v) unset($v) justo después
Modificar el array que recorres Añadir elementos dentro de su foreach Construye un array nuevo
Anidamiento excesivo 4 o 5 niveles de if Cláusulas de guarda y funciones
match sin default con datos externos UnhandledMatchError inesperado Añade default o valida antes

💡 Consejo: Algunos programadores escriben las comparaciones al revés, if ('admin' === $rol) (estilo “Yoda”), para que un = accidental dé error de sintaxis. Con ===, análisis estático y un buen editor, no es necesario, pero lo verás en proyectos como WordPress.


13. Ejercicios

Ejercicio 1: Clasificador de edades

Dada una edad, muestra la etapa vital con match(true): bebé (0-2), infancia (3-11), adolescencia (12-17), juventud (18-29), adultez (30-64) y jubilación (65 o más). Si la edad es negativa o mayor de 120, lanza una InvalidArgumentException desde el default. Prueba con al menos siete edades usando un foreach.

Ejercicio 2: Días del mes

Crea una función diasDelMes(int $mes, int $anio): int que use match para devolver los días de cada mes. Para febrero, comprueba si el año es bisiesto: lo es si es divisible entre 4, salvo los divisibles entre 100 que no lo son entre 400. Comprueba que diasDelMes(2, 2024) devuelve 29 y diasDelMes(2, 1900) devuelve 28.

Ejercicio 3: Tablero de ajedrez

Genera con dos bucles for anidados un tablero de 8x8 en HTML (una <table> con celdas de 50x50 px). Las casillas deben alternar blanco y negro: una casilla es oscura si ($fila + $columna) % 2 === 1. Usa la sintaxis alternativa (for: ... endfor;) en la plantilla.

Ejercicio 4: Adivina el número

Programa un juego de consola: el ordenador elige un número con random_int(1, 100) y el usuario tiene 7 intentos para adivinarlo. Tras cada intento, indica “mayor” o “menor”. Usa do-while y break cuando acierte. Al final muestra “¡Has ganado en N intentos!” o “Has perdido. El número era X”.

Ejercicio 5: Estadísticas de ventas

Dado este array de ventas mensuales:

<?php

$ventas = [
    'enero' => 12500, 'febrero' => 9800, 'marzo' => 15300,
    'abril' => 0, 'mayo' => 17200, 'junio' => 21000,
];

Con un único foreach, calcula: el total, la media de los meses con ventas (ignora los meses a 0 con continue), el mes con más ventas y el mes con menos ventas (sin contar los meses a 0). No uses funciones de arrays como max() o array_sum().

Ejercicio 6: Números primos

Muestra todos los números primos menores que 100. Un número es primo si solo es divisible por 1 y por sí mismo. Usa un bucle externo para los candidatos y uno interno para buscar divisores, con continue 2 en cuanto encuentres un divisor. Optimización: basta con buscar divisores hasta la raíz cuadrada del número.

Ejercicio 7: Corrige la referencia colgante

Este código debería subir un 10 % los precios y luego mostrarlos, pero el último precio sale mal. Explica por qué y corrígelo de dos formas distintas (con unset y sin usar referencias):

<?php

$precios = ['camiseta' => 15.0, 'pantalón' => 35.0, 'zapatillas' => 60.0];

foreach ($precios as &$precio) {
    $precio = round($precio * 1.10, 2);
}

foreach ($precios as $producto => $precio) {
    echo "$producto: $precio €" . PHP_EOL;
}
print_r($precios);

Resumen

Estructura Sintaxis Uso
if if (cond) { } elseif (cond) { } else { } Decisiones generales
Sintaxis alternativa if (cond): ... endif; Plantillas HTML
switch switch ($x) { case 1: ...; break; default: ... } Varias instrucciones por caso; compara con ==
match $r = match ($x) { 1, 2 => 'a', default => 'b' }; Obtener un valor; compara con ===
match(true) match (true) { $n > 5 => 'a', default => 'b' } Sustituir if/elseif que asignan
UnhandledMatchError — match sin coincidencia ni default
while while (cond) { } Repetir sin saber cuántas veces (0 o más)
do-while do { } while (cond); Repetir al menos una vez
for for ($i = 0; $i < $n; $i++) { } Número de vueltas conocido
foreach foreach ($array as $clave => $valor) { } Recorrer arrays
Por referencia foreach ($a as &$v) { } unset($v); Modificar el array original
break break; / break 2; Salir del bucle (o de N niveles)
continue continue; / continue 2; Saltar a la siguiente vuelta