PHPlay

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
}
Pergamino del porque

¿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";
Salida
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í, $x es la global.
  • $GLOBALS['x']: un array especial con todas las variables globales.
Pergamino del porque

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.

Cuidado

Ú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";
Salida
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";
Salida
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.

Truco

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"]));
Salida
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";
Salida
6
12
Pregunta

$n = 1; $f = function () use ($n) { return $n; }; $n = 2; echo $f(); imprime...

  1. 1
  2. 2
  3. 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);
Salida
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";
Salida
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.

Pergamino del porque

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";
Salida
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 $a va 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 $impuesto no 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 bucles for y $accion(); antes de cada mover(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) { ... }; — use copia el valor al crearla; use (&$y) por referencia.
    • Flecha: fn($x) => $x * $y captura 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).