PHP-TUT-10 Formularios en PHP (y II)

Facebooktwittergoogle_pluslinkedinmailFacebooktwittergoogle_pluslinkedinmail

En el artículo anterior aprendimos lo básico para mandar datos desde un formulario a un script PHP receptor. Ahora vamos a ir al siguiente nivel, aprendiendo a enviar y recibir ficheros cómo imágenes, audio, etc, y almacenarlos en el servidor.

ENVIANDO ARCHIVOS

Ya sabemos cómo enviar datos que el usuario escribe en un formulario y cómo recibirlos en un script en el servidor (lo que luego se pueda hacer con esos datos es tema para otros artículos). Pero hay un campo especial en algunos formularios. Se trata de los campos de tipo file que, como usted ya sabe, permiten el envío de archivos. ¿Cómo se reciben y procesan esos campos? PHP cuenta con una matriz específica llamada $_FILES, dónde se reciben los ficheros enviados.

Llegado este punto quiero recordarte algo que ya hemos comentado antes: PHP no reconoce ni puede procesar ficheros que hayan sido enviados mediante get. Cuando uses un formulario desde el cual el usuario pueda enviar ficheros al servidor usa SIEMPRE el método post.

Además, cuando tu formulario vaya a incluir un campo de fichero tienes que añadirle, SIEMPRE, el atributo enctype="multipart/form-data" cómo vamos a ver en los ejemplos.

La matriz $_FILES que gestiona los ficheros es asociativa de dos índices. El primero (el de las filas, para entendernos) contiene el nombre (atributo name) que se ha dado en el formulario a cada campo del fichero (puede haber más de uno en un mismo formulario). Dentro de cada fila encontramos otro índice con las propiedades del fichero, tales como nombre, tamaño, tipo, etc. Las propiedades que definen un archivo enviado al servidor aparecen recopiladas en la tabla reproducida a continuación:

PROPIEDAD CONTENIDO
error Indica si se ha producido un error en el envío del fichero. Si todo ha ido correctamente, esta propiedad almacena el valor 0. Si se ha producido un error, el valor es un código numérico.
name El nombre del archivo, tal como lo tiene almacenado el usuario en su ordenador.
tmp_name Es un nombre temporal que usa PHP para la gestión provisional del archivo, hasta que lo almacene en el disco del servidor, lo envíe por correo electrónico a donde corresponda, o cualquier otra acción que le hayamos programado al script.
type Almacena el tipo de fichero que se ha enviado. Puede ser una imagen, audio, vídeo, texto plano, etc.
size El peso del archivo en bytes.

Para empezar, mira el script enviarFichero.htm:

Carga este código en tu navegador, selecciona un fichero cualquiera de tu ordenador y envíalo. El script receptor es recibirFichero.php:

El resultado puede diferir con el que te muestro a continuación, pero eso depende del fichero que tú envíes. En los datos se parecerá al de la siguiente imagen:

Los datos de un fichero enviado.

Los datos de un fichero enviado.

Este script emplea un bucle foreach, que ya conocemos de este artículo, para recorrer cada uno de los elementos de una matriz que, a su vez, es el elemento "fichero" de la matriz $_FILES. El nombre de este elemento viene dado por el nombre del campo de archivo en el formulario. Todos los elementos de esta matriz (las propiedades que PHP reconoce del archivo enviado) se listan una a una.

  • En primer lugar, ves la propiedad name que, como se aprecia, corresponde al nombre con el que está grabado el archivo en el disco del cliente. El nombre, sin más, sin ruta ni nada adicional. Es lógico que la ruta no se transmita, puesto que el archivo no se va a grabar en la misma ruta en el servidor que aquella que el usuario tenga en su equipo cliente.
  • La propiedad type contiene el valor image/pjpeg. Este valor nos indica que el archivo es una imagen de tipo jpg. Cuando se envía una imagen, el valor de type siempre empieza por image/ y añade, detrás del slash, una referencia acerca del formato de la imagen. Esta propiedad continene el tipo MIME(1) del archivo.
  • La propiedad tmp-name puede parecer intrascendente, sobre todo si uno piensa que ha sido asignada por el propio intérprete de PHP. Sin embargo es, quizás, una de las propiedades más interesantes, como iremos viendo en su momento.
  • La propiedad error contiene el valor 0, lo que nos indica que la transferencia se ha hecho sin problemas.
  • Por último, la propiedad size almacena el peso del archivo en bytes. Ten en cuenta que un Kb son 1024 bytes, y un mega son 1024 Kb.

(1) Los tipos más comunes de archivos que se suelen enviar a través de un formulario establecen unos valores en la propiedad type que aparecen listados en la tabla reproducida a continuación. Hay más tipos de archivos, pero éstos son los más comunes. Además, ya sabes cómo visualizar las propiedades de un archivo concreto con el que puedas necesitar trabajar en el futuro.

 TYPE  TIPO DE ARCHIVO
image/pjpeg o image/jpeg Imagen en formato jpg.
image/gif Imagen en formato gif.
image/bmp o image/wbmp Imagen en formato bmp.
image/x-png o image/png Imagen en formato png.
audio/mpeg Audio en formato mpeg (normalmente mp3).
video/mpeg Vídeos en formato mpeg.
application/x-rar-compressed Cuando el tipo de un archivo empieza por application/ requiere ser abierto con algún programa específico. En este ejemplo, se trata de un archivo en formato rar, que puede ser abierto con determinadas utilidades de compresión-descompresión.
text/plain Texto plano.
text/richtext Texto enriquecido.
application/pdf Archivo para ser abierto con un visor o editor capaz de reconocer el formato pdf.

LIMITANDO EL TAMAÑO DEL ARCHIVO

Éste es un aspecto muy importante. Cuando incluimos en un formulario la posibilidad de que el usuario envíe archivos al servidor debemos, de algún modo, limitar el tamaño de dichos archivos. Suponte que quieres almacenar las imágenes que te mandan para un uso posterior. Y ahora supongamos que te mandan una foto del ayuntamiento de tu ciudad, a tamaño natural y en formato bmp. Ya tienes tu servidor colapsado hasta el fin de los tiempos. Bromas aparte, es importante establecer un límite. Para ello incluimos en el campo de archivo del formulario un campo oculto cuyo nombre sea “MAX_FILE_SIZE” y cuyo valor sea el peso en bytes que quieres establecer como límite. Mira un ejemplo de uso en ficheroMaximo.htm:

Observa la línea resaltada. Cuando se usa esta técnica para limitar el peso de un archivo, el campo oculto debe ir siempre antes del campo de archivo. Lo ideal es ponerlo inmediatamente antes, ya que así nos permite, al revisar el código, reconocer ese campo oculto como asociado al campo de archivo. Si el usuario intenta mandar un archivo que supere el tamaño máximo permitido, dicho archivo no será aceptado por PHP.

Esto significa que las propiedades type y tmp_name no tendrán contenido, la propiedad size tendrá el valor 0 y la propiedad error tendrá el valor 2. El hecho de que la propiedad tmp_name no tenga valor asignado imposibilita, en la práctica, el manejo del archivo por parte del script. En este mismo artículo entenderás por qué. De todos modos, este sistema no es seguro. Cualquier persona con un mínimo de práctica puede anular este control de tamaño mediante lo que se conoce como HTML Injection. Es una técnica de hacking muy simple para la manipulación de formularios web. No vamos a entrar aquí a detallar cómo funciona, ya que éste no es un tratado sobre ese tema. Lo único sobre lo que quiero llamar tu atención es que, si realmente deseas limitar el peso de los archivos que su script deba poder procesar, es mejor recurrir a sistemas más eficaces. Por ejemplo, puedes usar la propiedad size, de modo que el script no procese el archivo si el valor de esta propiedad supera un límite pre-establecido. Recuerda esto: siempre que sea posible elige, para llevar a cabo cualquier tarea, la ejecución en el lado del servidor. Esto aleja el funcionamiento de tu código de los usuarios. Ellos sólo necesitan obtener resultados rápidos y seguros, no saber cómo los obtienen.

ENVIANDO MÚLTIPLES ARCHIVOS

Puedes crear un formulario que permita al usuario enviar más de un archivo y tratarlos todos como elementos de una matriz. Por ejemplo, supón que tu página le da al cliente la opción de enviar hasta cuatro fotografías al servidor. Veamos cómo hacerlo en el código multiplesArchivos.htm, que hemos listado a continuación:

Quiero que te fijes en el atributo name de los campos de archivo. Lo he resaltado en el código para llamar su atención. Todos los campos tienen, aparentemente, el mismo valor en el atributo name. Esto no parece tener ningún sentido. Desde que aprendimos HTML, sabemos que cada campo de un formulario debe tener un nombre único para que se pueda trabajar con él de forma inequívoca. Esto también es válido cuando trabajamos con páginas dinámicas, ya que cada campo se envía como un par nombre-valor al servidor. Sin embargo, éste es un caso especial. Repara en que los nombres de los campos llevan corchetes, indicando que pertenecen a una matriz. Esto hará que los campos sean enviados al servidor como tal matriz. No se les ha atribuido un índice, de modo que el primer campo tomará el índice 0, el segundo el índice 1, y así sucesivamente.

Selecciona hasta cuatro archivos de tu ordenador y pulsa el botón Enviar. En ese momento los archivos son enviados, como una matriz, al script que aparece en el atributo action del formulario, cuyo nombre es multiplesArchivos.php, y cuyo listado aparece a continuación:

Vamos a ver cómo funciona este código. En primer lugar, se determina el número de elementos que componen la matriz de ficheros enviados al servidor. Nosotros sabemos que son cuatro elementos, ya que en el formulario hay cuatro campos de archivo con el nombre de esta matriz. Cuando se envían múltiples archivos al servidor usando el sistema de matriz, ésta se crea, siempre, con tantos elementos como campos contiene el formulario, con independencia de que el usuario haya seleccionado un archivo para todos los campos, para algunos, o para ninguno. Fíjate en la línea que cuenta los elementos de la matriz, reproducida a continuación:

$totalDeArchivos = count($_FILES["archivos"]["tmp_name"]);

En este caso, la matriz $_FILES tiene tres índices. El primero es el que corresponde al nombre de la matriz de campos: en este caso, "archivos". Recuerda que así es como hemos llamado a los campos en el formulario. El segundo índice corresponde a las propiedades de cada uno de los elementos de la matriz "archivos". El tercer índice es el número de elemento de la matriz "archivos" al que queremos referirnos. Para efectuar la cuenta de elementos de la matriz "archivos" hemos usado la propiedad name, pero podríamos haber usado cualquier otra. Eso no va a cambiar el resultado.

A continuación, fíjate en que entramos en un bucle que itera tantas veces como elementos tenga la matriz "archivos". En este caso son cuatro iteraciones. Dentro del bucle vamos a determinar el valor de la propiedad name de cada uno de los elementos, es decir, de cada uno de los archivos enviados al servidor. Si en alguno de los campos el usuario no ha seleccionado un archivo, esta propiedad no tendrá contenido. Para determinar esto empleamos un condicional, así:

if ($_FILES["archivos"]["name"][$contador] == ""){

Ahora carga en el navegador, si no lo has hecho antes, la página multiplesArchivos.htm, y selecciona un archivo para el primer campo, y otro para el tercero. Envía el formulario, que irá contra el script que acabamos de ver. Éste mostrará los nombres de los archivos que se han subido. En los correspondientes a los campos segundo y cuarto, donde no se seleccionó ningún fichero, se muestra un aviso de inexistente.

PROCESANDO LOS ARCHIVOS ENVIADOS

Uno de los usos más habituales de los archivos que son enviados al servidor es su almacenaje en el mismo para ponerlos a disposición de otros usuarios. Consideremos, por ejemplo, algo tan en boga como los portales de contactos, donde cada persona se anuncia para conocer a otras con las que pueda congeniar o hacer amistad o, cómo ponen muchos, “lo que surja”). Lo normal es que el anunciante tenga opción a enviar una o más fotografías que quedarán almacenadas en el servidor para que otros usuarios las vean. Vamos a aprender a grabar, en el disco duro del servidor, los archivos que nos envíe el usuario. Para ello, contamos con la función move_uploaded_file ().

La sintaxis normalizada implica que recibe dos argumentos, separados por una coma. El primero corresponde a la propiedad tmp-name del archivo subido. El segundo corresponde al nombre con el que se grabará dicho archivo en el servidor, incluyendo la ruta. Vamos a empezar considerando el listado grabarFichero.htm.

Como ves, no tiene nada de particular. Selecciona un fichero y lo envía al script que hay en grabarFichero.php, cuyo código es el siguiente:

Ejecuta grabarFichero.htm. Carga un fichero de imagen de tu ordenador en el campo de archivo. Pulsa el botón ENVIAR y pasarás el archivo seleccionado al script. Dado que no estás trabajando a través de Internet, sino en tu propio ordenador, la respuesta será prácticamente inmediata. El script te informará, mediante un mensaje en la página, de que el archivo ha sido grabado en el servidor. Cierra el navegador y abre la carpeta donde estás almacenado tus scripts. Allí encontrarás un archivo de imagen con el nombre fotoDelUsuario.jpg, que es copia de la imagen jpg que subiste. El archivo original no ha desaparecido ni sufrido alteración alguna. Es lógico. El usuario no va a subirnos un fichero al servidor a costa de perderlo en su equipo. Veamos qué es lo que hemos hecho. En primer lugar, hemos usado la propiedad tmp_name del archivo subido para referirnos a él, así:

$archivoRecibido = $_FILES["fichero"]["tmp_name"];

A continuación, hemos creado una variable con el nombre con el que grabaremos el archivo en el servidor, tal como se muestra a continuación:

$destino = "fotoDelUsuario.jpg";

Por último, hemos grabado el archivo mediante el uso de la función que estamos estudiando, así:

move_uploaded_file ($archivoRecibido, $destino);

Por supuesto, podríamos haberlo hecho todo en una sola línea de código, ahorrándonos, además, el uso de dos variables, tal como se ve a continuación:

move_uploaded_file ($_FILES["fichero"]["tmp_name"], "fotoDelUsuario.jpg");

Sin embargo, tal como lo hemos hecho queda mucho más legible y claro.

La función move_uploaded_file() devuelve un valor booleano, que será true, si la grabación ha podido realizarse, o false, si ha surgido algo que impida grabar el fichero en su ubicación de destino.

Cuando especificamos el nombre con el que vamos a grabar el fichero podemos incluir una ruta diferente a la actual. Esta puede ser ralativa al directorio dónde está almacenado en el servidor el script ("ficherosAlmacenados/imagenGrabada.jpg", por ejemplo), o absoluta ("http://www.miservidor.com/ficheros/imagenes/imagenParaGrabar.jpg"). En el ejemplo, no hemos especificado ruta, con lo que el fichero de imagen enviado se grabará en el mismo que estamos almacenando nuestros scripts. En la práctica, esto es algo muy desaconsejable. Cuando creamos un sitio web los elementos deben estar organizados en directorios, de forma que todo sea fácil de localizar.

En todo caso, debemos asegurarnos de que el directorio dónde queremos grabar el fichero enviado existe y Apache tiene permiso de escritura para poder grabar. Si no, la grabación no se llevará a cabo y move_uploaded_file() nos devolverá un false booleano, cómo hemos comentado.

En principio, este código funciona. Sin embargo, adolece de ciertas deficiencias que tenemos que subsanar. Por ejemplo, el usuario puede equivocarse y mandarnos un archivo de audio en lugar de una imagen. O quizás mande una imagen, pero ésta se encuentre en formato bmp, que no queremos usar en nuestra página. Para evitar esto vamos a usar la propiedad type del archivo subido. Lo que haremos será poner un condicional que ofrezca una "salida honrosa" si el archivo no es del tipo adecuado. Puede ser algo como lo siguiente:

if ($_FILES["fichero"]["type"] != "image/pjpeg") {
    die ("El fichero no tiene el formato adecuado.");
}

Lo que hacemos es comprobar si el fichero está en formato jpg. Esto no tiene nada que ver con que el nombre del archivo tenga la extensión adecuada. Puedes grabar un fichero cualquiera en tu disco duro y ponerle al nombre la extensión jpg, pero no por eso será una imagen en ese formato. La propiedad type no comprueba la extensión del nombre del archivo, sino el tipo MIME de formato en el que está grabado. Observa que, si el formato no es el adecuado, se ejecuta la función die (). Esta función, de nombre tan dramático (“die”, en inglés, significa, literalmente, “muere”), termina la ejecución del script mostrando el texto que recibe como argumento. Así pues, si el formato no es el adecuado, la ejecución del script termina en ese punto. Ya no se comprueba nada más, ni se graba nada, ni se hace nada. Sólo se muestra el mensaje y se acabó.

Esto nos proporciona, además, una posibilidad de limitar el peso de los archivos que el usuario puede enviar al servidor. Antes hemos visto una forma de hacer esto, pero hemos comentado que no es segura. Ahora podemos hacerlo de un modo más eficiente, así:

if ($_FILES["fichero"]["size"] > 200000) {
    die ("El fichero es demasiado grande.");
}

Como ves, esta vez hacemos uso de la propiedad size del archivo transmitido. Si el valor de esta propiedad supera el tope que hemos establecido (en este caso 200000 bytes) la ejecución se interrumpe. Y, ya puestos, podemos hacer otra comprobación, relativa a que se produzca algún error general durante la transferencia del archivo, así:

if ($_FILES["fichero"]["error"] != 0) {
    die ("Se ha producido un error.");
}

Sin embargo, aún esto es mejorable. Ya sabemos que podemos incluir etiquetas de HTML en nuestros scripts PHP. Lo hemos hecho varias veces a lo largo de este y otros artículos, insertando dichas etiquetas en la instrucción echo. Lo cierto es que también podemos incluir de este modo código JavaScript. Así pues, podemos hacer que el script regrese a la página del formulario si hay algún error, de modo muy sencillo. Observa el script comprobarYVolver.php:

Observa lo que hacemos. Comprobamos si las propiedades type, size y error del fichero enviado están dentro de los valores establecidos como válidos. Si no es así, ponemos el valor true en una variable lógica que, inicialmente, se declara como false. A continuación, comprobamos si se ha producido algún error (si la variable lógica empleada tiene el valor true). Si es así, se ejecuta un código JavaScript (ver en la parte resaltada), que devuelve al usuario a la página donde se pide el fichero.

Y sigamos pensando en mejorar nuestro script. Ya tenemos un código que hace todo lo que podemos necesitar. Pero ahora ponte en el papel del usuario. Si se le pide un fichero y, por error, selecciona uno inadecuado, el script hace que se le vuelva a pedir, sin darle ninguna indicación de lo que ha ocurrido. Es fácil que el usuario piense que nuestro sitio funciona mal y lo abandone sin más. Mala perspectiva. Es necesario hacer algo para informar al usuario de la causa por la que no se acepta su fichero antes de volver a pedírselo. Para ver cómo podemos resolver esto observa el script volverPorFuncion.php:

Veamos cómo opera este script. En primer lugar, ves que tiene una función de JavaScript destinada a devolverte a la página que contiene el formulario donde el usuario decide qué fichero enviar. Esta función, de momento, no se ejecuta. Veamos el código PHP, que es lo que nos interesa. La parte donde se comprueba si hay algún error es igual que en el caso anterior, solo que mostrando, en cada circunstancia, el tipo de error.

A continuación, se comprueba si la variable lógica que usamos como detector de haberse producido un error contiene el valor true. Si es así, se le muestra al usuario un botón que activa la función JavaScript para volver a la página anterior. Si la variable de error contiene un valor false, no se muestra el botón. En su lugar se almacena el archivo en la carpeta destinada a este fin en el servidor. Como ves, con este código el usuario ya tiene una idea muy clara de lo que ocurre cuando envía un archivo y éste no es correcto.

Un error durante el envío de un archivo puede producirse por múltiples causas. Seguro que has visitado páginas de Internet cuyo código es correcto y, sin embargo, se ha encontrado con alguna dificultad al enviar ficheros. Esto puede deberse a problemas en las líneas telefónicas u otros factores fortuitos. No siempre hay gran cosa que puedas hacer para evitar estos errores, pero si puede detectarlos a tiempo, tal como se describe en el siguiente apartado.

ERRORES IMPREVISTOS

Cuando se escribe para Internet, hay que pensar que nuestro sitio lo visitarán usuarios muy diversos, con distintos equipos cliente, con distintos navegadores y con muy diversos comportamientos. Es conveniente tratar de prever el máximo número posible de situaciones en las que pueda producirse un error durante la ejecución de nuestros scripts. Otras veces, esos errores estarán, incluso, causados por nosotros mismos, ya sea por un descuido durante la programación, un archivo que no está donde debe, etc. Se supone que los webmasters somos personas cuidadosas, detallistas, exigentes con nuestro propio trabajo y que probamos cada script hasta la saciedad. Sin embargo, también somos humanos y cometemos equivocaciones. De hecho, el que no se equivoca nunca es que tampoco está haciendo nada que valga la pena. En una ocasión tuve un error garrafal en una de mis clases. No voy a contarte cuál fue, porque todavía me avergüenzo de ello. El caso es que uno de mis alumnos me dijo: “Vaya. Si tú también te equivocas.”, a lo que le respondí: “Por supuesto. Me equivoco constantemente. Así es como aprendo”. Fue la salida más honrosa que se me ocurrió en ese momento. Anécdotas aparte, el caso es que, en ocasiones, se producen errores que no tenemos previstos. Por ejemplo, supón que, al querer grabar un fichero con la función move_uploaded_file (), escribimos, como ruta para almacenarlo en el servidor, el nombre de una carpeta que no existe. Imagina que, en los códigos anteriores, a la hora de grabar el archivo tenemos algo como lo siguiente:

Fíjate en la línea resaltada. Como ves, se va a intentar grabar el archivo en una ruta inexistente. Uno puede pensar que como no se podrá grabar, el script mostrará el mensaje que hemos previsto para este caso, aquel que dice:

El fichero no se ha podido grabar.

Pero no es sólo eso. PHP intenta realmente ejecutar la grabación del archivo, y al no encontrar la carpeta adecuada nos da un error como el siguiente:

Warning: move_uploaded_file(ficherosEnviados/fotoDelUsuario.jpg) [function.move-uploaded-file]: failed to open stream: No such file or directory in C:\Documents and Settings\Jose\Mis documentos\Mis webs dinamicas\operadorAntiError.php on line 37

Warning: move_uploaded_file() [function.move-uploaded-file]: Unable to move 'C:\WINDOWS\TEMP\php14.tmp' to 'ficherosEnviados/fotoDelUsuario.jpg' in C:\Documents and Settings\Jose\Mis documentos\Mis webs dinamicas\operadorAntiError.php on line 37

(o algo parecido, depende de la versión de PHP, tu navegador, etc, pero, en todo caso, un mensaje horroroso).

El fichero no se ha podido grabar.

Como ves, es una salida bastante desagradable a la vista del usuario. Además, en determinado tipo de errores puede llegar a detenerse la ejecución del script.

Cuando una instrucción de PHP pueda (o pensemos que puede) dar un error, lo que hacemos es anteponerle el signo arroba (@), de forma que no se muestren los mensajes de error de PHP y se pueda continuar procesando el script. En este caso, pondremos este operador de control de errores justo antes de move_uploaded_file (), así:

if (@move_uploaded_file ($archivoRecibido, $destino)) {

Al anteponer este operador, lo que le estamos diciendo al intérprete es que si esta función da un error, no lo muestre y continúe la ejecución del script (si es posible).

Cómo todo, esto tiene sus ventajas y sus inconvenentes. Si no recibes mensajes de error, te costará mucho más encontrar los posibles fallos y depurarlos. Lo suyo es que durante el desarrollo y las pruebas de un código, recibas tantos mensajes de error cómo sea posible. Los errores que te salten hoy a tí no le saltarán al usuario mañana.