Newer
Older
Alex Pott
committed
<?php
namespace Drupal\Core\Entity\Entity;
Alex Pott
committed
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Entity\EntityDisplayPluginCollection;
Alex Pott
committed
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\EntityDisplayBase;
Dries Buytaert
committed
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
Alex Pott
committed
/**
* Configuration entity that contains widget options for all components of a
* entity form in a given form mode.
*
Alex Pott
committed
* @ConfigEntityType(
Alex Pott
committed
* id = "entity_form_display",
* label = @Translation("Entity form display"),
* entity_keys = {
* "id" = "id",
* "status" = "status"
* },
* handlers = {
* "access" = "\Drupal\Core\Entity\Entity\Access\EntityFormDisplayAccessControlHandler",
* },
* config_export = {
* "id",
* "targetEntityType",
* "bundle",
* "mode",
* "content",
* "hidden",
Alex Pott
committed
* }
* )
*/
class EntityFormDisplay extends EntityDisplayBase implements EntityFormDisplayInterface {
Alex Pott
committed
/**
* {@inheritdoc}
*/
protected $displayContext = 'form';
/**
* Returns the entity_form_display object used to build an entity form.
*
* Depending on the configuration of the form mode for the entity bundle, this
Alex Pott
committed
* can be either the display object associated with the form mode, or the
* 'default' display.
*
* This method should only be used internally when rendering an entity form.
* When assigning suggested display options for a component in a given form
* mode, entity_get_form_display() should be used instead, in order to avoid
* inadvertently modifying the output of other form modes that might happen to
* use the 'default' display too. Those options will then be effectively
* applied only if the form mode is configured to use them.
*
* hook_entity_form_display_alter() is invoked on each display, allowing 3rd
* party code to alter the display options held in the display before they are
* used to generate render arrays.
*
Alex Pott
committed
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity for which the form is being built.
* @param string $form_mode
* The form mode.
*
* @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface
* The display object that should be used to build the entity form.
*
* @see entity_get_form_display()
* @see hook_entity_form_display_alter()
*/
Alex Pott
committed
public static function collectRenderDisplay(FieldableEntityInterface $entity, $form_mode) {
$entity_type = $entity->getEntityTypeId();
$bundle = $entity->bundle();
// Check the existence and status of:
// - the display for the form mode,
// - the 'default' display.
if ($form_mode != 'default') {
$candidate_ids[] = $entity_type . '.' . $bundle . '.' . $form_mode;
}
$candidate_ids[] = $entity_type . '.' . $bundle . '.default';
$results = \Drupal::entityQuery('entity_form_display')
->condition('id', $candidate_ids)
->condition('status', TRUE)
->execute();
// Load the first valid candidate display, if any.
catch
committed
$storage = \Drupal::entityManager()->getStorage('entity_form_display');
foreach ($candidate_ids as $candidate_id) {
if (isset($results[$candidate_id])) {
$display = $storage->load($candidate_id);
break;
}
}
// Else create a fresh runtime object.
if (empty($display)) {
$display = $storage->create([
'targetEntityType' => $entity_type,
'bundle' => $bundle,
'mode' => $form_mode,
'status' => TRUE,
]);
}
// Let the display know which form mode was originally requested.
$display->originalMode = $form_mode;
// Let modules alter the display.
$display_context = [
'entity_type' => $entity_type,
'bundle' => $bundle,
'form_mode' => $form_mode,
];
\Drupal::moduleHandler()->alter('entity_form_display', $display, $display_context);
return $display;
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
$this->pluginManager = \Drupal::service('plugin.manager.field.widget');
parent::__construct($values, $entity_type);
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
public function getRenderer($field_name) {
Alex Pott
committed
if (isset($this->plugins[$field_name])) {
return $this->plugins[$field_name];
}
// Instantiate the widget object from the stored display properties.
if (($configuration = $this->getComponent($field_name)) && isset($configuration['type']) && ($definition = $this->getFieldDefinition($field_name))) {
$widget = $this->pluginManager->getInstance([
'field_definition' => $definition,
Alex Pott
committed
'form_mode' => $this->originalMode,
// No need to prepare, defaults have been merged in setComponent().
'prepare' => FALSE,
'configuration' => $configuration
]);
Alex Pott
committed
}
else {
$widget = NULL;
}
// Persist the widget object.
$this->plugins[$field_name] = $widget;
return $widget;
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
public function buildForm(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state) {
// Set #parents to 'top-level' by default.
$form += ['#parents' => []];
// Let each widget generate the form elements.
foreach ($this->getComponents() as $name => $options) {
if ($widget = $this->getRenderer($name)) {
$items = $entity->get($name);
$items->filterEmptyItems();
$form[$name] = $widget->form($items, $form, $form_state);
$form[$name]['#access'] = $items->access('edit');
// Assign the correct weight. This duplicates the reordering done in
// processForm(), but is needed for other forms calling this method
// directly.
$form[$name]['#weight'] = $options['weight'];
Alex Pott
committed
// Associate the cache tags for the field definition & field storage
// definition.
$field_definition = $this->getFieldDefinition($name);
$this->renderer->addCacheableDependency($form[$name], $field_definition);
$this->renderer->addCacheableDependency($form[$name], $field_definition->getFieldStorageDefinition());
}
}
Alex Pott
committed
// Associate the cache tags for the form display.
$this->renderer->addCacheableDependency($form, $this);
Alex Pott
committed
// Add a process callback so we can assign weights and hide extra fields.
$form['#process'][] = [$this, 'processForm'];
}
/**
* Process callback: assigns weights and hides extra fields.
*
* @see \Drupal\Core\Entity\Entity\EntityFormDisplay::buildForm()
*/
Alex Pott
committed
public function processForm($element, FormStateInterface $form_state, $form) {
// Assign the weights configured in the form display.
foreach ($this->getComponents() as $name => $options) {
if (isset($element[$name])) {
$element[$name]['#weight'] = $options['weight'];
}
}
// Hide extra fields.
$extra_fields = \Drupal::entityManager()->getExtraFields($this->targetEntityType, $this->bundle);
$extra_fields = isset($extra_fields['form']) ? $extra_fields['form'] : [];
foreach ($extra_fields as $extra_field => $info) {
if (!$this->getComponent($extra_field)) {
$element[$extra_field]['#access'] = FALSE;
}
}
return $element;
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
public function extractFormValues(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state) {
$extracted = [];
foreach ($entity as $name => $items) {
if ($widget = $this->getRenderer($name)) {
$widget->extractFormValues($items, $form, $form_state);
$extracted[$name] = $name;
}
}
return $extracted;
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
public function validateFormValues(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state) {
$violations = $entity->validate();
$violations->filterByFieldAccess();
// Flag entity level violations.
foreach ($violations->getEntityViolations() as $violation) {
/** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
$form_state->setError($form, $violation->getMessage());
}
$this->flagWidgetsErrorsFromViolations($violations, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function flagWidgetsErrorsFromViolations(EntityConstraintViolationListInterface $violations, array &$form, FormStateInterface $form_state) {
$entity = $violations->getEntity();
foreach ($violations->getFieldNames() as $field_name) {
// Only show violations for fields that actually appear in the form, and
// let the widget assign the violations to the correct form elements.
if ($widget = $this->getRenderer($field_name)) {
$field_violations = $this->movePropertyPathViolationsRelativeToField($field_name, $violations->getByField($field_name));
$widget->flagErrors($entity->get($field_name), $field_violations, $form, $form_state);
}
}
}
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/**
* Moves the property path to be relative to field level.
*
* @param string $field_name
* The field name.
* @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
* The violations.
*
* @return \Symfony\Component\Validator\ConstraintViolationList
* A new constraint violation list with the changed property path.
*/
protected function movePropertyPathViolationsRelativeToField($field_name, ConstraintViolationListInterface $violations) {
$new_violations = new ConstraintViolationList();
foreach ($violations as $violation) {
// All the logic below is necessary to change the property path of the
// violations to be relative to the item list, so like title.0.value gets
// changed to 0.value. Sadly constraints in Symfony don't have setters so
// we have to create new objects.
/** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
// Create a new violation object with just a different property path.
$violation_path = $violation->getPropertyPath();
$path_parts = explode('.', $violation_path);
if ($path_parts[0] === $field_name) {
unset($path_parts[0]);
}
$new_path = implode('.', $path_parts);
$constraint = NULL;
$cause = NULL;
$parameters = [];
$plural = NULL;
if ($violation instanceof ConstraintViolation) {
$constraint = $violation->getConstraint();
$cause = $violation->getCause();
$parameters = $violation->getParameters();
$plural = $violation->getPlural();
}
$new_violation = new ConstraintViolation(
$violation->getMessage(),
$violation->getMessageTemplate(),
$parameters,
$violation->getRoot(),
$new_path,
$violation->getInvalidValue(),
$plural,
$violation->getCode(),
$constraint,
$cause
);
$new_violations->add($new_violation);
}
return $new_violations;
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
$configurations = [];
foreach ($this->getComponents() as $field_name => $configuration) {
if (!empty($configuration['type']) && ($field_definition = $this->getFieldDefinition($field_name))) {
$configurations[$configuration['type']] = $configuration + [
'field_definition' => $field_definition,
'form_mode' => $this->mode,
];
}
}
return [
'widgets' => new EntityDisplayPluginCollection($this->pluginManager, $configurations)
];
}