Nette Caching

La caché acelera su aplicación guardando datos cuya obtención resultó costosa en su momento, lo que permite acceder a ellos más rápido en el futuro. Veremos:

  • cómo usar la caché
  • cómo cambiar el almacenamiento
  • cómo invalidar correctamente la caché

Usar la caché en Nette es muy sencillo y, aun así, cubre necesidades de cacheo sofisticadas. Está diseñada para el rendimiento y una durabilidad del 100 %. Incluye adaptadores para los almacenamientos más habituales. Soporta la invalidación por etiquetas, la expiración por tiempo, la protección contra la estampida de caché y más.

Instalación

Descargue e instale el paquete con Composer:

composer require nette/caching

Uso básico

El elemento central para trabajar con la caché es el objeto Nette\Caching\Cache. Creamos una instancia suya y le pasamos al constructor un objeto de almacenamiento. Ese objeto de almacenamiento representa el lugar físico donde se guardarán los datos (base de datos, Memcached, archivos en disco, etc.). El objeto de almacenamiento lo obtiene normalmente mediante dependency injection pidiendo el tipo Nette\Caching\Storage. Lo esencial lo aprenderá en la sección Almacenamientos.

En la versión 3.0 la interfaz todavía llevaba el prefijo I, así que el nombre era Nette\Caching\IStorage. Además, las constantes de la clase Cache se escribían en mayúsculas, p. ej. Cache::EXPIRE en lugar de Cache::Expire.

En los ejemplos siguientes damos por hecho que tenemos un alias Cache y una instancia de almacenamiento en la variable $storage.

use Nette\Caching\Cache;

$storage = /* ... */; // instancia de Nette\Caching\Storage

La caché es, en esencia, un almacén clave-valor, es decir, leemos y escribimos los datos con claves, de forma parecida a los arrays asociativos. Las aplicaciones constan de varias partes independientes. Si todas usaran un único almacenamiento (imagine un solo directorio en el disco), tarde o temprano se producirían colisiones de claves. Nette Framework lo resuelve dividiendo el espacio de almacenamiento en espacios de nombres (conceptualmente, como subdirectorios). Cada parte de la aplicación trabaja entonces dentro de su propio espacio de nombres con un nombre único, lo que evita cualquier colisión.

Indique el nombre del espacio de nombres como segundo argumento del constructor de la clase Cache:

$cache = new Cache($storage, 'Full Html Pages');

Si hace falta, de una instancia existente puede derivar una caché nueva acotada a un subespacio de nombres con el método derive():

$subCache = $cache->derive('Images');

Ahora podemos usar el objeto $cache para leer de la caché y escribir en ella. Para ambas cosas sirve el método load(). El primer argumento es la clave y el segundo es un callback de PHP que se invoca si la clave no se encuentra en la caché. El callback genera el valor, lo devuelve, y el método load() lo guarda en la caché:

$value = $cache->load($key, function () use ($key) {
	$computedValue = /* ... */; // cálculo costoso
	return $computedValue;
});

Si se omite el segundo parámetro ($value = $cache->load($key)), load() devuelve null si el elemento no se encuentra en la caché.

Lo estupendo es que se pueden cachear todas las estructuras serializables, no solo cadenas. Lo mismo vale para las claves.

Para borrar un elemento de la caché, use el método remove():

$cache->remove($key);

También puede guardar un elemento en la caché con el método $cache->save($key, $data, ?array $dependencies = null). Pero, por lo general, es preferible el enfoque con load() mostrado arriba.

Memoización

La memoización consiste en cachear el resultado de la llamada a una función o método, de modo que la próxima vez que se llame con los mismos argumentos se devuelva el resultado cacheado en lugar de volver a calcularlo.

Los métodos y las funciones se pueden llamar de forma memoizada con call(callable $callback, ...$args):

$result = $cache->call('gethostbyaddr', $ip);

Así, la función gethostbyaddr() se llama solo una vez por cada argumento $ip único. Las llamadas siguientes con el mismo $ip devolverán el valor cacheado.

También es posible crear un envoltorio memoizado de un método o una función y llamarlo más tarde:

function factorial($num)
{
	return /* ... */;
}

$memoizedFactorial = $cache->wrap('factorial');

$result = $memoizedFactorial(5); // la primera vez lo calcula
$result = $memoizedFactorial(5); // la segunda vez lo devuelve de la caché

Expiración e invalidación

Al usar la caché hay que resolver la cuestión de cuándo dejan de ser válidos los datos guardados anteriormente. Nette Framework proporciona mecanismos para limitar la validez de los datos o para borrarlos explícitamente (en la terminología del framework, “invalidación”).

La validez de los datos se establece al guardarlos, normalmente con el tercer parámetro del método save(), p. ej.:

$cache->save($key, $value, [
	$cache::Expire => '20 minutes',
]);

Como alternativa se puede establecer con el parámetro $dependencies que se pasa por referencia al callback del método load(), p. ej.:

$value = $cache->load($key, function (&$dependencies) {
	$dependencies[Cache::Expire] = '20 minutes';
	return /* ... */;
});

O con el tercer parámetro del propio método load(), p. ej.:

$value = $cache->load($key, function () {
	return /* ... */;
}, [Cache::Expire => '20 minutes']);

En los ejemplos siguientes usaremos la segunda variante, con la variable $dependencies dentro del callback.

Expiración

La forma más sencilla de expiración es un límite de tiempo. Esto cachea los datos con una validez de 20 minutos:

// acepta también un número de segundos o una marca de tiempo UNIX
$dependencies[Cache::Expire] = '20 minutes';

Si quiere que el periodo de validez se prolongue con cada lectura (expiración deslizante), lo consigue así, pero tenga en cuenta que eso aumenta la sobrecarga de la caché:

$dependencies[Cache::Sliding] = true;

Una opción útil es hacer que los datos expiren cuando se modifique un archivo concreto o alguno de varios archivos. Es útil, por ejemplo, al cachear datos derivados de procesar esos archivos. Use rutas absolutas.

$dependencies[Cache::Files] = '/path/to/data.yaml';
// o
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];

Podemos hacer que un elemento de la caché expire cuando expire otro elemento concreto (o alguno de varios). Es útil al cachear, por ejemplo, una página HTML entera y sus fragmentos bajo claves distintas. Cuando cambia un fragmento, hay que invalidar toda la página. Si los fragmentos están guardados bajo claves como frag1 y frag2, use:

$dependencies[Cache::Items] = ['frag1', 'frag2'];

La expiración también se puede controlar con funciones propias o métodos estáticos. Se llaman en cada lectura para determinar si el elemento sigue siendo válido. Por ejemplo, podemos hacer que un elemento expire siempre que cambie la versión de PHP. Cree una función que compare la versión actual con un parámetro y, al guardar, añada a las dependencias un array con el formato [nombre de la función, ...argumentos]:

function checkPhpVersion($ver): bool
{
	return $ver === PHP_VERSION_ID;
}

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // expira cuando checkPhpVersion(...) === false
];

Naturalmente, todos estos criterios se pueden combinar. El elemento de la caché expira si al menos uno de los criterios deja de cumplirse.

$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';

Invalidación con etiquetas

Las etiquetas proporcionan un mecanismo de invalidación muy útil. A cada elemento guardado en la caché le podemos asignar una lista de etiquetas (cadenas arbitrarias). Por ejemplo, supongamos que tenemos una página HTML que muestra un artículo y sus comentarios, y que queremos cachear. Al guardarla indicamos las etiquetas pertinentes:

$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];

Pasemos ahora a la sección de administración. Ahí tenemos un formulario para editar los artículos. Junto con guardar el artículo en la base de datos, llamamos al método clean() para borrar los elementos cacheados según su etiqueta:

$cache->clean([
	$cache::Tags => ["article/$articleId"],
]);

Del mismo modo, al añadir un comentario nuevo (o al editarlo) tenemos que acordarnos de invalidar la etiqueta correspondiente:

$cache->clean([
	$cache::Tags => ["comments/$articleId"],
]);

¿Qué hemos conseguido? Que nuestra caché HTML se invalide (se borre) siempre que cambie el artículo asociado o sus comentarios. Al editar el artículo con ID = 10 se invalida la etiqueta article/10 y se borra la página HTML cacheada que lleva esa etiqueta. Lo mismo ocurre al añadir un comentario nuevo bajo el artículo correspondiente.

Las etiquetas requieren un Journal.

Invalidación por prioridad

A los distintos elementos de la caché les podemos asignar prioridades. Eso permite un borrado controlado, por ejemplo cuando la caché supera cierto límite de tamaño:

$dependencies[Cache::Priority] = 50;

Para borrar todos los elementos con una prioridad igual o menor que 100:

$cache->clean([
	$cache::Priority => 100,
]);

Las prioridades requieren el llamado Journal.

Vaciar la caché

El parámetro Cache::All lo borra todo:

$cache->clean([
	$cache::All => true,
]);

Lectura masiva

Para leer y escribir en la caché de forma masiva, use el método bulkLoad(). Pásele un array de claves y le devolverá un array con los valores correspondientes:

$values = $cache->bulkLoad($keys);

El método bulkLoad() funciona de forma parecida a load() y también acepta un segundo parámetro con un callback. Ese callback recibe la clave del elemento que se está generando:

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = /* ... */; // cálculo costoso
	return $computedValue;
});

Al revés, para escribir varios elementos a la vez use el método bulkSave(), que toma un array de pares clave => valor y, opcionalmente, las dependencias:

$cache->bulkSave([
	$key1 => $value1,
	$key2 => $value2,
], [Cache::Expire => '20 minutes']);

Uso con PSR-16

Para usar Nette Cache con una interfaz PSR-16 puede aprovechar PsrCacheAdapter. Permite una integración fluida entre Nette Cache y cualquier código o biblioteca que espere una implementación de caché compatible con PSR-16.

$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);

Ahora puede usar $psrCache como una caché PSR-16 estándar:

$psrCache->set('key', 'value', 3600); // guarda el valor durante 1 hora
$value = $psrCache->get('key', 'default');

El adaptador soporta todos los métodos definidos en PSR-16, incluidos getMultiple(), setMultiple() y deleteMultiple().

Cachear la salida

La salida se puede capturar y cachear de forma muy elegante:

if ($capture = $cache->capture($key)) {

	// echo ... imprime algunos datos

	$capture->end(); // guarda la salida en la caché
}

Si la salida ya está en la caché, el método capture() la imprime y devuelve null, así que el bloque de la condición if se salta. En caso contrario empieza a bufferizar la salida y devuelve un objeto $capture, que usa para guardar por fin los datos capturados en la caché con su método end().

En la versión 3.0 este método se llamaba $cache->start().

Cachear en Latte

Cachear en las plantillas de Latte es muy sencillo. Basta con envolver la parte de la plantilla que quiere cachear con las etiquetas {cache}...{/cache}. La caché se invalida automáticamente siempre que cambia el archivo fuente de la plantilla (incluidas las plantillas incluidas dentro del bloque cacheado). Las etiquetas {cache} se pueden anidar. Cuando se invalida un bloque anidado (p. ej. mediante una etiqueta), también se invalida su bloque padre.

Dentro de la etiqueta puede indicar las claves a las que se vinculará la entrada de la caché (aquí, la variable $id), establecer un tiempo de expiración y definir etiquetas de invalidación.

{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
	...
{/cache}

Todos estos parámetros son opcionales, así que no tiene que indicar la expiración, las etiquetas ni siquiera las claves.

El uso de la caché también se puede condicionar con if: el contenido se cacheará solo si se cumple la condición:

{cache $id, if: !$form->isSubmitted()}
	{$form}
{/cache}

Almacenamientos

Un almacenamiento es un objeto que representa el lugar físico donde se guardan los datos. Podemos usar una base de datos, un servidor Memcached o el almacenamiento más a mano: archivos en disco.

Almacenamiento Descripción
FileStorage Almacenamiento predeterminado, guarda la caché en archivos en disco.
MemcachedStorage Usa un servidor Memcached para guardar los datos.
MemoryStorage Los datos se guardan temporalmente en memoria (se pierden al terminar la petición).
SQLiteStorage Los datos se guardan en un archivo de base de datos SQLite.
DevNullStorage Los datos no se guardan realmente; útil para hacer pruebas.

El objeto de almacenamiento lo obtiene mediante dependency injection pidiendo el tipo Nette\Caching\Storage. De forma predeterminada, Nette proporciona un objeto FileStorage que guarda los datos en el subdirectorio cache dentro del directorio de archivos temporales.

Puede cambiar el almacenamiento predeterminado en la configuración:

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

FileStorage

Escribe las entradas de la caché en archivos en disco. El almacenamiento Nette\Caching\Storages\FileStorage está muy optimizado para el rendimiento y, sobre todo, garantiza la atomicidad plena de las operaciones. ¿Qué significa eso? Que al usar la caché no puede ocurrir que lea un archivo que otro hilo todavía no ha terminado de escribir, ni que alguien lo borre mientras lo está leyendo. Por tanto, usar este almacenamiento de caché es completamente seguro.

Este almacenamiento incluye además una función integrada importante que evita un aumento extremo del uso de CPU cuando la caché se vacía o todavía está “fría” (es decir, aún no creada). Se conoce como prevención de la estampida de caché. Ocurre cuando varias peticiones concurrentes piden a la vez el mismo elemento cacheado (p. ej. el resultado de una consulta SQL costosa). Si el elemento no está en la caché en ese momento, todos esos procesos podrían empezar a ejecutar la misma operación costosa (como la consulta SQL). Eso multiplica la carga del servidor, e incluso puede ocurrir que ningún hilo consiga responder dentro del límite de tiempo, la caché no llegue a crearse y la aplicación se caiga. Por suerte, la caché de Nette se ocupa de eso: cuando hay varias peticiones concurrentes del mismo elemento, solo el primer hilo lo genera. Los demás hilos esperan y usan después el resultado generado por el primero.

Ejemplo de creación de un FileStorage:

// el almacenamiento será el directorio '/path/to/temp' del disco
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

El servidor Memcached es un sistema distribuido de alto rendimiento para cachear objetos en memoria. Su adaptador en Nette es Nette\Caching\Storages\MemcachedStorage. En la configuración indique la dirección IP del servidor y el puerto si difiere del estándar 11211.

Requiere la extensión de PHP memcached.

services:
	cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')

MemoryStorage

Nette\Caching\Storages\MemoryStorage es un almacenamiento que mantiene los datos dentro de un array de PHP. Por consiguiente, los datos se pierden al terminar la petición.

SQLiteStorage

La base de datos SQLite, junto con el adaptador Nette\Caching\Storages\SQLiteStorage, proporciona una forma de cachear los datos dentro de un único archivo en disco. La configuración indica la ruta a ese archivo de base de datos.

Requiere las extensiones de PHP pdo y pdo_sqlite.

services:
	cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')

DevNullStorage

Una implementación especial de almacenamiento es Nette\Caching\Storages\DevNullStorage, que no guarda ningún dato. Por eso resulta adecuada para hacer pruebas cuando quiere eliminar los efectos del cacheo.

Usar la caché en el código

Al usar la caché en su código hay dos enfoques principales. El primero es obtener el objeto de almacenamiento mediante dependency injection y crear después usted mismo el objeto Cache:

use Nette;

class ClassOne
{
	private Nette\Caching\Cache $cache;

	public function __construct(Nette\Caching\Storage $storage)
	{
		$this->cache = new Nette\Caching\Cache($storage, 'my-namespace');
	}
}

La segunda opción es pedir directamente el objeto Cache:

class ClassTwo
{
	public function __construct(
		private Nette\Caching\Cache $cache,
	) {
	}
}

El objeto Cache hay que definirlo entonces en la configuración, por ejemplo así:

services:
	- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )

Journal

Nette guarda la información sobre las etiquetas y las prioridades en el llamado journal. De forma predeterminada, para ello se usa SQLite mediante el archivo journal.s3db, y se requieren las extensiones de PHP pdo y pdo_sqlite.

Puede cambiar la implementación del journal en la configuración:

services:
	cache.journal: MyJournal

Servicios DI

Estos servicios se añaden al contenedor DI:

Nombre Tipo Descripción
cache.journal Nette\Caching\Storages\Journal El almacenamiento del journal de la caché
cache.storage Nette\Caching\Storage El almacenamiento principal de la caché

Desactivar la caché

Una forma de desactivar el cacheo en su aplicación es establecer el almacenamiento a DevNullStorage:

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

Este ajuste no afecta al cacheo de las plantillas de Latte ni del contenedor DI, ya que esas bibliotecas no usan los servicios de nette/caching y gestionan sus cachés por su cuenta. Además, sus cachés normalmente no hace falta desactivarlas en modo de desarrollo.

Si está actualizando a una versión más reciente, vea la página de actualización.

versión: 3.x