Cuando apenas comienzas a involucrarte en las artes oscuras de la programación, normalmente te conformas con que las cosas funcionen. Pero conforme vas ganando experiencia y aprendes de tus errores, mejoras como programador y tu código es la evidencia... bueno, no en todos los casos.

Pero, pero... eso es muy idealista. ¿Y los deadlines qué? Mejor código funcional que código perfecto que nunca ve la luz del día.

Si bien es cierto que no hay que esperar a dominar un lenguaje para crear aplicaciones con él, debo decir que esto no justifica el ser un desarrollador mediocre. No se trata de ser una documentación viviente y saber de memoria las funciones y el órden de sus parámetros. Aún así, hay que invertir tiempo aprendiendo los fundamentos.

¿A qué viene todo esto? ¡Pensé que nunca lo preguntarían! No, en serio, pensé que nunca lo preguntarían...


La siguiente imagen es un ejemplo perfecto de lo que hablo. ¿Lo mejor de todo? Es un fragmento de código real, que existe en una aplicación PHP real.

¿Qué hay de malo en este código? Bueno, empecemos por el hecho de que es bastante confuso para lograr algo muy trivial: buscar si una categoría ($category) existe dentro de un array llamado $catComponents. Nada grave a primera vista; algo que se puede simplificar utilizando alguna función para arrays como array_key_exists().

Luego, nada como un poco de redundancia para darle sabor a nuestro código. La instrucción else y continue son innecesarias ya que no hay nada más además de la condición if. Si esta no se cumple, el código simplemente continuará con la siguiente iteración del ciclo.

Ahora vayamos un poco hacia arriba para analizar la condición if. Vemos que hay un preg_match() que sirve para comparar una cadena de texto utilizando expresiones regulares, pero se puede apreciar que no hay una expresión regular como tal, simplemente se compara que el valor de $component sea igual al de $category. Para una comparación como esta bastaría con utilizar alguna función como strcmp().

Algo que también es importante mencionar es que la función preg_match() es una de tantas funciones de PHP que —para bien o mal— devuelven un tipo de dato según sea el caso: un valor entero 1 si existe alguna coincidencia; un valor entero 0 si no existe coincidencia alguna y false si ocurre un error (por ejemplo si los caracteres para delimitar la expresión regular también forman parte de ella y no han sido escapados).

En el código de la imagen se comete un error muy común al utilizar este tipo de funciones: olvidar o ignorar que PHP es un lenguaje dinámico, por lo que realizará una conversión implícita. En este caso se utiliza dentro de una condición if, la cual espera expresiones tipo boolean, pero de no encontrar valores de este tipo los convertirá a su equivalente. Si preg_match() encuentra una coincidencia devolverá 1, el cual será convertido a true; si no hay coincidencias devolverá 0, el cual será convertido a false. Entonces, ¿cuál es el problema? Si ocurre un error la función regresará false, lo cual puede interpretarse como que simplemente no hubo alguna coincidencia.

Pongamos un sencillo ejemplo. En el siguiente fragmento de código comparamos si el valor de $currentPage coincide con el valor about. Los valores son los mismos (hay que recordar que cuando usamos expresiones regulares, los caracteres al inicio y final no forman parte del valor a comparar sino que son los delimitadores de la expresión regular), así que se imprimirá el mensaje existen coincidencias.

// imprime: existen coincidencias
$currentPage = 'about';

if(preg_match('/about/', $currentPage)) {
    echo 'existen coincidencias';
} else {
    echo 'no existen coincidencias';
}

Ahora modifiquemos ligeramente el código y agreguemos una diagonal al principio del valor a comparar, tanto en $currentPage como en preg_match() para que siga existiendo una coincidencia:

$currentPage = '/about';

if(preg_match('//about/', $currentPage)) {
    echo 'existen coincidencias';
} else {
    echo 'no existen coincidencias';
}

Pero si ejecutamos el código se imprime el mensaje no existen coincidencias.

Lo que sucede es que estamos incluyendo la diagonal como delimitador de la expresión regular y como parte de ella. Esto genera un error y hace que preg_match() devuelva false por lo que se ejecuta la instrucción else, la cual también se ejecuta cuando se devuelve 0 (el cual es convertido implícitamente a false). Cambiemos el código para que haga comparaciones estrictas e imprima el mensaje correcto.

$currentPage = '/about';

$matches = preg_match('//about/', $currentPage);

if($matches === 1) {
    echo 'existen coincidencias';
} elseif ($matches === 0) {
    echo 'no existen coincidencias';
} else {
    echo 'ocurrió un error';
}

Ahora el código imprimirá el mensaje ocurrió un error.

WTF, ¿todo un rant por un bloque de código?

Todavía no llego a mi parte favorita...

¡Aún hay más!

Quizá la parte que más llamó mi atención fue el uso del operador módulo % en la condición if. Me pregunto para que lo utiliza. Si la condición se cumple, entonces usa una variable llamada $explodeOver y le asigna un elemento del array; en este caso, el elemento que coincide con el índice actual de la iteración más uno.

Tiene que ser una broma... Al parecer usa los índices númericos 0, 2, 4, ... (números divisibles entre 2) para almacenar los "índices" del array, y los índices numéricos 1, 3, 5, ... (números no divisibles entre 2) para almacenar los valores. A esto me refiero cuando digo que debemos conocer por lo menos los fundamentos de un lenguaje. Entre algunas otras razones, para evitar crear código innecesariamente complejo.

El código anterior pudiera haber utilizado el array de categorías de la siguiente manera:

$catComponents = array(
    'article' => 'blog/articles',
    'tutorial' => 'techblog/tutorials'
);

Pero al ignorar el uso de los arrays asociativos, tenemos algo así:

$catComponents = array(
    'article',
    'blog/articles',
    'tutorial',
    'techblog/tutorials'
);

Usando un array asociativo es obvia la relación índice - valor; en el segundo caso hay que saber cómo funciona el código para entender dicha relación entre los valores.

Una forma simplificada de lograr lo anterior sería la siguiente. Por ejemplo, definir una función que busque en el array en base al índice y devuelva el valor correspondiente:

function findCategoryURI($category, $categories) {

    if(array_key_exists($category, $categories)){
        return $categories[$category];
    }

    return null;
}

Y un ejemplo de uso sería algo como lo siguiente:

$catComponents = array(
    'article' => 'blog/articles',
    'tutorial' => 'techblog/tutorials'
);

$categoryURI = findCategoryURI('article', $catComponents);

if(is_null($categoryURI)) {
    echo 'categoria no existente';
} else {
    echo "la URI de la categoria es $categoryURI";
}

El objetivo es el mismo: buscar un elemento en un array, pero con algunas diferencias. Al usar un array asociativo no tenemos que recurrir a técnicas de vudú como el uso del operador módulo; tampoco es necesario un ciclo ni condiciones, pues simplemente usamos una función para arrays disponible en el lenguaje: array_key_exists(). El resultado: código más simple y claro.

Y hablando de vudú, tengo otro ejemplo de código muy interesante...


Mejor usa un left join, patiño

A diferencia del código anterior, no tengo una captura de pantalla del siguiente ofensor. Lo bueno (?) es que recuerdo lo suficiente para recrearlo en PHP (el código estaba en Perl, que es casi lo mismo, pero no).

Se trataba de una aplicación para hacer exámenes o quizzes.

Utilizando una estructura simplificada de tablas, supondremos lo siguiente:

  • Hay una tabla de quizzes
  • Hay una tabla de preguntas. Cada pregunta sólo puede ser asociada con un quiz
  • Hay una tabla de respuestas. Las respuestas son abiertas. Cada respuesta está asociada con una pregunta. Hay respuestas que pueden estar en blanco.

Hay más información que debería ser incluída, como otra tabla donde almacenar las respuestas de un usuario para cada pregunta, pero esto lo dejaremos fuera pues con las tablas siguientes espero poder explicar el problema del código.

Tabla de quizzes

id title
4323 Quiz de prueba

Tabla de preguntas

id quiz_id title
4533 4323 ejemplo de pregunta
4534 4323 otro ejemplo de pregunta

Tabla de respuestas

id question_id answer
467 4533 respuesta para la pregunta 4533
524 3211 respuesta para pregunta XXXX
348 1125 respuesta para la pregunta XXXX

Las tablas muestran un quiz con dos preguntas asociadas. Y en la tabla de respuestas solo hay una respuesta asociada a este quiz: la que tiene el id 467, correspondiente a la pregunta 4533. Los registros con id 524 y 348 pertenecen a preguntas de otros quizzes, y la pregunta con id 4534 no tiene respuesta.

La aplicación en cuestión tenía una sección para mostrar un quiz que se deseara responder. Primero tomaba el id del quiz y en base a esto buscaba las preguntas y respuestas asociadas. No voy a mostrar código de conexión a base de datos ni nada por el estilo, únicamente mostraré arrays que simulen datos obtenidos de una base de datos.

Entonces, el código encargado de esta tarea hacía algo como lo siguiente:

<?php
// este array se obtenía realizando una consulta a la tabla 
// de quizzes, en base al quiz_id
$quizQuestions = [

    [
        'id'      => 4533,
        'quiz_id' => 4323,
        'title'   => 'ejemplo de pregunta'
    ],

    [
        'id'      => 4534,
        'quiz_id' => 4323,
        'title'   => 'otro ejemplo de pregunta'
    ]

];

// este array era una consulta que obtenía todos los registros
// de la tabla de respuestas. Sí, todos los registros
$answers = [

    [
        'id'          => 467,
        'question_id' => 4533,
        'answer'       => 'respuesta para la pregunta 4533'
    ],

    [
        'id'          => 524,
        'question_id' => 3211,
        'answer'       => 'respuesta para pregunta XXXX'
    ],

    [
        'id'          => 348,
        'question_id' => 1125,
        'answer'       => 'respuesta para pregunta XXXX'
    ]

];

// Se imprimen las preguntas y respuestas del quizz
// En la aplicación esto era un formulario con campos de texto
foreach($quizQuestions as $question) {
    echo $question['title'] . ' - respuesta: ';

    foreach($answers as $answer) {
        if($answer['question_id'] == $question['id']) {
            echo $answer['answer'];
        }
    }

    echo PHP_EOL;
}

Veamos. El código primero hace una consulta a la tabla de preguntas y obtiene aquellas correspondientes al quiz que desea mostrar; el resultado de la consulta se almacena en un array. Nada inusual, pero es a partir de aquí cuando todo comienza a desmoronarse.

Luego tenemos otro array que almacena todas las respuestas habidas y por haber. Cero discriminación, ¡todas las respuestas son bienvenidas!

Luego tenemos un ciclo foreach que recorre el array de preguntas del quiz y muestra el título de la pregunta. Después... qué tenemos aquí, ¡un ciclo anidado! Este segundo ciclo, por cada pregunta, recorre TODO el array de respuestas (que ya de por si contiene TODOS los registros de la tabla de respuestas) comparando el campo id de la pregunta con el índice id_question de la respuesta para determinar si la pregunta de la iteración actual tiene una respuesta almacenada, y de ser así, muestra la respuesta.

A veces creo que alguien le dijo a la persona o personas encargadas de este código:

Necesito que muestren las preguntas y respuestas de un quiz, pero quiero que sea complejo e ineficiente. Sean creativos, den rienda suelta a su imaginación. ¡Azoten las teclas como si no hubiera un mañana, hijos!

Creo que jamás sabré las razones verdaderas, pero mi teoría es la siguiente. Nuestros amigos se dieron cuenta que al relacionar dos tablas con el tipo de join más común del mundo (inner join) se muestran los registros de ambas únicamente si hay registros correspondientes. Esto les presentó un problema: cuando existieran preguntas sin responder, no existiría un registro en la tabla de respuestas para dicha pregunta. ¿Resultado? La información de la pregunta no se mostraría.

Había que encontrar la manera de mostrar el título de cada pregunta y su respuesta (en caso de haberla). Encontraron la manera, aunque no la mejor.

Si en lugar de eso se hubiera utilizado un LEFT JOIN, más que simplificar el código, se hubiera logrado algo más eficiente, que no tuviera que obtener la tabla completa de respuestas y recorrer dicho array por cada pregunta.

El siguiente código muestra un ejemplo de cómo puede haber sido una alternativa. Las preguntas sin respuesta correspondiente devuelven un valor null los campos seleccionados, por lo que simplemente hay que verificar si el valor es distinto a null para desplegar la respuesta.

/*
Internamente, la consulta pudo haber sido algo como lo siguiente:

SELECT q.id as question_id, q.title as question, a.answer as answer
  FROM questions q LEFT JOIN answers a
    ON q.id = a.question_id
 WHERE q.quiz_id = 4323;

 Lo que arrojaría los resultados como los de $questionData
 */

$questionData = [

    [
        'question_id' => 4533,
        'question'    => 'ejemplo de pregunta',
        'answer'      => 'respuesta para la pregunta 4533'
    ],

    [
        'question_id' => 4534,
        'question'    => 'otro ejemplo de pregunta',
        'answer'      => null
    ]

];

foreach($questionData as $data) {

    echo $data['question'];

    if(! is_null($data['answer'])) {
        echo  ' - respuesta: ' . $data['answer'];
    }

    echo PHP_EOL;

}

El resultado sería algo así:

ejemplo de pregunta - respuesta: respuesta para la pregunta 4533
otro ejemplo de pregunta

Moraleja: nunca se esfuercen.

Ya, en serio. Si quiero poner una especie de conclusión es la siguiente:

No hay que paralizarse buscando o esperando a tener la solución perfecta para un problema. Pero siempre hay que buscar la forma de mejorar cómo hacemos las cosas. Conocer las herramientas que te ofrece un lenguaje permite resolver problemas de manera más efectiva y en muchos casos, en una fracción del tiempo.

Haciendo referencia a una historia popular que tal vez ya conozcan: el mérito no está en ajustar un tornillo sino saber cuál ajustar.