field.crud.inc

Field CRUD API, handling field and field instance creation and deletion.

File

drupal/core/modules/field/field.crud.inc
View source
<?php

/**
 * @file
 * Field CRUD API, handling field and field instance creation and deletion.
 */
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Plugin\Core\Entity\Field;
use Drupal\field\Plugin\Core\Entity\FieldInstance;
use Drupal\field\FieldException;

/**
 * @defgroup field_crud Field CRUD API
 * @{
 * Creates, updates, and deletes Field API fields, bundles, and instances.
 *
 * Modules use this API, often in hook_install(), to create custom data
 * structures. UI modules will use it to create a user interface.
 *
 * The Field CRUD API uses @link field Field API data structures @endlink.
 *
 * See @link field Field API @endlink for information about the other parts of
 * the Field API.
 */

/**
 * Creates a field.
 *
 * This function does not bind the field to any bundle; use
 * field_create_instance() for that.
 *
 * @param array $field
 *   A field definition. The field_name and type properties are required.
 *   Other properties, if omitted, will be given the following default values:
 *   - cardinality: 1
 *   - locked: FALSE
 *   - indexes: the field-type indexes, specified by the field type's
 *     hook_field_schema(). The indexes specified in $field are added
 *     to those default indexes. It is possible to override the
 *     definition of a field-type index by providing an index with the
 *     same name, or to remove it by redefining it as an empty array
 *     of columns. Overriding field-type indexes should be done
 *     carefully, for it might seriously affect the site's performance.
 *   - settings: each omitted setting is given the default value defined in
 *     hook_field_info().
 *   - storage:
 *     - type: the storage backend specified in the
 *       'field.settings.default_storage' configuration.
 *     - settings: each omitted setting is given the default value specified in
 *       hook_field_storage_info().
 *
 * @return \Drupal\field\Plugin\Core\Entity\Field
 *   The field entity.
 *
 * @throws Drupal\field\FieldException
 *
 * @deprecated as of Drupal 8.0. Use
 *   entity_create('field_entity', $definition)->save().
 *
 * See: @link field Field API data structures @endlink.
 */
function field_create_field(array $field) {
  $field = entity_create('field_entity', $field);
  $field
    ->save();
  return $field;
}

/**
 * Updates a field.
 *
 * Any module may forbid any update for any reason. For example, the
 * field's storage module might forbid an update if it would change
 * the storage schema while data for the field exists. A field type
 * module might forbid an update if it would change existing data's
 * semantics, or if there are external dependencies on field settings
 * that cannot be updated.
 *
 * @param mixed $field
 *   Either the \Drupal\field\Plugin\Core\Entity\Field object to update, or a
 *   field array structure. If the latter, $field['field_name'] must provided;
 *   it identifies the field that will be updated to match this structure. Any
 *   other properties of the field that are not specified in $field will be left
 *   unchanged, so it is not necessary to pass in a fully populated $field
 *   structure.
 *
 * @throws Drupal\field\FieldException
 *
 * @deprecated as of Drupal 8.0. Use $field->save().
 *
 * @see field_create_field()
 */
function field_update_field($field) {

  // Module developers can still pass in an array of properties.
  if (is_array($field)) {
    $field_loaded = entity_load('field_entity', $field['field_name']);
    if (empty($field_loaded)) {
      throw new FieldException('Attempt to update a non-existent field.');
    }

    // Merge incoming values.
    foreach ($field as $key => $value) {
      $field_loaded[$key] = $value;
    }
    $field = $field_loaded;
  }
  $field
    ->save();
}

/**
 * Reads a single field record directly from the database.
 *
 * Generally, you should use the field_info_field() instead.
 *
 * This function will not return deleted fields. Use field_read_fields() instead
 * for this purpose.
 *
 * @param $field_name
 *   The field name to read.
 * @param array $include_additional
 *   The default behavior of this function is to not return a field that is
 *   inactive. Setting $include_additional['include_inactive'] to TRUE will
 *   override this behavior.
 *
 * @return
 *   A field definition array, or FALSE.
 */
function field_read_field($field_name, $include_additional = array()) {
  $fields = field_read_fields(array(
    'field_name' => $field_name,
  ), $include_additional);
  return $fields ? current($fields) : FALSE;
}

/**
 * Reads in fields that match an array of conditions.
 *
 * @param array $conditions
 *   An array of conditions to match against. Keys are names of properties found
 *   in field configuration files, and values are conditions to match.
 * @param array $include_additional
 *   The default behavior of this function is to not return fields that are
 *   inactive or have been deleted. Setting
 *   $include_additional['include_inactive'] or
 *   $include_additional['include_deleted'] to TRUE will override this behavior.
 *
 * @return
 *   An array of fields matching $params. If
 *   $include_additional['include_deleted'] is TRUE, the array is keyed by
 *   field ID, otherwise it is keyed by field name.
 */
function field_read_fields($conditions = array(), $include_additional = array()) {

  // Include inactive fields if specified in the $include_additional parameter.
  $include_inactive = isset($include_additional['include_inactive']) && $include_additional['include_inactive'];

  // Include deleted fields if specified either in the $include_additional or
  // the $conditions parameters.
  $include_deleted = isset($include_additional['include_deleted']) && $include_additional['include_deleted'] || isset($conditions['deleted']) && $conditions['deleted'];

  // Pass include_inactive and include_deleted to the $conditions array.
  $conditions['include_inactive'] = $include_inactive;
  $conditions['include_deleted'] = $include_deleted;
  return entity_load_multiple_by_properties('field_entity', $conditions);
}

/**
 * Marks a field and its instances and data for deletion.
 *
 * @param $field_name
 *   The field name to delete.
 *
 * @deprecated as of Drupal 8.0. Use $field->delete().
 */
function field_delete_field($field_name) {
  if ($field = field_info_field($field_name)) {
    $field
      ->delete();
  }
}

/**
 * Creates an instance of a field, binding it to a bundle.
 *
 * @param array $instance
 *   A field instance definition array. The field_name, entity_type and
 *   bundle properties are required. Other properties, if omitted,
 *   will be given the following default values:
 *   - label: the field name
 *   - description: empty string
 *   - required: FALSE
 *   - default_value_function: empty string
 *   - settings: each omitted setting is given the default value specified in
 *     hook_field_info().
 *   - widget:
 *     - type: the default widget specified in hook_field_info().
 *     - settings: each omitted setting is given the default value specified in
 *       hook_field_widget_info().
 *
 * @return \Drupal\field\Plugin\Core\Entity\FieldInstance
 *   The field instance entity.
 *
 * @throws Drupal\field\FieldException
 *
 * @deprecated as of Drupal 8.0. Use
 *   entity_create('field_instance', $definition)->save().
 *
 * See: @link field Field API data structures @endlink.
 */
function field_create_instance(array $instance) {
  $instance = entity_create('field_instance', $instance);
  $instance
    ->save();
  return $instance;
}

/**
 * Updates an instance of a field.
 *
 * @param mixed $instance
 *   Either the \Drupal\field\Plugin\Core\Entity\FieldInstance to update, or an
 *   associative array representing an instance structure. If the latter, the
 *   required keys and values are:
 *   - entity_type: The type of the entity the field is attached to.
 *   - bundle: The bundle this field belongs to.
 *   - field_name: The name of an existing field.
 *   The other array elements represent properties of the instance, and all
 *   properties must be specified or their default values will be used (except
 *   internal-use properties, which are assigned automatically). To avoid losing
 *   the previously stored properties of the instance when making a change,
 *   first load the instance with field_info_instance(), then override the
 *   values you want to override, and finally save using this function. Example:
 *   @code
 *   // Fetch an instance info array.
 *   $instance_info = field_info_instance($entity_type, $field_name, $bundle_name);
 *   // Change a single property in the instance definition.
 *   $instance_info['definition']['required'] = TRUE;
 *   // Write the changed definition back.
 *   field_update_instance($instance_info['definition']);
 *   @endcode
 *
 * @throws Drupal\field\FieldException
 *
 * @deprecated as of Drupal 8.0. Use $instance->save().
 *
 * @see field_info_instance()
 * @see field_create_instance()
 */
function field_update_instance($instance) {

  // Module developers can still pass in an array of properties.
  if (is_array($instance)) {
    $instance_loaded = entity_load('field_instance', $instance['entity_type'] . '.' . $instance['bundle'] . '.' . $instance['field_name']);
    if (empty($instance_loaded)) {
      throw new FieldException('Attempt to update a non-existent instance.');
    }

    // Merge incoming values.
    foreach ($instance as $key => $value) {
      $instance_loaded[$key] = $value;
    }
    $instance = $instance_loaded;
  }
  $instance
    ->save();
}

/**
 * Reads a single instance record from the database.
 *
 * Generally, you should use field_info_instance() instead, as it provides
 * caching and allows other modules the opportunity to append additional
 * formatters, widgets, and other information.
 *
 * @param $entity_type
 *   The type of entity to which the field is bound.
 * @param $field_name
 *   The field name to read.
 * @param $bundle
 *   The bundle to which the field is bound.
 * @param array $include_additional
 *   The default behavior of this function is to not return an instance that has
 *   been deleted, or whose field is inactive. Setting
 *   $include_additional['include_inactive'] or
 *   $include_additional['include_deleted'] to TRUE will override this behavior.
 *
 * @return
 *   An instance structure, or FALSE.
 */
function field_read_instance($entity_type, $field_name, $bundle, $include_additional = array()) {
  $instances = field_read_instances(array(
    'entity_type' => $entity_type,
    'field_name' => $field_name,
    'bundle' => $bundle,
  ), $include_additional);
  return $instances ? current($instances) : FALSE;
}

/**
 * Reads in field instances that match an array of conditions.
 *
 * @param $param
 *   An array of properties to use in selecting a field instance. Keys are names
 *   of properties found in field instance configuration files, and values are
 *   conditions to match.
 * @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
 *   $include_additional['include_inactive'] or
 *   $include_additional['include_deleted'] to TRUE will override this behavior.
 *
 * @return
 *   An array of instances matching the arguments.
 */
function field_read_instances($conditions = array(), $include_additional = array()) {

  // Include instances of inactive fields if specified in the
  // $include_additional parameter.
  $include_inactive = isset($include_additional['include_inactive']) && $include_additional['include_inactive'];

  // Include deleted instances if specified either in the $include_additional
  // or the $conditions parameters.
  $include_deleted = isset($include_additional['include_deleted']) && $include_additional['include_deleted'] || isset($conditions['deleted']) && $conditions['deleted'];

  // Pass include_inactive and include_deleted to the $conditions array.
  $conditions['include_inactive'] = $include_inactive;
  $conditions['include_deleted'] = $include_deleted;
  return entity_load_multiple_by_properties('field_instance', $conditions);
}

/**
 * Marks a field instance and its data for deletion.
 *
 * @param \Drupal\field\Plugin\Core\Entity\FieldInstance $instance
 *   The field instance.
 * @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.
 *
 * @deprecated as of Drupal 8.0. Use $instance->delete().
 */
function field_delete_instance(FieldInstance $instance, $field_cleanup = TRUE) {
  $instance
    ->delete($field_cleanup);
}

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

/**
 * @defgroup field_purge Field API bulk data deletion
 * @{
 * Cleans up after Field API bulk deletion operations.
 *
 * Field API provides functions for deleting data attached to individual
 * 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.
 *
 * These hooks are invoked immediately when field_attach_delete() is called.
 * Similar operations are performed for field_attach_delete_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
 * 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
 * through the three-step process described above and, finally, removing deleted
 * 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
 * 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.
 *
 * 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
 *
 * See @link field Field API @endlink for information about the other parts of
 * the Field API.
 */

/**
 * Purges a batch of deleted Field API data, instances, or fields.
 *
 * 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.
 *
 * @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.
  $instances = field_read_instances(array(
    'deleted' => TRUE,
  ), array(
    'include_deleted' => TRUE,
  ));
  $factory = Drupal::service('entity.query');
  $info = entity_get_info();
  foreach ($instances as $instance) {
    $entity_type = $instance['entity_type'];

    // EntityFieldQuery currently fails on conditions on comment bundle.
    // Remove when http://drupal.org/node/731724 is fixed.
    if ($entity_type == 'comment') {
      continue;
    }
    $ids = (object) array(
      'entity_type' => $entity_type,
      'bundle' => $instance['bundle'],
    );

    // field_purge_data() will need the field array.
    $field = field_info_field_by_id($instance['field_id']);

    // Retrieve some entities.
    $query = $factory
      ->get($entity_type)
      ->condition('id:' . $field['uuid'] . '.deleted', 1)
      ->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();
    if ($results) {
      $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);
      }
      field_attach_load($entity_type, $entities, FIELD_LOAD_CURRENT, array(
        'field_id' => $field['uuid'],
        'deleted' => 1,
      ));
      foreach ($entities as $entity) {

        // Purge the data for the entity.
        field_purge_data($entity, $field, $instance);
      }
    }
    else {

      // No field data remains for the instance, so we can remove it.
      field_purge_instance($instance);
    }
  }

  // Retrieve all deleted fields. Any that have no instances can be purged.
  $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,
    ));
    if (empty($instances)) {
      field_purge_field($field);
    }
  }
}

/**
 * Purges the field data for a single field on a single pseudo-entity.
 *
 * 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
 * possible that other field data will remain attached to it.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The pseudo-entity whose field data is being purged.
 * @param $field
 *   The (possibly deleted) field whose data is being purged.
 * @param $instance
 *   The deleted field instance whose data is being purged.
 */
function field_purge_data(EntityInterface $entity, $field, $instance) {

  // Each field type's hook_field_delete() only expects to operate on a single
  // field at a time, so we can use it as-is for purging.
  $options = array(
    'field_id' => $instance['field_id'],
    'deleted' => TRUE,
  );
  _field_invoke('delete', $entity, $dummy, $dummy, $options);

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

  // Let other modules act on purging the data.
  foreach (module_implements('field_attach_purge') as $module) {
    $function = $module . '_field_attach_purge';
    $function($entity, $field, $instance);
  }
}

/**
 * Purges a field instance record from the database.
 *
 * This function assumes all data for the instance has already been purged and
 * should only be called by field_purge_batch().
 *
 * @param $instance
 *   The instance record to purge.
 */
function field_purge_instance($instance) {

  // Notify the storage engine.
  $field = field_info_field_by_id($instance['field_id']);
  module_invoke($field['storage']['module'], 'field_storage_purge_instance', $instance);
  $state = Drupal::state();
  $deleted_instances = $state
    ->get('field.instance.deleted');
  unset($deleted_instances[$instance['uuid']]);
  $state
    ->set('field.instance.deleted', $deleted_instances);

  // Clear the cache.
  field_info_cache_clear();

  // Invoke external hooks after the cache is cleared for API consistency.
  module_invoke_all('field_purge_instance', $instance);
}

/**
 * Purges a field record from the database.
 *
 * 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) {
  $instances = field_read_instances(array(
    'field_id' => $field['uuid'],
  ), array(
    'include_deleted' => 1,
  ));
  if (count($instances) > 0) {
    throw new FieldException(t('Attempt to purge a field @field_name that still has instances.', array(
      '@field_name' => $field['field_name'],
    )));
  }
  $state = Drupal::state();
  $deleted_fields = $state
    ->get('field.field.deleted');
  unset($deleted_fields[$field['uuid']]);
  $state
    ->set('field.field.deleted', $deleted_fields);

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

  // Clear the cache.
  field_info_cache_clear();

  // Invoke external hooks after the cache is cleared for API consistency.
  module_invoke_all('field_purge_field', $field);
}

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

Functions

Namesort descending Description
field_create_field Deprecated Creates a field.
field_create_instance Deprecated Creates an instance of a field, binding it to a bundle.
field_delete_field Deprecated Marks a field and its instances and data for deletion.
field_delete_instance Deprecated Marks a field instance and its data for deletion.
field_purge_batch Purges a batch of deleted Field API data, instances, or fields.
field_purge_data Purges the field data for a single field on a single pseudo-entity.
field_purge_field Purges a field record from the database.
field_purge_instance Purges a field instance record from the database.
field_read_field Reads a single field record directly from the database.
field_read_fields Reads in fields that match an array of conditions.
field_read_instance Reads a single instance record from the database.
field_read_instances Reads in field instances that match an array of conditions.
field_update_field Deprecated Updates a field.
field_update_instance Deprecated Updates an instance of a field.