--- /dev/null
+<?php
+
+namespace Drupal\Core\Entity;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Entity form variant for content entity types.
+ *
+ * @see \Drupal\Core\ContentEntityBase
+ */
+class ContentEntityForm extends EntityForm implements ContentEntityFormInterface {
+
+ /**
+ * The entity manager.
+ *
+ * @var \Drupal\Core\Entity\EntityManagerInterface
+ */
+ protected $entityManager;
+
+ /**
+ * The entity being used by this form.
+ *
+ * @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\RevisionLogInterface
+ */
+ protected $entity;
+
+ /**
+ * The entity type bundle info service.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+ */
+ protected $entityTypeBundleInfo;
+
+ /**
+ * The time service.
+ *
+ * @var \Drupal\Component\Datetime\TimeInterface
+ */
+ protected $time;
+
+ /**
+ * Constructs a ContentEntityForm object.
+ *
+ * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+ * The entity manager.
+ * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
+ * The entity type bundle service.
+ * @param \Drupal\Component\Datetime\TimeInterface $time
+ * The time service.
+ */
+ public function __construct(EntityManagerInterface $entity_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
+ $this->entityManager = $entity_manager;
+
+ $this->entityTypeBundleInfo = $entity_type_bundle_info ?: \Drupal::service('entity_type.bundle.info');
+ $this->time = $time ?: \Drupal::service('datetime.time');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity.manager'),
+ $container->get('entity_type.bundle.info'),
+ $container->get('datetime.time')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function prepareEntity() {
+ parent::prepareEntity();
+
+ // Hide the current revision log message in UI.
+ if ($this->showRevisionUi() && !$this->entity->isNew() && $this->entity instanceof RevisionLogInterface) {
+ $this->entity->setRevisionLogMessage(NULL);
+ }
+ }
+
+ /**
+ * Returns the bundle entity of the entity, or NULL if there is none.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|null
+ * The bundle entity.
+ */
+ protected function getBundleEntity() {
+ if ($bundle_entity_type = $this->entity->getEntityType()->getBundleEntityType()) {
+ return $this->entityTypeManager->getStorage($bundle_entity_type)->load($this->entity->bundle());
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+
+ if ($this->showRevisionUi()) {
+ // Advanced tab must be the first, because other fields rely on that.
+ if (!isset($form['advanced'])) {
+ $form['advanced'] = [
+ '#type' => 'vertical_tabs',
+ '#weight' => 99,
+ ];
+ }
+ }
+
+ $form = parent::form($form, $form_state);
+
+ // Content entity forms do not use the parent's #after_build callback
+ // because they only need to rebuild the entity in the validation and the
+ // submit handler because Field API uses its own #after_build callback for
+ // its widgets.
+ unset($form['#after_build']);
+
+ $this->getFormDisplay($form_state)->buildForm($this->entity, $form, $form_state);
+ // Allow modules to act before and after form language is updated.
+ $form['#entity_builders']['update_form_langcode'] = '::updateFormLangcode';
+
+ if ($this->showRevisionUi()) {
+ $this->addRevisionableFormFields($form);
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ parent::submitForm($form, $form_state);
+ // Update the changed timestamp of the entity.
+ $this->updateChangedTime($this->entity);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildEntity(array $form, FormStateInterface $form_state) {
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ $entity = parent::buildEntity($form, $form_state);
+
+ // Mark the entity as requiring validation.
+ $entity->setValidationRequired(!$form_state->getTemporaryValue('entity_validated'));
+
+ // Save as a new revision if requested to do so.
+ if ($this->showRevisionUi() && !$form_state->isValueEmpty('revision')) {
+ $entity->setNewRevision();
+ if ($entity instanceof RevisionLogInterface) {
+ // If a new revision is created, save the current user as
+ // revision author.
+ $entity->setRevisionUserId($this->currentUser()->id());
+ $entity->setRevisionCreationTime($this->time->getRequestTime());
+ }
+ }
+
+ return $entity;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Button-level validation handlers are highly discouraged for entity forms,
+ * as they will prevent entity validation from running. If the entity is going
+ * to be saved during the form submission, this method should be manually
+ * invoked from the button-level validation handler, otherwise an exception
+ * will be thrown.
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ $entity = $this->buildEntity($form, $form_state);
+
+ $violations = $entity->validate();
+
+ // Remove violations of inaccessible fields and not edited fields.
+ $violations
+ ->filterByFieldAccess($this->currentUser())
+ ->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $this->getEditedFieldNames($form_state)));
+
+ $this->flagViolations($violations, $form, $form_state);
+
+ // The entity was validated.
+ $entity->setValidationRequired(FALSE);
+ $form_state->setTemporaryValue('entity_validated', TRUE);
+
+ return $entity;
+ }
+
+ /**
+ * Gets the names of all fields edited in the form.
+ *
+ * If the entity form customly adds some fields to the form (i.e. without
+ * using the form display), it needs to add its fields here and override
+ * flagViolations() for displaying the violations.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return string[]
+ * An array of field names.
+ */
+ protected function getEditedFieldNames(FormStateInterface $form_state) {
+ return array_keys($this->getFormDisplay($form_state)->getComponents());
+ }
+
+ /**
+ * Flags violations for the current form.
+ *
+ * If the entity form customly adds some fields to the form (i.e. without
+ * using the form display), it needs to add its fields to array returned by
+ * getEditedFieldNames() and overwrite this method in order to show any
+ * violations for those fields; e.g.:
+ * @code
+ * foreach ($violations->getByField('name') as $violation) {
+ * $form_state->setErrorByName('name', $violation->getMessage());
+ * }
+ * parent::flagViolations($violations, $form, $form_state);
+ * @endcode
+ *
+ * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
+ * The violations to flag.
+ * @param array $form
+ * A nested array of form elements comprising the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
+ // Flag entity level violations.
+ foreach ($violations->getEntityViolations() as $violation) {
+ /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
+ $form_state->setErrorByName('', $violation->getMessage());
+ }
+ // Let the form display flag violations of its fields.
+ $this->getFormDisplay($form_state)->flagWidgetsErrorsFromViolations($violations, $form, $form_state);
+ }
+
+ /**
+ * Initializes the form state and the entity before the first form build.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ protected function init(FormStateInterface $form_state) {
+ // Ensure we act on the translation object corresponding to the current form
+ // language.
+ $this->initFormLangcodes($form_state);
+ $langcode = $this->getFormLangcode($form_state);
+ $this->entity = $this->entity->hasTranslation($langcode) ? $this->entity->getTranslation($langcode) : $this->entity->addTranslation($langcode);
+
+ $form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation());
+ $this->setFormDisplay($form_display, $form_state);
+
+ parent::init($form_state);
+ }
+
+ /**
+ * Initializes form language code values.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ protected function initFormLangcodes(FormStateInterface $form_state) {
+ // Store the entity default language to allow checking whether the form is
+ // dealing with the original entity or a translation.
+ if (!$form_state->has('entity_default_langcode')) {
+ $form_state->set('entity_default_langcode', $this->entity->getUntranslated()->language()->getId());
+ }
+ // This value might have been explicitly populated to work with a particular
+ // entity translation. If not we fall back to the most proper language based
+ // on contextual information.
+ if (!$form_state->has('langcode')) {
+ // Imply a 'view' operation to ensure users edit entities in the same
+ // language they are displayed. This allows to keep contextual editing
+ // working also for multilingual entities.
+ $form_state->set('langcode', $this->entityManager->getTranslationFromContext($this->entity)->language()->getId());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormLangcode(FormStateInterface $form_state) {
+ $this->initFormLangcodes($form_state);
+ return $form_state->get('langcode');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDefaultFormLangcode(FormStateInterface $form_state) {
+ $this->initFormLangcodes($form_state);
+ return $form_state->get('langcode') == $form_state->get('entity_default_langcode');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $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->getValues() as $name => $values) {
+ if ($entity->hasField($name) && !isset($extracted[$name])) {
+ $entity->set($name, $values);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormDisplay(FormStateInterface $form_state) {
+ return $form_state->get('form_display');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFormDisplay(EntityFormDisplayInterface $form_display, FormStateInterface $form_state) {
+ $form_state->set('form_display', $form_display);
+ return $this;
+ }
+
+ /**
+ * Updates the form language to reflect any change to the entity language.
+ *
+ * There are use cases for modules to act both before and after form language
+ * being updated, thus the update is performed through an entity builder
+ * callback, which allows to support both cases.
+ *
+ * @param string $entity_type_id
+ * The entity type identifier.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity updated with the submitted values.
+ * @param array $form
+ * The complete form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @see \Drupal\Core\Entity\ContentEntityForm::form()
+ */
+ public function updateFormLangcode($entity_type_id, EntityInterface $entity, array $form, FormStateInterface $form_state) {
+ $langcode = $entity->language()->getId();
+ $form_state->set('langcode', $langcode);
+
+ // If this is the original entity language, also update the default
+ // langcode.
+ if ($langcode == $entity->getUntranslated()->language()->getId()) {
+ $form_state->set('entity_default_langcode', $langcode);
+ }
+ }
+
+ /**
+ * Updates the changed time of the entity.
+ *
+ * Applies only if the entity implements the EntityChangedInterface.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity updated with the submitted values.
+ */
+ public function updateChangedTime(EntityInterface $entity) {
+ if ($entity instanceof EntityChangedInterface) {
+ $entity->setChangedTime($this->time->getRequestTime());
+ }
+ }
+
+ /**
+ * Add revision form fields if the entity enabled the UI.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ */
+ protected function addRevisionableFormFields(array &$form) {
+ $entity_type = $this->entity->getEntityType();
+
+ $new_revision_default = $this->getNewRevisionDefault();
+
+ // Add a log field if the "Create new revision" option is checked, or if the
+ // current user has the ability to check that option.
+ $form['revision_information'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Revision information'),
+ // Open by default when "Create new revision" is checked.
+ '#open' => $new_revision_default,
+ '#group' => 'advanced',
+ '#weight' => 20,
+ '#access' => $new_revision_default || $this->entity->get($entity_type->getKey('revision'))->access('update'),
+ '#optional' => TRUE,
+ '#attributes' => [
+ 'class' => ['entity-content-form-revision-information'],
+ ],
+ '#attached' => [
+ 'library' => ['core/drupal.entity-form'],
+ ],
+ ];
+
+ $form['revision'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Create new revision'),
+ '#default_value' => $new_revision_default,
+ '#access' => !$this->entity->isNew() && $this->entity->get($entity_type->getKey('revision'))->access('update'),
+ '#group' => 'revision_information',
+ ];
+
+ if (isset($form['revision_log'])) {
+ $form['revision_log'] += [
+ '#group' => 'revision_information',
+ '#states' => [
+ 'visible' => [
+ ':input[name="revision"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ }
+ }
+
+ /**
+ * Should new revisions created on default.
+ *
+ * @return bool
+ * New revision on default.
+ */
+ protected function getNewRevisionDefault() {
+ $new_revision_default = FALSE;
+ $bundle_entity = $this->getBundleEntity();
+ if ($bundle_entity instanceof RevisionableEntityBundleInterface) {
+ // Always use the default revision setting.
+ $new_revision_default = $bundle_entity->shouldCreateNewRevision();
+ }
+ return $new_revision_default;
+ }
+
+ /**
+ * Checks whether the revision form fields should be added to the form.
+ *
+ * @return bool
+ * TRUE if the form field should be added, FALSE otherwise.
+ */
+ protected function showRevisionUi() {
+ return $this->entity->getEntityType()->showRevisionUi();
+ }
+
+}