EntityForm.php 15 KB

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