logo

额外区块类型 (EBT) - 全新的布局构建器体验❗

额外区块类型 (EBT) - 样式化、可定制的区块类型:幻灯片、标签页、卡片、手风琴等更多类型。内置背景、DOM Box、JavaScript 插件的设置。立即体验布局构建的未来。

演示 EBT 模块 下载 EBT 模块

❗额外段落类型 (EPT) - 全新的 Paragraphs 体验

额外段落类型 (EPT) - 类似的基于 Paragraph 的模块集合。

演示 EPT 模块 滚动

滚动

FieldTypes、FieldWidgets 和 FieldFormatters

30/09/2025, by Ivan

概览

Drupal 8 自带一个庞大的基础类库,让你可以处理自己的内容。当涉及到内容实体时,你会希望使用字段。理解字段非常重要,因为实体的数据就是保存在字段里的。

FieldTypes

核心字段类型:

 

自定义字段类型
每当你想要以 Drupal 未提供的方式来表示数据时,你可能就需要为你的数据创建一个新的字段类型。

比如说,你有一个内容对象,包含敏感数据。该内容的创建者可能允许特定用户通过密码访问对象,而密码是用户级别的。如果用数据库表表示,你会得到类似这样的东西:

| entity_id | uid | password      |
-----------------------------------
| 1         | 1   | 'helloworld'  |
| 1         | 2   | 'goodbye'     |

可以看到,实体 ID 为 1 的内容为两个不同用户保存了两个不同的密码。那么,如何在不手动建表的情况下在 Drupal 中实现这一点呢?我们需要创建一个新的字段类型。

因为 Drupal 把字段逻辑实现为插件,我们总能继承一个基类来让它在 Drupal 中运行。要实现新的字段类型,你需要在模块中创建以下目录结构:
modules/custom/MODULENAME/src/Plugin/Field/FieldType
这个路径很长,也有些烦,但这样 Drupal 能让模块中的所有功能良好地共存。

在这个例子中,我们创建文件 EntityUserAccessField.php

namespace Drupal\MODULENAME\Plugin\Field\FieldType;
     
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
     
/**
 * @FieldType(
 *   id = "entity_user_access",
 *   label = @Translation("Entity User Access"),
 *   description = @Translation("This field stores a reference to a user and a password for this user on the entity."),
 * )
*/
     
class EntityUserAccessField extends FieldItemBase {
  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    //ToDo: Implement this.
  }
     
  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    //ToDo: Implement this.
  }
}

可以看到,字段类型与内容实体非常相似。实际上,它们之间没有太大区别,不过这是另一个主题了;)

首先,我们在字段类型中定义了注解:

  • @FieldType: 调用 Drupal 库中的 FieldType 注解类
  • id: 字段类型的机器名,方便复用。注意不要使用 PHP 保留字
  • label: 用户可读的机器名翻译
  • description: 如果 label 不够直观,可以用描述进行补充

 

其次,我们的类继承自 FieldItemBase,这要求我们实现两个方法,以便能正确使用该字段类型:

  • propertyDefinitions(): 类似于内容实体的 baseFieldDefinition(注意并不相同!)。它定义出现在实体表单中的数据。
  • schema(): 在实体上这个方法已废弃,但在字段中依旧存在。它定义字段数据在数据库中的表现,可能与属性不同。

为了更直观地说明,我们在方法中添加一些代码:

public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
  $properties['uid'] = DataDefinition::create('integer')
      ->setLabel(t('User ID Reference'))
      ->setDescription(t('The ID of the referenced user.'))
      ->setSetting('unsigned', TRUE);

  $properties['password'] = DataDefinition::create('string')
      ->setLabel(t('Password'))
      ->setDescription(t('A password saved in plain text. That is not safe dude!'));

  $properties['created'] = DataDefinition::create('timestamp')
    ->setLabel(t('Created Time'))
    ->setDescription(t('The time that the entry was created'));

    // ToDo: 添加更多属性
 
    return $properties;
}

你也可以通过 DataReferenceDefinition 来保存用户 ID,这部分可以在未来进一步探讨。

public static function schema(FieldStorageDefinitionInterface $field_definition) {
  $columns = array(
    'uid' => array(
      'description' => 'The ID of the referenced user.',
      'type' => 'int',
      'unsigned' => TRUE,
    ),
    'password' => array(
      'description' => 'A plain text password.',
      'type' => 'varchar',
      'length' => 255,
    ),
    'created' => array(
      'description' => 'A timestamp of when this entry has been created.',
      'type' => 'int',
    ),

    // ToDo: 添加更多列
  );
 
  $schema = array(
    'columns' => $columns,
    'indexes' => array(),
    'foreign keys' => array(),
  );

  return $schema;
}

Schema() 方法必不可少,它告诉 Drupal 如何保存数据。Schema 的列必须是 propertyDefinitions() 属性的子集。

这样我们就有了一个全新的字段类型。它没有任何处理逻辑,但可以作为字段应用在任何内容实体上。如果需要,你还可以把它作为实体的 baseField:

public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
  // 上面的其他字段
 
  $fields['entity_user_access'] = BaseFieldDefinition::create('entity_user_access')
    ->setLabel(t('Entity User Access'))
    ->setDescription(t('Specify passwords for any user that want to see this entity.'))
    ->setCardinality(-1); // 确保可以有多个成员
 
  // 更多字段
 
  return $fields;
}
  • BaseFieldDefinition::create(): 必须传入字段类型的机器名
  • setCardinality(-1): 表示一个实体能保存多少条字段数据。例如,如果填 2,就只能有两个用户访问;填 3,就能有三个用户,以此类推。-1 表示无限制。

 

FieldWidget

如果你有自定义数据,可能还需要自定义这些数据在表单中的呈现方式。小部件用于定义用户如何输入这些数据。例如:

  • 需要输入整数,但用户只能通过复选框来设置
  • 你想要为数据提供自动完成
  • 密码输入通过特殊界面完成

等等。

在 Drupal 中,字段小部件位于:
modules/custom/MODULENAME/src/Plugin/Field/FieldWidget
这同样是个很长的路径。此时应该清楚为什么 Drupal 要这样组织代码了。它能让你更容易知道哪些文件属于哪里。

我们在 EntityUserAccessWidget.php 中创建小部件:

namespace Drupal\MODULENAME\Plugin\Field\FieldWidget;
 
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
 
/**
 * Plugin implementation of the 'entity_user_access_w' widget.
 *
 * @FieldWidget(
 *   id = "entity_user_access_w",
 *   label = @Translation("Entity User Access - Widget"),
 *   description = @Translation("Entity User Access - Widget"),
 *   field_types = {
 *     "entity_user_access",
 *   },
 *   multiple_values = TRUE,
 * )
 */
 
class EntityUserAccessWidget extends WidgetBase {
  /**
   * {@inheritdoc}
   */
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    // ToDo: 实现这里
  }
}

注意了吗?在 Drupal 8 中,如果你要实现功能,总是会看到这种风格:注解 + 继承基类。没错,Drupal 就是这么做的!

  • @FieldWidget: 定义注解类
  • id: 小部件的机器名
  • field_types: 可以使用此小部件的字段类型数组
  • multiple_values: 默认为 FALSE。如果为 TRUE,表单可以提交多个值

如果你想让字段类型使用该小部件,需要修改字段类型的注解:

/**
 * @FieldType(
 *   id = "entity_user_access",
 *   label = @Translation("Entity User Access"),
 *   description = @Translation("This field stores a reference to a user and a password for this user on the entity."),
 *   default_widget = "entity_user_access_w",
 * )
 */

完成了吗?不,还没有。因为我们必须在小部件中实现 formElement() 方法。

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $element['userlist'] = array(
      '#type' => 'select',
      '#title' => t('User'),
      '#description' => t('Select group members from the list.'),
      '#options' => array(
         0 => t('Anonymous'),
         1 => t('Admin'),
         2 => t('foobar'),
         // 这里应该用更好的实现!
       ),
  
    );
  
    $element['passwordlist'] = array(
      '#type' => 'password',
      '#title' => t('Password'),
      '#description' => t('Select a password for the user'),
    );

    // 为以上所有字段设置默认值
    $childs = Element::children($element);
    foreach ($childs as $child) {
        $element[$child]['#default_value'] = isset($items[$delta]->{$child}) ? $items[$delta]->{$child} : NULL;
    }
   
    return $element;
}

如果你现在打开带有这个小部件的表单,会看到至少两个输入字段:一个是用户选择,另一个是密码字段。如果你想实现数据保存方式,需要在小部件中实现验证方法,或在实体表单中实现。更多信息请查看 Drupal 8 Form API

到这里,你已经完成了大部分自定义字段的工作。如果还有不理解的,可以直接试试代码,或者看看核心模块的实现来深入学习。

FieldFormatters

最后一步是数据在视图模式下的展示——顺便说一句,小部件对应的是表单模式。最常见的情况就是当你访问:yourdrupalpage.com/myentity/1/view

这部分没什么太多要说的,我们直接看代码。在 modules/custom/MODULENAME/src/Plugin/Field/FieldFormatter 下创建 EntityUserAccessFormatter.php

namespace Drupal\MODULENAME\Plugin\Field\FieldFormatter;
     
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
     
/**
 * Plugin implementation of the 'entity_user_access_f' formatter.
 *
 * @FieldFormatter(
 *   id = "entity_user_access_f",
 *   label = @Translation("Entity User Access - Formatter"),
 *   description = @Translation("Entity User Access - Formatter"),
 *   field_types = {
 *     "entity_user_access",
 *   }
 * )
 */
     
class EntityUserAccessFormatter extends FormatterBase {
  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = array();
     
    foreach ($items as $delta => $item) {
      $elements[$delta] = array(
        'uid' => array(
          '#markup' => \Drupal\user\Entity\User::load($item->uid)->getUsername(),
          ),
        // 添加更多内容
      );
    }
     
    return $elements;
  }
}

这个例子和小部件的注解几乎一样,所以不用多解释。viewElements() 方法只是展示字段类型中保存的用户 ID 的用户名。访问控制逻辑应该在实体上实现。因此,这个实现会显示所有在实体上保存了密码的用户名。