Commit a820153f authored by alexpott's avatar alexpott

Issue #1735118 by swentel, yched, xjm, larowlan, alexpott, tim.plunkett: Convert Field API to CMI.

parent 5a6647be
......@@ -12,7 +12,7 @@
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Plugin\PluginSettingsBase;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\datetime\DateHelper;
......
......@@ -12,7 +12,7 @@
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Plugin\PluginSettingsBase;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
use Drupal\Core\Datetime\DrupalDateTime;
/**
......
......@@ -48,13 +48,12 @@ function setUp() {
$this->drupalLogin($web_user);
// Create a field with settings to validate.
$this->field = array(
$this->field = field_create_field(array(
'field_name' => drupal_strtolower($this->randomName()),
'type' => 'datetime',
'settings' => array('datetime_type' => 'date'),
);
field_create_field($this->field);
$this->instance = array(
));
$this->instance = field_create_instance(array(
'field_name' => $this->field['field_name'],
'entity_type' => 'test_entity',
'bundle' => 'test_bundle',
......@@ -64,8 +63,7 @@ function setUp() {
'settings' => array(
'default_value' => 'blank',
),
);
field_create_instance($this->instance);
));
$this->display_options = array(
'type' => 'datetime_default',
......@@ -304,6 +302,7 @@ function testDefaultValue() {
// Set the default value to 'blank'.
$this->instance['settings']['default_value'] = 'blank';
$this->instance['default_value_function'] = 'datetime_default_value';
field_update_instance($this->instance);
// Display creation form.
......
......@@ -9,7 +9,7 @@
use Drupal\Component\Plugin\PluginBase;
use Drupal\edit\EditorInterface;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
/**
* Defines a base editor (Create.js PropertyEditor widget) implementation.
......
......@@ -8,7 +8,7 @@
namespace Drupal\edit;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
/**
* Defines an interface for in-place editors (Create.js PropertyEditor widgets).
......
......@@ -9,7 +9,7 @@
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
/**
* Selects an in-place editor (an Editor plugin) for a field.
......
......@@ -7,7 +7,7 @@
namespace Drupal\edit;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
/**
* Interface for selecting an in-place editor (an Editor plugin) for a field.
......
......@@ -9,7 +9,7 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
use Drupal\edit\Access\EditEntityFieldAccessCheckInterface;
......
......@@ -8,7 +8,7 @@
namespace Drupal\edit;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
/**
* Interface for generating in-place editing metadata for an entity field.
......
......@@ -9,7 +9,7 @@
use Drupal\edit\EditorBase;
use Drupal\Component\Annotation\Plugin;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
/**
* Defines the "direct" Create.js PropertyEditor widget.
......
......@@ -9,7 +9,7 @@
use Drupal\edit\EditorBase;
use Drupal\Component\Annotation\Plugin;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
/**
* Defines the "form" Create.js PropertyEditor widget.
......
......@@ -28,7 +28,6 @@ function setUp() {
parent::setUp();
$this->installSchema('system', 'variable');
$this->installSchema('field', array('field_config', 'field_config_instance'));
$this->installSchema('entity_test', array('entity_test', 'entity_test_rev'));
// Set default storage backend.
......@@ -58,12 +57,12 @@ function setUp() {
*/
function createFieldWithInstance($field_name, $type, $cardinality, $label, $instance_settings, $widget_type, $widget_settings, $formatter_type, $formatter_settings) {
$field = $field_name . '_field';
$this->$field = array(
$this->field = array(
'field_name' => $field_name,
'type' => $type,
'cardinality' => $cardinality,
);
$this->$field_name = field_create_field($this->$field);
$this->$field = field_create_field($this->field);
$instance = $field_name . '_instance';
$this->$instance = array(
......
......@@ -9,7 +9,7 @@
use Drupal\edit\EditorBase;
use Drupal\Component\Annotation\Plugin;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
/**
* Defines the "wysiwyg" Create.js PropertyEditor widget.
......
......@@ -11,7 +11,7 @@
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\edit\EditorInterface;
use Drupal\field\FieldInstance;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
/**
......
......@@ -24,12 +24,6 @@ public static function getInfo() {
);
}
protected function setUp() {
parent::setUp();
$this->installSchema('field', array('field_config', 'field_config_instance'));
}
/**
* Tests basic CRUD operations on EntityDisplay objects.
*/
......
......@@ -234,6 +234,9 @@ function hook_field_info_alter(&$info) {
* indexes specified by the field-type module. Some storage engines might
* not support indexes.
* - foreign keys: (optional) An array of Schema API foreign key definitions.
* Note, however, that the field data is not necessarily stored in SQL.
* Also, the possible usage is limited, as you cannot specify another field
* as related, only existing SQL tables, such as {taxonomy_term_data}.
*/
function hook_field_schema($field) {
if ($field['type'] == 'text_long') {
......@@ -1302,7 +1305,7 @@ function hook_field_storage_details_alter(&$details, $field) {
* FIELD_LOAD_REVISION to load the version indicated by each entity.
* @param $fields
* An array listing the fields to be loaded. The keys of the array are field
* IDs, and the values of the array are the entity IDs (or revision IDs,
* UUIDs, and the values of the array are the entity IDs (or revision IDs,
* depending on the $age parameter) to add each field to.
* @param $options
* An associative array of additional options, with the following keys:
......@@ -1370,7 +1373,7 @@ function hook_field_storage_load($entity_type, $entities, $age, $fields, $option
* FIELD_STORAGE_INSERT when inserting a new entity.
* @param $fields
* An array listing the fields to be written. The keys and values of the
* array are field IDs.
* array are field UUIDs.
*/
function hook_field_storage_write(\Drupal\Core\Entity\EntityInterface $entity, $op, $fields) {
$id = $entity->id();
......@@ -1464,7 +1467,7 @@ function hook_field_storage_write(\Drupal\Core\Entity\EntityInterface $entity, $
* The entity on which to operate.
* @param $fields
* An array listing the fields to delete. The keys and values of the
* array are field IDs.
* array are field UUIDs.
*/
function hook_field_storage_delete(\Drupal\Core\Entity\EntityInterface $entity, $fields) {
foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) {
......@@ -1488,7 +1491,7 @@ function hook_field_storage_delete(\Drupal\Core\Entity\EntityInterface $entity,
* The entity on which to operate.
* @param $fields
* An array listing the fields to delete. The keys and values of the
* array are field IDs.
* array are field UUIDs.
*/
function hook_field_storage_delete_revision(\Drupal\Core\Entity\EntityInterface $entity, $fields) {
$vid = $entity->getRevisionId();
......@@ -1712,13 +1715,13 @@ function hook_field_storage_delete_instance($instance) {
* FIELD_LOAD_CURRENT to load the most recent revision for all fields, or
* FIELD_LOAD_REVISION to load the version indicated by each entity.
* @param $skip_fields
* An array keyed by field IDs whose data has already been loaded and
* An array keyed by field UUIDs whose data has already been loaded and
* therefore should not be loaded again. Add a key to this array to indicate
* that your module has already loaded a field.
* @param $options
* An associative array of additional options, with the following keys:
* - field_id: The field ID that should be loaded. If unset, all fields should
* be loaded.
* - field_id: The field UUID that should be loaded. If unset, all fields
* should be loaded.
* - deleted: If TRUE, deleted fields should be loaded as well as non-deleted
* fields. If unset or FALSE, only non-deleted fields should be loaded.
*/
......@@ -1735,11 +1738,11 @@ function hook_field_storage_pre_load($entity_type, $entities, $age, &$skip_field
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity with fields to save.
* @param $skip_fields
* An array keyed by field IDs whose data has already been written and
* An array keyed by field UUIDs whose data has already been written and
* therefore should not be written again. The values associated with these
* keys are not specified.
* @return
* Saved field IDs are set set as keys in $skip_fields.
* Saved field UUIDs are set as keys in $skip_fields.
*/
function hook_field_storage_pre_insert(\Drupal\Core\Entity\EntityInterface $entity, &$skip_fields) {
if ($entity->entityType() == 'node' && $entity->status && _forum_node_check_node_type($entity)) {
......@@ -1770,11 +1773,11 @@ function hook_field_storage_pre_insert(\Drupal\Core\Entity\EntityInterface $enti
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity with fields to save.
* @param $skip_fields
* An array keyed by field IDs whose data has already been written and
* An array keyed by field UUIDs whose data has already been written and
* therefore should not be written again. The values associated with these
* keys are not specified.
* @return
* Saved field IDs are set set as keys in $skip_fields.
* Saved field UUIDs are set as keys in $skip_fields.
*/
function hook_field_storage_pre_update(\Drupal\Core\Entity\EntityInterface $entity, &$skip_fields) {
$first_call = &drupal_static(__FUNCTION__, array());
......
......@@ -1170,7 +1170,7 @@ function field_attach_insert(EntityInterface $entity) {
$storages = array();
foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$field_id = $field['uuid'];
$field_name = $field['field_name'];
if (!empty($entity->$field_name)) {
// Collect the storage backend if the field has not been written yet.
......@@ -1211,7 +1211,7 @@ function field_attach_update(EntityInterface $entity) {
$storages = array();
foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$field_id = $field['uuid'];
$field_name = $field['field_name'];
// Leave the field untouched if $entity comes with no $field_name property,
// but empty the field if it comes as a NULL value or an empty array.
......@@ -1254,7 +1254,7 @@ function field_attach_delete(EntityInterface $entity) {
$storages = array();
foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$field_id = $field['uuid'];
$storages[$field['storage']['type']][$field_id] = $field_id;
}
......@@ -1287,7 +1287,7 @@ function field_attach_delete_revision(EntityInterface $entity) {
$storages = array();
foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$field_id = $field['uuid'];
$storages[$field['storage']['type']][$field_id] = $field_id;
}
......@@ -1531,11 +1531,16 @@ function field_entity_bundle_create($entity_type, $bundle) {
* Implements hook_entity_bundle_rename().
*/
function field_entity_bundle_rename($entity_type, $bundle_old, $bundle_new) {
db_update('field_config_instance')
->fields(array('bundle' => $bundle_new))
->condition('entity_type', $entity_type)
->condition('bundle', $bundle_old)
->execute();
$instances = field_read_instances();
foreach ($instances as $instance) {
if ($instance->entity_type == $entity_type && $instance->bundle == $bundle_old) {
$id_new = $instance['entity_type'] . '.' . $bundle_new . '.' . $instance['field_name'];
$instance->id = $id_new;
$instance->bundle = $bundle_new;
$instance->allowBundleRename();
$instance->save();
}
}
// Clear the cache.
field_cache_clear();
......
......@@ -6,6 +6,8 @@
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Plugin\Core\Entity\Field;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
use Drupal\field\FieldException;
/**
......@@ -28,8 +30,8 @@
* This function does not bind the field to any bundle; use
* field_create_instance() for that.
*
* @param $field
* A field definition array. The field_name and type properties are required.
* @param array $field
* A field definition. The field_name and type properties are required.
* Other properties, if omitted, will be given the following default values:
* - cardinality: 1
* - locked: FALSE
......@@ -48,157 +50,16 @@
* - settings: each omitted setting is given the default value specified in
* hook_field_storage_info().
*
* @return
* The $field array with the id property filled in.
* @return \Drupal\field\Plugin\Core\Entity\Field
* The field entity.
*
* @throws Drupal\field\FieldException
*
* See: @link field Field API data structures @endlink.
*/
function field_create_field($field) {
// Field name is required.
if (empty($field['field_name'])) {
throw new FieldException('Attempt to create an unnamed field.');
}
// Field type is required.
if (empty($field['type'])) {
throw new FieldException('Attempt to create a field with no type.');
}
// Field name cannot contain invalid characters.
if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $field['field_name'])) {
throw new FieldException('Attempt to create a field with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character');
}
// Field name cannot be longer than 32 characters. We use drupal_strlen()
// because the DB layer assumes that column widths are given in characters,
// not bytes.
if (drupal_strlen($field['field_name']) > 32) {
throw new FieldException(t('Attempt to create a field with a name longer than 32 characters: %name',
array('%name' => $field['field_name'])));
}
// Ensure the field name is unique over active and disabled fields.
// We do not care about deleted fields.
$prior_field = field_read_field($field['field_name'], array('include_inactive' => TRUE));
if (!empty($prior_field)) {
$message = $prior_field['active']?
t('Attempt to create field name %name which already exists and is active.', array('%name' => $field['field_name'])):
t('Attempt to create field name %name which already exists, although it is inactive.', array('%name' => $field['field_name']));
throw new FieldException($message);
}
// Disallow reserved field names. This can't prevent all field name
// collisions with existing entity properties, but some is better
// than none.
foreach (entity_get_info() as $type => $info) {
if (in_array($field['field_name'], $info['entity_keys'])) {
throw new FieldException(t('Attempt to create field name %name which is reserved by entity type %type.', array('%name' => $field['field_name'], '%type' => $type)));
}
}
$field += array(
'entity_types' => array(),
'cardinality' => 1,
'translatable' => FALSE,
'locked' => FALSE,
'settings' => array(),
'storage' => array(),
'deleted' => 0,
);
// Check that the field type is known.
$field_type = field_info_field_types($field['type']);
if (!$field_type) {
throw new FieldException(t('Attempt to create a field of unknown type %type.', array('%type' => $field['type'])));
}
// Create all per-field-type properties (needed here as long as we have
// settings that impact column definitions).
$field['settings'] += field_info_field_settings($field['type']);
$field['module'] = $field_type['module'];
$field['active'] = 1;
// Provide default storage.
$field['storage'] += array(
'type' => variable_get('field_storage_default', 'field_sql_storage'),
'settings' => array(),
);
// Check that the storage type is known.
$storage_type = field_info_storage_types($field['storage']['type']);
if (!$storage_type) {
throw new FieldException(t('Attempt to create a field with unknown storage type %type.', array('%type' => $field['storage']['type'])));
}
// Provide default storage settings.
$field['storage']['settings'] += field_info_storage_settings($field['storage']['type']);
$field['storage']['module'] = $storage_type['module'];
$field['storage']['active'] = 1;
// Collect storage information.
module_load_install($field['module']);
$schema = (array) module_invoke($field['module'], 'field_schema', $field);
$schema += array('columns' => array(), 'indexes' => array(), 'foreign keys' => array());
// 'columns' are hardcoded in the field type.
$field['columns'] = $schema['columns'];
if (array_intersect(array_keys($field['columns']), field_reserved_columns())) {
throw new FieldException(t('Illegal field type columns.'));
}
// 'foreign keys' are hardcoded in the field type.
$field['foreign keys'] = $schema['foreign keys'];
// 'indexes' can be both hardcoded in the field type, and specified in the
// incoming $field definition.
$field += array(
'indexes' => array(),
);
$field['indexes'] += $schema['indexes'];
// The serialized 'data' column contains everything from $field that does not
// have its own column and is not automatically populated when the field is
// read.
$data = $field;
unset($data['columns'], $data['field_name'], $data['type'], $data['active'], $data['module'], $data['storage_type'], $data['storage_active'], $data['storage_module'], $data['locked'], $data['cardinality'], $data['deleted']);
// Additionally, do not save the 'bundles' property populated by
// field_info_field().
unset($data['bundles']);
$record = array(
'field_name' => $field['field_name'],
'type' => $field['type'],
'module' => $field['module'],
'active' => $field['active'],
'storage_type' => $field['storage']['type'],
'storage_module' => $field['storage']['module'],
'storage_active' => $field['storage']['active'],
'locked' => $field['locked'],
'data' => $data,
'cardinality' => $field['cardinality'],
'translatable' => $field['translatable'],
'deleted' => $field['deleted'],
);
// Store the field and get the id back.
drupal_write_record('field_config', $record);
$field['id'] = $record['id'];
// Invoke hook_field_storage_create_field after the field is
// complete (e.g. it has its id).
try {
// Invoke hook_field_storage_create_field after
// drupal_write_record() sets the field id.
module_invoke($storage_type['module'], 'field_storage_create_field', $field);
}
catch (Exception $e) {
// If storage creation failed, remove the field_config record before
// rethrowing the exception.
db_delete('field_config')
->condition('id', $field['id'])
->execute();
throw $e;
}
// Clear caches
field_cache_clear(TRUE);
// Invoke external hooks after the cache is cleared for API consistency.
module_invoke_all('field_create_field', $field);
function field_create_field(array $field) {
$field = entity_create('field_entity', $field);
$field->save();
return $field;
}
......@@ -212,90 +73,33 @@ function field_create_field($field) {
* semantics, or if there are external dependencies on field settings
* that cannot be updated.
*
* @param $field
* A field structure. $field['field_name'] must provided; it
* identifies the field that will be updated to match this
* structure. Any other properties of the field that are not
* specified in $field will be left unchanged, so it is not
* necessary to pass in a fully populated $field structure.
* @param mixed $field
* Either the \Drupal\field\Plugin\Core\Entity\Field object to update, or a
* field array structure. If the latter, $field['field_name'] must provided;
* it identifies the field that will be updated to match this structure. Any
* other properties of the field that are not specified in $field will be left
* unchanged, so it is not necessary to pass in a fully populated $field
* structure.
*
* @throws Drupal\field\FieldException
*
* @see field_create_field()
*/
function field_update_field($field) {
// Check that the specified field exists.
$prior_field = field_read_field($field['field_name']);
if (empty($prior_field)) {
throw new FieldException('Attempt to update a non-existent field.');
}
// Use the prior field values for anything not specifically set by the new
// field to be sure that all values are set.
$field += $prior_field;
$field['settings'] += $prior_field['settings'];
// Some updates are always disallowed.
if ($field['type'] != $prior_field['type']) {
throw new FieldException("Cannot change an existing field's type.");
}
if ($field['entity_types'] != $prior_field['entity_types']) {
throw new FieldException("Cannot change an existing field's entity_types property.");
}
if ($field['storage']['type'] != $prior_field['storage']['type']) {
throw new FieldException("Cannot change an existing field's storage type.");
}
// Collect the new storage information, since what is in
// $prior_field may no longer be right.
module_load_install($field['module']);
$schema = (array) module_invoke($field['module'], 'field_schema', $field);
$schema += array('columns' => array(), 'indexes' => array());
// 'columns' are hardcoded in the field type.
$field['columns'] = $schema['columns'];
// 'indexes' can be both hardcoded in the field type, and specified in the
// incoming $field definition.
$field += array(
'indexes' => array(),
);
$field['indexes'] += $schema['indexes'];
$has_data = field_has_data($field);
// See if any module forbids the update by throwing an exception.
foreach (module_implements('field_update_forbid') as $module) {
$function = $module . '_field_update_forbid';
$function($field, $prior_field, $has_data);
// Module developers can still pass in an array of properties.
if (is_array($field)) {
$field_loaded = entity_load('field_entity', $field['field_name']);
if (empty($field_loaded)) {
throw new FieldException('Attempt to update a non-existent field.');
}
// Merge incoming values.
foreach ($field as $key => $value) {
$field_loaded[$key] = $value;
}
$field = $field_loaded;
}
// Tell the storage engine to update the field. Do this before
// saving the new definition since it still might fail.
$storage_type = field_info_storage_types($field['storage']['type']);
module_invoke($storage_type['module'], 'field_storage_update_field', $field, $prior_field, $has_data);
// Save the new field definition. @todo: refactor with
// field_create_field.
// The serialized 'data' column contains everything from $field that does not
// have its own column and is not automatically populated when the field is