Curso de PHP gratis · Capítulo 10 de 13 · con el Ermitaño
Ámbito, static, closures y callbacks en PHP
Cada función es una isla con sus propias variables. Aquí verás cómo pasar datos entre islas con global, static, use y las funciones flecha.
Cada función, su isla
Ya viste que cada llamada a una función crea su propia tabla de símbolos. La consecuencia: una función no ve las variables globales (las del programa principal), aunque tengan el mismo nombre.
$impuesto = 10; function precio($base) { return $base + $impuesto; // ¡Warning! aquí $impuesto no existe }
¿Por qué PHP es tan estricto? Otros lenguajes dejan que las funciones vean las variables de fuera. PHP decidió lo contrario para que cada función sea predecible: su resultado depende solo de sus parámetros. Si cualquier función pudiera leer y cambiar cualquier variable global, un cambio en un sitio rompería cosas en otro sin avisar.
La solución correcta casi siempre es pasar lo que necesitas como parámetro.
<?php
$impuesto = 10;
function precio_mal($base) {
return $base + $impuesto;
}
function precio_bien($base, $impuesto) {
return $base + $impuesto;
}
echo precio_mal(100), "\n";
echo precio_bien(100, $impuesto), "\n";100 110
global y $GLOBALS
Si de verdad necesitas una variable global dentro de una función, hay dos formas:
global $x;dentro de la función: a partir de ahí,$xes la global.$GLOBALS['x']: un array especial con todas las variables globales.
Un secreto: global $x; equivale exactamente a $x = &$GLOBALS['x'];. Crea una referencia: la variable local $x y la global comparten la misma caja. Lo entenderás del todo en la Torre de los Espejos.
Úsalo lo menos posible. El código con muchas globales es difícil de entender y de probar: cualquier función puede cambiar cualquier cosa.
<?php
$contador = 0;
function incrementar() {
global $contador;
$contador++;
}
incrementar();
incrementar();
echo $contador, "\n";
function leer() {
return $GLOBALS['contador'] * 10;
}
echo leer(), "\n";2 20
static: memoria entre llamadas
Normalmente las variables locales mueren al terminar la función. Pero si la declaras con static, conserva su valor entre llamadas:
function turno() { static $n = 0; // solo se inicializa la primera vez $n++; return $n; }
La línea static $n = 0; solo tiene efecto la primera vez. Después, $n recuerda el valor de la llamada anterior.
Es útil para contadores o para guardar resultados ya calculados (una caché), sin ensuciar el ámbito global.
<?php
function turno() {
static $n = 0;
$n++;
return $n;
}
echo turno(), turno(), turno(), "\n";
function sin_static() {
$n = 0;
$n++;
return $n;
}
echo sin_static(), sin_static(), sin_static(), "\n";123 111
Funciones como valores
En PHP, una función también puede ser un valor: puedes guardarla en una variable, pasarla a otra función o devolverla. Se llaman funciones anónimas o closures:
$doble = function ($x) { return $x * 2; }; // ¡ojo al punto y coma: es una asignación! echo $doble(21); // 42
¿Para qué? Para decirle a otras funciones qué hacer. Por ejemplo, array_map aplica una función a cada elemento de un array, y usort ordena usando la función de comparación que tú le des. A estas funciones que se pasan como argumento se las llama callbacks.
Una función con nombre también se puede pasar como callback escribiendo su nombre como string: array_map('strtoupper', $a).
<?php
$doble = function ($x) {
return $x * 2;
};
echo $doble(21), "\n";
$cosechas = [3, 8, 1, 12];
print_r(array_map($doble, $cosechas));
print_r(array_map('strtoupper', ["heno", "maiz"]));42
Array
(
[0] => 6
[1] => 16
[2] => 2
[3] => 24
)
Array
(
[0] => HENO
[1] => MAIZ
)use: llevar variables a la isla
Una closure tampoco ve las variables de fuera... salvo que las traigas con use:
$factor = 3; $multiplicar = function ($x) use ($factor) { return $x * $factor; };
Importante: use ($factor) copia el valor en el momento de crear la closure. Si después cambias $factor, la closure no se entera.
Si quieres que la closure vea (y pueda cambiar) la variable original, usa use (&$factor): se captura por referencia.
<?php
$factor = 3;
$mult = function ($x) use ($factor) {
return $x * $factor;
};
$factor = 100;
echo $mult(2), "\n";
$total = 0;
$sumar = function ($x) use (&$total) {
$total += $x;
};
$sumar(5);
$sumar(7);
echo $total, "\n";6 12
$n = 1; $f = function () use ($n) { return $n; }; $n = 2; echo $f(); imprime...
- 1
- 2
- Un Warning
Ver respuesta
A. 1
use ($n) copió el valor 1 al crear la closure. El cambio posterior no le afecta.
Funciones flecha: fn
Para closures de una sola expresión, PHP 7.4 trajo las funciones flecha:
$doble = fn($x) => $x * 2;
- No llevan llaves ni
return: devuelven la expresión de la derecha. - Capturan automáticamente (por valor) las variables de fuera que usen. ¡No necesitan
use!
Son perfectas para callbacks cortos.
<?php
$precios = [10, 25, 3, 40];
$iva = 1.21;
$conIva = array_map(fn($p) => round($p * $iva, 2), $precios);
print_r($conIva);
$caros = array_filter($precios, fn($p) => $p > 15);
print_r($caros);Array
(
[0] => 12.1
[1] => 30.25
[2] => 3.63
[3] => 48.4
)
Array
(
[1] => 25
[3] => 40
)Callbacks para arrays
Las funciones de arrays que aceptan callbacks son herramientas muy potentes:
array_map(fn, $a): transforma cada elemento.array_filter($a, fn): se queda con los que cumplen la condición (conserva las claves).array_reduce($a, fn, $inicial): "reduce" el array a un solo valor.usort($a, fn($x, $y) => ...): ordena con tu comparación. La función debe devolver negativo, 0 o positivo: ¡la nave espacial<=>es perfecta!
Mira cómo se ordena por dos criterios: primero por peso (de mayor a menor) y, si empatan, por nombre.
<?php
$sacos = [
["nombre" => "trigo", "peso" => 20],
["nombre" => "avena", "peso" => 35],
["nombre" => "cebada", "peso" => 20],
];
usort($sacos, fn($a, $b) => [$b["peso"], $a["nombre"]] <=> [$a["peso"], $b["nombre"]]);
foreach ($sacos as $s) {
echo $s["nombre"], " ", $s["peso"], "\n";
}
$total = array_reduce($sacos, fn($acc, $s) => $acc + $s["peso"], 0);
echo "Total: $total kg\n";avena 35 cebada 20 trigo 20 Total: 75 kg
Funciones que recuerdan
Juntando todo: una función que devuelve una closure que captura una variable por referencia. Cada closure creada tiene su propia variable "privada" que sobrevive entre llamadas:
function crear_contador() { $n = 0; return function () use (&$n) { return ++$n; }; }
Aunque crear_contador() ya terminó, su $n sigue viva porque la closure la sujeta (su refcount no llega a 0). Y dos contadores creados por separado no comparten nada.
Esta idea (una función que "encierra" variables de donde se creó) es la que da nombre a las closures (clausuras).
<?php
function crear_contador() {
$n = 0;
return function () use (&$n) {
return ++$n;
};
}
$a = crear_contador();
$b = crear_contador();
$a();
$a();
echo $a(), " ", $b(), "\n";3 1
Ejercicios de Ámbito, static, closures y callbacks en PHP resueltos
Intenta resolverlos antes de mirar la solución. En el juego puedes ejecutarlos y comprobarlos automáticamente.
Predicción 1. Islas y puentes
El Ermitaño dibuja islas en la arena. ¿Qué imprime su programa?
Escribe exactamente la salida.
<?php
$x = 10;
function a() {
$x = 1;
return $x;
}
function b() {
global $x;
$x++;
return $x;
}
function c() {
static $x = 5;
return ++$x;
}
echo a(), " ", b(), " ", c(), " ", c(), " ", $x;Ver solución
1 11 6 7 11
¡Tres islas distintas con tres $x distintas! Local, global y static.
Encargo 2. Los turnos del Faro
Los barcos piden turno para atracar. El Ermitaño necesita una función que dé números correlativos.
Declara siguiente_turno(): int que devuelva 1 la primera vez que se llama, 2 la segunda, etc. Sin usar variables globales.
Pistas
static $turno = 0;dentro de la función.
Ver solución
<?php
function siguiente_turno(): int {
static $turno = 0;
$turno++;
return $turno;
}
Encargo 3. Filtrar la cosecha
El Ermitaño apunta los sacos que llegan en $cosechas (una lista de números). Solo le interesan los de 10 o más kilos.
Imprime los sacos de 10 kg o más, cada uno con el formato N kg, separados por comas. Por ejemplo, para [3, 12, 10, 7, 25]:
12 kg, 10 kg, 25 kg
Usa array_filter y array_map con funciones flecha o closures.
Pistas
$grandes = array_filter($cosechas, fn($k) => $k >= 10);$textos = array_map(fn($k) => "$k kg", $grandes);echo implode(", ", $textos);
Ver solución
<?php
$grandes = array_filter($cosechas, fn($k) => $k >= 10);
$textos = array_map(fn($k) => "$k kg", $grandes);
echo implode(", ", $textos);
Encargo 4. Ordenar la bodega
La bodega del Faro guarda barriles en $barriles: cada uno es un array con nombre y litros.
Ordena los barriles de más a menos litros y, si empatan, por nombre alfabético. Luego imprime los nombres separados por espacios.
Pistas
usort($barriles, fn($a, $b) => ...);debe devolver negativo si$ava antes.- Primero compara los litros al revés:
$b["litros"] <=> $a["litros"]. Si da 0, compara los nombres. - Truco: compara arrays:
[$b["litros"], $a["nombre"]] <=> [$a["litros"], $b["nombre"]].
Ver solución
<?php
usort($barriles, function ($a, $b) {
if ($a["litros"] !== $b["litros"]) {
return $b["litros"] <=> $a["litros"];
}
return $a["nombre"] <=> $b["nombre"];
});
echo implode(" ", array_column($barriles, "nombre"));
Caza de bugs 5. La isla incomunicada
Esta closure debería añadir el impuesto del puerto a cada precio, pero da avisos y resultados raros.
Arregla el código (sin avisos) para que imprima 110, 220, 55.
Código de partida:
<?php
$impuesto = 10;
$aplicar = function ($precio) {
return $precio + $precio * $impuesto / 100;
};
echo implode(", ", array_map($aplicar, [100, 200, 50]));
Pistas
- Dentro de la closure
$impuestono existe. - Tráela con
use ($impuesto), o usa una función flecha, que captura sola.
Ver solución
<?php
$impuesto = 10;
$aplicar = function ($precio) use ($impuesto) {
return $precio + $precio * $impuesto / 100;
};
echo implode(", ", array_map($aplicar, [100, 200, 50]));
Misión del dron 6. Estrategias intercambiables
El Ermitaño quiere UNA función que recorra el campo y, en cada casilla, haga lo que tú le digas.
Declara recorrer(callable $accion) que visite las 9 casillas del campo 3x3 y llame a $accion() en cada una. Después úsala dos veces: primero para cosechar lo que esté maduro, y luego para plantar HIERBA donde haya quedado vacío.
Esta misión mueve al dron BIT-8 por un campo de cultivo: juégala en PHPlay para ver tu código animado.
Pistas
- Dentro de
recorrer: dos buclesfory$accion();antes de cadamover(ESTE). - Segunda llamada:
recorrer(fn() => que_hay() === null ? plantar(HIERBA) : null);
Ver solución
<?php
function recorrer(callable $accion) {
$n = tam();
for ($y = 0; $y < $n; $y++) {
for ($x = 0; $x < $n; $x++) {
$accion();
mover(ESTE);
}
mover(NORTE);
}
}
recorrer(function () {
if (puede_cosechar()) {
cosechar();
}
});
recorrer(function () {
if (que_hay() === null) {
plantar(HIERBA);
}
});
Chuleta: lo esencial
- Una función no ve las variables de fuera (su propia tabla de símbolos). Pasa parámetros.
global $x;≡$x = &$GLOBALS['x'];(una referencia). Evítalo.static $n = 0;conserva su valor entre llamadas.- Closure:
$f = function ($x) use ($y) { ... };—usecopia el valor al crearla;use (&$y)por referencia. - Flecha:
fn($x) => $x * $ycaptura sola, por valor. - Callbacks:
array_map(fn, $a),array_filter($a, fn)(conserva claves),array_reduce($a, fn, $ini),usort($a, fn($a, $b) => $a <=> $b). - Una closure que captura por referencia mantiene viva la variable (contador privado).