diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index 42e4d77..7f0b84f 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -195,7 +195,7 @@ public function processDefinition(&$definition, $plugin_id) { // Prepare entity schema fields SQL info for // Drupal\Core\Entity\DatabaseStorageControllerInterface::buildQuery(). - if (isset($definition['base_table'])) { + if (isset($definition['base_table']) && drupal_get_schema($definition['base_table']) !== FALSE) { $definition['schema_fields_sql']['base_table'] = drupal_schema_fields_sql($definition['base_table']); if (isset($definition['data_table'])) { $definition['schema_fields_sql']['data_table'] = drupal_schema_fields_sql($definition['data_table']); diff --git a/core/modules/comment/comment.install b/core/modules/comment/comment.install index 48bb5a4..2c6d416 100644 --- a/core/modules/comment/comment.install +++ b/core/modules/comment/comment.install @@ -16,7 +16,7 @@ function comment_uninstall() { variable_del('comment_block_count'); $node_types = array_keys(node_type_get_types()); foreach ($node_types as $node_type) { - field_attach_delete_bundle('comment', 'comment_node_' . $node_type); + field_attach_delete_bundle('comment', 'comment_node_' . $node_type, FALSE); variable_del('comment_' . $node_type); variable_del('comment_anonymous_' . $node_type); variable_del('comment_controls_' . $node_type); diff --git a/core/modules/comment/lib/Drupal/comment/Tests/CommentFieldsTest.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentFieldsTest.php index a29db8c..1282363 100644 --- a/core/modules/comment/lib/Drupal/comment/Tests/CommentFieldsTest.php +++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentFieldsTest.php @@ -41,7 +41,7 @@ function testCommentDefaultFields() { $this->assertTrue(isset($instances['comment_node_' . $type_name]['comment_body']), format_string('The comment_body field is present for comments on type @type', array('@type' => $type_name))); // Delete the instance along the way. - field_delete_instance($instances['comment_node_' . $type_name]['comment_body']); + field_delete_instance($instances['comment_node_' . $type_name]['comment_body']->getArray()); } // Check that the 'comment_body' field is deleted. diff --git a/core/modules/field/field.attach.inc b/core/modules/field/field.attach.inc index f9db7b5..a133aab 100644 --- a/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -1549,11 +1549,18 @@ function field_attach_create_bundle($entity_type, $bundle) { * The new name of the bundle. */ function field_attach_rename_bundle($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 $id => $instance) { + if ($instance['entity_type'] == $entity_type && $instance['bundle'] == $bundle_old) { + $new_instance_id = $instance['entity_type'] . '.' . $bundle_new . '.' . $instance['field_name']; + config('field.instance.' . $instance['entity_type'] . '.' . $bundle_old . '.' . $instance['field_name'])->rename('field.instance.' . $new_instance_id); + config('field.instance.' . $instance['entity_type'] . '.' . $bundle_new . '.' . $instance['field_name']) + ->set('data.bundle', $bundle_new) + ->set('id', $new_instance_id) + ->set('label', $new_instance_id) + ->save(); + } + } // Clear the cache. field_cache_clear(); @@ -1569,7 +1576,7 @@ function field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) { } /** - * Notifies field.module the a bundle was deleted. + * Notifies field.module that a bundle was deleted. * * This deletes the data for the field instances as well as the field instances * themselves. This function actually just marks the data and field instances as @@ -1582,14 +1589,18 @@ function field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) { * The entity type to which the bundle is bound. * @param $bundle * The bundle to delete. + * @param $field_cleanup + * If TRUE, the field will be deleted as well if its last instance is being + * deleted. If FALSE, it is the caller's responsibility to handle the case of + * fields left without instances. Defaults to TRUE. */ -function field_attach_delete_bundle($entity_type, $bundle) { +function field_attach_delete_bundle($entity_type, $bundle, $field_cleanup = TRUE) { // First, delete the instances themselves. field_read_instances() must be // used here since field_info_instances() does not return instances for // disabled entity types or bundles. $instances = field_read_instances(array('entity_type' => $entity_type, 'bundle' => $bundle), array('include_inactive' => 1)); foreach ($instances as $instance) { - field_delete_instance($instance); + field_delete_instance($instance, $field_cleanup); } // Clear the cache. diff --git a/core/modules/field/field.crud.inc b/core/modules/field/field.crud.inc index f6ee8a2..29372da 100644 --- a/core/modules/field/field.crud.inc +++ b/core/modules/field/field.crud.inc @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\field\FieldException; +use Drupal\Field\FieldInstance; /** * @defgroup field_crud Field CRUD API @@ -149,52 +150,42 @@ function field_create_field($field) { ); $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'], + $field += array( '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']; + // Do not save the 'bundles' property populated by + // field_info_field(). + unset($field['bundles']); + + // Generate id for the field. + $field['id'] = field_generate_id(); + + // Save the configuration. + $e_field = entity_create('field_entity', array( + 'id' => $field['field_name'], + 'label' => $field['field_name'], + 'data' => $field, + )); + $e_field->save(); // 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. + // Invoke hook_field_storage_create_field. 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(); + entity_delete_multiple('field_entity', array($field['field_name'])); throw $e; } // Clear caches - field_cache_clear(TRUE); + field_cache_clear(); // Invoke external hooks after the cache is cleared for API consistency. module_invoke_all('field_create_field', $field); @@ -273,26 +264,17 @@ function field_update_field($field) { $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 - // read. - $data = $field; - unset($data['columns'], $data['field_name'], $data['type'], $data['locked'], $data['module'], $data['cardinality'], $data['active'], $data['deleted']); // Additionally, do not save the 'bundles' property populated by // field_info_field(). - unset($data['bundles']); - - $field['data'] = $data; + unset($field['bundles']); - // Store the field and create the id. - $primary_key = array('id'); - drupal_write_record('field_config', $field, $primary_key); + // Store the field. + $e_field = entity_load('field_entity', $field['field_name']); + $e_field->data = $field; + $e_field->save(); // Clear caches - field_cache_clear(TRUE); + field_cache_clear(); // Invoke external hooks after the cache is cleared for API consistency. module_invoke_all('field_update_field', $field, $prior_field, $has_data); @@ -342,52 +324,43 @@ function field_read_field($field_name, $include_additional = array()) { * field ID, otherwise it is keyed by field name. */ function field_read_fields($params = array(), $include_additional = array()) { - $query = db_select('field_config', 'fc', array('fetch' => PDO::FETCH_ASSOC)); - $query->fields('fc'); - - // Turn the conditions into a query. - foreach ($params as $key => $value) { - // Allow filtering on the 'entity_type' and 'bundle' columns of the - // field_config_instance table. - if ($key == 'entity_type' || $key == 'bundle') { - if (empty($fci_join)) { - $fci_join = $query->join('field_config_instance', 'fci', 'fc.id = fci.field_id'); - } - $key = 'fci.' . $key; - } - else { - $key = 'fc.' . $key; - } + $fields = array(); - $query->condition($key, $value); + // Check if we need to retrieve deleted fields. + $include_deleted = (isset($include_additional['include_deleted']) && $include_additional['include_deleted']) || (isset($params['deleted']) && $params['deleted']); + if ($include_deleted) { + $deleted_fields = state()->get('field.field.deleted') ?: array(); } + // Add active and storage active parameters. if (!isset($include_additional['include_inactive']) || !$include_additional['include_inactive']) { - $query - ->condition('fc.active', 1) - ->condition('fc.storage_active', 1); + $params['active'] = 1; + $params['storage_active'] = 1; } - $include_deleted = (isset($include_additional['include_deleted']) && $include_additional['include_deleted']); - if (!$include_deleted) { - $query->condition('fc.deleted', 0); + + // Get configuration fields. + $config_fields = entity_load_multiple('field_entity'); + // Merge deleted fields. + if (!empty($deleted_fields)) { + $config_fields += $deleted_fields; } - $fields = array(); - $results = $query->execute(); - foreach ($results as $record) { - $field = unserialize($record['data']); - $field['id'] = $record['id']; - $field['field_name'] = $record['field_name']; - $field['type'] = $record['type']; - $field['module'] = $record['module']; - $field['active'] = $record['active']; - $field['storage']['type'] = $record['storage_type']; - $field['storage']['module'] = $record['storage_module']; - $field['storage']['active'] = $record['storage_active']; - $field['locked'] = $record['locked']; - $field['cardinality'] = $record['cardinality']; - $field['translatable'] = $record['translatable']; - $field['deleted'] = $record['deleted']; + foreach ($config_fields as $config) { + if (is_object($config)) { + $field = $config->data; + } + else { + $field = $config; + } + + // Conditions. + foreach ($params as $key => $value) { + if ($field[$key] != $value) { + continue 2; + } + } + + // Invoke read field. module_invoke_all('field_read_field', $field); @@ -415,11 +388,12 @@ function field_read_fields($params = array(), $include_additional = array()) { function field_delete_field($field_name) { // Delete all non-deleted instances. $field = field_info_field($field_name); + if (isset($field['bundles'])) { foreach ($field['bundles'] as $entity_type => $bundles) { foreach ($bundles as $bundle) { $instance = field_info_instance($entity_type, $field_name, $bundle); - field_delete_instance($instance, FALSE); + field_delete_instance($instance->getArray(), FALSE); } } } @@ -427,14 +401,17 @@ function field_delete_field($field_name) { // Mark field data for deletion. module_invoke($field['storage']['module'], 'field_storage_delete_field', $field); - // Mark the field for deletion. - db_update('field_config') - ->fields(array('deleted' => 1)) - ->condition('field_name', $field_name) - ->execute(); + // Delete the configuration of this field and save the field configuration + // in the key_value table so we can use it later during field_purge_batch(). + // This makes sure a new field can be created immediately with the same name. + $deleted_fields = state()->get('field.field.deleted') ?: array(); + $field['deleted'] = TRUE; + $deleted_fields[$field['id']] = $field; + state()->set('field.field.deleted', $deleted_fields); + entity_delete_multiple('field_entity', array($field['field_name'])); // Clear the cache. - field_cache_clear(TRUE); + field_cache_clear(); module_invoke_all('field_delete_field', $field); } @@ -494,6 +471,9 @@ function field_create_instance(&$instance) { // Set the field id. $instance['field_id'] = $field['id']; + // Generate id for the instance. + $instance['id'] = field_generate_id(); + // Note that we do *not* prevent creating a field on non-existing bundles, // because that would break the 'Body as field' upgrade for contrib // node types. @@ -513,7 +493,7 @@ function field_create_instance(&$instance) { throw new FieldException($message); } - _field_write_instance($instance); + _field_write_instance($instance, TRUE); // Clear caches field_cache_clear(); @@ -558,7 +538,7 @@ function field_update_instance($instance) { $instance['id'] = $prior_instance['id']; $instance['field_id'] = $prior_instance['field_id']; - _field_write_instance($instance, TRUE); + _field_write_instance($instance); // Clear caches. field_cache_clear(); @@ -571,10 +551,8 @@ function field_update_instance($instance) { * * @param $instance * An instance structure. - * @param $update - * Whether this is a new or existing instance. */ -function _field_write_instance(&$instance, $update = FALSE) { +function _field_write_instance(&$instance, $create = TRUE) { $field = field_read_field($instance['field_name']); $field_type = field_info_field_types($field['type']); @@ -598,7 +576,6 @@ function _field_write_instance(&$instance, $update = FALSE) { // Set default instance settings. $instance['settings'] += field_info_instance_settings($field['type']); - // Set default widget and settings. $instance['widget'] += array( // TODO: what if no 'default_widget' specified ? @@ -615,30 +592,20 @@ function _field_write_instance(&$instance, $update = FALSE) { $instance['widget']['module'] = $widget_type['module']; $instance['widget']['settings'] += field_info_widget_settings($instance['widget']['type']); - // The serialized 'data' column contains everything from $instance that does - // not have its own column and is not automatically populated when the - // instance is read. - $data = $instance; - unset($data['id'], $data['field_id'], $data['field_name'], $data['entity_type'], $data['bundle'], $data['deleted']); - - $record = array( - 'field_id' => $instance['field_id'], - 'field_name' => $instance['field_name'], - 'entity_type' => $instance['entity_type'], - 'bundle' => $instance['bundle'], - 'data' => $data, - 'deleted' => $instance['deleted'], - ); - // We need to tell drupal_update_record() the primary keys to trigger an - // update. - if ($update) { - $record['id'] = $instance['id']; - drupal_write_record('field_config_instance', $record, array('id')); - } - else { - drupal_write_record('field_config_instance', $record); - $instance['id'] = $record['id']; + // Save into config. + if ($create) { + $field_instance = entity_create('field_instance', array( + 'id' => $instance['entity_type'] . '.' . $instance['bundle'] . '.' . $instance['field_name'], + 'label' => $instance['entity_type'] . '.' . $instance['bundle'] . '.' . $instance['field_name'], + 'data' => $instance, + )); + } + else { + $field_instance = entity_load('field_instance', $instance['entity_type'] . '.' . $instance['bundle'] . '.' . $instance['field_name']); + $field_instance->data = $instance; } + + $field_instance->save(); } /** @@ -673,8 +640,8 @@ function field_read_instance($entity_type, $field_name, $bundle, $include_additi * * @param $param * An array of properties to use in selecting a field instance. Valid keys - * include any column of the field_config_instance table. If NULL, all - * instances will be returned. + * include any property of the instance config object, except for data. + * If NULL, all instances will be returned. * @param $include_additional * The default behavior of this function is to not return field instances that * have been marked deleted, or whose field is inactive. Setting @@ -685,44 +652,74 @@ function field_read_instance($entity_type, $field_name, $bundle, $include_additi * An array of instances matching the arguments. */ function field_read_instances($params = array(), $include_additional = array()) { - $include_inactive = isset($include_additional['include_inactive']) && $include_additional['include_inactive']; - $include_deleted = isset($include_additional['include_deleted']) && $include_additional['include_deleted']; + $instances = array(); - $query = db_select('field_config_instance', 'fci', array('fetch' => PDO::FETCH_ASSOC)); - $query->join('field_config', 'fc', 'fc.id = fci.field_id'); - $query->fields('fci'); + $deleted_fields = state()->get('field.field.deleted'); + $include_inactive = isset($include_additional['include_inactive']) && $include_additional['include_inactive']; + $include_deleted = (isset($include_additional['include_deleted']) && $include_additional['include_deleted']) || (isset($params['deleted']) && $params['deleted']); - // Turn the conditions into a query. - foreach ($params as $key => $value) { - $query->condition('fci.' . $key, $value); - } - if (!$include_inactive) { - $query - ->condition('fc.active', 1) - ->condition('fc.storage_active', 1); + if ($include_deleted) { + $deleted_instances = state()->get('field.instance.deleted') ?: array(); } - if (!$include_deleted) { - $query->condition('fc.deleted', 0); - $query->condition('fci.deleted', 0); + + // Get configuration instances. + $config_instances = entity_load_multiple('field_instance'); + // Merge deleted instances. + if (!empty($deleted_instances)) { + $config_instances += $deleted_instances; } - $instances = array(); - $results = $query->execute(); + foreach ($config_instances as $config) { + if (is_object($config)) { + $instance = $config->data; + } + else { + $instance = $config; + } + + $entity_info = entity_get_info($instance['entity_type']); + + // Get data from the field. If the field is marked as deleted, we + // need to get it from the state storage. + $field = entity_load('field_entity', $instance['field_name']); + if (is_object($field)) { + $field = $field->data; + } + if ($include_deleted) { + if (empty($field) && isset($deleted_fields[$instance['field_id']])) { + $field = $deleted_fields[$instance['field_id']]; + } + } + if (empty($field)) { + continue; + } + + $instance['active'] = $field['active']; + $instance['locked'] = $field['locked']; + $instance['type'] = $field['type']; + $instance['module'] = $field['module']; + $instance['storage_type'] = $field['storage_type']; + $instance['storage_active'] = $field['storage_active']; + $instance['storage_module'] = $field['storage_module']; - foreach ($results as $record) { - // Filter out instances on unknown entity types (for instance because the - // module exposing them was disabled). - $entity_info = entity_get_info($record['entity_type']); if ($include_inactive || $entity_info) { - $instance = unserialize($record['data']); - $instance['id'] = $record['id']; - $instance['field_id'] = $record['field_id']; - $instance['field_name'] = $record['field_name']; - $instance['entity_type'] = $record['entity_type']; - $instance['bundle'] = $record['bundle']; - $instance['deleted'] = $record['deleted']; + // Conditions. + if (!$include_inactive) { + $params['active'] = 1; + $params['storage_active'] = 1; + } + if (!$include_deleted) { + $params['deleted'] = 0; + } + foreach ($params as $key => $value) { + if ($instance[$key] != $value) { + continue 2; + } + } + // Invoke read instance. module_invoke_all('field_read_instance', $instance); + $instances[] = $instance; } } @@ -740,13 +737,14 @@ function field_read_instances($params = array(), $include_additional = array()) * fields left without instances. Defaults to TRUE. */ function field_delete_instance($instance, $field_cleanup = TRUE) { - // Mark the field instance for deletion. - db_update('field_config_instance') - ->fields(array('deleted' => 1)) - ->condition('field_name', $instance['field_name']) - ->condition('entity_type', $instance['entity_type']) - ->condition('bundle', $instance['bundle']) - ->execute(); + + // Delete the configuration of this instance and save the configuration + // in the key_value table so we can use it later during field_purge_batch(). + $deleted_instances = state()->get('field.instance.deleted') ?: array(); + $instance['deleted'] = TRUE; + $deleted_instances[$instance['id']] = $instance; + state()->set('field.instance.deleted', $deleted_instances); + entity_load('field_instance', $instance['entity_type'] . '.' . $instance['bundle'] . '.' . $instance['field_name'])->delete(); // Clear the cache. field_cache_clear(); @@ -897,8 +895,8 @@ function field_purge_batch($batch_size) { } // Retrieve all deleted fields. Any that have no instances can be purged. - $fields = field_read_fields(array('deleted' => 1), array('include_deleted' => 1)); - foreach ($fields as $field) { + $deleted_fields = state()->get('field.field.deleted') ?: array(); + foreach ($deleted_fields as $field) { $instances = field_read_instances(array('field_id' => $field['id']), array('include_deleted' => 1)); if (empty($instances)) { field_purge_field($field); @@ -946,14 +944,14 @@ function field_purge_data(EntityInterface $entity, $field, $instance) { * The instance record to purge. */ function field_purge_instance($instance) { - db_delete('field_config_instance') - ->condition('id', $instance['id']) - ->execute(); - // Notify the storage engine. $field = field_info_field_by_id($instance['field_id']); module_invoke($field['storage']['module'], 'field_storage_purge_instance', $instance); + $deleted_instances = state()->get('field.instance.deleted'); + unset($deleted_instances[$instance['id']]); + state()->set('field.instance.deleted', $deleted_instances); + // Clear the cache. field_info_cache_clear(); @@ -976,9 +974,9 @@ function field_purge_field($field) { throw new FieldException(t('Attempt to purge a field @field_name that still has instances.', array('@field_name' => $field['field_name']))); } - db_delete('field_config') - ->condition('id', $field['id']) - ->execute(); + $deleted_fields = state()->get('field.field.deleted'); + unset($deleted_fields[$field['id']]); + state()->set('field.field.deleted', $deleted_fields); // Notify the storage engine. module_invoke($field['storage']['module'], 'field_storage_purge_field', $field); diff --git a/core/modules/field/field.info b/core/modules/field/field.info index c74da0e..3c6c5fb 100644 --- a/core/modules/field/field.info +++ b/core/modules/field/field.info @@ -3,5 +3,4 @@ description = Field API to add fields to entities like nodes and users. package = Core version = VERSION core = 8.x -dependencies[] = field_sql_storage required = TRUE diff --git a/core/modules/field/field.install b/core/modules/field/field.install index 695fd89..40260b8 100644 --- a/core/modules/field/field.install +++ b/core/modules/field/field.install @@ -9,158 +9,7 @@ * Implements hook_schema(). */ function field_schema() { - // Static (meta) tables. - $schema['field_config'] = array( - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'The primary identifier for a field', - ), - 'field_name' => array( - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'description' => 'The name of this field. Non-deleted field names are unique, but multiple deleted fields can have the same name.', - ), - 'type' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'description' => 'The type of this field.', - ), - 'module' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The module that implements the field type.', - ), - 'active' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Boolean indicating whether the module that implements the field type is enabled.', - ), - 'storage_type' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'description' => 'The storage backend for the field.', - ), - 'storage_module' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The module that implements the storage backend.', - ), - 'storage_active' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Boolean indicating whether the module that implements the storage backend is enabled.', - ), - 'locked' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 0, - 'description' => '@TODO', - ), - 'data' => array( - 'type' => 'blob', - 'size' => 'big', - 'not null' => TRUE, - 'serialize' => TRUE, - 'description' => 'Serialized data containing the field properties that do not warrant a dedicated column.', - ), - 'cardinality' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 0, - ), - 'translatable' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 0, - ), - 'deleted' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'primary key' => array('id'), - 'indexes' => array( - 'field_name' => array('field_name'), - // Used by field_read_fields(). - 'active' => array('active'), - 'storage_active' => array('storage_active'), - 'deleted' => array('deleted'), - // Used by field_sync_field_status(). - 'module' => array('module'), - 'storage_module' => array('storage_module'), - 'type' => array('type'), - 'storage_type' => array('storage_type'), - ), - ); - $schema['field_config_instance'] = array( - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'The primary identifier for a field instance', - ), - 'field_id' => array( - 'type' => 'int', - 'not null' => TRUE, - 'description' => 'The identifier of the field attached by this instance', - ), - 'field_name' => array( - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '' - ), - 'entity_type' => array( - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '' - ), - 'bundle' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '' - ), - 'data' => array( - 'type' => 'blob', - 'size' => 'big', - 'not null' => TRUE, - 'serialize' => TRUE, - ), - 'deleted' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'primary key' => array('id'), - 'indexes' => array( - // Used by field_delete_instance(). - 'field_name_bundle' => array('field_name', 'entity_type', 'bundle'), - // Used by field_read_instances(). - 'deleted' => array('deleted'), - ), - ); + $schema['cache_field'] = drupal_get_schema_unprocessed('system', 'cache'); $schema['cache_field']['description'] = 'Cache table for the Field module to store already built field informations.'; @@ -271,7 +120,6 @@ function _update_7000_field_delete_field($field_name) { ->execute(); } - /** * Deletes an instance and all its data of a field stored in SQL Storage. * @@ -382,15 +230,14 @@ function _update_7000_field_create_instance($field, &$instance) { */ /** - * Reassign all list.module fields to be controlled by options.module. + * Implements hook_update_dependencies(). */ -function field_update_8001() { - db_update('field_config') - ->fields(array( - 'module' => 'options', - )) - ->condition('module', 'list') - ->execute(); +function field_update_dependencies() { + // Convert to config after SQL storage has been updated. + $dependencies['field_sql_storage'][8000] = array( + 'field' => 8002, + ); + return $dependencies; } /** @@ -398,7 +245,7 @@ function field_update_8001() { * * @ingroup config_upgrade */ -function field_update_8002() { +function field_update_8001() { $displays = array(); module_load_install('entity'); @@ -442,7 +289,7 @@ function field_update_8002() { // Migration of 'extra_fields' display settings. Avoid calling // entity_get_info() by fetching the relevant variables directly in the - // cariables table. + // variable table. $variables = array_map('unserialize', db_query("SELECT name, value FROM {variable} WHERE name LIKE '%field_bundle_settings_%'")->fetchAllKeyed()); foreach ($variables as $variable_name => $variable_value) { if (preg_match('/field_bundle_settings_(.*)__(.*)/', $variable_name, $matches)) { @@ -484,6 +331,85 @@ function field_update_8002() { } /** + * Convert fields and instances to config. + */ +function field_update_8002() { + $manifest_ids = array('fields' => array(), 'instances' => array()); + + $deleted_fields = state()->get('field.field.deleted') ?: array(); + $deleted_instances = state()->get('field.instance.deleted') ?: array(); + + $field_ids = array(); + $fields = db_query("SELECT * FROM {field_config}")->fetchAll(PDO::FETCH_ASSOC); + foreach ($fields as $field) { + $field += unserialize($field['data']); + unset($field['data']); + + // Generate id for the field. + $old_id = $field['id']; + $field['id'] = field_generate_id(); + + // Reassign all list.module fields to be controlled by options.module. + if ($field['module'] == 'list') { + $field['module'] = 'options'; + } + + $field['storage_type'] = $field['storage']['type']; + $field['storage_module'] = $field['storage']['module']; + $field['storage_active'] = $field['storage']['active']; + + if (!$field['deleted']) { + $e_field = entity_create('field_entity', array( + 'id' => $field['field_name'], + 'label' => $field['field_name'], + 'data' => $field, + )); + $e_field->save(); + $manifest_ids['fields'][] = $field['field_name']; + } + else { + $deleted_fields[$field['field_name']] = $field; + } + $field_ids[$old_id] = array( + 'id' => $field['id'], + ); + } + + $instances = db_query("SELECT * FROM {field_config_instance}")->fetchAll(PDO::FETCH_ASSOC); + foreach ($instances as $instance) { + $instance += unserialize($instance['data']); + unset($instance['data']); + + // Map old field id to new UUID. + $old_id = $instance['field_id']; + $instance['field_id'] = $field_ids[$old_id]['id']; + + // Generate id for the instance. + $instance['id'] = field_generate_id(); + + if (!$instance['deleted']) { + $field_instance = entity_create('field_instance', array( + 'id' => $instance['entity_type'] . '.' . $instance['bundle'] . '.' . $instance['field_name'], + 'label' => $instance['entity_type'] . '.' . $instance['bundle'] . '.' . $instance['field_name'], + 'data' => $instance, + ))->save(); + $manifest_ids['instances'][] = $instance['entity_type'] . '.' . $instance['bundle'] . '.' . $instance['field_name']; + } + else { + $deleted_instances[$instance['id']] = $instance; + } + } + + // Create the manifest files. + update_config_manifest_add('field.field', $manifest_ids['fields']); + update_config_manifest_add('field.instance', $manifest_ids['instances']); + + // Save the deleted fields and instances in state. + state()->set('field.field.deleted', $deleted_fields); + state()->set('field.instance.deleted', $deleted_instances); +} + +/** * @} End of "addtogroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */ diff --git a/core/modules/field/field.module b/core/modules/field/field.module index 36bd18a..6f3dc79 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -6,6 +6,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Template\Attribute; +use Drupal\Component\Uuid\Uuid; /* * Load all public Field API functions. Drupal currently has no @@ -334,7 +335,9 @@ function field_cron() { * required if there are any active fields of that type. */ function field_system_info_alter(&$info, $file, $type) { - if ($type == 'module' && module_hook($file->name, 'field_info')) { + // During a Drupal 7 to Drupal 8 upgrade it is only safe to call + // field_read_fields() once field_update_8002() has finished. + if ($type == 'module' && module_hook($file->name, 'field_info') && drupal_get_installed_schema_version('field') >= 8002) { $fields = field_read_fields(array('module' => $file->name), array('include_deleted' => TRUE)); if ($fields) { $info['required'] = TRUE; @@ -536,23 +539,45 @@ function field_modules_disabled($modules) { } /** - * Refreshes the 'active' and 'storage_active' columns for fields. + * Refreshes the 'active' and 'storage_active' values for fields. */ function field_sync_field_status() { - // Refresh the 'active' and 'storage_active' columns according to the current + + $deleted_fields = state()->get('field.field.deleted') ?: array(); + $fields = field_read_fields(array(), array('include_deleted' => 1 ,'include_inactive' => 1)); + // Refresh the 'active' and 'storage_active' values according to the current // set of enabled modules. $modules = array_keys(drupal_container()->get('module_handler')->getModuleList()); foreach ($modules as $module_name) { - field_associate_fields($module_name); + $fields = field_associate_fields($module_name, $fields); } - db_update('field_config') - ->fields(array('active' => 0)) - ->condition('module', $modules, 'NOT IN') - ->execute(); - db_update('field_config') - ->fields(array('storage_active' => 0)) - ->condition('storage_module', $modules, 'NOT IN') - ->execute(); + + foreach ($fields as $id => $field) { + if (!in_array($field['module'], $modules)) { + $fields[$id]['active'] = 0; + } + if (!in_array($field['storage_module'], $modules)) { + $fields[$id]['storage_active'] = 0; + } + } + + foreach ($fields as $id => $field) { + if (!$field['deleted']) { + // We can not use field_update_field because the prior_field does not + // check whether a field is really there or not. + $e_field = entity_load('field_entity', $field['field_name']); + $e_field->data = $field; + $e_field->save(); + } + else { + $deleted_fields[$field['id']] = $field; + } + } + + // Save the deleted fields. + state()->set('field.field.deleted', $deleted_fields); + + field_cache_clear(); } /** @@ -560,24 +585,37 @@ function field_sync_field_status() { * * @param $module * The name of the module to update on. + * @param $fields + * A collection of fields. */ -function field_associate_fields($module) { +function field_associate_fields($module, $fields) { + // Associate field types. $field_types = (array) module_invoke($module, 'field_info'); + if ($field_types) { - db_update('field_config') - ->fields(array('module' => $module, 'active' => 1)) - ->condition('type', array_keys($field_types)) - ->execute(); + $field_types = array_keys($field_types); + foreach ($fields as $id => $field) { + if (in_array($field['type'], $field_types)) { + $fields[$id]['module'] = $module; + $fields[$id]['active'] = TRUE; + } + } } + // Associate storage backends. $storage_types = (array) module_invoke($module, 'field_storage_info'); if ($storage_types) { - db_update('field_config') - ->fields(array('storage_module' => $module, 'storage_active' => 1)) - ->condition('storage_type', array_keys($storage_types)) - ->execute(); + $storage_types = array_keys($storage_types); + foreach ($fields as $id => $field) { + if (in_array($field['storage_type'], $storage_types)) { + $fields[$id]['storage_module'] = $module; + $fields[$id]['storage_active'] = TRUE; + } + } } + + return $fields; } /** @@ -784,6 +822,21 @@ function field_cache_clear() { } /** + * Generate an id for fields or instances. + * + * When a field is a deleted, the tables are renamed to {field_data_field_id} + * and {field_revision_field_id}. To make sure alternative uuid implementations + * don't generate longer uuid's and using str_replace() to replace dashes + * to underscores might end up in table names longer than 64 characters, we + * hash the uuid and then take the first 6 characters so we end up with a short + * unique id. + */ +function field_generate_id() { + $uuid = new Uuid(); + return substr(hash('sha256', $uuid->generate()), 0, 6); +} + +/** * Filters an HTML string to prevent cross-site-scripting (XSS) vulnerabilities. * * Like filter_xss_admin(), but with a shorter list of allowed tags. diff --git a/core/modules/field/lib/Drupal/field/FieldInfo.php b/core/modules/field/lib/Drupal/field/FieldInfo.php index 7ff6850..36c28e0 100644 --- a/core/modules/field/lib/Drupal/field/FieldInfo.php +++ b/core/modules/field/lib/Drupal/field/FieldInfo.php @@ -142,17 +142,10 @@ public function getFieldMap() { $map = array(); - $query = db_select('field_config_instance', 'fci'); - $query->join('field_config', 'fc', 'fc.id = fci.field_id'); - $query->fields('fc', array('type')); - $query->fields('fci', array('field_name', 'entity_type', 'bundle')) - ->condition('fc.active', 1) - ->condition('fc.storage_active', 1) - ->condition('fc.deleted', 0) - ->condition('fci.deleted', 0); - foreach ($query->execute() as $row) { - $map[$row->field_name]['bundles'][$row->entity_type][] = $row->bundle; - $map[$row->field_name]['type'] = $row->type; + $instances = field_read_instances(); + foreach ($instances as $key => $instance) { + $map[$instance['field_name']]['bundles'][$instance['entity_type']][] = $instance['bundle']; + $map[$instance['field_name']]['type'] = $instance['type']; } // Save in "static" and persistent caches. @@ -382,7 +375,7 @@ public function getBundleInstances($entity_type, $bundle) { // Collect the fields in the bundle. $params = array('entity_type' => $entity_type, 'bundle' => $bundle); - $fields = field_read_fields($params); + $fields = field_read_fields(); // This iterates on non-deleted instances, so deleted fields are kept out of // the persistent caches. @@ -413,6 +406,7 @@ public function getBundleInstances($entity_type, $bundle) { // The persistent cache additionally contains the definitions of the fields // involved in the bundle. + uasort($instances, array($this, '_orderInstances')); $cache = array( 'instances' => $instances, 'fields' => array() @@ -553,4 +547,11 @@ public function prepareExtraFields($extra_fields, $entity_type, $bundle) { return $result; } + + /** + * Helper function to sort the instances on the widget weight. + */ + function _orderInstances($a, $b) { + return ($a['widget']['weight'] < $b['widget']['weight']) ? -1 : 1; + } } diff --git a/core/modules/field/lib/Drupal/field/FieldInstanceStorageController.php b/core/modules/field/lib/Drupal/field/FieldInstanceStorageController.php new file mode 100644 index 0000000..8a4d0e9 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/FieldInstanceStorageController.php @@ -0,0 +1,46 @@ +get('data'); + field_create_instance($config); + } + + /** + * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::importChange(). + */ + public function importChange($name, Config $new_config, Config $old_config) { + $config = $new_config->get('data'); + field_update_instance($config); + } + + /** + * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::importDelete(). + */ + public function importDelete($name, Config $new_config, Config $old_config) { + $config = $old_config->get('data'); + // In case the field has been deleted through this import hook, + // the instance will be deleted by then already. + if (!empty($config)) { + field_delete_instance($config); + } + } + +} diff --git a/core/modules/field/lib/Drupal/field/FieldStorageController.php b/core/modules/field/lib/Drupal/field/FieldStorageController.php new file mode 100644 index 0000000..fb53dd1 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/FieldStorageController.php @@ -0,0 +1,42 @@ +get('data'); + field_create_field($config); + } + + /** + * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::importChange(). + */ + public function importChange($name, Config $new_config, Config $old_config) { + $config = $new_config->get('data'); + field_update_field($config); + } + + /** + * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::importDelete(). + */ + public function importDelete($name, Config $new_config, Config $old_config) { + $config = $old_config->get('data'); + field_delete_field($config['field_name']); + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldEntity.php b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldEntity.php new file mode 100644 index 0000000..e58cc71 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldEntity.php @@ -0,0 +1,63 @@ +entity_type, $field['field_name'], $bundle); - field_delete_instance($instance); + field_delete_instance($instance->getArray()); // The instance still exists, deleted. $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1)); @@ -218,7 +220,7 @@ function testPurgeInstance() { // Delete the instance. $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle); - field_delete_instance($instance); + field_delete_instance($instance->getArray()); // No field hooks were called. $mem = field_test_memorize(); @@ -282,7 +284,7 @@ function testPurgeField() { // Delete the first instance. $bundle = reset($this->bundles); $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle); - field_delete_instance($instance); + field_delete_instance($instance->getArray()); // Assert that hook_field_delete() was not called yet. $mem = field_test_memorize(); @@ -313,7 +315,7 @@ function testPurgeField() { // Delete the second instance. $bundle = next($this->bundles); $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle); - field_delete_instance($instance); + field_delete_instance($instance->getArray()); // Assert that hook_field_delete() was not called yet. $mem = field_test_memorize(); diff --git a/core/modules/field/lib/Drupal/field/Tests/CrudTest.php b/core/modules/field/lib/Drupal/field/Tests/CrudTest.php index 1c539da..504236c 100644 --- a/core/modules/field/lib/Drupal/field/Tests/CrudTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/CrudTest.php @@ -45,10 +45,9 @@ function testCreateField() { $mem = field_test_memorize(); $this->assertIdentical($mem['field_test_field_create_field'][0][0], $field_definition, 'hook_field_create_field() called with correct arguments.'); - // Read the raw record from the {field_config_instance} table. - $result = db_query('SELECT * FROM {field_config} WHERE field_name = :field_name', array(':field_name' => $field_definition['field_name'])); - $record = $result->fetchAssoc(); - $record['data'] = unserialize($record['data']); + // Read the configuration. + $record = entity_load('field_entity', $field_definition['field_name']); + $record = $record->data; // Ensure that basic properties are preserved. $this->assertEqual($record['field_name'], $field_definition['field_name'], 'The field name is properly saved.'); @@ -59,7 +58,7 @@ function testCreateField() { // Ensure that default settings are present. $field_type = field_info_field_types($field_definition['type']); - $this->assertIdentical($record['data']['settings'], $field_type['settings'], 'Default field settings have been written.'); + $this->assertEqual($record['settings'], $field_type['settings'], 'Default field settings have been written.'); // Ensure that default storage was set. $this->assertEqual($record['storage_type'], variable_get('field_storage_default'), 'The field type is properly saved.'); @@ -157,11 +156,10 @@ function testCreateField() { function testCreateFieldFail() { $field_name = 'duplicate'; $field_definition = array('field_name' => $field_name, 'type' => 'test_field', 'storage' => array('type' => 'field_test_storage_failure')); - $query = db_select('field_config')->condition('field_name', $field_name)->countQuery(); + $field = entity_load('field_entity', $field_name); - // The field does not appear in field_config. - $count = $query->execute()->fetchField(); - $this->assertEqual($count, 0, 'A field_config row for the field does not exist.'); + // The field does not exist. + $this->assertFalse($field, 'The field does not exist.'); // Try to create the field. try { @@ -172,9 +170,10 @@ function testCreateFieldFail() { $this->assertTrue(TRUE, 'Field creation (correctly) fails.'); } - // The field does not appear in field_config. - $count = $query->execute()->fetchField(); - $this->assertEqual($count, 0, 'A field_config row for the field does not exist.'); + // The field does not exist. + // $field = config('field.field.' . $field_name)->get(); + $field = entity_load('field_entity', $field_name); + $this->assertFalse($field, 'The field does not exist.'); } /** @@ -221,10 +220,11 @@ function testReadFields() { field_create_instance($instance_definition); // Check that criteria spanning over the field_config_instance table work. - $fields = field_read_fields(array('entity_type' => $instance_definition['entity_type'], 'bundle' => $instance_definition['bundle'])); + // @todo do we still want to support this ? + /*$fields = field_read_fields(array('entity_type' => $instance_definition['entity_type'], 'bundle' => $instance_definition['bundle'])); $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.'); $fields = field_read_fields(array('entity_type' => $instance_definition['entity_type'], 'field_name' => $instance_definition['field_name'])); - $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.'); + $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.');*/ } /** @@ -303,8 +303,8 @@ function testDeleteField() { // Make sure that the field is marked as deleted when it is specifically // loaded. - $field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE)); - $this->assertTrue(!empty($field['deleted']), 'A deleted field is marked for deletion.'); + $deleted_fields = state()->get('field.field.deleted'); + $this->assertTrue(isset($deleted_fields[$field['id']]), 'A deleted field is marked for deletion.'); // Make sure that this field's instance is marked as deleted when it is // specifically loaded. @@ -505,6 +505,6 @@ function _testActiveHelper($field_definition, $modules) { // Check that the field is active again after all modules have been // enabled. $field = field_read_field($field_name); - $this->assertTrue($field_definition <= $field, 'The field was was marked active.'); + $this->assertTrue($field_definition <= $field, 'The field was marked active.'); } } diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php new file mode 100644 index 0000000..67657da --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php @@ -0,0 +1,69 @@ + 'Field config change tests', + 'description' => 'Update field and instances during config change method invocation.', + 'group' => 'Field API', + ); + } + + function setUp() { + parent::setUp(); + + $this->instance_manifest = 'manifest.field.instance'; + $this->instance_name = 'field.instance.node.test_import.field_test_import'; + $this->drupalCreateContentType(array('type' => 'test_import', 'name' => 'Test import')); + + $admin_user = $this->drupalCreateUser(array('access administration pages', 'access content overview', 'administer nodes', 'bypass node access')); + $this->drupalLogin($admin_user); + } + + /** + * Test importing changes. + */ + function testImportChange() { + + // Assert default test import. + $this->drupalGet('node/add/test_import'); + $this->assertRaw('Test import field'); + + // Change label. + $active = $this->container->get('config.storage'); + $staging = $this->container->get('config.storage.staging'); + $manifest = $active->read($this->instance_manifest); + $instance = $active->read($this->instance_name); + $instance['data']['label'] = 'Test update import field'; + $staging->write($this->instance_name, $instance); + $staging->write($this->instance_manifest, $manifest); + + // Import. + config_import(); + + // Assert updated label. + $this->drupalGet('node/add/test_import'); + $this->assertText('Test update import field'); + } +} diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php new file mode 100644 index 0000000..6bda93f --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php @@ -0,0 +1,87 @@ + 'Field config create tests', + 'description' => 'Create field and instances during config create method invocation.', + 'group' => 'Field API', + ); + } + + function setUp() { + parent::setUp(); + + $this->field_test_import_staging = 'field.field.field_test_import_staging'; + $this->instance_test_import_staging = 'field.instance.node.test_import.field_test_import_staging'; + $this->field_manifest = 'manifest.field.field'; + $this->instance_manifest = 'manifest.field.instance'; + $this->drupalCreateContentType(array('type' => 'test_import', 'name' => 'Test import')); + + $admin_user = $this->drupalCreateUser(array('access administration pages', 'access content overview', 'administer nodes', 'bypass node access')); + $this->drupalLogin($admin_user); + } + + /** + * Test importing new fields. + */ + function testImportCreate() { + + // Assert default test import. + $this->drupalGet('node/add/test_import'); + $this->assertNoText('Test import field'); + + // Enable field_test_config module and assert the test import + // field and instance is available on the Test content type. + // This tests creating fields and instances that are provided + // by a module. + module_enable(array('field_test_config')); + $this->drupalGet('node/add/test_import'); + $this->assertText('Test import field'); + $module_path = drupal_get_path('module', 'field_test_config'); + + // Copy another field and instance to the staging directory + // on the Test content type and run config_import() to test + // importing from the staging directory. + $active = $this->container->get('config.storage'); + $staging = $this->container->get('config.storage.staging'); + $field_manifest = $active->read($this->field_manifest); + $instance_manifest = $active->read($this->instance_manifest); + + // Copy the files. + $copied = file_unmanaged_copy($module_path .'/staging/' . $this->field_test_import_staging . '.yml', 'public://config_staging/' . $this->field_test_import_staging . '.yml'); + $this->assertTrue($copied); + $copied = file_unmanaged_copy($module_path .'/staging/' . $this->instance_test_import_staging . '.yml', 'public://config_staging/' . $this->instance_test_import_staging . '.yml'); + $this->assertTrue($copied); + + // Add to manifest. + $field_manifest['field_test_import_staging'] = array('name' => $this->field_test_import_staging); + $instance_manifest['node.test_import.field_test_import_staging'] = array('name' => $this->instance_test_import_staging); + + // Write to manifest and new config. + $staging->write($this->field_manifest, $field_manifest); + $staging->write($this->instance_manifest, $instance_manifest); + + // Import. + config_import(); + + // Assert the staging field is there. + $this->drupalGet('node/add/test_import'); + $this->assertText('Import from staging'); + } +} diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php new file mode 100644 index 0000000..bc64f6e --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php @@ -0,0 +1,100 @@ + 'Field config delete tests', + 'description' => 'Delete field and instances during config delete method invocation.', + 'group' => 'Field API', + ); + } + + function setUp() { + parent::setUp(); + + $this->body_field_name = 'field.field.body'; + $this->test_import_field_name = 'field.field.test_import'; + $this->body_instance_name = 'field.instance.node.test_import.body'; + $this->test_import_instance_name = 'field.instance.node.test_import.field_test_import'; + $this->field_manifest = 'manifest.field.field'; + $this->instance_manifest = 'manifest.field.instance'; + $this->drupalCreateContentType(array('type' => 'test_import', 'name' => 'Test import')); + + $admin_user = $this->drupalCreateUser(array('access administration pages', 'access content overview', 'administer nodes', 'bypass node access')); + $this->drupalLogin($admin_user); + } + + /** + * Test importing deletions. + */ + function testImportDelete() { + + $body_field = field_info_field('body'); + $field_test_import = field_info_field('field_test_import'); + + // Assert default test import. + $this->drupalGet('node/add/test_import'); + $this->assertRaw('Test import field'); + $this->assertRaw('Body'); + + // Delete body field and instance, the test import instance + // from the manifest. + $active = $this->container->get('config.storage'); + $staging = $this->container->get('config.storage.staging'); + $field_manifest = $active->read($this->field_manifest); + $instance_manifest = $active->read($this->instance_manifest); + unset($field_manifest['body']); + unset($instance_manifest['node.test_import.body']); + unset($instance_manifest['node.test_import.field_test_import']); + $staging->write($this->field_manifest, $field_manifest); + $staging->write($this->instance_manifest, $instance_manifest); + + // Import. + config_import(); + + // Assert the field and instance are gone from the form. + $this->drupalGet('node/add/test_import'); + $this->assertNoText('Test import field'); + $this->assertNoText('Body'); + + // Check that body and import field are in state of deleted fields. + $deleted_fields = state()->get('field.field.deleted') ?: array(); + $this->assertTrue(isset($deleted_fields[$body_field['id']])); + $this->assertTrue(isset($deleted_fields[$field_test_import['id']])); + + // Run purge_batch(). + field_purge_batch(10); + + // Check that the deleted fields are removed from state. + $deleted_fields = state()->get('field.field.deleted') ?: array(); + $this->assertTrue(empty($deleted_fields), 'Fields are deleted'); + + // Check all config files are gone. + $active = $this->container->get('config.storage'); + $this->assertIdentical($active->listAll($this->body_field_name), array()); + $this->assertIdentical($active->listAll($this->test_import_field_name), array()); + $this->assertIdentical($active->listAll($this->body_instance_name), array()); + $this->assertIdentical($active->listAll($this->test_import_instance_name), array()); + } +} diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php index 3115851..08d6197 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php @@ -143,13 +143,10 @@ function testFieldPrepare() { // Simulate a stored field definition missing a field setting (e.g. a // third-party module adding a new field setting has been enabled, and // existing fields do not know the setting yet). - $data = db_query('SELECT data FROM {field_config} WHERE field_name = :field_name', array(':field_name' => $field_definition['field_name']))->fetchField(); - $data = unserialize($data); - $data['settings'] = array(); - db_update('field_config') - ->fields(array('data' => serialize($data))) - ->condition('field_name', $field_definition['field_name']) - ->execute(); + $field = entity_load('field_entity', $field_definition['field_name']); + $field = $field->data; + $field['data']['settings'] = array(); + field_update_field($field); field_cache_clear(); @@ -158,7 +155,7 @@ function testFieldPrepare() { // Check that all expected settings are in place. $field_type = field_info_field_types($field_definition['type']); - $this->assertIdentical($field['settings'], $field_type['settings'], 'All expected default field settings are present.'); + $this->assertEqual($field['settings'], $field_type['settings'], 'All expected default field settings are present.'); } /** @@ -180,25 +177,19 @@ function testInstancePrepare() { // Simulate a stored instance definition missing various settings (e.g. a // third-party module adding instance or widget settings has been enabled, // but existing instances do not know the new settings). - $data = db_query('SELECT data FROM {field_config_instance} WHERE field_name = :field_name AND bundle = :bundle', array(':field_name' => $instance_definition['field_name'], ':bundle' => $instance_definition['bundle']))->fetchField(); - $data = unserialize($data); - $data['settings'] = array(); - $data['widget']['settings'] = 'unavailable_widget'; - $data['widget']['settings'] = array(); - db_update('field_config_instance') - ->fields(array('data' => serialize($data))) - ->condition('field_name', $instance_definition['field_name']) - ->condition('bundle', $instance_definition['bundle']) - ->execute(); - - field_cache_clear(); + $entity_instance = entity_load('field_instance', $instance_definition['entity_type'] . '.' . $instance_definition['bundle'] . '.' . $instance_definition['field_name']); + $instance = $entity_instance->data; + $instance['settings'] = array(); + $instance['widget']['settings'] = 'unavailable_widget'; + $instance['widget']['settings'] = array(); + field_update_instance($instance); // Read the instance back. $instance = field_info_instance($instance_definition['entity_type'], $instance_definition['field_name'], $instance_definition['bundle']); // Check that all expected instance settings are in place. $field_type = field_info_field_types($field_definition['type']); - $this->assertIdentical($instance['settings'], $field_type['instance_settings'] , 'All expected instance settings are present.'); + $this->assertEqual($instance['settings'], $field_type['instance_settings'] , 'All expected instance settings are present.'); // Check that the default widget is used and expected settings are in place. $widget = $instance->getWidget(); diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldInstanceCrudTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldInstanceCrudTest.php index 90fcc05..d62fd02 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldInstanceCrudTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldInstanceCrudTest.php @@ -8,6 +8,7 @@ namespace Drupal\field\Tests; use Drupal\field\FieldException; +use Drupal\field\FieldInstance; class FieldInstanceCrudTest extends FieldTestBase { @@ -55,10 +56,9 @@ function setUp() { function testCreateFieldInstance() { field_create_instance($this->instance_definition); - // Read the raw record from the {field_config_instance} table. - $result = db_query('SELECT * FROM {field_config_instance} WHERE field_name = :field_name AND bundle = :bundle', array(':field_name' => $this->instance_definition['field_name'], ':bundle' => $this->instance_definition['bundle'])); - $record = $result->fetchAssoc(); - $record['data'] = unserialize($record['data']); + // Read the configuration. + $e_record = entity_load('field_instance', $this->instance_definition['entity_type'] . '.' . $this->instance_definition['bundle'] . '.' . $this->instance_definition['field_name']); + $record = $e_record->data; $field_type = field_info_field_types($this->field['type']); $widget_type = field_info_widget_types($field_type['default_widget']); @@ -67,14 +67,14 @@ function testCreateFieldInstance() { $this->assertIdentical($record['id'], $this->instance_definition['id'], 'The instance id is filled in'); // Check that default values are set. - $this->assertIdentical($record['data']['required'], FALSE, 'Required defaults to false.'); - $this->assertIdentical($record['data']['label'], $this->instance_definition['field_name'], 'Label defaults to field name.'); - $this->assertIdentical($record['data']['description'], '', 'Description defaults to empty string.'); - $this->assertIdentical($record['data']['widget']['type'], $field_type['default_widget'], 'Default widget has been written.'); + $this->assertEqual($record['required'], FALSE, 'Required defaults to false.'); + $this->assertIdentical($record['label'], $this->instance_definition['field_name'], 'Label defaults to field name.'); + $this->assertIdentical($record['description'], '', 'Description defaults to empty string.'); + $this->assertIdentical($record['widget']['type'], $field_type['default_widget'], 'Default widget has been written.'); // Check that default settings are set. - $this->assertIdentical($record['data']['settings'], $field_type['instance_settings'] , 'Default instance settings have been written.'); - $this->assertIdentical($record['data']['widget']['settings'], $widget_type['settings'] , 'Default widget settings have been written.'); + $this->assertEqual($record['settings'], $field_type['instance_settings'] , 'Default instance settings have been written.'); + $this->assertIdentical($record['widget']['settings'], $widget_type['settings'] , 'Default widget settings have been written.'); // Guarantee that the field/bundle combination is unique. try { @@ -139,7 +139,13 @@ function testReadFieldInstance() { // Read the instance back. $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); - $this->assertTrue($this->instance_definition == $instance, 'The field was properly read.'); + $this->assertTrue($this->instance_definition['id'] == $instance['id'], 'The field was properly read.'); + $this->assertTrue($this->instance_definition['label'] == $instance['label'], 'The field was properly read.'); + $this->assertTrue($this->instance_definition['field_id'] == $instance['field_id'], 'The field was properly read.'); + $this->assertTrue($this->instance_definition['entity_type'] == $instance['entity_type'], 'The field was properly read.'); + $this->assertTrue($this->instance_definition['bundle'] == $instance['bundle'], 'The field was properly read.'); + $this->assertTrue($this->instance_definition['settings'] == $instance['settings'], 'The field was properly read.'); + $this->assertTrue($this->instance_definition['widget'] == $instance['widget'], 'The field was properly read.'); } /** @@ -213,7 +219,9 @@ function testDeleteFieldInstance() { // Make sure the field is deleted when its last instance is deleted. field_delete_instance($another_instance); - $field = field_read_field($another_instance['field_name'], array('include_deleted' => TRUE)); - $this->assertTrue(!empty($field['deleted']), 'A deleted field is marked for deletion after all its instances have been marked for deletion.'); + $deleted_fields = state()->get('field.field.deleted'); + $this->assertTrue(isset($deleted_fields[$another_instance['field_id']]), 'A deleted field is marked for deletion.'); + $field = field_read_field(array('field_name' => $another_instance['field_name'])); + $this->assertFalse($field, 'The field marked to be deleted is not found anymore in the configuration.'); } } diff --git a/core/modules/field/tests/modules/field_test_config/config/field.field.field_test_import.yml b/core/modules/field/tests/modules/field_test_config/config/field.field.field_test_import.yml new file mode 100644 index 0000000..b9d108d --- /dev/null +++ b/core/modules/field/tests/modules/field_test_config/config/field.field.field_test_import.yml @@ -0,0 +1,53 @@ +id: field_test_import +uuid: fb38277f-1fd4-49d5-8d09-9d7037fdcce9 +name: '' +label: field_test_import +data: + field_name: field_test_import + type: text + translatable: false + entity_types: { } + cardinality: '1' + locked: false + settings: + max_length: '255' + storage: + type: field_sql_storage + settings: { } + module: field_sql_storage + active: 1 + details: + sql: + FIELD_LOAD_CURRENT: + field_data_field_test_import: + value: field_test_import_value + format: field_test_import_format + FIELD_LOAD_REVISION: + field_revision_field_test_import: + value: field_test_import_value + format: field_test_import_format + deleted: 0 + module: text + active: 1 + columns: + value: + type: varchar + length: '255' + 'not null': false + format: + type: varchar + length: 255 + 'not null': false + 'foreign keys': + format: + table: filter_format + columns: + format: format + indexes: + format: + - format + storage_type: field_sql_storage + storage_module: field_sql_storage + storage_active: 1 + id: b5a22b +langcode: und diff --git a/core/modules/field/tests/modules/field_test_config/config/field.instance.node.test_import.field_test_import.yml b/core/modules/field/tests/modules/field_test_config/config/field.instance.node.test_import.field_test_import.yml new file mode 100644 index 0000000..3920330 --- /dev/null +++ b/core/modules/field/tests/modules/field_test_config/config/field.instance.node.test_import.field_test_import.yml @@ -0,0 +1,33 @@ +id: node.test_import.field_test_import +uuid: 392b4e9d-6157-412e-9603-3d622512f498 +name: '' +label: node.test_import.field_test_import +data: + field_name: field_test_import + entity_type: node + bundle: test_import + label: 'Test import field' + widget: + weight: '-2' + type: text_textfield + module: text + active: 0 + settings: + size: '60' + field_id: b5a22b + id: 948dff + settings: + text_processing: '0' + user_register_form: false + required: 0 + description: '' + deleted: 0 + active: 1 + locked: false + type: text + module: text + storage_type: field_sql_storage + storage_active: 1 + storage_module: field_sql_storage + default_value: null +langcode: und diff --git a/core/modules/field/tests/modules/field_test_config/field_test_config.info b/core/modules/field/tests/modules/field_test_config/field_test_config.info new file mode 100644 index 0000000..49b0509 --- /dev/null +++ b/core/modules/field/tests/modules/field_test_config/field_test_config.info @@ -0,0 +1,6 @@ +name = "Field API configuration tests" +description = "Support module for the Field API configuration tests." +core = 8.x +package = Testing +version = VERSION +hidden = TRUE diff --git a/core/modules/field/tests/modules/field_test_config/field_test_config.module b/core/modules/field/tests/modules/field_test_config/field_test_config.module new file mode 100644 index 0000000..e208464 --- /dev/null +++ b/core/modules/field/tests/modules/field_test_config/field_test_config.module @@ -0,0 +1,6 @@ + TRUE, 'include_inactive' => TRUE)); - drupal_load('module', 'field_sql_storage'); - foreach ($fields as $field) { + // During a Drupal 7 to Drupal 8 upgrade it is only safe to call + // field configurations once field_update_8002() has finished. + if (drupal_get_installed_schema_version('field') >= 8002) { + // We don't call field_read_fields() as this will cause endless loops + // in entity_get_info(). + $fields = config_get_storage_names_with_prefix('field.field'); + foreach ($fields as $name) { + $field = config($name)->get('data'); if ($field['storage']['type'] == 'field_sql_storage') { $schema += _field_sql_storage_schema($field); } } } + return $schema; } diff --git a/core/modules/field_ui/field_ui.admin.inc b/core/modules/field_ui/field_ui.admin.inc index fd225ca..d26cd56 100644 --- a/core/modules/field_ui/field_ui.admin.inc +++ b/core/modules/field_ui/field_ui.admin.inc @@ -763,7 +763,7 @@ function field_ui_field_delete_form_submit($form, &$form_state) { $bundle_label = $bundles[$entity_type][$bundle]['label']; if (!empty($bundle) && $field && !$field['locked'] && $form_values['confirm']) { - field_delete_instance($instance); + field_delete_instance($instance->getArray()); drupal_set_message(t('The field %field has been deleted from the %type content type.', array('%field' => $instance['label'], '%type' => $bundle_label))); } else { diff --git a/core/modules/file/file.install b/core/modules/file/file.install index 27a2438..472f7e5 100644 --- a/core/modules/file/file.install +++ b/core/modules/file/file.install @@ -125,8 +125,8 @@ function file_schema() { ), 'id' => array( 'description' => 'The primary key of the object using the file.', - 'type' => 'int', - 'unsigned' => TRUE, + 'type' => 'varchar', + 'length' => 64, 'not null' => TRUE, 'default' => 0, ), @@ -234,6 +234,21 @@ function file_requirements($phase) { } /** + * Implements hook_update_dependencies(). + */ +function file_update_dependencies() { + // The update hook to convert the id column in the column table needs to be + // the first one because user update hooks will be called during system + // update hooks due to other dependencies and the user picture conversion + // might need to save a default image and the field it's stored in, does + // not have an integer id anymore. + $dependencies['system'][8001] = array( + 'file' => 8001, + ); + return $dependencies; +} + +/** * Converts default_file_main variable to config. * * @ingroup config_upgrade @@ -245,3 +260,17 @@ function file_update_8000() { 'file_icon_directory'=>'icon.directory', )); } + +/** + * Convert the id column in file_usage to store UUID's. + */ +function file_update_8001() { + $spec = array( + 'description' => 'The primary key of the object using the file.', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ); + db_change_field('file_usage', 'id', 'id', $spec); +} diff --git a/core/modules/forum/forum.install b/core/modules/forum/forum.install index b1e63de..3173cfb 100644 --- a/core/modules/forum/forum.install +++ b/core/modules/forum/forum.install @@ -22,10 +22,6 @@ function forum_install() { * Implements hook_enable(). */ function forum_enable() { - // If we enable forum at the same time as taxonomy we need to call - // field_associate_fields() as otherwise the field won't be enabled until - // hook modules_enabled is called which takes place after hook_enable events. - field_associate_fields('taxonomy'); // Create the forum vocabulary if it does not exist. // @todo Change Forum module so forum.settings can contain the vocabulary's @@ -55,7 +51,7 @@ function forum_enable() { } // Create the 'taxonomy_forums' field if it doesn't already exist. - if (!field_info_field('taxonomy_forums')) { + if (!field_read_field('taxonomy_forums', array('include_inactive' => TRUE))) { $field = array( 'field_name' => 'taxonomy_forums', 'type' => 'taxonomy_term_reference', diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/FieldUpgradePathTest.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/FieldUpgradePathTest.php index c0de21b..89cac76 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Upgrade/FieldUpgradePathTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/FieldUpgradePathTest.php @@ -87,4 +87,16 @@ public function testEntityDisplayUpgrade() { $this->assertEqual($displays['teaser']['content']['language'], $expected['teaser']); } + /** + * Tests upgrade of fields and instances to config. + */ + function testFieldUpgradeToConfig() { + $this->assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + + // Assert the body field and instance on the article are converted. + $body_field = field_info_field('body'); + $this->assertNotNull($body_field, 'The body field has been found.'); + $body_field_instance = field_info_instance('node', 'body', 'article'); + $this->assertNotNull($body_field, 'The body field instance on the article content type has been found.'); + } }