Crear un token personalizado en Drupal 8

Los tokens representan un reemplazo en línea al evaluar o representar texto. En Drupal, el módulo Token se remonta a Drupal 4.xy todavía existe para Drupal 8 (y Drupal 9). Esta ha sido durante mucho tiempo la columna vertebral de las funciones potentes en Drupal, como los alias dinámicos configurables por el usuario con Pathauto , el control robusto de metaetiquetas con Metatag , permite a los editores aprovechar los tokens en los campos de área de texto/WYSIWYG a través del filtro de token y muchos otros módulos.

Fuera de la caja, existen muchos tokens para resolver una variedad de casos de uso comunes para ellos. Ocasionalmente, puede encontrar un requisito donde no existe tal token para facilitarlo. Afortunadamente, implementar tokens personalizados en Drupal es bastante sencillo.

En un proyecto en el que estoy trabajando, estamos migrando más de 100,000 piezas de contenido de un sistema heredado. Hemos hecho todo lo posible para crear patrones de alias de URL para cada uno de los 10 tipos de contenido, pero hay un caso que no se puede hacer desde la interfaz de usuario solo con tokens estándar. Con tanto contenido de un sistema anterior, las posibilidades de encontrar el mismo título más de una vez son realmente altas. Lo que sucede entonces, si está familiarizado con Pathauto, es que en el caso de un alias existente, se deduce agregando un número al final del mismo. Entonces, si tenía un alias de capítulo uno, y luego migro otro nodo Capítulo también titulado "Capítulo uno" (esto sucede mucho), entonces mi alias de ruta terminará siendo 'capitulo-uno-0' o 'capitulo-uno-1'.

Este es un ejemplo básico y no es realmente un título real, pero dada su arquitectura de contenido, algunos capítulos son subcapítulos de un capítulo, y todos los capítulos son parte de un tipo de nodo de libro. Las colisiones de alias ocurren bastante. El cliente expresó la capacidad de usar un campo diferente para el alias si existe, en lugar de usar el título del nodo. No querían números agregados a la URL.

Hay algunas maneras en que esto podría haberse resuelto. Una forma hubiera sido usar hook_pathauto_pattern_alter para inspeccionar y cambiar el patrón utilizado para crear un alias al guardar según ciertas condiciones. Este es un enfoque válido. Sin embargo, en el contexto del cliente, pensé que este comportamiento sería demasiado "mágico" y tal vez no se aclararía más adelante, ya que el proceso editorial comienza a pasar de la aplicación anterior a la nueva plataforma Drupal. En segundo lugar, eliminaría la capacidad de dictar el patrón de URL de la interfaz de usuario. En tercer lugar, proporcionar un nuevo token abre algunas funcionalidades para resolver la URL sin sacrificar el control de la interfaz de usuario, y hace que ese token se pueda usar en otros contextos, por ejemplo, desambiguar el título de los resultados de búsqueda, lo que afecta la salida de la metaetiqueta u otros usos posibles. A través de la definición del token, puedo proporcionar una descripción para los editores para que sepan exactamente qué hará ese token.

Así que creemos ese token. Cuando se usa, el token se reemplazará con uno de los dos valores posibles:

  • El valor en el campo "Título de visualización del capítulo"
  • Recurrir al título del nodo si este campo no tiene valor

Prueba de token

Primero podemos escribir una prueba para nuestro token. Nuestro módulo se llama " mymodule_content_tokens ". " mymodule_content_tokens_test " es un módulo dentro de ese módulo que se utiliza para proporcionar una definición de tipo de nodo Capítulo y una definición de campo "Título de visualización de capítulo". Habilitar esto asegura que el tipo de nodo y el campo existan cuando se ejecuta la prueba. El método de prueba crea dos nodos de capítulo, uno con un valor de campo de visualización de capítulo y otro sin él, prueba el resultado del token para ellos y luego elimina el valor del campo del segundo nodo y prueba ese resultado nuevamente:

<?php

namespace Drupal\Tests\mymodule_content_tokens\Kernel\Utility;

use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\node\Entity\Node;
use Drupal\Core\Utility\Token;

/**
 * Class ContentTokensTest
 *
 * @group mymodule_content_tokens
 * @package Drupal\Tests\mymodule_content_tokens\Kernel\Utility
 */
class ContentTokensTest extends EntityKernelTestBase {

  use NodeCreationTrait;

  /**
   * @var \Drupal\Core\Utility\Token $tokenService
   */
  protected $tokenService;

  /**
   * @var array $modules
   */
  public static $modules = [
    'token',
    'node',
    'mymodule_content_tokens',
    'mymodule_content_tokens_test',
  ];

  /**
   * {@inheritdoc}
   */
  public function setUp() {
    parent::setUp();

    $this->installEntitySchema('node');
    $this->installConfig(['node']);
    $this->installSchema('node', ['node_access']);
    $this->installConfig(['mymodule_content_tokens_test']);

    $this->tokenService = $this->container->get('token');
  }

  /**
   * Tests that the chapter title token returns what we expect.
   */
  public function testChapterDisplayTitleTokenValue() {
    /** @var \Drupal\node\Entity\Node $node1 */
    $node1 = $this->createNode(['title' => 'Test Chapter One', 'type' => 'chapter']);

    /** @var \Drupal\node\Entity\Node $node2 */
    $node2 = $this->createNode(['title' => 'Test Chapter Two', 'type' => 'chapter', 'field_chapter_display_title' => 'Title Override']);

    $this->assertEqual('Test Chapter One', $this->tokenService->replace('[mymodule:chapter_display_title]', ['node' => $node1]));
    $this->assertEqual('Title Override', $this->tokenService->replace('[mymodule:chapter_display_title]', ['node' => $node2]));

    // Should revert back to node title when field_chapter_display_title is empty.
    $node2->field_chapter_display_title = '';
    $node2->save();

    $this->assertEqual('Test Chapter Two', $this->tokenService->replace('[mymodule:chapter_display_title]', ['node' => $node2]));
  }

}

Bastante simple, y proporciona un camino claro para nuestra implementación.

El token

Para proporcionar un nuevo token (s), necesitamos usar dos ganchos para decirle a Drupal sobre ellos: hook_token_info y hook_tokens . El enlace de información nos permite definir uno o más tokens, así como un grupo para colocarlos:

<?php

declare(strict_types = 1);

use Drupal\Core\Render\BubbleableMetadata;

/**
 * Implements hook_token_info().
 */
function mymodule_content_tokens_token_info() : array {
  $info = [];

  $info['types']['mymodule'] = [
    'name' => t('MyModule Tokens'),
    'description' => t('Custom tokens to solve use-case problems for the our website.'),
  ];

  $info['tokens']['mymodule']['chapter_display_title'] = [
    'name' => 'Chapter Display Title',
    'description' => t('This token will return the chapter of a title either using the default node title, or the chapter_display_title field value on the Chapter. Useful for setting URL alias pattern.')
  ];

  return $info;
}

Esto mostrará el token en todas partes del sistema en el tipo de grupo "MyModule Tokens". Puede usar grupos existentes si lo desea, como Node. En mi caso, quería separarlos para que sean fáciles de encontrar. El token tiene un nombre descriptivo y una descripción que dice lo que hace, y que aparece en el navegador de tokens.

El segundo gancho,  hook_tokens , le dice a Drupal con qué reemplazar nuestro token cuando se encuentra:

/**
 * Implements hook_tokens().
 */
function mymodule_content_tokens_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) : array {
  $replacements = [];

  if ($type == 'mymodule') {
    foreach ($tokens as $name => $original) {
      switch ($name) {
        case 'chapter_display_title':
          $node = $data['node'];

          if ($node->hasField('field_chapter_display_title') && !$node->get('field_chapter_display_title')->isEmpty()) {
            $text = $node->field_chapter_display_title->value;
          }

          $replacements[$original] = $text ?? $node->getTitle();
          break;

        default:
          break;
      }
    }
  }

  return $replacements;
}

Tomamos el contexto de datos del nodo y verificamos si este objeto de nodo tiene el campo (una simple verificación en caso de que el token se haya utilizado por error para otro tipo de nodo). Si lo hace y tiene un valor, entonces lo usamos. Si no, por defecto es el título del nodo que siempre existirá. El token se reemplazará con el valor asignado a $ text. Si el nodo se actualiza y ese campo se establece en vacío, entonces el token simplemente devolverá el título del nodo. Esto hace que el token sea útil para un editor de sitios.

Ahora si ejecutamos nuestras pruebas:

phpunit -- --group=mymodule_content_tokens
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

Testing 
.                                                                   1 / 1 (100%)

Time: 20 seconds, Memory: 10.00MB

OK (1 test, 10 assertions)

Woohoo! Ahora somos libres de usar nuestro nuevo token, y hemos sentado las bases para agregar más tokens en el futuro si los necesitamos. Quizás se pregunte por qué solo estamos probando el resultado de reemplazo de token y no también verificando un alias generado. Después de todo, ese era nuestro principal requisito, ¿verdad? Por supuesto. Este uso principal de tokens en particular estará en Pathauto. Sin embargo, todo lo que realmente queremos probar es que el token se reemplaza de la manera que esperamos. Como es, sabemos que Pathauto va a funcionar. No necesitamos agregar pruebas y específicamente resultados de pruebas de generación de alias.