14/7/10

Referencias en PHP y funciones útiles auxiliares

Uno de los aspectos interesantes de PHP es la gestión de referencias.
Como es bien sabido en PHP, intentar acceder a una variable o una clave en un array que no existe produce un notice y generalmente hay que estar tratando continuamente con isset y con bastantes código redundante.
El caso es que PHP permite crear una referencia a una variable inexistente para crearla a posteriori. Es como crear un slot de la variable en potencia y asignarlo si así se decide. También permite comprobar la existencia de la variable referenciada usando la variable de referencia.

Es bastante típico en PHP querer obtener el valor de una variable si existe, o un valor por defecto en el caso de que no exista. Ejemplo:

$page = isset($_GET['page']) ? $_GET['page'] : 1;

En PHP 5.3 se añadió un shortcut del operador ternario. A priori parecía bastante interesante, porque teóricamente hubiese podido permitir hacer esto: $page = $_GET['page'] ?: 1;
Pero resulta que la cagaron estrepitosamente. Resulta que lo que hace el shortcutdel ternario es evaluar la parte de la izquierda y si al castear implícitamente a bool, el resultado es false, se devuelve el resultado de la expresión de la derecha. Esto para empezar no evita el tema de los notices al intentar acceder a una clave inexistente en el array. Además hay un pequeño matiz que difiere con el isset. Y es que si la variable existe pero tiene un valor evaluado como false (como una cadena vacía o el valor 0), ya se estaría devolviendo lo de la derecha. Y generalmente no es lo deseado.
Así que veo completamente inútil esta adición al PHP 5.3.

Sin embargo PHP y sus referencias permiten crear funciones auxiliares que permiten resolver estos problemas con facilidad y evitando repetir la variable dos veces:

$page = isset_default($_GET['page'], 1);

Ésto es posible sin notices porque en las últimas versiones de PHP especificas los valores que se pasan por referencia en la propia descripción de la función. Así pues:


function isset_default(&$var, $default) {
    return isset($var) ? $var : $default;
}

Esto vendría a ser como una macro bastante útil.

También es bastante común comprobar la existencia de varias claves en un array.

if (isset($_GET['a'], $_GET['b'], $_GET['c'])) { }
->
if (isset_array($_GET, array('a', 'b', 'c'))) { }

Aquí la referencia evita que PHP duplique un array potencialmente grande.


function isset_array(array &$array, array $keys) {
    foreach ($keys as $key) if (!isset($array[$key])) return false;
    return true;
}

Otro patrón común que se puede resolver con macros de referencias es hacer un cambio a posteriori de una variable.

function method() {
    if ($this->alreadyPerformed) return;
    $this->alreadyPerformed = true;
    ...
}
->

function method() {
    if (post_assign($this->alreadyPerformed, true)) return;
    ...
}

function post_assign(&$var, $value) {
    $last_value = $var;
    $var = $value;
    return $last_value;
}


En este caso concreto también se podría usar el pequeño truco de usar enteros para evitar tener la variable dos veces referenciada. Y usar el operador unario del postincremento:

function method() {
    if ($this->alreadyPerformed++) return;
    ...
}

Hay muchas formas útiles de usar las referencias en PHP. Por ejemplo para tener una caché temporal en memoria muy ligera:

static function cached_method($key) {
    static $cache = array();
    $result = &$cache[$key];
    if (!isset($result)) {
        // ...
    }
    return $result;
}

Con referencias y PHP 5.3:

function &cache_result(&$var, $func) {
    if (!isset($var)) $var = $func();
    return $var;
}

static function cached_method($key) {
    static $cache = array();
    return cache_result($cache[$key], function() use ($key) {
         // ...
    });
}

Otras funciones que considero útiles aunque no tengan que ver directamente con referencias:

Por ejemplo si queremos obtener una array a partir de otro sin las claves que empiezan por _:

$array = array_filter_keys($_GET, function($key) { return substr($key, 0, 1) != '_'; } );

function array_filter_keys(&$array, $callback) {
    $result = array();
    foreach ($array as $key => $value) {
        if ($callback($key, $value)) $result[$key] = $value;
    }
    return $result;
}

En determinadas ocasiones queremos ver un array visualmente que contiene html. Con xdebug la functión var_dump puede estar coloreada. Pero ni siempre está disponible el xdebug, ni tampoco es siempre lo más cómodo visualmente. Aquí hay una función que permite mostrar un print_r para html. Haciendo uso del tag pre y del escapado de html.

function print_r_pre($var) {
    echo '<pre>';
    echo htmlspecialchars(print_r($var, true));
    echo '</pre>';
}