diff --git a/core/lib/Drupal/Core/Render/Element/MachineName.php b/core/lib/Drupal/Core/Render/Element/MachineName.php index 66c85c7..58402d0 100644 --- a/core/lib/Drupal/Core/Render/Element/MachineName.php +++ b/core/lib/Drupal/Core/Render/Element/MachineName.php @@ -170,8 +170,24 @@ public static function processMachineName(&$element, FormStateInterface $form_st $key_exists = NULL; $source = NestedArray::getValue($form_state->getCompleteForm(), $element['#machine_name']['source'], $key_exists); if (!$key_exists) { + // Try to find the source element by looking up the parents of the machine + // name element. + $element_parents = $element['#array_parents']; + + // Remove the element itself from the array. + array_pop($element_parents); + $source_parents = array_merge($element_parents, $element['#machine_name']['source']); + $source = NestedArray::getValue($form_state->getCompleteForm(), $source_parents, $key_exists); + } + + if (!$key_exists) { return $element; } + elseif (isset($source_parents)) { + // The source element was found in the form in a different location than + // initially specified so we need to update it. + $element['#machine_name']['source'] = $source_parents; + } $suffix_id = $source['#id'] . '-machine-name-suffix'; $element['#machine_name']['suffix'] = '#' . $suffix_id; diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php index 5fff1aa..42ccdda 100644 --- a/core/modules/field/src/Entity/FieldStorageConfig.php +++ b/core/modules/field/src/Entity/FieldStorageConfig.php @@ -237,12 +237,6 @@ class FieldStorageConfig extends ConfigEntityBase implements FieldStorageConfigI */ public function __construct(array $values, $entity_type = 'field_storage_config') { // Check required properties. - if (empty($values['field_name'])) { - throw new FieldException('Attempt to create a field storage without a field name.'); - } - if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $values['field_name'])) { - throw new FieldException("Attempt to create a field storage {$values['field_name']} with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character"); - } if (empty($values['type'])) { throw new FieldException("Attempt to create a field storage {$values['field_name']} with no type."); } @@ -304,6 +298,13 @@ protected function preSaveNew(EntityStorageInterface $storage) { // Assign the ID. $this->id = $this->id(); + // Validate that we have a field name and that it is valid. + if (!$this->getName()) { + throw new FieldException('Attempt to create a field storage without a field name.'); + } + if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $this->getName())) { + throw new FieldException("Attempt to create a field storage {$this->getName()} with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character"); + } // Field name cannot be longer than FieldStorageConfig::NAME_MAX_LENGTH characters. // We use Unicode::strlen() because the DB layer assumes that column widths // are given in characters rather than bytes. @@ -352,6 +353,9 @@ protected function preSaveUpdated(EntityStorageInterface $storage) { $entity_manager = \Drupal::entityManager(); // Some updates are always disallowed. + if ($this->getName() != $this->original->getName()) { + throw new FieldException("Cannot change the field name for an existing field storage."); + } if ($this->getType() != $this->original->getType()) { throw new FieldException("Cannot change the field type for an existing field storage."); } @@ -687,7 +691,7 @@ public function isQueryable() { * TRUE if the field has data for any entity; FALSE otherwise. */ public function hasData() { - return \Drupal::entityManager()->getStorage($this->entity_type)->countFieldData($this, TRUE); + return !$this->isNew() && \Drupal::entityManager()->getStorage($this->entity_type)->countFieldData($this, TRUE); } /** diff --git a/core/modules/field_ui/src/Form/FieldStorageAddForm.php b/core/modules/field_ui/src/Form/FieldStorageAddForm.php index 5f087a6..b587c67 100644 --- a/core/modules/field_ui/src/Form/FieldStorageAddForm.php +++ b/core/modules/field_ui/src/Form/FieldStorageAddForm.php @@ -7,12 +7,14 @@ namespace Drupal\field_ui\Form; -use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityFormBuilderInterface; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\FieldStorageConfigInterface; use Drupal\field_ui\FieldUI; @@ -59,11 +61,11 @@ class FieldStorageAddForm extends FormBase { public $queryFactory; /** - * The configuration factory. + * The entity form builder. * - * @var \Drupal\Core\Config\ConfigFactoryInterface + * @var \Drupal\Core\Entity\EntityFormBuilderInterface */ - protected $configFactory; + protected $entityFormBuilder; /** * Constructs a new FieldStorageAddForm object. @@ -74,14 +76,14 @@ class FieldStorageAddForm extends FormBase { * The field type plugin manager. * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory * The entity query factory. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The configuration factory. + * @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder + * The entity form builder. */ - public function __construct(EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, QueryFactory $query_factory, ConfigFactoryInterface $config_factory) { + public function __construct(EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, QueryFactory $query_factory, EntityFormBuilderInterface $entity_form_builder) { $this->entityManager = $entity_manager; $this->fieldTypePluginManager = $field_type_plugin_manager; $this->queryFactory = $query_factory; - $this->configFactory = $config_factory; + $this->entityFormBuilder = $entity_form_builder; } /** @@ -99,7 +101,7 @@ public static function create(ContainerInterface $container) { $container->get('entity.manager'), $container->get('plugin.manager.field.field_type'), $container->get('entity.query'), - $container->get('config.factory') + $container->get('entity.form_builder') ); } @@ -135,6 +137,10 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t '#title' => $this->t('Add a new field'), '#options' => $field_type_options, '#empty_option' => $this->t('- Select a field type -'), + '#ajax' => array( + 'callback' => '::buildAjaxFieldStorageConfig', + 'wrapper' => 'new-storage-wrapper', + ), ); // Re-use existing field. @@ -160,38 +166,80 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t ); } - // Field label and field_name. - $form['new_storage_wrapper'] = array( + // New field storage subform wrapper. + $form['new_storage_wrapper'] = [ '#type' => 'container', - '#states' => array( - '!visible' => array( - ':input[name="new_storage_type"]' => array('value' => ''), + '#attributes' => [ + 'id' => 'new-storage-wrapper', + ], + ]; + + if ($form_state->isRebuilding() && ($new_storage_type = $form_state->getValue('new_storage_type'))) { + // Field label and field_name. + $form['new_storage_wrapper']['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#size' => 15, + '#weight' => -15, + ); + + $field_prefix = $this->config('field_ui.settings')->get('field_prefix'); + $form['new_storage_wrapper']['field_name'] = array( + '#type' => 'machine_name', + // This field should stay LTR even for RTL languages. + '#field_prefix' => '' . $field_prefix, + '#field_suffix' => '‎', + '#size' => 15, + '#description' => $this->t('A unique machine-readable name containing letters, numbers, and underscores.'), + // Calculate characters depending on the length of the field prefix + // setting. Maximum length is 32. + '#maxlength' => FieldStorageConfig::NAME_MAX_LENGTH - strlen($field_prefix), + '#machine_name' => array( + 'source' => array('label'), + 'exists' => array($this, 'fieldNameExists'), ), - ), - ); - $form['new_storage_wrapper']['label'] = array( - '#type' => 'textfield', - '#title' => $this->t('Label'), - '#size' => 15, - ); + '#required' => FALSE, + '#weight' => -14, + ); - $field_prefix = $this->config('field_ui.settings')->get('field_prefix'); - $form['new_storage_wrapper']['field_name'] = array( - '#type' => 'machine_name', - // This field should stay LTR even for RTL languages. - '#field_prefix' => '' . $field_prefix, - '#field_suffix' => '‎', - '#size' => 15, - '#description' => $this->t('A unique machine-readable name containing letters, numbers, and underscores.'), - // Calculate characters depending on the length of the field prefix - // setting. Maximum length is 32. - '#maxlength' => FieldStorageConfig::NAME_MAX_LENGTH - strlen($field_prefix), - '#machine_name' => array( - 'source' => array('new_storage_wrapper', 'label'), - 'exists' => array($this, 'fieldNameExists'), - ), - '#required' => FALSE, - ); + // Instantiate the field storage config and field config objects that we + // will work with throughout the life-cycle of this form. + $preconfigured_values = $this->getPreconfiguredValues($new_storage_type); + + $field_storage_values = [ + 'entity_type' => $this->entityTypeId, + 'type' => $preconfigured_values['type'], + ] + $preconfigured_values['field_storage']; + $field_storage_config = $this->entityManager->getStorage('field_storage_config')->create($field_storage_values); + $form_state->set('field_storage_config', $field_storage_config); + + $field_values = [ + 'entity_type' => $this->entityTypeId, + 'bundle' => $this->bundle, + 'field_storage' => $field_storage_config, + ] + $preconfigured_values['field']; + $field_config = $this->entityManager->getStorage('field_config')->create($field_values); + $form_state->set('field_config', $field_config); + + $form_state->set('preconfigured_widget_id', $preconfigured_values['widget_id']); + $form_state->set('preconfigured_formatter_id', $preconfigured_values['formatter_id']); + + // Instantiate the field storage config entity form. + $form_object = $this->entityManager->getFormObject('field_storage_config', 'edit'); + $form_object->setEntity($field_storage_config); + + $form_state = (new FormState())->setFormState([ + 'entity_type_id' => $form_state->get('entity_type_id'), + 'bundle' => $form_state->get('bundle'), + 'field_config' => $field_config, + ]); + + $field_storage_form = $form_object->form([], $form_state); + unset($field_storage_form['#process'], $field_storage_form['#after_build']); + $field_storage_form['cardinality_container']['#parents'] = ['new_storage_wrapper']; + + $form['new_storage_wrapper'] += $field_storage_form; + } // Provide a separate label element for the "Re-use existing field" case // and place it outside the $form['add'] wrapper because those elements @@ -231,6 +279,22 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t } /** + * Form submission handler for the 'new field type' element. + */ + public function newStorageTypeSubmit($form, FormStateInterface $form_state) { + // @todo Set some property on $form_state to indicate that we need to + // display the field storage config subform. + $form_state->setRebuild(); + } + + /** + * Handles changes to the selected field storage type. + */ + public function buildAjaxFieldStorageConfig(array $form, FormStateInterface $form_state) { + return $form['new_storage_wrapper']; + } + + /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { @@ -261,7 +325,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { */ protected function validateAddNew(array $form, FormStateInterface $form_state) { // Validate if any information was provided in the 'add new field' case. - if ($form_state->getValue('new_storage_type')) { + if ($form_state->isRebuilding() && $form_state->getValue('new_storage_type')) { // Missing label. if (!$form_state->getValue('label')) { $form_state->setErrorByName('label', $this->t('Add new field: you need to provide a label.')); @@ -279,6 +343,11 @@ protected function validateAddNew(array $form, FormStateInterface $form_state) { $field_name = $this->configFactory->get('field_ui.settings')->get('field_prefix') . $field_name; $form_state->setValueForElement($form['new_storage_wrapper']['field_name'], $field_name); } + + // Also run the field storage config form validation. + $form_object = $this->entityManager->getFormObject('field_storage_config', 'edit'); + $form_object->setEntity($form_state->get('field_storage_config')); + $form_object->validateForm($form['new_storage_wrapper'], $form_state); } } @@ -312,67 +381,37 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Create new field. if ($values['new_storage_type']) { - $field_storage_values = [ - 'field_name' => $values['field_name'], - 'entity_type' => $this->entityTypeId, - 'type' => $values['new_storage_type'], - 'translatable' => $values['translatable'], - ]; - $field_values = [ - 'field_name' => $values['field_name'], - 'entity_type' => $this->entityTypeId, - 'bundle' => $this->bundle, - 'label' => $values['label'], - // Field translatability should be explicitly enabled by the users. - 'translatable' => FALSE, - ]; - $widget_id = $formatter_id = NULL; - - // Check if we're dealing with a preconfigured field. - if (strpos($field_storage_values['type'], 'field_ui:') !== FALSE) { - list(, $field_type, $option_key) = explode(':', $field_storage_values['type'], 3); - $field_storage_values['type'] = $field_type; - - $field_type_class = $this->fieldTypePluginManager->getDefinition($field_type)['class']; - $field_options = $field_type_class::getPreconfiguredOptions()[$option_key]; - - // Merge in preconfigured field storage options. - if (isset($field_options['field_storage_config'])) { - foreach (array('cardinality', 'settings') as $key) { - if (isset($field_options['field_storage_config'][$key])) { - $field_storage_values[$key] = $field_options['field_storage_config'][$key]; - } - } - } + $field_storage_config = $form_state->get('field_storage_config'); + $field_config = $form_state->get('field_config'); - // Merge in preconfigured field options. - if (isset($field_options['field_config'])) { - foreach (array('required', 'settings') as $key) { - if (isset($field_options['field_config'][$key])) { - $field_values[$key] = $field_options['field_config'][$key]; - } - } - } + $field_storage_config + ->set('field_name', $values['field_name']) + ->set('translatable', $values['translatable']); - $widget_id = isset($field_options['entity_form_display']['type']) ? $field_options['entity_form_display']['type'] : NULL; - $formatter_id = isset($field_options['entity_view_display']['type']) ? $field_options['entity_view_display']['type'] : NULL; - } + $field_config + ->set('field_name', $values['field_name']) + ->set('label', $values['label']) + // Field translatability should be explicitly enabled by the users. + ->set('translatable', FALSE); // Create the field storage and field. try { - $this->entityManager->getStorage('field_storage_config')->create($field_storage_values)->save(); - $field = $this->entityManager->getStorage('field_config')->create($field_values); - $field->save(); + // Allow the field storage edit subform to also set its values. + $form_object = $this->entityManager->getFormObject('field_storage_config', 'edit'); + $form_object->setEntity($field_storage_config); + $form_object->submitForm($form['new_storage_wrapper'], $form_state); + + $field_storage_config = $form_object->getEntity(); + $field_storage_config->save(); + $field_config->save(); - $this->configureEntityFormDisplay($values['field_name'], $widget_id); - $this->configureEntityViewDisplay($values['field_name'], $formatter_id); + $this->configureEntityFormDisplay($values['field_name'], $form_state->get('preconfigured_widget_id')); + $this->configureEntityViewDisplay($values['field_name'], $form_state->get('preconfigured_formatter_id')); - // Always show the field settings step, as the cardinality needs to be - // configured for new fields. + // Always show the field settings step. $route_parameters = array( - 'field_config' => $field->id(), + 'field_config' => $field_config->id(), ) + FieldUI::getRouteBundleParameter($entity_type, $this->bundle); - $destinations[] = array('route_name' => "entity.field_config.{$this->entityTypeId}_storage_edit_form", 'route_parameters' => $route_parameters); $destinations[] = array('route_name' => "entity.field_config.{$this->entityTypeId}_field_edit_form", 'route_parameters' => $route_parameters); $destinations[] = array('route_name' => "entity.{$this->entityTypeId}.field_ui_fields", 'route_parameters' => $route_parameters); @@ -427,6 +466,62 @@ public function submitForm(array &$form, FormStateInterface $form_state) { } /** + * Gets the default properties for the config entities created by this form. + * + * @param string $field_type + * The value selected in the 'new storage type' form element. + * + * @return array + * An array with the following structure: + * - type: (string) The field type plugin ID. + * - field_storage: (array) The default properties of a field storage + * config. + * - field: (array) The default properties of a field config. + * - widget_id: (string) The default widget plugin ID. + * - formatter_id: (string) The default formatter plugin ID. + */ + protected function getPreconfiguredValues($field_type) { + $preconfigured_values = [ + 'type' => $field_type, + 'field_storage' => [], + 'field' => [], + 'widget_id' => NULL, + 'formatter_id' => NULL, + ]; + + if (strpos($field_type, 'field_ui:') !== FALSE) { + list(, $field_type, $option_key) = explode(':', $field_type, 3); + $preconfigured_values['type'] = $field_type; + + $field_type_class = $this->fieldTypePluginManager->getDefinition($field_type)['class']; + $field_options = $field_type_class::getPreconfiguredOptions()[$option_key]; + + // Add pre-configured field storage options. + if (isset($field_options['field_storage_config'])) { + foreach (array('cardinality', 'settings') as $key) { + if (isset($field_options['field_storage_config'][$key])) { + $preconfigured_values['field_storage'][$key] = $field_options['field_storage_config'][$key]; + } + } + } + + // Add pre-configured field options. + if (isset($field_options['field_config'])) { + foreach (array('required', 'settings') as $key) { + if (isset($field_options['field_config'][$key])) { + $preconfigured_values['field'][$key] = $field_options['field_config'][$key]; + } + } + } + + $preconfigured_values['widget_id'] = isset($field_options['entity_form_display']['type']) ? $field_options['entity_form_display']['type'] : NULL; + $preconfigured_values['formatter_id'] = isset($field_options['entity_view_display']['type']) ? $field_options['entity_view_display']['type'] : NULL; + } + + return $preconfigured_values; + } + + /** * Configures the newly created field for the default view and form modes. * * @param string $field_name diff --git a/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php b/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php index adafcc7..00d1b5d 100644 --- a/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php +++ b/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php @@ -8,6 +8,8 @@ namespace Drupal\field_ui\Form; use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldItemList; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; @@ -61,14 +63,18 @@ public function buildForm(array $form, FormStateInterface $form_state, $field_co public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); - $field_label = $form_state->get('field_config')->label(); - $form['#title'] = $field_label; - $form['#prefix'] = '

' . $this->t('These settings apply to the %field field everywhere it is used. These settings impact the way that data is stored in the database and cannot be changed once data has been created.', array('%field' => $field_label)) . '

'; - - // See if data already exists for this field. - // If so, prevent changes to the field settings. - if ($this->entity->hasData()) { - $form['#prefix'] = '
' . $this->t('There is data for this field in the database. The field settings can no longer be changed.') . '
' . $form['#prefix']; + // We cannot use an 'add' entity form operation because this form is also + // used as an AJAX subform of \Drupal\field_ui\Form\FieldStorageAddForm. + if (!$this->entity->isNew()) { + $field_label = $form_state->get('field_config')->label(); + $form['#title'] = $field_label; + $form['#prefix'] = '

' . $this->t('These settings apply to the %field field everywhere it is used. These settings impact the way that data is stored in the database and cannot be changed once data has been created.', array('%field' => $field_label)) . '

'; + + // See if data already exists for this field. + // If so, prevent changes to the field settings. + if ($this->entity->hasData()) { + $form['#prefix'] = '
' . $this->t('There is data for this field in the database. The field settings can no longer be changed.') . '
' . $form['#prefix']; + } } // Add settings provided by the field module. The field module is @@ -86,7 +92,7 @@ public function form(array $form, FormStateInterface $form_state) { 'entity_id' => NULL ); $entity = _field_create_entity_from_ids($ids); - $items = $entity->get($this->entity->getName()); + $items = FieldItemList::createInstance($form_state->get('field_config'), NULL, $entity->getTypedData()); $item = $items->first() ?: $items->appendItem(); $form['settings'] += $item->storageSettingsForm($form, $form_state, $this->entity->hasData());