Commit 83b797df authored by catch's avatar catch

Issue #2095195 by yched, fago, Berdir, amateescu, swentel, pfrenssen, Xano:...

Issue #2095195 by yched, fago, Berdir, amateescu, swentel, pfrenssen, Xano: Remove deprecated field_attach_form_*().
parent 0b6e47e2
......@@ -7,7 +7,8 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Language\Language;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\entity\Entity\EntityFormDisplay;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -15,7 +16,7 @@
*
* @see \Drupal\Core\ContentEntityBase
*/
class ContentEntityFormController extends EntityFormController {
class ContentEntityFormController extends EntityFormController implements ContentEntityFormControllerInterface {
/**
* The entity manager.
......@@ -47,16 +48,8 @@ public static function create(ContainerInterface $container) {
* {@inheritdoc}
*/
public function form(array $form, array &$form_state) {
$entity = $this->entity;
// @todo Exploit the Field API to generate the default widgets for the
// entity fields.
if ($entity->getEntityType()->isFieldable()) {
field_attach_form($entity, $form, $form_state, $this->getFormLangcode($form_state));
}
// Add a process callback so we can assign weights and hide extra fields.
$form['#process'][] = array($this, 'processForm');
$form = parent::form($form, $form_state);
$this->getFormDisplay($form_state)->buildForm($this->entity, $form, $form_state);
return $form;
}
......@@ -66,27 +59,7 @@ public function form(array $form, array &$form_state) {
public function validate(array $form, array &$form_state) {
$this->updateFormLangcode($form_state);
$entity = $this->buildEntity($form, $form_state);
$entity_type = $entity->getEntityTypeId();
$entity_langcode = $entity->language()->id;
$violations = array();
foreach ($entity as $field_name => $field) {
$field_violations = $field->validate();
if (count($field_violations)) {
$violations[$field_name] = $field_violations;
}
}
// Map errors back to form elements.
if ($violations) {
foreach ($violations as $field_name => $field_violations) {
$field_state = field_form_get_state($form['#parents'], $field_name, $form_state);
$field_state['constraint_violations'] = $field_violations;
field_form_set_state($form['#parents'], $field_name, $form_state, $field_state);
}
field_invoke_method('flagErrors', _field_invoke_widget_target($form_state['form_display']), $entity, $form, $form_state);
}
$this->getFormDisplay($form_state)->validateFormValues($entity, $form, $form_state);
// @todo Remove this.
// Execute legacy global validation handlers.
......@@ -102,6 +75,10 @@ protected function init(array &$form_state) {
// language.
$langcode = $this->getFormLangcode($form_state);
$this->entity = $this->entity->getTranslation($langcode);
$form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation());
$this->setFormDisplay($form_display, $form_state);
parent::init($form_state);
}
......@@ -128,37 +105,33 @@ public function isDefaultFormLangcode(array $form_state) {
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, array &$form_state) {
$entity = clone $this->entity;
$entity_type_id = $entity->getEntityTypeId();
$entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
// @todo Exploit the Entity Field API to process the submitted field values.
// Copy top-level form values that are entity fields but not handled by
// field API without changing existing entity fields that are not being
// edited by this form. Values of fields handled by field API are copied
// by field_attach_extract_form_values() below.
$values_excluding_fields = $entity_type->isFieldable() ? array_diff_key($form_state['values'], field_info_instances($entity_type_id, $entity->bundle())) : $form_state['values'];
$definitions = $entity->getFieldDefinitions();
foreach ($values_excluding_fields as $key => $value) {
if (isset($definitions[$key])) {
$entity->$key = $value;
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, array &$form_state) {
// First, extract values from widgets.
$extracted = $this->getFormDisplay($form_state)->extractFormValues($entity, $form, $form_state);
// Then extract the values of fields that are not rendered through widgets,
// by simply copying from top-level form values. This leaves the fields
// that are not being edited within this form untouched.
foreach ($form_state['values'] as $name => $values) {
if ($entity->hasField($name) && !isset($extracted[$name])) {
$entity->set($name, $values);
}
}
}
// Invoke all specified builders for copying form values to entity fields.
if (isset($form['#entity_builders'])) {
foreach ($form['#entity_builders'] as $function) {
call_user_func_array($function, array($entity_type_id, $entity, &$form, &$form_state));
}
}
/**
* {@inheritdoc}
*/
public function getFormDisplay(array $form_state) {
return isset($form_state['form_display']) ? $form_state['form_display'] : NULL;
}
// Invoke field API for copying field values.
if ($entity_type->isFieldable()) {
field_attach_extract_form_values($entity, $form, $form_state, array('langcode' => $this->getFormLangcode($form_state)));
}
return $entity;
/**
* {@inheritdoc}
*/
public function setFormDisplay(EntityFormDisplayInterface $form_display, array &$form_state) {
$form_state['form_display'] = $form_display;
return $this;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\ContentEntityFormControllerInterface.
*/
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
/**
* Defines a common interface for content entity form controller classes.
*/
interface ContentEntityFormControllerInterface extends EntityFormControllerInterface {
/**
* Returns the form display.
*
* @param array $form_state
* An associative array containing the current state of the form.
*
* @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface.
* The current form display.
*/
public function getFormDisplay(array $form_state);
/**
* Sets the form display.
*
* Sets the form display which will be used for populating form element
* defaults.
*
* @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display
* The form display that the current form operates with.
* @param array $form_state
* An associative array containing the current state of the form.
*/
public function setFormDisplay(EntityFormDisplayInterface $form_display, array &$form_state);
}
......@@ -7,9 +7,153 @@
namespace Drupal\Core\Entity\Display;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Provides a common interface for entity form displays.
*/
interface EntityFormDisplayInterface extends EntityDisplayInterface {
/**
* Adds field widgets to an entity form.
*
* The form elements for the entity's fields are added by reference as direct
* children in the $form parameter. This parameter can be a full form
* structure (most common case for entity edit forms), or a sub-element of a
* larger form.
*
* By default, submitted field values appear at the top-level of
* $form_state['values']. A different location within $form_state['values']
* can be specified by setting the '#parents' property on the incoming $form
* parameter. Because of name clashes, two instances of the same field cannot
* appear within the same $form element, or within the same '#parents' space.
*
* Sample resulting structure in $form:
* @code
* '#parents' => The location of field values in $form_state['values'],
* '#entity_type' => The name of the entity type,
* '#bundle' => The name of the bundle,
* // One sub-array per field appearing in the entity, keyed by field name.
* // The structure of the array differs slightly depending on whether the
* // widget is 'single-value' (provides the input for one field value,
* // most common case), and will therefore be repeated as many times as
* // needed, or 'multiple-values' (one single widget allows the input of
* // several values, e.g checkboxes, select box...).
* 'field_foo' => array(
* '#access' => TRUE if the current user has 'edit' grants for the field,
* FALSE if not.
* 'widget' => array(
* '#field_name' => The name of the field,
* '#language' => $langcode,
* '#field_parents' => The 'parents' space for the field in the form,
* equal to the #parents property of the $form parameter received by
* this method,
* '#required' => Whether or not the field is required,
* '#title' => The label of the field instance,
* '#description' => The description text for the field instance,
*
* // Only for 'single' widgets:
* '#theme' => 'field_multiple_value_form',
* '#cardinality' => The field cardinality,
* '#cardinality_multiple => TRUE if the field can contain multiple
* items, FALSE otherwise.
* // One sub-array per copy of the widget, keyed by delta.
* 0 => array(
* '#entity_type' => The name of the entity type,
* '#bundle' => The name of the bundle,
* '#field_name' => The name of the field,
* '#field_parents' => The 'parents' space for the field in the form,
* equal to the #parents property of the $form parameter,
* '#title' => The title to be displayed by the widget,
* '#default_value' => The field value for delta 0,
* '#required' => Whether the widget should be marked required,
* '#delta' => 0,
* // The remaining elements in the sub-array depend on the widget.
* '#type' => The type of the widget,
* ...
* ),
* 1 => array(
* ...
* ),
*
* // Only for multiple widgets:
* '#entity_type' => The name of the entity type,
* '#bundle' => $instance['bundle'],
* // The remaining elements in the sub-array depend on the widget.
* '#type' => The type of the widget,
* ...
* ),
* ...
* ),
* )
* @endcode
*
* Additionally, some processing data is placed in $form_state, and can be
* accessed by field_form_get_state() and field_form_set_state().
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity.
* @param array $form
* The form structure to fill in. This can be a full form structure, or a
* sub-element of a larger form. The #parents property can be set to
* control the location of submitted field values within
* $form_state['values']. If not specified, $form['#parents'] is set to an
* empty array, which results in field values located at the top-level of
* $form_state['values'].
* @param array $form_state
* The form state.
*/
public function buildForm(ContentEntityInterface $entity, array &$form, array &$form_state);
/**
* Validates submitted widget values and sets the corresponding form errors.
*
* There are two levels of validation for fields in forms: widget validation
* and field validation.
* - Widget validation steps are specific to a given widget's own form
* structure and UI metaphors. They are executed during normal form
* validation, usually through Form API's #element_validate property.
* Errors reported at this level are typically those that prevent the
* extraction of proper field values from the submitted form input.
* - If no form / widget errors were reported for the field, field validation
* steps are performed according to the "constraints" specified by the
* field definition. Those are independent of the specific widget being
* used in a given form, and are also performed on REST entity submissions.
*
* This function performs field validation in the context of a form submission.
* It reports field constraint violations as form errors on the correct form
* elements.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity.
* @param array $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 array $form_state
* The form state.
*/
public function validateFormValues(ContentEntityInterface $entity, array &$form, array &$form_state);
/**
* Extracts field values from the submitted widget values into the entity.
*
* This accounts for drag-and-drop reordering of field values, and filtering
* of empty values.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity.
* @param array $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 array $form_state
* The form state.
*
* @return array
* An array whose keys and values are the keys of the top-level entries in
* $form_state['values'] that have been processed. The remaining entries, if
* any, do not correspond to widgets and should be extracted manually by
* the caller if needed.
*/
public function extractFormValues(ContentEntityInterface $entity, array &$form, array &$form_state);
}
......@@ -7,10 +7,8 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\entity\Entity\EntityFormDisplay;
/**
* Base class for entity form controllers.
......@@ -122,9 +120,6 @@ protected function init(array &$form_state) {
// Prepare the entity to be presented in the entity form.
$this->prepareEntity();
$form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation());
$this->setFormDisplay($form_display, $form_state);
// Invoke the prepare form hooks.
$this->prepareInvokeAll('entity_prepare_form', $form_state);
$this->prepareInvokeAll($this->entity->getEntityTypeId() . '_prepare_form', $form_state);
......@@ -137,13 +132,8 @@ protected function init(array &$form_state) {
*/
public function form(array $form, array &$form_state) {
$entity = $this->entity;
// @todo Exploit the Field API to generate the default widgets for the
// entity properties.
if ($entity->getEntityType()->isFieldable()) {
field_attach_form($entity, $form, $form_state, $this->getFormLangcode($form_state));
}
// Add a process callback so we can assign weights and hide extra fields.
// Add a process callback.
$form['#process'][] = array($this, 'processForm');
if (!isset($form['langcode'])) {
......@@ -168,25 +158,6 @@ public function processForm($element, $form_state, $form) {
// to the entity object, hence we must restore it.
$this->entity = $form_state['controller']->getEntity();
// Assign the weights configured in the form display.
foreach ($this->getFormDisplay($form_state)->getComponents() as $name => $options) {
if (isset($element[$name])) {
$element[$name]['#weight'] = $options['weight'];
}
}
// Hide or assign weights for extra fields.
$extra_fields = field_info_extra_fields($this->entity->getEntityTypeId(), $this->entity->bundle(), 'form');
foreach ($extra_fields as $extra_field => $info) {
$component = $this->getFormDisplay($form_state)->getComponent($extra_field);
if (!$component) {
$element[$extra_field]['#access'] = FALSE;
}
else {
$element[$extra_field]['#weight'] = $component['weight'];
}
}
return $element;
}
......@@ -359,7 +330,7 @@ public function buildEntity(array $form, array &$form_state) {
// controller of the current request.
$form_state['controller'] = $this;
$this->copyFormValuesToEntity($entity, $form_state);
$this->copyFormValuesToEntity($entity, $form, $form_state);
// Invoke all specified builders for copying form values to entity
// properties.
......@@ -380,10 +351,12 @@ public function buildEntity(array $form, array &$form_state) {
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity the current form should operate upon.
* @param array $form
* A nested array of form elements comprising the form.
* @param array $form_state
* An associative array containing the current state of the form.
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form_state) {
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, array &$form_state) {
// @todo: This relies on a method that only exists for config and content
// entities, in a different way. Consider moving this logic to a config
// entity specific implementation.
......@@ -427,27 +400,12 @@ protected function prepareInvokeAll($hook, array &$form_state) {
if (function_exists($function)) {
// Ensure we pass an updated translation object and form display at
// each invocation, since they depend on form state which is alterable.
$args = array($this->entity, $this->getFormDisplay($form_state), $this->operation, &$form_state);
$args = array($this->entity, $this->operation, &$form_state);
call_user_func_array($function, $args);
}
}
}
/**
* {@inheritdoc}
*/
public function getFormDisplay(array $form_state) {
return isset($form_state['form_display']) ? $form_state['form_display'] : NULL;
}
/**
* {@inheritdoc}
*/
public function setFormDisplay(EntityFormDisplayInterface $form_display, array &$form_state) {
$form_state['form_display'] = $form_display;
return $this;
}
/**
* {@inheritdoc}
*/
......
......@@ -7,7 +7,6 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
......@@ -57,30 +56,6 @@ public function setOperation($operation);
*/
public function getOperation();
/**
* Returns the form display.
*
* @param array $form_state
* An associative array containing the current state of the form.
*
* @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface.
* The current form display.
*/
public function getFormDisplay(array $form_state);
/**
* Sets the form display.
*
* Sets the form display which will be used for populating form element
* defaults.
*
* @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display
* The form display that the current form operates with.
* @param array $form_state
* An associative array containing the current state of the form.
*/
public function setFormDisplay(EntityFormDisplayInterface $form_display, array &$form_state);
/**
* Returns the form entity.
*
......
......@@ -331,15 +331,9 @@ public function defaultValuesFormValidate(array $element, array &$form, array &$
$widget->extractFormValues($this, $element, $form_state);
$violations = $this->validate();
// Assign reported errors to the correct form element.
if (count($violations)) {
// Store reported errors in $form_state.
$field_name = $this->getFieldDefinition()->getName();
$field_state = field_form_get_state($element['#parents'], $field_name, $form_state);
$field_state['constraint_violations'] = $violations;
field_form_set_state($element['#parents'], $field_name, $form_state, $field_state);
// Assign reported errors to the correct form element.
$widget->flagErrors($this, $element, $form_state);
$widget->flagErrors($this, $violations, $element, $form_state);
}
}
......
......@@ -11,6 +11,7 @@
use Drupal\Component\Utility\SortArray;
use Drupal\Component\Utility\String;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* Base class for 'Field widget' plugin implementations.
......@@ -61,7 +62,6 @@ public function form(FieldItemListInterface $items, array &$form, array &$form_s
$field_state = array(
'items_count' => count($items),
'array_parents' => array(),
'constraint_violations' => array(),
);
field_form_set_state($parents, $field_name, $form_state, $field_state);
}
......@@ -355,12 +355,12 @@ public function extractFormValues(FieldItemListInterface $items, array $form, ar
/**
* {@inheritdoc}
*/
public function flagErrors(FieldItemListInterface $items, array $form, array &$form_state) {
public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, array &$form_state) {
$field_name = $this->fieldDefinition->getName();
$field_state = field_form_get_state($form['#parents'], $field_name, $form_state);
if (!empty($field_state['constraint_violations'])) {
if ($violations->count()) {
$form_builder = \Drupal::formBuilder();
// Locate the correct element in the the form.
......@@ -384,7 +384,7 @@ public function flagErrors(FieldItemListInterface $items, array $form, array &$f
$handles_multiple = $this->handlesMultipleValues();
$violations_by_delta = array();
foreach ($field_state['constraint_violations'] as $violation) {
foreach ($violations as $violation) {
// Separate violations by delta.
$property_path = explode('.', $violation->getPropertyPath());
$delta = array_shift($property_path);
......@@ -396,6 +396,7 @@ public function flagErrors(FieldItemListInterface $items, array $form, array &$f
$violation->arrayPropertyPath = $property_path;
}
/** @var \Symfony\Component\Validator\ConstraintViolationInterface[] $delta_violations */
foreach ($violations_by_delta as $delta => $delta_violations) {
// Pass violations to the main element:
// - if this is a multiple-value widget,
......@@ -416,9 +417,6 @@ public function flagErrors(FieldItemListInterface $items, array $form, array &$f
}
}
}
// Reinitialize the errors list for the next submit.
$field_state['constraint_violations'] = array();
field_form_set_state($form['#parents'], $field_name, $form_state, $field_state);
}
}
}
......
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Field;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* Base interface definition for "Field widget" plugins.
*
......@@ -59,12 +61,14 @@ public function extractFormValues(FieldItemListInterface $items, array $form, ar
*
* @param \Drupal\Core\Field\FieldItemListInterface $items
* The field values.
* @param \Symfony\Component\Validator\ConstraintViolationListInterface|\Symfony\Component\Validator\ConstraintViolationInterface[] $violations
* The constraint violations that were detected.
* @param array $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 array $form_state
* The form state.
*/
public function flagErrors(FieldItemListInterface $items, array $form, array &$form_state);
public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, array &$form_state);
}
......@@ -499,7 +499,7 @@ function book_node_predelete(EntityInterface $node) {
/**
* Implements hook_node_prepare_form().
*/
function book_node_prepare_form(NodeInterface $node, $form_display, $operation, array &$form_state) {
function book_node_prepare_form(NodeInterface $node, $operation, array &$form_state) {
// Get BookManager service
$book_manager = \Drupal::service('book.manager');
......
......@@ -1002,7 +1002,7 @@ function datetime_form_node_form_alter(&$form, &$form_state, $form_id) {
/**
* Implements hook_node_prepare_form().
*/
function datetime_node_prepare_form(NodeInterface $node, $form_display, $operation, array &$form_state) {
function datetime_node_prepare_form(NodeInterface $node, $operation, array &$form_state) {
// Prepare the 'Authored on' date to use datetime.
$node->date = DrupalDateTime::createFromTimestamp($node->getCreatedTime());
}
......@@ -88,7 +88,7 @@ public function buildForm(array $form, array &$form_state, EntityInterface $enti
}
// Add the field form.
field_attach_form($form_state['entity'], $form, $form_state, $form_state['langcode'], array('field_name' => $form_state['field_name']));
$form_state['form_display']->buildForm($entity, $form, $form_state);
// Add a dummy changed timestamp field to attach form errors to.
if ($entity instanceof EntityChangedInterface) {
......@@ -128,9 +128,15 @@ protected function init(array &$form_state, EntityInterface $entity, $field_name
$form_state['entity'] = $entity;
$form_state['field_name'] = $field_name;
// @todo Allow the usage of different form modes by exposing a hook and the
// UI for them.