Caché HTTP

La naturaleza de las aplicaciones web ricas significa que son dinámicas. No importa qué tan eficiente sea tu aplicación, cada petición siempre contendrá más sobrecarga que simplemente servir un archivo estático.

Y para la mayoría de las aplicaciones Web, está bien. Symfony2 es tan rápido como el rayo, a menos que estés haciendo una muy complicada aplicación, cada petición se responderá rápidamente sin poner demasiada tensión a tu servidor.

Pero cuando tu sitio crezca, la sobrecarga general se puede convertir en un problema. El procesamiento que se realiza normalmente en cada petición se debe hacer sólo una vez. Este exactamente es el objetivo que tiene que consumar la memoria caché.

La memoria caché en hombros de gigantes

La manera más efectiva para mejorar el rendimiento de una aplicación es memorizar en caché la salida completa de una página y luego eludir por completo la aplicación en cada petición posterior. Por supuesto, esto no siempre es posible para los sitios web altamente dinámicos, ¿o no? En este capítulo, te mostraremos cómo funciona el sistema de caché Symfony2 y por qué este es el mejor enfoque posible.

El sistema de cache Symfony2 es diferente porque se basa en la simplicidad y el poder de la caché HTTP tal como está definida en la especificación HTTP. En lugar de reinventar una metodología de memoria caché, Symfony2 adopta la norma que define la comunicación básica en la Web. Una vez que comprendas los principios fundamentales de los modelos de caducidad y validación de la memoria caché HTTP, estarás listo para dominar el sistema de caché Symfony2.

Para efectos de aprender cómo guardar en caché con Symfony2, abordaremos el tema en cuatro pasos:

  1. Una pasarela de caché, o delegado inverso («proxy»), es una capa independiente situada frente a tu aplicación. La caché del delegado inverso responde a medida que son devueltas desde tu aplicación y contesta a peticiones con respuestas de la caché antes de que lleguen a tu aplicación. Symfony2 proporciona su propio delegado inverso, pero puedes utilizar cualquier delegado inverso.
  2. Las cabeceras de cache HTTP se utilizan para comunicarse con la pasarela de caché y cualquier otra caché entre tu aplicación y el cliente. Symfony2 proporciona parámetros predeterminados y una potente interfaz para interactuar con las cabeceras de caché.
  3. La caducidad y validación HTTP son los dos modelos utilizados para determinar si el contenido memorizado en caché es fresco (se puede reutilizar de la memoria caché) u obsoleto (lo debe regenerar la aplicación).
  4. Inclusión del borde lateral (Edge Side Includes -ESI) permite que la caché HTTP utilice fragmentos de la página en caché (incluso fragmentos anidados) independientemente. Con ESI, incluso puedes guardar en caché una página entera durante 60 minutos, pero una barra lateral integrada sólo por 5 minutos.

Dado que la memoria caché HTTP no es exclusiva de Symfony, ya existen muchos artículos sobre el tema. Si eres novato en el tema de la memoria caché HTTP, te recomendamos el artículo de Ryan Tomayko Things Caches Do. Otro recurso que aborda el tema es la Guía de caché de Mark Nottingham.

Memoria caché con pasarela de caché

Cuándo memorizar caché con HTTP, la caché está separada de tu aplicación por completo y se sitúa entre tu aplicación y el cliente haciendo la petición.

El trabajo de la caché es aceptar las peticiones del cliente y pasarlas de nuevo a tu aplicación. La memoria caché también recibirá las respuestas devueltas por tu aplicación y las remitirá al cliente. La caché es el «geniecillo» de la comunicación petición-respuesta entre el cliente y tu aplicación.

De paso, la memoria caché almacena cada respuesta que se considere «almacenable en caché» (consulta Introducción a la memoria caché HTTP). Si de nuevo se solicita el mismo recurso, la memoria caché envía la respuesta memorizada en caché al cliente, eludiendo tu aplicación por completo.

Este tipo de caché se conoce como pasarela de caché HTTP y existen muchas como Varnish, Squid en modo delegado inverso y el delegado inverso de Symfony2.

Tipos de Caché

Sin embargo, una pasarela de caché no es el único tipo de caché. De hecho, las cabeceras de caché HTTP enviadas por tu aplicación son consumidas e interpretadas por un máximo de tres diferentes tipos de caché:

  • Caché de navegadores: Cada navegador viene con su propia caché local, lo cual es realmente útil para cuando pulsas «atrás» o para imágenes y otros activos. La caché del navegador es una caché privada, los recursos memorizados en caché no se comparten con nadie más;
  • Delegados de caché: Un delegado de caché compartida es aquel en el cual muchas personas pueden estar detrás de uno solo. Por lo general instalado por las grandes corporaciones y proveedores de Internet para reducir latencia y tráfico de red;
  • Pasarela de caché: Al igual que un delegado, también es una caché compartida pero en el lado del servidor. Instalada por los administradores de red, esta tiene sitios web más escalables, confiables y prácticos.

Truco

Las pasarelas de caché a veces también se conocen como delegados inversos de caché, cachés alquiladas o incluso aceleradores HTTP.

Nota

La importancia de la caché privada frente a la compartida será más evidente a medida que hablemos de las respuestas en la memoria caché con contenido que es específico para un solo usuario (por ejemplo, información de cuenta).

Cada respuesta de tu aplicación probablemente vaya a través de uno o los dos primeros tipos de caché. Estas cachés están fuera de tu control, pero siguen las instrucciones de la caché HTTP establecidas en la respuesta.

Delegado inverso de Symfony2

Symfony2 viene con un delegado inverso de caché (también conocido como pasarela de caché) escrito en PHP. Que al activarla, inmediatamente puede memorizar en caché respuestas de tu aplicación. La instalación es muy fácil. Cada nueva aplicación Symfony2 viene con una caché preconfigurada en el núcleo (AppCache) que envuelve al predeterminado (AppKernel). El núcleo de la memoria caché es el delegado inverso.

Para habilitar la memoria caché, modifica el código de un controlador frontal para utilizar la caché del núcleo:

// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
require_once __DIR__.'/../app/AppCache.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
// envuelve el AppKernel predeterminado con un AppCache
$kernel = new AppCache($kernel);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

La memoria caché del núcleo actúa de inmediato como un delegado inverso —memorizando en caché las respuestas de tu aplicación y devolviéndolas al cliente—.

Truco

La caché del núcleo tiene un método especial getLog(), el cual devuelve una cadena que representa lo que sucedió en la capa de la caché. En el entorno de desarrollo, se usa para depurar y validar la estrategia de caché:

error_log($kernel->getLog());

El objeto AppCache tiene una sensible configuración predeterminada, pero la puedes afinar por medio de un conjunto de opciones que puedes configurar sustituyendo el método getOptions():

// app/AppCache.php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;

class AppCache extends HttpCache
{
    protected function getOptions()
    {
        return array(
            'debug'                  => false,
            'default_ttl'            => 0,
            'private_headers'        => array('Authorization', 'Cookie'),
            'allow_reload'           => false,
            'allow_revalidate'       => false,
            'stale_while_revalidate' => 2,
            'stale_if_error'         => 60,
        );
    }
}

Truco

A menos que la sustituyas en getOptions(), la opción debug se establecerá automáticamente al valor de depuración del AppKernel envuelto.

Aquí está una lista de las principales opciones:

  • default_ttl: El número de segundos que una entrada de caché se debe considerar nueva cuando no hay información fresca proporcionada explícitamente en una respuesta. Las cabeceras explícitas Cache-Control o Expires sustituyen este valor (predeterminado: 0);
  • private_headers: Conjunto de cabeceras de la petición que desencadenan el comportamiento Cache-Control «privado» en las respuestas en que no son clasificadas explícitamente como públicas o privadas vía una directiva Cache-Control. (default: Authorization y cookie);
  • allow_reload: Especifica si el cliente puede forzar una recarga desde caché incluyendo una directiva Cache-Control «no-cache» en la petición. Selecciona true para cumplir con la RFC 2616 (por omisión: false);
  • allow_revalidate: Especifica si el cliente puede forzar una revalidación de caché incluyendo una directiva Cache-Control max-age = 0 en la petición. Ponla en true para cumplir con la RFC 2616 (por omisión: false);
  • stale_while_revalidate: Especifica el número de segundos predeterminado (la granularidad es el segundo puesto que la precisión de respuesta TTL es un segundo) durante el cual la memoria caché puede regresar inmediatamente una respuesta obsoleta mientras que revalida en segundo plano (por omisión: 2); este ajuste lo reemplaza stale-while-revalidate de la extensión HTTP Cache-Control (consulta la RFC 5.861);
  • stale_if_error: Especifica el número de segundos predeterminado (la granularidad es el segundo) durante el cual la caché puede servir una respuesta obsoleta cuando se detecta un error (por omisión: 60). Este valor lo reemplaza stale-if-error de la extensión HTTP Cache-Control (consulta la RFC 5861).

Si debug es true, Symfony2 automáticamente agrega una cabecera X-Symfony-Cache a la respuesta que contiene información útil acerca de aciertos y errores de caché.

Nota

El rendimiento del delegado inverso de Symfony2 es independiente de la complejidad de tu aplicación. Eso es porque el núcleo de tu aplicación sólo se inicia cuando la petición se debe remitir a ella.

Introducción a la memoria caché HTTP

Para aprovechar las ventajas de las capas de memoria caché disponibles, tu aplicación se debe poder comunicar con las respuestas que son memorizables y las reglas que rigen cuándo y cómo la caché será obsoleta. Esto se hace ajustando las cabeceras de caché HTTP en la respuesta.

Truco

Ten en cuenta que HTTP no es más que el lenguaje (un lenguaje de texto simple) que los clientes web (navegadores, por ejemplo) y los servidores web utilizan para comunicarse entre sí. Cuando hablamos de la memoria caché HTTP, estamos hablando de la parte de ese lenguaje que permite a los clientes y servidores intercambiar información relacionada con la memoria caché.

HTTP especifica cuatro cabeceras de caché para respuestas en las que estamos interesados aquí:

  • Cache-Control
  • Expires
  • ETag
  • Last-Modified

La cabecera más importante y versátil es la cabecera Cache-Control, la cual en realidad es una colección de variada información de caché.

Nota

Cada una de las cabeceras se explica en detalle en la sección Caducidad y validación HTTP.

La cabecera Cache-Control

La cabecera Cache-Control es la única que no contiene una, sino varias piezas de información sobre la memoria caché de una respuesta. Cada pieza de información está separada por una coma:

Cache-Control: private, max-age=0, must-revalidate

Cache-Control: max-age=3600, must-revalidate

Symfony proporciona una abstracción de la cabecera Cache-Control para hacer más manejable su creación:

// ...

use Symfony\Component\HttpFoundation\Response;

$response = new Response();

// marca la respuesta como pública o privada
$response->setPublic();
$response->setPrivate();

// fija la edad máxima de privado o compartido
$response->setMaxAge(600);
$response->setSharedMaxAge(600);

// fija una directiva Cache-Control personalizada
$response->headers->addCacheControlDirective('must-revalidate', true);

Respuestas públicas frente a privadas

Ambas, la pasarela de caché y el delegado de caché, son considerados como cachés «compartidas» debido a que el contenido memorizado en caché se comparte con más de un usuario. Si cada vez equivocadamente una memoria caché compartida almacena una respuesta específica al usuario, posteriormente la puede devolver a cualquier cantidad de usuarios diferentes. ¡Imagina si la información de tu cuenta se memoriza en caché y luego la regresa a todos los usuarios posteriores que soliciten la página de su cuenta!

Para manejar esta situación, cada respuesta se puede fijar para que sea pública o privada:

  • public: Indica que la respuesta se puede memorizar en caché por ambas cachés privadas y compartidas;
  • private: Indica que la totalidad o parte del mensaje de la respuesta es para un solo usuario y no se debe memorizar en caché en una caché compartida.

Por omisión, Symfony conservadoramente fija cada respuesta para que sea privada. Para aprovechar las ventajas de las cachés compartidas (como el delegado inverso de Symfony2), explícitamente deberás fijar la respuesta como pública.

Métodos seguros

La memoria caché HTTP sólo funciona para métodos HTTP «seguros» (como GET y HEAD). Estar seguro significa que nunca cambia de estado la aplicación en el servidor al servir la petición (por supuesto puedes registrar información, datos de la caché, etc.) Esto tiene dos consecuencias muy razonables:

  • Nunca debes cambiar el estado de tu aplicación al responder a una petición GET o HEAD. Incluso si no utilizas una pasarela caché, la presencia del delegado de caché significa que alguna petición GET o HEAD puede o no llegar a tu servidor;
  • No esperes que haya métodos PUT, POST o DELETE en caché. Estos métodos están diseñados para utilizarse al mutar el estado de tu aplicación (por ejemplo, borrar una entrada de blog). La memoria caché debe impedir que determinadas peticiones toquen y muten tu aplicación.

Reglas de caché y valores predeterminados

HTTP 1.1 por omisión, permite memorizar en caché cualquier cosa a menos que haya una cabecera Cache-Control explícita. En la práctica, la mayoría de las cachés no hacen nada cuando las peticiones tienen una galleta, una cabecera de autorización, utilizan un método no seguro (es decir, PUT, POST, DELETE), o cuando las respuestas tienen código de redirección de estado.

Symfony2 automáticamente establece una sensible y conservadora cabecera Cache-Control cuando esta no está definida por el desarrollador, siguiendo estas reglas:

  • Si no has definido cabecera caché (Cache-Control, Expires, ETag o Last-Modified), Cache-Control es establecida en no-cache, lo cual significa que la respuesta no se guarda en caché;
  • Si Cache-Control está vacía (pero una de las otras cabeceras de caché está presente), su valor se establece en private, must-revalidate;
  • Pero si por lo menos una directiva Cache-Control está establecida, y no se han añadido directivas public o private de forma explícita, Symfony2 agrega la directiva private automáticamente (excepto cuando s-maxage está establecida).

Caducidad y validación HTTP

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

  • Con el modelo de caducidad, sólo tienes que especificar el tiempo en que la respuesta se debe considerar «fresca» incluyendo una cabecera Cache-Control y/o una Expires. Las cachés que entienden de expiración no harán la misma petición hasta que la versión en caché alcance el límite de caducidad y se vuelva «obsoleta»;
  • Cuando las páginas realmente son dinámicas (es decir, su representación cambia con mucha frecuencia), a menudo es necesario el modelo de validación. Con este modelo, la memoria caché memoriza la respuesta, pero, pregunta al servidor en cada petición si la respuesta memorizada sigue siendo válida. La aplicación utiliza un identificador de respuesta único (la cabecera Etag) y/o una marca de tiempo (la cabecera Last-Modified) para comprobar si la página ha cambiado desde su memorización en caché.

El objetivo de ambos modelos es nunca generar la misma respuesta en dos ocasiones dependiendo de una caché para almacenar y devolver respuestas «frescas».

Caducidad

El modelo de caducidad es el más eficiente y simple de los dos modelos de memoria caché y se debe utilizar siempre que sea posible. Cuando una respuesta se memoriza en caché con una caducidad, la caché memorizará la respuesta y la enviará directamente sin tocar a la aplicación hasta que esta caduque.

El modelo de caducidad se puede lograr usando una de dos, casi idénticas, cabeceras HTTP: Expires o Cache-Control.

Caducidad con la cabecera Expires

De acuerdo con la especificación HTTP «el campo de la cabecera Expires da la fecha/hora después de la cual se considera que la respuesta es vieja». La cabecera Expires se puede establecer con el método setExpires() de la Respuesta. Esta necesita una instancia de DateTime como argumento:

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

$response->setExpires($date);

La cabecera HTTP resultante se ve de la siguiente manera:

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

Nota

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

Ten en cuenta que en las versiones de HTTP anteriores a la 1.1 el servidor origen no estaba obligado a enviar la cabecera Date. En consecuencia, la memoria caché (por ejemplo el navegador) podría necesitar de contar en su reloj local para evaluar la cabecera Expires tomando el cálculo de la vida vulnerable para desviaciones del reloj. Otra limitación de la cabecera Expires es que la especificación establece que «Los servidores HTTP/1.1 no deben enviar fechas de más de un año en el futuro en Expires».

Caducidad con la cabecera Cache-Control

Debido a las limitaciones de la cabecera Expires, la mayor parte del tiempo, debes usar la cabecera Cache-Control en su lugar. Recordemos que la cabecera Cache-Control se utiliza para especificar muchas directivas de caché diferentes. Para 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 por las cachés compartidas:

// Establece el número de segundos después de que la
// respuesta ya no se debe considerar fresca
$response->setMaxAge(600);

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

La cabecera Cache-Control debería tener el siguiente formato (esta puede tener directivas adicionales):

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

Validando

Cuando un recurso se tiene que actualizar tan pronto como se realiza un cambio en los datos subyacentes, el modelo de caducidad se queda corto. Con el modelo de caducidad, no se pedirá a la aplicación que devuelva la respuesta actualizada hasta que la caché finalmente se convierta en obsoleta.

El modelo de validación soluciona este problema. Bajo este modelo, la memoria caché sigue almacenando las respuestas. La diferencia es que, por cada petición, la caché pregunta a la aplicación cuando o no la respuesta memorizada sigue siendo válida. Si la caché todavía es válida, tu aplicación debe devolver un código de estado 304 y no el contenido. Esto le dice a la caché que está bien devolver la respuesta memorizada.

Bajo este modelo, sobre todo ahorras ancho de banda ya que la representación no se envía dos veces al mismo cliente (en su lugar se envía una respuesta 304). Pero si diseñas cuidadosamente tu aplicación, es posible que puedas obtener los datos mínimos necesarios para enviar una respuesta 304 y ahorrar CPU también (más abajo puedes ver una implementación de ejemplo).

Truco

El código de estado 304 significa «No Modificado». Es importante porque este código de estado no tiene el contenido real solicitado. En cambio, la respuesta simplemente es un ligero conjunto de instrucciones que indican a la caché que se debe utilizar la versión almacenada.

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

Validando con la cabecera ETag

La cabecera ETag es una cabecera de cadena (llamada «entidad-etiqueta») que identifica unívocamente una representación del recurso destino. Este es generado completamente y establecido por tu aplicación de modo que puedes decir, por ejemplo, si el recurso memorizado /sobre está al día con el que tu aplicación iba a devolver. 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 representaciones de un mismo recurso.

Para ver una sencilla implementación, genera la ETag como el md5 del contenido:

public function indexAction()
{
    $response = $this->render('MyBundle:Main:index.html.twig');
    $response->setETag(md5($response->getContent()));
    $response->setPublic(); // verifica que la respuesta es púbica/susceptible de guardar en caché
    $response->isNotModified($this->getRequest());

    return $response;
}

El método isNotModified() compara la ETag enviada en la Petición con la configurada en la Respuesta. Si ambas coinciden, el método automáticamente establece el código de estado de la Respuesta a 304.

Este algoritmo es bastante simple y muy genérico, pero es necesario crear la Respuesta completa antes de ser capaz de calcular la ETag, lo cual es subóptimo. En otras palabras, esta ahorra ancho de banda, pero no ciclos de la CPU.

En la sección Optimizando tu código con validación, te mostraré cómo puedes utilizar la validación de manera más inteligente para determinar la validez de una caché sin hacer tanto trabajo.

Truco

Symfony2 también apoya ETags débiles pasando true como segundo argumento del método setETag().

Validando con la cabecera Last-Modified

La cabecera Last-Modified es la segunda forma de validación. De acuerdo con la especificación HTTP, «El campo de la cabecera Last-Modified indica la fecha y hora en que el servidor origen considera que la representación fue modificada por última vez». En otras palabras, la aplicación decide si o no el contenido memorizado se ha actualizado en función de sí o no se ha actualizado desde que la respuesta entró en caché.

Por ejemplo, puedes utilizar la última fecha de actualización de todos los objetos necesarios para calcular la representación del recurso como valor para el valor de la cabecera Last-Modified:

public function showAction($articleSlug)
{
    // ...

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

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

    $response->setLastModified($date);
    // Ajusta la respuesta como pública. De lo contrario será privada por omisión.
    $response->setPublic();

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

    // ... hace más trabajo para poblar la respuesta con el contenido completo

    return $response;
}

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

Nota

La cabecera If-Modified-Since de la petición es igual a la cabecera Last-Modified de la última respuesta enviada al cliente por ese recurso en particular. Así es como se comunican el cliente y el servidor entre ellos y deciden si el recurso se ha actualizado desde que se memorizó.

Optimizando tu código con validación

El objetivo principal de cualquier estrategia de memoria caché es aligerar la carga de la aplicación. Dicho de otra manera, cuanto menos hagas en tu aplicación para devolver una respuesta 304, mejor. El método Response::isNotModified() hace exactamente eso al exponer un patrón simple y eficiente:

use Symfony\Component\HttpFoundation\Response;

public function showAction($articleSlug)
{
    // Obtiene la mínima información para calcular la ETag
    // o el valor de Last-Modified (basado en la petición,
    // los datos se recuperan de una base de datos o un par
    // clave-valor guardado, por ejemplo)
    $article = ...;

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

    // Ajusta la respuesta como pública. De lo contrario será privada por omisión.
    $response->setPublic();

    // verifica que la respuesta no se ha modificado para la petición dada
    if ($response->isNotModified($this->getRequest())) {
        // devuelve la instancia de la aplicación
        return $response;
    } else {
        // aquí haz algo más - como recuperar más datos
        $comments = ...;

        // o reproduce una plantilla con la $response que acabas de iniciar
        return $this->render(
            'MyBundle:MyController:article.html.twig',
            array('article' => $article, 'comments' => $comments),
            $response
        );
    }
}

Cuando la Respuesta no es modificada, el isNotModified() automáticamente fija el código de estado de la respuesta a 304, remueve el contenido, y remueve algunas cabeceras que no deben estar presentes en respuestas 304 (consulta setNotModified()).

Variando la respuesta

Hasta ahora, asumiste que cada URI tiene exactamente una representación del recurso destino. De forma predeterminada, la caché HTTP se memoriza usando la URI del recurso como la clave de la caché. Si dos personas solicitan la misma URI de un recurso memorizable, la segunda persona recibirá la versión en caché.

A veces esto no es suficiente y se necesita memorizar en caché diferentes versiones de la misma URI basándose en uno o más valores de las cabeceras de la petición. Por ejemplo, si comprimes las páginas cuando el cliente lo permite, cualquier URI tiene dos representaciones: una cuando el cliente es compatible con la compresión, y otra cuando no. Esta determinación se hace por el valor de la cabecera Accept-Encoding de la petición.

En este caso, necesitas que la memoria almacene una versión comprimida y otra sin comprimir de la respuesta para la URI particular y devolverlas basándose en el valor de la cabecera Accept-Encoding. Esto se hace usando la cabecera Vary de la respuesta, la cual es una lista separada por comas de diferentes cabeceras cuyos valores lanzan una diferente representación de los recursos solicitados:

Vary: Accept-Encoding, User-Agent

Truco

Esta cabecera Vary particular debería memorizar diferentes versiones de cada recurso en base a la URI y el valor de las cabeceras Accept-Encoding y User-Agent de la petición.

El objeto Respuesta ofrece una interfaz limpia 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() toma un nombre de cabecera o un arreglo de nombres de cabecera de cual respuesta varía.

Caducidad y validación

Por supuesto, puedes utilizar tanto la caducidad como la validación de la misma Respuesta. La caducidad gana a la validación, te puedes beneficiar de lo mejor de ambos mundos. En otras palabras, utilizando tanto la caducidad como la validación, puedes instruir a la caché para que sirva el contenido memorizado, mientras que revisas de nuevo algún intervalo (de caducidad) para verificar que el contenido sigue siendo válido.

Más métodos de respuesta

La clase Respuesta proporciona muchos métodos más relacionados con la caché. Estos son los más útiles:

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

// Fuerza a la respuesta a devolver una adecuada respuesta 304 sin contenido
$response->setNotModified();

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

// Establece la configuración de caché en una llamada
$response->setCache(array(
    'etag'          => $etag,
    'last_modified' => $date,
    'max_age'       => 10,
    's_maxage'      => 10,
    'public'        => true,
    // 'private'    => true,
));

Usando inclusión del borde lateral

Las pasarelas de caché son una excelente forma de hacer que tu sitio web tenga un mejor desempeño. Pero tienen una limitación: sólo pueden memorizar páginas enteras. Si no puedes memorizar todas las páginas o si partes de una página tienen «más» elementos dinámicos, se te acabó la suerte. Afortunadamente, Symfony2 ofrece una solución para estos casos, basada ​​en una tecnología llamada ESI, o Inclusión de bordes laterales («Edge Side Includes»). Akamaï escribió esta especificación hace casi 10 años, y esta permite que partes específicas de una página tengan una estrategia de memorización diferente a la de la página principal.

La especificación ESI describe las etiquetas que puedes incrustar en tus páginas para comunicarte con la pasarela de caché. Symfony2 sólo implementa una etiqueta, include, ya que es la única útil fuera del contexto de Akamaï:

<!DOCTYPE html>
    <html>
        <body>
        <!-- ... algún contenido -->

        <!-- Aquí incluye el contenido de otra página -->
        <esi:include src="http://..." />

        <!-- ... más contenido -->
    </body>
</html>

Nota

Observa que en el ejemplo cada etiqueta ESI tiene una URL completamente cualificada. Una etiqueta ESI representa un fragmento de página que se puede recuperar a través de la URL.

Cuando se maneja una petición, la pasarela de caché obtiene toda la página de su caché o la pide a partir de la interfaz de administración de tu aplicación. Si la respuesta contiene una o más etiquetas ESI, estas se procesan de la misma manera. En otras palabras, la pasarela caché o bien, recupera el fragmento de página incluida en su caché o de nuevo pide el fragmento de página desde la interfaz de administración de tu aplicación. Cuando se han resuelto todas las etiquetas ESI, la pasarela caché une cada una en la página principal y envía el contenido final al cliente.

Todo esto sucede de forma transparente a nivel de la pasarela caché (es decir, fuera de tu aplicación). Como verás, si decides tomar ventaja de las etiquetas ESI, Symfony2 hace que el proceso de incluirlas sea casi sin esfuerzo.

Usando ESI en Symfony2

Primero, para usar ESI, asegúrate de activarlo en la configuración de tu aplicación:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        esi: { enabled: true }
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config ...>
        <!-- ... -->
        <framework:esi enabled="true" />
    </framework:config>
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'esi'    => array('enabled' => true),
    ));
    

Ahora, supongamos que tenemos una página que es relativamente estática, salvo por un teletipo de noticias en la parte inferior del contenido. Con ESI, puedes memorizar el teletipo de noticias independientemente del resto de la página.

public function indexAction()
{
    $response = $this->render('MyBundle:MyController:index.html.twig');
    // Pone el tiempo compartido máximo - el cual además marca la respuesta como pública
    $response->setSharedMaxAge(600);

    return $response;
}

En este ejemplo, la caché de la página completa tiene un tiempo de vida de diez minutos. En seguida, incluirás el teletipo de noticias en la plantilla incorporando una acción. Esto se hace a través del ayudante render (Consulta Integrando controladores para más detalles).

Como el contenido integrado viene de otra página (o controlador en este caso), Symfony2 utiliza el ayudante render estándar para configurar las etiquetas ESI:

  • Twig
    {# puedes usar una referencia al controlador #}
    {{ render_esi(controller('...:news', { 'max': 5 })) }}
    
    {# ... o una URL #}
    {{ render_esi(url('latest_news', { 'max': 5 })) }}
    
  • PHP
    <?php echo $view['actions']->render(
        new ControllerReference('...:news', array('max' => 5)),
        array('renderer' => 'esi'))
    ?>
    
    <?php echo $view['actions']->render(
        $view['router']->generate('latest_news', array('max' => 5), true),
        array('renderer' => 'esi'),
    ) ?>
    

Al utilizar el esi reproducido (vía la función render_esi de Twig), le dices a Symfony2 que la acción se debería reproducir como una etiqueta ESI. Te podrías estar preguntando por qué querrías utilizar un ayudante en vez de sólo escribir la etiqueta ESI directamente. Esto se debe a que al utilizar un ayudante haces que tu aplicación trabaje incluso si no tienes instalada ninguna pasarela de caché.

Al utilizar la función render predefinida (o configurar la estrategia a inline), Symfony2 fusiona el contenido de la página incluida con la principal antes de enviar la respuesta al cliente. Pero si utilizas la estrategia esi (es decir, llamas a render_esi), y si Symfony2 detecta que está hablando con una pasarela de caché que cuenta con soporte ESI, genera una etiqueta que incluye ESI. Pero si no hay ninguna pasarela de caché o si no cuenta con soporte ESI, Symfony2 sólo fusionará el contenido de la página incluida dentro de la principal tal como lo haría si hubieras utilizado render.

Nota

Symfony2 detecta si una pasarela caché admite ESI a través de otra especificación Akamaï que fuera de la caja es compatible con el delegado inverso de Symfony2.

La acción integrada ahora puede especificar sus propias reglas de caché, totalmente independientes de la página principal.

public function newsAction($max)
{
    // ...

    $response->setSharedMaxAge(60);
}

Con ESI, la caché de la página completa será válida durante 600 segundos, pero la caché del componente de noticias sólo dura 60 segundos.

Cuándo utilizas una referencia al controlador, la etiqueta ESI se debe referir a la acción incorporada como una URL accesible a modo de que la pasarela de caché la pueda recuperar independientemente del resto de la página. Symfony2 cuida de generar una URL única para cualquier referencia de controlador y es capaz de enrutarla correctamente gracias a un escucha que debes habilitar en tu configuración:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        fragments: { path: /_fragment }
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <framework:fragments path="/_fragment" />
    </framework:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'fragments' => array('path' => '/_fragment'),
    ));
    

Una gran ventaja de estrategia ESI es que puedes hacer tu aplicación tan dinámica como sea necesario y al mismo tiempo, tocar la aplicación lo menos posible.

Truco

El escucha sólo responde a direcciones IP locales o a delegados inversos confiables.

Nota

Una vez que comiences a usar ESI, recuerda usar siempre la directiva s-maxage en lugar de max-age. Como el navegador nunca recibe recursos agregados, no es consciente del subcomponente, y por lo tanto obedecerá la directiva max-age y memorizará la página completa. Y no quieres eso.

El ayudante render_esi apoya otras dos útiles opciones:

  • alt: utilizada como el atributo alt en la etiqueta ESI, el cual te permite especificar una URL alternativa para utilizarla si no se puede encontrar src;
  • ignore_errors: si la fijas a true, se agrega un atributo onerror a la ESI con un valor de continue indicando que, en caso de una falla, la pasarela caché simplemente debe eliminar la etiqueta ESI silenciosamente.

Invalidando la caché

«Sólo hay dos cosas difíciles en Ciencias de la Computación: Invalidación de caché y nombrar cosas» –Phil Karlton

Nunca debería ser necesario invalidar los datos memorizados en caché porque la invalidación ya se tiene en cuenta de forma nativa en los modelos de caché HTTP. Si utilizas la validación, por definición, no será necesario invalidar ninguna cosa; y si utilizas la caducidad y necesitas invalidar un recurso, significa que estableciste la fecha de caducidad muy adelante en el futuro.

Nota

Debido a que la invalidación es un tema específico de cada tipo de delegado inverso, si no te preocupa la invalidación, puedes cambiar entre los delegados inversos sin cambiar nada en el código de tu aplicación.

En realidad, todos los delegados inversas proporcionan una manera de purgar datos almacenados en caché, pero lo debes evitar tanto como sea posible. La forma más habitual es purgar la caché de una URL dada solicitándola con el método especial PURGE de HTTP.

Aquí está cómo puedes configurar la caché del delegado inverso de Symfony2 para apoyar el método PURGE de HTTP:

// app/AppCache.php

// ...
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class AppCache extends HttpCache
{
    protected function invalidate(Request $request, $catch = false)
    {
        if ('PURGE' !== $request->getMethod()) {
            return parent::invalidate($request, $catch);
        }

        $response = new Response();
        if (!$this->getStore()->purge($request->getUri())) {
            $response->setStatusCode(404, 'Not purged');
        } else {
            $response->setStatusCode(200, 'Purged');
        }

        return $response;
    }
}

Prudencia

De alguna manera, debes proteger el método PURGE de HTTP para evitar que alguien aleatoriamente purgue los datos memorizados.

Resumen

Symfony2 fue diseñado para seguir las reglas probadas de la carretera: HTTP. El almacenamiento en caché no es una excepción. Dominar el sistema caché de Symfony2 significa familiarizarse con los modelos de caché HTTP y usarlos eficientemente. Esto significa que, en lugar de confiar sólo en la documentación de Symfony2 y ejemplos de código, tienes acceso a un mundo de conocimientos relacionados con la memorización en caché HTTP y la pasarela caché, tal como Varnish.

Bifúrcame en GitHub