Contains \Drupal\field\Plugin\Core\Entity\Field.
<?php
/**
* @file
* Contains \Drupal\field\Plugin\Core\Entity\Field.
*/
namespace Drupal\field\Plugin\Core\Entity;
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\field\FieldException;
use Drupal\field\FieldInterface;
/**
* Defines the Field entity.
*
* @todo use 'field' as the id once hook_field_load() and friends
* are removed.
*
* @EntityType(
* id = "field_entity",
* label = @Translation("Field"),
* module = "field",
* controllers = {
* "storage" = "Drupal\field\FieldStorageController"
* },
* config_prefix = "field.field",
* entity_keys = {
* "id" = "id",
* "label" = "id",
* "uuid" = "uuid"
* }
* )
*/
class Field extends ConfigEntityBase implements FieldInterface {
/**
* The maximum length of the field ID (machine name), in characters.
*
* For fields created through Field UI, this includes the 'field_' prefix.
*/
const ID_MAX_LENGTH = 32;
/**
* The field ID (machine name).
*
* This is the name of the property under which the field values are placed in
* an entity : $entity-{>$field_id}. The maximum length is
* Field:ID_MAX_LENGTH.
*
* Example: body, field_main_image.
*
* @var string
*/
public $id;
/**
* The field UUID.
*
* This is assigned automatically when the field is created.
*
* @var string
*/
public $uuid;
/**
* The field type.
*
* Field types are defined by modules that implement hook_field_info().
*
* Example: text, number_integer.
*
* @var string
*/
public $type;
/**
* The name of the module that provides the field type.
*
* @var string
*/
public $module;
/**
* Flag indicating whether the field type module is enabled.
*
* @var bool
*/
public $active;
/**
* Field-type specific settings.
*
* An array of key/value pairs, The keys and default values are defined by the
* field type in the 'settings' entry of hook_field_info().
*
* @var array
*/
public $settings = array();
/**
* The field cardinality.
*
* The maximum number of values the field can hold. Possible values are
* positive integers or FIELD_CARDINALITY_UNLIMITED. Defaults to 1.
*
* @var integer
*/
public $cardinality = 1;
/**
* Flag indicating whether the field is translatable.
*
* Defaults to FALSE.
*
* @var bool
*/
public $translatable = FALSE;
/**
* The entity types on which the field is allowed to have instances.
*
* If empty or not specified, the field is allowed to have instances in any
* entity type.
*
* @var array
*/
public $entity_types = array();
/**
* Flag indicating whether the field is available for editing.
*
* If TRUE, some actions not available though the UI (but are still possible
* through direct API manipulation):
* - field settings cannot be changed,
* - new instances cannot be created
* - existing instances cannot be deleted.
* Defaults to FALSE.
*
* @var bool
*/
public $locked = FALSE;
/**
* The field storage definition.
*
* An array of key/value pairs identifying the storage backend to use for the
* field:
* - type: (string) The storage backend used by the field. Storage backends
* are defined by modules that implement hook_field_storage_info().
* - settings: (array) A sub-array of key/value pairs of settings. The keys
* and default values are defined by the storage backend in the 'settings'
* entry of hook_field_storage_info().
* - module: (string, read-only) The name of the module that implements the
* storage backend.
* - active: (integer, read-only) TRUE if the module that implements the
* storage backend is currently enabled, FALSE otherwise.
*
* @var array
*/
public $storage = array();
/**
* The custom storage indexes for the field data storage.
*
* This set of indexes is merged with the "default" indexes specified by the
* field type in hook_field_schema() to determine the actual set of indexes
* that get created.
*
* The indexes are defined using the same definition format as Schema API
* index specifications. Only columns that are part of the field schema, as
* defined by the field type in hook_field_schema(), are allowed.
*
* Some storage backends might not support indexes, and discard that
* information.
*
* @var array
*/
public $indexes = array();
/**
* Flag indicating whether the field is deleted.
*
* The delete() method marks the field as "deleted" and removes the
* corresponding entry from the config storage, but keeps its definition in
* the state storage while field data is purged by a separate
* garbage-collection process.
*
* Deleted fields stay out of the regular entity lifecycle (notably, their
* values are not populated in loaded entities, and are not saved back).
*
* @var bool
*/
public $deleted = FALSE;
/**
* The field schema.
*
* @var array
*/
protected $schema;
/**
* The storage information for the field.
*
* @var array
*/
protected $storageDetails;
/**
* Constructs a Field object.
*
* @param array $values
* An array of field properties, keyed by property name. Most array
* elements will be used to set the corresponding properties on the class;
* see the class property documentation for details. Some array elements
* have special meanings and a few are required. Special elements are:
* - id: required. As a temporary Backwards Compatibility layer right now,
* a 'field_name' property can be accepted in place of 'id'.
* - type: required.
*
* In most cases, Field entities are created via
* entity_create('field_entity', $values)), where $values is the same
* parameter as in this constructor.
*
* @see entity_create()
*
* @ingroup field_crud
*/
public function __construct(array $values, $entity_type = 'field_entity') {
// Check required properties.
if (empty($values['type'])) {
throw new FieldException('Attempt to create a field with no type.');
}
// Temporary BC layer: accept both 'id' and 'field_name'.
// @todo $field_name and the handling for it will be removed in
// http://drupal.org/node/1953408.
if (empty($values['field_name']) && empty($values['id'])) {
throw new FieldException('Attempt to create an unnamed field.');
}
if (empty($values['id'])) {
$values['id'] = $values['field_name'];
unset($values['field_name']);
}
if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $values['id'])) {
throw new FieldException('Attempt to create a field with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character');
}
parent::__construct($values, $entity_type);
}
/**
* {@inheritdoc}
*/
public function getExportProperties() {
$names = array(
'id',
'uuid',
'status',
'langcode',
'type',
'settings',
'module',
'active',
'entity_types',
'storage',
'locked',
'cardinality',
'translatable',
'indexes',
);
$properties = array();
foreach ($names as $name) {
$properties[$name] = $this
->get($name);
}
return $properties;
}
/**
* Overrides \Drupal\Core\Entity\Entity::save().
*
* @return int
* Either SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
*
* @throws \Drupal\field\FieldException
* If the field definition is invalid.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
*/
public function save() {
// Clear the derived data about the field.
unset($this->schema, $this->storageDetails);
if ($this
->isNew()) {
return $this
->saveNew();
}
else {
return $this
->saveUpdated();
}
}
/**
* Saves a new field definition.
*
* @return int
* SAVED_NEW if the definition was saved.
*
* @throws \Drupal\field\FieldException
* If the field definition is invalid.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
*/
protected function saveNew() {
$module_handler = \Drupal::moduleHandler();
$entity_manager = \Drupal::entityManager();
$storage_controller = $entity_manager
->getStorageController($this->entityType);
// Field name cannot be longer than Field::ID_MAX_LENGTH characters. We
// use drupal_strlen() because the DB layer assumes that column widths
// are given in characters rather than bytes.
if (drupal_strlen($this->id) > static::ID_MAX_LENGTH) {
throw new FieldException(format_string('Attempt to create a field with an ID longer than @max characters: %id', array(
'@max' => static::ID_MAX_LENGTH,
'%id' => $this->id,
)));
}
// Ensure the field name is unique (we do not care about deleted fields).
if ($prior_field = current($storage_controller
->load(array(
$this->id,
)))) {
$message = $prior_field->active ? 'Attempt to create field name %id which already exists and is active.' : 'Attempt to create field name %id which already exists, although it is inactive.';
throw new FieldException(format_string($message, array(
'%id' => $this->id,
)));
}
// Disallow reserved field names. This can't prevent all field name
// collisions with existing entity properties, but some is better than
// none.
foreach ($entity_manager
->getDefinitions() as $type => $info) {
if (in_array($this->id, $info['entity_keys'])) {
throw new FieldException(format_string('Attempt to create field %id which is reserved by entity type %type.', array(
'%id' => $this->id,
'%type' => $type,
)));
}
}
// Check that the field type is known.
$field_type = field_info_field_types($this->type);
if (!$field_type) {
throw new FieldException(format_string('Attempt to create a field of unknown type %type.', array(
'%type' => $this->type,
)));
}
$this->module = $field_type['module'];
$this->active = TRUE;
// Make sure all settings are present, so that a complete field
// definition is passed to the various hooks and written to config.
$this->settings += $field_type['settings'];
// Provide default storage.
$this->storage += array(
'type' => variable_get('field_storage_default', 'field_sql_storage'),
'settings' => array(),
);
// Check that the storage type is known.
$storage_type = field_info_storage_types($this->storage['type']);
if (!$storage_type) {
throw new FieldException(format_string('Attempt to create a field with unknown storage type %type.', array(
'%type' => $this->storage['type'],
)));
}
$this->storage['module'] = $storage_type['module'];
$this->storage['active'] = TRUE;
// Provide default storage settings.
$this->storage['settings'] += $storage_type['settings'];
// Invoke the storage backend's hook_field_storage_create_field().
$module_handler
->invoke($this->storage['module'], 'field_storage_create_field', array(
$this,
));
// Save the configuration.
$result = parent::save();
field_cache_clear();
// Invoke hook_field_create_field() after the cache is cleared for API
// consistency.
$module_handler
->invokeAll('field_create_field', array(
$this,
));
return $result;
}
/**
* Saves an updated field definition.
*
* @return int
* SAVED_UPDATED if the definition was saved.
*
* @throws \Drupal\field\FieldException
* If the field definition is invalid.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
*/
protected function saveUpdated() {
$module_handler = \Drupal::moduleHandler();
$storage_controller = \Drupal::entityManager()
->getStorageController($this->entityType);
$original = $storage_controller
->loadUnchanged($this
->id());
// Some updates are always disallowed.
if ($this->type != $original->type) {
throw new FieldException("Cannot change an existing field's type.");
}
if ($this->entity_types != $original->entity_types) {
throw new FieldException("Cannot change an existing field's entity_types property.");
}
if ($this->storage['type'] != $original->storage['type']) {
throw new FieldException("Cannot change an existing field's storage type.");
}
// Make sure all settings are present, so that a complete field definition
// is saved. This allows calling code to perform partial updates on field
// objects.
$this->settings += $original->settings;
$has_data = field_has_data($this);
// See if any module forbids the update by throwing an exception. This
// invokes hook_field_update_forbid().
$module_handler
->invokeAll('field_update_forbid', array(
$this,
$original,
$has_data,
));
// Tell the storage engine to update the field by invoking the
// hook_field_storage_update_field(). The storage engine can reject the
// definition update as invalid by raising an exception, which stops
// execution before the definition is written to config.
$module_handler
->invoke($this->storage['module'], 'field_storage_update_field', array(
$this,
$original,
$has_data,
));
// Save the configuration.
$result = parent::save();
field_cache_clear();
// Invoke hook_field_update_field() after the cache is cleared for API
// consistency.
$module_handler
->invokeAll('field_update_field', array(
$this,
$original,
$has_data,
));
return $result;
}
/**
* {@inheritdoc}
*/
public function delete() {
if (!$this->deleted) {
$module_handler = \Drupal::moduleHandler();
$instance_controller = \Drupal::entityManager()
->getStorageController('field_instance');
$state = \Drupal::state();
// Delete all non-deleted instances.
$instance_ids = array();
foreach ($this
->getBundles() as $entity_type => $bundles) {
foreach ($bundles as $bundle) {
$instance_ids[] = "{$entity_type}.{$bundle}.{$this->id}";
}
}
foreach ($instance_controller
->load($instance_ids) as $instance) {
// By default, FieldInstance::delete() will automatically try to delete
// a field definition when it is deleting the last instance of the
// field. Since the whole field is being deleted here, pass FALSE as
// the $field_cleanup parameter to prevent a loop.
$instance
->delete(FALSE);
}
// Mark field data for deletion by invoking
// hook_field_storage_delete_field().
$module_handler
->invoke($this->storage['module'], 'field_storage_delete_field', array(
$this,
));
// 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();
$config = $this
->getExportProperties();
$config['deleted'] = TRUE;
$deleted_fields[$this->uuid] = $config;
$state
->set('field.field.deleted', $deleted_fields);
parent::delete();
// Clear the cache.
field_cache_clear();
// Invoke hook_field_delete_field().
$module_handler
->invokeAll('field_delete_field', array(
$this,
));
}
}
/**
* {@inheritdoc}
*/
public function getSchema() {
if (!isset($this->schema)) {
$module_handler = \Drupal::moduleHandler();
// Collect the schema from the field type.
// @todo Use $module_handler->loadInclude() once
// http://drupal.org/node/1941000 is fixed.
module_load_install($this->module);
// Invoke hook_field_schema() for the field.
$schema = (array) $module_handler
->invoke($this->module, 'field_schema', array(
$this,
));
$schema += array(
'columns' => array(),
'indexes' => array(),
'foreign keys' => array(),
);
// Check that the schema does not include forbidden column names.
if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) {
throw new FieldException('Illegal field type columns.');
}
// Merge custom indexes with those specified by the field type. Custom
// indexes prevail.
$schema['indexes'] = $this->indexes + $schema['indexes'];
$this->schema = $schema;
}
return $this->schema;
}
/**
* {@inheritdoc}
*/
public function getStorageDetails() {
if (!isset($this->storageDetails)) {
$module_handler = \Drupal::moduleHandler();
// Collect the storage details from the storage backend, and let other
// modules alter it. This invokes hook_field_storage_details() and
// hook_field_storage_details_alter().
$details = (array) $module_handler
->invoke($this->storage['module'], 'field_storage_details', array(
$this,
));
$module_handler
->alter('field_storage_details', $details, $this);
$this->storageDetails = $details;
}
return $this->storageDetails;
}
/**
* {@inheritdoc}
*/
public function getBundles() {
if (empty($this->deleted)) {
$map = field_info_field_map();
if (isset($map[$this->id]['bundles'])) {
return $map[$this->id]['bundles'];
}
}
return array();
}
/**
* {@inheritdoc}
*/
public function offsetExists($offset) {
return isset($this->{$offset}) || in_array($offset, array(
'columns',
'foreign keys',
'bundles',
'storage_details',
));
}
/**
* {@inheritdoc}
*/
public function &offsetGet($offset) {
switch ($offset) {
case 'id':
return $this->uuid;
case 'field_name':
return $this->id;
case 'columns':
$this
->getSchema();
return $this->schema['columns'];
case 'foreign keys':
$this
->getSchema();
return $this->schema['foreign keys'];
case 'bundles':
$bundles = $this
->getBundles();
return $bundles;
case 'storage_details':
$this
->getStorageDetails();
return $this->storageDetails;
}
return $this->{$offset};
}
/**
* {@inheritdoc}
*/
public function offsetSet($offset, $value) {
if (!in_array($offset, array(
'columns',
'foreign keys',
'bundles',
'storage_details',
))) {
$this->{$offset} = $value;
}
}
/**
* {@inheritdoc}
*/
public function offsetUnset($offset) {
if (!in_array($offset, array(
'columns',
'foreign keys',
'bundles',
'storage_details',
))) {
unset($this->{$offset});
}
}
/**
* {@inheritdoc}
*/
public function serialize() {
// Only store the definition, not external objects or derived data.
return serialize($this
->getExportProperties());
}
/**
* {@inheritdoc}
*/
public function unserialize($serialized) {
$this
->__construct(unserialize($serialized));
}
/**
* A list of columns that can not be used as field type columns.
*
* @return array
*/
public static function getReservedColumns() {
return array(
'deleted',
);
}
}