Symfony 1.4, la guía definitiva

12.1. Guardando la respuesta en la cache

El principio básico de las caches de HTML es muy sencillo: parte o todo el código HTML que se envía al usuario como respuesta a su petición se puede reutilizar en peticiones similares. El código HTML se almacena en un directorio especial (el directorio cache/) donde el controlador frontal lo busca antes de ejecutar la acción. Si se encuentra el código en la cache, se envía sin ejecutar la acción, por lo que se consigue un gran ahorro de tiempo de ejecución. Si no se encuentra el código, se ejecuta la acción y su respuesta (la vista) se guarda en el directorio de la cache para las futuras peticiones.

Como todas las páginas pueden contener información dinámica, la cache HTML está deshabilitada por defecto. El administrador del sitio web debe activarla para mejorar el rendimiento de la aplicación.

Symfony permite gestionar tres tipos diferentes de cache HTML:

  • Cache de una acción (con o sin layout)
  • Cache de un elemento parcial o un componente
  • Cache de un trozo de plantilla

Los dos primeros tipos de cache se controlan mediante archivos YAML de configuración. La cache de trozos de plantillas se controla mediante llamadas a helpers dentro de las propias plantillas.

12.1.1. Opciones de la cache global

La cache HTML se puede habilitar y deshabilitar (su valor por defecto) para cada aplicación de un proyecto y para cada entorno mediante la opción cache del archivo settings.yml. El listado 12-1 muestra cómo activar el mecanismo de cache. Activarlo no significa que se empiecen a guardar elementos en la cache, para eso debes activar la cache en cada módulo/acción, como se explica más adelante.

Listado 12-1 - Activando la cache, en frontend/config/settings.yml

dev:
  .settings:
    cache:   true

12.1.2. Guardando una acción en la cache

Las acciones que muestran información estática (que no depende de bases de datos ni de información guardada en la sesión) y las acciones que leen información de una base de datos pero no la modifican (acciones típicas del método GET) son el tipo de acción ideal para almacenar su resultado en la cache. La figura 12-1 muestra los elementos de la página que se guardan en la cache en este caso: o el resultado de la acción (su plantilla) o el resultado de la acción junto con el layout.

Guardando una acción en la cache

Figura 12.1 Guardando una acción en la cache

Si se dispone por ejemplo de una acción usuario/listado que devuelve un listado de todos los usuarios de un sitio web, a no ser que se modifique, añada o elimine un usuario (que se verá más adelante en la sección "Eliminar elementos de la cache") la lista contiene siempre la misma información, por lo que esta acción es ideal para guardarla en la cache.

La activación de la cache y las opciones para cada acción se definen en el archivo cache.yml del directorio config/ del módulo. El listado 12-2 muestra un ejemplo de este archivo.

Listado 12-2 - Activando la cache de una acción, en frontend/modules/usuario/config/cache.yml

listado:
  enabled:     true
  with_layout: false   # Valor por defecto
  lifetime:    86400   # Valor por defecto

La anterior configuración activa la cache para la acción listado y el layout no se guarda junto con el resultado de la acción (que además, es el comportamiento por defecto). Por tanto, aunque exista en la cache el resultado de la acción, el layout completo (junto con sus elementos parciales y componentes) se sigue ejecutando. Si la opción with_layout vale true, en la cache se guarda el resultado de la acción junto con el layout, por lo que este último no se vuelve a ejecutar.

Para probar las opciones de la cache, se accede con el navegador a la acción en el entorno de desarrollo.

http://miaplicacion.ejemplo.com/frontend_dev.php/usuario/listado

Ahora se puede apreciar un borde que encierra la zona del área en la página. La primera vez, el área tiene una cabecera azul, lo que indica que no se ha obtenido de la cache. Si se recarga la página, el área de la acción muestra una cabecera amarilla, indicando que esta vez sí se ha obtenido directamente de la cache (resultando en una gran reducción en el tiempo de respuesta de la acción). Más adelante en este capítulo se detallan las formas de probar y monitorizar el funcionamiento de la cache.

Nota Los slots son parte de la plantilla, por lo que si se guarda el resultado de una acción en la cache, también se guarda el valor de los slots definidos en la plantilla de la acción. De esta forma, la cache funciona de forma nativa para los slots.

El sistema de cache también funciona para las páginas que utilizan parámetros. El módulo usuario anterior podría disponer de una acción llamada ver y a la que se pasa como parámetro una variable llamada id para poder mostrar los detalles de un usuario. El listado 12-3 muestra como modificar los cambios necesarios en el archivo cache.yml para habilitar la cache también en esta acción.

Se puede organizar de forma más clara el archivo cache.yml reagrupando las opciones comunes a todas las acciones del módulo bajo la clave all:, como también muestra el listado 12-3.

Listado 12-3 - Ejemplo de cache.yml completo, en frontend/modules/usuario/config/cache.yml

listado:
  enabled:    true
ver:
  enabled:    true

all:
  with_layout: false   # Valor por defecto
  lifetime:    86400   # Valor por defecto

Ahora, cada llamada a la acción usuario/ver que tenga un valor del parámetro id diferente, crea un nuevo archivo en la cache. De esta forma, la cache para la petición:

http://frontend.ejemplo.com/usuario/ver/id/12

es completamente diferente de la cache de la petición:

http://frontend.ejemplo.com/usuario/ver/id/25

Nota Las acciones que se ejecutan mediante el método POST o que tienen parámetros GET no se guardan en la cache.

La opción with_layout merece una explicación más detallada. Esta opción determina el tipo de información que se guarda en la cache. Si vale true, solo se almacenan en la cache el resultado de la ejecución de la plantilla y las variables de la acción. Si la opción vale false, se guarda el objeto response entero. Por tanto, la cache en la que se guarda el layout (valor true) es mucho más rápido que la cache sin el layout.

Si es posible, es decir, si el layout no depende por ejemplo de datos de sesión, es conveniente optar por la opción que guarda el layout en la cache. Desgraciadamente, el layout normalmente contiene elementos dinámicos (como por ejemplo el nombre del usuario que está conectado), por lo que la opción habitual es la de no almacenar el layout en la cache. No obstante, las páginas que no depende de cookies, los canales RSS, las ventanas emergentes, etc. se pueden guardar en la cache incluyendo su layout.

12.1.3. Guardando en la cache un elemento parcial o un componente

En el Capítulo 7 se explicó la forma de reutilizar trozos de código en varias plantillas mediante el helper include_partial(). Guardar un elemento parcial en la cache es tan sencillo como hacerlo en una acción y se activa de la misma forma, tal y como muestra la figura 12-2.

Guardando un elemento parcial o un componente en la cache

Figura 12.2 Guardando un elemento parcial o un componente en la cache

El listado 12-4 por ejemplo muestra los cambios necesarios en el archivo cache.yml para activar la cache en el elemento parcial _mi_parcial.php que pertenece al módulo usuario. La opción with_layout no tiene sentido en este caso.

Listado 12-4 - Guardando un elemento parcial en la cache, en frontend/modules/usuario/config/cache.yml

_mi_parcial:
  enabled:    true
listado:
  enabled:    true
...

Ahora todas las plantillas que incluyen este elemento parcial no ejecutan su código PHP, sino que utilizan la versión almacenada en la cache.

<?php include_partial('usuario/mi_parcial') ?>

Al igual que sucede en las acciones, la información que se guarda en la cache depende de los parámetros que se pasan al elemento parcial. El sistema de cache almacena tantas versiones diferentes como valores diferentes de parámetros se pasen al elemento parcial.

<?php include_partial('usuario/mi_otro_parcial', array('parametro' => 'valor')) ?>

Nota Guardar la acción en la cache es más avanzado que guardar elementos parciales, ya que cuando una acción se encuentra en la cache, la plantilla ni siquiera se ejecuta; si la plantilla incluye elementos parciales, no se realizan las llamadas a esos elementos parciales. Por tanto, guardar elementos parciales en la cache solo es útil cuando no se está guardando en la cache la acción que se ejecuta o para los elementos parciales incluidos en el layout.

Recordando lo que se explicó en el Capítulo 7: un componente es una pequeña acción que utiliza como vista un elemento parcial. Como tienen características similares a los elementos parciales, el funcionamiento de su cache es muy parecido. Si el layout global incluye un componente llamado dia mediante include_component('general/dia') para mostrar la fecha, el archivo cache.yml del módulo general debería activar la cache de ese componente de la siguiente forma:

_dia:
  enabled: true

Cuando se guarda un componente o un elemento parcial en la cache, se debe decidir si se almacena solo una versión para todas las plantillas o una versión para cada plantilla. Por defecto, los componentes se guardan independientemente de la plantilla que lo incluye. No obstante, los componentes contextuales, como por ejemplo los componentes que muestran una zona lateral diferente en cada acción, deben almacenarse tantas veces como el número de plantillas diferentes que los incluyan. El sistema de cache se encarga automáticamente de este último caso, siempre que se establezca el valor true a la opción contextual:

_dia:
  contextual: true
  enabled:    true

Nota Los componentes globales (los que se guardan en el directorio templates/ de la aplicación) también se pueden guardar en la cache, siempre que se configuren sus opciones de cache en el archivo cache.yml de la aplicación.

12.1.4. Guardando un fragmento de plantilla en la cache

Guardar en la cache el resultado completo de una acción solamente es posible para algunas acciones. Para el resto de acciones, las que actualizan información y las que muestran en la plantilla información que depende de la sesión, todavía es posible mejorar su rendimiento mediante la cache, pero de forma muy diferente. Symfony incluye un tercer tipo de cache, que se utiliza para los fragmentos de las plantillas y que se activa directamente en la propia plantilla, como se muestra en la figura 12-3.

Guardando un fragmento de plantilla en la cache

Figura 12.3 Guardando un fragmento de plantilla en la cache

Si por ejemplo se dispone de un listado de usuarios que muestra un enlace al último usuario que se ha accedido, esta última información es dinámica. El helper cache() define las partes de la plantilla que se pueden guardar en la cache. El listado 12-5 muestra los detalles sobre su sintaxis.

Listado 12-5 - Uso del helper cache(), en frontend/modules/usuario/templates/listadoSuccess.php

<!-- Código que se ejecuta cada vez -->
<?php echo link_to('Último usuario accedido', 'usuario/ver?id='.$id_ultimo_usuario_accedido) ?>

<!-- Código guardado en la cache -->
<?php if (!cache('usuarios')): ?>
  <?php foreach ($usuarios as $usuario): ?>
    <?php echo $usuario->getNombre() ?>
  <?php endforeach; ?>
  <?php cache_save() ?>
<?php endif; ?>

Así es como funciona esta cache:

  • Si se encuentra en la cache una versión del fragmento llamado 'usuarios', se utiliza para reemplazar todo el código existente entre <?php if (!cache('usuarios')): ?> y <?php endif; ?>.
  • Si no se encuentra, se ejecuta el código definido entre esas dos líneas y el resultado se guarda en la cache identificado con el nombre indicando en la llamada al helper cache().

Todo el código que no se incluye entre esas dos líneas, se ejecuta siempre y por tanto nunca se guarda en la cache.

Nota La acción (listado en este ejemplo) no puede tener activada la cache, ya que en ese caso, no se ejecutaría la plantilla y se ignoraría por completo la declaración de la cache de los fragmentos.

La mejora en la velocidad de la aplicación cuando se utiliza esta cache no es tan significativa como cuando se guarda en la cache la acción entera, ya que en este caso siempre se ejecuta la acción, la plantilla se procesa al menos de forma parcial y siempre se utiliza el layout para decorar la plantilla.

Se pueden guardar otros fragmentos de la misma plantilla en la cache; sin embargo, en este caso se debe indicar un nombre único a cada fragmento, de forma que el sistema de cache de Symfony pueda encontrarlos cuando sea necesario.

Como sucede con las acciones y los componentes, los fragmentos que se guardan en la cache pueden tener definido un tiempo de vida en segundos como segundo argumento de la llamada al helper cache().

<?php if (!cache('usuarios', 43200)): ?>

Si no se indica explícitamente en el helper, se utiliza el valor por defecto para el tiempo de vida de la cache (que son 86400 segundos, equivalentes a 1 día).

Nota Otra forma de hacer que una acción se pueda guardar en la cache es pasar las variables que modifican su comportamiento en el patrón del sistema de enrutamiento de la acción. Si la página principal muestra el nombre del usuario que está conectado, no se puede cachear la página a menos que la URL contenga el nombre del usuario. Otro caso es el de las aplicaciones multi-idioma: si se quiete activar la cache para una página que tiene varias traducciones, el código del idioma debería incluirse dentro del patrón de la URL. Aunque este truco aumenta el número de páginas que se guardan en la cache, puede ser muy útil para acelerar las aplicaciones que son muy interactivas.

12.1.5. Configuración dinámica de la cache

El archivo cache.yml es uno de los métodos disponibles para definir las opciones de la cache, pero tiene el inconveniente de que no se puede modificar de forma dinámica. No obstante, como sucede habitualmente en Symfony, se puede utilizar código PHP en vez de archivos YAML, por lo que se puede configurar de forma dinámica la cache.

¿Para qué puede ser útil modificar dinámicamente las opciones de la cache? Un ejemplo práctico puede ser el de una página que es diferente para los usuarios autenticados y para los usuarios anónimos, aunque la URL sea la misma. Si se dispone de una página creada por la acción articulo/ver y que contiene un sistema de puntuación para los artículos, el sistema de puntuación podría estar deshabilitado para los usuarios anónimos. Para este tipo de usuarios, se muestra el formulario para registrarse cuando pinchan en el sistema de puntuación. Esta versión de la página se puede guardar tranquilamente en la cache. Por otra parte, los usuarios autenticados que pinchan sobre el sistema de puntuación, generan una petición POST que se emplea para calcular la nueva puntuación del artículo. En esta ocasión, la cache se debería deshabilitar para que Symfony cree la página de forma dinámica.

El sitio adecuado para definir las opciones dinámicas de la cache es en un filtro que se ejecute antes de sfCacheFilter. De hecho, todo el sistema de cache es un filtro de Symfony, como también lo son las opciones de seguridad. Para habilitar la cache en la acción articulo/ver solo cuando el usuario no está autenticado, se crea el archivo conditionalCacheFilter en el directorio lib/ de la aplicación, tal y como se muestra en el listado 12-6.

Listado 12-6 - Configurando la cache mediante PHP, en frontend/lib/conditionalCacheFilter.class.php

class conditionalCacheFilter extends sfFilter
{
  public function execute($filterChain)
  {
    $contexto = $this->getContext();
    if (!$contexto->getUser()->isAuthenticated())
    {
      foreach ($this->getParameter('pages') as $pagina)
      {
        $contexto->getViewCacheManager()->addCache($pagina['module'], $pagina['action'], array('lifeTime' => 86400));
      }
    }

    // Ejecutar el siguiente filtro
    $filterChain->execute();
  }
}

Este filtro se debe registrar en el archivo filters.yml antes de sfCacheFilter, como se muestra en el listado 12-7.

Listado 12-7 - Registrando un filtro propio, en frontend/config/filters.yml

...
security: ~

conditionalCache:
  class: conditionalCacheFilter
  param:
    pages:
      - { module: articulo, action: ver }

cache: ~
...

Para que la cache condicional pueda utilizarse, solo es necesario borrar la cache de Symfony para que se autocargue la clase del nuevo filtro. La cache solo se habilitará para las páginas definidas en el parámetro pages y solo para los usuarios que no están autenticados.

El método addCache() del objeto sfViewCacheManager requiere como parámetros el nombre de un módulo, el nombre de una acción y un array asociativo con las mismas opciones que se definen en el archivo cache.yml. Si por ejemplo se necesita guardar en la cache la acción articulo/ver con el layout y con un tiempo de vida de 300 segundos, se puede utilizar el siguiente código:

$contexto->getViewCacheManager()->addCache('articulo', 'ver', array(
  'withLayout' => true,
  'lifeTime'   => 3600,
));

12.1.6. Uso de la cache super rápida

Todas las páginas guardadas en la cache que se han explicado anteriormente implican la ejecución de algo de código PHP. En este tipo de páginas, Symfony carga toda la configuración, crea la respuesta, etc. Si se está completamente seguro de que una página no va a cambiar durante un periodo de tiempo, se puede saltar completamente Symfony si se guarda en la carpeta web/ el código HTML completo de la página. Este funcionamiento es posible gracias a las opciones del módulo mod_rewrite de Apache, siempre que la regla de enrutamiento defina un patrón que no termine en ningún sufijo o en .html.

Para guardar las páginas completas en la cache, se puede acceder manualmente a todas las páginas mediante la siguiente instrucción ejecutada en la línea de comandos:

$ curl http://frontend.ejemplo.com/usuario/listado.html > web/usuario/listado.html

Una vez ejecutado el anterior comando, cada vez que se realice una petición a la acción usuario/listado, Apache encuentra la página listado.html y la sirve directamente sin llegar a ejecutar Symfony. Aunque la desventaja es que no se puede controlar mediante Symfony las opciones de esa cache (tiempo de vida, borrado automático, etc.) la gran ventaja es el increíble aumento del rendimiento de la aplicación.

Una forma más cómoda de generar estas páginas estáticas es la de utilizar el plugin sfSuperCache, que automatiza todo el proceso, permite definir el tiempo de vida de la cache e incluso permite el borrado de las páginas guardadas en la cache. El Capítulo 17 incluye más información sobre los plugins.