Yellow Seesaw on Blue Background

A principios de este año, comentamos cómo puedes implementar tu propia versión de Zod en PHP para definir esquemas y, así, validar/sanear datos. Hoy me gustaría que hiciéramos algo similar con otra biblioteca muy chula… ¡Implementemos nuestra propia versión de Lodash para PHP!

Pero antes de entrar en faena, permíteme recordarte que lo que veremos hoy aquí no es estrictamente necesario: si necesitas Lodash en PHP, existe un paquete Composer para ello (vamos, igual que pasaba con Zod). Pero hacerlo tú mismo tiene su gracia, ¿no crees? Arremangándote y poniéndote manos a la obra entenderás mejor cómo funcionan estas bibliotecas y probablemente descubras cositas chulas por el camino. Así que, venga, ¿nos ponemos?

Lodash

Primero de todo, ¿qué es Lodash? A ver, que si estás aquí imagino que es porque ya lo sabes… pero para asegurarnos de que todos estamos en la misma página, repasémoslo en un par de minutos.

Lodash es una biblioteca de utilidades para JavaScript. Es decir, es una colección de funciones auxiliares que realizan tareas comunes con el objetivo de hacer más sencilla la vida de los desarrolladores al resolver problemas frecuentes.

Por ejemplo, digamos que tienes una colección de elementos como ésta:

const collection = [
  { name: 'Alice', age: 41 },
  { name: 'Bob', age: 23 },
  { name: 'John', age: 38 },
];Lenguaje del código: TypeScript (typescript)

y te gustaría tener un diccionario de elementos, indexándolos por su nombre:

const dictionary = {
  Alice: { name: 'Alice', age: 41 },
  Bob:   { name: 'Bob', age: 23 },
  John:  { name: 'John', age: 38 },
};Lenguaje del código: TypeScript (typescript)

¿Cómo lo harías en JavaScript? ¿Cómo pasarías de la collection al dictionary?

Lógicamente hay un montón de opciones. La primera y más obvia, si no quieres usar bibliotecas externas, es usando la función reduce de un array :

const dictionary = collection.reduce(
  ( r, i ) => ( { ...r, [ i.name ]: i } ),
  {}
);Lenguaje del código: TypeScript (typescript)

en el cual vamos acumulando cada elemento de la colección al diccionario de salida. Pero coincidirás conmigo de que el código anterior requiere un poquito de esfuerzo para entenderlo. Salvo que, claro está, lo abstraigas en una función… que es precisamente lo que hace Lodash por ti:

import { keyBy } from 'lodash';
const dictionary = keyBy( collection, 'name' );Lenguaje del código: TypeScript (typescript)

La belleza de las bibliotecas de utilidades como Lodash es que ponen al alcance de tu mano un montón de funciones super cómodas, con comportamientos bien definidos y bien documentadas, para solventar problemas comunes, generando así un código más limpio e inteligible. Además, si estás desarrollando en WordPress, usar Lodash no supone ninguna sobrecarga; la biblioteca está disponible en el Core así que, ¿por qué no usarla?

Implementación

Así que, venga, después de esta pequeña introducción y ejemplo, vamos a implementar unas pocas funciones de Lodash en PHP y comentemos los problemas o dificultades que puedes encontrarte al hacerlo.

Solo un último aviso: para simplificar, omitiré los namespaces en todos los fragmentos de código que comparta contigo. Pero tú supón que está ahí, por aquello de evitar colisiones de nombres y tal…

Utilidades y funciones

Comencemos con algunas funciones auxiliares interesantes.

flow

flow es una función que me encanta. Si empiezas a usarla, verás cómo el código resultante es más bonito y fácil de entender que las alternativas donde no dispones de dicha función.

Echemos un vistazo a la documentación oficial para entender un poco qué hace. En esencia, dada una lista de funciones, [flow] creará una nueva función encargada de invocar todas las funciones en orden de tal forma que el resultado de cada invocación se convierte en la entrada de al siguiente.

Por ejemplo, supongamos que tienes una lista de números ($numbers) y quieres convertir en una cadena (stringify) el número más grande (get_max) de la lista para, finalmente, obtener la longitud (strlen) de dicha cadena. No preguntes por qué; tú sígueme el rollo. Pues bien, probablemente harías algo tal que así:

$max     = get_max( $numbers );
$string  = stringify( $max );
$result  = strlen( $string );Lenguaje del código: PHP (php)

¿Ves? Aquí está más o menos claro que vamos aplicando transformaciones en cada línea de código. Pero hay un poco de ruído: en cada paso (o línea) hay que definir una variable intermedia y hay que invocar explícitamente cada función con el resultado anterior. No es un drama, pero bueno.

Otra forma de implementarlo sería esta solución:

$result = strlen( stringify( get_max( $numbers ) ) );Lenguaje del código: PHP (php)

la cual es mucho más compacta, pero sigue sin ser ideal. Para empezar, hay muchas funciones anidadas y, cuantas más funciones tengas, más complicado será leerlas. Además, las funciones aparecen en el orden opuesto: la primera función en la que se fijan tus ojos es strlen, aunque ese sea el último paso del proceso de transformación.

Pues bien, flow tiene como objetivo resolver todos los problemas anteriores. Al invocar flow, todo lo que tienes que hacer es proporcionar una lista de funciones/transformaciones y el resultado será, como dijimos, una nueva función que las aplicará todas en secuencia:

flow( [
  'get_max',
  'stringify',
  'strlen',
] )( $numbers )Lenguaje del código: PHP (php)

Aquí, de un vistazo, te queda claro qué transformaciones aplicaremos, en qué orden y cuál será el tipo del resultado final, ¿no?

¿Cómo podemos implementar esto en PHP? Pues muy fácil, la verdad:

function flow( array $functions ) {
  return fn( $value ) => array_reduce(
    $functions,
    fn( $result, $function ) => call_user_func( $function, $result ),
    $value
  );
}Lenguaje del código: PHP (php)

Te lo explico:

  • En primer lugar, definimos flow como una función que recibe una lista de $functions
  • Sabemos que flow produce una nueva función, por lo que lo normal es encontrarnos una función en el return. Y eso es lo que tenemos: la construcción de una nueva función anónima fn( $value ) con un único parámetro de entrada $value. Este $value es, lógicamente, el valor inicial al que aplicaremos todas las transformaciones.
  • Finalmente, solo nos queda pensar cómocombinar todas $functions de forma que el resultado de una función se convierta en la entrada de la siguiente. Hay muchas formas de hacerlo, pero en mi opinión la más natural es usando array_reduce:
    • El primer argumento es la lista sobre la que queremos aplicar la reducción: $functions.
    • En segundo lugar, le damos la función que nos permite combinar cada elemento de $functions en un resultado final es una función anónima que toma dos argumentos: el $result del paso anterior y la $function que necesitamos aplicar ahora usando call_user_func para generar un nuevo resultado.
    • Como tercer parámetro, especificamos el valor que le debemos pasar a la primera función en este preceso de transformaciones. Lógicamente, ese valor es el $value que nos pasarán cuando alguien invoque a la función anónima devuelta por flow.

negate

Un predicado es una función que, dado un valor determinado, devuelve un valor boolean según aplique o no alguna condición o evaluación. Por ejemplo, el predicado even (par) devolvería true si un número es par y false en caso contrario.

Pues bien, negate es una función que trabaja con predicados y que, como su nombre indica, niega el resultado de un predicado cualquiera. Por ejemplo, si quieres crear el predicado odd (impar), puedes definirlo como negate( even ).

A pesar de lo simple que es esta función, tenerla a mano nos será muy útil para construir funciones más complejas con poco código. Fíjate qué pinta tiene:

function negate( $predicate ) {
  return fn( ...$values ) => ! call_user_func( $predicate, ...$values );
}Lenguaje del código: PHP (php)

Como era de esperar, negate crea un nuevo predicado que invoca al$predicate original y niega su resultado. Fácil, ¿eh?

Colecciones

Ahora que hemos sentado algunas bases, veamos cómo podemos aprovechar (algunas de) las funciones anteriores para crear funciones más interesantes de Lodash con nuestro propio Lodash.

some

Dada una $collection de elementos y un $predicate, some verifica si el $predicate devuelve true para algún elemento de la colección. Basta con que uno cumpla el predicado para dar la colección por buena.

Esto lo podemos conseguir usando una vez más array_reduce :

function some( array $collection, callable $predicate ): bool {
  return array_reduce(
    $collection,
    fn( $result, $item ) => (
      $result ||
      ! empty( call_user_func( $predicate, $item ) )
    ),
    false
  );
}Lenguaje del código: PHP (php)

Sin embargo, en Lodash nos dicen que «la iteración se detiene tan pronto como se encuentra un elemento que cumple el predicado». Sin embargo, array_reduce itera sobre todos los elementos sí o sí… Si queremos aplicar esta pequeña optimización a nuestro código, debermos usar un bucle foreach :

function some( array $collection, callable $predicate ): bool {
  foreach ( $collection as $item ) {
    if ( ! empty( call_user_func( $predicate, $item ) ) ) {
      return true;
    }
  }
  return false;
}Lenguaje del código: PHP (php)

every

every es muy similar a some, pero en este caso particular verificamos que todos los elementos de $collection satisfacen el $predicate. Por supuesto, podríamos implementar esta nueva función utilizando un array_reduce que utilice ands en lugar de ors:

function every( array $collection, callable $predicate ): bool {
  return array_reduce(
    $collection,
    fn( $result, $item ) => (
      $result &&
      ! empty( call_user_func( $predicate, $item ) )
    ),
    true
  );
}Lenguaje del código: PHP (php)

y ya estaría. Pero, ¿y si queremos aplicar la misma optimización que antes? ¿Podemos ahorrarnos evaluar esta condición en todos los elementos? ¿Hay alguna forma de saber antes de haber visitado toda la $collection si se va a cumplir o no la condición para todos ellos? Bueno, pensemos en ello por un momento.

Si todos los elementos satisfacen el predicado, significa que no hay ninguno que no lo haga. Bastante obvio, ¿no? Entonces, si le damos al vuelta a esto quiere decir que, si encontramos un elemento que no cumple el predicado, entonces no todos los elementos lo cumplen y, por lo tanto, el resultado de every debería ser false.

Veámoslo con un foreach :

function every( array $collection, callable $predicate ): bool {
  foreach ( $collection as $item ) {
    if ( empty( call_user_func( $predicate, $item ) ) ) {
      return false;
    }
  }
  return true;
}Lenguaje del código: PHP (php)

¿Ves? En el momento en el que encontramos un elemento que no satisface el $predicate, nos salimos de la iteración y devolvemos false. Pero si llegamos al final y no hemos encontrado ninguno que fallara… es que todos lo satisfacían, así que podemos devolver true. ¡Genial!

De nuevo, con esta implementación ya tendríamos el trabajo hecho. Pero vamos a darle una pensadita más. Fíjate en la definición que hemos usado para implementar every: «todos los elementos satisfacen el predicado si, y solo si, no hay un solo elemento que no lo haga». ¿No teníamos alguna función para encontrar algún elemento?

¡Pues claro! Podemos implementar every con some y buscando algún elemento que no cumpla el predicado (o, dicho de otra forma, que cumpla el predicado negado):

function every( array $collection, callable $predicate ): bool {
  return ! some( $collection, negate( $predicate ) );
}Lenguaje del código: PHP (php)

Y, ahora sí, fíjate qué pasada: ¡Hemos implementado un método completamente nuevo usando los elementos básicos que hemos estado creando hasta ahora! No me digas no te está gustando esta movida…

key_by

El último ejemplo que quería ver hoy era, por supuesto, el ejemplo con el que comenzamos este post: keyBy. Recuerda: se trata de convertir una lista de elementos en un diccionario, indexando dichos elementos en función de algún valor que todos ellos tienen.

Una primera versión en PHP podría ser tal que así:

function key_by( $collection, $prop ) {
  $keys = array_map(
    fn( $item ) => isset( $item[ $prop ] ) ? $item[ $prop ] : '',
    $collection
  );
  return array_combine( $keys, $collection );
}Lenguaje del código: PHP (php)

En detalle:

  • En primer lugar, necesitamos extraer la clave con la que indexaremos cada elemento de nuestra $collection. Esto es fácil: simplemente comprobamos si el elemento tiene la $prop con la que queremos indexarlo y, si la tiene, la extraemos. De lo contrario, devolvemos la cadena vacía ''.
  • Con esto conseguimos una lista de claves de indexación llamada $keys que, fíjate, tiene exactamente el mismo número de elementos que $collection. Por lo tanto, podemos usar la función array_combine de PHP para «crear un diccionario que utiliza el primer array como claves y el segundo como valores».

Y ya está, ya tienes implementada key_by para que funcione tal cual la usamos en el ejemplo del principio. Pero, entonces, ¿por qué dije que esta es solo una «primera versión»?

Si te lees la documentación de Lodash, verás que el segundo argumento en keyBy no es necesariamente una propiedad de los elementos incluidos en la $collection, sino que también puede ser una función que genera la clave a partir de un elemento.

Y esto no es lo que hemos implementado, claro.

Vaaaale, vamos a escribir la función de nuevo teniendo en cuenta que el segundo parámetro puede ser una función o un string:

function key_by( $collection, $iteratee ) {
  $get_key = is_callable( $iteratee )
    ? $iteratee
    : fn( $item ) => isset( $item[ $iteratee ] ) ? $item[ $iteratee ] : '';
  $keys = array_map(
    fn( $item ) => call_user_func( $get_key, $item ),
    $collection
  );
  return array_combine( $keys, $collection );
}Lenguaje del código: PHP (php)

¿Complicado? Yo te lo cuento:

  • Como ya hemos discutido, el segundo argumento en key_by, $iteratee, puede ser (a) una función que, dado un elemento en la $collection, produce una clave o (b) una propiedad que los elementos en la $collection pueden tener.
  • Dado que $iteratee puede ser dos cosas, necesitamos que nuestra implementación sepa lidiar con ambos casos. De ahí que $get_key se inicialice de una forma u otra según el tipo de $iteratee:
    • Si $iteratee es invocable (es decir, si es una función), sabemos que tenemos que usarlo para obtener la clave de un elemento de la colección, y eso es lo que metemos en $get_key.
    • Si no se puede llamar, entonces es una propiedad que debemos extraer de todos los elementos. Así que, por comodidad, lo que haremos será inicializar $get_key como una función que, dado un elemento de la $collection, extrae la propiedad adecuada (tal y como hacíamos originalmente).
  • De esta forma, tenemos que $get_key siempre es una función (o bien nos la han dado o bien la hemos construido nosotros) que podemos usar dentro de array_map para extraer la clave pertinente que corresponde a cada elemento de la colección.

Así que ya está, ¿no? ¡¿No?!

Siento decirte que no, que todavía no hemos acabado. Lo que acabamos de implementar no siempre funciona… y es por culpa de PHP.

Como imagino que ya sabes, en PHP, puedes especificar funciones callback indicando simplemente su nombre:

$dictionary = key_by( $collection, 'something' );Lenguaje del código: PHP (php)

y, claro, esto plantea un problema interesante que, quizás, no habías pensado. Si existe una función llamada something en nuestro programa y los elementos de nuestra $collection tienen un atributo something en ellos:

function something( $item ) { ... }
$collection = [
  array( 'something' => 'a', ... ),
  array( 'something' => 'b', ... ),
  array( 'something' => 'c', ... ),
];
$dictionary = key_by( $collection, 'something' );Lenguaje del código: PHP (php)

cuando invocamos key_by usando 'something' como $iteratee, ¿qué diablos se supone que tenemos que hacer? Generamos las claves usando la propiedad o la función? Pues, colega, no hay forma de saberlo. Es cosa tuya decidir cómo quieres que funcione tu key_by… pero ahí van un par de ideas.

Solución 1. Crea dos funciones diferentes: key_by para usar propiedades y key_by_fn para usar una función callback. Esto elimina toda ambigüedad y, de hecho, te permite escribir una en función de la otra:

function key_by( $collection, $prop ) {
  $iteratee = fn( $item ) => isset( $item[ $iteratee ] ) ? $item[ $iteratee ] : '';
  return key_by_fn( $collection, $iteratee );
}
function key_by_fn( $collection, $iteratee ) {
  $keys = array_map(
    fn( $item ) => call_user_func( $get_key, $item ),
    $collection
  );
  return array_combine( $keys, $collection );
}Lenguaje del código: PHP (php)

Solución 2. Supón que siempre que te entre un string, será una propiedad y no un callback. Es decir, no aceptes funciones callback en formato string:

function key_by( $collection, $iteratee ) {
  $get_key = is_string( $iteratee ) || ! is_callable( $iteratee )
    ? fn( $item ) => isset( $item[ $iteratee ] ) ? $item[ $iteratee ] : '';
    : $iteratee;
  $keys = array_map(
    fn( $item ) => call_user_func( $get_key, $item ),
    $collection
  );
  return array_combine( $keys, $collection );
}Lenguaje del código: PHP (php)

Pero, claro, ¿y si quieres usar la función something? Pues nada, tendrás que montar una función anónima in situ:

function something( $item ) { ... }
$dictionary = key_by( $collection, fn( $item ) => something( $item ) );Lenguaje del código: PHP (php)

lo cual tampoco es un drama, ¿no?

Y ahora, ¿qué?

Hoy hemos visto cómo convertir una biblioteca tipo Lodash a PHP no es necesariamente una tarea sencilla. Hay que tener en cuenta las particularidades de cada lenguaje, pensar cómo pueden afectarte y buscar soluciones originales.

También has visto que es relativamente sencillo traerte las funciones que te gustan a PHP. Porque, a ver, no nos engañemos: si estás acostumbrado a programar en JavaScript con Lodash, seguro que alguna vez habrás echado de menos alguna de sus funciones en PHP… Y ahora ya sabes que, cuando eso ocurra, crearla tu mismo cuando la necesites no es difícil ni descabellado.

Por ejemplo, hay un método en particular que utilizo con bastante frecuencia, get, el cual te permite acceder a una propiedad anidada cómodamente sin preocuparte de comprobar que toda la ruta existe:

$value = get( $object, [ 'some', 'nested', 'properties', 0', 'value' ], '' );Lenguaje del código: PHP (php)

Tú simplemente especificas la ruta y esa función se encarga de ir a buscar el dato comprobando a cada paso que todo está bien. Y si en algún momento falla, devuelve un valor por defecto y a tirar millas.

De hecho, incluso puedes hacer una versión más cómoda en la que la ruta es un único string como este:

$value = get( $object, 'some.nested.properties[0].value', '' );Lenguaje del código: PHP (php)

¿Te animas a implementar esto en PHP y compartes con nosotros tu solución? ¡Me encantaría verla!

Así que, ya sabes, anímate a probar cosas nuevas y no tengas miedo a implementar estas pequeñas funciones auxiliares tú mismo. Disfrutarás el reto y lo pasarás bien, créeme.

Imagen destacada de Chun Kit Soo en Unsplash.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

He leído y acepto la política de privacidad de Nelio Software

Tus datos personales se almacenarán en SiteGround y serán usados por Nelio Software con el único objetivo de publicar tu comentario aquí. Con el envío de este comentario, nos das el consentimiento expreso para ello. Escríbenos para acceder, rectificar, limitar o eliminar tus datos personales.