Main Image
Imagen
Cookie
15. Julio 2022

Controlar la caché de la página de Drupal con cookies

by Jürgen Haas

Un cliente de LakeDrops ejecuta una campaña en la que un único bloque que contiene una vista sólo debe ser visible para los usuarios invitados, aún anónimos. Este bloque está incrustado en una página de aterrizaje que permanece accesible para todos los visitantes. Debido al alto tráfico, desactivar la caché de la página de no es una opción. Entonces, ¿cómo se podría hacer? El problema a resolver: una vez que la página de aterrizaje es visitada por primera vez, ese contenido será cacheado y entregado tal cual a todos los visitantes anónimos posteriores.

Para resolver este problema, hemos elegido un enfoque de 3 pasos:

  1. establecer una cookie para los usuarios invitados
  2. controlar el acceso a la vista mediante esa cookie
  3. desactivar la caché de la página en la página de aterrizaje sólo para aquellos visitantes con la cookie presente

Esto no es nada difícil, el reto más duradero está detrás de nosotros en averiguar cómo se podría hacer. Vamos a repasar los pasos necesarios.

Configurar una cookie para los usuarios invitados

Digamos que la página de aterrizaje está disponible desde /nuestros-productos, y creamos otra ruta para la campaña como /estas-invitado-para-ofertas-especiales?token=SOMEHASH que se enviará a todos los clientes o prospectos invitados. Una vez que visiten esa URL, se establecerá una cookie y el visitante será redirigido a la lista de productos automáticamente.

Para ello, tenemos que definir un controlador:

# Archivo: mymodule.routing.yml
mymodule.campaign:
  path: '/usted-esta-invitado-para-ofertas-especiales'
  defaults:
    _title: 'Nombre de la campaña'
    _controller: '\Drupal\mymodule\Controller\Campaign::build'
  requirements:
    _custom_access: '\Drupal\mymodule\Controller\Campaign::checkAccess'

La clase del controlador tiene entonces que implementar el control de acceso y la función de construcción:

public function checkAccess(): AccessResultInterface {
  if (($query = $this->request->getCurrentRequest()->query) && $query->get('token', FALSE)) {
    return AccessResult::allowed();
  }
  return AccessResult::forbidden();
}

public function build() {
  \Drupal::service('page_cache_kill_switch')->trigger();
  $token = $this->request->getCurrentRequest()->query->get('token');
  $cookie = new Cookie('mymodule_campaign_token', $token, '+24 horas');
  $response = new RedirectResponse(Url::fromUserInput('/nuestros-productos')->toString());
  $response->headers->setCookie($cookie);
  return $response;
}

El control de acceso sólo permite el acceso, si la petición contiene un argumento de consulta token, todos los demás recibirán una respuesta de "acceso denegado" a esa ruta.

El método build primero mata la caché de la página para esta ruta, ya que necesitamos procesar cada petición individualmente. A continuación, creamos una cookie que contiene el valor del token dado y es válida sólo durante 24 horas. La redirección lleva al visitante a la lista de productos y la cookie se asociará a esa respuesta. Paso 1 de 3 completado.

Principio de acceso a las cookies

La vista con el contenido privilegiado ya existe, por lo que sólo necesitamos controlar el acceso a esa vista con la cookie. Esto se puede hacer con un plugin:

<?php

namespace Drupal\mymodule\Plugin\views\access;

use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\views\Plugin\views\access\AccessPluginBase;
use Symfony\Component\Routing\Route;

/**
 * Access plugin that provides access depending on cookies.
 *
 * @ingroup views_access_plugins
 *
 * @ViewsAccess(
 *   id = "mymodule_cookie",
 *   title = @Translation("Cookie")
 * )
 */
class Cookie extends AccessPluginBase implements CacheableDependencyInterface {

  /**
   * {@inheritdoc}
   */
  protected $usesOptions = TRUE;

  /**
   * {@inheritdoc}
   */
  public function summaryTitle(): string {
    return $this->t('Cookie');
  }

  /**
   * {@inheritdoc}
   */
  public function access(AccountInterface $account): bool {
    $cookieName = $this->options['cookie_name'];
    $cookieValue = $this->options['cookie_value'];
    $request = \Drupal::service('request_stack');
    if (($cookies = $request->getCurrentRequest()->cookies) && $value = $cookies->get($cookieName, FALSE)) {
      if (empty($cookieValue) || $cookieValue === $value) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function alterRouteDefinition(Route $route): void {
    $route->setRequirement('_custom_access', '\Drupal\mymodule\Plugin\views\access\Cookie::access');
  }

  /**
   * {@inheritdoc}
   */
  protected function defineOptions(): array {
    $options = parent::defineOptions();
    $options['cookie_name'] = ['default' => ''];
    $options['cookie_value'] = ['default' => ''];

    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state): void {
    parent::buildOptionsForm($form, $form_state);
    $form['cookie_name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Name'),
      '#default_value' => $this->options['cookie_name'],
    ];
    $form['cookie_value'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Value'),
      '#default_value' => $this->options['cookie_value'],
      '#description' => $this->t('Leave empty if you only want to check the existance of the cookie.'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge(): int {
    return 0;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts(): array {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags(): array {
    return [];
  }

}

Este código podría explicarse por sí mismo, por lo que llamamos al paso 2 de 3 completado también.

Desactivar la caché de la página sólo para las peticiones que contengan la cookie

Este casi nos deja sin pelo, ya que estuvimos tirando de él mientras probábamos varios intentos diferentes: los hooks de pre-renderización no funcionaban, ni ningún suscriptor de eventos. Todos ellos se saltan las páginas en caché y, por tanto, son inútiles en este contexto. La siguiente parada, hemos probado la implementación de nuestro propio StackMiddleware que puede ser activado antes del middleware de caché de páginas por Symfony. Este enfoque nos llevó al enfoque final que adapta el comportamiento de la caché de páginas: una política de solicitud de caché de páginas.

Una vez que conocimos este tipo de políticas, la implementación resultó ser más que sencilla. En primer lugar, vamos a definir el servicio de políticas en nuestro personalizado:

# Archivo: mymodule.services.yml
servicios:
  mymodule.page_cache_request_policy.disallow_campaign_requests:
    class: Drupal\mymodule\PageCache\DisallowCampaignRequests
    public: false
    tags:
      - {nombre: page_cache_request_policy }

La clase de servicio entonces se siente realmente minimalista:

<?php

namespace Drupal\mymodule\PageCache;

use Drupal\Core\PageCache\RequestPolicyInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Cache policy for /our-products when the campaign cookie is being set.
 */
class DisallowCampaignRequests implements RequestPolicyInterface {

  /**
   * {@inheritdoc}
   */
  public function check(Request $request) {
    if ($request->cookies->get('mymodule_campaign_token', FALSE) && $request->getPathInfo() === '/our-products') {
      return static::DENY;
    }
  }

}

Sí, eso es todo, último paso completado también.

Conclusión

No ha sido la primera vez que la caché de páginas de Drupal nos impresiona hasta tal punto. Se engancha a la petición http tan pronto, que casi todo lo que proporciona Drupal lo ignora por completo. Y por lo tanto la respuesta es tan rápida. Imagínese, el contenido de la caché se almacena en la memoria RAM, por ejemplo, con Redis, el comportamiento de tal sitio de Drupal está cerca del rendimiento de un sitio web estático, ¿no?

Nuestro mayor problema para dar la funcionalidad a este requisito del cliente no ha sido técnico, ha sido el desconocimiento de las opciones disponibles. Haberlo resuelto después de un par de horas nos deja con una noción de emoción - ¡y esa es una de las razones por las que amamos tanto a Drupal!

Tags

Tools

Añadir nuevo comentario