3 namespace Drupal\field_ui\Form;
5 use Drupal\Core\Config\ConfigFactoryInterface;
6 use Drupal\Core\Entity\EntityManagerInterface;
7 use Drupal\Core\Field\FieldTypePluginManagerInterface;
8 use Drupal\Core\Form\FormBase;
9 use Drupal\Core\Form\FormStateInterface;
10 use Drupal\field\Entity\FieldStorageConfig;
11 use Drupal\field\FieldStorageConfigInterface;
12 use Drupal\field_ui\FieldUI;
13 use Symfony\Component\DependencyInjection\ContainerInterface;
16 * Provides a form for the "field storage" add page.
20 class FieldStorageAddForm extends FormBase {
23 * The name of the entity type.
27 protected $entityTypeId;
39 * @var \Drupal\Core\Entity\EntityManager
41 protected $entityManager;
44 * The field type plugin manager.
46 * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
48 protected $fieldTypePluginManager;
51 * The configuration factory.
53 * @var \Drupal\Core\Config\ConfigFactoryInterface
55 protected $configFactory;
58 * Constructs a new FieldStorageAddForm object.
60 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
62 * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_plugin_manager
63 * The field type plugin manager.
64 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
65 * The configuration factory.
67 public function __construct(EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, ConfigFactoryInterface $config_factory) {
68 $this->entityManager = $entity_manager;
69 $this->fieldTypePluginManager = $field_type_plugin_manager;
70 $this->configFactory = $config_factory;
76 public function getFormId() {
77 return 'field_ui_field_storage_add_form';
83 public static function create(ContainerInterface $container) {
85 $container->get('entity.manager'),
86 $container->get('plugin.manager.field.field_type'),
87 $container->get('config.factory')
94 public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $bundle = NULL) {
95 if (!$form_state->get('entity_type_id')) {
96 $form_state->set('entity_type_id', $entity_type_id);
98 if (!$form_state->get('bundle')) {
99 $form_state->set('bundle', $bundle);
102 $this->entityTypeId = $form_state->get('entity_type_id');
103 $this->bundle = $form_state->get('bundle');
105 // Gather valid field types.
106 $field_type_options = [];
107 foreach ($this->fieldTypePluginManager->getGroupedDefinitions($this->fieldTypePluginManager->getUiDefinitions()) as $category => $field_types) {
108 foreach ($field_types as $name => $field_type) {
109 $field_type_options[$category][$name] = $field_type['label'];
114 '#type' => 'container',
115 '#attributes' => ['class' => ['form--inline', 'clearfix']],
118 $form['add']['new_storage_type'] = [
120 '#title' => $this->t('Add a new field'),
121 '#options' => $field_type_options,
122 '#empty_option' => $this->t('- Select a field type -'),
125 // Re-use existing field.
126 if ($existing_field_storage_options = $this->getExistingFieldStorageOptions()) {
127 $form['add']['separator'] = [
129 '#markup' => $this->t('or'),
131 $form['add']['existing_storage_name'] = [
133 '#title' => $this->t('Re-use an existing field'),
134 '#options' => $existing_field_storage_options,
135 '#empty_option' => $this->t('- Select an existing field -'),
138 $form['#attached']['drupalSettings']['existingFieldLabels'] = $this->getExistingFieldLabels(array_keys($existing_field_storage_options));
141 // Provide a placeholder form element to simplify the validation code.
142 $form['add']['existing_storage_name'] = [
148 // Field label and field_name.
149 $form['new_storage_wrapper'] = [
150 '#type' => 'container',
153 ':input[name="new_storage_type"]' => ['value' => ''],
157 $form['new_storage_wrapper']['label'] = [
158 '#type' => 'textfield',
159 '#title' => $this->t('Label'),
163 $field_prefix = $this->config('field_ui.settings')->get('field_prefix');
164 $form['new_storage_wrapper']['field_name'] = [
165 '#type' => 'machine_name',
166 // This field should stay LTR even for RTL languages.
167 '#field_prefix' => '<span dir="ltr">' . $field_prefix,
168 '#field_suffix' => '</span>‎',
170 '#description' => $this->t('A unique machine-readable name containing letters, numbers, and underscores.'),
171 // Calculate characters depending on the length of the field prefix
172 // setting. Maximum length is 32.
173 '#maxlength' => FieldStorageConfig::NAME_MAX_LENGTH - strlen($field_prefix),
175 'source' => ['new_storage_wrapper', 'label'],
176 'exists' => [$this, 'fieldNameExists'],
178 '#required' => FALSE,
181 // Provide a separate label element for the "Re-use existing field" case
182 // and place it outside the $form['add'] wrapper because those elements
183 // are displayed inline.
184 if ($existing_field_storage_options) {
185 $form['existing_storage_label'] = [
186 '#type' => 'textfield',
187 '#title' => $this->t('Label'),
191 ':input[name="existing_storage_name"]' => ['value' => ''],
197 // Place the 'translatable' property as an explicit value so that contrib
198 // modules can form_alter() the value for newly created fields. By default
199 // we create field storage as translatable so it will be possible to enable
200 // translation at field level.
201 $form['translatable'] = [
206 $form['actions'] = ['#type' => 'actions'];
207 $form['actions']['submit'] = [
209 '#value' => $this->t('Save and continue'),
210 '#button_type' => 'primary',
213 $form['#attached']['library'][] = 'field_ui/drupal.field_ui';
221 public function validateForm(array &$form, FormStateInterface $form_state) {
222 // Missing field type.
223 if (!$form_state->getValue('new_storage_type') && !$form_state->getValue('existing_storage_name')) {
224 $form_state->setErrorByName('new_storage_type', $this->t('You need to select a field type or an existing field.'));
226 // Both field type and existing field option selected. This is prevented in
227 // the UI with JavaScript but we also need a proper server-side validation.
228 elseif ($form_state->getValue('new_storage_type') && $form_state->getValue('existing_storage_name')) {
229 $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.'));
233 $this->validateAddNew($form, $form_state);
234 $this->validateAddExisting($form, $form_state);
238 * Validates the 'add new field' case.
241 * An associative array containing the structure of the form.
242 * @param \Drupal\Core\Form\FormStateInterface $form_state
243 * The current state of the form.
245 * @see \Drupal\field_ui\Form\FieldStorageAddForm::validateForm()
247 protected function validateAddNew(array $form, FormStateInterface $form_state) {
248 // Validate if any information was provided in the 'add new field' case.
249 if ($form_state->getValue('new_storage_type')) {
251 if (!$form_state->getValue('label')) {
252 $form_state->setErrorByName('label', $this->t('Add new field: you need to provide a label.'));
255 // Missing field name.
256 if (!$form_state->getValue('field_name')) {
257 $form_state->setErrorByName('field_name', $this->t('Add new field: you need to provide a machine name for the field.'));
259 // Field name validation.
261 $field_name = $form_state->getValue('field_name');
263 // Add the field prefix.
264 $field_name = $this->configFactory->get('field_ui.settings')->get('field_prefix') . $field_name;
265 $form_state->setValueForElement($form['new_storage_wrapper']['field_name'], $field_name);
271 * Validates the 're-use existing field' case.
274 * An associative array containing the structure of the form.
275 * @param \Drupal\Core\Form\FormStateInterface $form_state
276 * The current state of the form.
278 * @see \Drupal\field_ui\Form\FieldStorageAddForm::validateForm()
280 protected function validateAddExisting(array $form, FormStateInterface $form_state) {
281 if ($form_state->getValue('existing_storage_name')) {
283 if (!$form_state->getValue('existing_storage_label')) {
284 $form_state->setErrorByName('existing_storage_label', $this->t('Re-use existing field: you need to provide a label.'));
292 public function submitForm(array &$form, FormStateInterface $form_state) {
294 $values = $form_state->getValues();
296 $entity_type = $this->entityManager->getDefinition($this->entityTypeId);
299 if ($values['new_storage_type']) {
300 $field_storage_values = [
301 'field_name' => $values['field_name'],
302 'entity_type' => $this->entityTypeId,
303 'type' => $values['new_storage_type'],
304 'translatable' => $values['translatable'],
307 'field_name' => $values['field_name'],
308 'entity_type' => $this->entityTypeId,
309 'bundle' => $this->bundle,
310 'label' => $values['label'],
311 // Field translatability should be explicitly enabled by the users.
312 'translatable' => FALSE,
314 $widget_id = $formatter_id = NULL;
315 $widget_settings = $formatter_settings = [];
317 // Check if we're dealing with a preconfigured field.
318 if (strpos($field_storage_values['type'], 'field_ui:') !== FALSE) {
319 list(, $field_type, $option_key) = explode(':', $field_storage_values['type'], 3);
320 $field_storage_values['type'] = $field_type;
322 $field_definition = $this->fieldTypePluginManager->getDefinition($field_type);
323 $options = $this->fieldTypePluginManager->getPreconfiguredOptions($field_definition['id']);
324 $field_options = $options[$option_key];
326 // Merge in preconfigured field storage options.
327 if (isset($field_options['field_storage_config'])) {
328 foreach (['cardinality', 'settings'] as $key) {
329 if (isset($field_options['field_storage_config'][$key])) {
330 $field_storage_values[$key] = $field_options['field_storage_config'][$key];
335 // Merge in preconfigured field options.
336 if (isset($field_options['field_config'])) {
337 foreach (['required', 'settings'] as $key) {
338 if (isset($field_options['field_config'][$key])) {
339 $field_values[$key] = $field_options['field_config'][$key];
344 $widget_id = isset($field_options['entity_form_display']['type']) ? $field_options['entity_form_display']['type'] : NULL;
345 $widget_settings = isset($field_options['entity_form_display']['settings']) ? $field_options['entity_form_display']['settings'] : [];
346 $formatter_id = isset($field_options['entity_view_display']['type']) ? $field_options['entity_view_display']['type'] : NULL;
347 $formatter_settings = isset($field_options['entity_view_display']['settings']) ? $field_options['entity_view_display']['settings'] : [];
350 // Create the field storage and field.
352 $this->entityManager->getStorage('field_storage_config')->create($field_storage_values)->save();
353 $field = $this->entityManager->getStorage('field_config')->create($field_values);
356 $this->configureEntityFormDisplay($values['field_name'], $widget_id, $widget_settings);
357 $this->configureEntityViewDisplay($values['field_name'], $formatter_id, $formatter_settings);
359 // Always show the field settings step, as the cardinality needs to be
360 // configured for new fields.
361 $route_parameters = [
362 'field_config' => $field->id(),
363 ] + FieldUI::getRouteBundleParameter($entity_type, $this->bundle);
364 $destinations[] = ['route_name' => "entity.field_config.{$this->entityTypeId}_storage_edit_form", 'route_parameters' => $route_parameters];
365 $destinations[] = ['route_name' => "entity.field_config.{$this->entityTypeId}_field_edit_form", 'route_parameters' => $route_parameters];
366 $destinations[] = ['route_name' => "entity.{$this->entityTypeId}.field_ui_fields", 'route_parameters' => $route_parameters];
368 // Store new field information for any additional submit handlers.
369 $form_state->set(['fields_added', '_add_new_field'], $values['field_name']);
371 catch (\Exception $e) {
373 drupal_set_message($this->t('There was a problem creating field %label: @message', ['%label' => $values['label'], '@message' => $e->getMessage()]), 'error');
377 // Re-use existing field.
378 if ($values['existing_storage_name']) {
379 $field_name = $values['existing_storage_name'];
382 $field = $this->entityManager->getStorage('field_config')->create([
383 'field_name' => $field_name,
384 'entity_type' => $this->entityTypeId,
385 'bundle' => $this->bundle,
386 'label' => $values['existing_storage_label'],
390 $this->configureEntityFormDisplay($field_name);
391 $this->configureEntityViewDisplay($field_name);
393 $route_parameters = [
394 'field_config' => $field->id(),
395 ] + FieldUI::getRouteBundleParameter($entity_type, $this->bundle);
396 $destinations[] = ['route_name' => "entity.field_config.{$this->entityTypeId}_field_edit_form", 'route_parameters' => $route_parameters];
397 $destinations[] = ['route_name' => "entity.{$this->entityTypeId}.field_ui_fields", 'route_parameters' => $route_parameters];
399 // Store new field information for any additional submit handlers.
400 $form_state->set(['fields_added', '_add_existing_field'], $field_name);
402 catch (\Exception $e) {
404 drupal_set_message($this->t('There was a problem creating field %label: @message', ['%label' => $values['label'], '@message' => $e->getMessage()]), 'error');
409 $destination = $this->getDestinationArray();
410 $destinations[] = $destination['destination'];
411 $form_state->setRedirectUrl(FieldUI::getNextDestination($destinations, $form_state));
414 drupal_set_message($this->t('Your settings have been saved.'));
419 * Configures the field for the default form mode.
421 * @param string $field_name
423 * @param string|null $widget_id
424 * (optional) The plugin ID of the widget. Defaults to NULL.
425 * @param array $widget_settings
426 * (optional) An array of widget settings. Defaults to an empty array.
428 protected function configureEntityFormDisplay($field_name, $widget_id = NULL, array $widget_settings = []) {
431 $options['type'] = $widget_id;
432 if (!empty($widget_settings)) {
433 $options['settings'] = $widget_settings;
436 // Make sure the field is displayed in the 'default' form mode (using
437 // default widget and settings). It stays hidden for other form modes
438 // until it is explicitly configured.
439 entity_get_form_display($this->entityTypeId, $this->bundle, 'default')
440 ->setComponent($field_name, $options)
445 * Configures the field for the default view mode.
447 * @param string $field_name
449 * @param string|null $formatter_id
450 * (optional) The plugin ID of the formatter. Defaults to NULL.
451 * @param array $formatter_settings
452 * (optional) An array of formatter settings. Defaults to an empty array.
454 protected function configureEntityViewDisplay($field_name, $formatter_id = NULL, array $formatter_settings = []) {
457 $options['type'] = $formatter_id;
458 if (!empty($formatter_settings)) {
459 $options['settings'] = $formatter_settings;
462 // Make sure the field is displayed in the 'default' view mode (using
463 // default formatter and settings). It stays hidden for other view
464 // modes until it is explicitly configured.
465 entity_get_display($this->entityTypeId, $this->bundle, 'default')
466 ->setComponent($field_name, $options)
471 * Returns an array of existing field storages that can be added to a bundle.
474 * An array of existing field storages keyed by name.
476 protected function getExistingFieldStorageOptions() {
478 // Load the field_storages and build the list of options.
479 $field_types = $this->fieldTypePluginManager->getDefinitions();
480 foreach ($this->entityManager->getFieldStorageDefinitions($this->entityTypeId) as $field_name => $field_storage) {
482 // - non-configurable field storages,
483 // - locked field storages,
484 // - field storages that should not be added via user interface,
485 // - field storages that already have a field in the bundle.
486 $field_type = $field_storage->getType();
487 if ($field_storage instanceof FieldStorageConfigInterface
488 && !$field_storage->isLocked()
489 && empty($field_types[$field_type]['no_ui'])
490 && !in_array($this->bundle, $field_storage->getBundles(), TRUE)) {
491 $options[$field_name] = $this->t('@type: @field', [
492 '@type' => $field_types[$field_type]['label'],
493 '@field' => $field_name,
503 * Gets the human-readable labels for the given field storage names.
505 * Since not all field storages are required to have a field, we can only
506 * provide the field labels on a best-effort basis (e.g. the label of a field
507 * storage without any field attached to a bundle will be the field name).
509 * @param array $field_names
510 * An array of field names.
513 * An array of field labels keyed by field name.
515 protected function getExistingFieldLabels(array $field_names) {
516 // Get all the fields corresponding to the given field storage names and
518 $field_ids = $this->entityManager->getStorage('field_config')->getQuery()
519 ->condition('entity_type', $this->entityTypeId)
520 ->condition('field_name', $field_names)
522 $fields = $this->entityManager->getStorage('field_config')->loadMultiple($field_ids);
524 // Go through all the fields and use the label of the first encounter.
526 foreach ($fields as $field) {
527 if (!isset($labels[$field->getName()])) {
528 $labels[$field->getName()] = $field->label();
532 // For field storages without any fields attached to a bundle, the default
533 // label is the field name.
534 $labels += array_combine($field_names, $field_names);
540 * Checks if a field machine name is taken.
542 * @param string $value
543 * The machine name, not prefixed.
544 * @param array $element
545 * An array containing the structure of the 'field_name' element.
546 * @param \Drupal\Core\Form\FormStateInterface $form_state
547 * The current state of the form.
550 * Whether or not the field machine name is taken.
552 public function fieldNameExists($value, $element, FormStateInterface $form_state) {
553 // Don't validate the case when an existing field has been selected.
554 if ($form_state->getValue('existing_storage_name')) {
558 // Add the field prefix.
559 $field_name = $this->configFactory->get('field_ui.settings')->get('field_prefix') . $value;
561 $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
562 return isset($field_storage_definitions[$field_name]);