field.purge.inc 9.69 KB
Newer Older
1
2
<?php

3
4
/**
 * @file
5
 * Field CRUD API, handling field and field instance creation and deletion.
6
7
 */

8
use Drupal\Core\Entity\EntityInterface;
9
use Drupal\field\Entity\Field;
10
11
use Drupal\field\FieldException;

12
/**
13
14
 * @defgroup field_purge Field API bulk data deletion
 * @{
15
 * Cleans up after Field API bulk deletion operations.
16
17
 *
 * Field API provides functions for deleting data attached to individual
18
 * entities as well as deleting entire fields or field instances in a single
19
20
 * operation.
 *
21
 * Deleting field data items for an entity with field_attach_delete() involves
22
23
 * three separate operations:
 * - Invoking the Field Type API hook_field_delete() for each field on the
24
25
26
27
28
29
 *   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.
30
 * - Invoking the global Field Attach API hook_field_attach_delete() for all
31
32
33
 *   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.
34
 *
35
36
 * These hooks are invoked immediately when field_attach_delete() is called.
 * Similar operations are performed for field_attach_delete_revision().
37
38
 *
 * When a field, bundle, or field instance is deleted, it is not practical to
39
 * invoke these hooks immediately on every affected entity in a single page
40
41
42
43
 * 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
45
 * through the three-step process described above and, finally, removing deleted
 * field and instance records.
46
47
 *
 * Purging field data is made somewhat tricky by the fact that, while
48
 * field_attach_delete() has a complete entity to pass to the various deletion
49
 * hooks, the Field API purge process only has the field data it has previously
50
51
 * stored. It cannot reconstruct complete original entities to pass to the
 * deletion hooks. It is even possible that the original entity to which some
52
53
54
 * Field API data was attached has been itself deleted before the field purge
 * operation takes place.
 *
55
56
 * Field API resolves this problem by using "pseudo-entities" during purge
 * operations. A pseudo-entity contains only the information from the original
57
58
59
60
61
62
 * 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:
63
64
 *
 * @code
65
 *   $entity = stdClass Object(
66
67
68
69
70
71
72
73
74
75
 *     [nid] => 37,
 *     [vid] => 37,
 *     [type] => 'story',
 *     [subtitle] => array(
 *       [0] => array(
 *         'value' => 'subtitle text',
 *       ),
 *     ),
 *   );
 * @endcode
76
 *
77
78
 * See @link field Field API @endlink for information about the other parts of
 * the Field API.
79
80
81
 */

/**
82
 * Purges a batch of deleted Field API data, instances, or fields.
83
 *
84
85
86
87
88
89
 * This function will purge deleted field data in batches. The batch size
 * is defined as an argument to the function, and once each batch is finished,
 * it continues with the next batch until all have completed. If a deleted field
 * instance with no remaining data records is found, the instance itself will
 * be purged. If a deleted field with no remaining field instances is found, the
 * field itself will be purged.
90
91
92
93
94
95
96
 *
 * @param $batch_size
 *   The maximum number of field data records to purge before returning.
 */
function field_purge_batch($batch_size) {
  // Retrieve all deleted field instances. We cannot use field_info_instances()
  // because that function does not return deleted instances.
97
  $instances = field_read_instances(array('deleted' => TRUE), array('include_deleted' => TRUE));
98
  $factory = Drupal::service('entity.query');
99
  $info = entity_get_info();
100
  foreach ($instances as $instance) {
101
    $entity_type = $instance['entity_type'];
102
103
104
105
106
107
108

    // EntityFieldQuery currently fails on conditions on comment bundle.
    // Remove when http://drupal.org/node/731724 is fixed.
    if ($entity_type == 'comment') {
      continue;
    }

109
110
111
112
    $ids = (object) array(
      'entity_type' => $entity_type,
      'bundle' => $instance['bundle'],
    );
113
    // field_purge_data() will need the field array.
114
    $field = field_info_field_by_id($instance['field_id']);
115
    // Retrieve some entities.
116
    $query = $factory->get($entity_type)
117
      ->condition('id:' . $field['uuid'] . '.deleted', 1)
118
119
120
121
122
123
      ->range(0, $batch_size);
    // If there's no bundle key, all results will have the same bundle.
    if (!empty($info[$entity_type]['entity_keys']['bundle'])) {
      $query->condition($info[$entity_type]['entity_keys']['bundle'], $ids->bundle);
    }
    $results = $query->execute();
124

125
    if ($results) {
126
127
128
129
130
131
132
133
134
      $entities = array();
      foreach ($results as $revision_id => $entity_id) {
        // This might not be the revision id if the entity type does not support
        // revisions but _field_create_entity_from_ids() checks that and
        // disregards this value so there is no harm setting it.
        $ids->revision_id = $revision_id;
        $ids->entity_id = $entity_id;
        $entities[$entity_id] = _field_create_entity_from_ids($ids);
      }
135
      field_attach_load($entity_type, $entities, FIELD_LOAD_CURRENT, array('instance' => $instance));
136
137
      foreach ($entities as $entity) {
        // Purge the data for the entity.
138
        field_purge_data($entity, $field, $instance);
139
140
141
142
143
144
145
146
      }
    }
    else {
      // No field data remains for the instance, so we can remove it.
      field_purge_instance($instance);
    }
  }

147
  // Retrieve all deleted fields. Any that have no instances can be purged.
148
149
150
151
  $deleted_fields = Drupal::state()->get('field.field.deleted') ?: array();
  foreach ($deleted_fields as $field) {
    $field = new Field($field);
    $instances = field_read_instances(array('field_id' => $field['uuid']), array('include_deleted' => 1));
152
    if (empty($instances)) {
153
154
155
156
157
158
      field_purge_field($field);
    }
  }
}

/**
159
 * Purges the field data for a single field on a single pseudo-entity.
160
 *
161
162
 * This is basically the same as field_attach_delete() except it only applies to
 * a single field. The entity itself is not being deleted, and it is quite
163
164
 * possible that other field data will remain attached to it.
 *
165
 * @param \Drupal\Core\Entity\EntityInterface $entity
166
 *   The pseudo-entity whose field data is being purged.
167
168
169
170
171
 * @param $field
 *   The (possibly deleted) field whose data is being purged.
 * @param $instance
 *   The deleted field instance whose data is being purged.
 */
172
function field_purge_data(EntityInterface $entity, $field, $instance) {
173
174
175
176
177
  foreach ($entity->{$field->id()} as $value) {
    $definition = _field_generate_entity_field_definition($field, $instance);
    $items = \Drupal::typedData()->create($definition, $value, $field->id(), $entity);
    $items->delete();
  }
178
179

  // Tell the field storage system to purge the data.
180
  module_invoke($field['storage']['module'], 'field_storage_purge', $entity, $field, $instance);
181
182

  // Let other modules act on purging the data.
183
  foreach (Drupal::moduleHandler()->getImplementations('field_attach_purge') as $module) {
184
    $function = $module . '_field_attach_purge';
185
    $function($entity, $field, $instance);
186
187
188
189
  }
}

/**
190
 * Purges a field instance record from the database.
191
 *
192
 * This function assumes all data for the instance has already been purged and
193
194
195
196
197
198
199
 * should only be called by field_purge_batch().
 *
 * @param $instance
 *   The instance record to purge.
 */
function field_purge_instance($instance) {
  // Notify the storage engine.
200
201
  $field = field_info_field_by_id($instance['field_id']);
  module_invoke($field['storage']['module'], 'field_storage_purge_instance', $instance);
202

203
204
205
206
207
  $state = Drupal::state();
  $deleted_instances = $state->get('field.instance.deleted');
  unset($deleted_instances[$instance['uuid']]);
  $state->set('field.instance.deleted', $deleted_instances);

208
  // Clear the cache.
209
  field_info_cache_clear();
210
211

  // Invoke external hooks after the cache is cleared for API consistency.
212
  Drupal::moduleHandler()->invokeAll('field_purge_instance', array($instance));
213
214
215
}

/**
216
 * Purges a field record from the database.
217
218
219
220
221
222
223
224
 *
 * This function assumes all instances for the field has already been purged,
 * and should only be called by field_purge_batch().
 *
 * @param $field
 *   The field record to purge.
 */
function field_purge_field($field) {
225
  $instances = field_read_instances(array('field_id' => $field['uuid']), array('include_deleted' => 1));
226
  if (count($instances) > 0) {
227
    throw new FieldException(t('Attempt to purge a field @field_name that still has instances.', array('@field_name' => $field['field_name'])));
228
229
  }

230
231
232
233
  $state = Drupal::state();
  $deleted_fields = $state->get('field.field.deleted');
  unset($deleted_fields[$field['uuid']]);
  $state->set('field.field.deleted', $deleted_fields);
234
235

  // Notify the storage engine.
236
  module_invoke($field['storage']['module'], 'field_storage_purge_field', $field);
237
238

  // Clear the cache.
239
  field_info_cache_clear();
240
241

  // Invoke external hooks after the cache is cleared for API consistency.
242
  Drupal::moduleHandler()->invokeAll('field_purge_field', array($field));
243
244
245
246
247
}

/**
 * @} End of "defgroup field_purge".
 */