Cómo evitar duplicar registros al hacer Refrescar (F5) en el navegador
Una pregunta frecuente es cómo evitar que cuando un usuario a hecho una operación que modifica la base de datos, se pueda evitar que esta operación se repita si el usuario, inseguro de su resultado, pide un refresco de la página o, después de haber navegado a otros lugares, pase accidentalmente por esta página al retroceder.
Debemos primero comprender dónde está el problema. El navegador mantiene una
lista de los lugares visitados en su historial. Cuando se pide un refresco, el
navegador va a la dirección más reciente de esta lista, cuando se vuelve
atrás, se va retrocediendo en las direcciones de esta lista. El valor
almacenado no sólo es la dirección de la página sino también todos los datos
que se hubieran enviado, ya fuera por
GET o POST, por lo tanto, cuando la operación se
repite, se repite en su totalidad, con todos sus datos y todas sus
consecuencias.
Desde PHP no se puede hacer mucho por controlar esta lista una vez que una
dirección ingresó en ella, pero sí podemos engañar al navegador para que no
ingrese esa dirección, y sus datos, sino otra que nosotros le indiquemos. Esto
lo hacemos mediante la función
header(), específicamente enviándole el header `Location: `.
Este header le indica al navegador que la página solicitada ya no
existe y que está en una nueva ubicación, la que se le indica con ese
encabezado. El navegador, a fin de acelerar futuros accesos a la página
pedida, en lugar de guardar la dirección original, que supone obsoleta, guarda
en el historial la nueva, que supone es la correcta de allí en más.
El
ejemplo
muestra cómo funciona este mecanismo. Para evitar usar tablas en base de
datos, lo que hemos hecho es usar una variable de sesión (array
$_SESSION) que vamos incrementando. Para simular que esta operación funcione o falle,
como podría ocurrir con una operación de SQL, le damos la opción al usuario
para generar un éxito o un fracaso a través de sendos botones.
Si el usuario pulsa el botón
Funciona, el contador en $_SESSION['contador'] se incrementará en uno y
se mostrará un mensaje confirmando el éxito de la operación. Si el usuario
pulsa el botón No funciona, el programa hará como si hubiera habido un fallo.
Debajo de los botones se muestra el valor del contador, que inicialmente
estará en blanco. Al pulsar el botón
Funciona, se incrementará en uno. Al pulsar el botón
No Funciona, no se incrementará pues, supuestamente, habría habido un error. Lo
importante es que en el caso de pedir un refresco de la página tanto luego de
que funcione como de que no, el contador no cambiará.
Al principio del script, tras iniciar la sesión (función
session_start(), sino el array $_SESSION no mantiene los valores entre accesos)
defino un par de funciones redireccion_relativa() y
redireccion_aqui(). Estas funciones arman un header del tipo
'Location: ` para forzar una redirección. La primera de las
funciones hace una redirección relativa a la página actual, la segunda hace
una redirección a sí misma. Ambas admiten un par de argumentos que permiten
armar un URL con argumentos, usando la función
http_build_query(). La primera de estas funciones no se usa en el ejemplo, la incluí como
`tip´. Para completar las posibilidades, faltan las funciones
redireccion_absoluta() y redireccion_raiz(), la
primera es trivial, va donde le digas. La segunda sirve para ir a una página
relativa a la raíz de la aplicación. Esta función depende de una variable
externa a esta página que indique la dirección raíz de la aplicación. Esta
variable usualmente estará guardada en un archivo de configuración de la
aplicación o en un registro de la base de datos. Por depender de variables
externas al ejemplo, no la incluí, pero es fácil hacerla a partir de las
dadas.
Tras estas funciones comienza el proceso en sí. Noten que hasta este punto no
se ha enviado al navegador ni un solo carácter. Esto es importante, el
header `Location: ` no funciona si ya se ha enviado algo
y da error, así que es importante que no haya ni siquiera un carácter en
blanco. Vale la pena mencionar aquí algo que hace que este error salga sin que
uno lo quiera. Aunque uno haya buscado hasta el aburrimiento que no hubiera
caracteres extra (por ejemplo, no debe haber nada antes del primer
<?php) el error aparece. Esto suele ocurrir cuando la página
incluye otros scripts a través de la instrucción
include(). Si alguno cualquiera de estos archivos incluidos tienen aunque más no fuera
una interlínea después del último ?> de cierre, ese carácter se
enviará al navegador y generará error. En ocasiones, el editor de texto que
usamos agrega automáticamente una interlínea al final del archivo sin
preguntar. La forma de evitar este error es simplemente dejar el archivo
incluído sin el ?> final. Esto no genera error y, de hecho, es la
sugerencia de
Rasmus Lerdorf, quien ha de saber lo que hace.Volviendo al programa, en caso de recibir un
argumento de nombre 'submit' verifico que el valor sea
'Funciona', en ese caso, incremento el contador y hago una
redirección a esta misma página pero cambiando los argumentos. No quiero que
haya un argumento de nombre 'submit' con valor
'Funciona' pues esto haría que el contador se volviera a
incrementar. Invento otro argumento, de nombre 'confirma' y valor
'ok'. Es este el valor que el navegador recibirá como nueva
ubicación de esta página y la que guardará en su historial. De hecho, si se
mira en la casilla de dirección del navegador, se verá que el URL que muestra
es el de la página de confirmación, no el del formulario.
En el caso de error, no hago ninguna redirección, lo habitual es que el form
se vuelva a mostrar con los valores que el usuario hubiera ingresado, que se
obtendrán de las variables
$_GET
o
$_POST
según se hubiera indicado en el formulario.
El resto del programa no requiere mayor explicación que la que dan los comentarios que contiene.