1<?php 2 3namespace Drupal\field_ui\Form; 4 5use Drupal\Core\Config\ConfigFactoryInterface; 6use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait; 7use Drupal\Core\Entity\EntityDisplayRepositoryInterface; 8use Drupal\Core\Entity\EntityFieldManagerInterface; 9use Drupal\Core\Entity\EntityTypeManagerInterface; 10use Drupal\Core\Field\FieldTypePluginManagerInterface; 11use Drupal\Core\Form\FormBase; 12use Drupal\Core\Form\FormStateInterface; 13use Drupal\field\Entity\FieldStorageConfig; 14use Drupal\field\FieldStorageConfigInterface; 15use Drupal\field_ui\FieldUI; 16use Symfony\Component\DependencyInjection\ContainerInterface; 17 18/** 19 * Provides a form for the "field storage" add page. 20 * 21 * @internal 22 */ 23class FieldStorageAddForm extends FormBase { 24 use DeprecatedServicePropertyTrait; 25 26 /** 27 * {@inheritdoc} 28 */ 29 protected $deprecatedProperties = [ 30 'entityManager' => 'entity.manager', 31 ]; 32 33 /** 34 * The name of the entity type. 35 * 36 * @var string 37 */ 38 protected $entityTypeId; 39 40 /** 41 * The entity bundle. 42 * 43 * @var string 44 */ 45 protected $bundle; 46 47 /** 48 * The entity type manager. 49 * 50 * @var \Drupal\Core\Entity\EntityTypeManagerInterface 51 */ 52 protected $entityTypeManager; 53 54 /** 55 * The entity field manager. 56 * 57 * @var \Drupal\Core\Entity\EntityFieldManagerInterface 58 */ 59 protected $entityFieldManager; 60 61 /** 62 * The entity display repository. 63 * 64 * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface 65 */ 66 protected $entityDisplayRepository; 67 68 /** 69 * The field type plugin manager. 70 * 71 * @var \Drupal\Core\Field\FieldTypePluginManagerInterface 72 */ 73 protected $fieldTypePluginManager; 74 75 /** 76 * The configuration factory. 77 * 78 * @var \Drupal\Core\Config\ConfigFactoryInterface 79 */ 80 protected $configFactory; 81 82 /** 83 * Constructs a new FieldStorageAddForm object. 84 * 85 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager 86 * The entity type manager. 87 * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_plugin_manager 88 * The field type plugin manager. 89 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory 90 * The configuration factory. 91 * @param \Drupal\Core\Entity\EntityFieldManagerInterface|null $entity_field_manager 92 * (optional) The entity field manager. 93 * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository 94 * (optional) The entity display repository. 95 */ 96 public function __construct(EntityTypeManagerInterface $entity_type_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, ConfigFactoryInterface $config_factory, EntityFieldManagerInterface $entity_field_manager = NULL, EntityDisplayRepositoryInterface $entity_display_repository = NULL) { 97 $this->entityTypeManager = $entity_type_manager; 98 $this->fieldTypePluginManager = $field_type_plugin_manager; 99 $this->configFactory = $config_factory; 100 if (!$entity_field_manager) { 101 @trigger_error('Calling FieldStorageAddForm::__construct() with the $entity_field_manager argument is supported in Drupal 8.7.0 and will be required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); 102 $entity_field_manager = \Drupal::service('entity_field.manager'); 103 } 104 $this->entityFieldManager = $entity_field_manager; 105 106 if (!$entity_display_repository) { 107 @trigger_error('Calling FieldStorageAddForm::__construct() with the $entity_display_repository argument is supported in Drupal 8.8.0 and will be required before Drupal 9.0.0. See https://www.drupal.org/node/2835616.', E_USER_DEPRECATED); 108 $entity_display_repository = \Drupal::service('entity_display.repository'); 109 } 110 $this->entityDisplayRepository = $entity_display_repository; 111 } 112 113 /** 114 * {@inheritdoc} 115 */ 116 public function getFormId() { 117 return 'field_ui_field_storage_add_form'; 118 } 119 120 /** 121 * {@inheritdoc} 122 */ 123 public static function create(ContainerInterface $container) { 124 return new static( 125 $container->get('entity_type.manager'), 126 $container->get('plugin.manager.field.field_type'), 127 $container->get('config.factory'), 128 $container->get('entity_field.manager'), 129 $container->get('entity_display.repository') 130 ); 131 } 132 133 /** 134 * {@inheritdoc} 135 */ 136 public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $bundle = NULL) { 137 if (!$form_state->get('entity_type_id')) { 138 $form_state->set('entity_type_id', $entity_type_id); 139 } 140 if (!$form_state->get('bundle')) { 141 $form_state->set('bundle', $bundle); 142 } 143 144 $this->entityTypeId = $form_state->get('entity_type_id'); 145 $this->bundle = $form_state->get('bundle'); 146 147 // Gather valid field types. 148 $field_type_options = []; 149 foreach ($this->fieldTypePluginManager->getGroupedDefinitions($this->fieldTypePluginManager->getUiDefinitions()) as $category => $field_types) { 150 foreach ($field_types as $name => $field_type) { 151 $field_type_options[$category][$name] = $field_type['label']; 152 } 153 } 154 155 $form['add'] = [ 156 '#type' => 'container', 157 '#attributes' => ['class' => ['form--inline', 'clearfix']], 158 ]; 159 160 $form['add']['new_storage_type'] = [ 161 '#type' => 'select', 162 '#title' => $this->t('Add a new field'), 163 '#options' => $field_type_options, 164 '#empty_option' => $this->t('- Select a field type -'), 165 ]; 166 167 // Re-use existing field. 168 if ($existing_field_storage_options = $this->getExistingFieldStorageOptions()) { 169 $form['add']['separator'] = [ 170 '#type' => 'item', 171 '#markup' => $this->t('or'), 172 ]; 173 $form['add']['existing_storage_name'] = [ 174 '#type' => 'select', 175 '#title' => $this->t('Re-use an existing field'), 176 '#options' => $existing_field_storage_options, 177 '#empty_option' => $this->t('- Select an existing field -'), 178 ]; 179 180 $form['#attached']['drupalSettings']['existingFieldLabels'] = $this->getExistingFieldLabels(array_keys($existing_field_storage_options)); 181 } 182 else { 183 // Provide a placeholder form element to simplify the validation code. 184 $form['add']['existing_storage_name'] = [ 185 '#type' => 'value', 186 '#value' => FALSE, 187 ]; 188 } 189 190 // Field label and field_name. 191 $form['new_storage_wrapper'] = [ 192 '#type' => 'container', 193 '#states' => [ 194 '!visible' => [ 195 ':input[name="new_storage_type"]' => ['value' => ''], 196 ], 197 ], 198 ]; 199 $form['new_storage_wrapper']['label'] = [ 200 '#type' => 'textfield', 201 '#title' => $this->t('Label'), 202 '#size' => 15, 203 ]; 204 205 $field_prefix = $this->config('field_ui.settings')->get('field_prefix'); 206 $form['new_storage_wrapper']['field_name'] = [ 207 '#type' => 'machine_name', 208 // This field should stay LTR even for RTL languages. 209 '#field_prefix' => '<span dir="ltr">' . $field_prefix, 210 '#field_suffix' => '</span>‎', 211 '#size' => 15, 212 '#description' => $this->t('A unique machine-readable name containing letters, numbers, and underscores.'), 213 // Calculate characters depending on the length of the field prefix 214 // setting. Maximum length is 32. 215 '#maxlength' => FieldStorageConfig::NAME_MAX_LENGTH - strlen($field_prefix), 216 '#machine_name' => [ 217 'source' => ['new_storage_wrapper', 'label'], 218 'exists' => [$this, 'fieldNameExists'], 219 ], 220 '#required' => FALSE, 221 ]; 222 223 // Provide a separate label element for the "Re-use existing field" case 224 // and place it outside the $form['add'] wrapper because those elements 225 // are displayed inline. 226 if ($existing_field_storage_options) { 227 $form['existing_storage_label'] = [ 228 '#type' => 'textfield', 229 '#title' => $this->t('Label'), 230 '#size' => 15, 231 '#states' => [ 232 '!visible' => [ 233 ':input[name="existing_storage_name"]' => ['value' => ''], 234 ], 235 ], 236 ]; 237 } 238 239 // Place the 'translatable' property as an explicit value so that contrib 240 // modules can form_alter() the value for newly created fields. By default 241 // we create field storage as translatable so it will be possible to enable 242 // translation at field level. 243 $form['translatable'] = [ 244 '#type' => 'value', 245 '#value' => TRUE, 246 ]; 247 248 $form['actions'] = ['#type' => 'actions']; 249 $form['actions']['submit'] = [ 250 '#type' => 'submit', 251 '#value' => $this->t('Save and continue'), 252 '#button_type' => 'primary', 253 ]; 254 255 $form['#attached']['library'][] = 'field_ui/drupal.field_ui'; 256 257 return $form; 258 } 259 260 /** 261 * {@inheritdoc} 262 */ 263 public function validateForm(array &$form, FormStateInterface $form_state) { 264 // Missing field type. 265 if (!$form_state->getValue('new_storage_type') && !$form_state->getValue('existing_storage_name')) { 266 $form_state->setErrorByName('new_storage_type', $this->t('You need to select a field type or an existing field.')); 267 } 268 // Both field type and existing field option selected. This is prevented in 269 // the UI with JavaScript but we also need a proper server-side validation. 270 elseif ($form_state->getValue('new_storage_type') && $form_state->getValue('existing_storage_name')) { 271 $form_state->setErrorByName('new_storage_type', $this->t('Adding a new field and re-using an existing field at the same time is not allowed.')); 272 return; 273 } 274 275 $this->validateAddNew($form, $form_state); 276 $this->validateAddExisting($form, $form_state); 277 } 278 279 /** 280 * Validates the 'add new field' case. 281 * 282 * @param array $form 283 * An associative array containing the structure of the form. 284 * @param \Drupal\Core\Form\FormStateInterface $form_state 285 * The current state of the form. 286 * 287 * @see \Drupal\field_ui\Form\FieldStorageAddForm::validateForm() 288 */ 289 protected function validateAddNew(array $form, FormStateInterface $form_state) { 290 // Validate if any information was provided in the 'add new field' case. 291 if ($form_state->getValue('new_storage_type')) { 292 // Missing label. 293 if (!$form_state->getValue('label')) { 294 $form_state->setErrorByName('label', $this->t('Add new field: you need to provide a label.')); 295 } 296 297 // Missing field name. 298 if (!$form_state->getValue('field_name')) { 299 $form_state->setErrorByName('field_name', $this->t('Add new field: you need to provide a machine name for the field.')); 300 } 301 // Field name validation. 302 else { 303 $field_name = $form_state->getValue('field_name'); 304 305 // Add the field prefix. 306 $field_name = $this->configFactory->get('field_ui.settings')->get('field_prefix') . $field_name; 307 $form_state->setValueForElement($form['new_storage_wrapper']['field_name'], $field_name); 308 } 309 } 310 } 311 312 /** 313 * Validates the 're-use existing field' case. 314 * 315 * @param array $form 316 * An associative array containing the structure of the form. 317 * @param \Drupal\Core\Form\FormStateInterface $form_state 318 * The current state of the form. 319 * 320 * @see \Drupal\field_ui\Form\FieldStorageAddForm::validateForm() 321 */ 322 protected function validateAddExisting(array $form, FormStateInterface $form_state) { 323 if ($form_state->getValue('existing_storage_name')) { 324 // Missing label. 325 if (!$form_state->getValue('existing_storage_label')) { 326 $form_state->setErrorByName('existing_storage_label', $this->t('Re-use existing field: you need to provide a label.')); 327 } 328 } 329 } 330 331 /** 332 * {@inheritdoc} 333 */ 334 public function submitForm(array &$form, FormStateInterface $form_state) { 335 $error = FALSE; 336 $values = $form_state->getValues(); 337 $destinations = []; 338 $entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId); 339 340 // Create new field. 341 if ($values['new_storage_type']) { 342 $field_storage_values = [ 343 'field_name' => $values['field_name'], 344 'entity_type' => $this->entityTypeId, 345 'type' => $values['new_storage_type'], 346 'translatable' => $values['translatable'], 347 ]; 348 $field_values = [ 349 'field_name' => $values['field_name'], 350 'entity_type' => $this->entityTypeId, 351 'bundle' => $this->bundle, 352 'label' => $values['label'], 353 // Field translatability should be explicitly enabled by the users. 354 'translatable' => FALSE, 355 ]; 356 $widget_id = $formatter_id = NULL; 357 $widget_settings = $formatter_settings = []; 358 359 // Check if we're dealing with a preconfigured field. 360 if (strpos($field_storage_values['type'], 'field_ui:') !== FALSE) { 361 list(, $field_type, $option_key) = explode(':', $field_storage_values['type'], 3); 362 $field_storage_values['type'] = $field_type; 363 364 $field_definition = $this->fieldTypePluginManager->getDefinition($field_type); 365 $options = $this->fieldTypePluginManager->getPreconfiguredOptions($field_definition['id']); 366 $field_options = $options[$option_key]; 367 368 // Merge in preconfigured field storage options. 369 if (isset($field_options['field_storage_config'])) { 370 foreach (['cardinality', 'settings'] as $key) { 371 if (isset($field_options['field_storage_config'][$key])) { 372 $field_storage_values[$key] = $field_options['field_storage_config'][$key]; 373 } 374 } 375 } 376 377 // Merge in preconfigured field options. 378 if (isset($field_options['field_config'])) { 379 foreach (['required', 'settings'] as $key) { 380 if (isset($field_options['field_config'][$key])) { 381 $field_values[$key] = $field_options['field_config'][$key]; 382 } 383 } 384 } 385 386 $widget_id = isset($field_options['entity_form_display']['type']) ? $field_options['entity_form_display']['type'] : NULL; 387 $widget_settings = isset($field_options['entity_form_display']['settings']) ? $field_options['entity_form_display']['settings'] : []; 388 $formatter_id = isset($field_options['entity_view_display']['type']) ? $field_options['entity_view_display']['type'] : NULL; 389 $formatter_settings = isset($field_options['entity_view_display']['settings']) ? $field_options['entity_view_display']['settings'] : []; 390 } 391 392 // Create the field storage and field. 393 try { 394 $this->entityTypeManager->getStorage('field_storage_config')->create($field_storage_values)->save(); 395 $field = $this->entityTypeManager->getStorage('field_config')->create($field_values); 396 $field->save(); 397 398 $this->configureEntityFormDisplay($values['field_name'], $widget_id, $widget_settings); 399 $this->configureEntityViewDisplay($values['field_name'], $formatter_id, $formatter_settings); 400 401 // Always show the field settings step, as the cardinality needs to be 402 // configured for new fields. 403 $route_parameters = [ 404 'field_config' => $field->id(), 405 ] + FieldUI::getRouteBundleParameter($entity_type, $this->bundle); 406 $destinations[] = ['route_name' => "entity.field_config.{$this->entityTypeId}_storage_edit_form", 'route_parameters' => $route_parameters]; 407 $destinations[] = ['route_name' => "entity.field_config.{$this->entityTypeId}_field_edit_form", 'route_parameters' => $route_parameters]; 408 $destinations[] = ['route_name' => "entity.{$this->entityTypeId}.field_ui_fields", 'route_parameters' => $route_parameters]; 409 410 // Store new field information for any additional submit handlers. 411 $form_state->set(['fields_added', '_add_new_field'], $values['field_name']); 412 } 413 catch (\Exception $e) { 414 $error = TRUE; 415 $this->messenger()->addError($this->t('There was a problem creating field %label: @message', ['%label' => $values['label'], '@message' => $e->getMessage()])); 416 } 417 } 418 419 // Re-use existing field. 420 if ($values['existing_storage_name']) { 421 $field_name = $values['existing_storage_name']; 422 423 try { 424 $field = $this->entityTypeManager->getStorage('field_config')->create([ 425 'field_name' => $field_name, 426 'entity_type' => $this->entityTypeId, 427 'bundle' => $this->bundle, 428 'label' => $values['existing_storage_label'], 429 ]); 430 $field->save(); 431 432 $this->configureEntityFormDisplay($field_name); 433 $this->configureEntityViewDisplay($field_name); 434 435 $route_parameters = [ 436 'field_config' => $field->id(), 437 ] + FieldUI::getRouteBundleParameter($entity_type, $this->bundle); 438 $destinations[] = ['route_name' => "entity.field_config.{$this->entityTypeId}_field_edit_form", 'route_parameters' => $route_parameters]; 439 $destinations[] = ['route_name' => "entity.{$this->entityTypeId}.field_ui_fields", 'route_parameters' => $route_parameters]; 440 441 // Store new field information for any additional submit handlers. 442 $form_state->set(['fields_added', '_add_existing_field'], $field_name); 443 } 444 catch (\Exception $e) { 445 $error = TRUE; 446 $this->messenger()->addError($this->t('There was a problem creating field %label: @message', ['%label' => $values['label'], '@message' => $e->getMessage()])); 447 } 448 } 449 450 if ($destinations) { 451 $destination = $this->getDestinationArray(); 452 $destinations[] = $destination['destination']; 453 $form_state->setRedirectUrl(FieldUI::getNextDestination($destinations, $form_state)); 454 } 455 elseif (!$error) { 456 $this->messenger()->addStatus($this->t('Your settings have been saved.')); 457 } 458 } 459 460 /** 461 * Configures the field for the default form mode. 462 * 463 * @param string $field_name 464 * The field name. 465 * @param string|null $widget_id 466 * (optional) The plugin ID of the widget. Defaults to NULL. 467 * @param array $widget_settings 468 * (optional) An array of widget settings. Defaults to an empty array. 469 */ 470 protected function configureEntityFormDisplay($field_name, $widget_id = NULL, array $widget_settings = []) { 471 $options = []; 472 if ($widget_id) { 473 $options['type'] = $widget_id; 474 if (!empty($widget_settings)) { 475 $options['settings'] = $widget_settings; 476 } 477 } 478 // Make sure the field is displayed in the 'default' form mode (using 479 // default widget and settings). It stays hidden for other form modes 480 // until it is explicitly configured. 481 $this->entityDisplayRepository->getFormDisplay($this->entityTypeId, $this->bundle, 'default') 482 ->setComponent($field_name, $options) 483 ->save(); 484 } 485 486 /** 487 * Configures the field for the default view mode. 488 * 489 * @param string $field_name 490 * The field name. 491 * @param string|null $formatter_id 492 * (optional) The plugin ID of the formatter. Defaults to NULL. 493 * @param array $formatter_settings 494 * (optional) An array of formatter settings. Defaults to an empty array. 495 */ 496 protected function configureEntityViewDisplay($field_name, $formatter_id = NULL, array $formatter_settings = []) { 497 $options = []; 498 if ($formatter_id) { 499 $options['type'] = $formatter_id; 500 if (!empty($formatter_settings)) { 501 $options['settings'] = $formatter_settings; 502 } 503 } 504 // Make sure the field is displayed in the 'default' view mode (using 505 // default formatter and settings). It stays hidden for other view 506 // modes until it is explicitly configured. 507 $this->entityDisplayRepository->getViewDisplay($this->entityTypeId, $this->bundle) 508 ->setComponent($field_name, $options) 509 ->save(); 510 } 511 512 /** 513 * Returns an array of existing field storages that can be added to a bundle. 514 * 515 * @return array 516 * An array of existing field storages keyed by name. 517 */ 518 protected function getExistingFieldStorageOptions() { 519 $options = []; 520 // Load the field_storages and build the list of options. 521 $field_types = $this->fieldTypePluginManager->getDefinitions(); 522 foreach ($this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId) as $field_name => $field_storage) { 523 // Do not show: 524 // - non-configurable field storages, 525 // - locked field storages, 526 // - field storages that should not be added via user interface, 527 // - field storages that already have a field in the bundle. 528 $field_type = $field_storage->getType(); 529 if ($field_storage instanceof FieldStorageConfigInterface 530 && !$field_storage->isLocked() 531 && empty($field_types[$field_type]['no_ui']) 532 && !in_array($this->bundle, $field_storage->getBundles(), TRUE)) { 533 $options[$field_name] = $this->t('@type: @field', [ 534 '@type' => $field_types[$field_type]['label'], 535 '@field' => $field_name, 536 ]); 537 } 538 } 539 asort($options); 540 541 return $options; 542 } 543 544 /** 545 * Gets the human-readable labels for the given field storage names. 546 * 547 * Since not all field storages are required to have a field, we can only 548 * provide the field labels on a best-effort basis (e.g. the label of a field 549 * storage without any field attached to a bundle will be the field name). 550 * 551 * @param array $field_names 552 * An array of field names. 553 * 554 * @return array 555 * An array of field labels keyed by field name. 556 */ 557 protected function getExistingFieldLabels(array $field_names) { 558 // Get all the fields corresponding to the given field storage names and 559 // this entity type. 560 $field_ids = $this->entityTypeManager->getStorage('field_config')->getQuery() 561 ->condition('entity_type', $this->entityTypeId) 562 ->condition('field_name', $field_names) 563 ->execute(); 564 $fields = $this->entityTypeManager->getStorage('field_config')->loadMultiple($field_ids); 565 566 // Go through all the fields and use the label of the first encounter. 567 $labels = []; 568 foreach ($fields as $field) { 569 if (!isset($labels[$field->getName()])) { 570 $labels[$field->getName()] = $field->label(); 571 } 572 } 573 574 // For field storages without any fields attached to a bundle, the default 575 // label is the field name. 576 $labels += array_combine($field_names, $field_names); 577 578 return $labels; 579 } 580 581 /** 582 * Checks if a field machine name is taken. 583 * 584 * @param string $value 585 * The machine name, not prefixed. 586 * @param array $element 587 * An array containing the structure of the 'field_name' element. 588 * @param \Drupal\Core\Form\FormStateInterface $form_state 589 * The current state of the form. 590 * 591 * @return bool 592 * Whether or not the field machine name is taken. 593 */ 594 public function fieldNameExists($value, $element, FormStateInterface $form_state) { 595 // Don't validate the case when an existing field has been selected. 596 if ($form_state->getValue('existing_storage_name')) { 597 return FALSE; 598 } 599 600 // Add the field prefix. 601 $field_name = $this->configFactory->get('field_ui.settings')->get('field_prefix') . $value; 602 603 $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId); 604 return isset($field_storage_definitions[$field_name]); 605 } 606 607} 608