Main Image
Bild
Cookie
15. Juli 2022

Kontrolle des Drupal-Seiten-Caches mit Cookies

by Jürgen Haas

Ein LakeDrops-Kunde führt eine Kampagne durch, bei der ein einzelner Block mit einer Ansicht nur für eingeladene, noch anonyme Benutzer sichtbar sein soll. Dieser Block ist in eine Landing Page eingebettet, die für alle Besucher zugänglich bleibt. Aufgrund des hohen Verkehrsaufkommens ist es keine Option, den Seitencache von abzuschalten. Wie könnte man das also bewerkstelligen? Das zu lösende Problem: Sobald die Landing Page das erste Mal besucht wird, wird dieser Inhalt zwischengespeichert und allen nachfolgenden anonymen Besuchern unverändert zur Verfügung gestellt.

Um dieses Problem zu lösen, haben wir einen 3-Schritt-Ansatz gewählt:

  1. Ein Cookie für eingeladene Benutzer setzen
  2. Zugriff auf die Ansicht über dieses Cookie steuern
  3. Ausschalten des Seitencaches auf der Landing Page nur für die Besucher, die das Cookie gesetzt haben

Das ist gar nicht so schwierig, die größte Herausforderung liegt darin, herauszufinden, wie man das machen kann. Gehen wir die notwendigen Schritte durch.

Ein Cookie für eingeladene Benutzer setzen

Angenommen, die Landing Page ist unter /unsere-produkte verfügbar, und wir erstellen einen weiteren Pfad für die Kampagne als /sie-sind-eingeladen-für-besondere-angebote?token=SOMEHASH, der an alle eingeladenen Kunden oder Interessenten gesendet wird. Sobald sie diese URL besuchen, wird ein Cookie gesetzt und der Besucher wird automatisch zur Produktliste weitergeleitet.

Um dies zu erreichen, müssen wir einen Controller definieren:

# Datei: mymodule.routing.yml
mymodule.campaign:
  Pfad: '/Sie-sind-eingeladen-für-besondere-Angebote'
  defaults:
    _title: 'Kampagnenname'
    Controller: '\Drupal\mymodule\Controller\Kampagne::build'
  Anforderungen:
    _custom_access: '\Drupal\mymodule\Controller\Kampagne::checkAccess'

Die Controller-Klasse muss dann die Zugriffskontrolle und die Build-Funktion implementieren:

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 Stunden');
  $response = new RedirectResponse(Url::fromUserInput('/unser-produkte')->toString());
  $response->headers->setCookie($cookie);
  return $response;
}

Die Zugriffskontrolle erlaubt nur den Zugriff, wenn die Anfrage ein Abfrageargument token enthält, alle anderen erhalten eine "access denied"-Antwort auf diese Route.

Die Build-Methode löscht zunächst den Seiten-Cache für diese Route, da wir jede Anfrage einzeln verarbeiten müssen. Anschließend wird ein Cookie erstellt, das den angegebenen Token-Wert enthält und nur 24 Stunden lang gültig ist. Die Weiterleitung bringt den Besucher zur Produktliste und das Cookie wird mit dieser Antwort verknüpft. Schritt 1 von 3 ist abgeschlossen.

Zugriffs-Plugin für Cookies anzeigen

Die Ansicht mit dem privilegierten Inhalt existiert bereits, so dass wir nur noch den Zugriff auf diese Ansicht mit dem Cookie steuern müssen. Dies kann mit einem Plugin geschehen:

<?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, das den Zugriff in Abhängigkeit von Cookies ermöglicht.
 *
 * @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) {
        TRUE zurückgeben;
      }
    }
    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'] = [
      '#Typ' => 'Textfeld',
      '#title' => $this->t('Name'),
      '#default_value' => $this->options['cookie_name'],
    ];
    $form['cookie_value'] = [
      '#Typ' => 'Textfeld',
      '#title' => $this->t('Wert'),
      '#default_value' => $this->options['cookie_value'],
      '#description' => $this->t('Leer lassen, wenn Sie nur das Vorhandensein des Cookies prüfen wollen.'),
    ];
  }

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

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

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

}

Dieser Code dürfte selbsterklärend sein, so dass wir auch Schritt 2 von 3 als abgeschlossen betrachten.

Seiten-Cache nur für Anfragen deaktivieren, die den Cookie enthalten

Dieser Schritt hat uns fast die Haare zu Berge stehen lassen, denn wir haben ihn in mehreren Anläufen ausprobiert: Pre-Render-Hooks funktionierten nicht, ebenso wenig wie Event Subscriber. Sie werden alle bei gecachten Seiten übersprungen und sind daher in diesem Zusammenhang nutzlos. Als nächstes haben wir die Implementierung einer eigenen StackMiddleware versucht, die vor der Page-Cache-Middleware von Symfony ausgelöst werden kann. Dieser Ansatz führte uns dann zum endgültigen Ansatz, der das Verhalten des Seitencaches anpasst: eine Seitencache-Request-Policy.

Nachdem wir uns über solche Policies informiert hatten, erwies sich die Implementierung als mehr als einfach. Definieren wir zunächst den Policy-Dienst in unserem benutzerdefinierten :

# Datei: mymodule.services.yml
services:
  mymodule.page_cache_request_policy.disallow_campaign_requests:
    class: Drupal\mymodule\PageCache\DisallowCampaignRequests
    public: false
    tags:
      - { name: page_cache_request_policy }

Die Serviceklasse wirkt dann tatsächlich minimalistisch:

<?php

namespace Drupal\mymodule\PageCache;

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

/**
 * Cache-Richtlinie für /our-products, wenn das Kampagnen-Cookie gesetzt wird.
 */
class DisallowCampaignRequests implements RequestPolicyInterface {

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

}

Ja, das war's, auch der letzte Schritt ist erledigt.

Abschluss

Es war nicht das erste Mal, dass uns der Seitencache von Drupal so beeindruckt hat. Er klinkt sich so früh in den http-Request ein, dass fast alles, was Drupal anbietet, einfach komplett ignoriert wird. Und deshalb ist die Antwort so schnell. Stellen Sie sich vor, der Cache-Inhalt wird im RAM gespeichert, z.B. mit Redis, das Verhalten einer solchen Drupal-Site kommt der Performance einer statischen Website nahe, nicht wahr?

Unser größtes Problem bei der Bereitstellung der Funktionalität für diese Kundenanforderung war nicht technischer Natur, sondern das mangelnde Wissen über die verfügbaren Optionen. Wenn wir es nach ein paar Stunden gelöst haben, sind wir begeistert - und das ist ein Grund, warum wir Drupal so sehr lieben!

Tags

Tools

Neuen Kommentar hinzufügen