Symfony 2.4, el libro oficial

14.4.  Caducidad y validación HTTP

La especificación HTTP define dos modelos de memoria caché:

  • Con el modelo de caducidad (en inglés, expiration model), sólo tienes que especificar el tiempo durante el que la respuesta se considerar válida (en inglés, fresh). Para ello, utiliza la cabecera Cache-Control y/o Expires. Las cachés que entienden el modelo de caducidad no harán la misma petición hasta que la versión guardada en la caché alcance la fecha de caducidad y por tanto, se vuelva caducada (en inglés, stale).
  • Cuando las páginas son muy dinámicas (es decir, su contenido cambia con mucha frecuencia), es mejor utilizar el modelo de validación (en inglés, validation model). Con este modelo, la caché guarda la respuesta, pero, pregunta al servidor en cada petición si la respuesta cacheada sigue siendo válida. La aplicación añade un identificador único a las respuestas (la cabecera Etag) y/o una marca de tiempo (la cabecera Last-Modified) para comprobar si la página ha cambiado desde que se guardó en la caché.

El objetivo de ambos modelos es no tener que generar nunca la misma respuesta dos veces, para lo cual se utilizan cachés para guardar las páginas y las reglas anteriores para determinar si las páginas siguen siendo válidas.

14.4.1. Caducidad

El modelo de caducidad es el más eficiente y simple de los dos modelos de memoria caché y es aconsejable utilizarlo siempre que sea posible. Cuando la respuesta se guarda en la caché con una caducidad, la caché reutiliza esa misma respuesta sin volver a ejecutar la aplicación hasta que caduque.

En el modelo de caducidad se utiliza una de las dos siguientes cabeceras HTTP: Expires o Cache-Control.

14.4.2. Caducidad con la cabecera Expires

Según la especificación HTTP, "el campo de la cabecera Expires indica la fecha/hora después de la cual se considera que la respuesta ha caducado". La cabecera Expires se puede establecer con el método setExpires() del objeto Response, pasándole una instancia de DateTime como argumento:

$date = new DateTime();
$date->modify('+600 seconds');

$response->setExpires($date);

La cabecera HTTP resultante tiene el siguiente aspecto:

Expires: Thu, 01 Mar 2013 16:00:00 GMT

Nota El método setExpires() convierte automáticamente la fecha a la zona horaria GMT, tal y como lo requiere la especificación.

Ten en cuenta que en las versiones de HTTP anteriores a la 1.1, el servidor no está obligado a enviar la cabecera Date. Por tanto, la memoria caché (por ejemplo el navegador) puede acabar utilizando su propio reloj local para evaluar la cabecera Expires, lo que puede provocar errores de sincronización. Otra limitación de la cabecera Expires es que la especificación establece que Los servidores HTTP/1.1 no deben enviar fechas más allá de 1 año de plazo en la cabecera Expires.

14.4.3. Caducidad con la cabecera Cache-Control

Debido a las limitaciones de la cabecera Expires, casi siempre debes utilizar en su lugar la cabecera Cache-Control. Recuerda que la cabecera Cache-Control se utiliza para especificar muchas directivas de caché diferentes. Para la caducidad hay dos directivas: max-age y s-maxage. La primera la utilizan todas las cachés, mientras que la segunda sólo se tiene en cuenta en las cachés compartidas:

// Establece el número de segundos después de los cuales
// la respuesta ya no se considera válida
$response->setMaxAge(600);

// Lo mismo que la anterior pero sólo para cachés compartidas
$response->setSharedMaxAge(600);

Utilizando el código anterior, la cabecera Cache-Control tendrá el siguiente aspecto (puede contener otras directivas adicionales):

Cache-Control: max-age=600, s-maxage=600

14.4.4. Validación

Cuando un recurso se tiene que actualizar tan pronto como cambien sus contenidos, el modelo de caducidad se queda corto. Con el modelo de caducidad, no se pide a la aplicación que regenere la respuesta hasta que la caché no considere que la respuesta ha caducado.

El modelo de validación soluciona este problema. En este modelo la caché sigue almacenando las respuestas. Pero en este caso, con cada petición del usuario la caché pregunta a la aplicación si la respuesta guardada sigue siendo válida o no. Si la página cacheada todavía es válida, la aplicación devuelve un código de estado 304 y ningún otro contenido. De esta forma la caché sabe que puede devolver al usuario la respuesta cacheada.

Con este modelo de caché, sólo ahorras CPU si te cuesta menos trabajo determinar si la página cacheada sigue siendo válida que generar de nuevo la página entera, tal y como se explicará detalladamente más adelante.

Truco El código de estado 304 significa Not Modified. Se trata de un código muy importante porque permite que las respuestas no incluyan ningún contenido. La respuesta es simplemente una instrucción directa a la caché para que siga utilizando el contenido cacheado.

Al igual que con la caducidad, hay dos cabeceras HTTP diferentes que puedes utilizar para implementar el modelo de validación: Etag y Last-Modified.

14.4.5. Validando con la cabecera ETag

La cabecera ETag (del inglés, entity tag) es una cabecera cuyo valor es una cadena de texto que identifica unívocamente no sólo un recurso sino una versión concreta de ese recurso. Este valor lo debe generar la aplicación, ya que es la única que sabe si por ejemplo el recurso /about cacheado sigue siendo válido o no. Una ETag es como una huella digital y se utiliza para comparar rápidamente si dos versiones diferentes de un recurso son equivalentes. Como las huellas digitales, cada ETag debe ser única en todas las versiones o representaciones de un mismo recurso.

Una de las implementaciones más sencillas para generar la ETag es utilizar como valor el md5 del contenido:

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $response = $this->render('MyBundle:Main:index.html.twig');
    $response->setETag(md5($response->getContent()));
    $response->setPublic(); // haec que la respuesta sea cacheable
    $response->isNotModified($request;

    return $response;
}

El método isNotModified() compara el valor de la cabecera If-None-Match de la petición con el valor de la ETag que contiene el objeto Response. Si ambos coinciden, el método establece automáticamente el código de estado de la respuesta a 304.

Nota Antes de devolver la petición a la aplicación, el sistema de caché establece en la cabecera If-None-Match el valor adecuado en función del valor de la cabecera ETag de la petición. Este es el mecanismo que utilizan la caché y el servidor para comunicarse entre sí y para decidir si el recurso se ha actualizado desde que se guardó en la caché.

Este algoritmo es bastante simple, pero tiene el inconveniente de que es necesario crear el objeto Response completo antes de ser capaz de calcular la ETag, lo cual no es nada óptimo. En otras palabras, ahorra ancho de banda pero no ciclos de la CPU.

Más adelante en este capítulo se explica cómo utilizar la validación de una manera más inteligente para determinar si la respuesta sigue siendo válida, pero sin tener que generar la respuesta completa.

Truco Symfony2 también soporta las denominadas ETag débiles. Para utilizarlas, pasa true como segundo argumento del método setETag().

14.4.6. Validando con la cabecera Last-Modified

La cabecera Last-Modified es la segunda forma de validación. Según la especificación HTTP, "El campo de la cabecera Last-Modified indica la fecha y hora en la que el servidor considera que el recurso fue modificado por última vez". En otras palabras, la aplicación decide si el contenido cacheado ha sido actualizado en base a comparar la fecha de última actualización con la fecha en la que la respuesta se guardó en la caché.

Como valor de la cabecera Last-Modified se puede utilizar por ejemplo la fecha de actualización más reciente de todos los objetos que se utilizan para crear la página:

use Symfony\Component\HttpFoundation\Request;

public function showAction($articleSlug, Request $request)
{
    // ...

    $articleDate = new \DateTime($article->getUpdatedAt());
    $authorDate = new \DateTime($author->getUpdatedAt());

    $date = $authorDate > $articleDate ? $authorDate : $articleDate;

    $response->setLastModified($date);
    // Establece la respuesta a pública
    $response->setPublic();

    if ($response->isNotModified($request)) {
        return $response;
    }

    // ...

    return $response;
}

El método isNotModified() compara el valor de la cabecera If-Modified-Since enviada por la petición con la cabecera Last-Modified configurada en la respuesta. Si son equivalentes, establece el código de estado de la respuesta a 304.

Nota Antes de devolver la petición a la aplicación, el sistema de caché establece en la cabecera If-Modified-Since el valor adecuado en función del valor de la cabecera Last-Modified de la petición. Este es el mecanismo que utilizan la caché y el servidor para comunicarse entre sí y para decidir si el recurso se ha actualizado desde que se guardó en la caché.

14.4.7. Optimizando tu código con validación

El objetivo principal de cualquier estrategia de caché es aligerar la carga de la aplicación. Dicho de otra manera, cuanto menos hagas en tu aplicación para devolver una respuesta de tipo 304, mejor. El método isNotModified() te permite hacer exactamente eso:

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

public function showAction($articleSlug, Request $request)
{
    // Obtiene la mínima información para calcular la ETag
    // o el valor de Last-Modified (en función de la petición,
    // los datos de alguna base de datos, etc.)
    $article = ...;

    // crea una respuesta con una cabecera ETag y/o Last-Modified
    $response = new Response();
    $response->setETag($article->computeETag());
    $response->setLastModified($article->getPublishedAt());

    // establece la respuesta a pública (por defecto es privada)
    $response->setPublic();

    // verifica que la respuesta no se ha modificado para la petición dada
    if ($response->isNotModified($request)) {
        // devuelve inmediatamente la respuesta 304
        return $response;
    }

    // obtiene el resto de información de la página
    $comments = ...;

    // o renderiza una plantilla con la $response creada antes
    return $this->render(
        'MyBundle:MyController:article.html.twig',
        array('article' => $article, 'comments' => $comments),
        $response
    );
}

Cuando la respuesta no es modificada, el método isNotModified() establece automáticamente el código de estado de la respuesta a 304, elimina el contenido, y también elimina algunas cabeceras que no deben estar presentes en respuestas 304.

14.4.8. Variando la respuesta

Hasta ahora se ha supuesto que cada URI se corresponde con una única versión o variante de un recurso. La caché HTTP utiliza por defecto la URI del recurso como calve para almacenar la información en la caché. Si dos personas solicitan la misma URI, la segunda persona recibe el recurso cacheado.

En ocasiones esto no es suficiente y se necesita guardar en la caché diferentes versiones de la misma URI en función de uno o más valores de las cabeceras de la petición. Si comprimes por ejemplo las páginas cuando el cliente lo permite, cualquier URI tendrá dos representaciones: una cuando el cliente es compatible con la compresión, y otra cuando no. Esta decisión se toma a partir del valor de la cabecera Accept-Encoding de la petición.

En este caso, necesitas que la caché almacene una versión comprimida y otra sin comprimir de la respuesta para cada URI y que devuelva la versión correcta en función del valor de la cabecera Accept-Encoding. Para ello se utiliza la cabecera Vary de la respuesta, que es una lista separada por comas de diferentes cabeceras cuyos valores activan una representación diferente del recurso solicitado:

Vary: Accept-Encoding, User-Agent

Esta cabecera Vary indica que se deben guardar en la caché versiones diferentes del recurso en función de la URI y del valor de las cabeceras Accept-Encoding y User-Agent.

El objeto Response ofrece una interfaz muy sencilla para gestionar la cabecera Vary:

// establece una cabecera vary
$response->setVary('Accept-Encoding');

// establece múltiples cabeceras vary
$response->setVary(array('Accept-Encoding', 'User-Agent'));

El método setVary() acepta como parámetro el nombre de cabecera o un array de nombres de cabecera.

14.4.9. Caducidad y validación

Por supuesto, también puedes combinar tanto la caducidad como la validación de la misma Response. Como la caducidad siempre tiene prioridad sobre la validación, puedes aprovechar lo mejor de los dos mundos. En otras palabras, utilizando tanto la caducidad como la validación, puedes indicar a la caché que sirva el contenido guardado, mientras que revisas cada cierto tiempo que el contenido sigue siendo válido.

14.4.10. Otros métodos de Response

La clase Response proporciona varios métodos adicionales relacionados con la caché. Estos son los más útiles:

// marca la respuesta como obsoleta
$response->expire();

// devuelve una respuesta sin contenido y con el código 304
$response->setNotModified();

Además, puedes configurar muchas de las cabeceras HTTP relacionadas con la caché a través del método setCache():

// establece la configuración de caché con un solo método
$response->setCache(array(
    'etag'          => $etag,
    'last_modified' => $date,
    'max_age'       => 10,
    's_maxage'      => 10,
    'public'        => true,
    // 'private'    => true,
));