Modelo de página activa de ingreso de datos
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.