Módulo 07: Funciones

Objetivos del módulo

  • Comprender qué es una función y por qué es la herramienta básica para organizar código
  • Definir funciones con parámetros, valores por defecto y valor de retorno
  • Aplicar tipos de parámetros y de retorno con declare(strict_types=1)
  • Dominar el paso por referencia, los parámetros variádicos y los argumentos con nombre
  • Entender el ámbito de las variables, global y static
  • Crear funciones anónimas (closures), arrow functions y callables
  • Usar la sintaxis first-class callable (strlen(...)) y funciones de orden superior
  • Resolver problemas con recursividad
  • Encadenar transformaciones con el operador pipe |> de PHP 8.5
  • Proteger valores de retorno importantes con #[\NoDiscard]
  • Organizar funciones en varios ficheros con require_once y documentarlas con PHPDoc

1. ¿Qué es una función?

📘 Concepto: Una función es un bloque de código con nombre que realiza una tarea concreta. Recibe datos de entrada (parámetros), trabaja con ellos y, opcionalmente, devuelve un resultado. La escribes una vez y la usas todas las veces que quieras.

Piensa en una función como en una receta de cocina: tiene un nombre (“tortilla de patatas”), unos ingredientes (parámetros: huevos, patatas, aceite) y produce un resultado (la tortilla). No necesitas reescribir la receta cada vez que cocinas: la sigues.

Ya has usado muchas funciones nativas de PHP: strlen(), count(), array_map(), str_contains()… En este módulo aprenderás a crear las tuyas.

Beneficios

Beneficio Explicación
Reutilización Escribes el código una vez y lo llamas desde muchos sitios
Organización Divides un programa grande en piezas pequeñas con nombre
Mantenimiento Si hay un error, lo corriges en un único lugar
Legibilidad calcularIva($precio) se entiende mejor que la fórmula suelta
Pruebas Una función pequeña se puede probar de forma aislada (módulo 18)

2. Definir y llamar funciones

Sintaxis básica

<?php
declare(strict_types=1);

function saludar(): void
{
    echo "¡Hola desde una función!" . PHP_EOL;
}

saludar(); // ¡Hola desde una función!
saludar(); // Se puede llamar tantas veces como quieras
Parte Significado
function Palabra reservada que declara una función
saludar Nombre de la función (en PHP se usa camelCase por convención)
() Lista de parámetros (vacía en este caso)
: void Tipo de retorno: void significa “no devuelve nada”
{ ... } Cuerpo de la función: el código que se ejecuta

💡 Consejo: Los nombres de función deberían ser verbos que describan lo que hacen: calcularTotal(), enviarCorreo(), esPar(), formatearFecha(). Si te cuesta ponerle nombre, probablemente la función hace demasiadas cosas.

Los nombres de función no distinguen mayúsculas

<?php
function despedir(): void
{
    echo "Adiós" . PHP_EOL;
}

despedir();  // Adiós
DESPEDIR();  // Adiós (funciona, pero no lo hagas nunca)

⚠️ Importante: A diferencia de las variables ($nombre y $Nombre son distintas), los nombres de funciones no distinguen mayúsculas y minúsculas. Llámalas siempre exactamente como las declaraste. Además, no puedes declarar dos funciones con el mismo nombre: obtendrás Fatal error: Cannot redeclare function.

Puedes llamar a una función antes de declararla

PHP analiza el fichero entero antes de ejecutarlo, así que una función declarada al nivel principal del fichero está disponible desde la primera línea:

<?php
echo doble(21) . PHP_EOL; // 42 (la función está más abajo)

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

La excepción son las funciones declaradas dentro de un if o de otra función: solo existen cuando la ejecución pasa por ahí.


3. Parámetros y valor de retorno

📘 Concepto: Los parámetros son las variables que declaras entre paréntesis al definir la función. Los argumentos son los valores concretos que pasas al llamarla.

<?php
declare(strict_types=1);

//                  parámetros
function sumar(int $a, int $b): int
{
    return $a + $b;
}

//               argumentos
$resultado = sumar(5, 3);
echo $resultado . PHP_EOL;      // 8
echo sumar(10, 20) . PHP_EOL;   // 30

return termina la función

return hace dos cosas: devuelve un valor y sale de la función inmediatamente. Todo lo que hay después no se ejecuta:

<?php
declare(strict_types=1);

function clasificarEdad(int $edad): string
{
    if ($edad < 0) {
        return 'Edad no válida';   // Sale aquí si la edad es negativa
    }

    if ($edad < 18) {
        return 'Menor de edad';
    }

    return 'Mayor de edad';
}

echo clasificarEdad(15) . PHP_EOL;  // Menor de edad
echo clasificarEdad(40) . PHP_EOL;  // Mayor de edad

💡 Consejo: Este estilo de salir pronto ante casos especiales se llama cláusulas de guarda (guard clauses). Evita anidar muchos if y hace el código más fácil de leer.

Devolver varios valores

Una función solo devuelve un valor, pero ese valor puede ser un array. Con la desestructuración (módulo 05) lo recoges cómodamente:

<?php
declare(strict_types=1);

function estadisticas(array $numeros): array
{
    return [
        'min'   => min($numeros),
        'max'   => max($numeros),
        'media' => array_sum($numeros) / count($numeros),
    ];
}

['min' => $min, 'max' => $max, 'media' => $media] = estadisticas([4, 8, 15, 16, 23, 42]);

echo "Mínimo: $min, Máximo: $max, Media: $media" . PHP_EOL;
// Mínimo: 4, Máximo: 42, Media: 18

Si no hay return

Una función sin return devuelve null. Si su tipo de retorno es void, no debe devolver nada (puede usar return; sin valor para salir antes).


4. Tipos de parámetros y de retorno

Desde PHP 7 puedes (y debes) indicar el tipo de cada parámetro y del valor devuelto. PHP comprobará los tipos en cada llamada.

<?php
declare(strict_types=1);

function precioConIva(float $precio, float $iva = 21.0): float
{
    return round($precio * (1 + $iva / 100), 2);
}

echo precioConIva(100.0) . PHP_EOL;  // 121
echo precioConIva(50) . PHP_EOL;     // 60.5 (int → float sí se permite)
// echo precioConIva("100");         // TypeError con strict_types=1

⚠️ Importante: Sin declare(strict_types=1); PHP intenta convertir los valores (por ejemplo "100" a 100.0). Con modo estricto, pasar un tipo incorrecto lanza un TypeError. La única conversión que se mantiene en modo estricto es de int a float. Usa siempre el modo estricto: detecta errores antes. La declaración debe ser la primera instrucción del fichero.

Tipos disponibles

Tipo Ejemplo Notas
Escalares int, float, string, bool Los más comunes
Compuestos array, object, callable, iterable callable no se puede usar en propiedades
Clases DateTime, Producto Cualquier clase o interfaz
Nullable ?string Equivale a string\|null
Unión int\|float Uno de varios tipos (PHP 8.0)
Intersección Countable&Iterator Debe cumplir todos (PHP 8.1)
DNF (A&B)\|null Combinación de unión e intersección (PHP 8.2)
mixed mixed Cualquier valor (úsalo poco)
void : void Solo retorno: no devuelve nada
never : never Solo retorno: la función nunca termina normalmente (lanza o hace exit)
null, true, false : true Tipos independientes desde PHP 8.2
static, self : static Para métodos (módulos 11-12)
<?php
declare(strict_types=1);

// Tipo unión: acepta int o float
function cuadrado(int|float $n): int|float
{
    return $n * $n;
}

// Tipo nullable: puede devolver un string o null
function buscarUsuario(int $id): ?string
{
    $usuarios = [1 => 'Ana', 2 => 'Luis'];
    return $usuarios[$id] ?? null;
}

// never: la función termina el script siempre
function abortar(string $mensaje): never
{
    http_response_code(500);
    exit($mensaje);
}

echo cuadrado(3) . PHP_EOL;                         // 9
echo cuadrado(1.5) . PHP_EOL;                       // 2.25
echo buscarUsuario(1) ?? 'No existe', PHP_EOL;      // Ana
echo buscarUsuario(99) ?? 'No existe', PHP_EOL;     // No existe

5. Valores por defecto

Un parámetro puede tener un valor por defecto; si no le pasas argumento, se usa ese valor:

<?php
declare(strict_types=1);

function formatearPrecio(float $cantidad, string $moneda = '€', int $decimales = 2): string
{
    return number_format($cantidad, $decimales, ',', '.') . ' ' . $moneda;
}

echo formatearPrecio(1234.5) . PHP_EOL;          // 1.234,50 €
echo formatearPrecio(1234.5, '$') . PHP_EOL;     // 1.234,50 $
echo formatearPrecio(1234.5, '€', 0) . PHP_EOL;  // 1.235 €

⚠️ Importante: Los parámetros con valor por defecto deben ir después de los obligatorios. Declarar function f($a = 1, $b) está obsoleto (deprecated) desde PHP 8.0, porque el valor por defecto nunca se podría usar.

Los valores por defecto deben ser expresiones constantes: literales, constantes, arrays, null, new Clase() (desde PHP 8.1) y, desde PHP 8.5, también closures estáticas y first-class callables como strlen(...).

<?php
declare(strict_types=1);

// PHP 8.5: un callable como valor por defecto
function transformarTodo(array $textos, Closure $fn = mb_strtoupper(...)): array
{
    return array_map($fn, $textos);
}

print_r(transformarTodo(['hola', 'adiós']));            // [HOLA, ADIÓS]
print_r(transformarTodo(['Hola'], mb_strtolower(...))); // [hola]

💡 Consejo: Recuerda del módulo 06 que strtoupper() no entiende de multibyte: 'adiós' se convertiría en 'ADIóS'. Para texto en español usa siempre las versiones mb_*.


6. Paso por valor y paso por referencia

Paso por valor (lo habitual)

Por defecto, la función recibe una copia del argumento. Modificar el parámetro dentro no afecta a la variable original:

<?php
declare(strict_types=1);

function duplicar(int $numero): void
{
    $numero *= 2;
    echo "Dentro: $numero" . PHP_EOL;  // Dentro: 20
}

$x = 10;
duplicar($x);
echo "Fuera: $x" . PHP_EOL;            // Fuera: 10 (no ha cambiado)

Esto también ocurre con los arrays: se pasan por valor (PHP usa copy-on-write, así que solo se copian realmente si los modificas).

Paso por referencia con &

Si antepones & al parámetro, la función trabaja con la variable original:

<?php
declare(strict_types=1);

function duplicarRef(int &$numero): void
{
    $numero *= 2;
}

$x = 10;
duplicarRef($x);
echo $x . PHP_EOL; // 20 (ahora sí ha cambiado)

function anadirEtiqueta(array &$etiquetas, string $nueva): void
{
    if (!in_array($nueva, $etiquetas, true)) {
        $etiquetas[] = $nueva;
    }
}

$tags = ['php', 'web'];
anadirEtiqueta($tags, 'backend');
anadirEtiqueta($tags, 'php');      // No se repite
print_r($tags); // [php, web, backend]

Funciones nativas como sort(), shuffle(), array_push() o preg_match() (su tercer parámetro) usan paso por referencia: por eso sort($array) modifica el array y devuelve un bool.

⚠️ Importante: Solo puedes pasar variables a un parámetro por referencia. duplicarRef(10) produce un error, porque un literal no es una variable que se pueda modificar.

💡 Consejo: Abusar de las referencias hace el código difícil de seguir (“¿quién ha cambiado esta variable?”). Prefiere devolver el nuevo valor: $tags = anadirEtiqueta($tags, 'php');. Usa & solo cuando tenga una ventaja clara.


7. Parámetros variádicos (...)

Un parámetro variádico recoge cualquier número de argumentos en un array. Se escribe con ... delante del nombre y debe ser el último:

<?php
declare(strict_types=1);

function sumarTodos(int ...$numeros): int
{
    // $numeros es un array con todos los argumentos
    return array_sum($numeros);
}

echo sumarTodos() . PHP_EOL;              // 0
echo sumarTodos(1, 2) . PHP_EOL;          // 3
echo sumarTodos(1, 2, 3, 4, 5) . PHP_EOL; // 15

function etiquetaHtml(string $etiqueta, string ...$clases): string
{
    $atributo = $clases ? ' class="' . implode(' ', $clases) . '"' : '';
    return "<$etiqueta$atributo></$etiqueta>";
}

echo etiquetaHtml('div') . PHP_EOL;                      // <div></div>
echo etiquetaHtml('div', 'card', 'shadow') . PHP_EOL;    // <div class="card shadow"></div>

El operador spread al llamar

El mismo ... en la llamada hace lo contrario: despliega un array en argumentos sueltos:

<?php
declare(strict_types=1);

function sumarTres(int $a, int $b, int $c): int
{
    return $a + $b + $c;
}

$valores = [1, 2, 3];
echo sumarTres(...$valores) . PHP_EOL;  // 6

// Con claves de texto, se convierten en argumentos con nombre (PHP 8.1)
$args = ['c' => 30, 'a' => 10, 'b' => 20];
echo sumarTres(...$args) . PHP_EOL;     // 60

8. Argumentos con nombre (named arguments)

Desde PHP 8.0 puedes pasar argumentos indicando el nombre del parámetro. Esto permite cambiar el orden, saltarte opcionales y, sobre todo, hace las llamadas autoexplicativas.

<?php
declare(strict_types=1);

function crearUsuario(string $nombre, int $edad, string $ciudad = 'Madrid', bool $activo = true): string
{
    $estado = $activo ? 'activo' : 'inactivo';
    return "$nombre ($edad años, $ciudad) - $estado";
}

// Por posición
echo crearUsuario('Ana', 25, 'Sevilla') . PHP_EOL;

// Con nombre: el orden da igual
echo crearUsuario(edad: 30, nombre: 'Luis') . PHP_EOL;

// Te saltas $ciudad y cambias solo $activo
echo crearUsuario('Marta', 41, activo: false) . PHP_EOL;
// Marta (41 años, Madrid) - inactivo

Brillan especialmente con funciones nativas de muchos parámetros:

<?php
// ¿Qué significan esos argumentos?
echo htmlspecialchars($texto, ENT_QUOTES, 'UTF-8', false);

// Mucho más claro
echo htmlspecialchars($texto, double_encode: false);

// array_fill con nombre
$ceros = array_fill(start_index: 0, count: 5, value: 0);

// json_encode con flags
echo json_encode(['ñ' => 'año'], flags: JSON_UNESCAPED_UNICODE);
Regla Ejemplo
Los posicionales van antes que los nombrados f(1, b: 2) ✅ — f(a: 1, 2) ❌
No puedes repetir un parámetro f(1, a: 2) ❌ Error
El nombre es el del parámetro, sin $ crearUsuario(nombre: 'Ana', ...)
Con variádicos, los nombres extra van con clave de texto f(...$args)

⚠️ Importante: Con los argumentos con nombre, el nombre de los parámetros pasa a formar parte de la API de tu función. Si renombras $ciudad a $localidad, romperás el código de quien llamaba con ciudad:. Elige buenos nombres desde el principio.


9. Funciones nativas útiles

PHP incluye miles de funciones. Antes de escribir la tuya, comprueba si ya existe. Aquí tienes una selección de las que más usarás (las de cadenas y arrays las viste en los módulos 05 y 06):

Categoría Funciones
Matemáticas abs(), round(), floor(), ceil(), max(), min(), intdiv(), fdiv(), pow() / **, sqrt()
Aleatorios random_int() (segura), mt_rand(), array_rand(), shuffle()
Tipos gettype(), get_debug_type(), is_int(), is_string(), is_array(), is_numeric(), is_callable()
Arrays (8.4) array_find(), array_find_key(), array_any(), array_all()
Arrays (8.5) array_first(), array_last()
Fecha time(), date(), mktime(), checkdate(), strtotime()
Funciones function_exists(), func_get_args(), call_user_func(), call_user_func_array()
Depuración var_dump(), print_r(), var_export(), debug_print_backtrace()
<?php
declare(strict_types=1);

$pedidos = [
    ['id' => 1, 'total' => 25.0, 'pagado' => true],
    ['id' => 2, 'total' => 120.5, 'pagado' => false],
    ['id' => 3, 'total' => 60.0, 'pagado' => true],
];

// PHP 8.4: primer elemento que cumple la condición
$grande = array_find($pedidos, fn(array $p) => $p['total'] > 100);
echo $grande['id'] . PHP_EOL;                                        // 2

// PHP 8.4: ¿alguno / todos cumplen?
var_dump(array_any($pedidos, fn(array $p) => !$p['pagado']));       // bool(true)
var_dump(array_all($pedidos, fn(array $p) => $p['total'] > 10));    // bool(true)

// PHP 8.5: primer y último valor sin tocar el puntero interno
echo array_first($pedidos)['id'] . PHP_EOL;                          // 1
echo array_last($pedidos)['id'] . PHP_EOL;                           // 3

echo random_int(1, 6) . PHP_EOL;          // Dado: 1..6
echo intdiv(17, 5) . PHP_EOL;             // 3
echo get_debug_type(3.14) . PHP_EOL;      // float
var_dump(function_exists('array_first')); // bool(true) en PHP 8.5

💡 Consejo: El manual oficial (https://www.php.net/manual/es/) es tu mejor amigo. Escribe en el buscador php.net/nombre_funcion (por ejemplo php.net/array_find) y llegarás directamente a su ficha.


10. Ámbito de las variables: global y static

📘 Concepto: El ámbito (scope) es la zona del código donde una variable existe. En PHP, cada función tiene su propio ámbito aislado: no ve las variables de fuera, y las suyas desaparecen al terminar.

<?php
$mensaje = 'Hola';

function mostrar(): void
{
    // echo $mensaje; // Warning: Undefined variable $mensaje
    $interna = 'Solo existo aquí';
    echo $interna . PHP_EOL;
}

mostrar();
// echo $interna;     // Warning: Undefined variable $interna

⚠️ Importante: Esto sorprende a quien viene de JavaScript, donde una función sí ve las variables exteriores. En PHP, una función normal solo ve sus parámetros, sus variables locales y las superglobales ($_GET, $_POST…).

global (y por qué evitarlo)

La palabra global importa una variable del ámbito global:

<?php
$contador = 0;

function incrementar(): void
{
    global $contador;  // Ahora $contador es la variable global
    $contador++;
}

incrementar();
incrementar();
echo $contador . PHP_EOL; // 2

Funciona, pero crea dependencias ocultas: mirando la firma incrementar(): void nadie sabe que modifica algo externo. Es difícil de probar y de depurar. La alternativa limpia es pasar el dato como parámetro y devolver el resultado:

<?php
declare(strict_types=1);

function incrementar(int $contador): int
{
    return $contador + 1;
}

$contador = 0;
$contador = incrementar($contador);

Variables static

Una variable static dentro de una función conserva su valor entre llamadas. Se inicializa solo la primera vez:

<?php
declare(strict_types=1);

function siguienteId(): int
{
    static $id = 0;  // Solo se ejecuta en la primera llamada
    return ++$id;
}

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

Un uso práctico es la caché (memoización) de cálculos costosos:

<?php
declare(strict_types=1);

function configuracion(): array
{
    static $config = null;

    if ($config === null) {
        echo "(leyendo configuración...)" . PHP_EOL;
        $config = ['app' => 'Tienda', 'debug' => true]; // Imagina que viene de un fichero
    }

    return $config;
}

configuracion();                          // (leyendo configuración...)
echo configuracion()['app'] . PHP_EOL;    // Tienda (sin volver a leer)

11. Funciones anónimas (closures)

📘 Concepto: Una función anónima es una función sin nombre que se puede guardar en una variable, pasar como argumento o devolver desde otra función. En PHP son objetos de la clase Closure.

<?php
declare(strict_types=1);

$saludar = function (string $nombre): string {
    return "Hola, $nombre";
};  // ← Ojo: lleva punto y coma, es una asignación

echo $saludar('Ana') . PHP_EOL;       // Hola, Ana
echo get_debug_type($saludar) . PHP_EOL; // Closure

Su uso más habitual es como argumento de otra función:

<?php
declare(strict_types=1);

$precios = [10, 25, 7, 100];

$conIva = array_map(function (int $p): float {
    return $p * 1.21;
}, $precios);

print_r($conIva); // [12.1, 30.25, 8.47, 121]

Capturar variables con use

Igual que las funciones normales, una closure no ve las variables exteriores… salvo que las captures con use:

<?php
declare(strict_types=1);

$iva = 21;

$aplicarIva = function (float $precio) use ($iva): float {
    return $precio * (1 + $iva / 100);
};

echo $aplicarIva(100) . PHP_EOL; // 121

Captura por valor (por defecto)

use ($iva) copia el valor en el momento de crear la closure. Si después cambias $iva, la closure no se entera:

<?php
$iva = 21;
$calcular = function (float $p) use ($iva): float {
    return $p * (1 + $iva / 100);
};

$iva = 10;                        // Cambio posterior
echo $calcular(100) . PHP_EOL;    // 121 (sigue usando 21)

Captura por referencia con &

Con use (&$variable) la closure comparte la variable original: ve los cambios y puede modificarla.

<?php
declare(strict_types=1);

$total = 0;

$sumarAlTotal = function (int $cantidad) use (&$total): void {
    $total += $cantidad;
};

$sumarAlTotal(10);
$sumarAlTotal(5);
echo $total . PHP_EOL; // 15

// Ejemplo práctico: contar elementos procesados dentro de array_walk
$procesados = 0;
$nombres = ['ana', 'luis', 'marta'];
array_walk($nombres, function (string &$n) use (&$procesados): void {
    $n = ucfirst($n);
    $procesados++;
});
print_r($nombres);                 // [Ana, Luis, Marta]
echo $procesados . PHP_EOL;        // 3
Captura Sintaxis ¿Ve cambios posteriores? ¿Puede modificar la original?
Por valor use ($x) No No
Por referencia use (&$x) Sí Sí

Funciones que devuelven funciones

Una closure puede “recordar” datos: es una forma elegante de crear funciones configurables (fábricas):

<?php
declare(strict_types=1);

function crearMultiplicador(int $factor): Closure
{
    return function (int $n) use ($factor): int {
        return $n * $factor;
    };
}

$doble  = crearMultiplicador(2);
$triple = crearMultiplicador(3);

echo $doble(5) . PHP_EOL;   // 10
echo $triple(5) . PHP_EOL;  // 15

Closures estáticas

Si una closure se crea dentro de una clase, captura automáticamente $this. Declararla con static function evita esa captura (ligeramente más eficiente y obligatorio en expresiones constantes de PHP 8.5). Lo verás en los módulos de POO.


12. Arrow functions (fn() =>)

PHP 7.4 introdujo una sintaxis corta para closures de una sola expresión:

<?php
declare(strict_types=1);

// Closure clásica
$cuadrado = function (int $n): int {
    return $n * $n;
};

// Arrow function equivalente
$cuadrado = fn(int $n): int => $n * $n;

echo $cuadrado(4) . PHP_EOL; // 16

Captura automática por valor

La gran diferencia: una arrow function captura automáticamente (por valor) todas las variables exteriores que use. No necesitas use:

<?php
declare(strict_types=1);

$iva = 21;
$descuento = 0.10;

$precioFinal = fn(float $p): float => $p * (1 - $descuento) * (1 + $iva / 100);

echo $precioFinal(100) . PHP_EOL; // 108.9

// Ideal para array_map / array_filter
$productos = [
    ['nombre' => 'Teclado', 'precio' => 45],
    ['nombre' => 'Monitor', 'precio' => 180],
    ['nombre' => 'Ratón',   'precio' => 20],
];
$limite = 50;

$baratos = array_filter($productos, fn(array $p) => $p['precio'] < $limite);
$nombres = array_map(fn(array $p) => $p['nombre'], $baratos);

print_r($nombres); // [0 => Teclado, 2 => Ratón]
Característica function () use () fn() =>
Cuerpo Bloque con varias instrucciones Una sola expresión
return Explícito Implícito
Captura de variables Manual con use Automática
Captura por referencia Sí, use (&$x) No (siempre por valor)
Uso típico Lógica compleja, modificar estado Transformaciones y filtros cortos

⚠️ Importante: Una arrow function no puede modificar las variables exteriores: trabaja sobre una copia. fn() => $total++ no cambia $total fuera. Si necesitas modificar, usa function () use (&$total).


13. Callables y sintaxis first-class callable

📘 Concepto: Un callable es cualquier cosa que PHP puede “llamar” como una función. El tipo callable acepta todas estas formas.

Forma Ejemplo
Nombre de función como string 'strtoupper'
Closure o arrow function fn($x) => $x * 2
Método de objeto [$objeto, 'metodo']
Método estático [Clase::class, 'metodo'] o 'Clase::metodo'
Objeto invocable Un objeto con método __invoke() (módulo 12)
First-class callable (PHP 8.1) strtoupper(...), $obj->metodo(...), Clase::metodo(...)

First-class callable syntax: funcion(...)

Escribir el nombre de una función seguido de (...) (tres puntos literales, no es una omisión) no la llama: crea un objeto Closure que apunta a ella.

<?php
declare(strict_types=1);

$longitud = strlen(...);         // Closure que envuelve strlen
echo $longitud('PHP 8.5') . PHP_EOL; // 7

$palabras = ['  hola ', ' mundo  '];
print_r(array_map(trim(...), $palabras));       // [hola, mundo]

// Con funciones propias
function esPar(int $n): bool
{
    return $n % 2 === 0;
}
print_r(array_filter([1, 2, 3, 4, 5, 6], esPar(...))); // [2, 4, 6]

Ventajas frente al string 'trim':

  • El IDE y los analizadores estáticos (PHPStan) entienden que es una referencia a la función: autocompletado, renombrado y detección de errores.
  • Si la función no existe, el error aparece al crear el callable, no más tarde.
  • Funciona con métodos sin la incómoda sintaxis de array [$this, 'metodo'].

💡 Consejo: En código moderno, usa siempre funcion(...) en lugar de 'funcion' como string. Es la forma que verás también con el operador pipe.

Recibir un callable como parámetro

<?php
declare(strict_types=1);

function aplicarDosVeces(callable $fn, mixed $valor): mixed
{
    return $fn($fn($valor));
}

echo aplicarDosVeces(fn(int $n) => $n + 10, 1) . PHP_EOL;  // 21
echo aplicarDosVeces(strrev(...), 'PHP') . PHP_EOL;         // PHP

Usa callable para parámetros; si quieres algo más específico o guardarlo en una propiedad de clase, usa Closure.


14. Funciones de orden superior

📘 Concepto: Una función de orden superior es la que recibe funciones como argumento o devuelve una función. array_map, array_filter, usort y array_reduce son funciones de orden superior.

Las cuatro grandes

<?php
declare(strict_types=1);

$ventas = [
    ['producto' => 'Café',  'unidades' => 3, 'precio' => 1.5],
    ['producto' => 'Té',    'unidades' => 1, 'precio' => 1.2],
    ['producto' => 'Tarta', 'unidades' => 2, 'precio' => 3.8],
];

// array_map: transforma cada elemento
$importes = array_map(fn(array $v) => $v['unidades'] * $v['precio'], $ventas);
// [4.5, 1.2, 7.6]

// array_filter: se queda con los que cumplen
$grandes = array_filter($importes, fn(float $i) => $i > 2);
// [0 => 4.5, 2 => 7.6]

// array_reduce: reduce todo a un valor
$total = array_reduce($importes, fn(float $acc, float $i) => $acc + $i, 0.0);
echo "Total: $total" . PHP_EOL; // Total: 13.3

// usort: ordena con un criterio propio (nave espacial <=>)
usort($ventas, fn(array $a, array $b) => $b['precio'] <=> $a['precio']);
echo $ventas[0]['producto'] . PHP_EOL; // Tarta

Crear tus propias funciones de orden superior

<?php
declare(strict_types=1);

/** Ejecuta una función y mide cuánto tarda */
function cronometrar(callable $tarea): mixed
{
    $inicio = hrtime(true);
    $resultado = $tarea();
    $ms = (hrtime(true) - $inicio) / 1_000_000;
    echo sprintf("Tardó %.3f ms", $ms) . PHP_EOL;
    return $resultado;
}

$suma = cronometrar(fn() => array_sum(range(1, 1_000_000)));
echo $suma . PHP_EOL; // 500000500000

/** Devuelve una versión de $fn que guarda resultados ya calculados */
function memoizar(callable $fn): Closure
{
    $cache = [];
    return function (mixed ...$args) use ($fn, &$cache): mixed {
        $clave = serialize($args);
        return $cache[$clave] ??= $fn(...$args);
    };
}

$lenta = function (int $n): int {
    usleep(200_000); // Simula 0,2 s de trabajo
    return $n * $n;
};
$rapida = memoizar($lenta);
cronometrar(fn() => $rapida(9)); // ~200 ms
cronometrar(fn() => $rapida(9)); // ~0 ms (desde la caché)

Componer funciones

Componer es crear una función nueva que aplica otras en cadena. Antes del operador pipe era la forma de expresar transformaciones encadenadas:

<?php
declare(strict_types=1);

function componer(callable ...$fns): Closure
{
    return fn(mixed $valor) => array_reduce($fns, fn($acc, $fn) => $fn($acc), $valor);
}

$limpiar = componer(trim(...), mb_strtolower(...), ucfirst(...));
echo $limpiar('   HOLA MUNDO  ') . PHP_EOL; // Hola mundo

15. Recursividad

📘 Concepto: Una función recursiva es aquella que se llama a sí misma. Divide un problema en una versión más pequeña del mismo problema. Siempre necesita un caso base que detenga las llamadas.

Analogía: las muñecas rusas (matrioskas). Para llegar a la más pequeña, abres una y dentro hay otra igual pero más pequeña… hasta que llegas a la que ya no se abre (caso base).

Factorial

<?php
declare(strict_types=1);

function factorial(int $n): int
{
    if ($n <= 1) {           // Caso base
        return 1;
    }
    return $n * factorial($n - 1); // Caso recursivo
}

echo factorial(5) . PHP_EOL;  // 120
factorial(5)
  → 5 * factorial(4)
    → 4 * factorial(3)
      → 3 * factorial(2)
        → 2 * factorial(1)
          → 1               (caso base)
        → 2 * 1 = 2
      → 3 * 2 = 6
    → 4 * 6 = 24
  → 5 * 24 = 120

Recorrer estructuras anidadas

La recursividad brilla con datos con forma de árbol: menús, categorías, carpetas, comentarios con respuestas…

<?php
declare(strict_types=1);

$menu = [
    ['titulo' => 'Inicio', 'url' => '/'],
    ['titulo' => 'Productos', 'url' => '/productos', 'hijos' => [
        ['titulo' => 'Portátiles', 'url' => '/productos/portatiles'],
        ['titulo' => 'Accesorios', 'url' => '/productos/accesorios', 'hijos' => [
            ['titulo' => 'Ratones', 'url' => '/productos/accesorios/ratones'],
        ]],
    ]],
    ['titulo' => 'Contacto', 'url' => '/contacto'],
];

function pintarMenu(array $items, int $nivel = 0): string
{
    $sangria = str_repeat('  ', $nivel);
    $html = "$sangria<ul>\n";
    foreach ($items as $item) {
        $html .= "$sangria  <li><a href=\"{$item['url']}\">{$item['titulo']}</a>";
        if (!empty($item['hijos'])) {
            $html .= "\n" . pintarMenu($item['hijos'], $nivel + 2) . "$sangria  ";
        }
        $html .= "</li>\n";
    }
    return $html . "$sangria</ul>\n";
}

echo pintarMenu($menu);

/** Suma todos los números de un array con cualquier profundidad */
function sumaProfunda(array $datos): int|float
{
    $total = 0;
    foreach ($datos as $valor) {
        $total += is_array($valor) ? sumaProfunda($valor) : $valor;
    }
    return $total;
}

echo sumaProfunda([1, [2, 3], [4, [5, [6]]]]) . PHP_EOL; // 21

Fibonacci y el coste de la recursividad ingenua

<?php
declare(strict_types=1);

// Versión ingenua: recalcula lo mismo miles de veces (exponencial)
function fib(int $n): int
{
    return $n < 2 ? $n : fib($n - 1) + fib($n - 2);
}

// Versión con memoización usando una variable static
function fibMemo(int $n): int
{
    static $memo = [0 => 0, 1 => 1];
    return $memo[$n] ??= fibMemo($n - 1) + fibMemo($n - 2);
}

echo fib(25) . PHP_EOL;     // 75025 (ya tarda algo)
echo fibMemo(90) . PHP_EOL; // 2880067194370816120 (instantáneo)

⚠️ Importante: Cada llamada recursiva ocupa memoria en la pila. Una recursión sin caso base (o muy profunda) agota la memoria y termina con un error fatal. Si un bucle resuelve el problema de forma sencilla, suele ser mejor opción.


16. El operador pipe |> (PHP 8.5)

El problema: funciones anidadas

Imagina que quieres convertir el título " Curso de PHP 8.5 " en un slug para una URL: curso-de-php-85. Con funciones anidadas el código se lee de dentro hacia fuera, al revés de como ocurre:

<?php
$titulo = '  Curso de PHP 8.5  ';

$slug = strtolower(str_replace('.', '', str_replace(' ', '-', trim($titulo))));

La alternativa clásica son variables intermedias, que se leen bien pero ensucian el código:

<?php
$paso1 = trim($titulo);
$paso2 = str_replace(' ', '-', $paso1);
$paso3 = str_replace('.', '', $paso2);
$slug  = strtolower($paso3);

La solución: |>

📘 Concepto: El operador pipe |> toma el valor de su izquierda y se lo pasa como único argumento al callable de su derecha. $x |> f(...) equivale a f($x). Encadenando varios, los datos “fluyen” de izquierda a derecha, como en una cadena de montaje.

<?php
declare(strict_types=1);

$titulo = '  Curso de PHP 8.5  ';

$slug = $titulo
    |> trim(...)
    |> (fn(string $s) => str_replace(' ', '-', $s))
    |> (fn(string $s) => str_replace('.', '', $s))
    |> strtolower(...);

var_dump($slug); // string(15) "curso-de-php-85"
flowchart LR
    A["'  Curso de PHP 8.5  '"] -->|trim| B["'Curso de PHP 8.5'"]
    B -->|espacios a guiones| C["'Curso-de-PHP-8.5'"]
    C -->|quitar puntos| D["'Curso-de-PHP-85'"]
    D -->|strtolower| E["'curso-de-php-85'"]

Las reglas del pipe

Regla Explicación
El lado derecho es un callable trim(...), una closure, $obj->metodo(...), una variable con una closure…
Debe aceptar un solo parámetro obligatorio El valor de la izquierda es ese único argumento
Las arrow functions van entre paréntesis \|> (fn($x) => ...); sin paréntesis la arrow function “se tragaría” el resto de la cadena
No admite parámetros por referencia $arr \|> sort(...) no es válido
Se evalúa de izquierda a derecha Asociativo a la izquierda
Precedencia Se aplica después de las operaciones aritméticas y antes de las comparaciones y de ??

Con funciones nativas de un parámetro

Muchas funciones nativas encajan directamente porque reciben un único argumento: trim, strtolower, strtoupper, ucfirst, strrev, strlen, count, array_unique, array_values, array_sum, json_encode…

<?php
declare(strict_types=1);

$numeros = [5, 3, 5, 8, 3, 1];

$suma = $numeros
    |> array_unique(...)   // [5, 3, 8, 1]
    |> array_sum(...);     // 17

echo $suma . PHP_EOL; // 17

// Funciones con parámetros opcionales también valen (solo se pasa el primero)
echo '  hola  ' |> trim(...) |> strlen(...), PHP_EOL; // 4

Con funciones de varios parámetros

Si la función necesita más argumentos (como str_replace o array_map), envuélvela en una arrow function entre paréntesis que reciba el valor:

<?php
declare(strict_types=1);

$csv = ' ana , LUIS,  marta ,ana ';

$nombres = $csv
    |> (fn(string $s) => explode(',', $s))
    |> (fn(array $a) => array_map(trim(...), $a))
    |> (fn(array $a) => array_map(mb_strtolower(...), $a))
    |> array_unique(...)
    |> (fn(array $a) => array_map(ucfirst(...), $a))
    |> (fn(array $a) => implode(', ', $a));

echo $nombres . PHP_EOL; // Ana, Luis, Marta

Con funciones propias

Las funciones propias de un parámetro son las que mejor encajan con el pipe. Esto te anima a escribir funciones pequeñas y con un único propósito:

<?php
declare(strict_types=1);

function quitarTildes(string $s): string
{
    return strtr($s, ['á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', 'ñ' => 'n', 'ü' => 'u']);
}

function soloAlfanumerico(string $s): string
{
    return preg_replace('/[^a-z0-9]+/', '-', $s);
}

function recortarGuiones(string $s): string
{
    return trim($s, '-');
}

function slugify(string $texto): string
{
    return $texto
        |> trim(...)
        |> mb_strtolower(...)
        |> quitarTildes(...)
        |> soloAlfanumerico(...)
        |> recortarGuiones(...);
}

echo slugify('  ¡Programación en PHP 8.5: más fácil!  ') . PHP_EOL;
// programacion-en-php-8-5-mas-facil

También puedes usar una fábrica de funciones (sección 11) para crear callables de un parámetro “a medida”:

<?php
declare(strict_types=1);

function multiplicarPor(int $factor): Closure
{
    return fn(int $n): int => $n * $factor;
}

function sumar(int $cantidad): Closure
{
    return fn(int $n): int => $n + $cantidad;
}

echo 5 |> multiplicarPor(3) |> sumar(1), PHP_EOL; // 16

Fíjate: aquí multiplicarPor(3) sí se llama (no lleva ...), y lo que devuelve es la closure que recibe el valor.

Precedencia en la práctica

<?php
declare(strict_types=1);

// La aritmética se evalúa antes: equivale a (5 + 2) |> ...
echo 5 + 2 |> (fn(int $n) => $n * 10), PHP_EOL;  // 70

// La comparación se evalúa después: compara el resultado del pipe
var_dump('beep' |> strlen(...) === 4);           // bool(true)

// ?? se aplica al resultado de toda la cadena
function buscarNombre(int $id): ?string
{
    return [1 => 'Ana'][$id] ?? null;
}
echo 7 |> buscarNombre(...) ?? 'Invitado', PHP_EOL; // Invitado

💡 Consejo: Aunque la precedencia esté bien definida, cuando mezcles pipe con otros operadores usa paréntesis para que la intención sea obvia a cualquiera que lea el código.

Comparativa: anidamiento frente a pipe

Aspecto Anidamiento f(g(h($x))) Variables intermedias Pipe $x \|> h(...) \|> g(...) \|> f(...)
Orden de lectura De dentro hacia fuera De arriba abajo De izquierda a derecha / arriba abajo
Variables extra No Sí, una por paso No
Añadir/quitar un paso Difícil (paréntesis) Fácil Muy fácil (una línea)
Funciones con varios parámetros Directo Directo Necesitan envolverse en fn
Versión de PHP Todas Todas 8.5+

⚠️ Importante: No conviertas todo en pipes. Para una o dos funciones, strtoupper(trim($x)) sigue siendo perfectamente legible. El pipe compensa en cadenas de tres o más transformaciones, especialmente al procesar datos.


17. El atributo #[\NoDiscard] (PHP 8.5)

Algunas funciones devuelven un valor que nunca debería ignorarse: el resultado de una validación, un objeto nuevo (en vez de modificar el original), un código de error… Ignorarlo casi siempre es un error.

📘 Concepto: Al marcar una función con el atributo #[\NoDiscard], PHP emite un aviso (warning) si alguien la llama sin usar su valor de retorno. Opcionalmente puedes añadir un mensaje que explique por qué.

<?php
declare(strict_types=1);

#[\NoDiscard("porque contiene los errores de validación")]
function validarRegistro(array $datos): array
{
    $errores = [];
    if (($datos['email'] ?? '') === '') {
        $errores['email'] = 'El email es obligatorio';
    }
    return $errores;
}

validarRegistro(['email' => '']);
// Warning: The return value of function validarRegistro() should either be used
// or intentionally ignored by casting it as (void), porque contiene los errores de validación

$errores = validarRegistro(['email' => '']);  // ✅ Sin aviso: el valor se usa

Ignorarlo a propósito: (void)

Si de verdad quieres descartar el valor, PHP 8.5 añade el cast (void) para dejar claro que es intencionado:

<?php
(void) validarRegistro($datos); // Sin aviso: "sé lo que hago"
Detalle Comportamiento
Dónde se aplica Funciones y métodos
No se permite en Funciones con retorno : void o : never (no tendría sentido)
Tipo de aviso E_USER_WARNING en funciones propias
(void) Es una instrucción, no una expresión: no puede ir dentro de otra expresión
Compatibilidad En PHP < 8.5 el atributo simplemente se ignora

💡 Consejo: Úsalo con moderación, en funciones donde ignorar el resultado sea un fallo real (validaciones, métodos with* de objetos inmutables que verás en el módulo 11). Si lo pones en todas partes, la gente acabará llenando el código de (void).


18. Organizar funciones en ficheros: include y require

Cuando tu proyecto crece, no quieres todas las funciones en un solo fichero. PHP permite incluir el contenido de otro fichero:

mi-proyecto/
├── index.php
└── lib/
    ├── cadenas.php
    └── precios.php

lib/precios.php

<?php
declare(strict_types=1);

function precioConIva(float $precio, float $iva = 21.0): float
{
    return round($precio * (1 + $iva / 100), 2);
}

function formatearEuros(float $cantidad): string
{
    return number_format($cantidad, 2, ',', '.') . ' €';
}

index.php

<?php
declare(strict_types=1);

require_once __DIR__ . '/lib/precios.php';
require_once __DIR__ . '/lib/cadenas.php';

echo formatearEuros(precioConIva(99.99)) . PHP_EOL; // 120,99 €

Las cuatro variantes

Instrucción Si el fichero no existe Si ya se incluyó antes
include Warning y el script continúa Lo vuelve a incluir
require Error fatal y el script se detiene Lo vuelve a incluir
include_once Warning y continúa No hace nada
require_once Error fatal No hace nada

⚠️ Importante: Para ficheros con funciones usa require_once: si el fichero se incluyera dos veces, PHP intentaría redeclarar las funciones y fallaría con Cannot redeclare function. include tiene sentido para fragmentos opcionales, como plantillas de vista.

💡 Consejo: Usa siempre rutas absolutas con __DIR__ (la carpeta del fichero actual). Una ruta relativa como 'lib/precios.php' depende del directorio desde el que se ejecute el script y falla de forma imprevisible.

include también devuelve valores

Un fichero incluido puede terminar con return, lo que se usa mucho para ficheros de configuración:

<?php
// config.php
return [
    'app_nombre' => 'Mi tienda',
    'iva'        => 21,
];
<?php
// index.php
$config = require __DIR__ . '/config.php';
echo $config['app_nombre'] . PHP_EOL; // Mi tienda

En el módulo 13 verás que con clases y Composer no necesitarás escribir require para cada fichero: el autoload los cargará automáticamente.


19. Documentar funciones con PHPDoc

PHPDoc es el estándar de comentarios de documentación en PHP. Empieza con /** y usa etiquetas con @. Los IDE (VS Code con Intelephense, PhpStorm) y herramientas como PHPStan lo leen para darte ayuda y detectar errores.

<?php
declare(strict_types=1);

/**
 * Calcula el precio final de un producto aplicando descuento e IVA.
 *
 * El descuento se aplica antes del impuesto.
 *
 * @param float $precio    Precio base sin impuestos, en euros.
 * @param float $descuento Porcentaje de descuento entre 0 y 100.
 * @param float $iva       Porcentaje de IVA (21 por defecto).
 *
 * @return float Precio final redondeado a 2 decimales.
 *
 * @throws InvalidArgumentException Si el descuento está fuera de rango.
 *
 * @example precioFinal(100, 10) // 108.9
 */
function precioFinal(float $precio, float $descuento = 0, float $iva = 21.0): float
{
    if ($descuento < 0 || $descuento > 100) {
        throw new InvalidArgumentException('El descuento debe estar entre 0 y 100');
    }

    return round($precio * (1 - $descuento / 100) * (1 + $iva / 100), 2);
}

PHPDoc completa lo que los tipos no pueden expresar

El tipo array de PHP no dice qué contiene el array. PHPDoc sí:

<?php
declare(strict_types=1);

/**
 * @param list<string>          $nombres  Lista de nombres (índices 0, 1, 2...)
 * @param array<string, int>    $edades   Mapa nombre => edad
 * @return array{media: float, total: int} Array con forma conocida
 */
function resumen(array $nombres, array $edades): array
{
    $total = count($nombres);
    return ['media' => array_sum($edades) / max($total, 1), 'total' => $total];
}
Etiqueta Uso
@param tipo $nombre descripción Describe un parámetro
@return tipo descripción Describe el valor devuelto
@throws Clase Excepciones que puede lanzar
@deprecated Marca como obsoleta (en PHP 8.4 existe también el atributo #[\Deprecated])
@see, @link Referencias a otro código o URL
list<T>, array<K, V>, array{...} Tipos de array detallados

💡 Consejo: No repitas lo obvio. Si la firma es function sumar(int $a, int $b): int, un PHPDoc que diga “@param int $a el primer número” no aporta nada. Documenta el porqué, las unidades, los rangos válidos y la forma de los arrays.


20. Errores comunes con funciones

Error Causa Solución
Undefined variable $x dentro de la función Las funciones no ven variables exteriores Pásala como parámetro (o use en closures)
Cannot redeclare function Fichero incluido dos veces o nombre repetido Usa require_once y nombres únicos
TypeError: Argument #1 must be of type int, string given Tipo incorrecto con strict_types Convierte el valor antes ((int)) o valida la entrada
ArgumentCountError: Too few arguments Falta un argumento obligatorio Pasa todos los obligatorios o añade valor por defecto
La closure no ve el valor actualizado use ($x) captura por valor Usa use (&$x) si de verdad necesitas el cambio
fn() => $total++ no modifica $total Las arrow functions capturan por valor Usa una closure con use (&$total)
Error de sintaxis en un pipe con fn Arrow function sin paréntesis Escribe \|> (fn($x) => ...)
Allowed memory size exhausted en recursión Falta caso base o recursión demasiado profunda Revisa el caso base o usa un bucle

21. Ejercicios

Ejercicio 1: Biblioteca de conversiones

Crea un fichero lib/conversiones.php con funciones tipadas: celsiusAFahrenheit(float $c): float, fahrenheitACelsius(float $f): float, kmAMillas(float $km): float y formatearNumero(float $n, int $decimales = 2): string (con formato español). Inclúyelo desde index.php con require_once y muestra una tabla de conversiones del 0 al 100 °C de 10 en 10. Documenta cada función con PHPDoc.

Ejercicio 2: Estadísticas variádicas

Escribe estadisticas(int|float ...$numeros): array que devuelva un array con las claves min, max, media y mediana. Si no recibe números, debe devolver un array con todos los valores a null. Llámala con argumentos sueltos y también desplegando un array con .... Usa desestructuración para recoger el resultado.

Ejercicio 3: Generador de etiquetas HTML con argumentos con nombre

Crea enlace(string $url, string $texto, bool $nuevaPestana = false, string $clase = '', ?string $titulo = null): string que genere un <a> correcto (escapa los valores con htmlspecialchars). Haz 4 llamadas distintas usando argumentos con nombre para saltarte parámetros opcionales, por ejemplo enlace('https://php.net', 'PHP', clase: 'btn').

Ejercicio 4: Fábrica de validadores

Escribe funciones que devuelvan closures de validación: longitudMinima(int $min): Closure, contiene(string $texto): Closure y noVacio(): Closure. Cada closure recibe un string y devuelve bool. Después crea validar(string $valor, Closure ...$reglas): bool que devuelva true solo si se cumplen todas (pista: array_all() de PHP 8.4). Prueba a validar contraseñas.

Ejercicio 5: Contador con static y con closure

Implementa un contador de dos formas: (a) una función contador(): int con una variable static; (b) una función crearContador(int $inicio = 0): Closure que devuelva una closure con use (&$cuenta). Demuestra que con la opción (b) puedes tener varios contadores independientes y con la (a) no.

Ejercicio 6: Árbol de categorías recursivo

Dado un array plano de categorías [['id' => 1, 'nombre' => 'Informática', 'padre' => null], ['id' => 2, 'nombre' => 'Portátiles', 'padre' => 1], ...] con al menos 3 niveles, escribe una función recursiva construirArbol(array $categorias, ?int $padre = null): array que devuelva la estructura anidada, y otra pintarArbol(array $arbol, int $nivel = 0): void que la muestre con sangría.

Ejercicio 7: Procesador de texto con pipe

Usando exclusivamente el operador |>, escribe una función normalizarNombre(string $nombre): string que: elimine espacios sobrantes al principio y al final, reduzca los espacios internos múltiples a uno, ponga todo en minúsculas (multibyte) y la primera letra de cada palabra en mayúscula (mb_convert_case con MB_CASE_TITLE). Prueba con ' maría josé GARCÍA '. Después escribe la misma función con funciones anidadas y compara la legibilidad.

Ejercicio 8: Validación que no se puede ignorar

Crea una función comprobarStock(array $inventario, string $producto, int $cantidad): bool marcada con #[\NoDiscard] y un mensaje explicativo. Llámala una vez sin usar el resultado (observa el aviso), otra guardando el resultado en una variable y una tercera con (void). Explica en un comentario en qué casos tendría sentido usar (void).


Resumen

Concepto Sintaxis
Definir función function nombre(int $a): int { return $a; }
Sin retorno function f(): void { }
Tipos unión / nullable int\|float $n, ?string $s
Valor por defecto function f(string $m = '€')
Paso por referencia function f(array &$datos)
Variádico function f(int ...$nums)
Spread al llamar f(...$array)
Argumento con nombre f(ciudad: 'Sevilla')
Variable estática static $n = 0;
Closure con captura function ($x) use ($iva, &$total) { }
Arrow function fn(int $x) => $x * $factor
First-class callable strlen(...), $obj->metodo(...)
Orden superior array_map, array_filter, array_reduce, usort
Recursividad Caso base + llamada a sí misma
Pipe (8.5) $x \|> trim(...) \|> (fn($s) => ...)
NoDiscard (8.5) #[\NoDiscard("motivo")] y (void) f();
Incluir ficheros require_once __DIR__ . '/lib/f.php';
PHPDoc /** @param list<string> $x @return int */