Debug en PHP

Consideraciones

Escribir código es sólo una parte de la programación, y quizá no la más importante. Por otra está el diseño del algoritmo y las tareas de debug que verifiquen el correcto funcionamiento del programa en todos los casos.

En esta parte se centrará este manual. Al margen quedan los errores generados por PHP. Un parse error, un warning o un error fatal tienen en la mayoría de los casos una resolución inmediata: Una comilla mal puesta, una llave sin cerrar...

Para asegurarnos de que éste es nuestro caso, y antes de comenzar a debugear, no debe haber ningún error de PHP (parse errors, warnings o fatal errors...(los notices los dejo a gusto del consumidor)).

Para ello comprobad en vuestro php.ini o haciendo un

 <?php phpinfo(); ?> :
  • que la directiva display_errors se encuentra a On
  • que error_reporting se encuentra como mínimo a E_ALL & ~E_NOTICE (2039) ó E_ALL (2047)

En el servidor final (donde colgamos nuestra página) no es interesante que se muestren este tipo de errores, primero porque en ese punto nuestros scripts deberían estar suficientemente probados, y segundo porque los errores podrían revelar paths u otra información sensible que comprometería la seguridad del servidor. (Para más información consultad el artículo sobre seguridad de Aeoris).

En el servidor del programador (localhost o un server de pruebas) sucede lo contrario. Necesitamos activar esas directivas si no queremos volvernos locos programando a ciegas.

En este documento, salvo que se indique lo contrario, se habla de errores funcionales. Un script tiene un error funcional si a pesar de no generar errores visibles, no se comporta como nosotros esperábamos

Cuando nos libremos de los mensajes de PHP, y nuestro script no se comporte de forma adecuada, es cuando deberemos comenzar nuestras tareas de debug. Principios del debug

Debugear es la reacción natural de un buen programador ante un problema en su script. Es sorprendente ver que los usuarios que llegan al canal con dudas, a veces desesperados por no poder solucionar un error funcional no han realizado el más mínimo debug en el código para intentar arreglarlo.

El código no se arregla sólo

Puedes patalear, llorar y entrar en el canal a dar gritos y esto no cambiará. Cruzándote de brazos y ejecutando el script una y otra vez no se arreglará el problema por arte de magia.

¿Por qué el usuario no debugea? Principalmente es por tozudez. La primera reacción de un mal programador ante un error inesperado es delegar la culpa en otros factores. PHP nunca tiene la culpa de que el script no funcione y tu script probablemente hace exactamente lo que le pides. El problema radica en que lo que le pides no coincide con lo que tú quieres hacer. Así pues, el segundo principio del debug es: El script no funciona porque el programador ha cometido uno o más errores.

con una probabilidad infinitesimal de que nos encontremos con un bug REAL en PHP.

Cuanto antes lo asumas antes corregirás el error.

El programador, cuando relee el script buscando errores suele ser benévolo consigo mismo, y las instrucciones escritas le parecen tan lógicas como quince minutos atrás. En su mente todo funciona bien, todas las variables contienen lo que esperamos, los bucles iteran perfectamente, todas las condiciones se cumplen... Y éste es el principal error. No des NADA por supuesto

El debug es una oportunidad de aprender. Debugeando conocerás como funciona tu script hasta las últimas consecuencias. Aprenderás a enfrentarte a los problemas tú mismo.

Algunos usuarios prefieren, en último caso, borrar todo el script y empezarlo desde 0. Esto es siempre un error, perderás la oportunidad de aprender y estarás condenado a repetir una y otra vez tus mismos fallos.

Otros usuarios prefieren la técnica del ensayo-error, toquetear en el script hasta que funcione. Esto puede estar bien para los monos y otro tipo de simios. Pero tú no lo eres. Piensa bien todos los cambios que realices al script y por qué pones o dejas de poner cada cosa. En definitiva: Usa la cabeza

El debug es el arte de eliminar probabilidades de error. Cuando nos encontramos con un script que falla, y lo miramos con escepticismo, las posibles cosas que pueden producir el fallo se multiplican. Debugear es el arte de ver qué se cumple y qué se deja de cumplir en tu script, acotando el error, reduciendo las posibilidades hasta que sabemos con certeza qué es lo que falla.

En cuanto sabemos lo que falla, su resolución suele ser trivial.

El principal error es no saber qué produce el error

Algunas personas cuando se les manda debugear con un echo o similar en el canal o en el foro, vuelven al mismo y responden "sigue sin ir". Si has comprendido bien los puntos anteriores habras deducido que... El debug no corrige por sí mismo los errores

Tan sólo localiza el error.

Herramientas básicas del debug

En PHP existen dos herramientas que casi todos los que se lanzan a programar en este lenguaje conocen. No se trata de funciones sofisticadas, pero servirán a la perfección para nuestro propósito. echo

Esta construcción del lenguaje nos valdrá para tres cosas:

  • Volcar el contenido de una variable para verificar que tiene asignado el valor deseado:

Para ello es recomendable añadir un texto previo, ya que si la variable está vacía podríamos no darnos cuenta:

Ejemplo:

<?php
echo 'el valor de $variable_a_debugear es '.$variable_a_debugear;
?>
  • Situarnos dentro de la ejecución del script:

Con un echo podremos comprobar si nuestro script ha interpretado una serie de instrucciones que se encuentren dentro de una condición if.

Ejemplo:

<?php
if (!empty($_GET['variable'])) {
    echo 'Se cumple if. la variable no está vacía<br />';
    // instrucciones
}
  • Comprobar las veces que itera un bucle:

Ejemplo:

<?php
for ($i=0;!feof($puntero);$i++) {
    echo 'iteracion numero '.$i.'<br />';
    // resto de instrucciones
}

Mucho más sobre el echo en FAQ del echo y strings

print_r()

Con print_r podremos fácilmente comprobar tanto la estructura como el contenido de cualquier array.

Ejemplo:

<?php
  print_r($_SESSION);
 ?>

Mostraría las variables de sesión que estén disponibles en el script. Por ejemplo:

Array
(
     [usuario] => pepe
     [tiempo] => 1140439106
)

Se ve de forma intuitiva la estructura del array, si posee mas de una dimensión, o si se trata de un array asociativo:

<?php
$array=array("verduras"=>array("alcachofa","puerro","berza"),
    "frutas"=>array("invierno"=>naranja,"verano"=>array("fresa","melocoton","higo")));
print_r($array);
 ?>

Array
(
  [verduras] => Array
    (
        [0] => alcachofa
        [1] => puerro
        [2] => berza
     )

 [frutas] => Array
    (
        [invierno] => naranja
        [verano] => Array
            (
                [0] => fresa
                [1] => melocoton
                [2] => higo
            )

     )

)

Si estamos visualizando un print_r en el navegador, sería deseable escribirlo de esta forma:

<?php
   echo '<pre>'; print_r($array); echo '</pre>';
?>

En caso contrario nos aparecerá en una única línea y no podremos observar bien la estructura del array. Algunos ejemplos prácticos

Los ejemplos que se muestran pueden parecer triviales. Donde realmente se comprueba lo efectivo del debug, es en scripts más complejos, con múltiples archivos, includes... o sea, en los casos reales. Así todo estas pequeñas muestras puede que ayuden a entender los conceptos del debug.

Imaginad que tenemos este sencillo script:

<?php
  if (isset($_POST['enviar'])) {
    if (isset($_POST['usuario'])) {

     header("Location: http://midominio.com/index.php");
   }
}
?>    

<form method="post" action="<?php echo    $_SERVER['PHP_SELF']; ?>">
 <input type="text" name="Usuario">
  <select name="pais">
    <option value="es">España</option>
    <option value="fr">Francia</option>
  </select>
 <input type="submit" name="enviar" value="Enviar">
</form>

En este script se muestra un formulario. La intención es que cuando el usuario lo rellene y lo envíe, nos redirija a otro script php donde validaremos la información o haremos otro tipo de tareas.

Este script no da errores, pero no funcionará. De lo que se trata es de iniciar un proceso metódico que nos diga exactamente cuál es el fallo, si es que no lo ves a simple vista.

Alguien que tuviera este script entraría al canal o un foro y diría "no funciona", o "no me redirige bien". Esta pregunta es siempre errónea: el usuario tiene en cuenta el efecto, pero no la causa. Según los principios del debug ese script SÍ funciona, pero no como tu quieres.

Lo primero es verificar si el programa entra en el if, para descartar que se trate de un error al hacer el header.

(No des NADA por supuesto) Siempre tendemos a tener prejuicios y dar por hecho que los errores se acumulan en las funciones que menos conocemos o que suelen dar más problemas como header() , y no siempre es así.

Procedamos pues a descartar:

<?php
 if (isset($_POST['enviar'])) {
     if (isset($_POST['usuario'])) {

        echo "Se ha enviado un nombre de usuario";

      // Comentamos temporalmente las funciones que puedan alterar el flujo del programa
       // como por ejemplo header()
       //header("Location: http://midominio.com/index.php");
   }
 }

?>

<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
  <input type="text" name="Usuario">
  <select name="pais">
     <option value="es">España</option>
    <option value="fr">Francia</option>
  </select>
  <input type="submit" name="enviar" value="Enviar">
 </form>

Al ejecutar el script y enviar el formulario vemos que el echo no se cumple. Acabamos de descubrir que nuestro error, de momento no tiene nada que ver con header. Ya estamos acotando el error.

Sabemos que ese if no se cumple, ahora bien. ¿Por qué? ¿No llega por POST?. Comprobémoslo.

<?php

// ¿Qué está llegando por POST a nuestro script?

echo '<pre>'; print_r($_POST); echo '</pre>';

if (isset($_POST['enviar'])) {
    if (isset($_POST['usuario'])) {

       echo "Se ha enviado un nombre de usuario";

        // Comentamos temporalmente las funciones que puedan alterar el flujo del programa
       // como por ejemplo header()
       //header("Location: http://midominio.com/index.php");
    }
 }
?>    

<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
  <input type="text" name="Usuario">
  <select name="pais">
    <option value="es">España</option>
    <option value="fr">Francia</option>
  </select>
  <input type="submit" name="enviar" value="Enviar">
</form>

El print_r resultante podría ser el siguiente:

 Array
(
   [Usuario] => pepe
   [pais] => es
   [enviar] => Enviar
)

Si aún no vemos el error, podemos hacer un echo $_POST['usuario']; Si no lo sabíamos de antemano, descubriremos que $_POST['usuario'] y $_POST['Usuario'] son dos cosas distintas, y que hemos de tener mas cuidado a la hora de escribirlas.

Tras corregir "usuario" por "Usuario" veremos como en efecto, el script entra en el if como esperamos.

En caso de no recibir nada del print_r($_POST); aparte de un array vacío, el problema sería otro, como por ejemplo que nos hemos olvidado de ponerle method="post" al formulario.

Imaginad ahora que tenemos el siguiente archivo de texto: usuarios.txt con usuarios y sus passwords

pepe:3a5f9ef84
o'connel:91c18dcd
jeremias:f05dad42c
thessoro:65d74e

En el siguiente script verificamos que el user/password introducido es correcto

<?php

// Supongamos que, de un formulario que tiene como destino este script
// nos llegan los siguientes datos:
// $_POST['usuario']=jeremias
// $_POST['password']=f05dad42c

$array = file("usuarios.txt");
foreach ($array as $linea) {
    list($user,$pass) = explode(":",$linea);
    if ($_POST['usuario']==$user && $_POST['password']==$pass) {
        header("Location: http://mipagina.com/usuario_ok.php");
        exit();
    }
 }
echo "Usuario/password incorrecto";
?>

Este script tendría un comportamiento muy raro. Después de testearlo descubriríamos que sólo funciona si nos logueamos con el user/password de thessoro. ¿Extraño verdad?

<?php

// Supongamos que, de un formulario que tiene como destino este script
// nos llegan los siguientes datos:
// $_POST['usuario']=jeremias
// $_POST['password']=f05dad42c

$array = file("usuarios.txt");

// ¿que diablos pasa con este array?
echo '<pre>'; print_r($array); echo '</pre>';

foreach ($array as $linea) {
    list($user,$pass) = explode(":",$linea);
    if ($_POST['usuario']==$user && $_POST['password']==$pass) {
       // header("Location: http://mipagina.com/usuario_ok.php");
       exit();
    }
 }
echo "Usuario/password incorrecto";
?>    

nos daría el siguiente resultado:

Array
(
   [0] => pepe:3a5f9ef84

   [1] => o'connel:91c18dcd

   [2] => jeremias:f05dad42c

    [3] => thessoro:65d74e
)

En este momento, unos ojos avispados acostumbrados a ver un montón de print_r's dará con el problema.

¿Que diferencia hay entre este print_r y los anteriores?

Pues bien, hay saltos de línea al final de cada elemento, excepto en el de thessoro, que es el último del archivo.

Por eso, la password no se verifica, ya que "f05dad42c\n" no es igual a "f05dad42c" !!.

Si no lo sabíamos de antes, lo habremos aprendido con el debug. file() mete a un array las líneas del fichero ¡incluyendo los saltos de línea!. La solución sería simplemente hacer un trim().

<?php

// Supongamos que, de un formulario que tiene como destino este script
// nos llegan los siguientes datos:
// $_POST['usuario']=jeremias
// $_POST['password']=f05dad42c

$array = file("usuarios.txt");

foreach ($array as $linea) {

    // quitamos los saltos de linea, y espacios de cada linea
   $linea=trim($linea);

   list($user,$pass) = explode(":",$linea);
   if ($_POST['usuario']==$user && $_POST['password']==$pass) {
       header("Location: http://mipagina.com/usuario_ok.php");
       exit();
   }
 }
echo "Usuario/password incorrecto";
?>        

Debugeando un query MySQL

Debugear queries MySQL se escapa un poco de la dinámica anterior, pero he decidido incluir esta sección por dos motivos:

  • Los errores en queries MySQL también pueden ser silenciosos y necesitan debug.

    Warning: supplied argument is not a valid MySQL result resource. A pesar de ser un warning, se debugea de la misma forma.

Cuando mysql_num_rows, mysql_fetch_array o cualquier variante (mysqlfetch*) nos da un error como el anterior solamente puede ser por 3 motivos:

No se ha podido conectar con la base de datos y nuestro mysql_connect tiene la salida anulada: @mysql_connect

En caso contrario, nos habría aparecido un error de conexión previo al warning. Recordemos que los errores de PHP que nos aparezcan han de corregirse en riguroso orden de aparición ya que en ocasiones errores posteriores dependen de los primeros, y corregidos estos el resto desaparecen.

Para evitar esto bastará cambiar nuestro mysql_connect:

<?php

@mysql_connect($host,$user,$pass) or die (mysql_error());

// Nota:
// mysql_connect($host,$user,$pass); también nos valdría. En este caso
// la ejecución del script continuaría, y aparecerían un montón de warnings
// por cada query posterior. En el ejemplo el script detendría su ejecución
// en caso de que no se pudiera conectar a la base de datos.
?>

Nos hemos equivocado al seleccionar la base de datos.

Este error es silencioso a no ser que imprimamos explícitamente el mysql_error. En caso contrario, nos aparecerá directamente el Warning: supplied argument is not a valid MySQL result resource

Para evitar esto pondremos:

<?php

@mysql_connect($host,$user,$pass) or die (mysql_error());
mysql_select_db("nombre_db") or die (mysql_error());

// Si nos equivocamos en el nombre de la db, mysql_error() nos dirá:
// Unknown database: nombre_db
// Y se detendrá la ejecución del script. En caso contrario, no nos
// avisará del error, y lo que fallarán parecerán ser los queries.
?>

El query es erróneo (debug con echo y or die)

Una vez hemos llegado hasta aquí, sabemos que si obtenemos Warning: supplied argument is not a valid MySQL result resource es debido a un query erróneo. Si no somos capaces de ver por qué falla, usaremos este sencillo procedimiento que nos desenmascara el 100% de los queries erróneos: tanto los que provocan un warning como los que no hacen lo que esperamos.

<?php

// Haremos un echo del query, y visualizaremos un posible error con mysql_error()

$link=@mysql_connect($host,$user,$pass) or die (mysql_error());

mysql_select_db("nombre_db") or die (mysql_error());

$query="SELECT campo1,campo2,campo3 FROM tabla WHERE campo1 LIKE '%{$_POST['nombre']}%' OR campo2 = '".$_SESSION['pais']."' ORDER BY campo3 DESC LIMIT 3";

echo 'Query ->'.$query.'<br />';

$result_id=mysql_query($query,$link) or die (mysql_error());

while ($datos=mysql_fetch_assoc($result_id)) {
    echo '<pre>'; print_r($datos); echo '</pre>';
}
?>
  • El echo nos revelará los errores funcionales:

Si alguna variable llega vacía:

Error funcional: Si tenemos "SELECT * FROM tabla WHERE campo='$nombre'" y $nombre no tiene valor, el query sería correcto de todas maneras y MySQL nos devolvería todos los registros donde campo estuviera vacío.

Errores relacionados con slashes:

MySQL tampoco nos daría error, pero lo descubriríamos al hacer echo.

    Si tenemos: "INSERT INTO tabla (campo) VALUES('$nombre')" y $nombre vale "o'connel", veremos el siguiente query:

    "INSERT INTO tabla (campo) VALUES('o'connel')" : No da error, pero no se inserta el registro, la solución sería un addslashes.

    "INSERT INTO tabla (campo) VALUES('o\'connel')". Este query se comportaría como esperamos
    .
  • El or die(mysql_error()); nos revelará errores sintácticos:

    Nombre incorrecto de campos

    Nombre incorrecto de tablas

    Orden incorrecto en cláusulas

cláusula LIMIT antes de WHERE, ORDER BY después de LIMIT... etc

    ...

Una última nota: mysql_num_rows y todo el repertorio de funciones mysqlfetch* no dan error si el query no devuelve resultados. Un query que no devuelve resultados es perfectamente correcto.

Sin embargo mysql_result espera un resultado, si no lo hay, dará un warning. Para solucionarlo, sólo deberíamos hacer un mysql_result en caso de que un mysql_num_rows anterior diera > 1.

Resumen

Este artículo es una sencilla guía con ejemplos que explica cómo depurar nuestro código PHP para localizar fallos en su funcionamiento.

Índice

  1. Debug

Otros artículos