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