3 namespace Drupal\Core\Entity;
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;
12 * Entity form variant for content entity types.
14 * @see \Drupal\Core\ContentEntityBase
16 class ContentEntityForm extends EntityForm implements ContentEntityFormInterface {
19 * The entity being used by this form.
21 * @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\RevisionLogInterface
26 * The entity type bundle info service.
28 * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
30 protected $entityTypeBundleInfo;
35 * @var \Drupal\Component\Datetime\TimeInterface
40 * The entity repository service.
42 * @var \Drupal\Core\Entity\EntityRepositoryInterface
44 protected $entityRepository;
47 * Constructs a ContentEntityForm object.
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
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;
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');
69 public static function create(ContainerInterface $container) {
71 $container->get('entity.repository'),
72 $container->get('entity_type.bundle.info'),
73 $container->get('datetime.time')
80 protected function prepareEntity() {
81 parent::prepareEntity();
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);
90 * Returns the bundle entity of the entity, or NULL if there is none.
92 * @return \Drupal\Core\Entity\EntityInterface|null
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());
105 public function form(array $form, FormStateInterface $form_state) {
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',
117 $form = parent::form($form, $form_state);
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
123 unset($form['#after_build']);
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';
129 if ($this->showRevisionUi()) {
130 $this->addRevisionableFormFields($form);
134 '#type' => 'container',
137 'class' => ['entity-content-form-footer'],
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);
157 public function buildEntity(array $form, FormStateInterface $form_state) {
158 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
159 $entity = parent::buildEntity($form, $form_state);
161 // Mark the entity as requiring validation.
162 $entity->setValidationRequired(!$form_state->getTemporaryValue('entity_validated'));
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
170 $entity->setRevisionUserId($this->currentUser()->id());
171 $entity->setRevisionCreationTime($this->time->getRequestTime());
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
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);
192 $violations = $entity->validate();
194 // Remove violations of inaccessible fields.
195 $violations->filterByFieldAccess($this->currentUser());
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
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;
209 $edited_fields = array_unique($edited_fields);
212 $edited_fields = $this->getEditedFieldNames($form_state);
215 // Remove violations for fields that are not edited.
216 $violations->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $edited_fields));
218 $this->flagViolations($violations, $form, $form_state);
220 // The entity was validated.
221 $entity->setValidationRequired(FALSE);
222 $form_state->setTemporaryValue('entity_validated', TRUE);
228 * Gets the names of all fields edited in the form.
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.
234 * @param \Drupal\Core\Form\FormStateInterface $form_state
235 * The current state of the form.
238 * An array of field names.
240 protected function getEditedFieldNames(FormStateInterface $form_state) {
241 return array_keys($this->getFormDisplay($form_state)->getComponents());
245 * Flags violations for the current form.
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.:
252 * foreach ($violations->getByField('name') as $violation) {
253 * $form_state->setErrorByName('name', $violation->getMessage());
255 * parent::flagViolations($violations, $form, $form_state);
258 * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
259 * The violations to flag.
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.
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());
271 // Let the form display flag violations of its fields.
272 $this->getFormDisplay($form_state)->flagWidgetsErrorsFromViolations($violations, $form, $form_state);
276 * Initializes the form state and the entity before the first form build.
278 * @param \Drupal\Core\Form\FormStateInterface $form_state
279 * The current state of the form.
281 protected function init(FormStateInterface $form_state) {
282 // Ensure we act on the translation object corresponding to the current form
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);
288 $form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation());
289 $this->setFormDisplay($form_display, $form_state);
291 parent::init($form_state);
295 * Initializes form language code values.
297 * @param \Drupal\Core\Form\FormStateInterface $form_state
298 * The current state of the form.
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());
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());
320 public function getFormLangcode(FormStateInterface $form_state) {
321 $this->initFormLangcodes($form_state);
322 return $form_state->get('langcode');
328 public function isDefaultFormLangcode(FormStateInterface $form_state) {
329 $this->initFormLangcodes($form_state);
330 return $form_state->get('langcode') == $form_state->get('entity_default_langcode');
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);
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);
353 public function getFormDisplay(FormStateInterface $form_state) {
354 return $form_state->get('form_display');
360 public function setFormDisplay(EntityFormDisplayInterface $form_display, FormStateInterface $form_state) {
361 $form_state->set('form_display', $form_display);
366 * Updates the form language to reflect any change to the entity language.
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.
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.
377 * The complete form array.
378 * @param \Drupal\Core\Form\FormStateInterface $form_state
379 * The current state of the form.
381 * @see \Drupal\Core\Entity\ContentEntityForm::form()
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);
387 // If this is the original entity language, also update the default
389 if ($langcode == $entity->getUntranslated()->language()->getId()) {
390 $form_state->set('entity_default_langcode', $langcode);
395 * Updates the changed time of the entity.
397 * Applies only if the entity implements the EntityChangedInterface.
399 * @param \Drupal\Core\Entity\EntityInterface $entity
400 * The entity updated with the submitted values.
402 public function updateChangedTime(EntityInterface $entity) {
403 if ($entity instanceof EntityChangedInterface) {
404 $entity->setChangedTime($this->time->getRequestTime());
409 * Add revision form fields if the entity enabled the UI.
412 * An associative array containing the structure of the form.
414 protected function addRevisionableFormFields(array &$form) {
415 /** @var ContentEntityTypeInterface $entity_type */
416 $entity_type = $this->entity->getEntityType();
418 $new_revision_default = $this->getNewRevisionDefault();
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',
429 '#access' => $new_revision_default || $this->entity->get($entity_type->getKey('revision'))->access('update'),
432 'class' => ['entity-content-form-revision-information'],
435 'library' => ['core/drupal.entity-form'],
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',
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',
453 ':input[name="revision"]' => ['checked' => TRUE],
461 * Should new revisions created on default.
464 * New revision on default.
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();
473 return $new_revision_default;
477 * Checks whether the revision form fields should be added to the form.
480 * TRUE if the form field should be added, FALSE otherwise.
482 protected function showRevisionUi() {
483 return $this->entity->getEntityType()->showRevisionUi();