diff -u b/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php --- b/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -287,7 +287,7 @@ // dsm($violation->getInvalidValue()->getValue(), 'InvalidValue'); // dsm($violation->getCode(), 'Code'); // } - $langcode = field_is_translatable($entity->entityType(), $instance->getField()) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED; + $langcode = field_is_translatable($entity->entityType(), field_info_field($field_name)) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED; $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state); $field_state['constraint_violations'] = $field_violations; field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); diff -u b/core/modules/field/field.attach.inc b/core/modules/field/field.attach.inc --- b/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -6,8 +6,10 @@ */ use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityNG; use Drupal\entity\Plugin\Core\Entity\EntityDisplay; use Drupal\entity\Plugin\Core\Entity\EntityFormDisplay; +use Drupal\Core\Language\Language; /** * @defgroup field_storage Field Storage API @@ -1027,6 +1028,55 @@ } /** + * Performs field validation against form-submitted field values. + * + * This function does not need to be called on regular entity forms, where the + * "entity form controller" takes care of validating field values. + * It is only preserved to support the use case of nested entity forms, until a + * more generic solution is figured out in http://drupal.org/node/1728816. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being submitted. The actual field values will be read + * from $form_state['values']. + * @param $form + * The form structure where field elements are attached to. This might be a + * full form structure, or a sub-element of a larger form. + * @param $form_state + * An associative array containing the current state of the form. + * @param array $options + * An associative array of additional options. See field_invoke_method() for + * details. + */ +function field_attach_form_validate(EntityInterface $entity, $form, &$form_state, array $options = array()) { + // Only support NG entities. + if (!($entity instanceof EntityNG)) { + return; + } + + $has_violations = FALSE; + foreach ($entity as $field_name => $field) { + $definition = $field->getDefinition(); + if (!empty($definition['configurable']) && (empty($options['field_name']) || $options['field_name'] == $field_name)) { + $field_violations = $field->validate(); + if (count($field_violations)) { + $has_violations = TRUE; + + // Place violations in $form_state. + $langcode = field_is_translatable($entity->entityType(), field_info_field($field_name)) ? $entity->language()->langcode : Language::LANGCODE_NOT_SPECIFIED; + $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state); + $field_state['constraint_violations'] = $field_violations; + field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); + } + } + } + + if ($has_violations) { + // Map errors back to form elements. + field_invoke_method('flagErrors', _field_invoke_widget_target($form_state['form_display']), $entity, $form, $form_state, $options); + } +} + +/** * Populates an entity object with values from a form submission. * * Currently, this accounts for drag-and-drop reordering of field values, and diff -u b/core/modules/field/lib/Drupal/field/Tests/FormTest.php b/core/modules/field/lib/Drupal/field/Tests/FormTest.php --- b/core/modules/field/lib/Drupal/field/Tests/FormTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FormTest.php @@ -496,121 +496,6 @@ } /** - * Tests Field API form integration within a subform. - */ -// function testNestedFieldForm() { -// // Add two instances on the 'test_bundle' -// field_create_field($this->field_single); -// field_create_field($this->field_unlimited); -// $this->instance['field_name'] = 'field_single'; -// $this->instance['label'] = 'Single field'; -// field_create_instance($this->instance); -// entity_get_form_display($this->instance['entity_type'], $this->instance['bundle'], 'default') -// ->setComponent($this->instance['field_name']) -// ->save(); -// $this->instance['field_name'] = 'field_unlimited'; -// $this->instance['label'] = 'Unlimited field'; -// field_create_instance($this->instance); -// entity_get_form_display($this->instance['entity_type'], $this->instance['bundle'], 'default') -// ->setComponent($this->instance['field_name']) -// ->save(); -// -// // Create two entities. -// $entity_1 = field_test_create_entity(1, 1); -// $entity_1->is_new = TRUE; -// $entity_1->field_single[Language::LANGCODE_NOT_SPECIFIED][] = array('value' => 0); -// $entity_1->field_unlimited[Language::LANGCODE_NOT_SPECIFIED][] = array('value' => 1); -// field_test_entity_save($entity_1); -// -// $entity_2 = field_test_create_entity(2, 2); -// $entity_2->is_new = TRUE; -// $entity_2->field_single[Language::LANGCODE_NOT_SPECIFIED][] = array('value' => 10); -// $entity_2->field_unlimited[Language::LANGCODE_NOT_SPECIFIED][] = array('value' => 11); -// field_test_entity_save($entity_2); -// -// // Display the 'combined form'. -// $this->drupalGet('test-entity/nested/1/2'); -// $this->assertFieldByName('field_single[und][0][value]', 0, 'Entity 1: field_single value appears correctly is the form.'); -// $this->assertFieldByName('field_unlimited[und][0][value]', 1, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); -// $this->assertFieldByName('entity_2[field_single][und][0][value]', 10, 'Entity 2: field_single value appears correctly is the form.'); -// $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); -// -// // Submit the form and check that the entities are updated accordingly. -// $edit = array( -// 'field_single[und][0][value]' => 1, -// 'field_unlimited[und][0][value]' => 2, -// 'field_unlimited[und][1][value]' => 3, -// 'entity_2[field_single][und][0][value]' => 11, -// 'entity_2[field_unlimited][und][0][value]' => 12, -// 'entity_2[field_unlimited][und][1][value]' => 13, -// ); -// $this->drupalPost(NULL, $edit, t('Save')); -// field_cache_clear(); -// $entity_1 = field_test_create_entity(1); -// $entity_2 = field_test_create_entity(2); -// $this->assertFieldValues($entity_1, 'field_single', Language::LANGCODE_NOT_SPECIFIED, array(1)); -// $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(2, 3)); -// $this->assertFieldValues($entity_2, 'field_single', Language::LANGCODE_NOT_SPECIFIED, array(11)); -// $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(12, 13)); -// -// // Submit invalid values and check that errors are reported on the -// // correct widgets. -// $edit = array( -// 'field_unlimited[und][1][value]' => -1, -// ); -// $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); -// $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 1: the field validation error was reported.'); -// $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-field-unlimited-und-1-value')); -// $this->assertTrue($error_field, 'Entity 1: the error was flagged on the correct element.'); -// $edit = array( -// 'entity_2[field_unlimited][und][1][value]' => -1, -// ); -// $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); -// $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 2: the field validation error was reported.'); -// $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-entity-2-field-unlimited-und-1-value')); -// $this->assertTrue($error_field, 'Entity 2: the error was flagged on the correct element.'); -// -// // Test that reordering works on both entities. -// $edit = array( -// 'field_unlimited[und][0][_weight]' => 0, -// 'field_unlimited[und][1][_weight]' => -1, -// 'entity_2[field_unlimited][und][0][_weight]' => 0, -// 'entity_2[field_unlimited][und][1][_weight]' => -1, -// ); -// $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); -// field_cache_clear(); -// $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(3, 2)); -// $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(13, 12)); -// -// // Test the 'add more' buttons. Only Ajax submission is tested, because -// // the two 'add more' buttons present in the form have the same #value, -// // which confuses drupalPost(). -// // 'Add more' button in the first entity: -// $this->drupalGet('test-entity/nested/1/2'); -// $this->drupalPostAJAX(NULL, array(), 'field_unlimited_add_more'); -// $this->assertFieldByName('field_unlimited[und][0][value]', 3, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); -// $this->assertFieldByName('field_unlimited[und][1][value]', 2, 'Entity 1: field_unlimited value 1 appears correctly is the form.'); -// $this->assertFieldByName('field_unlimited[und][2][value]', '', 'Entity 1: field_unlimited value 2 appears correctly is the form.'); -// $this->assertFieldByName('field_unlimited[und][3][value]', '', 'Entity 1: an empty widget was added for field_unlimited value 3.'); -// // 'Add more' button in the first entity (changing field values): -// $edit = array( -// 'entity_2[field_unlimited][und][0][value]' => 13, -// 'entity_2[field_unlimited][und][1][value]' => 14, -// 'entity_2[field_unlimited][und][2][value]' => 15, -// ); -// $this->drupalPostAJAX(NULL, $edit, 'entity_2_field_unlimited_add_more'); -// $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 13, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); -// $this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, 'Entity 2: field_unlimited value 1 appears correctly is the form.'); -// $this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, 'Entity 2: field_unlimited value 2 appears correctly is the form.'); -// $this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', 'Entity 2: an empty widget was added for field_unlimited value 3.'); -// // Save the form and check values are saved correclty. -// $this->drupalPost(NULL, array(), t('Save')); -// field_cache_clear(); -// $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(3, 2)); -// $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(13, 14, 15)); -// } - - /** * Tests the Hidden widget. */ function testFieldFormHiddenWidget() { --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Tests/NestedFormTest.php @@ -0,0 +1,197 @@ + 'Nested form tests', + 'description' => 'Test nested form handling.', + 'group' => 'Field API', + ); + } + + public function setUp() { + parent::setUp(); + + $web_user = $this->drupalCreateUser(array('view test entity', 'administer entity_test content')); + $this->drupalLogin($web_user); + + $this->field_single = array('field_name' => 'field_single', 'type' => 'test_field'); + $this->field_unlimited = array('field_name' => 'field_unlimited', 'type' => 'test_field', 'cardinality' => FIELD_CARDINALITY_UNLIMITED); + + $this->instance = array( + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + 'label' => $this->randomName() . '_label', + 'description' => '[site:name]_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + ); + } + + /** + * Tests Field API form integration within a subform. + */ + function testNestedFieldForm() { + // Add two instances on the 'entity_test' + field_create_field($this->field_single); + field_create_field($this->field_unlimited); + $this->instance['field_name'] = 'field_single'; + $this->instance['label'] = 'Single field'; + field_create_instance($this->instance); + entity_get_form_display($this->instance['entity_type'], $this->instance['bundle'], 'default') + ->setComponent($this->instance['field_name']) + ->save(); + $this->instance['field_name'] = 'field_unlimited'; + $this->instance['label'] = 'Unlimited field'; + field_create_instance($this->instance); + entity_get_form_display($this->instance['entity_type'], $this->instance['bundle'], 'default') + ->setComponent($this->instance['field_name']) + ->save(); + + // Create two entities. + $entity_type = 'entity_test'; + $entity_1 = entity_create($entity_type, array('id' => 1)); + $entity_1->enforceIsNew(); + $entity_1->field_single->value = 0; + $entity_1->field_unlimited->value = 1; + $entity_1->save(); + + $entity_2 = entity_create($entity_type, array('id' => 2)); + $entity_2->enforceIsNew(); + $entity_2->field_single->value = 10; + $entity_2->field_unlimited->value = 11; + $entity_2->save(); + + // Display the 'combined form'. + $this->drupalGet('test-entity/nested/1/2'); + $this->assertFieldByName('field_single[und][0][value]', 0, 'Entity 1: field_single value appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][0][value]', 1, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_single][und][0][value]', 10, 'Entity 2: field_single value appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); + + // Submit the form and check that the entities are updated accordingly. + $edit = array( + 'field_single[und][0][value]' => 1, + 'field_unlimited[und][0][value]' => 2, + 'field_unlimited[und][1][value]' => 3, + 'entity_2[field_single][und][0][value]' => 11, + 'entity_2[field_unlimited][und][0][value]' => 12, + 'entity_2[field_unlimited][und][1][value]' => 13, + ); + $this->drupalPost(NULL, $edit, t('Save')); + field_cache_clear(); + $entity_1 = entity_load($entity_type, 1); + $entity_2 = entity_load($entity_type, 2); + $this->assertFieldValues($entity_1, 'field_single', Language::LANGCODE_NOT_SPECIFIED, array(1)); + $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(2, 3)); + $this->assertFieldValues($entity_2, 'field_single', Language::LANGCODE_NOT_SPECIFIED, array(11)); + $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(12, 13)); + + // Submit invalid values and check that errors are reported on the + // correct widgets. + $edit = array( + 'field_unlimited[und][1][value]' => -1, + ); + $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); + $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 1: the field validation error was reported.'); + $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-field-unlimited-und-1-value')); + $this->assertTrue($error_field, 'Entity 1: the error was flagged on the correct element.'); + $edit = array( + 'entity_2[field_unlimited][und][1][value]' => -1, + ); + $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); + $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 2: the field validation error was reported.'); + $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-entity-2-field-unlimited-und-1-value')); + $this->assertTrue($error_field, 'Entity 2: the error was flagged on the correct element.'); + + // Test that reordering works on both entities. + $edit = array( + 'field_unlimited[und][0][_weight]' => 0, + 'field_unlimited[und][1][_weight]' => -1, + 'entity_2[field_unlimited][und][0][_weight]' => 0, + 'entity_2[field_unlimited][und][1][_weight]' => -1, + ); + $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); + field_cache_clear(); + $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(3, 2)); + $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(13, 12)); + + // Test the 'add more' buttons. Only Ajax submission is tested, because + // the two 'add more' buttons present in the form have the same #value, + // which confuses drupalPost(). + // 'Add more' button in the first entity: + $this->drupalGet('test-entity/nested/1/2'); + $this->drupalPostAJAX(NULL, array(), 'field_unlimited_add_more'); + $this->assertFieldByName('field_unlimited[und][0][value]', 3, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][1][value]', 2, 'Entity 1: field_unlimited value 1 appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][2][value]', '', 'Entity 1: field_unlimited value 2 appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][3][value]', '', 'Entity 1: an empty widget was added for field_unlimited value 3.'); + // 'Add more' button in the first entity (changing field values): + $edit = array( + 'entity_2[field_unlimited][und][0][value]' => 13, + 'entity_2[field_unlimited][und][1][value]' => 14, + 'entity_2[field_unlimited][und][2][value]' => 15, + ); + $this->drupalPostAJAX(NULL, $edit, 'entity_2_field_unlimited_add_more'); + $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 13, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, 'Entity 2: field_unlimited value 1 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, 'Entity 2: field_unlimited value 2 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', 'Entity 2: an empty widget was added for field_unlimited value 3.'); + // Save the form and check values are saved correclty. + $this->drupalPost(NULL, array(), t('Save')); + field_cache_clear(); + $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(3, 2)); + $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(13, 14, 15)); + } + + /** + * Assert that a field has the expected values in an entity. + * + * This function only checks a single column in the field values. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to test. + * @param string $field_name + * The name of the field to test + * @param string $langcode + * The language code for the values. + * @param array $expected_values + * The array of expected values. + * @param string $column + * (Optional) the name of the column to check. + */ + function assertFieldValues(EntityInterface $entity, $field_name, $langcode, $expected_values, $column = 'value') { + // Re-load the entity to make sure we have the latest changes. + entity_get_controller($entity->entityType())->resetCache(array($entity->id())); + $e = entity_load($entity->entityType(), $entity->id()); + $field = $values = $e->getTranslation($langcode, FALSE)->$field_name; + // Filter out empty values so that they don't mess with the assertions. + $field->filterEmptyValues(); + $values = $field->getValue(); + $this->assertEqual(count($values), count($expected_values), 'Expected number of values were saved.'); + foreach ($expected_values as $key => $value) { + $this->assertEqual($values[$key][$column], $value, format_string('Value @value was saved correctly.', array('@value' => $value))); + } + } + +} only in patch2: unchanged: --- a/core/modules/field/tests/modules/field_test/field_test.entity.inc +++ b/core/modules/field/tests/modules/field_test/field_test.entity.inc @@ -221,10 +221,10 @@ function field_test_entity_edit(TestEntity $entity) { */ function field_test_entity_nested_form($form, &$form_state, $entity_1, $entity_2) { // First entity. - foreach (array('ftid', 'ftvid', 'fttype') as $key) { + foreach (array('id', 'type') as $key) { $form[$key] = array( '#type' => 'value', - '#value' => $entity_1->$key, + '#value' => $entity_1->$key->value, ); } $form_state['form_display'] = entity_get_form_display($entity_1->entityType(), $entity_1->bundle(), 'default'); @@ -238,10 +238,10 @@ function field_test_entity_nested_form($form, &$form_state, $entity_1, $entity_2 '#parents' => array('entity_2'), '#weight' => 50, ); - foreach (array('ftid', 'ftvid', 'fttype') as $key) { + foreach (array('id', 'type') as $key) { $form['entity_2'][$key] = array( '#type' => 'value', - '#value' => $entity_2->$key, + '#value' => $entity_2->$key->value, ); } $form_state['form_display'] = entity_get_form_display($entity_1->entityType(), $entity_1->bundle(), 'default'); @@ -260,11 +260,11 @@ function field_test_entity_nested_form($form, &$form_state, $entity_1, $entity_2 * Validate handler for field_test_entity_nested_form(). */ function field_test_entity_nested_form_validate($form, &$form_state) { - $entity_1 = entity_create('test_entity', $form_state['values']); + $entity_1 = entity_create('entity_test', $form_state['values']); field_attach_extract_form_values($entity_1, $form, $form_state); field_attach_form_validate($entity_1, $form, $form_state); - $entity_2 = entity_create('test_entity', $form_state['values']['entity_2']); + $entity_2 = entity_create('entity_test', $form_state['values']['entity_2']); field_attach_extract_form_values($entity_2, $form['entity_2'], $form_state); field_attach_form_validate($entity_2, $form['entity_2'], $form_state); } @@ -273,13 +273,13 @@ function field_test_entity_nested_form_validate($form, &$form_state) { * Submit handler for field_test_entity_nested_form(). */ function field_test_entity_nested_form_submit($form, &$form_state) { - $entity_1 = entity_create('test_entity', $form_state['values']); + $entity_1 = entity_create('entity_test', $form_state['values']); field_attach_extract_form_values($entity_1, $form, $form_state); field_test_entity_save($entity_1); - $entity_2 = entity_create('test_entity', $form_state['values']['entity_2']); + $entity_2 = entity_create('entity_test', $form_state['values']['entity_2']); field_attach_extract_form_values($entity_2, $form['entity_2'], $form_state); field_test_entity_save($entity_2); - drupal_set_message(t('test_entities @id_1 and @id_2 have been updated.', array('@id_1' => $entity_1->ftid, '@id_2' => $entity_2->ftid))); + drupal_set_message(t('test_entities @id_1 and @id_2 have been updated.', array('@id_1' => $entity_1->id(), '@id_2' => $entity_2->id()))); } only in patch2: unchanged: --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -63,11 +63,11 @@ function field_test_menu() { 'type' => MENU_NORMAL_ITEM, ); - $items['test-entity/nested/%field_test_entity_test/%field_test_entity_test'] = array( + $items['test-entity/nested/%entity_test/%entity_test'] = array( 'title' => 'Nested entity form', 'page callback' => 'drupal_get_form', 'page arguments' => array('field_test_entity_nested_form', 2, 3), - 'access arguments' => array('administer field_test content'), + 'access arguments' => array('administer entity_test content'), 'type' => MENU_NORMAL_ITEM, );