EntityForm.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <?php
  2. namespace Drupal\Core\Entity;
  3. use Drupal\Core\Form\FormBase;
  4. use Drupal\Core\Extension\ModuleHandlerInterface;
  5. use Drupal\Core\Form\FormStateInterface;
  6. use Drupal\Core\Render\Element;
  7. use Drupal\Core\Routing\RouteMatchInterface;
  8. /**
  9. * Base class for entity forms.
  10. *
  11. * @ingroup entity_api
  12. */
  13. class EntityForm extends FormBase implements EntityFormInterface {
  14. /**
  15. * The name of the current operation.
  16. *
  17. * Subclasses may use this to implement different behaviors depending on its
  18. * value.
  19. *
  20. * @var string
  21. */
  22. protected $operation;
  23. /**
  24. * The module handler service.
  25. *
  26. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  27. */
  28. protected $moduleHandler;
  29. /**
  30. * The entity manager.
  31. *
  32. * @var \Drupal\Core\Entity\EntityManagerInterface
  33. *
  34. * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
  35. *
  36. * @see https://www.drupal.org/node/2549139
  37. */
  38. protected $entityManager;
  39. /**
  40. * The entity type manager.
  41. *
  42. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  43. */
  44. protected $entityTypeManager;
  45. /**
  46. * The entity being used by this form.
  47. *
  48. * @var \Drupal\Core\Entity\EntityInterface
  49. */
  50. protected $entity;
  51. /**
  52. * {@inheritdoc}
  53. */
  54. public function setOperation($operation) {
  55. // If NULL is passed, do not overwrite the operation.
  56. if ($operation) {
  57. $this->operation = $operation;
  58. }
  59. return $this;
  60. }
  61. /**
  62. * {@inheritdoc}
  63. */
  64. public function getBaseFormId() {
  65. // Assign ENTITYTYPE_form as base form ID to invoke corresponding
  66. // hook_form_alter(), #validate, #submit, and #theme callbacks, but only if
  67. // it is different from the actual form ID, since callbacks would be invoked
  68. // twice otherwise.
  69. $base_form_id = $this->entity->getEntityTypeId() . '_form';
  70. if ($base_form_id == $this->getFormId()) {
  71. $base_form_id = NULL;
  72. }
  73. return $base_form_id;
  74. }
  75. /**
  76. * {@inheritdoc}
  77. */
  78. public function getFormId() {
  79. $form_id = $this->entity->getEntityTypeId();
  80. if ($this->entity->getEntityType()->hasKey('bundle')) {
  81. $form_id .= '_' . $this->entity->bundle();
  82. }
  83. if ($this->operation != 'default') {
  84. $form_id = $form_id . '_' . $this->operation;
  85. }
  86. return $form_id . '_form';
  87. }
  88. /**
  89. * {@inheritdoc}
  90. */
  91. public function buildForm(array $form, FormStateInterface $form_state) {
  92. // During the initial form build, add this form object to the form state and
  93. // allow for initial preparation before form building and processing.
  94. if (!$form_state->has('entity_form_initialized')) {
  95. $this->init($form_state);
  96. }
  97. // Ensure that edit forms have the correct cacheability metadata so they can
  98. // be cached.
  99. if (!$this->entity->isNew()) {
  100. \Drupal::service('renderer')->addCacheableDependency($form, $this->entity);
  101. }
  102. // Retrieve the form array using the possibly updated entity in form state.
  103. $form = $this->form($form, $form_state);
  104. // Retrieve and add the form actions array.
  105. $actions = $this->actionsElement($form, $form_state);
  106. if (!empty($actions)) {
  107. $form['actions'] = $actions;
  108. }
  109. return $form;
  110. }
  111. /**
  112. * Initialize the form state and the entity before the first form build.
  113. */
  114. protected function init(FormStateInterface $form_state) {
  115. // Flag that this form has been initialized.
  116. $form_state->set('entity_form_initialized', TRUE);
  117. // Prepare the entity to be presented in the entity form.
  118. $this->prepareEntity();
  119. // Invoke the prepare form hooks.
  120. $this->prepareInvokeAll('entity_prepare_form', $form_state);
  121. $this->prepareInvokeAll($this->entity->getEntityTypeId() . '_prepare_form', $form_state);
  122. }
  123. /**
  124. * Gets the actual form array to be built.
  125. *
  126. * @see \Drupal\Core\Entity\EntityForm::processForm()
  127. * @see \Drupal\Core\Entity\EntityForm::afterBuild()
  128. */
  129. public function form(array $form, FormStateInterface $form_state) {
  130. // Add #process and #after_build callbacks.
  131. $form['#process'][] = '::processForm';
  132. $form['#after_build'][] = '::afterBuild';
  133. return $form;
  134. }
  135. /**
  136. * Process callback: assigns weights and hides extra fields.
  137. *
  138. * @see \Drupal\Core\Entity\EntityForm::form()
  139. */
  140. public function processForm($element, FormStateInterface $form_state, $form) {
  141. // If the form is cached, process callbacks may not have a valid reference
  142. // to the entity object, hence we must restore it.
  143. $this->entity = $form_state->getFormObject()->getEntity();
  144. return $element;
  145. }
  146. /**
  147. * Form element #after_build callback: Updates the entity with submitted data.
  148. *
  149. * Updates the internal $this->entity object with submitted values when the
  150. * form is being rebuilt (e.g. submitted via AJAX), so that subsequent
  151. * processing (e.g. AJAX callbacks) can rely on it.
  152. */
  153. public function afterBuild(array $element, FormStateInterface $form_state) {
  154. // Rebuild the entity if #after_build is being called as part of a form
  155. // rebuild, i.e. if we are processing input.
  156. if ($form_state->isProcessingInput()) {
  157. $this->entity = $this->buildEntity($element, $form_state);
  158. }
  159. return $element;
  160. }
  161. /**
  162. * Returns the action form element for the current entity form.
  163. */
  164. protected function actionsElement(array $form, FormStateInterface $form_state) {
  165. $element = $this->actions($form, $form_state);
  166. if (isset($element['delete'])) {
  167. // Move the delete action as last one, unless weights are explicitly
  168. // provided.
  169. $delete = $element['delete'];
  170. unset($element['delete']);
  171. $element['delete'] = $delete;
  172. $element['delete']['#button_type'] = 'danger';
  173. }
  174. if (isset($element['submit'])) {
  175. // Give the primary submit button a #button_type of primary.
  176. $element['submit']['#button_type'] = 'primary';
  177. }
  178. $count = 0;
  179. foreach (Element::children($element) as $action) {
  180. $element[$action] += [
  181. '#weight' => ++$count * 5,
  182. ];
  183. }
  184. if (!empty($element)) {
  185. $element['#type'] = 'actions';
  186. }
  187. return $element;
  188. }
  189. /**
  190. * Returns an array of supported actions for the current entity form.
  191. *
  192. * @todo Consider introducing a 'preview' action here, since it is used by
  193. * many entity types.
  194. */
  195. protected function actions(array $form, FormStateInterface $form_state) {
  196. // @todo Consider renaming the action key from submit to save. The impacts
  197. // are hard to predict. For example, see
  198. // \Drupal\language\Element\LanguageConfiguration::processLanguageConfiguration().
  199. $actions['submit'] = [
  200. '#type' => 'submit',
  201. '#value' => $this->t('Save'),
  202. '#submit' => ['::submitForm', '::save'],
  203. ];
  204. if (!$this->entity->isNew() && $this->entity->hasLinkTemplate('delete-form')) {
  205. $route_info = $this->entity->urlInfo('delete-form');
  206. if ($this->getRequest()->query->has('destination')) {
  207. $query = $route_info->getOption('query');
  208. $query['destination'] = $this->getRequest()->query->get('destination');
  209. $route_info->setOption('query', $query);
  210. }
  211. $actions['delete'] = [
  212. '#type' => 'link',
  213. '#title' => $this->t('Delete'),
  214. '#access' => $this->entity->access('delete'),
  215. '#attributes' => [
  216. 'class' => ['button', 'button--danger'],
  217. ],
  218. ];
  219. $actions['delete']['#url'] = $route_info;
  220. }
  221. return $actions;
  222. }
  223. /**
  224. * {@inheritdoc}
  225. *
  226. * This is the default entity object builder function. It is called before any
  227. * other submit handler to build the new entity object to be used by the
  228. * following submit handlers. At this point of the form workflow the entity is
  229. * validated and the form state can be updated, this way the subsequently
  230. * invoked handlers can retrieve a regular entity object to act on. Generally
  231. * this method should not be overridden unless the entity requires the same
  232. * preparation for two actions, see \Drupal\comment\CommentForm for an example
  233. * with the save and preview actions.
  234. *
  235. * @param array $form
  236. * An associative array containing the structure of the form.
  237. * @param \Drupal\Core\Form\FormStateInterface $form_state
  238. * The current state of the form.
  239. */
  240. public function submitForm(array &$form, FormStateInterface $form_state) {
  241. // Remove button and internal Form API values from submitted values.
  242. $form_state->cleanValues();
  243. $this->entity = $this->buildEntity($form, $form_state);
  244. }
  245. /**
  246. * {@inheritdoc}
  247. */
  248. public function save(array $form, FormStateInterface $form_state) {
  249. return $this->entity->save();
  250. }
  251. /**
  252. * {@inheritdoc}
  253. */
  254. public function buildEntity(array $form, FormStateInterface $form_state) {
  255. $entity = clone $this->entity;
  256. $this->copyFormValuesToEntity($entity, $form, $form_state);
  257. // Invoke all specified builders for copying form values to entity
  258. // properties.
  259. if (isset($form['#entity_builders'])) {
  260. foreach ($form['#entity_builders'] as $function) {
  261. call_user_func_array($form_state->prepareCallback($function), [$entity->getEntityTypeId(), $entity, &$form, &$form_state]);
  262. }
  263. }
  264. return $entity;
  265. }
  266. /**
  267. * Copies top-level form values to entity properties
  268. *
  269. * This should not change existing entity properties that are not being edited
  270. * by this form.
  271. *
  272. * @param \Drupal\Core\Entity\EntityInterface $entity
  273. * The entity the current form should operate upon.
  274. * @param array $form
  275. * A nested array of form elements comprising the form.
  276. * @param \Drupal\Core\Form\FormStateInterface $form_state
  277. * The current state of the form.
  278. */
  279. protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
  280. $values = $form_state->getValues();
  281. if ($this->entity instanceof EntityWithPluginCollectionInterface) {
  282. // Do not manually update values represented by plugin collections.
  283. $values = array_diff_key($values, $this->entity->getPluginCollections());
  284. }
  285. // @todo: This relies on a method that only exists for config and content
  286. // entities, in a different way. Consider moving this logic to a config
  287. // entity specific implementation.
  288. foreach ($values as $key => $value) {
  289. $entity->set($key, $value);
  290. }
  291. }
  292. /**
  293. * {@inheritdoc}
  294. */
  295. public function getEntity() {
  296. return $this->entity;
  297. }
  298. /**
  299. * {@inheritdoc}
  300. */
  301. public function setEntity(EntityInterface $entity) {
  302. $this->entity = $entity;
  303. return $this;
  304. }
  305. /**
  306. * {@inheritdoc}
  307. */
  308. public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
  309. if ($route_match->getRawParameter($entity_type_id) !== NULL) {
  310. $entity = $route_match->getParameter($entity_type_id);
  311. }
  312. else {
  313. $values = [];
  314. // If the entity has bundles, fetch it from the route match.
  315. $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
  316. if ($bundle_key = $entity_type->getKey('bundle')) {
  317. if (($bundle_entity_type_id = $entity_type->getBundleEntityType()) && $route_match->getRawParameter($bundle_entity_type_id)) {
  318. $values[$bundle_key] = $route_match->getParameter($bundle_entity_type_id)->id();
  319. }
  320. elseif ($route_match->getRawParameter($bundle_key)) {
  321. $values[$bundle_key] = $route_match->getParameter($bundle_key);
  322. }
  323. }
  324. $entity = $this->entityTypeManager->getStorage($entity_type_id)->create($values);
  325. }
  326. return $entity;
  327. }
  328. /**
  329. * Prepares the entity object before the form is built first.
  330. */
  331. protected function prepareEntity() {}
  332. /**
  333. * Invokes the specified prepare hook variant.
  334. *
  335. * @param string $hook
  336. * The hook variant name.
  337. * @param \Drupal\Core\Form\FormStateInterface $form_state
  338. * The current state of the form.
  339. */
  340. protected function prepareInvokeAll($hook, FormStateInterface $form_state) {
  341. $implementations = $this->moduleHandler->getImplementations($hook);
  342. foreach ($implementations as $module) {
  343. $function = $module . '_' . $hook;
  344. if (function_exists($function)) {
  345. // Ensure we pass an updated translation object and form display at
  346. // each invocation, since they depend on form state which is alterable.
  347. $args = [$this->entity, $this->operation, &$form_state];
  348. call_user_func_array($function, $args);
  349. }
  350. }
  351. }
  352. /**
  353. * {@inheritdoc}
  354. */
  355. public function getOperation() {
  356. return $this->operation;
  357. }
  358. /**
  359. * {@inheritdoc}
  360. */
  361. public function setModuleHandler(ModuleHandlerInterface $module_handler) {
  362. $this->moduleHandler = $module_handler;
  363. return $this;
  364. }
  365. /**
  366. * {@inheritdoc}
  367. */
  368. public function setEntityManager(EntityManagerInterface $entity_manager) {
  369. $this->entityManager = $entity_manager;
  370. return $this;
  371. }
  372. /**
  373. * {@inheritdoc}
  374. */
  375. public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
  376. $this->entityTypeManager = $entity_type_manager;
  377. return $this;
  378. }
  379. }