FieldTypes、FieldWidgets 和 FieldFormatters
概览
Drupal 8 自带一个庞大的基础类库,让你可以处理自己的内容。当涉及到内容实体时,你会希望使用字段。理解字段非常重要,因为实体的数据就是保存在字段里的。
FieldTypes
核心字段类型:
- boolean
- changed
- comment
- created
- datetime
- daterange
- decimal
- entity_reference
- file
- float
- image
- integer
- language
- link
- list_float
- list_integer
- list_string
- map
- password
- path
- string
- string_long
- telephone
- text
- text_long
- text_with_summary
- timestamp
- uri
- uuid
自定义字段类型
每当你想要以 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 的用户名。访问控制逻辑应该在实体上实现。因此,这个实现会显示所有在实体上保存了密码的用户名。