ContentEntityForm.php 17 KB

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