Drupal 8: Creación de campos con Field API

Drupal 8: Creación de campos con Field API

Drupal 8: Creación de campos con Field API

  • Autor: fjavimartin

  • Fecha de Creación: 22/06/2017

  • Categorías:

    • Drupal,
    • Drupal 8,
    • Field api,
    • Widget,
    • Formatter,
    • Drupal console

Nuestro objetivo en este artículo será la creación de un campo con field api de drupal 8 que nos sirva para almacenar una dirección. Utilizaremos todo lo aprendido en anteriores artículos con nuestro formulario básico.

Mediante field API podremos crear entidades que nos permitirán añadir campos extra a la entidades de drupal 8 que lo permitan. Los campos se definen a partir de plugins y básicamente tienen los siguientes componentes:

  • Definición del campo mediante anotaciones y con una clase que extiende de Drupal\Core\Field\FieldItemBase
  • Esquema de configuración del campo donde especificamos los campos que tendrá en la base de datos.
  • Uno o varios formularios (widget) que definirán el formulario de alta y modificación del campo.
  • Uno o varios formateadores (formatter) que definirán como se mostrará el contenido del campo.

En este artículo crearemos un módulo nuevo (address_field) en el que definiremos un campo nuevo que nos permitirá almacenar una dirección junto su latitud y longitud.

1. Creación de nuestro módulo

Ya hemos creado anteriormente otros módulos y no daremos demasiadas explicaciones al respecto. Crearemos una carpeta address_field dentro del directorio modules/custom e incluiremos el fichero address_field.info.yml:

name: Address Field
type: module
description: 'Practicando con campos'
package: Forcontu
core: 8.x

2. Generación del código inicial

El código de drupal 8 muchas veces es complejo y, por lo menos a mi, me resulta imposible recordar todas las clases/métodos/parámetros que tenemos que implementar como mínimo para que todo funcione por lo que utilizaremos drupal console para la generación de código. Para el caso en el que no tengas instalada esta herramienta.......que no cunda el pánico, en este enlace tenéis un tutorial sobre su instalación.

Antes de generar nada tendremos que tener bien claros los nombres de las clases, campos en la base de datos, tamaño/tipo de los mismo e identificador/descripción para el campo/widget/formatter:

  • Base de datos: Definiremos los campos address/latitude/logitude. El primero de ellos será de tipo “varchar(255)” y los dos siguientes serán de tipo “float” con un tamaño “normal”.
  • Definición campos: Necesitamos asignar un identificador tanto para nuestro field como para nuestro widget y formatter. En la siguiente lista veremos en primer lugar el nombre de la clase y después la descripción del mismo.
    • AddressField → field_address/”Field Address”
    • AddressDefaultWidget → field_address_default_widget/”Default Widget for Address Field”
    • AddressTextFormatter → field_address_text_formatter/”Text Formatter”

Una vez que tenemos claro los nombres de todo ejecutamos drupal console para que nos genere el código inicial del field:

$ drupal generate:plugin:field

Mediante una interface de texto tendremos que ir respondiendo a sus preguntas que se resumirán en el nombre del módulo (address_field) y los identificadores/descripciones para el field/widget/formatter.

Dentro de nuestro módulo se habrá generado una carpeta src/Plugin/Field con los directorios FieldType/FieldWidget/FieldFormatter donde se almacenaran los ficheros php con el nombre de las clases que hemos especificado al ejecutar el comando de drupal console.

3. AddressField.php: Implementación del tipo de field

En este fichero definiremos tres cosas principales:

  • Almacenamiento en la base de datos
  • Función que determina si el campo está vacío
  • Función en la definimos las propiedades del campo

Partiendo desde el código generado lo primero que veremos será un bloque annotation en el que veremos los valores introducidos durante la creación del mismo con drupal console:

/**
 * Plugin implementation of the 'field_address' field type.
 *
 * @FieldType(
 *   id = "field_address",
 *   label = @Translation("Field Address"),
 *   description = @Translation("Field Address"),
 *   default_widget = "field_address_default_widget",
 *   default_formatter = "field_address_text_formatter"
 * )
 */

El primer método que reescribiremos será “schema” donde personalizaremos los campos que vamos a necesitar alojar en la base de datos para guardar nuestras direcciones:

public static function schema(FieldStorageDefinitionInterface $field_definition) {
    $schema = [
        'columns' => [
            'address' => [
                'type' => 'varchar',
                'length' => '255',
                'not null' => TRUE,
            ],
            'latitude' => [
                'type' => 'float',
                'size' => 'normal',
                'description' => 'Address Latitude'
            ],
            'longitude' => [
                'type' => 'float',
                'size' => 'normal',
                'description' => 'Address Longitude'
            ],
        ],
    ];
  
    return $schema;
  }

Redefiniremos la función que determinará si nuestro campo está vacío:

public function isEmpty() {
    $value = $this->get('address')->getValue();
    return $value === NULL || $value === '';
  }

Por último definiremos las propiedades de nuestro campo mediante la función “propertyDefinitions”:

public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    // Prevent early t() calls by using the TranslatableMarkup.
    $properties['address'] = DataDefinition::create('string')
    ->setLabel(t('Address name'));
    
    $properties['latitude'] = DataDefinition::create('float')
    ->setLabel(t('Address Latitude'));
    
    $properties['longitude'] = DataDefinition::create('float')
    ->setLabel(t('Address Longitude'));
  
    return $properties;
  }

El código de la clase completa se parecerá al siguiente:

namespace Drupal\address_field\Plugin\Field\FieldType;

use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;

/**
 * Plugin implementation of the 'field_address' field type.
 *
 * @FieldType(
 *   id = "field_address",
 *   label = @Translation("Field Address"),
 *   description = @Translation("Field Address"),
 *   default_widget = "field_address_default_widget",
 *   default_formatter = "field_address_text_formatter"
 * )
 */
class AddressField extends FieldItemBase {

  
  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    $schema = [
        'columns' => [
            'address' => [
                'type' => 'varchar',
                'length' => '255',
                'not null' => TRUE,
            ],
            'latitude' => [
                'type' => 'float',
                'size' => 'normal',
                'description' => 'Address Latitude'
            ],
            'longitude' => [
                'type' => 'float',
                'size' => 'normal',
                'description' => 'Address Longitude'
            ],
        ],
    ];
  
    return $schema;
  }
  
  /**
   * {@inheritdoc}
   */
  public function isEmpty() {
    $value = $this->get('address')->getValue();
    return $value === NULL || $value === '';
  }
  
  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    // Prevent early t() calls by using the TranslatableMarkup.
    $properties['address'] = DataDefinition::create('string')
    ->setLabel(t('Address name'));
    
    $properties['latitude'] = DataDefinition::create('float')
    ->setLabel(t('Address Latitude'));
    
    $properties['longitude'] = DataDefinition::create('float')
    ->setLabel(t('Address Longitude'));
  
    return $properties;
  }
}

4. AddressDefaultWidget.php: Implementación del widget para entrada de datos

En este fichero definiremos como será nuestro formulario de entrada de datos. En nuestro caso solamente tendremos un campo de texto para introducir la dirección y dos campos más para introducir latitud y longitud de nuestra dirección.

La primera parte del fichero estará formada por un bloque annotation donde informaremos del identificador, descripción y campos para los que se mostrará como posible widget.

/**
 * Plugin implementation of the 'field_address_default_widget' widget.
 *
 * @FieldWidget(
 *   id = "field_address_default_widget",
 *   label = @Translation("Default Widget"),
 *   field_types = {
 *     "field_address"
 *   }
 * )
 */

Definiremos el método formElement donde devolveremos el formulario que se mostrará en el widget para introducir datos en nuestro campo.

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $fieldclass = isset($items[$delta]) ? $items[$delta] : NULL;
    
    $element['address'] = [
        '#type' => 'textfield',
        '#title' => t('Address'),
        '#prefix' => '<div id="field-address">',
        '#suffix' => '</div>',
        '#default_value' => 
          isset($fieldclass) ? $fieldclass->get('address')->getValue() : NULL,       
        '#autocomplete_route_name' => 'address_field.autocomplete_address',
        '#autocomplete_route_parameters' => array('key' => $key),
        '#ajax' => [
            'callback' => [ $this, 'geocodeAddress'],
            'wrapper' => 'field-coordinates',
            'disable-refocus' => TRUE,
        ],
        '#delta' => $delta,
    ];
    
    $element['latitude'] = [
        '#type' => 'textfield',
        '#title' => t('Address Latitude'),
        '#default_value' => 
          isset($fieldclass) ? $fieldclass->get('latitude')->getValue() : NULL,
    ];
    
    $element['longitude'] = [
        '#type' => 'textfield',
        '#title' => t('Address Longitude'),
        '#default_value' => 
          isset($fieldclass) ? $fieldclass->get('longitude')->getValue() : NULL,
    ];
    
    return $element;
  }

Este método es el equivalente a buildForm en un formulario tradicional, pero con los siguientes parámetros extra:

  • FieldItemListInterface $items → Dado que un nodo puede tener varios campos dirección, en esta lista tendremos todas las ocurrencias.
  • $delta → Contendrá el número de campo de todos los contenidos en $items que estamos modificando en un momento determinado.
  • $element → Array con el formulario de nuestro widget para un $delta determinado.

Al inicio de la función obtendremos el objeto del campo que estamos modificando en este momento:

$fieldclass = isset($items[$delta]) ? $items[$delta] : NULL;

Podremos acceder a los valores por defecto para address/latitude/longitude de esta manera:

$fieldclass→get('address')→getValue();

$fieldclass→get('latitude')→getValue();

$fieldclass→get('longitude')→getValue();

Por último será imprescindible implementar los siguientes métodos:

public static function defaultSettings() {
    return parent::defaultSettings();
  }
public function settingsForm(array $form, FormStateInterface $form_state) {
    parent::settingsForm($form, $form_state);
  }

Estos dos métodos nos servirán para implementar un formulario de configuración para nuestro widget y para devolver un array que mostrará, a modo de resumen, la configuración que tiene el widget. De momento no los utilizaremos, pero será imprescindible implementarlos para que nuestro campo funcione.

El código completo de nuestro widget será:

namespace Drupal\address_field\Plugin\Field\FieldWidget;

use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Plugin implementation of the 'field_address_default_widget' widget.
 *
 * @FieldWidget(
 *   id = "field_address_default_widget",
 *   label = @Translation("Default Widget"),
 *   field_types = {
 *     "field_address"
 *   }
 * )
 */
class AddressDefaultWidget extends WidgetBase {
  
  /**
   * {@inheritdoc}
   */
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $fieldclass = isset($items[$delta]) ? $items[$delta] : NULL;
    
    $element['address'] = [
        '#type' => 'textfield',
        '#title' => t('Address'),
        '#prefix' => '<div id="field-address">',
        '#suffix' => '</div>',
        '#default_value' => 
          isset($fieldclass) ? $fieldclass->get('address')->getValue() : NULL,       
    ];
    
    $element['latitude'] = [
        '#type' => 'textfield',
        '#title' => t('Address Latitude'),
        '#prefix' => '<div id="field-coordinates">',
        '#default_value' => 
          isset($fieldclass) ? $fieldclass->get('latitude')->getValue() : NULL,
    ];
    
    $element['longitude'] = [
        '#type' => 'textfield',
        '#title' => t('Address Longitude'),
        '#suffix' => '</div>',
        '#default_value' => 
          isset($fieldclass) ? $fieldclass->get('longitude')->getValue() : NULL,
    ];
    
    return $element;
  }
  
  /**
   * {@inheritdoc}
   */
  public static function defaultSettings() {
    return parent::defaultSettings();
  }
  
  /**
   * {@inheritdoc}
   */
  
  public function settingsForm(array $form, FormStateInterface $form_state) {
    parent::settingsForm($form, $form_state);
  }
}

Nuestro widget se parecerá bastante al que podéis ver en la siguiente captura:

Drupal 8 - Field API - Widget para el módulo address_field

Drupal 8 - Field API - Widget para el módulo address_field

5. AddressTextFormatter.php: Implementación del formatter

En este fichero implementaremos un formatter de texto para nuestro campo, será lo más simple posible y solamente mostraremos el contenido de los campos address/latitude/longitude.

Al igual que los anteriores ficheros, empezaremos con un bloque annotation donde definiremos el identificador del formatter, su descripción y los campos para los que se mostrará como posible opción:

/**
 * Plugin implementation of the 'field_address_text_formatter' formatter.
 *
 * @FieldFormatter(
 *   id = "field_address_text_formatter",
 *   label = @Translation("Text Formatter"),
 *   field_types = {
 *     "field_address"
 *   }
 * )
 */

El corazón de esta clase será el método viewElements donde recibiremos como parámetros una lista con todos los campos dirección configurados en el nodo actual y el código de idioma para el que se están mostrando.

public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = [];
  
    foreach ($items as $delta => $item) {
      $elements[$delta] = [
          '#markup' => $this->t('Address: @address [@latitude, @longitude]',
              [ '@address' => $item->address,
                '@latitude' => $item->latitude,
                '@longitude' => $item->longitude
              ])
      ];
    }
  
    return $elements;
  }

Simplemente crearemos un array renderizable en el que tendremos un elemento para cada campo dirección con su correspondiente contenido.

El código completo de la clase será:

<?php

namespace Drupal\address_field\Plugin\Field\FieldFormatter;

use Drupal\Component\Utility\Html;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Plugin implementation of the 'field_address_text_formatter' formatter.
 *
 * @FieldFormatter(
 *   id = "field_address_text_formatter",
 *   label = @Translation("Text Formatter"),
 *   field_types = {
 *     "field_address"
 *   }
 * )
 */
class AddressTextFormatter extends FormatterBase {

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    // dump($items);
    
    $elements = [];
  
    foreach ($items as $delta => $item) {
      $elements[$delta] = [
          '#markup' => $this->t('Address: @address [@latitude, @longitude]',
              [ '@address' => $item->address,
                '@latitude' => $item->latitude,
                '@longitude' => $item->longitude
              ])
      ];
    }
  
    return $elements;
  }
}

Si todo ha ido bien, cuando accedamos al contenido para el que hayamos añadido nuestro campo nuestro formatter devolverá algo parecido a lo siguiente:

Drupal 8 - Field API - Formatter para el módulo address_field

Drupal 8 - Field API - Formatter para el módulo address_field

7. Conclusiones

Llegados a este punto solamente nos quedará activar nuestro módulo y asignarla a cualquiera de los tipos de contenido que tengamos implementados para empezar a disfrutarlo.

Recursos

http://www.e-quipos.es/blog/drupal-8-creaci%C3%B3n-de-formularios

http://www.e-quipos.es/blog/drupal-8-actualizar-textfield-en-formulario-con-ajax

http://www.e-quipos.es/blog/drupal-instalando-drupal-console-en-ubuntu

https://api.drupal.org/api/drupal/core!modules!field!field.module/group/field/8.2.x

http://metadrop.net/articulo/crear-campo-custom-drupal-8

Todos los Derechos Reservados © 2016

Funciona con Drupal