Further Drupal 8.6.4 changes. Some core files were not committed before a commit...
[yaffs-website] / web / core / lib / Drupal / Core / Entity / ContentEntityForm.php
1 <?php
2
3 namespace Drupal\Core\Entity;
4
5 use Drupal\Component\Datetime\TimeInterface;
6 use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
7 use Drupal\Core\Entity\Entity\EntityFormDisplay;
8 use Drupal\Core\Form\FormStateInterface;
9 use Symfony\Component\DependencyInjection\ContainerInterface;
10
11 /**
12  * Entity form variant for content entity types.
13  *
14  * @see \Drupal\Core\ContentEntityBase
15  */
16 class ContentEntityForm extends EntityForm implements ContentEntityFormInterface {
17
18   /**
19    * The entity being used by this form.
20    *
21    * @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\RevisionLogInterface
22    */
23   protected $entity;
24
25   /**
26    * The entity type bundle info service.
27    *
28    * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
29    */
30   protected $entityTypeBundleInfo;
31
32   /**
33    * The time service.
34    *
35    * @var \Drupal\Component\Datetime\TimeInterface
36    */
37   protected $time;
38
39   /**
40    * The entity repository service.
41    *
42    * @var \Drupal\Core\Entity\EntityRepositoryInterface
43    */
44   protected $entityRepository;
45
46   /**
47    * Constructs a ContentEntityForm object.
48    *
49    * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
50    *   The entity repository service.
51    * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
52    *   The entity type bundle service.
53    * @param \Drupal\Component\Datetime\TimeInterface $time
54    *   The time service.
55    */
56   public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
57     if ($entity_repository instanceof EntityManagerInterface) {
58       @trigger_error('Passing the entity.manager service to ContentEntityForm::__construct() is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Pass the entity.repository service instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
59       $this->entityManager = $entity_repository;
60     }
61     $this->entityRepository = $entity_repository;
62     $this->entityTypeBundleInfo = $entity_type_bundle_info ?: \Drupal::service('entity_type.bundle.info');
63     $this->time = $time ?: \Drupal::service('datetime.time');
64   }
65
66   /**
67    * {@inheritdoc}
68    */
69   public static function create(ContainerInterface $container) {
70     return new static(
71       $container->get('entity.repository'),
72       $container->get('entity_type.bundle.info'),
73       $container->get('datetime.time')
74     );
75   }
76
77   /**
78    * {@inheritdoc}
79    */
80   protected function prepareEntity() {
81     parent::prepareEntity();
82
83     // Hide the current revision log message in UI.
84     if ($this->showRevisionUi() && !$this->entity->isNew() && $this->entity instanceof RevisionLogInterface) {
85       $this->entity->setRevisionLogMessage(NULL);
86     }
87   }
88
89   /**
90    * Returns the bundle entity of the entity, or NULL if there is none.
91    *
92    * @return \Drupal\Core\Entity\EntityInterface|null
93    *   The bundle entity.
94    */
95   protected function getBundleEntity() {
96     if ($bundle_entity_type = $this->entity->getEntityType()->getBundleEntityType()) {
97       return $this->entityTypeManager->getStorage($bundle_entity_type)->load($this->entity->bundle());
98     }
99     return NULL;
100   }
101
102   /**
103    * {@inheritdoc}
104    */
105   public function form(array $form, FormStateInterface $form_state) {
106
107     if ($this->showRevisionUi()) {
108       // Advanced tab must be the first, because other fields rely on that.
109       if (!isset($form['advanced'])) {
110         $form['advanced'] = [
111           '#type' => 'vertical_tabs',
112           '#weight' => 99,
113         ];
114       }
115     }
116
117     $form = parent::form($form, $form_state);
118
119     // Content entity forms do not use the parent's #after_build callback
120     // because they only need to rebuild the entity in the validation and the
121     // submit handler because Field API uses its own #after_build callback for
122     // its widgets.
123     unset($form['#after_build']);
124
125     $this->getFormDisplay($form_state)->buildForm($this->entity, $form, $form_state);
126     // Allow modules to act before and after form language is updated.
127     $form['#entity_builders']['update_form_langcode'] = '::updateFormLangcode';
128
129     if ($this->showRevisionUi()) {
130       $this->addRevisionableFormFields($form);
131     }
132
133     $form['footer'] = [
134       '#type' => 'container',
135       '#weight' => 99,
136       '#attributes' => [
137         'class' => ['entity-content-form-footer'],
138       ],
139       '#optional' => TRUE,
140     ];
141
142     return $form;
143   }
144
145   /**
146    * {@inheritdoc}
147    */
148   public function submitForm(array &$form, FormStateInterface $form_state) {
149     parent::submitForm($form, $form_state);
150     // Update the changed timestamp of the entity.
151     $this->updateChangedTime($this->entity);
152   }
153
154   /**
155    * {@inheritdoc}
156    */
157   public function buildEntity(array $form, FormStateInterface $form_state) {
158     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
159     $entity = parent::buildEntity($form, $form_state);
160
161     // Mark the entity as requiring validation.
162     $entity->setValidationRequired(!$form_state->getTemporaryValue('entity_validated'));
163
164     // Save as a new revision if requested to do so.
165     if ($this->showRevisionUi() && !$form_state->isValueEmpty('revision')) {
166       $entity->setNewRevision();
167       if ($entity instanceof RevisionLogInterface) {
168         // If a new revision is created, save the current user as
169         // revision author.
170         $entity->setRevisionUserId($this->currentUser()->id());
171         $entity->setRevisionCreationTime($this->time->getRequestTime());
172       }
173     }
174
175     return $entity;
176   }
177
178   /**
179    * {@inheritdoc}
180    *
181    * Button-level validation handlers are highly discouraged for entity forms,
182    * as they will prevent entity validation from running. If the entity is going
183    * to be saved during the form submission, this method should be manually
184    * invoked from the button-level validation handler, otherwise an exception
185    * will be thrown.
186    */
187   public function validateForm(array &$form, FormStateInterface $form_state) {
188     parent::validateForm($form, $form_state);
189     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
190     $entity = $this->buildEntity($form, $form_state);
191
192     $violations = $entity->validate();
193
194     // Remove violations of inaccessible fields.
195     $violations->filterByFieldAccess($this->currentUser());
196
197     // In case a field-level submit button is clicked, for example the 'Add
198     // another item' button for multi-value fields or the 'Upload' button for a
199     // File or an Image field, make sure that we only keep violations for that
200     // specific field.
201     $edited_fields = [];
202     if ($limit_validation_errors = $form_state->getLimitValidationErrors()) {
203       foreach ($limit_validation_errors as $section) {
204         $field_name = reset($section);
205         if ($entity->hasField($field_name)) {
206           $edited_fields[] = $field_name;
207         }
208       }
209       $edited_fields = array_unique($edited_fields);
210     }
211     else {
212       $edited_fields = $this->getEditedFieldNames($form_state);
213     }
214
215     // Remove violations for fields that are not edited.
216     $violations->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $edited_fields));
217
218     $this->flagViolations($violations, $form, $form_state);
219
220     // The entity was validated.
221     $entity->setValidationRequired(FALSE);
222     $form_state->setTemporaryValue('entity_validated', TRUE);
223
224     return $entity;
225   }
226
227   /**
228    * Gets the names of all fields edited in the form.
229    *
230    * If the entity form customly adds some fields to the form (i.e. without
231    * using the form display), it needs to add its fields here and override
232    * flagViolations() for displaying the violations.
233    *
234    * @param \Drupal\Core\Form\FormStateInterface $form_state
235    *   The current state of the form.
236    *
237    * @return string[]
238    *   An array of field names.
239    */
240   protected function getEditedFieldNames(FormStateInterface $form_state) {
241     return array_keys($this->getFormDisplay($form_state)->getComponents());
242   }
243
244   /**
245    * Flags violations for the current form.
246    *
247    * If the entity form customly adds some fields to the form (i.e. without
248    * using the form display), it needs to add its fields to array returned by
249    * getEditedFieldNames() and overwrite this method in order to show any
250    * violations for those fields; e.g.:
251    * @code
252    * foreach ($violations->getByField('name') as $violation) {
253    *   $form_state->setErrorByName('name', $violation->getMessage());
254    * }
255    * parent::flagViolations($violations, $form, $form_state);
256    * @endcode
257    *
258    * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
259    *   The violations to flag.
260    * @param array $form
261    *   A nested array of form elements comprising the form.
262    * @param \Drupal\Core\Form\FormStateInterface $form_state
263    *   The current state of the form.
264    */
265   protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
266     // Flag entity level violations.
267     foreach ($violations->getEntityViolations() as $violation) {
268       /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
269       $form_state->setErrorByName(str_replace('.', '][', $violation->getPropertyPath()), $violation->getMessage());
270     }
271     // Let the form display flag violations of its fields.
272     $this->getFormDisplay($form_state)->flagWidgetsErrorsFromViolations($violations, $form, $form_state);
273   }
274
275   /**
276    * Initializes the form state and the entity before the first form build.
277    *
278    * @param \Drupal\Core\Form\FormStateInterface $form_state
279    *   The current state of the form.
280    */
281   protected function init(FormStateInterface $form_state) {
282     // Ensure we act on the translation object corresponding to the current form
283     // language.
284     $this->initFormLangcodes($form_state);
285     $langcode = $this->getFormLangcode($form_state);
286     $this->entity = $this->entity->hasTranslation($langcode) ? $this->entity->getTranslation($langcode) : $this->entity->addTranslation($langcode);
287
288     $form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation());
289     $this->setFormDisplay($form_display, $form_state);
290
291     parent::init($form_state);
292   }
293
294   /**
295    * Initializes form language code values.
296    *
297    * @param \Drupal\Core\Form\FormStateInterface $form_state
298    *   The current state of the form.
299    */
300   protected function initFormLangcodes(FormStateInterface $form_state) {
301     // Store the entity default language to allow checking whether the form is
302     // dealing with the original entity or a translation.
303     if (!$form_state->has('entity_default_langcode')) {
304       $form_state->set('entity_default_langcode', $this->entity->getUntranslated()->language()->getId());
305     }
306     // This value might have been explicitly populated to work with a particular
307     // entity translation. If not we fall back to the most proper language based
308     // on contextual information.
309     if (!$form_state->has('langcode')) {
310       // Imply a 'view' operation to ensure users edit entities in the same
311       // language they are displayed. This allows to keep contextual editing
312       // working also for multilingual entities.
313       $form_state->set('langcode', $this->entityRepository->getTranslationFromContext($this->entity)->language()->getId());
314     }
315   }
316
317   /**
318    * {@inheritdoc}
319    */
320   public function getFormLangcode(FormStateInterface $form_state) {
321     $this->initFormLangcodes($form_state);
322     return $form_state->get('langcode');
323   }
324
325   /**
326    * {@inheritdoc}
327    */
328   public function isDefaultFormLangcode(FormStateInterface $form_state) {
329     $this->initFormLangcodes($form_state);
330     return $form_state->get('langcode') == $form_state->get('entity_default_langcode');
331   }
332
333   /**
334    * {@inheritdoc}
335    */
336   protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
337     // First, extract values from widgets.
338     $extracted = $this->getFormDisplay($form_state)->extractFormValues($entity, $form, $form_state);
339
340     // Then extract the values of fields that are not rendered through widgets,
341     // by simply copying from top-level form values. This leaves the fields
342     // that are not being edited within this form untouched.
343     foreach ($form_state->getValues() as $name => $values) {
344       if ($entity->hasField($name) && !isset($extracted[$name])) {
345         $entity->set($name, $values);
346       }
347     }
348   }
349
350   /**
351    * {@inheritdoc}
352    */
353   public function getFormDisplay(FormStateInterface $form_state) {
354     return $form_state->get('form_display');
355   }
356
357   /**
358    * {@inheritdoc}
359    */
360   public function setFormDisplay(EntityFormDisplayInterface $form_display, FormStateInterface $form_state) {
361     $form_state->set('form_display', $form_display);
362     return $this;
363   }
364
365   /**
366    * Updates the form language to reflect any change to the entity language.
367    *
368    * There are use cases for modules to act both before and after form language
369    * being updated, thus the update is performed through an entity builder
370    * callback, which allows to support both cases.
371    *
372    * @param string $entity_type_id
373    *   The entity type identifier.
374    * @param \Drupal\Core\Entity\EntityInterface $entity
375    *   The entity updated with the submitted values.
376    * @param array $form
377    *   The complete form array.
378    * @param \Drupal\Core\Form\FormStateInterface $form_state
379    *   The current state of the form.
380    *
381    * @see \Drupal\Core\Entity\ContentEntityForm::form()
382    */
383   public function updateFormLangcode($entity_type_id, EntityInterface $entity, array $form, FormStateInterface $form_state) {
384     $langcode = $entity->language()->getId();
385     $form_state->set('langcode', $langcode);
386
387     // If this is the original entity language, also update the default
388     // langcode.
389     if ($langcode == $entity->getUntranslated()->language()->getId()) {
390       $form_state->set('entity_default_langcode', $langcode);
391     }
392   }
393
394   /**
395    * Updates the changed time of the entity.
396    *
397    * Applies only if the entity implements the EntityChangedInterface.
398    *
399    * @param \Drupal\Core\Entity\EntityInterface $entity
400    *   The entity updated with the submitted values.
401    */
402   public function updateChangedTime(EntityInterface $entity) {
403     if ($entity instanceof EntityChangedInterface) {
404       $entity->setChangedTime($this->time->getRequestTime());
405     }
406   }
407
408   /**
409    * Add revision form fields if the entity enabled the UI.
410    *
411    * @param array $form
412    *   An associative array containing the structure of the form.
413    */
414   protected function addRevisionableFormFields(array &$form) {
415     /** @var ContentEntityTypeInterface $entity_type */
416     $entity_type = $this->entity->getEntityType();
417
418     $new_revision_default = $this->getNewRevisionDefault();
419
420     // Add a log field if the "Create new revision" option is checked, or if the
421     // current user has the ability to check that option.
422     $form['revision_information'] = [
423       '#type' => 'details',
424       '#title' => $this->t('Revision information'),
425       // Open by default when "Create new revision" is checked.
426       '#open' => $new_revision_default,
427       '#group' => 'advanced',
428       '#weight' => 20,
429       '#access' => $new_revision_default || $this->entity->get($entity_type->getKey('revision'))->access('update'),
430       '#optional' => TRUE,
431       '#attributes' => [
432         'class' => ['entity-content-form-revision-information'],
433       ],
434       '#attached' => [
435         'library' => ['core/drupal.entity-form'],
436       ],
437     ];
438
439     $form['revision'] = [
440       '#type' => 'checkbox',
441       '#title' => $this->t('Create new revision'),
442       '#default_value' => $new_revision_default,
443       '#access' => !$this->entity->isNew() && $this->entity->get($entity_type->getKey('revision'))->access('update'),
444       '#group' => 'revision_information',
445     ];
446     // Get log message field's key from definition.
447     $log_message_field = $entity_type->getRevisionMetadataKey('revision_log_message');
448     if ($log_message_field && isset($form[$log_message_field])) {
449       $form[$log_message_field] += [
450         '#group' => 'revision_information',
451         '#states' => [
452           'visible' => [
453             ':input[name="revision"]' => ['checked' => TRUE],
454           ],
455         ],
456       ];
457     }
458   }
459
460   /**
461    * Should new revisions created on default.
462    *
463    * @return bool
464    *   New revision on default.
465    */
466   protected function getNewRevisionDefault() {
467     $new_revision_default = FALSE;
468     $bundle_entity = $this->getBundleEntity();
469     if ($bundle_entity instanceof RevisionableEntityBundleInterface) {
470       // Always use the default revision setting.
471       $new_revision_default = $bundle_entity->shouldCreateNewRevision();
472     }
473     return $new_revision_default;
474   }
475
476   /**
477    * Checks whether the revision form fields should be added to the form.
478    *
479    * @return bool
480    *   TRUE if the form field should be added, FALSE otherwise.
481    */
482   protected function showRevisionUi() {
483     return $this->entity->getEntityType()->showRevisionUi();
484   }
485
486 }