Para aplicaciones de cierta envergadura, el uso de ‘frameworks‘ de desarrollo o motores de ‘templates‘ es altamente recomendado, pero para muchas aplicaciones pequeñas o para las páginas de mantenimiento de tablas, que pueden no ser particularmente importantes, un modelo más simple es más que suficiente. Comentaré un modelo simple, cuyo código se puede ver en esta página. y el resultado en esta otra. El ejemplo no funciona del todo pues para ello requeriría una base de datos real y no la tiene. Para seguir este comentario haré referencia a los números de línea en la página referida (indicando el número de línea entre corchetes) por ello es conveniente tener ambas ventanas abiertas una al lado de la otra.

Mucho del código que muestro es de uso general y por lo tanto apropiado para ubicar en uno o más archivos que se incluirán en la página mediante la instrucción include. No lo estoy haciendo en este ejemplo para que el lector pueda ver todo en un solo vistazo sin andar saltando de un archivo al otro. De hecho el único include que muestro [2] es a BuildSql.php cuya funcionalidad comento en otro artículo.

Por ejemplo, la página hace uso de una conexión a una base de datos que se ha abierto en algún lugar que no se muestra. También es frecuente que la parte variable de la aplicación esté rodeada de un marco con información general del sitio, logotipo de la empresa, el menú de navegación y otras cosas que son comunes a todas las páginas del sitio. Todo esto convendrá que esté en archivos de include.

Las funciones de validación de datos también son excelentes candidatas a residir en archivos de include. En el ejemplo, he incorporado su código directamente a la página para mostrar cómo pueden hacerse. Lo mejor es tener una biblioteca de funciones de validación en un include externo, esta página sólo cumple un fin didáctico.

La página permite editar una simple tabla compuesta de 4 campos, IdDatos, un entero que es la clave principal de la tabla, Nombre, un varchar, FechaNacimiento, una fecha y NumDependientes, un entero.

A esta página se puede llegar de dos formas. Si en el URL se incluye un argumento id_datos, es que se desea ver y/o editar el registro cuya clave se indica. Si no se incluye un id_datos o este es cero, es que se desea agregar un nuevo registro.

En primer lugar, [6-15] se lee y valida este argumento. Esta técnica se utiliza con variaciones con el resto de los datos. Salvo que los espacios en blanco en los extremos fueran importantes, lo primero [6] será eliminarlos con trim(). Luego [7], se verifica la longitud de lo que queda con strlen(), lo cual permite controlar datos que sean obligatorios y mostrar un mensaje, si correspondiere. En este caso, si no hubiera un argumento id_datos, simplemente se lo supone cero [14]. Dado que el campo IdDatos es un autonumérico y que estos comienzan en 1, 0 es una obvia indicación de que no nos referimos a un registro existente sino uno nuevo que todavía no tiene IdDatos.

Usamos expresiones regulares para validar los datos. Funciones como intval() o floatval() interpretan todo lo que encuentran como numérico hasta el primer carácter que no lo sea. Por ello, no son suficientes para detectar que ’12 de octubre’ es un texto, no un número, pues cualquiera de ellas lee y convierte el 12 e ignora el resto. Otro peligro de intval() es que si la cadena a evaluar comienza en 0, la interpretará como octal, dando resultados inesperados. Es importante [9] agregar el segundo parámetro que es la base de conversión para salvar este peligro.

En caso de éxito, este segmento devuelve en $id_datos el valor convertido a entero [9], un 0 si no estuviera presente [14] o sale por error [11] si fuera inválido. A diferencia de los otros datos en que se muestra un mensaje de error al usuario, este dato sólo puede venir a través de enlaces desde otras páginas por lo tanto no es un error que el usuario pudiera salvar. El error del que hablamos no es simplemente un número de registro que pudiera haber sido borrado (que es salvable) sino un argumento id_datos que contenga caracteres no-numéricos, lo que señala un posible intento de jugar con el URL para romper la aplicación, lo cual no es aceptable y conviene impedir que el operador pueda continuar más allá.

En [18] preguntamos si hay un argumento 'submit' con valor 'Aceptar'. Esto es indicio que a esta página se llegó al pulsar el botón Aceptar al pie del formulario. En este caso, leemos también el resto de las variables, comenzando por Nombre.

La opción magic_quotes_gpc del php.ini, que está en on por defecto, agrega la barra invertida por delante de cualquier apóstrofe o comilla, por lo que lo primero [20] es eliminar los blancos en los extremos y sacar estas barras con la función stripslashes(). La razón por la que PHP agrega estas barras es salvar a programadores inexpertos de pasar datos sin validar a la base de datos y quedar expuestos a lo que se denomina ‘SQL injection’ o dejar comillas desapareadas en las instrucciones SQL. Aquí no sólo estamos validando los datos, como corresponde, sino que también la función BuildSql() comentada anteriormente salva las comillas y apóstrofes antes de grabar.

En [21] controlamos que no esté vacía y en [22] controlamos que contenga sólo caracteres aceptables, incluidos letras acentuadas, apóstrofes (D´Elía, O´Higgins) o guiones. Esta lista se podrá ampliar o limitar a voluntad. Dado que el valor es de por si una cadena de caracteres, si valida, no hay nada más que hacer. Si no valida, entonces en [25] preparamos el mensaje de error. Otro tanto ocurre en [28] en el caso de que el campo hubiera estado en blanco.

Los errores no los mostraremos en el momento por dos razones. Todavía no hemos enviado el encabezamiento de la página al navegador, por lo que los mensajes quedarían fuera de contexto y no enviaremos estos encabezados hasta más adelante [71] por razones que ya comentaremos. Por otro lado, este esquema nos permitirá mostrar los errores junto al campo en error, lo cual será más útil al usuario.

Para los errores, tenemos reservada una variable $errores, que inicializamos en [4] con un array en blanco. Los errores de cada campo los agregaremos a este array usando el nombre del campo como clave, según se ve en [25] y [28]. Adicionalmente, previendo la posibilidad de que hubiera más de un error para un mismo campo, agregamos el par de corchetes vacíos para que el mensaje se agregue sobre una posible lista de errores para ese mismo campo. Aunque en este ejemplo no se usa, reservamos el elemento $errores[null] para mensajes de error genéricos que no estén asociados a ningún campo.

En el caso de la fecha [31-40], usamos la misma función preg_match() no sólo para validar sino también para separar las partes (día/mes/año) permitiendo usar indistintamente barras o guiones para separar las partes. Como ya se comentara, en un caso real, todas estas funciones de validación y conversión deberían estar en un include y esta en particular, como también las funciones de validación y conversión de importes monetarios deberán adecuarse al ‘locale’, según comentara en un artículo previo. En el caso de las fechas, la función nl_langinfo(D_T_FMT) nos dará una cadena de la que podemos deducir el orden de las partes (dd/mm/aaaa, mm/dd/aaaa o aaaa/mm/dd) y en el caso de importes monetarios, la función localeconv() nos permitirá también armar la expresión regular correcta.

Salvo $id_datos, el resto de los datos los hemos puesto como obligatorios y mostramos mensajes de error al efecto. En caso de no serlo, basta cambiar la generación del mensaje de error por una asignación a la variable interna el valor predeterminado para el campo o asegurarse de dejarla en blanco, cero o null, según se quiera grabar en la base de datos. Yo soy partidario del uso de null en la base de datos para indicar la ausencia de información, pero como al mostrar una variable que contiene null ocasionalmente se puede ver el texto 'null', la función BuildSql() provee el modificador n (por ejemplo ?ns en lugar de simplemente ?s) para grabar los ceros o cadenas vacías como null en la base de datos.

Es un buen momento para resaltar algo importante. Mi criterio es que todos los datos, además de validarse, deben convertirse al formato nativo apenas se reciben y sólo deben formatearse a la salida. La fecha es el caso en que con más frecuencia veo tropezarse a los novatos. Muchas veces, reciben la fecha del campo del formulario y así como está, la graban en la base de datos en un campo de texto. Al poco tiempo llegan a la lista de correos de PHP preguntando cómo pueden hacer para ordenar los registros por fechas o seleccionar los registros del último mes. Para ese entonces es tarde. SQL dispone de una enorme variedad de funciones para operar y seleccionar registros sobre fechas y aunque PHP es un poco mezquino, hay muchas bibliotecas de funciones para salvar esta carencia de funciones para manipular fechas. Ninguna de estas funciones puede operar sobre una fecha expresada como una cadena de caracteres, con variedad de separadores, con ceros para completar los días y meses a dos dígitos o no, con los años en 2 ó 4 dígitos. En resumen, el dato carece de valor semántico que permita operar sobre él. Es la misma razón por la que a las cadenas de caracteres les aplico stripslashes() y a los importes los convierto a float.

Reitero: Los datos siempre se deben validar y convertir al formato nativo apenas se reciben y se deben formatear recién al enviarlos fuera, ya sea al navegador o a la base de datos. En el caso de la base de datos, la función BuildSql() se encarga de esto.

Volviendo al modelo, en la línea [53] controlamos que aún no haya errores y si efectivamente no los hay, procedemos a grabar la información. En primer lugar vemos si $id_datos está o no en cero. Si $id_datos es cero es un alta de registro, si es distinto de cero, es una actualización. MySql dispone de la instrucción REPLACE que hace irrelevante la diferencia, pero esa instrucción no es estándar de SQL por lo cual mejor evitarla.

En [55] utilizo la función BuildSql() para armar la instrucción en $sql, luego procedo a ejecutarla sobre la base de datos [57]. En producción, acostumbro a eliminar el paso intermedio de la variable $sql, simplemente hago $mysql_query(BuildSql( ....)), pero en las primeras etapas conviene disponer el texto de la instrucción en $sql pues si hubiera error, es más fácil para ver qué ocurrió.

Es IMPRESCINDIBLE controlar el resultado de mysql_query() tras su ejecución. Un buen número de mensajes en la lista de PHP se ahorrarían si los programadores inexpertos o descuidados adoptaran esta norma. Hasta escribí un artículo al respecto, lo llamo Programación Balística, escribes el programa y lo disparas, esperando que acierte en el blanco pero, al igual que un proyectil balístico, una vez que abandona el cañón, ya no tienes ningún control sobre el mismo. Luego de días de arrancarte los pelos, consultas a la lista de PHP para que te solucionen el problema que tu podrías haber detectado desde el inicio.

El uso del operador or en este caso es bastante singular. Es un viejo truco pero, atención, no funciona en todos los lenguajes, en Visual Basic no funciona y en algunas versiones de Pascal tampoco. El caso es que el operador or devuelve verdadero si cualquiera de sus operandos es verdadero, por lo tanto, si el primer operando, en este caso, la función mysql_query() devuelve un valor no-falso, el intérprete no se molesta siquiera en evaluar el segundo operando, en este caso la función die(), pero, si el primer operando da falso, que en el caso de mysql_query() indica un error, entonces el operador or se ve forzado a evaluar el segundo operando, en este caso la función die(). Esta función, en realidad, no devuelve ningún valor, de hecho, no retorna pues termina la ejecución del script, por lo tanto el operador or no podrá devolver nada, pero para ese entonces no importa. El operador or está, en este caso, actuando más como una estructura de control que como un operador, pues lo que importa es cómo afecta la secuencia de ejecución de las instrucciones, en este caso, si el die() se ejecuta o no. Lo mismo se podría hacer con un if. La ventaja que le veo a esta sintaxis es que el or die() queda como un condimento al final del mysql_query() que es lo fundamental. Si encierro el mysql_query() dentro del if, el if parece tomar más importancia que el mysql_query() y me distrae de la secuencia normal de ejecución del programa para llamarme la atención sobre la recuperación de un eventual error.

Es en esta llamada a la función die() donde la variable $sql es de gran utilidad pues la función mysql_error() nos indica el tipo de error, pero no nos muestra la instrucción que ha generado el error: para eso mostramos $sql. Nuevamente, en este caso usamos la función die() en lugar de mostrar un error al usuario pues estos errores sólo ocurren en dos circunstancias. La primera es cuando se está en desarrollo y se ha cometido un error de sintaxis en la instrucción SQL. Lo mejor es corregir el error antes de seguir adelante con las pruebas. La función BuildSql() salva la mayoría de los errores que potencialmente se podrían producir por datos inválidos, que son sobre los cuales el usuario podría tener influencia llegada a la etapa de producción. Por esto es también que llegado a la etapa de producción, el paso intermedio de guardar la instrucción en $sql se puede eliminar. La segunda circunstancia, ya en producción, es cuando se cae la base de datos, en cuyo caso de poco sirve querer continuar.

En [58] uso la función header() para hacer que el intérprete envíe al navegador un encabezado de tipo Location, lo cual produce una redirección. Este es el truco para evitar insertar múltiples registros cuando el operador pulsa F5 repetidas veces o hace clic sobre el botón de refrescar. Esta técnica la he comentado en otro artículo, así que no la desarrollaré aquí. Dado que no se pueden enviar headers al navegador una vez que se ha comenzado a enviar texto, hasta ahora nos hemos abstenido de hacerlo. Esta es una de las razones para no haber mostrado los mensajes de error que hubieramos encontrado.

Baste decir que en este caso agrego al URL de esta misma página (que genero dinámicamente para hacerla portable) dos argumentos, confirma recibe un entero distinto de cero que simplemente indica el texto del mensaje de error a mostrar. El segundo argumento, $id_datos, es la referencia al registro actualizado, para poder seguir operando sobre él. El número y tipo de estos argumentos es arbitrario, pero es necesario que haya al menos un argumento (en este caso confirma) que permita diferenciar un URL de confirmación de uno de consulta o de modificación (argumento submit). Convendrá agregar los suficientes argumentos para proveer al usuario de un mensaje significativo o, alternativamente, si ya se estuvieran usando sesiones, se puede guardar la información en variables de sesión en $_SESSION.

En el caso de ser una inserción de nuevo registro, hacemos lo mismo [60-64] salvo que en [63] leemos el valor de la clave autonumérica generada, para usar en el mensaje de confirmación. En ambos casos, en [66] salimos del script. Las funciones exit() y die() son sinónimos, pero yo prefiero usar die() para las salidas catastróficas y exit() para las intencionales.

En [71], finalmente, comenzamos con el contenido de la página. La declaración de tipo de documento es conveniente pues permite validar el documento con HTML Tidy y asegura un comprotamiento más predecible en el navegador. Para ser concreto, el Internet Explorer 7 tiene dos modos de funcionamiento, uno en que reproduce algunas incompatibilidades de versiones pasadas (IE6 y previas) y otro en que se ajusta más a la norma, y esto lo decide en función del DOCTYPE.

La declaración en [74] también es importante para permitir el uso de letras con acentos o eñie sin tener que recurrir a cosas como á o ñ, lo que hace que la parte estática del texto sea más fácil de leer y, por ende, menos pasible de contener errores.

Si bien en las líneas [77-95] he incluído la declaración de estilos que uso en la página, lo recomendable es guardar estos estilos en una página de estilos y referenciarla con un link como el que se muestra (<!–comentado–>) en [76]. De entre todos los estilos, el que quiero resaltar es el uso del atributo float:left para la etiqueta label. Esto permite armar el formulario con los rótulos junto a los campos, sin necesidad de usar tablas, que era la técnica habitual para alinear los campos con sus leyendas.

En [101-108] controlo el argumento confirma y si está presente, muestro el mensaje de confirmación correspondiente. En este caso, la única acción es mostrar uno u otro mensaje pero en un caso real podría, por ejemplo, volver a cargar las variables con sus valores predeterminados pues, en este caso, cuando se da de alta un registro, se vuelve a mostrar el registro con los valores recién ingresados cuando que lo más práctico podría ser volver a ofrecer el formulario en blanco para ingresar nuevos datos.

En [110-115] controlo si hay errores globales, o sea, si hay mensajes cargados bajo la clave null en $errores. Si los hay, los muestro individualmente. Una vez terminado, los borro del array. Esto es para el beneficio de la instrucción siguiente en [116] pues si luego de borrados los errores generales aún quedaran errores, es que son errores de los campos. En ese caso, muestro una ayuda indicando cómo reconocer los errores. De hecho, esta parte sí funciona en el ejemplo sin base de datos, pues como si hay errores no intenta grabar nada, la ausencia de la base de datos no molesta. Con sólo hacer clic sobre Aceptar se verá cómo muestra los errores indicando que los campos son obligatorios.

En [124-134] en el caso de que el registro existiera ($id_datos distinto de cero) y que no hubiera errores, leo el registro de la base de datos y cargo las variables con sus valores. En el caso de la fecha, uso la función ReadSqlDate() que se encuentra en BuildSql.php.

En [132] muestro un mensaje de error indicando que el registro pedido no existe. Este caso es distinto del caso en [11] pues este sí es un error en que el usuario puede hacer algo al respecto. El primero lo más probable es que fuera un intento de intrusión, este, en cambio, es posible que sea a consecuencia de pedir un registro que fue borrado. Quizás el usuario lo tenía guardado en su lista de favoritos, quizás había pedido un listado y lo tuvo en su pantalla un tiempo hasta que se decidió a hacer clic en alguno de sus enlaces y mientras tanto otro usuario le borró el registro. En cualquier caso el usuario puede hacer algo con la información.

En [136-159] muestro el formulario de ingreso de datos. Noten el uso de las etiquetas <fieldset>, <legend> y <label> que son relativamente nuevas, aunque hace ya un tiempo que son soportadas por los navegadores. Si se es conservador, se puede preferir usar la técnica de usar una tabla para alinear los campos con sus leyendas.

Para el formulario en [138] uso el método get. En la práctica lo más conveniente y seguro es usar post. El get es bueno en desarrollo pues me permite ver en la barra de direcciones del navegador qué datos se han transmitido y hasta me permite jugar con ellos directamente en la barra de direcciones para ver cómo responde la aplicación. Sin embargo, el get expone información innecesariamente, lo cual puede hacer que un usuario pícaro pueda querer jugar con esos datos. Al decir un usuario pícaro tampoco me refiero a un hacker, pues este, con ver el fuente también podría intentar algo, simplemente me refiero a un usuario que quiera ver qué pasa si hace esto o aquello sin realmente saber. El get también tiene limitación en el largo de los datos a transmitir, largo que es desconocido, pues cada navegador se comporta distinto, incluso entre versiones de la misma marca, y si se excede este largo no da error, simplemente se trunca la información. El post, por el contrario, no tiene límite de longitud.

Por esta misma razón he usado $_REQUEST en lugar de $_GET o $_POST. Al usar $_REQUEST, si cambio el método de get a post, la aplicación sigue funcionando. Una vez hecha la transición de get a post cuando la aplicación pase de desarrollo a producción, convendrá cambiar los $_REQUEST por $_POST. Nótese, sin embargo, que en el caso de $id_datos, es necesario usar $_REQUEST dado que el argumento puede ser parte de un URL en un enlace que viene de una página de listado o puede venir del submit del formulario, así que debe ser posible leerlo en ambos casos.

En [139] incluyo un campo de tipo hidden para guardar el valor de la clave del registro, ya fuera cero, para indicar que se va a hacer un alta o el valor concreto de la clave primaria del registro que se va a modificar.

En todos los campos, cargo el atributo value con el valor de la variable que corresponde. En [142] al valor de $nombre le aplico la función htmlentities() para asegurarme que cualquier caracter especial que pudiera contener este campo no generará HTML inválido. En este caso no sería necesario pues por la validación que hiciera en [22] y la declaración del charset a usar en [74], no deberían presentarse caracteres especiales.

Tras cada campo <input> llamo a la función mostrar_error(), que se muestra en [164-173]. Lo que hace es verificar si el array $errores contiene valores bajo la clave que se le indica y que usualmente corresponde al nombre del campo, y si los hubiera, muestra un icono de alerta y carga el mensaje como tooltip para que el usuario lo pueda ver con sólo dejar el cursor un momento sobre el símbolo. Esto permite mostrar la ubicación de los errores y dar un mensaje de error significativo sin estropear el diseño de la página.

Aunque así como aparece el ejemplo parece muy largo, para una aplicación simple, hay que notar que buena parte está ocupada por la validación de datos que, en la práctica, se haría con simples llamadas a funciones en algún archivo include. El estilo debería darse en una hoja de estilo, no dentro de la misma página, estilo que sería compartido por todas las páginas del sitio, asegurándose aspecto consistente. La función mostrar_error() obviamente debería ser parte de las funciones comunes al sitio. Nótese también la regularidad en el armado de los campos del formulario. Todos los campos se podrían generar con una llamada por cada campo a una función a la que se le indicara el nombre del campo, el valor y el tipo de dato.

Toda la validación hecha en la primera parte debería ser, en realidad, la segunda validación. La primera validación debería hacerse mediante JavaScript en el mismo navegador. Ambas son importantes y necesarias. La primera, en JavaScript en el cliente, le provee al usuario un feedback inmediato, sin tener que esperar la respuesta desde el servidor y sin ocupar recursos de este. La segunda, en el servidor, es necesaria pues algunos usuarios inhabilitan el JavaScript en sus navegadores por lo que no puede suponerse que los datos que llegan estén validados. La validación en el cliente es por una cuestión de fluidez de la aplicación, la segunda es imprescindible por seguridad.