Publi

Lectura, escritura y eliminación de elementos de un array multidimensional en PHP usando separadores

14300968086_927be23a6f_o

Puede parecer muy complejo así dicho. Pero de lo que se trata es de proporcionar una forma más natural para acceder a lo elementos de un array en PHP. Nos podemos imaginar un array de configuración de una aplicación, donde encontremos apartados como cookies, idiomas, usuarios, rutas, urls, apis externas, bases de datos e infinidad de cosas más. Hace un tiempo veíamos una función para acceder a una clave de un array, comprobando antes la existencia de esa clave y dándonos la opción de devolver un valor por defecto en caso de que dicha clave no exista.

Nota: si ya eres usuario de un Framework PHP, seguramente tengas métodos para hacer esto mismo, y ya estará todo hecho, a lo mejor con más opciones, aunque si quieres ver cómo está hecho, continúa leyendo. 🙂

Aquí recuerdo dicha función (porque el post anterior es muy largo):

1
2
3
4
5
<?php
function av($array, $key, $default=null)
{
  return (isset($array[$key]))?$array[$key]:$default;
}

De esta forma, en lugar de hacer $array[‘clave’] para leer un valor del array, haremos av($array, ‘clave’, 123), de esta forma podremos devolver 123 en caso de que la clave del array no exista, además de verificar la existencia de la clave previa al acceso, con lo que ahorraremos tiempo de ejecución (no es una eternidad, pero todo lo que podamos optimizar, mejor). En el post vemos que la mejor forma es no utilizar la función pero sí comprobar la existencia de la variable, aunque de esta forma estaremos escribiendo el array y la clave dos veces, y de cara a la programación eso no es muy útil y puede dar lugar a errores.

A lo que vamos hoy es a un array como el siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$datos = array('usuarios' => array('admin' => array('email' => 'admin@miweb.com',
                                                    'nombre' => 'Administrador',
                                                    'nivel' => 999,
                                                    'ultima_entrada' => '2015-07-30 10:10:10',
                                                    ),
                                  'bartolome' => array('email' => 'bartolome@miweb.com',
                                                       'nombre' => 'Bartolomé Zancajo',
                                                       'ultima_entrada' => '2015-08-01 10:10:10',
                                                       ),
                                   'carlos' => array('email' => 'carlos@miweb.com',
                                                     'nombre' => 'Carlos Yuste',
                                                     'nivel' => 101,
                                                     'ultima_entrada' => '2015-08-02 10:10:10',
                                                     ),
                                   'diego' => array('email' => 'diego@miweb.com',
                                                    'nombre' => 'Diego Ximenez',
                                                    'ultima_entrada' => '2015-08-03 10:10:10',
                                                    ),
                                   'ernesto' => array('email' => 'ernesto@miweb.com',
                                                      'nombre' => 'Ernesto Wisconsin',
                                                      'nivel' => 103,
                                                      'ultima_entrada' => '2015-08-04 10:10:10',
                                                      ),
                                   ),
                 );

Sería muy interesante poder acceder al nivel del usuario carlos especificando la ruta: «usuarios/carlos/nivel». Por un lado, podemos pensar que es inmediato hacer $datos[‘usuarios’][‘carlos’][‘nivel’]. Pero tenemos el problema de comprobar que todos los elementos de la ruta existen (si miráis el array, no todos los usuarios tienen nivel, por lo que puede haber un nivel por defecto para todos los usuarios que no lo tengan, y así ahorramos algo de memoria; por lo que para poder acceder correctamente, es decir, sin producir errores, deberíamos hacer lo siguiente:

1
2
3
<?php
if ( (isset($datos['usuarios'])) && (isset($datos['usuarios']['carlos'])) && (isset($datos['usuarios']['carlos']['nivel'])) )
   echo $datos['usuarios']['carlos']['nivel'];

y eso es muy largo. Es más, no es muy costoso generar la cadena «usuarios/carlos/nivel» o «usuarios.carlos.nivel» y pasársela a una función tipo:

1
2
<?php
  echo ag($datos, 'usuarios/carlos/nivel');

es más, dicha cadena puede ser construida de forma fácil y dinámica, por ejemplo con los valores de un formulario. En fin, tiene muchas posibilidades.

Dejo aquí un ejemplo completo para la función ag(), para hacer copia y pega directamente (el array utilizado varía ligeramente del anterior, para hacerlo un poco más largo y meter índices numéricos, sólo por hacer un test más):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
function av($array, $key, $default=null)
{
  return (isset($array[$key]))?$array[$key]:$default;
}

function ag($data, $path, $default=null, $sep='/')
{
  if (empty($data))
    return $default;

  $components = explode($sep, $path);

  foreach ($components as $k)
    {
      if (!isset($data[$k]))
        return $default;
      $data=$data[$k];
    }
  return $data;
}

$usuarios = array('admin' => array('email' => 'admin@miweb.com',
                                   'nombre' => 'Administrador',
                                   'nivel' => 999,
                                   'ultima_entrada' => '2015-07-30 10:10:10',
                                   ),
                  'bartolome' => array('email' => 'bartolome@miweb.com',
                                   'nombre' => 'Bartolomé Zancajo',
                                   'ultima_entrada' => '2015-08-01 10:10:10',
                                   ),
                  'carlos' => array('email' => 'carlos@miweb.com',
                                   'nombre' => 'Carlos Yuste',
                                   'nivel' => 101,
                                   'ultima_entrada' => '2015-08-02 10:10:10',
                                   ),
                  'diego' => array('email' => 'diego@miweb.com',
                                   'nombre' => 'Diego Ximenez',
                                   'ultima_entrada' => '2015-08-03 10:10:10',
                                   ),
                  'ernesto' => array('email' => 'ernesto@miweb.com',
                                   'nombre' => 'Ernesto Wisconsin',
                                   'nivel' => 103,
                                   'ultima_entrada' => '2015-08-04 10:10:10',
                                   ),
                  );
$larga = array('datos' => array('colecciones' => array($usuarios),
                                'permisos' => 'read,write'),
               );

echo "Email existente: ".ag($usuarios, 'ernesto/email')."\n";
echo "No existe user: ".ag($usuarios, 'gabriel/email')."\n";
echo "No existe elemento: ".ag($usuarios, 'carlos/apellido')."\n";
echo "Devuelvo array en cadena larga: ";
print_r(ag($larga, 'datos/colecciones/0/admin'));

Un vistazo a la función

¿ Cómo estamos haciendo esto ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ag($data, $path, $default=null, $sep='/')
{
  if (empty($data))
    return $default;

  $components = explode($sep, $path);

  foreach ($components as $k)
    {
      if (!isset($data[$k]))
        return $default;
      $data=$data[$k];
    }
  return $data;
}

La clave de la realización de esta función radica en la separación de los elementos de la ruta (con explode() y el hacer corresponder cada uno de los elementos de la ruta con las claves del array, para eso usamos el foreach(). Para cada $k que obtenemos, realizamos la comprobación de existencia de dicha clave en el array de datos, en caso de no existir, sabemos que ya no vamos a encontrar más elementos de la ruta, y por tanto, devolvemos el valor por defecto; en caso contrario (si la clave existe en el array), haremos que el array con el que trabajamos se reduzca al elemento $k de nuestro array.

¡ Queremos algo más !

Vale, pues qué tal la creación y eliminación de elementos dentro de ese array. Es decir, que con una sola línea de código podamos dar valor a la ruta «datos/colecciones/0/felipe/email», es decir, debería crearse el elemento «felipe» y luego el elemento «email», o, complicándolo más, podemos crear la ruta: «datos/config/database/username, es decir, crear config, database y username, eso sí, aunque eso podríamos hacerlo de forma normal:

1
$array['datos']['config'] = array('database' => array('username' => 'nombre'));

podríamos tener un problema si no sabemos seguro si algún elemento de la cadena existe, por ejemplo, que ‘config’ pueda existir o no.

Para ello, tenemos la función aset():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
function aset(&$data, $path, $newData, $sep='/')
{
  $_data=&$data;                /* Hacemos copia de la referencia de data, para poder  */
                                /* operar con $_data sin miedo a perder la posición original */
  $components = explode($sep, $path);
  $ccount = count($components);

  for ($i=0; $i<$ccount; $i++)
    {
      $key = $components[$i];
      if ($i == $ccount-1)
          $_data[$key] = $newData;
      elseif (!isset($_data[$key]))
        $_data[$key] = array();

      $_data =& $_data[$key];   /* Movemos el puntero de $data a $data[$key] */
    }

  return $data;                 /* Devolvemos el $data original */
}

Aquí, la clave está en que, en lugar de machacar nuestro array con $data[$k], estamos diciendo que $_data coja la referencia de $_data[$key], es decir, el array entero se mantiene en memoria, nosotros simplemente navegamos por él, por el árbol original, y no por copias de sub-árboles (y todo esto, sólo poniendo un &, esto se hace mucho en C, en PHP no tanto, pero podemos).

Para hacer una pequeña prueba podemos:

1
2
3
4
5
<?php
/* Definimos un elemento existente */
aset($larga, 'datos/colecciones/0/ernesto/email', 'ernest@hemingway.hem');
/* Definimos un elemento no existente */
aset($larga, 'datos/colecciones/1/fernando/nombre', 'Fernando Vicario');

símplemente, copiando la función y estas líneas en el ejemplo de ag().

¿Y si quiero borrar una rama?

Pues también podemos, de una forma muy parecida,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
function adel(&$data, $path, $sep='/')
{
  $_data = &$data;

  $components = explode($sep, $path);
  $ccount = count($components);

  for ($i=0; $i<$ccount; $i++)
    {
      $key = $components[$i];
      if (!isset($_data[$key]))
        return false;
      elseif ($i == $ccount-1)
        {
          unset($_data[$key]);
          return true;
        }

      $_data =& $_data[$key];
    }

  return false;                 /* Nunca llegaremos aquí porque si el elemento no existe,
                                   el return false lo haremos arriba, y si existe, alguno
                                   tendrá que ser el último existe y ese devolverá true.*/

                                /* Siempre podemos poder un break en lugar de return false dentro
                                   del bucle. */

}

Y también dejo algo de código para probar esta función:

1
2
3
4
5
6
7
<?php
/* Borramos un elemento existente */
echo "Borro un elemento existente: ";
var_dump(adel($larga, 'datos/colecciones/0/bartolome/ultima_entrada'));
echo "Borro un elemento inexistente: ";
var_dump(adel($larga, 'datos/elemento/no/existe'));
print_r($larga);

Algunas notas más

Como vemos, estas funciones tienen un argumento llamado $sep, que por defecto vale una barra «/», aunque no es necesario utilizarlo, puede que nos interese separar los elementos de la ruta por un punto en lugar de una barra, basta con poner ese valor a «.».

Aunque las funciones no son especialmente lentas, si vamos a hacer muchas llamadas (imaginemos dentro de un array), es inevitable que se retrase la ejecución del código, por lo que es una buena técnica que en lugar de esto:

1
2
3
4
echo "Nombre: ".ag($larga, 'datos/colecciones/0/ernesto/nombre')."\n";
echo "Nivel: ".ag($larga, 'datos/colecciones/0/ernesto/nivel')."\n";
echo "E-mail: ".ag($larga, 'datos/colecciones/0/ernesto/email')."\n";
echo "Última entrada: ".ag($larga, 'datos/colecciones/0/ernesto/ultima_entrada')."\n";

hagáis esto:

1
2
3
4
5
$usuario = ag($larga, 'datos/colecciones/0/ernesto');
echo "Nombre: ".av($usuario, 'nombre')."\n";
echo "Nivel: ".ag($usuario, 'nivel')."\n";
echo "E-mail: ".ag($usuario, 'email')."\n";
echo "Última entrada: ".av($usuario, 'ultima_entrada')."\n";

Por lo que la búsqueda grande sólo la hacemos una vez… y, si de verdad estamos seguros de que el usuario devuelve todos los datos sin problema, podemos prescindir de av() y hacer $usuario[‘nombre’], $usuario[‘nivel’]…

Por último, indicar que todos los nombres (con apellidos) utilizados en los ejemplos son inventados, o de personas famosas (lo digo por ernest@hemingway, si os fijáis, quitando Administrador, el nombre que empieza por B, su apellido por Z, el que empieza por C, su apellido por Y, y así sucesivamente.

Foto: Carrie Roy (Flickr CC)

También podría interesarte....

Leave a Reply