/** * Forbid a field storage update from occurring. * * Any module may forbid any update for any reason. For example, the * field's storage module might forbid an update if it would change * the storage schema while data for the field exists. A field type * module might forbid an update if it would change existing data's * semantics, or if there are external dependencies on field settings * that cannot be updated. * * To forbid the update from occurring, throw a * \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException. * * @param \Drupal\field\FieldStorageConfigInterface $field_storage * The field storage as it will be post-update. * @param \Drupal\field\FieldStorageConfigInterface $prior_field_storage * The field storage as it is pre-update. * * @see entity_crud */ function hook_field_storage_config_update_forbid(\Drupal\field\FieldStorageConfigInterface $field_storage, \Drupal\field\FieldStorageConfigInterface $prior_field_storage) { if ($field_storage->module == 'options' && $field_storage->hasData()) { // Forbid any update that removes allowed values with actual data. $allowed_values = $field_storage->getSetting('allowed_values'); $prior_allowed_values = $prior_field_storage->getSetting('allowed_values'); $lost_keys = array_keys(array_diff_key($prior_allowed_values, $allowed_values)); if (_options_values_in_use($field_storage->getTargetEntityTypeId(), $field_storage->getName(), $lost_keys)) { throw new \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field_storage->getName()))); } } }
/** * {@inheritdoc} */ public function setUp() { parent::setUp(); $this->installEntitySchema('entity_test_rev'); $this->installEntitySchema('user'); $field_name = 'test'; $this->fieldStorage = FieldStorageConfig::create(['field_name' => $field_name, 'entity_type' => 'entity_test_rev', 'type' => 'string', 'cardinality' => 1]); $this->fieldStorage->save(); $this->field = FieldConfig::create(['field_name' => $field_name, 'entity_type' => 'entity_test_rev', 'bundle' => 'entity_test_rev', 'required' => TRUE]); $this->field->save(); // Create an entity with field data. $this->entity = EntityTestRev::create(['user_id' => mt_rand(1, 10), 'name' => $this->randomMachineName(), $field_name => $this->randomString()]); $this->entity->save(); }
/** * @covers ::getType */ public function testGetType() { // Ensure that FieldConfig::getType() is not delegated to // FieldStorage. $this->entityManager->expects($this->never())->method('getFieldStorageDefinitions'); $this->fieldStorage->expects($this->never())->method('getType'); $field = new FieldConfig(array('field_name' => $this->fieldStorage->getName(), 'entity_type' => 'test_entity_type', 'bundle' => 'test_bundle', 'field_type' => 'test_field'), $this->entityTypeId); $this->assertEquals('test_field', $field->getType()); }
/** * @covers ::toArray() */ public function testToArray() { $values = array('field_name' => $this->fieldStorage->getName(), 'entity_type' => 'test_entity_type', 'bundle' => 'test_bundle'); $instance = new FieldInstanceConfig($values, $this->entityTypeId); $expected = array('id' => 'test_entity_type.test_bundle.field_test', 'uuid' => NULL, 'status' => TRUE, 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, 'field_name' => 'field_test', 'entity_type' => 'test_entity_type', 'bundle' => 'test_bundle', 'label' => '', 'description' => '', 'required' => FALSE, 'default_value' => array(), 'default_value_function' => '', 'settings' => array(), 'dependencies' => array(), 'field_type' => 'test_field'); $this->entityManager->expects($this->any())->method('getDefinition')->with($this->entityTypeId)->will($this->returnValue($this->entityType)); $this->entityType->expects($this->once())->method('getKey')->with('id')->will($this->returnValue('id')); $this->typedConfigManager->expects($this->once())->method('getDefinition')->will($this->returnValue(array('mapping' => array_fill_keys(array_keys($expected), '')))); $export = $instance->toArray(); $this->assertEquals($expected, $export); }
protected function setUp() { parent::setUp(); // Create two content types. One will have an autocomplete tagging field, // and one won't. $this->nodeTypeWithTags = $this->drupalCreateContentType(); $this->nodeTypeWithoutTags = $this->drupalCreateContentType(); // Create the vocabulary for the tag field. $this->tagVocabulary = entity_create('taxonomy_vocabulary', array('name' => 'Views testing tags', 'vid' => 'views_testing_tags')); $this->tagVocabulary->save(); // Create the tag field itself. $this->tagFieldName = 'field_views_testing_tags'; $this->tagFieldStorage = entity_create('field_storage_config', array('field_name' => $this->tagFieldName, 'entity_type' => 'node', 'type' => 'taxonomy_term_reference', 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, 'settings' => array('allowed_values' => array(array('vocabulary' => $this->tagVocabulary->id(), 'parent' => 0))))); $this->tagFieldStorage->save(); // Create an instance of the tag field on one of the content types, and // configure it to display an autocomplete widget. $this->tagField = array('field_storage' => $this->tagFieldStorage, 'bundle' => $this->nodeTypeWithTags->id()); entity_create('field_config', $this->tagField)->save(); entity_get_form_display('node', $this->nodeTypeWithTags->id(), 'default')->setComponent('field_views_testing_tags', array('type' => 'taxonomy_autocomplete'))->save(); entity_get_display('node', $this->nodeTypeWithTags->id(), 'default')->setComponent('field_views_testing_tags', array('type' => 'taxonomy_term_reference_link', 'weight' => 10))->save(); entity_get_display('node', $this->nodeTypeWithTags->id(), 'teaser')->setComponent('field_views_testing_tags', array('type' => 'taxonomy_term_reference_link', 'weight' => 10))->save(); }
/** * Tests that vocabulary machine name changes are mirrored in field definitions. */ function testTaxonomyTermFieldChangeMachineName() { // Add several entries in the 'allowed_values' setting, to make sure that // they all get updated. $this->field_storage->settings['allowed_values'] = array(array('vocabulary' => $this->vocabulary->id(), 'parent' => '0'), array('vocabulary' => $this->vocabulary->id(), 'parent' => '0'), array('vocabulary' => 'foo', 'parent' => '0')); $this->field_storage->save(); // Change the machine name. $new_name = drupal_strtolower($this->randomMachineName()); $this->vocabulary->vid = $new_name; $this->vocabulary->save(); // Check that the field is still attached to the vocabulary. $field_storage = FieldStorageConfig::loadByName('entity_test', $this->field_name); $allowed_values = $field_storage->getSetting('allowed_values'); $this->assertEqual($allowed_values[0]['vocabulary'], $new_name, 'Index 0: Machine name was updated correctly.'); $this->assertEqual($allowed_values[1]['vocabulary'], $new_name, 'Index 1: Machine name was updated correctly.'); $this->assertEqual($allowed_values[2]['vocabulary'], 'foo', 'Index 2: Machine name was left untouched.'); }
/** * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { $field_label = $form_state->get('field_config')->label(); try { $this->entity->save(); drupal_set_message($this->t('Updated field %label field settings.', array('%label' => $field_label))); $request = $this->getRequest(); if (($destinations = $request->query->get('destinations')) && ($next_destination = FieldUI::getNextDestination($destinations))) { $request->query->remove('destinations'); $form_state->setRedirectUrl($next_destination); } else { $form_state->setRedirectUrl(FieldUI::getOverviewRouteInfo($form_state->get('entity_type_id'), $form_state->get('bundle'))); } } catch (\Exception $e) { drupal_set_message($this->t('Attempt to update field %label failed: %message.', array('%label' => $field_label, '%message' => $e->getMessage())), 'error'); } }
/** * Alter the Views data on a per field basis. * * The field module's implementation of hook_views_data_alter() invokes this for * each field storage, in the module that defines the field type. It is not * invoked in other modules. * * Unlike hook_field_views_data_alter(), this operates on the whole of the views * data. This allows a field type to add data that concerns its fields in * other tables, which would not yet be defined at the point when * hook_field_views_data() and hook_field_views_data_alter() are invoked. For * example, entityreference adds reverse relationships on the tables for the * entities which are referenced by entityreference fields. * * (Note: this is weirdly named so as not to conflict with * hook_field_views_data_alter().) * * @param array $data * The views data. * @param \Drupal\field\FieldStorageConfigInterface $field * The field storage config entity. * * @see hook_field_views_data() * @see hook_field_views_data_alter() * @see field_views_data_alter() */ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\FieldStorageConfigInterface $field) { $field_name = $field->getName(); $data_key = 'field_data_' . $field_name; $entity_type_id = $field->entity_type; $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); // Views data for this field is in $data[$data_key]. $data[$data_key][$pseudo_field_name]['relationship'] = array('title' => t('@entity using @field', array('@entity' => $entity_type->getLabel(), '@field' => $label)), 'help' => t('Relate each @entity with a @field set to the term.', array('@entity' => $entity_type->getLabel(), '@field' => $label)), 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), 'label' => t('!field_name', array('!field_name' => $field_name)), 'join_extra' => array(0 => array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE))); }
/** * Forbid a field storage update from occurring. * * Any module may forbid any update for any reason. For example, the * field's storage module might forbid an update if it would change * the storage schema while data for the field exists. A field type * module might forbid an update if it would change existing data's * semantics, or if there are external dependencies on field settings * that cannot be updated. * * To forbid the update from occurring, throw a * \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException. * * @param \Drupal\field\FieldStorageConfigInterface $field_storage * The field storage as it will be post-update. * @param \Drupal\field\FieldStorageConfigInterface $prior_field_storage * The field storage as it is pre-update. * * @see entity_crud */ function hook_field_storage_config_update_forbid(\Drupal\field\FieldStorageConfigInterface $field_storage, \Drupal\field\FieldStorageConfigInterface $prior_field_storage) { // A 'list' field stores integer keys mapped to display values. If // the new field will have fewer values, and any data exists for the // abandoned keys, the field will have no way to display them. So, // forbid such an update. if ($field_storage->hasData() && count($field_storage['settings']['allowed_values']) < count($prior_field_storage['settings']['allowed_values'])) { // Identify the keys that will be lost. $lost_keys = array_diff(array_keys($field_storage['settings']['allowed_values']), array_keys($prior_field_storage['settings']['allowed_values'])); // If any data exist for those keys, forbid the update. $query = new EntityFieldQuery(); $found = $query->fieldCondition($prior_field_storage['field_name'], 'value', $lost_keys)->range(0, 1)->execute(); if ($found) { throw new \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException("Cannot update a list field storage not to include keys with existing data"); } } }
/** * Tests a simple site export import case. */ public function testExportImport() { // After installation there is no snapshot and nothing to import. $this->drupalGet('admin/config/development/configuration'); $this->assertNoText(t('Warning message')); $this->assertText(t('There are no configuration changes to import.')); $this->originalSlogan = $this->config('system.site')->get('slogan'); $this->newSlogan = $this->randomString(16); $this->assertNotEqual($this->newSlogan, $this->originalSlogan); $this->config('system.site')->set('slogan', $this->newSlogan)->save(); $this->assertEqual($this->config('system.site')->get('slogan'), $this->newSlogan); // Create a content type. $this->contentType = $this->drupalCreateContentType(); // Create a field. $this->fieldName = Unicode::strtolower($this->randomMachineName()); $this->fieldStorage = entity_create('field_storage_config', array('field_name' => $this->fieldName, 'entity_type' => 'node', 'type' => 'text')); $this->fieldStorage->save(); entity_create('field_config', array('field_storage' => $this->fieldStorage, 'bundle' => $this->contentType->id()))->save(); // Update the displays so that configuration does not change unexpectedly on // import. entity_get_form_display('node', $this->contentType->id(), 'default')->setComponent($this->fieldName, array('type' => 'text_textfield'))->save(); entity_get_display('node', $this->contentType->id(), 'full')->setComponent($this->fieldName)->save(); entity_get_display('node', $this->contentType->id(), 'default')->setComponent($this->fieldName)->save(); entity_get_display('node', $this->contentType->id(), 'teaser')->removeComponent($this->fieldName)->save(); $this->drupalGet('node/add/' . $this->contentType->id()); $this->assertFieldByName("{$this->fieldName}[0][value]", '', 'Widget is displayed'); // Export the configuration. $this->drupalPostForm('admin/config/development/configuration/full/export', array(), 'Export'); $this->tarball = $this->getRawContent(); $this->config('system.site')->set('slogan', $this->originalSlogan)->save(); $this->assertEqual($this->config('system.site')->get('slogan'), $this->originalSlogan); // Delete the custom field. $fields = FieldConfig::loadMultiple(); foreach ($fields as $field) { if ($field->getName() == $this->fieldName) { $field->delete(); } } $field_storages = FieldStorageConfig::loadMultiple(); foreach ($field_storages as $field_storage) { if ($field_storage->getName() == $this->fieldName) { $field_storage->delete(); } } $this->drupalGet('node/add/' . $this->contentType->id()); $this->assertNoFieldByName("{$this->fieldName}[0][value]", '', 'Widget is not displayed'); // Import the configuration. $filename = 'temporary://' . $this->randomMachineName(); file_put_contents($filename, $this->tarball); $this->drupalPostForm('admin/config/development/configuration/full/import', array('files[import_tarball]' => $filename), 'Upload'); // There is no snapshot yet because an import has never run. $this->assertNoText(t('Warning message')); $this->assertNoText(t('There are no configuration changes to import.')); $this->assertText($this->contentType->label()); $this->drupalPostForm(NULL, array(), 'Import all'); // After importing the snapshot has been updated an there are no warnings. $this->assertNoText(t('Warning message')); $this->assertText(t('There are no configuration changes to import.')); $this->assertEqual($this->config('system.site')->get('slogan'), $this->newSlogan); $this->drupalGet('node/add'); $this->assertFieldByName("{$this->fieldName}[0][value]", '', 'Widget is displayed'); $this->config('system.site')->set('slogan', $this->originalSlogan)->save(); $this->drupalGet('admin/config/development/configuration'); $this->assertText(t('Warning message')); $this->assertText('The following items in your active configuration have changes since the last import that may be lost on the next import.'); // Ensure the item is displayed as part of a list (to avoid false matches // on the rest of the page) and that the list markup is not escaped. $this->assertRaw('<li>system.site</li>'); // Remove everything from staging. The warning about differences between the // active and snapshot should no longer exist. \Drupal::service('config.storage.staging')->deleteAll(); $this->drupalGet('admin/config/development/configuration'); $this->assertNoText(t('Warning message')); $this->assertNoText('The following items in your active configuration have changes since the last import that may be lost on the next import.'); $this->assertText(t('There are no configuration changes to import.')); // Write a file to staging. The warning about differences between the // active and snapshot should now exist. /** @var \Drupal\Core\Config\StorageInterface $staging */ $staging = $this->container->get('config.storage.staging'); $data = $this->config('system.site')->get(); $data['slogan'] = 'in the face'; $this->copyConfig($this->container->get('config.storage'), $staging); $staging->write('system.site', $data); $this->drupalGet('admin/config/development/configuration'); $this->assertText(t('Warning message')); $this->assertText('The following items in your active configuration have changes since the last import that may be lost on the next import.'); // Ensure the item is displayed as part of a list (to avoid false matches // on the rest of the page) and that the list markup is not escaped. $this->assertRaw('<li>system.site</li>'); }
/** * {@inheritdoc} */ public function fieldGetPermissionType(FieldStorageConfigInterface $field) { $config = \Drupal::service('config.factory')->getEditable('field_permissions.field.settings'); $field_settings_perm = $config->get('permission_type_' . $field->getName()); return $field_settings_perm ? $field_settings_perm : FIELD_PERMISSIONS_PUBLIC; }
/** * Tests a simple site export import case. */ public function testExportImport() { // After installation there is no snapshot and nothing to import. $this->drupalGet('admin/config/development/configuration'); $this->assertNoText(t('Warning message')); $this->assertText(t('There are no configuration changes to import.')); $this->originalSlogan = $this->config('system.site')->get('slogan'); $this->newSlogan = $this->randomString(16); $this->assertNotEqual($this->newSlogan, $this->originalSlogan); $this->config('system.site')->set('slogan', $this->newSlogan)->save(); $this->assertEqual($this->config('system.site')->get('slogan'), $this->newSlogan); // Create a content type. $this->contentType = $this->drupalCreateContentType(); // Create a field. $this->fieldName = Unicode::strtolower($this->randomMachineName()); $this->fieldStorage = entity_create('field_storage_config', array('field_name' => $this->fieldName, 'entity_type' => 'node', 'type' => 'text')); $this->fieldStorage->save(); entity_create('field_config', array('field_storage' => $this->fieldStorage, 'bundle' => $this->contentType->id()))->save(); // Update the displays so that configuration does not change unexpectedly on // import. entity_get_form_display('node', $this->contentType->id(), 'default')->setComponent($this->fieldName, array('type' => 'text_textfield'))->save(); entity_get_display('node', $this->contentType->id(), 'full')->setComponent($this->fieldName)->save(); entity_get_display('node', $this->contentType->id(), 'default')->setComponent($this->fieldName)->save(); entity_get_display('node', $this->contentType->id(), 'teaser')->removeComponent($this->fieldName)->save(); $this->drupalGet('node/add/' . $this->contentType->id()); $this->assertFieldByName("{$this->fieldName}[0][value]", '', 'Widget is displayed'); // Export the configuration. $this->drupalPostForm('admin/config/development/configuration/full/export', array(), 'Export'); $this->tarball = $this->getRawContent(); $this->config('system.site')->set('slogan', $this->originalSlogan)->save(); $this->assertEqual($this->config('system.site')->get('slogan'), $this->originalSlogan); // Delete the custom field. $fields = FieldConfig::loadMultiple(); foreach ($fields as $field) { if ($field->field_name == $this->fieldName) { $field->delete(); } } $field_storages = FieldStorageConfig::loadMultiple(); foreach ($field_storages as $field_storage) { if ($field_storage->getName() == $this->fieldName) { $field_storage->delete(); } } $this->drupalGet('node/add/' . $this->contentType->id()); $this->assertNoFieldByName("{$this->fieldName}[0][value]", '', 'Widget is not displayed'); // Import the configuration. $filename = 'temporary://' . $this->randomMachineName(); file_put_contents($filename, $this->tarball); $this->drupalPostForm('admin/config/development/configuration/full/import', array('files[import_tarball]' => $filename), 'Upload'); // There is no snapshot yet because an import has never run. $this->assertNoText(t('Warning message')); $this->assertNoText(t('There are no configuration changes to import.')); $this->assertText($this->contentType->label()); $this->drupalPostForm(NULL, array(), 'Import all'); // After importing the snapshot has been updated an there are no warnings. $this->assertNoText(t('Warning message')); $this->assertText(t('There are no configuration changes to import.')); $this->assertEqual($this->config('system.site')->get('slogan'), $this->newSlogan); $this->drupalGet('node/add'); $this->assertFieldByName("{$this->fieldName}[0][value]", '', 'Widget is displayed'); $this->config('system.site')->set('slogan', $this->originalSlogan)->save(); $this->drupalGet('admin/config/development/configuration'); $this->assertText(t('Warning message')); $this->assertText('Your current configuration has changed. Changes to these configuration items will be lost on the next synchronization: system.site'); // Remove everything from staging. The warning about differences between the // active and snapshot should still exist. \Drupal::service('config.storage.staging')->deleteAll(); $this->drupalGet('admin/config/development/configuration'); $this->assertText(t('Warning message')); $this->assertText('Your current configuration has changed. Changes to these configuration items will be lost on the next synchronization: system.site'); $this->assertText(t('There are no configuration changes to import.')); }