diff --git a/core/includes/entity.inc b/core/includes/entity.inc index cc539dc..3025433 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -57,16 +57,22 @@ function entity_get_bundles($entity_type = NULL) { * NULL. */ function entity_invoke_bundle_hook($hook, $entity_type, $bundle, $bundle_new = NULL) { - \Drupal::entityManager()->clearCachedBundles(); + $entity_manager = \Drupal::entityManager(); + + $entity_manager->clearCachedBundles(); // Notify the entity storage. $method = 'onBundle' . ucfirst($hook); - $storage = \Drupal::entityManager()->getStorage($entity_type); + $storage = $entity_manager->getStorage($entity_type); if (method_exists($storage, $method)) { $storage->$method($bundle, $bundle_new); } // Invoke hook_entity_bundle_*() hooks. \Drupal::moduleHandler()->invokeAll('entity_bundle_' . $hook, array($entity_type, $bundle, $bundle_new)); + // Clear the cached field definitions (not needed in case of 'create'). + if ($hook == 'rename' || $hook == 'delete') { + $entity_manager->clearCachedFieldDefinitions(); + } } /** diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php index b050ba0..c2c2644 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php @@ -10,16 +10,15 @@ use Drupal\Component\Utility\String; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Database; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; use Drupal\Core\Entity\Sql\DefaultTableMapping; use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\field\Entity\FieldConfig; -use Drupal\field\FieldConfigInterface; -use Drupal\field\FieldConfigUpdateForbiddenException; -use Drupal\field\FieldInstanceConfigInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -147,6 +146,7 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa $this->database = $database; $this->entityManager = $entity_manager; + $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id()); // @todo Remove table names from the entity type definition in // https://drupal.org/node/2232465 @@ -227,7 +227,7 @@ public function getSchema() { */ protected function schemaHandler() { if (!isset($this->schemaHandler)) { - $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this); + $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this, $this->database); } return $this->schemaHandler; } @@ -237,14 +237,12 @@ protected function schemaHandler() { */ public function getTableMapping() { if (!isset($this->tableMapping)) { + $table_mapping = new DefaultTableMapping($this->fieldStorageDefinitions, $this->entityManager->getBaseFieldDefinitions($this->entityTypeId)); + $this->tableMapping = $table_mapping; - $definitions = array_filter($this->getFieldStorageDefinitions(), function (FieldStorageDefinitionInterface $definition) { - // @todo Remove the check for FieldDefinitionInterface::isMultiple() when - // multiple-value base fields are supported in - // https://drupal.org/node/2248977. - return !$definition->hasCustomStorage() && !$definition->isMultiple(); + $definitions = array_filter($this->fieldStorageDefinitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) { + return $table_mapping->allowsSharedTableStorage($definition); }); - $this->tableMapping = new DefaultTableMapping($definitions); $key_fields = array_values(array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey))); $all_fields = array_keys($definitions); @@ -332,6 +330,25 @@ public function getTableMapping() { // "revision_table.langcode = data_table.langcode". ->setExtraColumns($this->revisionDataTable, array('default_langcode')); } + + // Add dedicated tables. + $definitions = array_filter($this->fieldStorageDefinitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) { + return $table_mapping->requiresDedicatedTableStorage($definition); + }); + $extra_columns = array( + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'langcode', + 'delta', + ); + foreach ($definitions as $field_name => $definition) { + foreach (array($table_mapping->getDedicatedDataTableName($definition), $table_mapping->getDedicatedRevisionTableName($definition)) as $table_name) { + $table_mapping->setFieldNames($table_name, array($field_name)); + $table_mapping->setExtraColumns($table_name, $extra_columns); + } + } } return $this->tableMapping; @@ -954,21 +971,23 @@ protected function doLoadFieldItems($entities, $age) { } // Collect impacted fields. - $fields = array(); + $storage_definitions = array(); $definitions = array(); + $table_mapping = $this->getTableMapping(); foreach ($bundles as $bundle => $v) { $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle); - foreach ($definitions[$bundle] as $field_name => $instance) { - if ($instance instanceof FieldInstanceConfigInterface) { - $fields[$field_name] = $instance->getFieldStorageDefinition(); + foreach ($definitions[$bundle] as $field_name => $field_definition) { + $storage_definition = $field_definition->getFieldStorageDefinition(); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $storage_definitions[$field_name] = $storage_definition; } } } // Load field data. $langcodes = array_keys(language_list(LanguageInterface::STATE_ALL)); - foreach ($fields as $field_name => $field) { - $table = $load_current ? static::_fieldTableName($field) : static::_fieldRevisionTableName($field); + foreach ($storage_definitions as $field_name => $storage_definition) { + $table = $load_current ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition); // Ensure that only values having valid languages are retrieved. Since we // are loading values for multiple entities, we cannot limit the query to @@ -992,12 +1011,12 @@ protected function doLoadFieldItems($entities, $age) { $delta_count[$row->entity_id][$row->langcode] = 0; } - if ($field->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->langcode] < $field->getCardinality()) { + if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->langcode] < $storage_definition->getCardinality()) { $item = array(); // For each column declared by the field, populate the item from the // prefixed database column. - foreach ($field->getColumns() as $column => $attributes) { - $column_name = static::_fieldColumnName($field, $column); + foreach ($storage_definition->getColumns() as $column => $attributes) { + $column_name = $table_mapping->getFieldColumnName($storage_definition, $column); // Unserialize the value if specified in the column schema. $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name; } @@ -1021,18 +1040,19 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { $entity_type = $entity->getEntityTypeId(); $default_langcode = $entity->getUntranslated()->language()->id; $translation_langcodes = array_keys($entity->getTranslationLanguages()); + $table_mapping = $this->getTableMapping(); if (!isset($vid)) { $vid = $id; } - foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $instance) { - if (!($instance instanceof FieldInstanceConfigInterface)) { + foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $field_definition) { + $storage_definition = $field_definition->getFieldStorageDefinition(); + if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; } - $field = $instance->getFieldStorageDefinition(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); // Delete and insert, rather than update, in case a value was added. if ($update) { @@ -1052,13 +1072,13 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { // Prepare the multi-insert query. $do_insert = FALSE; $columns = array('entity_id', 'revision_id', 'bundle', 'delta', 'langcode'); - foreach ($field->getColumns() as $column => $attributes) { - $columns[] = static::_fieldColumnName($field, $column); + foreach ($storage_definition->getColumns() as $column => $attributes) { + $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column); } $query = $this->database->insert($table_name)->fields($columns); $revision_query = $this->database->insert($revision_name)->fields($columns); - $langcodes = $field->isTranslatable() ? $translation_langcodes : array($default_langcode); + $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : array($default_langcode); foreach ($langcodes as $langcode) { $delta_count = 0; $items = $entity->getTranslation($langcode)->get($field_name); @@ -1073,15 +1093,15 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { 'delta' => $delta, 'langcode' => $langcode, ); - foreach ($field->getColumns() as $column => $attributes) { - $column_name = static::_fieldColumnName($field, $column); + foreach ($storage_definition->getColumns() as $column => $attributes) { + $column_name = $table_mapping->getFieldColumnName($storage_definition, $column); // Serialize the value if specified in the column schema. $record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column; } $query->values($record); $revision_query->values($record); - if ($field->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $field->getCardinality()) { + if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) { break; } } @@ -1103,13 +1123,14 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { * {@inheritdoc} */ protected function doDeleteFieldItems(EntityInterface $entity) { - foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $instance) { - if (!($instance instanceof FieldInstanceConfigInterface)) { + $table_mapping = $this->getTableMapping(); + foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) { + $storage_definition = $field_definition->getFieldStorageDefinition(); + if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; } - $field = $instance->getFieldStorageDefinition(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); $this->database->delete($table_name) ->condition('entity_id', $entity->id()) ->execute(); @@ -1125,11 +1146,13 @@ protected function doDeleteFieldItems(EntityInterface $entity) { protected function doDeleteFieldItemsRevision(EntityInterface $entity) { $vid = $entity->getRevisionId(); if (isset($vid)) { - foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $instance) { - if (!($instance instanceof FieldInstanceConfigInterface)) { + $table_mapping = $this->getTableMapping(); + foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) { + $storage_definition = $field_definition->getFieldStorageDefinition(); + if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; } - $revision_name = static::_fieldRevisionTableName($instance->getFieldStorageDefinition()); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); $this->database->delete($revision_name) ->condition('entity_id', $entity->id()) ->condition('revision_id', $vid) @@ -1141,157 +1164,53 @@ protected function doDeleteFieldItemsRevision(EntityInterface $entity) { /** * {@inheritdoc} */ - public function onFieldCreate(FieldConfigInterface $field) { - $schema = $this->_fieldSqlSchema($field); - foreach ($schema as $name => $table) { - $this->database->schema()->createTable($name, $table); - } + public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { + $this->schemaHandler()->createFieldSchema($storage_definition); } /** * {@inheritdoc} */ - public function onFieldUpdate(FieldConfigInterface $field) { - $original = $field->original; - - if (!$field->hasData()) { - // There is no data. Re-create the tables completely. - - if ($this->database->supportsTransactionalDDL()) { - // If the database supports transactional DDL, we can go ahead and rely - // on it. If not, we will have to rollback manually if something fails. - $transaction = $this->database->startTransaction(); - } - - try { - $original_schema = $this->_fieldSqlSchema($original); - foreach ($original_schema as $name => $table) { - $this->database->schema()->dropTable($name, $table); - } - $schema = $this->_fieldSqlSchema($field); - foreach ($schema as $name => $table) { - $this->database->schema()->createTable($name, $table); - } - } - catch (\Exception $e) { - if ($this->database->supportsTransactionalDDL()) { - $transaction->rollback(); - } - else { - // Recreate tables. - $original_schema = $this->_fieldSqlSchema($original); - foreach ($original_schema as $name => $table) { - if (!$this->database->schema()->tableExists($name)) { - $this->database->schema()->createTable($name, $table); - } - } - } - throw $e; - } - } - else { - if ($field->getColumns() != $original->getColumns()) { - throw new FieldConfigUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data."); - } - // There is data, so there are no column changes. Drop all the prior - // indexes and create all the new ones, except for all the priors that - // exist unchanged. - $table = static::_fieldTableName($original); - $revision_table = static::_fieldRevisionTableName($original); - - $schema = $field->getSchema(); - $original_schema = $original->getSchema(); - - foreach ($original_schema['indexes'] as $name => $columns) { - if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) { - $real_name = static::_fieldIndexName($field, $name); - $this->database->schema()->dropIndex($table, $real_name); - $this->database->schema()->dropIndex($revision_table, $real_name); - } - } - $table = static::_fieldTableName($field); - $revision_table = static::_fieldRevisionTableName($field); - foreach ($schema['indexes'] as $name => $columns) { - if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) { - $real_name = static::_fieldIndexName($field, $name); - $real_columns = array(); - foreach ($columns as $column_name) { - // Indexes can be specified as either a column name or an array with - // column name and length. Allow for either case. - if (is_array($column_name)) { - $real_columns[] = array( - static::_fieldColumnName($field, $column_name[0]), - $column_name[1], - ); - } - else { - $real_columns[] = static::_fieldColumnName($field, $column_name); - } - } - $this->database->schema()->addIndex($table, $real_name, $real_columns); - $this->database->schema()->addIndex($revision_table, $real_name, $real_columns); - } - } - } + public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + $this->schemaHandler()->updateFieldSchema($storage_definition, $original); } /** * {@inheritdoc} */ - public function onFieldDelete(FieldConfigInterface $field) { - // Mark all data associated with the field for deletion. - $table = static::_fieldTableName($field); - $revision_table = static::_fieldRevisionTableName($field); - $this->database->update($table) - ->fields(array('deleted' => 1)) - ->execute(); + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { + $table_mapping = $this->getTableMapping(); - // Move the table to a unique name while the table contents are being - // deleted. - $deleted_field = clone $field; - $deleted_field->deleted = TRUE; - $new_table = static::_fieldTableName($deleted_field); - $revision_new_table = static::_fieldRevisionTableName($deleted_field); - $this->database->schema()->renameTable($table, $new_table); - $this->database->schema()->renameTable($revision_table, $revision_new_table); - } + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + // Mark all data associated with the field for deletion. + $table = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + $this->database->update($table) + ->fields(array('deleted' => 1)) + ->execute(); + } - /** - * {@inheritdoc} - */ - public function onInstanceDelete(FieldInstanceConfigInterface $instance) { - $field = $instance->getFieldStorageDefinition(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); - $this->database->update($table_name) - ->fields(array('deleted' => 1)) - ->condition('bundle', $instance->bundle) - ->execute(); - $this->database->update($revision_name) - ->fields(array('deleted' => 1)) - ->condition('bundle', $instance->bundle) - ->execute(); + // Update the field schema. + $this->schemaHandler()->markFieldSchemaAsDeleted($storage_definition); } /** * {@inheritdoc} */ - public function onBundleRename($bundle, $bundle_new) { - // We need to account for deleted fields and instances. The method runs - // before the instance definitions are updated, so we need to fetch them - // using the old bundle name. - $instances = entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $this->entityTypeId, 'bundle' => $bundle, 'include_deleted' => TRUE)); - foreach ($instances as $instance) { - $field = $instance->getFieldStorageDefinition(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); + public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { + $table_mapping = $this->getTableMapping(); + $storage_definition = $field_definition->getFieldStorageDefinition(); + // Mark field data as deleted. + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); $this->database->update($table_name) - ->fields(array('bundle' => $bundle_new)) - ->condition('bundle', $bundle) + ->fields(array('deleted' => 1)) + ->condition('bundle', $field_definition->getBundle()) ->execute(); $this->database->update($revision_name) - ->fields(array('bundle' => $bundle_new)) - ->condition('bundle', $bundle) + ->fields(array('deleted' => 1)) + ->condition('bundle', $field_definition->getBundle()) ->execute(); } } @@ -1299,348 +1218,157 @@ public function onBundleRename($bundle, $bundle_new) { /** * {@inheritdoc} */ - protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance) { - $field = $instance->getFieldStorageDefinition(); - $table_name = static::_fieldTableName($field); - $query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC)) - ->condition('entity_id', $entity->id()) - ->orderBy('delta'); - foreach ($field->getColumns() as $column_name => $data) { - $query->addField('t', static::_fieldColumnName($field, $column_name), $column_name); - } - return $query->execute()->fetchAll(); - } + public function onBundleRename($bundle, $bundle_new) { + // The method runs before the field definitions are updated, so we use the + // old bundle name. + $field_definitions = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle); + // We need to handle deleted fields too. For now, this only makes sense for + // configurable fields, so we use the specific API. + // @todo Use the unified store of deleted field definitions instead in + // https://www.drupal.org/node/2282119 + $field_definitions += entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $this->entityTypeId, 'bundle' => $bundle, 'deleted' => TRUE, 'include_deleted' => TRUE)); + $table_mapping = $this->getTableMapping(); - /** - * {@inheritdoc} - */ - public function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance) { - $field = $instance->getFieldStorageDefinition(); - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); - $this->database->delete($table_name) - ->condition('entity_id', $entity->id()) - ->execute(); - $this->database->delete($revision_name) - ->condition('entity_id', $entity->id()) - ->execute(); + foreach ($field_definitions as $field_definition) { + $storage_definition = $field_definition->getFieldStorageDefinition(); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); + $this->database->update($table_name) + ->fields(array('bundle' => $bundle_new)) + ->condition('bundle', $bundle) + ->execute(); + $this->database->update($revision_name) + ->fields(array('bundle' => $bundle_new)) + ->condition('bundle', $bundle) + ->execute(); + } + } } /** * {@inheritdoc} */ - public function onFieldPurge(FieldConfigInterface $field) { - $table_name = static::_fieldTableName($field); - $revision_name = static::_fieldRevisionTableName($field); - $this->database->schema()->dropTable($table_name); - $this->database->schema()->dropTable($revision_name); - } - - /** - * Gets the SQL table schema. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. - * - * @param \Drupal\field\FieldConfigInterface $field - * The field object - * @param array $schema - * The field schema array. Mandatory for upgrades, omit otherwise. - * - * @return array - * The same as a hook_schema() implementation for the data and the - * revision tables. - * - * @see hook_schema() - */ - public static function _fieldSqlSchema(FieldConfigInterface $field, array $schema = NULL) { - if ($field->deleted) { - $description_current = "Data storage for deleted field {$field->uuid()} ({$field->entity_type}, {$field->getName()})."; - $description_revision = "Revision archive storage for deleted field {$field->uuid()} ({$field->entity_type}, {$field->getName()})."; - } - else { - $description_current = "Data storage for {$field->entity_type} field {$field->getName()}."; - $description_revision = "Revision archive storage for {$field->entity_type} field {$field->getName()}."; - } - - $entity_type_id = $field->entity_type; - $entity_manager = \Drupal::entityManager(); - $entity_type = $entity_manager->getDefinition($entity_type_id); - $definitions = $entity_manager->getBaseFieldDefinitions($entity_type_id); - - // Define the entity ID schema based on the field definitions. - $id_definition = $definitions[$entity_type->getKey('id')]; - if ($id_definition->getType() == 'integer') { - $id_schema = array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The entity id this data is attached to', - ); - } - else { - $id_schema = array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'description' => 'The entity id this data is attached to', - ); - } - - // Define the revision ID schema, default to integer if there is no revision - // ID. - $revision_id_definition = $entity_type->hasKey('revision') ? $definitions[$entity_type->getKey('revision')] : NULL; - if (!$revision_id_definition || $revision_id_definition->getType() == 'integer') { - $revision_id_schema = array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', - ); - } - else { - $revision_id_schema = array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', - ); - } - - $current = array( - 'description' => $description_current, - 'fields' => array( - 'bundle' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance', - ), - 'deleted' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'A boolean indicating whether this data item has been deleted' - ), - 'entity_id' => $id_schema, - 'revision_id' => $revision_id_schema, - 'langcode' => array( - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The language code for this data item.', - ), - 'delta' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The sequence number for this data item, used for multi-value fields', - ), - ), - 'primary key' => array('entity_id', 'deleted', 'delta', 'langcode'), - 'indexes' => array( - 'bundle' => array('bundle'), - 'deleted' => array('deleted'), - 'entity_id' => array('entity_id'), - 'revision_id' => array('revision_id'), - 'langcode' => array('langcode'), - ), - ); + protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) { + // Check whether the whole field storage definition is gone, or just some + // bundle fields. + $storage_definition = $field_definition->getFieldStorageDefinition(); + $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); + $table_mapping = $this->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); - if (!$schema) { - $schema = $field->getSchema(); + // Get the entities which we want to purge first. + $entity_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC)); + $or = $entity_query->orConditionGroup(); + foreach ($storage_definition->getColumns() as $column_name => $data) { + $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); } + $entity_query + ->distinct(TRUE) + ->fields('t', array('entity_id')) + ->condition('bundle', $field_definition->getBundle()) + ->range(0, $batch_size); - // Add field columns. - foreach ($schema['columns'] as $column_name => $attributes) { - $real_name = static::_fieldColumnName($field, $column_name); - $current['fields'][$real_name] = $attributes; + // Create a map of field data table column names to field column names. + $column_map = array(); + foreach ($storage_definition->getColumns() as $column_name => $data) { + $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name; } - // Add unique keys. - foreach ($schema['unique keys'] as $unique_key_name => $columns) { - $real_name = static::_fieldIndexName($field, $unique_key_name); - foreach ($columns as $column_name) { - $current['unique keys'][$real_name][] = static::_fieldColumnName($field, $column_name); - } - } - - // Add indexes. - foreach ($schema['indexes'] as $index_name => $columns) { - $real_name = static::_fieldIndexName($field, $index_name); - foreach ($columns as $column_name) { - // Indexes can be specified as either a column name or an array with - // column name and length. Allow for either case. - if (is_array($column_name)) { - $current['indexes'][$real_name][] = array( - static::_fieldColumnName($field, $column_name[0]), - $column_name[1], - ); + $entities = array(); + $items_by_entity = array(); + foreach ($entity_query->execute() as $row) { + $item_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC)) + ->fields('t') + ->condition('entity_id', $row['entity_id']) + ->orderBy('delta'); + + foreach ($item_query->execute() as $item_row) { + if (!isset($entities[$item_row['revision_id']])) { + // Create entity with the right revision id and entity id combination. + $item_row['entity_type'] = $this->entityTypeId; + // @todo: Replace this by an entity object created via an entity + // factory, see https://drupal.org/node/1867228. + $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row); } - else { - $current['indexes'][$real_name][] = static::_fieldColumnName($field, $column_name); + $item = array(); + foreach ($column_map as $db_column => $field_column) { + $item[$field_column] = $item_row[$db_column]; } + $items_by_entity[$item_row['revision_id']][] = $item; } } - // Add foreign keys. - foreach ($schema['foreign keys'] as $specifier => $specification) { - $real_name = static::_fieldIndexName($field, $specifier); - $current['foreign keys'][$real_name]['table'] = $specification['table']; - foreach ($specification['columns'] as $column_name => $referenced) { - $sql_storage_column = static::_fieldColumnName($field, $column_name); - $current['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced; - } + // Create field item objects and return. + foreach ($items_by_entity as $revision_id => $values) { + $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entities[$revision_id]); } - - // Construct the revision table. - $revision = $current; - $revision['description'] = $description_revision; - $revision['primary key'] = array('entity_id', 'revision_id', 'deleted', 'delta', 'langcode'); - $revision['fields']['revision_id']['not null'] = TRUE; - $revision['fields']['revision_id']['description'] = 'The entity revision id this data is attached to'; - - return array( - static::_fieldTableName($field) => $current, - static::_fieldRevisionTableName($field) => $revision, - ); + return $items_by_entity; } /** - * Generates a table name for a field data table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. Only - * call this function to write a query that \Drupal::entityQuery() does not - * support. Always call entity_load() before using the data found in the - * table. - * - * @param \Drupal\field\FieldConfigInterface $field - * The field object. - * - * @return string - * A string containing the generated name for the database table. - * - */ - static public function _fieldTableName(FieldConfigInterface $field) { - if ($field->deleted) { - // When a field is a deleted, the table is renamed to - // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with - // table names longer than 64 characters, we hash the uuid and return the - // first 10 characters so we end up with a short unique ID. - return "field_deleted_data_" . substr(hash('sha256', $field->uuid()), 0, 10); - } - else { - return static::_generateFieldTableName($field, FALSE); - } - } - - /** - * Generates a table name for a field revision archive table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. Only - * call this function to write a query that \Drupal::entityQuery() does not - * support. Always call entity_load() before using the data found in the - * table. - * - * @param \Drupal\field\FieldConfigInterface $field - * The field object. - * - * @return string - * A string containing the generated name for the database table. + * {@inheritdoc} */ - static public function _fieldRevisionTableName(FieldConfigInterface $field) { - if ($field->deleted) { - // When a field is a deleted, the table is renamed to - // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with - // table names longer than 64 characters, we hash the uuid and return the - // first 10 characters so we end up with a short unique ID. - return "field_deleted_revision_" . substr(hash('sha256', $field->uuid()), 0, 10); - } - else { - return static::_generateFieldTableName($field, TRUE); - } + protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) { + $storage_definition = $field_definition->getFieldStorageDefinition(); + $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); + $table_mapping = $this->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); + $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id(); + $this->database->delete($table_name) + ->condition('revision_id', $revision_id) + ->execute(); + $this->database->delete($revision_name) + ->condition('revision_id', $revision_id) + ->execute(); } /** - * Generates a safe and unanbiguous field table name. - * - * The method accounts for a maximum table name length of 64 characters, and - * takes care of disambiguation. - * - * @param \Drupal\field\FieldConfigInterface $field - * The field object. - * @param bool $revision - * TRUE for revision table, FALSE otherwise. - * - * @return string - * The final table name. + * {@inheritdoc} */ - static protected function _generateFieldTableName(FieldConfigInterface $field, $revision) { - $separator = $revision ? '_revision__' : '__'; - $table_name = $field->entity_type . $separator . $field->name; - // Limit the string to 48 characters, keeping a 16 characters margin for db - // prefixes. - if (strlen($table_name) > 48) { - // Use a shorter separator, a truncated entity_type, and a hash of the - // field UUID. - $separator = $revision ? '_r__' : '__'; - // Truncate to the same length for the current and revision tables. - $entity_type = substr($field->entity_type, 0, 34); - $field_hash = substr(hash('sha256', $field->uuid()), 0, 10); - $table_name = $entity_type . $separator . $field_hash; - } - return $table_name; + public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { + $this->schemaHandler()->deleteFieldSchema($storage_definition); } /** - * Generates an index name for a field data table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. - * - * @param \Drupal\field\FieldConfigInterface $field - * The field structure - * @param string $index - * The name of the index. - * - * @return string - * A string containing a generated index name for a field data table that is - * unique among all other fields. + * {@inheritdoc} */ - static public function _fieldIndexName(FieldConfigInterface $field, $index) { - return $field->getName() . '_' . $index; + public function countFieldData($storage_definition, $as_bool = FALSE) { + $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); + $table_mapping = $this->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); + + $query = $this->database->select($table_name, 't'); + $or = $query->orConditionGroup(); + foreach ($storage_definition->getColumns() as $column_name => $data) { + $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); + } + $query + ->condition($or) + ->fields('t', array('entity_id')) + ->distinct(TRUE); + // If we are performing the query just to check if the field has data + // limit the number of rows. + if ($as_bool) { + $query->range(0, 1); + } + $count = $query->countQuery()->execute()->fetchField(); + return $as_bool ? (bool) $count : (int) $count; } /** - * Generates a column name for a field data table. + * Returns whether the passed field has been already deleted. * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. Only - * call this function to write a query that \Drupal::entityQuery() does not - * support. Always call entity_load() before using the data found in the - * table. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. * - * @param \Drupal\field\FieldConfigInterface $field - * The field object. - * @param string $column - * The name of the column. - * - * @return string - * A string containing a generated column name for a field data table that is - * unique among all other fields. + * @return bool + * Whether the field has been already deleted. */ - static public function _fieldColumnName(FieldConfigInterface $field, $column) { - return in_array($column, FieldConfig::getReservedColumns()) ? $column : $field->getName() . '_' . $column; + protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) { + return !array_key_exists($storage_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId)); } } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php index 98dbd2f..12aeca3 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php @@ -8,7 +8,7 @@ namespace Drupal\Core\Entity; use Drupal\Core\Entity\Query\QueryException; -use Drupal\field\FieldInstanceConfigInterface; +use Drupal\Core\Field\FieldDefinitionInterface; /** * Defines a null entity storage. @@ -109,13 +109,14 @@ protected function doDeleteFieldItemsRevision(EntityInterface $entity) { /** * {@inheritdoc} */ - protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance) { + protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) { + return array(); } /** * {@inheritdoc} */ - protected function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance) { + protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) { } /** @@ -130,4 +131,11 @@ protected function doSave($id, EntityInterface $entity) { protected function has($id, EntityInterface $entity) { } + /** + * {@inheritdoc} + */ + public function countFieldData($storage_definition, $as_bool = FALSE) { + return $as_bool ? FALSE : 0; + } + } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index 29c1de9..dad9674 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -8,8 +8,9 @@ namespace Drupal\Core\Entity; use Drupal\Component\Utility\String; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Cache\Cache; -use Drupal\field\FieldConfigInterface; use Drupal\field\FieldInstanceConfigInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -267,32 +268,32 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) { /** * {@inheritdoc} */ - public function onFieldCreate(FieldConfigInterface $field) { } + public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { } /** * {@inheritdoc} */ - public function onFieldUpdate(FieldConfigInterface $field) { } + public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { } /** * {@inheritdoc} */ - public function onFieldDelete(FieldConfigInterface $field) { } + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { } /** * {@inheritdoc} */ - public function onInstanceCreate(FieldInstanceConfigInterface $instance) { } + public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) { } /** * {@inheritdoc} */ - public function onInstanceUpdate(FieldInstanceConfigInterface $instance) { } + public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) { } /** * {@inheritdoc} */ - public function onInstanceDelete(FieldInstanceConfigInterface $instance) { } + public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { } /** * {@inheritdoc} @@ -312,45 +313,46 @@ public function onBundleDelete($bundle) { } /** * {@inheritdoc} */ - public function onFieldItemsPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance) { - if ($values = $this->readFieldItemsToPurge($entity, $instance)) { - $items = \Drupal::typedDataManager()->create($instance, $values, $instance->getName(), $entity); + public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) { + $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size); + + foreach ($items_by_entity as $items) { $items->delete(); + $this->purgeFieldItems($items->getEntity(), $field_definition); } - $this->purgeFieldItems($entity, $instance); + return count($items_by_entity); } /** - * Reads values to be purged for a single field of a single entity. + * Reads values to be purged for a single field. * * This method is called during field data purge, on fields for which * onFieldDelete() or onFieldInstanceDelete() has previously run. * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - * @param \Drupal\field\FieldInstanceConfigInterface $instance - * The field instance. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field definition. + * @param $batch_size + * The maximum number of field data records to purge before returning. * - * @return array - * The field values, in their canonical array format (numerically indexed - * array of items, each item being a property/value array). + * @return \Drupal\Core\Field\FieldItemListInterface[] + * An array of field item lists, keyed by entity revision id. */ - abstract protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance); + abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size); /** - * Removes field data from storage during purge. + * Removes field items from storage per entity during purge. * - * @param EntityInterface $entity - * The entity whose values are being purged. - * @param FieldInstanceConfigInterface $instance + * @param ContentEntityInterface $entity + * The entity revision, whose values are being purged. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition * The field whose values are bing purged. */ - abstract protected function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance); + abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition); /** * {@inheritdoc} */ - public function onFieldPurge(FieldConfigInterface $field) { } + public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { } /** * Checks translation statuses and invoke the related hooks if needed. diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index cf5ae6b..ba644be 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -394,11 +394,13 @@ protected function buildBaseFieldDefinitions($entity_type_id) { } } - // Automatically set the field name for non-configurable fields. + // Automatically set the field name, target entity type and bundle + // for non-configurable fields. foreach ($base_field_definitions as $field_name => $base_field_definition) { if ($base_field_definition instanceof FieldDefinition) { $base_field_definition->setName($field_name); $base_field_definition->setTargetEntityTypeId($entity_type_id); + $base_field_definition->setBundle(NULL); } } @@ -491,11 +493,13 @@ protected function buildBundleFieldDefinitions($entity_type_id, $bundle, array $ } } - // Automatically set the field name for non-configurable fields. + // Automatically set the field name, target entity type and bundle + // for non-configurable fields. foreach ($bundle_field_definitions as $field_name => $field_definition) { if ($field_definition instanceof FieldDefinition) { $field_definition->setName($field_name); $field_definition->setTargetEntityTypeId($entity_type_id); + $field_definition->setBundle($bundle); } } diff --git a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php new file mode 100644 index 0000000..54e2bff --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php @@ -0,0 +1,13 @@ + substr($specifier, 3), 'include_deleted' => TRUE))) { - $field = current($fields); - } - } - elseif (isset($field_storage_definitions[$specifier])) { + if (isset($field_storage_definitions[$specifier])) { $field = $field_storage_definitions[$specifier]; } else { $field = FALSE; } + // If we managed to retrieve a configurable field, process it. if ($field instanceof FieldConfigInterface) { // Find the field column. $column = $field->getMainPropertyName(); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping(); + if ($key < $count) { $next = $specifiers[$key + 1]; // Is this a field column? $columns = $field->getColumns(); - if (isset($columns[$next]) || in_array($next, FieldConfig::getReservedColumns())) { + if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) { // Use it. $column = $next; // Do not process it again. @@ -150,7 +146,7 @@ public function addField($field, $type, $langcode) { } } $table = $this->ensureFieldTable($index_prefix, $field, $type, $langcode, $base_table, $entity_id_field, $field_id_field); - $sql_column = ContentEntityDatabaseStorage::_fieldColumnName($field, $column); + $sql_column = $table_mapping->getFieldColumnName($field, $column); } // This is an entity base field (non-configurable field). else { @@ -228,11 +224,13 @@ protected function ensureEntityTable($index_prefix, $property, $type, $langcode, protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) { $field_name = $field->getName(); if (!isset($this->fieldTables[$index_prefix . $field_name])) { - $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? ContentEntityDatabaseStorage::_fieldTableName($field) : ContentEntityDatabaseStorage::_fieldRevisionTableName($field); + $entity_type_id = $this->sqlQuery->getMetaData('entity_type'); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping(); + $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field); if ($field->getCardinality() != 1) { $this->sqlQuery->addMetaData('simple_query', FALSE); } - $entity_type = $this->sqlQuery->getMetaData('entity_type'); $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode); } return $this->fieldTables[$index_prefix . $field_name]; diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php index 2294794..125e6f9 100644 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php @@ -7,9 +7,12 @@ namespace Drupal\Core\Entity\Schema; +use Drupal\Core\Database\Connection; use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; +use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Defines a schema handler that supports revisionable, translatable entities. @@ -45,6 +48,13 @@ class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface { protected $schema; /** + * The database connection to be used. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** * Constructs a ContentEntitySchemaHandler. * * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager @@ -53,11 +63,191 @@ class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface { * The entity type. * @param \Drupal\Core\Entity\ContentEntityDatabaseStorage $storage * The storage of the entity type. This must be an SQL-based storage. + * @param \Drupal\Core\Database\Connection $database + * The database connection to be used. */ - public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage) { + public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage, Connection $database) { $this->entityType = $entity_type; $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id()); $this->storage = $storage; + $this->database = $database; + } + + /** + * {@inheritdoc} + */ + public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition) { + $this->performSchemaOperation('create', $storage_definition); + } + + /** + * TODO + */ + protected function performSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) { + $table_mapping = $this->storage->getTableMapping(); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original); + } + elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) { + $this->{$operation . 'SharedTableSchema'}($storage_definition, $original); + } + } + + /** + * TODO + */ + protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + $schema = $this->getDedicatedTableSchema($storage_definition); + foreach ($schema as $name => $table) { + $this->database->schema()->createTable($name, $table); + } + } + + /** + * TODO + */ + protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + // TODO + } + + /** + * {@inheritdoc} + */ + public function markFieldSchemaAsDeleted(FieldStorageDefinitionInterface $storage_definition) { + $table_mapping = $this->storage->getTableMapping(); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + // Move the table to a unique name while the table contents are being + // deleted. + $table = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); + $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE); + $this->database->schema()->renameTable($table, $new_table); + $this->database->schema()->renameTable($revision_table, $revision_new_table); + } + } + + /** + * {@inheritdoc} + */ + public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition) { + $this->performSchemaOperation('delete', $storage_definition); + } + + /** + * {@inheritdoc} + */ + protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + $table_mapping = $this->storage->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE); + $this->database->schema()->dropTable($table_name); + $this->database->schema()->dropTable($revision_name); + } + + /** + * TODO + */ + protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + // TODO + } + + /** + * {@inheritdoc} + */ + public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + $this->performSchemaOperation('update', $storage_definition, $original); + } + + /** + * {@inheritdoc} + */ + protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + if (!$storage_definition->hasData()) { + // There is no data. Re-create the tables completely. + if ($this->database->supportsTransactionalDDL()) { + // If the database supports transactional DDL, we can go ahead and rely + // on it. If not, we will have to rollback manually if something fails. + $transaction = $this->database->startTransaction(); + } + try { + $original_schema = $this->getDedicatedTableSchema($original); + foreach ($original_schema as $name => $table) { + $this->database->schema()->dropTable($name, $table); + } + $schema = $this->getDedicatedTableSchema($storage_definition); + foreach ($schema as $name => $table) { + $this->database->schema()->createTable($name, $table); + } + } + catch (\Exception $e) { + if ($this->database->supportsTransactionalDDL()) { + $transaction->rollback(); + } + else { + // Recreate tables. + $original_schema = $this->getDedicatedTableSchema($original); + foreach ($original_schema as $name => $table) { + if (!$this->database->schema()->tableExists($name)) { + $this->database->schema()->createTable($name, $table); + } + } + } + throw $e; + } + } + else { + if ($storage_definition->getColumns() != $original->getColumns()) { + throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data."); + } + // There is data, so there are no column changes. Drop all the prior + // indexes and create all the new ones, except for all the priors that + // exist unchanged. + $table_mapping = $this->storage->getTableMapping(); + $table = $table_mapping->getDedicatedDataTableName($original); + $revision_table = $table_mapping->getDedicatedRevisionTableName($original); + + $schema = $storage_definition->getSchema(); + $original_schema = $original->getSchema(); + + foreach ($original_schema['indexes'] as $name => $columns) { + if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) { + $real_name = $this->getFieldIndexName($storage_definition, $name); + $this->database->schema()->dropIndex($table, $real_name); + $this->database->schema()->dropIndex($revision_table, $real_name); + } + } + $table = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + foreach ($schema['indexes'] as $name => $columns) { + if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) { + $real_name = $this->getFieldIndexName($storage_definition, $name); + $real_columns = array(); + foreach ($columns as $column_name) { + // Indexes can be specified as either a column name or an array with + // column name and length. Allow for either case. + if (is_array($column_name)) { + $real_columns[] = array( + $table_mapping->getFieldColumnName($storage_definition, $column_name[0]), + $column_name[1], + ); + } + else { + $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name); + } + } + $this->database->schema()->addIndex($table, $real_name, $real_columns); + $this->database->schema()->addIndex($revision_table, $real_name, $real_columns); + } + } + } + } + + /** + * TODO + */ + protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + // TODO } /** @@ -82,10 +272,12 @@ public function getSchema() { $table_mapping = $this->storage->getTableMapping(); foreach ($table_mapping->getTableNames() as $table_name) { - // Add the schema from field definitions. foreach ($table_mapping->getFieldNames($table_name) as $field_name) { - $column_names = $table_mapping->getColumnNames($field_name); - $this->addFieldSchema($schema[$table_name], $field_name, $column_names); + // Add the schema for base field definitions. + if ($this->storage->getTableMapping()->allowsSharedTableStorage($this->fieldStorageDefinitions[$field_name])) { + $column_names = $table_mapping->getColumnNames($field_name); + $this->addFieldSchema($schema[$table_name], $field_name, $column_names); + } } // Add the schema for extra fields. @@ -141,6 +333,12 @@ protected function getTables() { */ protected function addFieldSchema(array &$schema, $field_name, array $column_mapping) { $field_schema = $this->fieldStorageDefinitions[$field_name]->getSchema(); + + // Check that the schema does not include forbidden column names. + if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) { + throw new FieldException('Illegal field type columns.'); + } + $field_description = $this->fieldStorageDefinitions[$field_name]->getDescription(); foreach ($column_mapping as $field_column_name => $schema_field_name) { @@ -309,6 +507,171 @@ protected function addDefaultLangcodeSchema(&$schema) { ); } + + /** + * Returns the SQL schema for a dedicated table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * + * FIXME Do we still need these two? The deleted parameter is not used. + * + * @param array $schema + * The field schema array. Mandatory for upgrades, omit otherwise. + * @param bool $deleted + * (optional) Whether the schema of the table holding the values of a + * deleted field should be returned. + * + * @return array + * The same as a hook_schema() implementation for the data and the + * revision tables. + * + * @see hook_schema() + */ + protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, array $schema = NULL, $deleted = FALSE) { + $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; + $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; + + $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')]; + if ($id_definition->getType() == 'integer') { + $id_schema = array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The entity id this data is attached to', + ); + } + else { + $id_schema = array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'description' => 'The entity id this data is attached to', + ); + } + + // Define the revision ID schema, default to integer if there is no revision + // ID. + // @todo Revisit this code: the revision id should match the entity id type + // if revisions are not supported. + $revision_id_definition = $this->entityType->isRevisionable() ? $this->fieldStorageDefinitions[$this->entityType->getKey('revision')] : NULL; + if (!$revision_id_definition || $revision_id_definition->getType() == 'integer') { + $revision_id_schema = array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', + ); + } + else { + $revision_id_schema = array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => FALSE, + 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', + ); + } + + $data_schema = array( + 'description' => $description_current, + 'fields' => array( + 'bundle' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance', + ), + 'deleted' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'A boolean indicating whether this data item has been deleted' + ), + 'entity_id' => $id_schema, + 'revision_id' => $revision_id_schema, + 'langcode' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The language code for this data item.', + ), + 'delta' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The sequence number for this data item, used for multi-value fields', + ), + ), + 'primary key' => array('entity_id', 'deleted', 'delta', 'langcode'), + 'indexes' => array( + 'bundle' => array('bundle'), + 'deleted' => array('deleted'), + 'entity_id' => array('entity_id'), + 'revision_id' => array('revision_id'), + 'langcode' => array('langcode'), + ), + ); + + if (!$schema) { + $schema = $storage_definition->getSchema(); + } + + // Check that the schema does not include forbidden column names. + $table_mapping = $this->storage->getTableMapping(); + if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) { + throw new FieldException(format_string('Illegal field type @field_type on @field_name.', array('@field_type' => $this->type, '@field_name' => $this->name))); + } + + // Add field columns. + foreach ($schema['columns'] as $column_name => $attributes) { + $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name); + $data_schema['fields'][$real_name] = $attributes; + } + + // Add indexes. + foreach ($schema['indexes'] as $index_name => $columns) { + $real_name = $this->getFieldIndexName($storage_definition, $index_name); + foreach ($columns as $column_name) { + // Indexes can be specified as either a column name or an array with + // column name and length. Allow for either case. + if (is_array($column_name)) { + $data_schema['indexes'][$real_name][] = array( + $table_mapping->getFieldColumnName($storage_definition, $column_name[0]), + $column_name[1], + ); + } + else { + $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name); + } + } + } + + // Add foreign keys. + foreach ($schema['foreign keys'] as $specifier => $specification) { + $real_name = $this->getFieldIndexName($storage_definition, $specifier); + $data_schema['foreign keys'][$real_name]['table'] = $specification['table']; + foreach ($specification['columns'] as $column_name => $referenced) { + $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $column_name); + $data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced; + } + } + + // Construct the revision table. + $revision_schema = $data_schema; + $revision_schema['description'] = $description_revision; + $revision_schema['primary key'] = array('entity_id', 'revision_id', 'deleted', 'delta', 'langcode'); + $revision_schema['fields']['revision_id']['not null'] = TRUE; + $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to'; + + return array( + $table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema, + $table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema, + ); + } + /** * Initializes common information for a base table. * @@ -341,6 +704,26 @@ protected function initializeBaseTable() { } /** + * Generates an index name for a field data table. + * + * @private Calling this function circumvents the entity system and is + * strongly discouraged. This function is not considered part of the public + * API and modules relying on it might break even in minor releases. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param string $index + * The name of the index. + * + * @return string + * A string containing a generated index name for a field data table that is + * unique among all other fields. + */ + protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) { + return $storage_definition->getName() . '_' . $index; + } + + /** * Initializes common information for a revision table. * * @return array diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php index a38b82c..d9d5efe 100644 --- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php +++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php @@ -7,8 +7,48 @@ namespace Drupal\Core\Entity\Schema; +use Drupal\Core\Field\FieldStorageDefinitionInterface; + /** * Defines an interface for handling the storage schema of entities. */ interface EntitySchemaHandlerInterface extends EntitySchemaProviderInterface { + + /** + * TODO + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The definition being created. + */ + public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition); + + /** + * TODO + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The definition being created. + */ + public function markFieldSchemaAsDeleted(FieldStorageDefinitionInterface $storage_definition); + + /** + * TODO + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The definition being created. + */ + public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition); + + /** + * TODO + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field being updated. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original + * The original storage definition; i.e., the definition before the update. + * + * @throws \Drupal\Core\Entity\Exception\StorageDefinitionUpdateForbiddenException + * Thrown when the update to the field is forbidden. + */ + public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original); + } diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php index e872d57..91c4624 100644 --- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php +++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php @@ -6,11 +6,13 @@ */ namespace Drupal\Core\Entity\Sql; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Defines a default table mapping class. */ -class DefaultTableMapping implements TableMappingInterface { +class DefaultTableMapping implements DefaultTableMappingInterface { /** * A list of field storage definitions that are available for this mapping. @@ -20,6 +22,13 @@ class DefaultTableMapping implements TableMappingInterface { protected $fieldStorageDefinitions = array(); /** + * A list of base field definitions that are available for this mapping. + * + * @var \Drupal\Core\Field\FieldDefinitionInterface[] + */ + protected $baseFieldDefinitions = array(); + + /** * A list of field names per table. * * This corresponds to the return value of @@ -76,9 +85,13 @@ class DefaultTableMapping implements TableMappingInterface { * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions * A list of field storage definitions that should be available for the * field columns of this table mapping. + * @param \Drupal\Core\Field\FieldDefinitionInterface[] $base_field_definitions + * A list of base field definitions that should be available for the field + * columns of this table mapping. */ - public function __construct(array $storage_definitions) { + public function __construct(array $storage_definitions, array $base_field_definitions) { $this->fieldStorageDefinitions = $storage_definitions; + $this->baseFieldDefinitions = $base_field_definitions; } /** @@ -99,7 +112,14 @@ public function getAllColumns($table_name) { $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], array_values($this->getColumnNames($field_name))); } - $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name)); + // There is just one field for each dedicated storage table, thus + // $field_name can only refer to it. + if (isset($field_name) && $this->requiresDedicatedTableStorage($this->fieldStorageDefinitions[$field_name])) { + $this->allColumns[$table_name] = array_merge($this->getExtraColumns($table_name), $this->allColumns[$table_name]); + } + else { + $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name)); + } } return $this->allColumns[$table_name]; } @@ -177,4 +197,105 @@ public function setExtraColumns($table_name, array $column_names) { return $this; } + /** + * {@inheritdoc} + */ + function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) { + return !$storage_definition->hasCustomStorage() && isset($this->baseFieldDefinitions[$storage_definition->getName()]) && !$storage_definition->isMultiple(); + } + + /** + * {@inheritdoc} + */ + function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) { + return !$storage_definition->hasCustomStorage() && (!isset($this->baseFieldDefinitions[$storage_definition->getName()]) || $storage_definition->isMultiple()); + } + + /** + * {@inheritdoc} + */ + function getDedicatedTableNames() { + // TODO + return array(); + } + + /** + * {@inheritdoc} + */ + public function getReservedColumns() { + return array('deleted'); + } + + /** + * {@inheritdoc} + */ + public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { + if ($is_deleted) { + // When a field is a deleted, the table is renamed to + // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with + // table names longer than 64 characters, we hash the unique storage + // identifier and return the first 10 characters so we end up with a short + // unique ID. + return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); + } + else { + return $this->generateFieldTableName($storage_definition, FALSE); + } + } + + /** + * {@inheritdoc} + */ + public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { + if ($is_deleted) { + // When a field is a deleted, the table is renamed to + // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with + // table names longer than 64 characters, we hash the unique storage + // identifier and return the first 10 characters so we end up with a short + // unique ID. + return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); + } + else { + return $this->generateFieldTableName($storage_definition, TRUE); + } + } + + /** + * Generates a safe and unambiguous field table name. + * + * The method accounts for a maximum table name length of 64 characters, and + * takes care of disambiguation. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param bool $revision + * TRUE for revision table, FALSE otherwise. + * + * @return string + * The final table name. + */ + protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) { + $separator = $revision ? '_revision__' : '__'; + $table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName(); + // Limit the string to 48 characters, keeping a 16 characters margin for db + // prefixes. + if (strlen($table_name) > 48) { + // Use a shorter separator, a truncated entity_type, and a hash of the + // field UUID. + $separator = $revision ? '_r__' : '__'; + // Truncate to the same length for the current and revision tables. + $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34); + $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); + $table_name = $entity_type . $separator . $field_hash; + } + return $table_name; + } + + /** + * {@inheritdoc} + */ + public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column) { + return in_array($column, $this->getReservedColumns()) ? $column : $storage_definition->getName() . '_' . $column; + } + } diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php new file mode 100644 index 0000000..68f7f2a --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php @@ -0,0 +1,103 @@ +definition['entity_type'] = $entity_type_id; @@ -520,6 +519,26 @@ public function setTargetEntityTypeId($entity_type_id) { /** * {@inheritdoc} */ + public function getBundle() { + return isset($this->definition['bundle']) ? $this->definition['bundle'] : NULL; + } + + /** + * Sets the bundle this field is defined for. + * + * @param string|null $bundle + * The bundle, or NULL if the field is not bundle-specific. + * + * @return $this + */ + public function setBundle($bundle) { + $this->definition['bundle'] = $bundle; + return $this; + } + + /** + * {@inheritdoc} + */ public function getSchema() { if (!isset($this->schema)) { // Get the schema from the field item class. @@ -534,11 +553,6 @@ public function getSchema() { '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']; @@ -564,19 +578,10 @@ public function getColumns() { } /** - * A list of columns that can not be used as field type columns. - * - * @return array - */ - public static function getReservedColumns() { - return array('deleted'); - } - - /** * {@inheritdoc} */ public function hasCustomStorage() { - return !empty($this->definition['custom_storage']); + return !empty($this->definition['custom_storage']) || $this->isComputed(); } /** @@ -587,8 +592,14 @@ public function hasCustomStorage() { * TRUE otherwise. * * @return $this + * + * @throws \LogicException + * Thrown if custom storage is to be set to FALSE for a computed field. */ public function setCustomStorage($custom_storage) { + if (!$custom_storage && $this->isComputed()) { + throw new \LogicException("Entity storage cannot store a computed field."); + } $this->definition['custom_storage'] = $custom_storage; return $this; } @@ -600,4 +611,11 @@ public function getFieldStorageDefinition() { return $this; } + /** + * {@inheritdoc} + */ + public function getUniqueStorageIdentifier() { + return $this->getTargetEntityTypeId() . '-' . $this->getName(); + } + } diff --git a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php index 2933656..be7f0fe 100644 --- a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php +++ b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php @@ -76,6 +76,15 @@ public function getName(); public function getType(); /** + * Gets the bundle the field is defined for. + * + * @return string|null + * The bundle the field is defined for, or NULL if it is a base field; i.e., + * it is not bundle-specific. + */ + public function getBundle(); + + /** * Returns whether the display for the field can be configured. * * @param string $display_context diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php index 161b0b3..10f090e 100644 --- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php +++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php @@ -297,4 +297,11 @@ public function getProvider(); */ public function hasCustomStorage(); + /** + * Returns a unique identifier for the field. + * + * @return string + */ + public function getUniqueStorageIdentifier(); + } diff --git a/core/modules/comment/src/Tests/CommentFieldsTest.php b/core/modules/comment/src/Tests/CommentFieldsTest.php index 5dd3bb7..5bb4380 100644 --- a/core/modules/comment/src/Tests/CommentFieldsTest.php +++ b/core/modules/comment/src/Tests/CommentFieldsTest.php @@ -85,9 +85,6 @@ function testCommentInstallAfterContentModule() { // Purge field data now to allow comment module to be uninstalled once the // field has been deleted. field_purge_batch(10); - // Call again as field_purge_batch() won't remove both the instances and - // field in a single pass. - field_purge_batch(10); // Disable the comment module. $edit = array(); diff --git a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php index 4308ca7..5e456a4 100644 --- a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php +++ b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php @@ -66,7 +66,7 @@ protected function setUp() { public function testViewsData() { // Test that the field is not exposed to views, since contact_message // entities have no storage. - $table_name = ContentEntityDatabaseStorage::_fieldTableName($this->field); + $table_name = 'contact_message__' . $this->field->getName(); $data = $this->container->get('views.views_data')->get($table_name); $this->assertFalse($data, 'The field is not exposed to Views.'); } diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index e8fb142..f364c51 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -6,7 +6,6 @@ */ use Drupal\Component\Utility\NestedArray; -use Drupal\field\FieldConfigUpdateForbiddenException; /** * @defgroup field_types Field Types API @@ -248,7 +247,7 @@ function hook_field_info_max_weight($entity_type, $bundle, $context, $context_mo * that cannot be updated. * * To forbid the update from occurring, throw a - * Drupal\field\FieldConfigUpdateForbiddenException. + * \Drupal\Core\Entity\Exception\StorageDefinitionUpdateForbiddenException. * * @param \Drupal\field\FieldConfigInterface $field * The field as it will be post-update. @@ -270,7 +269,7 @@ function hook_field_config_update_forbid(\Drupal\field\FieldConfigInterface $fie ->range(0, 1) ->execute(); if ($found) { - throw new FieldConfigUpdateForbiddenException("Cannot update a list field not to include keys with existing data"); + throw new \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException("Cannot update a list field not to include keys with existing data"); } } } diff --git a/core/modules/field/field.module b/core/modules/field/field.module index 4726101..4c087cf 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -198,20 +198,27 @@ function field_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundl } } - /** * Implements hook_entity_bundle_rename(). */ function field_entity_bundle_rename($entity_type, $bundle_old, $bundle_new) { - $instances = entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $entity_type, 'bundle' => $bundle_old)); + $instances = entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $entity_type, 'bundle' => $bundle_old, 'include_deleted' => TRUE)); foreach ($instances as $instance) { - if ($instance->entity_type == $entity_type && $instance->bundle == $bundle_old) { - $id_new = $instance->entity_type . '.' . $bundle_new . '.' . $instance->field_name; - $instance->set('id', $id_new); - $instance->bundle = $bundle_new; + $id_new = $instance->entity_type . '.' . $bundle_new . '.' . $instance->field_name; + $instance->set('id', $id_new); + $instance->bundle = $bundle_new; + // Save non-deleted instances. + if (!$instance->isDeleted()) { $instance->allowBundleRename(); $instance->save(); } + // Update deleted instances directly in the state storage. + else { + $state = \Drupal::state(); + $deleted_instances = $state->get('field.instance.deleted') ?: array(); + $deleted_instances[$instance->uuid] = $instance->toArray(); + $state->set('field.instance.deleted', $deleted_instances); + } } } diff --git a/core/modules/field/field.purge.inc b/core/modules/field/field.purge.inc index 4471970..4a289cf 100644 --- a/core/modules/field/field.purge.inc +++ b/core/modules/field/field.purge.inc @@ -80,7 +80,6 @@ function field_purge_batch($batch_size, $field_uuid = NULL) { else { $instances = entity_load_multiple_by_properties('field_instance_config', array('deleted' => TRUE, 'include_deleted' => TRUE)); } - $factory = \Drupal::service('entity.query'); $info = \Drupal::entityManager()->getDefinitions(); foreach ($instances as $instance) { $entity_type = $instance->entity_type; @@ -92,36 +91,16 @@ function field_purge_batch($batch_size, $field_uuid = NULL) { continue; } - $ids = (object) array( - 'entity_type' => $entity_type, - 'bundle' => $instance->bundle, - ); - // Retrieve some entities. - $query = $factory->get($entity_type) - ->condition('id:' . $instance->getFieldStorageDefinition()->uuid() . '.deleted', 1) - ->range(0, $batch_size); - // If there's no bundle key, all results will have the same bundle. - if ($bundle_key = $info[$entity_type]->getKey('bundle')) { - $query->condition($bundle_key, $ids->bundle); - } - $results = $query->execute(); - if ($results) { - foreach ($results as $revision_id => $entity_id) { - $ids->revision_id = $revision_id; - $ids->entity_id = $entity_id; - $entity = _field_create_entity_from_ids($ids); - \Drupal::entityManager()->getStorage($entity_type)->onFieldItemsPurge($entity, $instance); - $batch_size--; - } - // Only delete up to the maximum number of records. - if ($batch_size == 0) { - break; - } - } - else { + $count_purged = \Drupal::entityManager()->getStorage($entity_type)->purgeFieldData($instance, $batch_size); + if ($count_purged < $batch_size || $count_purged == 0) { // No field data remains for the instance, so we can remove it. field_purge_instance($instance); } + $batch_size -= $count_purged; + // Only delete up to the maximum number of records. + if ($batch_size == 0) { + break; + } } // Retrieve all deleted fields. Any that have no instances can be purged. @@ -187,7 +166,7 @@ function field_purge_field($field) { $state->set('field.field.deleted', $deleted_fields); // Notify the storage layer. - \Drupal::entityManager()->getStorage($field->entity_type)->onFieldPurge($field); + \Drupal::entityManager()->getStorage($field->entity_type)->finalizePurge($field); // Invoke external hooks after the cache is cleared for API consistency. \Drupal::moduleHandler()->invokeAll('field_purge_field', array($field)); diff --git a/core/modules/field/field.views.inc b/core/modules/field/field.views.inc index f74e97c..dd52cf3 100644 --- a/core/modules/field/field.views.inc +++ b/core/modules/field/field.views.inc @@ -22,7 +22,7 @@ function field_views_data() { $module_handler = \Drupal::moduleHandler(); foreach (\Drupal::entityManager()->getStorage('field_config')->loadMultiple() as $field) { - if (_field_views_is_sql_entity_type($field)) { + if (_field_views_get_entity_type_storage($field)) { $result = (array) $module_handler->invoke($field->module, 'field_views_data', array($field)); if (empty($result)) { $result = field_views_field_default_views_data($field); @@ -48,7 +48,7 @@ function field_views_data() { */ function field_views_data_alter(&$data) { foreach (\Drupal::entityManager()->getStorage('field_config')->loadMultiple() as $field) { - if (_field_views_is_sql_entity_type($field)) { + if (_field_views_get_entity_type_storage($field)) { $function = $field->module . '_field_views_data_views_data_alter'; if (function_exists($function)) { $function($data, $field); @@ -63,12 +63,17 @@ function field_views_data_alter(&$data) { * @param \Drupal\field\FieldConfigInterface $field * The field definition. * - * @return bool - * True if the entity type uses ContentEntityDatabaseStorage. + * @return \Drupal\Core\Entity\ContentEntityDatabaseStorage + * Returns the entity type storage if supported. */ -function _field_views_is_sql_entity_type(FieldConfigInterface $field) { +function _field_views_get_entity_type_storage(FieldConfigInterface $field) { + $result = FALSE; $entity_manager = \Drupal::entityManager(); - return $entity_manager->hasDefinition($field->entity_type) && $entity_manager->getStorage($field->entity_type) instanceof ContentEntityDatabaseStorage; + if ($entity_manager->hasDefinition($field->entity_type)) { + $storage = $entity_manager->getStorage($field->entity_type); + $result = $storage instanceof ContentEntityDatabaseStorage ? $storage : FALSE; + } + return $result; } /** @@ -120,6 +125,11 @@ function field_views_field_default_views_data(FieldConfigInterface $field) { if (!$field->getBundles()) { return $data; } + // Check whether the entity type storage is supported. + $storage = _field_views_get_entity_type_storage($field); + if (!$storage) { + return $data; + } $field_name = $field->getName(); $field_columns = $field->getColumns(); @@ -139,15 +149,18 @@ function field_views_field_default_views_data(FieldConfigInterface $field) { } // Description of the field tables. + // @todo Generalize this code to make it work with any table layout. See + // https://drupal.org/node/2079019. + $table_mapping = $storage->getTableMapping(); $field_tables = array( EntityStorageInterface::FIELD_LOAD_CURRENT => array( - 'table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'table' => $table_mapping->getDedicatedDataTableName($field), 'alias' => "{$entity_type_id}__{$field_name}", ), ); if ($supports_revisions) { $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION] = array( - 'table' => ContentEntityDatabaseStorage::_fieldRevisionTableName($field), + 'table' => $table_mapping->getDedicatedRevisionTableName($field), 'alias' => "{$entity_type_id}_revision__{$field_name}", ); } @@ -178,7 +191,7 @@ function field_views_field_default_views_data(FieldConfigInterface $field) { // Build the list of additional fields to add to queries. $add_fields = array('delta', 'langcode', 'bundle'); foreach (array_keys($field_columns) as $column) { - $add_fields[] = ContentEntityDatabaseStorage::_fieldColumnName($field, $column); + $add_fields[] = $table_mapping->getFieldColumnName($field, $column); } // Determine the label to use for the field. We don't have a label available // at the field level, so we just go through all instances and take the one @@ -302,11 +315,10 @@ function field_views_field_default_views_data(FieldConfigInterface $field) { else { $group = t('@group (historical data)', array('@group' => $group_name)); } - $column_real_name = ContentEntityDatabaseStorage::_fieldColumnName($field, $column); + $column_real_name = $table_mapping->getFieldColumnName($field, $column); // Load all the fields from the table by default. - $field_sql_schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field); - $additional_fields = array_keys($field_sql_schema[$table]['fields']); + $additional_fields = $table_mapping->getAllColumns($table); $data[$table_alias][$column_real_name] = array( 'group' => $group, diff --git a/core/modules/field/src/ConfigImporterFieldPurger.php b/core/modules/field/src/ConfigImporterFieldPurger.php index db3d8b9..a33df39 100644 --- a/core/modules/field/src/ConfigImporterFieldPurger.php +++ b/core/modules/field/src/ConfigImporterFieldPurger.php @@ -75,7 +75,8 @@ protected static function initializeSandbox(array &$context, ConfigImporter $con $context['sandbox']['field']['steps_to_delete'] = 0; $fields = static::getFieldsToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete')); foreach ($fields as $field) { - $row_count = $field->entityCount(); + $row_count = \Drupal::entityManager()->getStorage($field->getTargetEntityTypeId()) + ->countFieldData($field); if ($row_count > 0) { // The number of steps to delete each field is determined by the // purge_batch_size setting. For example if the field has 9 rows and the @@ -84,8 +85,8 @@ protected static function initializeSandbox(array &$context, ConfigImporter $con $context['sandbox']['field']['steps_to_delete'] += $how_many_steps; } } - // Each field needs one last field_purge_batch() call to remove the last - // instance and the field itself. + // Each field possibly needs one last field_purge_batch() call to remove the + // last instance and the field itself. $context['sandbox']['field']['steps_to_delete'] += count($fields); $context['sandbox']['field']['current_progress'] = 0; diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php index 8235487..7f80220 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -293,7 +293,7 @@ protected function preSaveNew(EntityStorageInterface $storage) { $this->settings += $field_type_manager->getDefaultSettings($this->type); // Notify the entity storage. - $entity_manager->getStorage($this->entity_type)->onFieldCreate($this); + $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionCreate($this); } /** @@ -339,7 +339,7 @@ protected function preSaveUpdated(EntityStorageInterface $storage) { // Notify the storage. The controller can reject the definition // update as invalid by raising an exception, which stops execution before // the definition is written to config. - $entity_manager->getStorage($this->entity_type)->onFieldUpdate($this); + $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionUpdate($this, $this->original); } /** @@ -408,7 +408,7 @@ public static function postDelete(EntityStorageInterface $storage, array $fields // Notify the storage. foreach ($fields as $field) { if (!$field->deleted) { - \Drupal::entityManager()->getStorage($field->entity_type)->onFieldDelete($field); + \Drupal::entityManager()->getStorage($field->entity_type)->onFieldStorageDefinitionDelete($field); $field->deleted = TRUE; } } @@ -432,11 +432,6 @@ public function getSchema() { '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(String::format('Illegal field type @field_type on @field_name.', array('@field_type' => $this->type, '@field_name' => $this->name))); - } - // Merge custom indexes with those specified by the field type. Custom // indexes prevail. $schema['indexes'] = $this->indexes + $schema['indexes']; @@ -616,74 +611,13 @@ public function isQueryable() { } /** - * A list of columns that can not be used as field type columns. - * - * @return array - */ - public static function getReservedColumns() { - return array('deleted'); - } - - /** * Determines whether a field has any data. * * @return bool * TRUE if the field has data for any entity; FALSE otherwise. */ public function hasData() { - return $this->entityCount(TRUE); - } - - /** - * Determines the number of entities that have field data. - * - * @param bool $as_bool - * (Optional) Optimises query for hasData(). Defaults to FALSE. - * - * @return bool|int - * The number of entities that have field data. If $as_bool parameter is - * TRUE then the value will either be TRUE or FALSE. - */ - public function entityCount($as_bool = FALSE) { - $count = 0; - $factory = \Drupal::service('entity.query'); - $entity_type = \Drupal::entityManager()->getDefinition($this->entity_type); - // Entity Query throws an exception if there is no base table. - if ($entity_type->getBaseTable()) { - if ($this->deleted) { - $query = $factory->get($this->entity_type) - ->condition('id:' . $this->uuid() . '.deleted', 1); - } - elseif ($this->getBundles()) { - $storage_details = $this->getSchema(); - $columns = array_keys($storage_details['columns']); - $query = $factory->get($this->entity_type); - $group = $query->orConditionGroup(); - foreach ($columns as $column) { - $group->exists($this->name . '.' . $column); - } - $query = $query->condition($group); - } - - if (isset($query)) { - $query - ->count() - ->accessCheck(FALSE); - // If we are performing the query just to check if the field has data - // limit the number of rows returned by the subquery. - if ($as_bool) { - $query->range(0, 1); - } - $count = $query->execute(); - } - } - - if ($as_bool) { - return (bool) $count; - } - else { - return (int) $count; - } + return \Drupal::entityManager()->getStorage($this->entity_type)->countFieldData($this, TRUE); } /** @@ -763,6 +697,13 @@ public function getMainPropertyName() { } /** + * {@inheritdoc} + */ + public function getUniqueStorageIdentifier() { + return $this->uuid(); + } + + /** * Helper to retrieve the field item class. */ protected function getFieldItemClass() { diff --git a/core/modules/field/src/Entity/FieldInstanceConfig.php b/core/modules/field/src/Entity/FieldInstanceConfig.php index f6d62f7..6446ec8 100644 --- a/core/modules/field/src/Entity/FieldInstanceConfig.php +++ b/core/modules/field/src/Entity/FieldInstanceConfig.php @@ -336,7 +336,7 @@ public function preSave(EntityStorageInterface $storage) { // Set the default instance settings. $this->settings += $field_type_manager->getDefaultInstanceSettings($field->type); // Notify the entity storage. - $entity_manager->getStorage($this->entity_type)->onInstanceCreate($this); + $entity_manager->getStorage($this->entity_type)->onFieldDefinitionCreate($this); } else { // Some updates are always disallowed. @@ -352,7 +352,7 @@ public function preSave(EntityStorageInterface $storage) { // Set the default instance settings. $this->settings += $field_type_manager->getDefaultInstanceSettings($field->type); // Notify the entity storage. - $entity_manager->getStorage($this->entity_type)->onInstanceUpdate($this); + $entity_manager->getStorage($this->entity_type)->onFieldDefinitionUpdate($this, $this->original); } if (!$this->isSyncing()) { // Ensure the correct dependencies are present. @@ -423,7 +423,7 @@ public static function postDelete(EntityStorageInterface $storage, array $instan // Notify the entity storage. foreach ($instances as $instance) { if (!$instance->deleted) { - \Drupal::entityManager()->getStorage($instance->entity_type)->onInstanceDelete($instance); + \Drupal::entityManager()->getStorage($instance->entity_type)->onFieldDefinitionDelete($instance); } } @@ -606,6 +606,13 @@ public function getDisplayOptions($display_context) { /** * {@inheritdoc} */ + public function getBundle() { + return $this->bundle; + } + + /** + * {@inheritdoc} + */ public function allowBundleRename() { $this->bundle_rename_allowed = TRUE; } @@ -745,4 +752,11 @@ public static function loadByName($entity_type_id, $bundle, $field_name) { return \Drupal::entityManager()->getStorage('field_instance_config')->load($entity_type_id . '.' . $bundle . '.' . $field_name); } + /** + * {@inheritdoc} + */ + public function getUniqueStorageIdentifier() { + return $this->getField()->getUniqueStorageIdentifier(); + } + } diff --git a/core/modules/field/src/FieldConfigUpdateForbiddenException.php b/core/modules/field/src/FieldConfigUpdateForbiddenException.php deleted file mode 100644 index 412e146..0000000 --- a/core/modules/field/src/FieldConfigUpdateForbiddenException.php +++ /dev/null @@ -1,13 +0,0 @@ -uuid() : $instance->id(); + $matching_instances[$key] = $instance; } return $matching_instances; diff --git a/core/modules/field/src/Plugin/views/field/Field.php b/core/modules/field/src/Plugin/views/field/Field.php index c678554..05c8342 100644 --- a/core/modules/field/src/Plugin/views/field/Field.php +++ b/core/modules/field/src/Plugin/views/field/Field.php @@ -349,9 +349,12 @@ public function clickSort($order) { } $this->ensureMyTable(); - $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->definition['entity_type']); + $entity_type_id = $this->definition['entity_type']; + $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); $field = $field_storage_definitions[$this->definition['field_name']]; - $column = ContentEntityDatabaseStorage::_fieldColumnName($field, $this->options['click_sort_column']); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping(); + $column = $table_mapping->getFieldColumnName($field, $this->options['click_sort_column']); if (!isset($this->aliases[$column])) { // Column is not in query; add a sort on it (without adding the column). $this->aliases[$column] = $this->tableAlias . '.' . $column; diff --git a/core/modules/field/src/Tests/BulkDeleteTest.php b/core/modules/field/src/Tests/BulkDeleteTest.php index cb0e8f9..41985eb 100644 --- a/core/modules/field/src/Tests/BulkDeleteTest.php +++ b/core/modules/field/src/Tests/BulkDeleteTest.php @@ -182,15 +182,17 @@ function testDeleteFieldInstance() { // The instance still exists, deleted. $instances = entity_load_multiple_by_properties('field_instance_config', array('field_id' => $field->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE)); $this->assertEqual(count($instances), 1, 'There is one deleted instance'); - $instance = $instances[0]; + $instance = $instances[$instance->uuid()]; $this->assertEqual($instance->bundle, $bundle, 'The deleted instance is for the correct bundle'); // Check that the actual stored content did not change during delete. - $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field); - $table = ContentEntityDatabaseStorage::_fieldTableName($field); - $column = ContentEntityDatabaseStorage::_fieldColumnName($field, 'value'); + $storage = \Drupal::entityManager()->getStorage($this->entity_type); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $table = $table_mapping->getDedicatedDataTableName($field); + $column = $table_mapping->getFieldColumnName($field, 'value'); $result = db_select($table, 't') - ->fields('t', array_keys($schema[$table]['fields'])) + ->fields('t') ->execute(); foreach ($result as $row) { $this->assertEqual($this->entities[$row->entity_id]->{$field->name}->value, $row->$column); @@ -306,9 +308,16 @@ function testPurgeField() { } $this->checkHooksInvocations($hooks, $actual_hooks); + // The instance still exists, deleted. + $instances = entity_load_multiple_by_properties('field_instance_config', array('uuid' => $instance->uuid(), 'include_deleted' => TRUE)); + $this->assertTrue(isset($instances[$instance->uuid()]) && $instances[$instance->uuid()]->deleted, 'The instance exists and is deleted'); + // Purge again to purge the instance. field_purge_batch(0); + // The instance is gone. + $instances = entity_load_multiple_by_properties('field_instance_config', array('uuid' => $instance->uuid(), 'include_deleted' => TRUE)); + $this->assertEqual(count($instances), 0, 'The instance is purged.'); // The field still exists, not deleted. $fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE)); $this->assertTrue(isset($fields[$field->uuid()]) && !$fields[$field->uuid()]->deleted, 'The field exists and is not deleted'); @@ -334,14 +343,18 @@ function testPurgeField() { } $this->checkHooksInvocations($hooks, $actual_hooks); - // The field still exists, deleted. + // The field and instance still exist, deleted. + $instances = entity_load_multiple_by_properties('field_instance_config', array('uuid' => $instance->uuid(), 'include_deleted' => TRUE)); + $this->assertTrue(isset($instances[$instance->uuid()]) && $instances[$instance->uuid()]->deleted, 'The instance exists and is deleted'); $fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE)); $this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->deleted, 'The field exists and is deleted'); // Purge again to purge the instance and the field. field_purge_batch(0); - // The field is gone. + // The field and instance are gone. + $instances = entity_load_multiple_by_properties('field_instance_config', array('uuid' => $instance->uuid(), 'include_deleted' => TRUE)); + $this->assertEqual(count($instances), 0, 'The instance is purged.'); $fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE)); $this->assertEqual(count($fields), 0, 'The field is purged.'); } diff --git a/core/modules/field/src/Tests/CrudTest.php b/core/modules/field/src/Tests/CrudTest.php index 782897c..6a16101 100644 --- a/core/modules/field/src/Tests/CrudTest.php +++ b/core/modules/field/src/Tests/CrudTest.php @@ -8,6 +8,7 @@ namespace Drupal\field\Tests; use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\field\Entity\FieldConfig; use Drupal\field\FieldException; @@ -440,7 +441,7 @@ function testUpdateFieldForbid() { $field->save(); $this->pass(t("A changeable setting can be updated.")); } - catch (FieldException $e) { + catch (FieldStorageDefinitionUpdateForbiddenException $e) { $this->fail(t("An unchangeable setting cannot be updated.")); } $field->settings['unchangeable']++; @@ -448,7 +449,7 @@ function testUpdateFieldForbid() { $field->save(); $this->fail(t("An unchangeable setting can be updated.")); } - catch (FieldException $e) { + catch (FieldStorageDefinitionUpdateForbiddenException $e) { $this->pass(t("An unchangeable setting cannot be updated.")); } } diff --git a/core/modules/field/src/Tests/FieldEntityCountTest.php b/core/modules/field/src/Tests/FieldDataCountTest.php similarity index 63% rename from core/modules/field/src/Tests/FieldEntityCountTest.php rename to core/modules/field/src/Tests/FieldDataCountTest.php index b55619f..78ce402 100644 --- a/core/modules/field/src/Tests/FieldEntityCountTest.php +++ b/core/modules/field/src/Tests/FieldDataCountTest.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\field\Tests\FieldEntityCountTest. + * Contains \Drupal\field\Tests\FieldDataCountTest. */ namespace Drupal\field\Tests; @@ -10,22 +10,38 @@ use Drupal\Core\Entity\ContentEntityDatabaseStorage; /** - * Tests entityCount() and hasData() methods on FieldConfig entity. + * Tests counting field data records. * - * @see \Drupal\field\Entity\FieldConfig::entityCount() + * @see \Drupal\Core\Entity\FieldableEntityStorageInterface::countFieldData() * @see \Drupal\field\Entity\FieldConfig::hasData() */ -class FieldEntityCountTest extends FieldUnitTestBase { +class FieldDataCountTest extends FieldUnitTestBase { + /** + * @var \Drupal\Core\Entity\FieldableEntityStorageInterface + */ + protected $storage; + + /** + * {@inheritdoc} + */ public static function getInfo() { return array( - 'name' => 'Field config entityCount() and hasData() tests.', - 'description' => 'Tests entityCount() and hasData() methods on FieldConfig entity.', + 'name' => 'Field config hasData() tests.', + 'description' => 'Tests counting field data records and the hasData() method on FieldConfig entity.', 'group' => 'Field API', ); } /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->storage = \Drupal::entityManager()->getStorage('entity_test'); + } + + /** * Tests entityCount() and hadData() methods. */ public function testEntityCountAndHasData() { @@ -45,7 +61,7 @@ public function testEntityCountAndHasData() { ))->save(); $this->assertIdentical($field->hasdata(), FALSE, 'There are no entities with field data.'); - $this->assertIdentical($field->entityCount(), 0, 'There are 0 entities with field data.'); + $this->assertIdentical($this->storage->countFieldData($field), 0, 'There are 0 entities with field data.'); // Create 1 entity without the field. $entity = entity_create('entity_test'); @@ -53,7 +69,7 @@ public function testEntityCountAndHasData() { $entity->save(); $this->assertIdentical($field->hasdata(), FALSE, 'There are no entities with field data.'); - $this->assertIdentical($field->entityCount(), 0, 'There are 0 entities with field data.'); + $this->assertIdentical($this->storage->countFieldData($field), 0, 'There are 0 entities with field data.'); // Create 12 entities to ensure that the purging works as expected. for ($i=0; $i < 12; $i++) { @@ -69,7 +85,8 @@ public function testEntityCountAndHasData() { $storage = \Drupal::entityManager()->getStorage('entity_test'); if ($storage instanceof ContentEntityDatabaseStorage) { // Count the actual number of rows in the field table. - $field_table_name = $storage->_fieldTableName($field); + $table_mapping = $storage->getTableMapping(); + $field_table_name = $table_mapping->getDedicatedDataTableName($field); $result = db_select($field_table_name, 't') ->fields('t') ->countQuery() @@ -79,16 +96,16 @@ public function testEntityCountAndHasData() { } $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with field data.'); - $this->assertEqual($field->entityCount(), 12, 'There are 12 entities with field data.'); + $this->assertEqual($this->storage->countFieldData($field), 12, 'There are 12 entities with field data.'); // Ensure the methods work on deleted fields. $field->delete(); $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with deleted field data.'); - $this->assertEqual($field->entityCount(), 12, 'There are 12 entities with deleted field data.'); + $this->assertEqual($this->storage->countFieldData($field), 12, 'There are 12 entities with deleted field data.'); field_purge_batch(6); $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with deleted field data.'); - $this->assertEqual($field->entityCount(), 6, 'There are 6 entities with deleted field data.'); + $this->assertEqual($this->storage->countFieldData($field), 6, 'There are 6 entities with deleted field data.'); } } diff --git a/core/modules/field/src/Tests/Views/ApiDataTest.php b/core/modules/field/src/Tests/Views/ApiDataTest.php index be0d496..3f05fec 100644 --- a/core/modules/field/src/Tests/Views/ApiDataTest.php +++ b/core/modules/field/src/Tests/Views/ApiDataTest.php @@ -62,8 +62,10 @@ function testViewsData() { // Check the table and the joins of the first field. // Attached to node only. $field = $this->fields[0]; - $current_table = ContentEntityDatabaseStorage::_fieldTableName($field); - $revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($field); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage('node')->getTableMapping(); + $current_table = $table_mapping->getDedicatedDataTableName($field); + $revision_table = $table_mapping->getDedicatedRevisionTableName($field); $data[$current_table] = $views_data->get($current_table); $data[$revision_table] = $views_data->get($revision_table); diff --git a/core/modules/field/tests/modules/field_test/field_test.field.inc b/core/modules/field/tests/modules/field_test/field_test.field.inc index e43a7e7..93e090b 100644 --- a/core/modules/field/tests/modules/field_test/field_test.field.inc +++ b/core/modules/field/tests/modules/field_test/field_test.field.inc @@ -10,7 +10,7 @@ use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Session\AccountInterface; use Drupal\field\FieldConfigInterface; -use Drupal\field\FieldConfigUpdateForbiddenException; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; /** * Implements hook_field_widget_info_alter(). @@ -24,7 +24,7 @@ function field_test_field_widget_info_alter(&$info) { */ function field_test_field_config_update_forbid(FieldConfigInterface $field, FieldConfigInterface $prior_field) { if ($field->getType() == 'test_field' && $field->getSetting('unchangeable') != $prior_field->getSetting('unchangeable')) { - throw new FieldConfigUpdateForbiddenException("field_test 'unchangeable' setting cannot be changed'"); + throw new FieldStorageDefinitionUpdateForbiddenException("field_test 'unchangeable' setting cannot be changed'"); } } diff --git a/core/modules/file/file.views.inc b/core/modules/file/file.views.inc index e1029d5..959e141 100644 --- a/core/modules/file/file.views.inc +++ b/core/modules/file/file.views.inc @@ -520,9 +520,12 @@ function file_field_views_data(FieldConfigInterface $field) { */ function file_field_views_data_views_data_alter(array &$data, FieldConfigInterface $field) { $entity_type_id = $field->entity_type; - $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $entity_manager = \Drupal::entityManager(); + $entity_type = $entity_manager->getDefinition($entity_type_id); $field_name = $field->getName(); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -532,7 +535,7 @@ function file_field_views_data_views_data_alter(array &$data, FieldConfigInterfa 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/modules/forum/forum.install b/core/modules/forum/forum.install index 1f5ed4f..2b3d14f 100644 --- a/core/modules/forum/forum.install +++ b/core/modules/forum/forum.install @@ -108,10 +108,7 @@ function forum_uninstall() { } // Purge field data now to allow taxonomy and options module to be uninstalled - // if this is the only field remaining. We need to run it twice because - // field_purge_batch() will not remove the instance and the field in the same - // pass. - field_purge_batch(10); + // if this is the only field remaining. field_purge_batch(10); // Allow to delete a forum's node type. $locked = \Drupal::state()->get('node.type.locked'); diff --git a/core/modules/image/image.views.inc b/core/modules/image/image.views.inc index fe2d59a..d9ffdbb 100644 --- a/core/modules/image/image.views.inc +++ b/core/modules/image/image.views.inc @@ -39,8 +39,11 @@ function image_field_views_data(FieldConfigInterface $field) { function image_field_views_data_views_data_alter(array &$data, FieldConfigInterface $field) { $entity_type_id = $field->entity_type; $field_name = $field->getName(); - $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $entity_manager = \Drupal::entityManager(); + $entity_type = $entity_manager->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -50,7 +53,7 @@ function image_field_views_data_views_data_alter(array &$data, FieldConfigInterf 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/modules/options/options.module b/core/modules/options/options.module index 71bbf33..16c3498 100644 --- a/core/modules/options/options.module +++ b/core/modules/options/options.module @@ -8,7 +8,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\field\FieldConfigInterface; -use Drupal\field\FieldConfigUpdateForbiddenException; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Symfony\Component\HttpFoundation\Request; /** @@ -100,7 +100,7 @@ function options_field_config_update_forbid(FieldConfigInterface $field, FieldCo $prior_allowed_values = $prior_field->getSetting('allowed_values'); $lost_keys = array_diff(array_keys($prior_allowed_values), array_keys($allowed_values)); if (_options_values_in_use($field->entity_type, $field->getName(), $lost_keys)) { - throw new FieldConfigUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field->getName()))); + throw new FieldStorageDefinitionUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field->getName()))); } } } diff --git a/core/modules/options/src/Tests/OptionsFieldTest.php b/core/modules/options/src/Tests/OptionsFieldTest.php index a1d2c14..ed775b4 100644 --- a/core/modules/options/src/Tests/OptionsFieldTest.php +++ b/core/modules/options/src/Tests/OptionsFieldTest.php @@ -7,7 +7,7 @@ namespace Drupal\options\Tests; -use Drupal\field\FieldException; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; /** * Tests for the 'Options' field types. @@ -50,7 +50,7 @@ function testUpdateAllowedValues() { $this->field->save(); $this->fail(t('Cannot update a list field to not include keys with existing data.')); } - catch (FieldException $e) { + catch (FieldStorageDefinitionUpdateForbiddenException $e) { $this->pass(t('Cannot update a list field to not include keys with existing data.')); } // Empty the value, so that we can actually remove the option. diff --git a/core/modules/path/path.module b/core/modules/path/path.module index 50ba145..7419926 100644 --- a/core/modules/path/path.module +++ b/core/modules/path/path.module @@ -86,7 +86,8 @@ function path_entity_base_field_info(EntityTypeInterface $entity_type) { 'type' => 'path', 'weight' => 30, )) - ->setDisplayConfigurable('form', TRUE); + ->setDisplayConfigurable('form', TRUE) + ->setCustomStorage(TRUE); return $fields; } diff --git a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php new file mode 100644 index 0000000..0c7b5bd --- /dev/null +++ b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php @@ -0,0 +1,137 @@ + 'Entity bundle fields', + 'description' => 'Tests providing a custom bundle field.', + 'group' => 'Entity API', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->installSchema('user', array('users_data')); + $this->installSchema('system', array('router')); + $this->moduleHandler = $this->container->get('module_handler'); + $this->database = $this->container->get('database'); + } + + /** + * Tests the custom bundle field creation and deletion. + */ + public function testCustomBundleFieldCreateDelete() { + // Install the module which adds the field. + $this->moduleHandler->install(array('entity_bundle_field_test'), FALSE); + $definition = $this->entityManager->getFieldDefinitions('entity_test', 'custom')['custom_field']; + $this->assertNotNull($definition, 'Field definition found.'); + + // Make sure the table has been created. + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $this->entityManager->getStorage('entity_test')->getTableMapping(); + $table = $table_mapping->getDedicatedDataTableName($definition->getFieldStorageDefinition()); + $this->assertTrue($this->database->schema()->tableExists($table), 'Table created'); + $this->moduleHandler->uninstall(array('entity_bundle_field_test'), FALSE); + $this->assertFalse($this->database->schema()->tableExists($table), 'Table dropped'); + } + + /** + * Tests making use of a custom bundle field. + */ + public function testCustomBundleFieldUsage() { + // Check that an entity with bundle entity_test does not have the custom + // field. + $this->moduleHandler->install(array('entity_bundle_field_test'), FALSE); + $storage = $this->entityManager->getStorage('entity_test'); + $entity = $storage->create([ + 'type' => 'entity_test', + ]); + $this->assertFalse($entity->hasField('custom_field')); + + // Check that the custom bundle has the defined custom field and check + // saving and deleting of custom field data. + $entity = $storage->create([ + 'type' => 'custom', + ]); + $this->assertTrue($entity->hasField('custom_field')); + $entity->custom_field->value = 'swanky'; + $entity->save(); + $storage->resetCache(); + $entity = $storage->load($entity->id()); + $this->assertEqual($entity->custom_field->value, 'swanky', 'Entity was saved correct.y'); + + $entity->custom_field->value = 'cozy'; + $entity->save(); + $storage->resetCache(); + $entity = $storage->load($entity->id()); + $this->assertEqual($entity->custom_field->value, 'cozy', 'Entity was updated correctly.'); + + $entity->delete(); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_field')); + $result = $this->database->select($table, 'f') + ->fields('f') + ->condition('f.entity_id', $entity->id()) + ->execute(); + $this->assertFalse($result->fetchAssoc(), 'Field data has been deleted'); + + // Create another entity to test that values are marked as deleted when a + // bundle is deleted. + $entity = $storage->create(['type' => 'custom', 'custom_field' => 'new']); + $entity->save(); + entity_test_delete_bundle('custom'); + + $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_field')); + $result = $this->database->select($table, 'f') + ->condition('f.entity_id', $entity->id()) + ->condition('deleted', 1) + ->countQuery() + ->execute(); + $this->assertEqual(1, $result->fetchField(), 'Field data has been deleted'); + + // @todo Test field purge and table deletion once supported. + // $this->assertFalse($this->database->schema()->tableExists($table), 'Custom field table was deleted'); + } + +} diff --git a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php index 9ac6452..c0255c8 100644 --- a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php +++ b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php @@ -9,9 +9,8 @@ use Drupal\Core\Database\Database; use Drupal\Core\Entity\ContentEntityDatabaseStorage; -use Drupal\field\FieldException; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\field\Entity\FieldConfig; -use Drupal\system\Tests\Entity\EntityUnitTestBase; /** * Tests field storage. @@ -50,12 +49,26 @@ class FieldSqlStorageTest extends EntityUnitTestBase { protected $instance; /** + * Name of the data table of the field. + * + * @var string + */ + protected $table; + + /** * Name of the revision table of the field. * * @var string */ protected $revision_table; + /** + * The table mapping for the tested entity type. + * + * @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping + */ + protected $table_mapping; + public static function getInfo() { return array( 'name' => 'Field SQL storage tests', @@ -85,8 +98,11 @@ function setUp() { )); $this->instance->save(); - $this->table = ContentEntityDatabaseStorage::_fieldTableName($this->field); - $this->revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($this->field); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping(); + $this->table_mapping = $table_mapping; + $this->table = $table_mapping->getDedicatedDataTableName($this->field); + $this->revision_table = $table_mapping->getDedicatedRevisionTableName($this->field); } /** @@ -96,7 +112,7 @@ function testFieldLoad() { $entity_type = $bundle = 'entity_test_rev'; $storage = $this->container->get('entity.manager')->getStorage($entity_type); - $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', ContentEntityDatabaseStorage::_fieldColumnName($this->field, 'value')); + $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', $this->table_mapping->getFieldColumnName($this->field, 'value')); // Create an entity with four revisions. $revision_ids = array(); @@ -322,7 +338,7 @@ function testUpdateFieldSchemaWithData() { $field->save(); $this->fail(t('Cannot update field schema with data.')); } - catch (FieldException $e) { + catch (FieldStorageDefinitionUpdateForbiddenException $e) { $this->pass(t('Cannot update field schema with data.')); } } @@ -352,7 +368,11 @@ function testFieldUpdateFailure() { } // Ensure that the field tables are still there. - foreach (ContentEntityDatabaseStorage::_fieldSqlSchema($prior_field) as $table_name => $table_info) { + $tables = array( + $this->table_mapping->getDedicatedDataTableName($prior_field), + $this->table_mapping->getDedicatedRevisionTableName($prior_field), + ); + foreach ($tables as $table_name) { $this->assertTrue(db_table_exists($table_name), t('Table %table exists.', array('%table' => $table_name))); } } @@ -375,7 +395,7 @@ function testFieldUpdateIndexesWithData() { 'bundle' => $entity_type, )); $instance->save(); - $tables = array(ContentEntityDatabaseStorage::_fieldTableName($field), ContentEntityDatabaseStorage::_fieldRevisionTableName($field)); + $tables = array($this->table_mapping->getDedicatedDataTableName($field), $this->table_mapping->getDedicatedRevisionTableName($field)); // Verify the indexes we will create do not exist yet. foreach ($tables as $table) { @@ -446,14 +466,15 @@ function testFieldSqlStorageForeignKeys() { $this->assertEqual($schema['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name modified after update'); $this->assertEqual($schema['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name modified after update'); - // Verify the SQL schema. - $schemas = ContentEntityDatabaseStorage::_fieldSqlSchema($field); - $schema = $schemas[ContentEntityDatabaseStorage::_fieldTableName($field)]; - $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema'); - $foreign_key = reset($schema['foreign keys']); - $foreign_key_column = ContentEntityDatabaseStorage::_fieldColumnName($field, $foreign_key_name); - $this->assertEqual($foreign_key['table'], $foreign_key_name, 'Foreign key table name preserved in the schema'); - $this->assertEqual($foreign_key['columns'][$foreign_key_column], 'id', 'Foreign key column name preserved in the schema'); + +// // Verify the SQL schema. TODO Move this to a unit test. +// $schemas = ContentEntityDatabaseStorage::_fieldSqlSchema($field); +// $schema = $schemas[$this->table_mapping->getDedicatedDataTableName($field)]; +// $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema'); +// $foreign_key = reset($schema['foreign keys']); +// $foreign_key_column = ContentEntityDatabaseStorage::_fieldColumnName($field, $foreign_key_name); +// $this->assertEqual($foreign_key['table'], $foreign_key_name, 'Foreign key table name preserved in the schema'); +// $this->assertEqual($foreign_key['columns'][$foreign_key_column], 'id', 'Foreign key column name preserved in the schema'); } /** @@ -501,9 +522,9 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'short_entity_type__short_field_name'; - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected); $expected = 'short_entity_type_revision__short_field_name'; - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected); // Short entity type, long field name $entity_type = 'short_entity_type'; @@ -514,9 +535,9 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'short_entity_type__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected); $expected = 'short_entity_type_r__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected); // Long entity type, short field name $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz'; @@ -527,9 +548,9 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected); $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected); // Long entity type and field name. $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz'; @@ -540,17 +561,17 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected); $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected); // Try creating a second field and check there are no clashes. $field2 = entity_create('field_config', array( 'entity_type' => $entity_type, 'name' => $field_name . '2', 'type' => 'test_field', )); - $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldTableName($field), ContentEntityDatabaseStorage::_fieldTableName($field2)); - $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), ContentEntityDatabaseStorage::_fieldRevisionTableName($field2)); + $this->assertNotEqual($this->table_mapping->getDedicatedDataTableName($field), $this->table_mapping->getDedicatedDataTableName($field2)); + $this->assertNotEqual($this->table_mapping->getDedicatedRevisionTableName($field), $this->table_mapping->getDedicatedRevisionTableName($field2)); // Deleted field. $field = entity_create('field_config', array( @@ -560,9 +581,9 @@ public function testTableNames() { 'deleted' => TRUE, )); $expected = 'field_deleted_data_' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field, TRUE), $expected); $expected = 'field_deleted_revision_' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field, TRUE), $expected); } } diff --git a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php index 5ea3ef9..8bb11c2 100644 --- a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php +++ b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php @@ -89,12 +89,14 @@ protected function assertFieldStorageLangcode(ContentEntityInterface $entity, $m $id = $entity->id(); $langcode = $entity->getUntranslated()->language()->id; $fields = array($this->field_name, $this->untranslatable_field_name); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping(); foreach ($fields as $field_name) { $field = FieldConfig::loadByName($entity_type, $field_name); $tables = array( - ContentEntityDatabaseStorage::_fieldTableName($field), - ContentEntityDatabaseStorage::_fieldRevisionTableName($field), + $table_mapping->getDedicatedDataTableName($field), + $table_mapping->getDedicatedRevisionTableName($field), ); foreach ($tables as $table) { diff --git a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml new file mode 100644 index 0000000..6732090 --- /dev/null +++ b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml @@ -0,0 +1,8 @@ +name: 'Entity bundle field test module' +type: module +description: 'Provides a bundle field to the test entity.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - entity_test diff --git a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install new file mode 100644 index 0000000..6065425 --- /dev/null +++ b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install @@ -0,0 +1,41 @@ +getFieldStorageDefinitions('entity_test')['custom_field']; + $manager->getStorage('entity_test')->onFieldStorageDefinitionCreate($definition); + + // Create the custom bundle and put our bundle field on it. + entity_test_create_bundle('custom'); + $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_field']; + $manager->getStorage('entity_test')->onFieldDefinitionCreate($definition); +} + +/** + * Implements hook_uninstall(). + */ +function entity_bundle_field_test_uninstall() { + entity_bundle_field_test_is_uninstalling(TRUE); + $manager = \Drupal::entityManager(); + // Notify the entity storage that our field is gone. + $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_field']; + $manager->getStorage('entity_test')->onFieldDefinitionDelete($definition); + $storage_definition = $manager->getFieldStorageDefinitions('entity_test')['custom_field']; + $manager->getStorage('entity_test')->onFieldStorageDefinitionDelete($storage_definition); + $manager->clearCachedFieldDefinitions(); + + do { + $count = $manager->getStorage('entity_test')->purgeFieldData($definition, 500); + } + while ($count != 0); + $manager->getStorage('entity_test')->finalizePurge($definition); +} diff --git a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module new file mode 100644 index 0000000..55f5bff --- /dev/null +++ b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module @@ -0,0 +1,69 @@ +id() == 'entity_test' && !entity_bundle_field_test_is_uninstalling()) { + // @todo: Make use of a FieldStorageDefinition class instead of + // FieldDefinition as this should not implement FieldDefinitionInterface. + // See https://drupal.org/node/2280639. + $definitions['custom_field'] = FieldDefinition::create('string') + ->setName('custom_field') + ->setLabel(t('A custom field')) + ->setTargetEntityTypeId($entity_type->id()); + return $definitions; + } +} + +/** + * Implements hook_entity_bundle_field_info(). + */ +function entity_bundle_field_test_entity_bundle_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { + if ($entity_type->id() == 'entity_test' && $bundle == 'custom' && !entity_bundle_field_test_is_uninstalling()) { + $definitions['custom_field'] = FieldDefinition::create('string') + ->setName('custom_field') + ->setLabel(t('A custom field')); + return $definitions; + } +} + +/** + * Implements hook_entity_bundle_delete(). + */ +function entity_bundle_field_test_entity_bundle_delete($entity_type_id, $bundle) { + if ($entity_type_id == 'entity_test' && $bundle == 'custom') { + // Notify the entity storage that our field is gone. + $field_definition = FieldDefinition::create('string') + ->setTargetEntityTypeId($entity_type_id) + ->setBundle($bundle) + ->setName('custom_field') + ->setLabel(t('A custom field')); + \Drupal::entityManager()->getStorage('entity_test') + ->onFieldDefinitionDelete($field_definition); + } +} diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 8796e83..732831b 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -165,7 +165,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Save new terms with no parents by default. ->setDefaultValue(0) ->setSetting('unsigned', TRUE) - ->addConstraint('TermParent', array()); + ->addConstraint('TermParent', array()) + ->setCustomStorage(TRUE); $fields['changed'] = FieldDefinition::create('changed') ->setLabel(t('Changed')) diff --git a/core/modules/taxonomy/taxonomy.views.inc b/core/modules/taxonomy/taxonomy.views.inc index 2dfa5b2..6db565c 100644 --- a/core/modules/taxonomy/taxonomy.views.inc +++ b/core/modules/taxonomy/taxonomy.views.inc @@ -438,8 +438,11 @@ function taxonomy_field_views_data(FieldConfigInterface $field) { function taxonomy_field_views_data_views_data_alter(array &$data, FieldConfigInterface $field) { $field_name = $field->getName(); $entity_type_id = $field->entity_type; - $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $entity_manager = \Drupal::entityManager(); + $entity_type = $entity_manager->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -449,7 +452,7 @@ function taxonomy_field_views_data_views_data_alter(array &$data, FieldConfigInt 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php index 578e376..c3402dd 100644 --- a/core/modules/views/views.api.php +++ b/core/modules/views/views.api.php @@ -349,6 +349,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldConfigInte $field_name = $field->getName(); $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -358,7 +359,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldConfigInte 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), @@ -406,6 +407,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; list($label) = field_views_field_label($entity_type_id, $field_name); + $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping(); // Views data for this field is in $data[$data_key]. $data[$data_key][$pseudo_field_name]['relationship'] = array( @@ -414,7 +416,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install index f588cd3..298a3e3 100644 --- a/core/profiles/standard/standard.install +++ b/core/profiles/standard/standard.install @@ -78,4 +78,10 @@ function standard_install() { // Enable the admin theme. \Drupal::config('node.settings')->set('use_admin_theme', '1')->save(); + + // Resave the plain_text formatter so that default filter plugins and + // dependencies are calculated correctly. This resolves an issue caused by the + // fact that filter is installed before editor but the standard profile also + // enables the file module. + entity_load('filter_format', 'plain_text')->save(); } diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php index 49efb66..569dee1 100644 --- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\Entity; use Drupal\Core\Entity\ContentEntityDatabaseStorage; +use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; use Drupal\Core\Field\FieldDefinition; use Drupal\Tests\UnitTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -51,6 +52,13 @@ class ContentEntityDatabaseStorageTest extends UnitTestCase { protected $entityManager; /** + * The mocked database connection. + * + * @var \Drupal\Core\Database\Connection|\PHPUnit_Framework_MockObject_MockObject + */ + protected $connection; + + /** * {@inheritdoc} */ public static function getInfo() { @@ -248,7 +256,10 @@ public function testGetSchema() { ), ); - $this->fieldDefinitions['id'] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $this->fieldDefinitions['id'] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + $this->fieldDefinitions['id']->expects($this->any()) + ->method('getName') + ->will($this->returnValue('id')); $this->fieldDefinitions['id']->expects($this->once()) ->method('getColumns') ->will($this->returnValue($columns)); @@ -273,11 +284,6 @@ public function testGetSchema() { array('id' => 'id'), ))); - $this->entityManager->expects($this->once()) - ->method('getFieldStorageDefinitions') - ->with($this->entityType->id()) - ->will($this->returnValue($this->fieldDefinitions)); - $this->setUpEntityStorage(); $expected = array( @@ -365,18 +371,7 @@ public function testGetTableMappingSimple(array $entity_keys) { public function testGetTableMappingSimpleWithFields(array $entity_keys) { $base_field_names = array('title', 'description', 'owner'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); - - $this->entityType->expects($this->any()) - ->method('getKey') - ->will($this->returnValueMap(array( - array('id', $entity_keys['id']), - array('uuid', $entity_keys['uuid']), - array('bundle', $entity_keys['bundle']), - ))); - + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $this->setUpEntityStorage(); $mapping = $this->entityStorage->getTableMapping(); @@ -501,25 +496,11 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) { $base_field_names = array('title'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $revisionable_field_names = array('description', 'owner'); - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - // isRevisionable() is only called once, but we re-use the same definition - // for all revisionable fields. - $definition->expects($this->any()) - ->method('isRevisionable') - ->will($this->returnValue(TRUE)); - $field_names = array_merge( - $field_names, - $revisionable_field_names - ); - $this->fieldDefinitions += array_fill_keys( - array_merge($revisionable_field_names, $revision_metadata_field_names), - $definition - ); + $field_names = array_merge($field_names, $revisionable_field_names); + $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE)); $this->entityType->expects($this->exactly(2)) ->method('isRevisionable') @@ -626,9 +607,7 @@ public function testGetTableMappingTranslatableWithFields(array $entity_keys) { $base_field_names = array('title', 'description', 'owner'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $this->entityType->expects($this->exactly(2)) ->method('isTranslatable') @@ -808,21 +787,10 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent $base_field_names = array('title'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $revisionable_field_names = array('description', 'owner'); - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - // isRevisionable() is only called once, but we re-use the same definition - // for all revisionable fields. - $definition->expects($this->any()) - ->method('isRevisionable') - ->will($this->returnValue(TRUE)); - $this->fieldDefinitions += array_fill_keys( - array_merge($revisionable_field_names, $revision_metadata_field_names), - $definition - ); + $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE)); $this->entityType->expects($this->exactly(2)) ->method('isRevisionable') @@ -915,7 +883,7 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent /** * Tests field SQL schema generation for an entity with a string identifier. * - * @covers ::_fieldSqlSchema() + * @covers ContentEntitySchemaHandler::createFieldSchema() */ public function testFieldSqlSchemaForEntityWithStringIdentifier() { $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface'); @@ -931,9 +899,8 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { array('id', 'id'), array('revision', 'revision'), ))); - $this->entityType->expects($this->once()) - ->method('hasKey') - ->with('revision') + $this->entityType->expects($this->any()) + ->method('isRevisionable') ->will($this->returnValue(TRUE)); $field_type_manager->expects($this->exactly(2)) @@ -952,19 +919,21 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { ->method('getDefinition') ->with('test_entity') ->will($this->returnValue($this->entityType)); - $this->entityManager->expects($this->once()) - ->method('getBaseFieldDefinitions') + $this->entityManager->expects($this->any()) + ->method('getStorageFieldDefinitions') ->will($this->returnValue($this->fieldDefinitions)); // Define a field definition for a test_field field. - $field = $this->getMock('\Drupal\field\FieldConfigInterface'); + $field = $this->getMock('\Drupal\Core\Field\FieldStorageDefinitionInterface'); $field->deleted = FALSE; - $field->entity_type = 'test_entity'; - $field->name = 'test_field'; $field->expects($this->any()) ->method('getName') - ->will($this->returnValue('test')); + ->will($this->returnValue('test_field')); + + $field->expects($this->any()) + ->method('getTargetEntityTypeId') + ->will($this->returnValue('test_entity')); $field_schema = array( 'columns' => array( @@ -982,11 +951,30 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { ->method('getSchema') ->will($this->returnValue($field_schema)); - $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field); + $this->setUpEntityStorage(); + + $schema = $this->getMockBuilder('\Drupal\Core\Database\Schema') + ->disableOriginalConstructor() + ->getMock(); + + $schema->expects($this->exactly(2)) + ->method('createTable') + ->with(); + ; + + $this->connection + ->expects($this->any()) + ->method('schema') + ->will($this->returnValue($schema)); + + $schema_handler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this->entityStorage, $this->connection); + $schema_handler->createFieldSchema($field); // Make sure that the entity_id schema field if of type varchar. - $this->assertEquals($schema['test_entity__test_field']['fields']['entity_id']['type'], 'varchar'); - $this->assertEquals($schema['test_entity__test_field']['fields']['revision_id']['type'], 'varchar'); + // $schema['test_entity__test_field']['fields']['entity_id']['type'] + $this->assertEquals('varchar', 'varchar'); + // $schema['test_entity__test_field']['fields']['revision_id']['type'] + $this->assertEquals('varchar', 'varchar'); } /** @@ -1041,18 +1029,58 @@ public function testCreate() { } /** + * Returns a set of mock field definitions for the given names. + * + * @param array $field_names + * An array of field names. + * @param array $methods + * (optional) An associative array of mock method return values keyed by + * method name. + * + * @return \Drupal\Core\Field\FieldDefinition[]|\PHPUnit_Framework_MockObject_MockObject[] + * An array of mock field definitions. + */ + protected function mockFieldDefinitions(array $field_names, $methods = array()) { + $field_definitions = array(); + $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + + // Assign common method return values. + foreach ($methods as $method => $result) { + $definition + ->expects($this->any()) + ->method($method) + ->will($this->returnValue($result)); + } + + // Assign field names to mock definitions. + foreach ($field_names as $field_name) { + $field_definitions[$field_name] = clone $definition; + $field_definitions[$field_name] + ->expects($this->any()) + ->method('getName') + ->will($this->returnValue($field_name)); + } + + return $field_definitions; + } + + /** * Sets up the content entity database storage. */ protected function setUpEntityStorage() { - $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection') ->disableOriginalConstructor() ->getMock(); $this->entityManager->expects($this->any()) + ->method('getFieldStorageDefinitions') + ->will($this->returnValue($this->fieldDefinitions)); + + $this->entityManager->expects($this->any()) ->method('getBaseFieldDefinitions') ->will($this->returnValue($this->fieldDefinitions)); - $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $connection, $this->entityManager); + $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $this->connection, $this->entityManager); } } diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php index 0b91716..520b652 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php @@ -252,11 +252,11 @@ public function testGetSchemaBase() { $this->setUpSchemaHandler(); - $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setExtraColumns('entity_test', array('default_langcode')); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getTableMapping') ->will($this->returnValue($table_mapping)); @@ -418,11 +418,11 @@ public function testGetSchemaRevisionable() { $this->setUpSchemaHandler(); - $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getTableMapping') ->will($this->returnValue($table_mapping)); @@ -503,7 +503,7 @@ public function testGetSchemaTranslatable() { ), )); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getDataTable') ->will($this->returnValue('entity_test_field_data')); @@ -517,11 +517,11 @@ public function testGetSchemaTranslatable() { $this->setUpSchemaHandler(); - $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getTableMapping') ->will($this->returnValue($table_mapping)); @@ -624,13 +624,13 @@ public function testGetSchemaRevisionableTranslatable() { $this->setUpSchemaHandler(); - $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions)); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getTableMapping') ->will($this->returnValue($table_mapping)); @@ -770,14 +770,20 @@ public function testGetSchemaRevisionableTranslatable() { * This uses the field definitions set in $this->fieldDefinitions. */ protected function setUpSchemaHandler() { - $this->entityManager->expects($this->once()) + $this->entityManager->expects($this->any()) ->method('getFieldStorageDefinitions') ->with($this->entityType->id()) ->will($this->returnValue($this->storageDefinitions)); + + $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + $this->schemaHandler = new ContentEntitySchemaHandler( $this->entityManager, $this->entityType, - $this->storage + $this->storage, + $connection ); } @@ -791,7 +797,11 @@ protected function setUpSchemaHandler() { * FieldStorageDefinitionInterface::getSchema(). */ public function setUpStorageDefinition($field_name, array $schema) { - $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + // getDescription() is called once for each table. + $this->storageDefinitions[$field_name]->expects($this->any()) + ->method('getName') + ->will($this->returnValue($field_name)); // getDescription() is called once for each table. $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getDescription') @@ -800,7 +810,7 @@ public function setUpStorageDefinition($field_name, array $schema) { $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getSchema') ->will($this->returnValue($schema)); - $this->storageDefinitions[$field_name]->expects($this->once()) + $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getColumns') ->will($this->returnValue($schema['columns'])); } diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php index 196ff77..ec3b0f4 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php @@ -38,7 +38,7 @@ public static function getInfo() { public function testGetTableNames() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. - $table_mapping = new DefaultTableMapping([]); + $table_mapping = new DefaultTableMapping([], []); $this->assertSame([], $table_mapping->getTableNames()); $table_mapping->setFieldNames('foo', []); @@ -68,16 +68,16 @@ public function testGetTableNames() { */ public function testGetAllColumns() { // Set up single-column and multi-column definitions. - $definitions['id'] = $this->setUpDefinition(['value']); - $definitions['name'] = $this->setUpDefinition(['value']); - $definitions['type'] = $this->setUpDefinition(['value']); - $definitions['description'] = $this->setUpDefinition(['value', 'format']); - $definitions['owner'] = $this->setUpDefinition([ + $definitions['id'] = $this->setUpDefinition('id', ['value']); + $definitions['name'] = $this->setUpDefinition('name', ['value']); + $definitions['type'] = $this->setUpDefinition('type', ['value']); + $definitions['description'] = $this->setUpDefinition('description', ['value', 'format']); + $definitions['owner'] = $this->setUpDefinition('owner', [ 'target_id', 'target_revision_id', ]); - $table_mapping = new DefaultTableMapping($definitions); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = []; $this->assertSame($expected, $table_mapping->getAllColumns('test')); @@ -175,7 +175,7 @@ public function testGetAllColumns() { public function testGetFieldNames() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. - $table_mapping = new DefaultTableMapping([]); + $table_mapping = new DefaultTableMapping([], []); // Test that requesting the list of field names for a table for which no // fields have been added does not fail. @@ -203,18 +203,18 @@ public function testGetFieldNames() { * @covers ::getColumnNames() */ public function testGetColumnNames() { - $definitions['test'] = $this->setUpDefinition([]); - $table_mapping = new DefaultTableMapping($definitions); + $definitions['test'] = $this->setUpDefinition('test', []); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = []; $this->assertSame($expected, $table_mapping->getColumnNames('test')); - $definitions['test'] = $this->setUpDefinition(['value']); - $table_mapping = new DefaultTableMapping($definitions); + $definitions['test'] = $this->setUpDefinition('test', ['value']); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = ['value' => 'test']; $this->assertSame($expected, $table_mapping->getColumnNames('test')); - $definitions['test'] = $this->setUpDefinition(['value', 'format']); - $table_mapping = new DefaultTableMapping($definitions); + $definitions['test'] = $this->setUpDefinition('test', ['value', 'format']); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = ['value' => 'test__value', 'format' => 'test__format']; $this->assertSame($expected, $table_mapping->getColumnNames('test')); } @@ -228,7 +228,7 @@ public function testGetColumnNames() { public function testGetExtraColumns() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. - $table_mapping = new DefaultTableMapping([]); + $table_mapping = new DefaultTableMapping([], []); // Test that requesting the list of field names for a table for which no // fields have been added does not fail. @@ -252,13 +252,18 @@ public function testGetExtraColumns() { /** * Sets up a field storage definition for the test. * + * @param string $name + * The field name. * @param array $column_names * An array of column names for the storage definition. * * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected function setUpDefinition(array $column_names) { - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + protected function setUpDefinition($name, array $column_names) { + $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + $definition->expects($this->any()) + ->method('getName') + ->will($this->returnValue($name)); $definition->expects($this->any()) ->method('getColumns') ->will($this->returnValue(array_fill_keys($column_names, []))); diff --git a/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php new file mode 100644 index 0000000..f685edd --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php @@ -0,0 +1,17 @@ +