diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 9f81f860bf47cfc57be6bdacc8407ef55e5aee12..7f5dec53013d796365b675bab2d1715d2bb86212 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -112,9 +112,6 @@ function hook_field_extra_fields_alter(&$info) { * appears in edit forms, while @link field_formatter formatters @endlink * specify how the field appears in displayed entities. * - * A third kind of pluggable handler, storage backends, is defined by the - * @link field_storage Field Storage API @endlink. - * * See @link field Field API @endlink for information about the other parts of * the Field API. */ @@ -541,11 +538,10 @@ function hook_field_update_forbid($field, $prior_field) { /** * Acts when a field record is being purged. * - * In field_purge_field(), after the field configuration has been removed from - * the database, the field storage module has had a chance to run its - * hook_field_storage_purge_field(), and the field info cache has been cleared, - * this hook is invoked on all modules to allow them to respond to the field - * being purged. + * In field_purge_field(), after the field definition has been removed from the + * the system, the entity storage has purged stored field data, and the field + * info cache has been cleared, this hook is invoked on all modules to allow + * them to respond to the field being purged. * * @param $field * The field being purged. @@ -559,11 +555,10 @@ function hook_field_purge_field($field) { /** * Acts when a field instance is being purged. * - * In field_purge_instance(), after the field instance has been removed from the - * database, the field storage module has had a chance to run its - * hook_field_storage_purge_instance(), and the field info cache has been - * cleared, this hook is invoked on all modules to allow them to respond to the - * field instance being purged. + * In field_purge_instance(), after the instance definition has been removed + * from the the system, the entity storage has purged stored field data, and the + * field info cache has been cleared, this hook is invoked on all modules to + * allow them to respond to the field instance being purged. * * @param $instance * The instance being purged. diff --git a/core/modules/field/field.attach.inc b/core/modules/field/field.attach.inc index 29036c75c3aad1e85876083aa549cd0e744cfa18..f12459ba19507bb9b8938b3e8695e5e136b2c97e 100644 --- a/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -48,22 +48,7 @@ * allows any module to act on Field Attach operations for any entity after the * operation is complete, and access or modify all the field, form, or display * data for that entity and operation. For example, field_attach_view() invokes - * hook_field_attach_view_alter(). These all-module hooks are distinct from - * those of the Field Types API, such as hook_field_load(), that are only - * invoked for the module that defines a specific field type. - * - * field_attach_load(), field_attach_insert(), and field_attach_update() also - * define pre-operation hooks, e.g. hook_field_attach_pre_load(). These hooks - * run before the corresponding Field Storage API and Field Type API operations. - * They allow modules to define additional storage locations (e.g. - * denormalizing, mirroring) for field data on a per-field basis. They also - * allow modules to take over field storage completely by instructing other - * implementations of the same hook and the Field Storage API itself not to - * operate on specified fields. - * - * The pre-operation hooks do not make the Field Storage API irrelevant. The - * Field Storage API is essentially the "fallback mechanism" for any fields that - * aren't being intercepted explicitly by pre-operation hooks. + * hook_field_attach_view_alter(). * * @link field_language Field language API @endlink provides information about * the structure of field objects. diff --git a/core/modules/field/field.module b/core/modules/field/field.module index 1a673bce786c46e4fec1114f32af2952bf1b78ce..dabe6e7389f5528aff18f9d138e02ccf9328f148 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -69,10 +69,6 @@ * fields, instances, widgets, and related information defined by or with the * Field API. * - * - @link field_storage Field Storage API @endlink: Provides a pluggable back - * -end storage system for actual field data. The default implementation, - * field_sql_storage.module, stores field data in the local SQL database. - * * - @link field_purge Field API bulk data deletion @endlink: Cleans up after * bulk deletion operations such as deletion of field or field_instance. * @@ -113,15 +109,11 @@ /** * Load the most recent version of an entity's field data. - * - * @see field_attach_load(). */ const FIELD_LOAD_CURRENT = 'FIELD_LOAD_CURRENT'; /** * Load the version of an entity's field data specified in the entity. - * - * @see field_attach_load(). */ const FIELD_LOAD_REVISION = 'FIELD_LOAD_REVISION'; @@ -159,10 +151,7 @@ function field_help($path, $arg) { '#theme' => 'item_list', '#items' => $items['items'], ); - $output .= drupal_render($item_list) . '</dd>'; - $output .= '<dt>' . t('Managing field data storage') . '</dt>'; - $output .= '<dd>' . t('Developers of field modules can either use the default <a href="@sql-store">Field SQL Storage module</a> to store data for their fields, or a contributed or custom module developed using the <a href="@storage-api">field storage API</a>.', array('@storage-api' => 'http://api.drupal.org/api/group/field_storage/8', '@sql-store' => url('admin/help/field_sql_storage'))) . '</dd>'; - $output .= '</dl>'; + $output .= drupal_render($item_list); return $output; } } diff --git a/core/modules/field/field.purge.inc b/core/modules/field/field.purge.inc index 7333f978f005c3df853963d4f6d2ff46553b9074..e38f57fc3c6f320277a00ef1d178cdba63b7e961 100644 --- a/core/modules/field/field.purge.inc +++ b/core/modules/field/field.purge.inc @@ -2,7 +2,7 @@ /** * @file - * Field CRUD API, handling field and field instance creation and deletion. + * Provides support for field data purge after mass deletion. */ use Drupal\field\Entity\Field; @@ -17,26 +17,21 @@ * entities as well as deleting entire fields or field instances in a single * operation. * - * Deleting field data items for an entity with field_attach_delete() involves - * three separate operations: - * - Invoking the Field Type API hook_field_delete() for each field on the - * entity. The hook for each field type receives the entity and the specific - * field being deleted. A file field module might use this hook to delete - * uploaded files from the filesystem. - * - Invoking the Field Storage API hook_field_storage_delete() to remove data - * from the primary field storage. The hook implementation receives the entity - * being deleted and deletes data for all of the entity's bundle's fields. - * - Invoking the global Field Attach API hook_field_attach_delete() for all - * modules that implement it. Each hook implementation receives the entity - * being deleted and can operate on whichever subset of the entity's bundle's - * fields it chooses to. + * When a single entity is deleted, the Entity storage controller performs the + * following operations: + * - Invoking the FieldInterface delete() method for each field on the + * entity. A file field type might use this method to delete uploaded files + * from the filesystem. + * - Removing the data from storage. + * - Invoking the global hook_entity_delete() for all modules that implement it. + * Each hook implementation receives the entity being deleted and can operate + * on whichever subset of the entity's bundle's fields it chooses to. * - * These hooks are invoked immediately when field_attach_delete() is called. - * Similar operations are performed for field_attach_delete_revision(). + * Similar operations are performed on deletion of a single entity revision. * * When a field, bundle, or field instance is deleted, it is not practical to - * invoke these hooks immediately on every affected entity in a single page - * request; there could be thousands or millions of them. Instead, the + * perform those operations immediately on every affected entity in a single + * page request; there could be thousands or millions of them. Instead, the * appropriate field data items, instances, and/or fields are marked as deleted * so that subsequent load or query operations will not return them. Later, a * separate process cleans up, or "purges", the marked-as-deleted data by going @@ -44,34 +39,18 @@ * field and instance records. * * Purging field data is made somewhat tricky by the fact that, while - * field_attach_delete() has a complete entity to pass to the various deletion - * hooks, the Field API purge process only has the field data it has previously + * $entity->delete() has a complete entity to pass to the various deletion + * steps, the Field API purge process only has the field data it has previously * stored. It cannot reconstruct complete original entities to pass to the - * deletion hooks. It is even possible that the original entity to which some - * Field API data was attached has been itself deleted before the field purge - * operation takes place. + * deletion operations. It is even possible that the original entity to which + * some Field API data was attached has been itself deleted before the field + * purge operation takes place. * - * Field API resolves this problem by using "pseudo-entities" during purge - * operations. A pseudo-entity contains only the information from the original - * entity that Field API knows about: entity type, ID, revision ID, and bundle. - * It also contains the field data for whichever field instance is currently - * being purged. For example, suppose that the node type 'story' used to contain - * a field called 'subtitle' but the field was deleted. If node 37 was a story - * with a subtitle, the pseudo-entity passed to the purge hooks would look - * something like this: - * - * @code - * $entity = stdClass Object( - * [nid] => 37, - * [vid] => 37, - * [type] => 'story', - * [subtitle] => array( - * [0] => array( - * 'value' => 'subtitle text', - * ), - * ), - * ); - * @endcode + * Field API resolves this problem by using stub entities during purge + * operations, containing only the information from the original entity that + * Field API knows about: entity type, ID, revision ID, and bundle. It also + * contains the field data for whichever field instance is currently being + * purged. * * See @link field Field API @endlink for information about the other parts of * the Field API. diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php index 92c88f2301dec8956d213bd5389a7c87bbd12e69..103521290e3d79af5be42d8c4833060fc5174a6a 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php @@ -7,13 +7,8 @@ namespace Drupal\field\Tests; -use Drupal\Core\Language\Language; - /** - * Unit test class for storage-related field_attach_* functions. - * - * All field_attach_* test work with all field_storage plugins and - * all hook_field_attach_pre_{load,insert,update}() hooks. + * Unit test class for storage-related field behavior. */ class FieldAttachStorageTest extends FieldUnitTestBase { diff --git a/core/modules/field/tests/modules/field_test/field_test.storage.inc b/core/modules/field/tests/modules/field_test/field_test.storage.inc deleted file mode 100644 index 3bbbae2f3e1cdeed6eb4981c1260759fea52d467..0000000000000000000000000000000000000000 --- a/core/modules/field/tests/modules/field_test/field_test.storage.inc +++ /dev/null @@ -1,463 +0,0 @@ -<?php - -/** - * @file - * Defines a field storage backend. - */ - -use Drupal\Core\Entity\EntityInterface; -use Drupal\field\FieldInstanceInterface; - -/** - * Implements hook_field_storage_info(). - */ -function field_test_field_storage_info() { - return array( - 'field_test_storage' => array( - 'label' => t('Test storage'), - 'description' => t('Dummy test storage backend. Stores field values in the variable table.'), - ), - 'field_test_storage_failure' => array( - 'label' => t('Test storage failure'), - 'description' => t('Dummy test storage backend. Always fails to create fields.'), - ), - ); -} - -/** - * Implements hook_field_storage_details(). - */ -function field_test_field_storage_details($field) { - $details = array(); - - // Add field columns. - $columns = array(); - foreach ((array) $field['columns'] as $column_name => $attributes) { - $columns[$column_name] = $column_name; - } - return array( - 'drupal_variables' => array( - 'field_test_storage_data[FIELD_LOAD_CURRENT]' => $columns, - 'field_test_storage_data[FIELD_LOAD_REVISION]' => $columns, - ), - ); -} - -/** - * Implements hook_field_storage_details_alter(). - * - * @see FieldAttachStorageTestCase::testFieldStorageDetailsAlter() - */ -function field_test_field_storage_details_alter(&$details, $field) { - - // For testing, storage details are changed only because of the field name. - if ($field['field_name'] == 'field_test_change_my_details') { - $columns = array(); - foreach ((array) $field['columns'] as $column_name => $attributes) { - $columns[$column_name] = $column_name; - } - $details['drupal_variables'] = array( - FIELD_LOAD_CURRENT => array( - 'moon' => $columns, - ), - FIELD_LOAD_REVISION => array( - 'mars' => $columns, - ), - ); - } -} - -/** - * Helper function: stores or retrieves data from the 'storage backend'. - */ -function _field_test_storage_data($data = NULL) { - if (!isset($data)) { - return Drupal::state()->get('field_test.storage_data'); - } - else { - Drupal::state()->set('field_test.storage_data', $data); - } -} - -/** - * Implements hook_field_storage_load(). - */ -function field_test_field_storage_load($entity_type, $entities, $age, $fields, $options) { - $data = _field_test_storage_data(); - - $load_current = $age == FIELD_LOAD_CURRENT; - - foreach ($fields as $field_id => $ids) { - $field = field_info_field_by_id($field_id); - $field_name = $field['field_name']; - $field_data = $data[$field['uuid']]; - $sub_table = $load_current ? 'current' : 'revisions'; - $delta_count = array(); - foreach ($field_data[$sub_table] as $row) { - if ($row->type == $entity_type && (!$row->deleted || $options['deleted'])) { - if (($load_current && in_array($row->entity_id, $ids)) || (!$load_current && in_array($row->revision_id, $ids))) { - if (in_array($row->langcode, field_available_languages($entity_type, $field))) { - if (!isset($delta_count[$row->entity_id][$row->langcode])) { - $delta_count[$row->entity_id][$row->langcode] = 0; - } - if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->langcode] < $field['cardinality']) { - $item = array(); - foreach ($field['columns'] as $column => $attributes) { - $item[$column] = $row->{$column}; - } - $entities[$row->entity_id]->{$field_name}[$row->langcode][] = $item; - $delta_count[$row->entity_id][$row->langcode]++; - } - } - } - } - } - } -} - -/** - * Implements hook_field_storage_write(). - */ -function field_test_field_storage_write(EntityInterface $entity, $op, $fields) { - $data = _field_test_storage_data(); - - $id = $entity->id(); - $vid = $entity->getRevisionId(); - $bundle = $entity->bundle(); - - foreach ($fields as $field_id) { - $field = field_info_field_by_id($field_id); - $field_name = $field['field_name']; - $field_data = &$data[$field_id]; - - $all_langcodes = field_available_languages($entity->entityType(), $field); - $field_langcodes = array_intersect($all_langcodes, array_keys((array) $entity->$field_name)); - - // Delete and insert, rather than update, in case a value was added. - if ($op == FIELD_STORAGE_UPDATE) { - // Delete languages present in the incoming $entity->$field_name. - // Delete all languages if $entity->$field_name is empty. - $langcodes = !empty($entity->$field_name) ? $field_langcodes : $all_langcodes; - if ($langcodes) { - foreach ($field_data['current'] as $key => $row) { - if ($row->type == $entity->entityType() && $row->entity_id == $id && in_array($row->langcode, $langcodes)) { - unset($field_data['current'][$key]); - } - } - if (isset($vid)) { - foreach ($field_data['revisions'] as $key => $row) { - if ($row->type == $entity->entityType() && $row->revision_id == $vid) { - unset($field_data['revisions'][$key]); - } - } - } - } - } - - foreach ($field_langcodes as $langcode) { - $items = (array) $entity->{$field_name}[$langcode]; - $delta_count = 0; - foreach ($items as $delta => $item) { - $row = (object) array( - 'field_id' => $field_id, - 'type' => $entity->entityType(), - 'entity_id' => $id, - 'revision_id' => $vid, - 'bundle' => $bundle, - 'delta' => $delta, - 'deleted' => FALSE, - 'langcode' => $langcode, - ); - foreach ($field['columns'] as $column => $attributes) { - $row->{$column} = isset($item[$column]) ? $item[$column] : NULL; - } - - $field_data['current'][] = $row; - if (isset($vid)) { - $field_data['revisions'][] = $row; - } - - if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) { - break; - } - } - } - } - - _field_test_storage_data($data); -} - -/** - * Implements hook_field_storage_delete(). - */ -function field_test_field_storage_delete(EntityInterface $entity, $fields) { - // Note: reusing field_test_storage_purge(), like field_sql_storage.module - // does, is highly inefficient in our case... - foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) { - if (isset($fields[$instance['field_id']])) { - $field = $instance->getField(); - field_test_field_storage_purge($entity, $field, $instance); - } - } -} - -/** - * Implements hook_field_storage_purge(). - */ -function field_test_field_storage_purge(EntityInterface $entity, $field, $instance) { - $data = _field_test_storage_data(); - - $field_data = &$data[$field['uuid']]; - foreach (array('current', 'revisions') as $sub_table) { - foreach ($field_data[$sub_table] as $key => $row) { - if ($row->type == $entity->entityType() && $row->entity_id == $entity->id()) { - unset($field_data[$sub_table][$key]); - } - } - } - - _field_test_storage_data($data); -} - -/** - * Implements hook_field_storage_delete_revision(). - */ -function field_test_field_storage_delete_revision(EntityInterface $entity, $fields) { - $data = _field_test_storage_data(); - - foreach ($fields as $field_id) { - $field_data = &$data[$field_id]; - foreach (array('current', 'revisions') as $sub_table) { - foreach ($field_data[$sub_table] as $key => $row) { - if ($row->type == $entity->entityType() && $row->entity_id == $entity->id() && $row->revision_id == $entity->getRevisionId()) { - unset($field_data[$sub_table][$key]); - } - } - } - } - - _field_test_storage_data($data); -} - -/** - * Implements hook_field_storage_query(). - */ -function field_test_field_storage_query($field_id, $conditions, $count, &$cursor = NULL, $age) { - $data = _field_test_storage_data(); - - $load_current = $age == FIELD_LOAD_CURRENT; - - $field = field_info_field_by_id($field_id); - $field_columns = array_keys($field['columns']); - - $field_data = $data[$field['uuid']]; - $sub_table = $load_current ? 'current' : 'revisions'; - // We need to sort records by entity type and entity id. - usort($field_data[$sub_table], '_field_test_field_storage_query_sort_helper'); - - // Initialize results array. - $return = array(); - $entity_count = 0; - $rows_count = 0; - $rows_total = count($field_data[$sub_table]); - $skip = $cursor; - $skipped = 0; - - foreach ($field_data[$sub_table] as $row) { - if ($count != FIELD_QUERY_NO_LIMIT && $entity_count >= $count) { - break; - } - - if ($row->field_id == $field['uuid']) { - $match = TRUE; - $condition_deleted = FALSE; - // Add conditions. - foreach ($conditions as $condition) { - @list($column, $value, $operator) = $condition; - if (empty($operator)) { - $operator = is_array($value) ? 'IN' : '='; - } - switch ($operator) { - case '=': - $match = $match && $row->{$column} == $value; - break; - case '<>': - case '<': - case '<=': - case '>': - case '>=': - eval('$match = $match && ' . $row->{$column} . ' ' . $operator . ' '. $value); - break; - case 'IN': - $match = $match && in_array($row->{$column}, $value); - break; - case 'NOT IN': - $match = $match && !in_array($row->{$column}, $value); - break; - case 'BETWEEN': - $match = $match && $row->{$column} >= $value[0] && $row->{$column} <= $value[1]; - break; - case 'STARTS_WITH': - case 'ENDS_WITH': - case 'CONTAINS': - // Not supported. - $match = FALSE; - break; - } - // Track condition on 'deleted'. - if ($column == 'deleted') { - $condition_deleted = TRUE; - } - } - - // Exclude deleted data unless we have a condition on it. - if (!$condition_deleted && $row->deleted) { - $match = FALSE; - } - - if ($match) { - if (!isset($skip) || $skipped >= $skip) { - $cursor++; - // If querying all revisions and the entity type has revisions, we need - // to key the results by revision_ids. - $entity_type = entity_get_info($row->type); - $id = ($load_current || empty($entity_type['entity_keys']['revision'])) ? $row->entity_id : $row->revision_id; - - if (!isset($return[$row->type][$id])) { - $return[$row->type][$id] = (object) array('entity_id' => $row->entity_id, 'revision_id' => $row->revision_id, 'bundle' => $row->bundle); - $entity_count++; - } - } - else { - $skipped++; - } - } - } - $rows_count++; - - // The query is complete if we walked the whole array. - if ($count != FIELD_QUERY_NO_LIMIT && $rows_count >= $rows_total) { - $cursor = FIELD_QUERY_COMPLETE; - } - } - - return $return; -} - -/** - * Sort helper for field_test_field_storage_query(). - * - * Sorts by entity type and entity id. - */ -function _field_test_field_storage_query_sort_helper($a, $b) { - if ($a->type == $b->type) { - if ($a->entity_id == $b->entity_id) { - return 0; - } - else { - return $a->entity_id < $b->entity_id ? -1 : 1; - } - } - else { - return $a->type < $b->type ? -1 : 1; - } -} - -/** - * Implements hook_field_storage_create_field(). - */ -function field_test_field_storage_create_field($field) { - if ($field['storage']['type'] == 'field_test_storage_failure') { - throw new Exception('field_test_storage_failure engine always fails to create fields'); - } - - $data = _field_test_storage_data(); - - $data[$field['uuid']] = array( - 'current' => array(), - 'revisions' => array(), - ); - - _field_test_storage_data($data); -} - -/** - * Implements hook_field_storage_delete_field(). - */ -function field_test_field_storage_delete_field($field) { - $data = _field_test_storage_data(); - - $field_data = &$data[$field['uuid']]; - foreach (array('current', 'revisions') as $sub_table) { - foreach ($field_data[$sub_table] as &$row) { - $row->deleted = TRUE; - } - } - - _field_test_storage_data($data); -} - -/** - * Implements hook_field_storage_delete_instance(). - */ -function field_test_field_storage_delete_instance($instance) { - $data = _field_test_storage_data(); - - $field = $instance->getField(); - $field_data = &$data[$field['uuid']]; - foreach (array('current', 'revisions') as $sub_table) { - foreach ($field_data[$sub_table] as &$row) { - if ($row->bundle == $instance['bundle']) { - $row->deleted = TRUE; - } - } - } - - _field_test_storage_data($data); -} - -/** - * Implements hook_entity_bundle_rename(). - */ -function field_test_entity_bundle_rename($entity_type, $bundle_old, $bundle_new) { - $data = _field_test_storage_data(); - - // We need to account for deleted or inactive fields and instances. - $instances = field_read_instances(array('bundle' => $bundle_new), array('include_deleted' => TRUE, 'include_inactive' => TRUE)); - foreach ($instances as $instance) { - $field = $instance->getField(); - if ($field && $field['storage']['type'] == 'field_test_storage') { - $field_data = &$data[$field['uuid']]; - foreach (array('current', 'revisions') as $sub_table) { - foreach ($field_data[$sub_table] as &$row) { - if ($row->bundle == $bundle_old) { - $row->bundle = $bundle_new; - } - } - } - } - } - - _field_test_storage_data($data); -} - -/** - * Implements hook_ENTITY_TYPE_delete() for 'field_instance'. - */ -function field_test_field_instance_delete(FieldInstanceInterface $field_instance) { - $data = _field_test_storage_data(); - - $field = $field_instance->getField(); - if ($field->storage['type'] == 'field_test_storage') { - $field_data = &$data[$field->uuid]; - foreach (array('current', 'revisions') as $sub_table) { - foreach ($field_data[$sub_table] as &$row) { - if ($row->bundle == $field_instance->bundle) { - $row->deleted = TRUE; - } - } - } - } - - _field_test_storage_data($data); -} diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php index cc7205fae4cd85690aaec09797903c3de79c8e39..275e232e55ecc670aabf814e40f7935575cbfdf3 100644 --- a/core/modules/node/node.api.php +++ b/core/modules/node/node.api.php @@ -492,11 +492,10 @@ function hook_node_create(\Drupal\Core\Entity\EntityInterface $node) { * * This hook is invoked during node loading, which is handled by entity_load(), * via classes Drupal\node\NodeStorageController and - * Drupal\Core\Entity\DatabaseStorageController. After the node information is - * read from the database or the entity cache, then field_attach_load_revision() - * or field_attach_load() is called, then hook_entity_load() is invoked on all - * implementing modules, and finally hook_node_load() is invoked on all - * implementing modules. + * Drupal\Core\Entity\DatabaseStorageController. After the node information and + * field values are read from the database or the entity cache, + * hook_entity_load() is invoked on all implementing modules, and finally + * hook_node_load() is invoked on all implementing modules. * * @param $nodes * An array of the nodes being loaded, keyed by nid.