diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php index 612e668..94998ac 100644 --- a/core/lib/Drupal/Core/Field/WidgetBase.php +++ b/core/lib/Drupal/Core/Field/WidgetBase.php @@ -328,9 +328,24 @@ public function flagErrors(FieldItemListInterface $items, array $form, array &$f $field_state = field_form_get_state($form['#parents'], $field_name, $form_state); if (!empty($field_state['constraint_violations'])) { + $form_builder = \Drupal::formBuilder(); + // Locate the correct element in the the form. $element = NestedArray::getValue($form_state['complete_form'], $field_state['array_parents']); + // Do not report entity-level validation errors if Form API errors have + // already been reported for the field. + // @todo Field validation should not be run on fields with FAPI errors to + // begin with. See https://drupal.org/node/2070429. + $element_path = implode('][', $element['#parents']); + if ($reported_errors = $form_builder->getErrors()) { + foreach (array_keys($reported_errors) as $error_path) { + if (strpos($error_path, $element_path) === 0) { + return; + } + } + } + // Only set errors if the element is accessible. if (!isset($element['#access']) || $element['#access']) { $definition = $this->getPluginDefinition(); @@ -341,16 +356,22 @@ public function flagErrors(FieldItemListInterface $items, array $form, array &$f // Separate violations by delta. $property_path = explode('.', $violation->getPropertyPath()); $delta = array_shift($property_path); - $violation->arrayPropertyPath = $property_path; + // Violations at the ItemList level are not associated to any delta, + // we file them under $delta NULL. + $delta = is_numeric($delta) ? $delta : NULL; + $violations_by_delta[$delta][] = $violation; + $violation->arrayPropertyPath = $property_path; } foreach ($violations_by_delta as $delta => $delta_violations) { - // For a multiple-value widget, pass all errors to the main widget. - // For single-value widgets, pass errors by delta. - if ($is_multiple) { + // Pass violations to the main element: + // - if this is a multiple-value widget, + // - or if the violations are at the ItemList level. + if ($is_multiple || $delta === NULL) { $delta_element = $element; } + // Otherwise, pass errors by delta to the corresponding sub-element. else { $original_delta = $field_state['original_deltas'][$delta]; $delta_element = $element[$original_delta]; @@ -359,7 +380,7 @@ public function flagErrors(FieldItemListInterface $items, array $form, array &$f // @todo: Pass $violation->arrayPropertyPath as property path. $error_element = $this->errorElement($delta_element, $violation, $form, $form_state); if ($error_element !== FALSE) { - form_error($error_element, $violation->getMessage()); + $form_builder->setError($error_element, $violation->getMessage()); } } } diff --git a/core/modules/field/field.attach.inc b/core/modules/field/field.attach.inc index 84f8361..594fdd6 100644 --- a/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -78,71 +78,38 @@ * keys: * - field_name: The name of the field whose operation should be invoked. By * default, the operation is invoked on all the fields in the entity's - * bundle. NOTE: This option is not compatible with the 'deleted' option; - * the 'field_id' option should be used instead. - * - field_id: The ID of the field whose operation should be invoked. By - * default, the operation is invoked on all the fields in the entity's' - * bundles. - * - deleted: If TRUE, the function will operate on deleted fields as well - * as non-deleted fields. If unset or FALSE, only non-deleted fields are - * operated on. - * - langcode: A language code or an array of language codes keyed by field - * name. It will be used to narrow down to a single value the available - * languages to act on. + * bundle. * * @return array * An array of returned values. */ function field_invoke_method($method, $target_function, EntityInterface $entity, &$a = NULL, &$b = NULL, array $options = array()) { - // Merge default options. - $default_options = array( - 'deleted' => FALSE, - 'langcode' => NULL, - ); - $options += $default_options; - $entity_type = $entity->entityType(); + // Determine the list of fields to iterate on. $field_definitions = _field_invoke_get_field_definitions($entity_type, $entity->bundle(), $options); // Iterate through the fields and collect results. $return = array(); foreach ($field_definitions as $field_definition) { - // Let the function determine the target object on which the method should be // called. $target = call_user_func($target_function, $field_definition); if (method_exists($target, $method)) { - // Determine the list of languages to iterate on. - // @todo Unify the language fallback handling here between configurable - // and base fields once field_available_languages() is either removed - // or fixed to not require a configurable field: [#2067079]. - if ($field_definition instanceof FieldInstanceInterface) { - $available_langcodes = field_available_languages($entity_type, $field_definition->getField()); - $langcodes = _field_language_suggestion($available_langcodes, $options['langcode'], $field_definition->getFieldName()); - } - else { - $langcodes = array($entity->language()->id); - } - - foreach ($langcodes as $langcode) { - if ($entity->hasTranslation($langcode)) { - $items = $entity->getTranslation($langcode)->get($field_definition->getFieldName()); - $items->filterEmptyValues(); + $items = $entity->get($field_definition->getFieldName()); + $items->filterEmptyValues(); - $result = $target->$method($items, $a, $b); + $result = $target->$method($items, $a, $b); - if (isset($result)) { - // For methods with array results, we merge results together. - // For methods with scalar results, we collect results in an array. - if (is_array($result)) { - $return = array_merge($return, $result); - } - else { - $return[] = $result; - } - } + if (isset($result)) { + // For methods with array results, we merge results together. + // For methods with scalar results, we collect results in an array. + if (is_array($result)) { + $return = array_merge($return, $result); + } + else { + $return[] = $result; } } } @@ -157,8 +124,8 @@ function field_invoke_method($method, $target_function, EntityInterface $entity, * @param string $method * The name of the method to invoke. * @param callable $target_function - * A function that receives an $instance object and returns the object on - * which the method should be invoked. + * A function that receives a FieldDefinitionInterface object and a bundle + * name and returns the object on which the method should be invoked. * @param array $entities * An array of entities, keyed by entity ID. * @param mixed $a @@ -170,17 +137,7 @@ function field_invoke_method($method, $target_function, EntityInterface $entity, * keys: * - field_name: The name of the field whose operation should be invoked. By * default, the operation is invoked on all the fields in the entity's - * bundle. NOTE: This option is not compatible with the 'deleted' option; - * the 'field_id' option should be used instead. - * - field_id: The ID of the field whose operation should be invoked. By - * default, the operation is invoked on all the fields in the entity's' - * bundles. - * - deleted: If TRUE, the function will operate on deleted fields as well - * as non-deleted fields. If unset or FALSE, only non-deleted fields are - * operated on. - * - langcode: A language code or an array of language codes keyed by field - * name. It will be used to narrow down to a single value the available - * languages to act on. + * bundle. * * @return array * An array of returned values keyed by entity ID. @@ -188,14 +145,6 @@ function field_invoke_method($method, $target_function, EntityInterface $entity, * @see field_invoke_method() */ function field_invoke_method_multiple($method, $target_function, array $entities, &$a = NULL, &$b = NULL, array $options = array()) { - // Merge default options. - $default_options = array( - 'deleted' => FALSE, - 'langcode' => NULL, - ); - $options += $default_options; - - $instances = array(); $grouped_items = array(); $grouped_targets = array(); $return = array(); @@ -203,65 +152,54 @@ function field_invoke_method_multiple($method, $target_function, array $entities // Go through the entities and collect the instances on which the method // should be called. foreach ($entities as $entity) { - $id = $entity->id(); $entity_type = $entity->entityType(); + $bundle = $entity->bundle(); + $id = $entity->id(); - // Determine the list of instances to iterate on. - $entity_instances = _field_invoke_get_field_definitions($entity_type, $entity->bundle(), $options); + // Determine the list of fields to iterate on. + $field_definitions = _field_invoke_get_field_definitions($entity_type, $bundle, $options); - foreach ($entity_instances as $instance) { - $instance_uuid = $instance->uuid(); - $field_name = $instance->getFieldName(); + foreach ($field_definitions as $field_definition) { + $field_name = $field_definition->getFieldName(); + $group_key = "$bundle:$field_name"; // Let the closure determine the target object on which the method should // be called. - if (empty($grouped_targets[$instance_uuid])) { - $grouped_targets[$instance_uuid] = call_user_func($target_function, $instance); - } - - if (method_exists($grouped_targets[$instance_uuid], $method)) { - // Add the instance to the list of instances to invoke the hook on. - if (!isset($instances[$instance_uuid])) { - $instances[$instance_uuid] = $instance; + if (empty($grouped_targets[$group_key])) { + $target = call_user_func($target_function, $field_definition, $bundle); + if (method_exists($target, $method)) { + $grouped_targets[$group_key] = $target; } - - // Unless a language code suggestion is provided we iterate on all the - // available language codes. - $field = $instance->getField(); - $available_langcodes = field_available_languages($entity_type, $field); - $langcode = !empty($options['langcode'][$id]) ? $options['langcode'][$id] : $options['langcode']; - $langcodes = _field_language_suggestion($available_langcodes, $langcode, $field_name); - foreach ($langcodes as $langcode) { - if ($entity->hasTranslation($langcode)) { - // Group the items corresponding to the current field. - $items = $entity->getTranslation($langcode)->get($field_name); - $items->filterEmptyValues(); - $grouped_items[$instance_uuid][$langcode][$id] = $items; - } + else { + $grouped_targets[$group_key] = FALSE; } } + + // If there is a target, group the field items. + if ($grouped_targets[$group_key]) { + $items = $entity->get($field_name); + $items->filterEmptyValues(); + $grouped_items[$group_key][$id] = $items; + } } // Initialize the return value for each entity. $return[$id] = array(); } - // For each instance, invoke the method and collect results. - foreach ($instances as $instance_uuid => $instance) { - // Iterate over all the field translations. - foreach ($grouped_items[$instance_uuid] as $items) { - $results = $grouped_targets[$instance_uuid]->$method($items, $a, $b); + // For each field, invoke the method and collect results. + foreach ($grouped_items as $key => $entities_items) { + $results = $grouped_targets[$key]->$method($entities_items, $a, $b); - if (isset($results)) { - // Collect results by entity. - // For hooks with array results, we merge results together. - // For hooks with scalar results, we collect results in an array. - foreach ($results as $id => $result) { - if (is_array($result)) { - $return[$id] = array_merge($return[$id], $result); - } - else { - $return[$id][] = $result; - } + if (isset($results)) { + // Collect results by entity. + // For hooks with array results, we merge results together. + // For hooks with scalar results, we collect results in an array. + foreach ($results as $id => $result) { + if (is_array($result)) { + $return[$id] = array_merge($return[$id], $result); + } + else { + $return[$id][] = $result; } } } @@ -291,48 +229,19 @@ function field_invoke_method_multiple($method, $target_function, array $entities * The array of selected field definitions. */ function _field_invoke_get_field_definitions($entity_type, $bundle, $options) { - if ($options['deleted']) { - // Deleted fields are not included in field_info_instances(), and need to - // be fetched from the database with field_read_instances(). - $params = array('entity_type' => $entity_type, 'bundle' => $bundle); - if (isset($options['field_id'])) { - // Single-field mode by field id: field_read_instances() does the filtering. - // Single-field mode by field name is not compatible with the 'deleted' - // option. - $params['field_id'] = $options['field_id']; - } - $field_definitions = field_read_instances($params, array('include_deleted' => TRUE)); - } - elseif (isset($options['field_name'])) { - // @todo Replace with \Drupal::entityManager()->getFieldDefinition() after - // [#2047229] lands. - $entity = _field_create_entity_from_ids((object) array('entity_type' => $entity_type, 'bundle' => $bundle, 'entity_id' => NULL)); - $field_definitions = array($entity->get($options['field_name'])->getFieldDefinition()); - } - elseif (isset($options['display'])) { - // @todo Replace with \Drupal::entityManager()->getFieldDefinitions() after - // [#2047229] lands. - $entity = _field_create_entity_from_ids((object) array('entity_type' => $entity_type, 'bundle' => $bundle, 'entity_id' => NULL)); - $field_definitions = array(); - foreach ($options['display']->getComponents() as $name => $component_options) { - if ($entity->hasField($name)) { - $field_definitions[] = $entity->get($name)->getFieldDefinition(); - } + // @todo Replace with \Drupal::entityManager()->getFieldDefinition() after + // [#2047229] lands. + $entity = _field_create_entity_from_ids((object) array('entity_type' => $entity_type, 'bundle' => $bundle, 'entity_id' => NULL)); + $field_definitions = array(); + if (isset($options['field_name'])) { + if ($entity->hasField($options['field_name'])) { + $field_definitions[] = $entity->get($options['field_name'])->getFieldDefinition(); } } else { - $instances = field_info_instances($entity_type, $bundle); - if (isset($options['field_id'])) { - // Single-field mode by field id: we need to loop on each instance to - // find the right one. - foreach ($instances as $instance) { - if ($instance->getField()->uuid() == $options['field_id']) { - $instances = array($instance); - break; - } - } + foreach ($entity as $items) { + $field_definitions[] = $items->getFieldDefinition(); } - $field_definitions = $instances; } return $field_definitions; diff --git a/core/modules/field/field.deprecated.inc b/core/modules/field/field.deprecated.inc index a8f08b2..ba23c2f 100644 --- a/core/modules/field/field.deprecated.inc +++ b/core/modules/field/field.deprecated.inc @@ -564,10 +564,7 @@ function field_attach_form(EntityInterface $entity, &$form, &$form_state, $langc // Get the entity_form_display object for this form. $form_display = $form_state['form_display']; - $options['display'] = $form_display; - // If no language is provided use the default site language. - $options['langcode'] = field_valid_language($langcode); $form += (array) field_invoke_method('form', _field_invoke_widget_target($form_display), $entity, $form, $form_state, $options); $form['#entity_type'] = $entity->entityType(); @@ -633,7 +630,6 @@ function field_attach_form_validate(ContentEntityInterface $entity, $form, &$for if ($has_violations) { // Map errors back to form elements. $form_display = $form_state['form_display']; - $options['display'] = $form_display; field_invoke_method('flagErrors', _field_invoke_widget_target($form_display), $entity, $form, $form_state, $options); } } @@ -661,7 +657,6 @@ function field_attach_form_validate(ContentEntityInterface $entity, $form, &$for function field_attach_extract_form_values(EntityInterface $entity, $form, &$form_state, array $options = array()) { // Extract field values from submitted values. $form_display = $form_state['form_display']; - $options['display'] = $form_display; field_invoke_method('extractFormValues', _field_invoke_widget_target($form_display), $entity, $form, $form_state, $options); // Let other modules act on submitting the entity. @@ -700,8 +695,6 @@ function field_attach_extract_form_values(EntityInterface $entity, $form, &$form * @deprecated as of Drupal 8.0. Use the entity system instead. */ function field_attach_prepare_view($entity_type, array $entities, array $displays, $langcode = NULL) { - $options['langcode'] = array(); - // To ensure hooks are only run once per entity, only process items without // the _field_view_prepared flag. // @todo: resolve this more generally for both entity and field level hooks. @@ -711,10 +704,6 @@ function field_attach_prepare_view($entity_type, array $entities, array $display // Add this entity to the items to be prepared. $prepare[$id] = $entity; - // Determine the actual language code to display for each field, given the - // language codes available in the field data. - $options['langcode'][$id] = field_language($entity, NULL, $langcode); - // Mark this item as prepared. $entity->_field_view_prepared = TRUE; } @@ -723,9 +712,9 @@ function field_attach_prepare_view($entity_type, array $entities, array $display // Then let the formatters do their own specific massaging. For each // instance, call the prepareView() method on the formatter object handed by // the entity display. - $target_function = function ($instance) use ($displays) { - if (isset($displays[$instance->bundle])) { - return $displays[$instance->bundle]->getRenderer($instance->getFieldName()); + $target_function = function ($field_definition, $bundle) use ($displays) { + if (isset($displays[$bundle])) { + return $displays[$bundle]->getRenderer($field_definition->getFieldName()); } }; $null = NULL; @@ -753,7 +742,7 @@ function field_attach_prepare_view($entity_type, array $entities, array $display * provided the current language is used. * @param array $options * An associative array of additional options. See field_invoke_method() for - * details. Note: key "langcode" will be overridden inside this function. + * details. * * @return array * A renderable array for the field values. @@ -761,11 +750,6 @@ function field_attach_prepare_view($entity_type, array $entities, array $display * @deprecated as of Drupal 8.0. Use the entity system instead. */ function field_attach_view(EntityInterface $entity, EntityDisplay $display, $langcode = NULL, array $options = array()) { - $options['display'] = $display; - // Determine the actual language code to display for each field, given the - // language codes available in the field data. - $options['langcode'] = field_language($entity, NULL, $langcode); - // For each field, call the view() method on the formatter object handed // by the entity display. $target_function = function (FieldDefinitionInterface $field_definition) use ($display) { diff --git a/core/modules/node/lib/Drupal/node/Tests/PagePreviewTest.php b/core/modules/node/lib/Drupal/node/Tests/PagePreviewTest.php index 601dc54..9704459 100644 --- a/core/modules/node/lib/Drupal/node/Tests/PagePreviewTest.php +++ b/core/modules/node/lib/Drupal/node/Tests/PagePreviewTest.php @@ -159,7 +159,7 @@ function testPagePreview() { $this->assertNoLink($newterm1); $this->assertNoLink($newterm2); - $this->drupalPostForm('node/add/page', $edit, t('Save')); + $this->drupalPostForm(NULL, $edit, t('Save')); // Check with one more new term, keeping old terms, removing the existing // one. @@ -171,10 +171,10 @@ function testPagePreview() { $this->assertRaw('>' . $newterm2 . '<', 'Second existing term displayed.'); $this->assertRaw('>' . $newterm3 . '<', 'Third new term displayed.'); $this->assertNoText($this->term->label()); - $this->assertNoLink($newterm1); - $this->assertNoLink($newterm2); + $this->assertLink($newterm1); + $this->assertLink($newterm2); $this->assertNoLink($newterm3); - $this->drupalPostForm('node/add/page', $edit, t('Save')); + $this->drupalPostForm(NULL, $edit, t('Save')); } /**