faa6d0d6af22c038c17c58fdb5cfdc1a78a7e710
[yaffs-website] / web / core / lib / Drupal / Core / Entity / Entity / EntityFormDisplay.php
1 <?php
2
3 namespace Drupal\Core\Entity\Entity;
4
5 use Drupal\Core\Entity\EntityConstraintViolationListInterface;
6 use Drupal\Core\Entity\EntityDisplayPluginCollection;
7 use Drupal\Core\Entity\FieldableEntityInterface;
8 use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
9 use Drupal\Core\Entity\EntityDisplayBase;
10 use Drupal\Core\Form\FormStateInterface;
11 use Symfony\Component\Validator\ConstraintViolation;
12 use Symfony\Component\Validator\ConstraintViolationList;
13 use Symfony\Component\Validator\ConstraintViolationListInterface;
14
15 /**
16  * Configuration entity that contains widget options for all components of a
17  * entity form in a given form mode.
18  *
19  * @ConfigEntityType(
20  *   id = "entity_form_display",
21  *   label = @Translation("Entity form display"),
22  *   entity_keys = {
23  *     "id" = "id",
24  *     "status" = "status"
25  *   },
26  *   config_export = {
27  *     "id",
28  *     "targetEntityType",
29  *     "bundle",
30  *     "mode",
31  *     "content",
32  *     "hidden",
33  *   }
34  * )
35  */
36 class EntityFormDisplay extends EntityDisplayBase implements EntityFormDisplayInterface {
37
38   /**
39    * {@inheritdoc}
40    */
41   protected $displayContext = 'form';
42
43   /**
44    * Returns the entity_form_display object used to build an entity form.
45    *
46    * Depending on the configuration of the form mode for the entity bundle, this
47    * can be either the display object associated with the form mode, or the
48    * 'default' display.
49    *
50    * This method should only be used internally when rendering an entity form.
51    * When assigning suggested display options for a component in a given form
52    * mode, entity_get_form_display() should be used instead, in order to avoid
53    * inadvertently modifying the output of other form modes that might happen to
54    * use the 'default' display too. Those options will then be effectively
55    * applied only if the form mode is configured to use them.
56    *
57    * hook_entity_form_display_alter() is invoked on each display, allowing 3rd
58    * party code to alter the display options held in the display before they are
59    * used to generate render arrays.
60    *
61    * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
62    *   The entity for which the form is being built.
63    * @param string $form_mode
64    *   The form mode.
65    *
66    * @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface
67    *   The display object that should be used to build the entity form.
68    *
69    * @see entity_get_form_display()
70    * @see hook_entity_form_display_alter()
71    */
72   public static function collectRenderDisplay(FieldableEntityInterface $entity, $form_mode) {
73     $entity_type = $entity->getEntityTypeId();
74     $bundle = $entity->bundle();
75
76     // Check the existence and status of:
77     // - the display for the form mode,
78     // - the 'default' display.
79     if ($form_mode != 'default') {
80       $candidate_ids[] = $entity_type . '.' . $bundle . '.' . $form_mode;
81     }
82     $candidate_ids[] = $entity_type . '.' . $bundle . '.default';
83     $results = \Drupal::entityQuery('entity_form_display')
84       ->condition('id', $candidate_ids)
85       ->condition('status', TRUE)
86       ->execute();
87
88     // Load the first valid candidate display, if any.
89     $storage = \Drupal::entityManager()->getStorage('entity_form_display');
90     foreach ($candidate_ids as $candidate_id) {
91       if (isset($results[$candidate_id])) {
92         $display = $storage->load($candidate_id);
93         break;
94       }
95     }
96     // Else create a fresh runtime object.
97     if (empty($display)) {
98       $display = $storage->create([
99         'targetEntityType' => $entity_type,
100         'bundle' => $bundle,
101         'mode' => $form_mode,
102         'status' => TRUE,
103       ]);
104     }
105
106     // Let the display know which form mode was originally requested.
107     $display->originalMode = $form_mode;
108
109     // Let modules alter the display.
110     $display_context = [
111       'entity_type' => $entity_type,
112       'bundle' => $bundle,
113       'form_mode' => $form_mode,
114     ];
115     \Drupal::moduleHandler()->alter('entity_form_display', $display, $display_context);
116
117     return $display;
118   }
119
120   /**
121    * {@inheritdoc}
122    */
123   public function __construct(array $values, $entity_type) {
124     $this->pluginManager = \Drupal::service('plugin.manager.field.widget');
125
126     parent::__construct($values, $entity_type);
127   }
128
129   /**
130    * {@inheritdoc}
131    */
132   public function getRenderer($field_name) {
133     if (isset($this->plugins[$field_name])) {
134       return $this->plugins[$field_name];
135     }
136
137     // Instantiate the widget object from the stored display properties.
138     if (($configuration = $this->getComponent($field_name)) && isset($configuration['type']) && ($definition = $this->getFieldDefinition($field_name))) {
139       $widget = $this->pluginManager->getInstance([
140         'field_definition' => $definition,
141         'form_mode' => $this->originalMode,
142         // No need to prepare, defaults have been merged in setComponent().
143         'prepare' => FALSE,
144         'configuration' => $configuration
145       ]);
146     }
147     else {
148       $widget = NULL;
149     }
150
151     // Persist the widget object.
152     $this->plugins[$field_name] = $widget;
153     return $widget;
154   }
155
156   /**
157    * {@inheritdoc}
158    */
159   public function buildForm(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state) {
160     // Set #parents to 'top-level' by default.
161     $form += ['#parents' => []];
162
163     // Let each widget generate the form elements.
164     foreach ($this->getComponents() as $name => $options) {
165       if ($widget = $this->getRenderer($name)) {
166         $items = $entity->get($name);
167         $items->filterEmptyItems();
168         $form[$name] = $widget->form($items, $form, $form_state);
169         $form[$name]['#access'] = $items->access('edit');
170
171         // Assign the correct weight. This duplicates the reordering done in
172         // processForm(), but is needed for other forms calling this method
173         // directly.
174         $form[$name]['#weight'] = $options['weight'];
175
176         // Associate the cache tags for the field definition & field storage
177         // definition.
178         $field_definition = $this->getFieldDefinition($name);
179         $this->renderer->addCacheableDependency($form[$name], $field_definition);
180         $this->renderer->addCacheableDependency($form[$name], $field_definition->getFieldStorageDefinition());
181       }
182     }
183
184     // Associate the cache tags for the form display.
185     $this->renderer->addCacheableDependency($form, $this);
186
187     // Add a process callback so we can assign weights and hide extra fields.
188     $form['#process'][] = [$this, 'processForm'];
189   }
190
191   /**
192    * Process callback: assigns weights and hides extra fields.
193    *
194    * @see \Drupal\Core\Entity\Entity\EntityFormDisplay::buildForm()
195    */
196   public function processForm($element, FormStateInterface $form_state, $form) {
197     // Assign the weights configured in the form display.
198     foreach ($this->getComponents() as $name => $options) {
199       if (isset($element[$name])) {
200         $element[$name]['#weight'] = $options['weight'];
201       }
202     }
203
204     // Hide extra fields.
205     $extra_fields = \Drupal::entityManager()->getExtraFields($this->targetEntityType, $this->bundle);
206     $extra_fields = isset($extra_fields['form']) ? $extra_fields['form'] : [];
207     foreach ($extra_fields as $extra_field => $info) {
208       if (!$this->getComponent($extra_field)) {
209         $element[$extra_field]['#access'] = FALSE;
210       }
211     }
212     return $element;
213   }
214
215   /**
216    * {@inheritdoc}
217    */
218   public function extractFormValues(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state) {
219     $extracted = [];
220     foreach ($entity as $name => $items) {
221       if ($widget = $this->getRenderer($name)) {
222         $widget->extractFormValues($items, $form, $form_state);
223         $extracted[$name] = $name;
224       }
225     }
226     return $extracted;
227   }
228
229   /**
230    * {@inheritdoc}
231    */
232   public function validateFormValues(FieldableEntityInterface $entity, array &$form, FormStateInterface $form_state) {
233     $violations = $entity->validate();
234     $violations->filterByFieldAccess();
235
236     // Flag entity level violations.
237     foreach ($violations->getEntityViolations() as $violation) {
238       /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
239       $form_state->setError($form, $violation->getMessage());
240     }
241
242     $this->flagWidgetsErrorsFromViolations($violations, $form, $form_state);
243   }
244
245   /**
246    * {@inheritdoc}
247    */
248   public function flagWidgetsErrorsFromViolations(EntityConstraintViolationListInterface $violations, array &$form, FormStateInterface $form_state) {
249     $entity = $violations->getEntity();
250     foreach ($violations->getFieldNames() as $field_name) {
251       // Only show violations for fields that actually appear in the form, and
252       // let the widget assign the violations to the correct form elements.
253       if ($widget = $this->getRenderer($field_name)) {
254         $field_violations = $this->movePropertyPathViolationsRelativeToField($field_name, $violations->getByField($field_name));
255         $widget->flagErrors($entity->get($field_name), $field_violations, $form, $form_state);
256       }
257     }
258   }
259
260   /**
261    * Moves the property path to be relative to field level.
262    *
263    * @param string $field_name
264    *   The field name.
265    * @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
266    *   The violations.
267    *
268    * @return \Symfony\Component\Validator\ConstraintViolationList
269    *   A new constraint violation list with the changed property path.
270    */
271   protected function movePropertyPathViolationsRelativeToField($field_name, ConstraintViolationListInterface $violations) {
272     $new_violations = new ConstraintViolationList();
273     foreach ($violations as $violation) {
274       // All the logic below is necessary to change the property path of the
275       // violations to be relative to the item list, so like title.0.value gets
276       // changed to 0.value. Sadly constraints in Symfony don't have setters so
277       // we have to create new objects.
278       /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
279       // Create a new violation object with just a different property path.
280       $violation_path = $violation->getPropertyPath();
281       $path_parts = explode('.', $violation_path);
282       if ($path_parts[0] === $field_name) {
283         unset($path_parts[0]);
284       }
285       $new_path = implode('.', $path_parts);
286
287       $constraint = NULL;
288       $cause = NULL;
289       $parameters = [];
290       $plural = NULL;
291       if ($violation instanceof ConstraintViolation) {
292         $constraint = $violation->getConstraint();
293         $cause = $violation->getCause();
294         $parameters = $violation->getParameters();
295         $plural = $violation->getPlural();
296       }
297
298       $new_violation = new ConstraintViolation(
299         $violation->getMessage(),
300         $violation->getMessageTemplate(),
301         $parameters,
302         $violation->getRoot(),
303         $new_path,
304         $violation->getInvalidValue(),
305         $plural,
306         $violation->getCode(),
307         $constraint,
308         $cause
309       );
310       $new_violations->add($new_violation);
311     }
312     return $new_violations;
313   }
314
315   /**
316    * {@inheritdoc}
317    */
318   public function getPluginCollections() {
319     $configurations = [];
320     foreach ($this->getComponents() as $field_name => $configuration) {
321       if (!empty($configuration['type']) && ($field_definition = $this->getFieldDefinition($field_name))) {
322         $configurations[$configuration['type']] = $configuration + [
323           'field_definition' => $field_definition,
324           'form_mode' => $this->mode,
325         ];
326       }
327     }
328
329     return [
330       'widgets' => new EntityDisplayPluginCollection($this->pluginManager, $configurations)
331     ];
332   }
333
334 }