EntityForm.php 13.3 KB
Newer Older
1 2
<?php

3
namespace Drupal\Core\Entity;
4

5
use Drupal\Core\Form\FormBase;
6
use Drupal\Core\Extension\ModuleHandlerInterface;
7
use Drupal\Core\Form\FormStateInterface;
8
use Drupal\Core\Render\Element;
9
use Drupal\Core\Routing\RouteMatchInterface;
10

11
/**
12
 * Base class for entity forms.
13 14
 *
 * @ingroup entity_api
15
 */
16
class EntityForm extends FormBase implements EntityFormInterface {
17 18 19 20 21 22 23 24 25 26 27

  /**
   * The name of the current operation.
   *
   * Subclasses may use this to implement different behaviors depending on its
   * value.
   *
   * @var string
   */
  protected $operation;

28 29 30 31 32 33 34
  /**
   * The module handler service.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

35 36 37 38
  /**
   * The entity manager.
   *
   * @var \Drupal\Core\Entity\EntityManagerInterface
39 40
   *
   * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
41 42
   *
   * @see https://www.drupal.org/node/2549139
43 44 45
   */
  protected $entityManager;

46 47 48 49 50 51 52
  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

53 54 55 56 57 58 59
  /**
   * The entity being used by this form.
   *
   * @var \Drupal\Core\Entity\EntityInterface
   */
  protected $entity;

60 61
  /**
   * {@inheritdoc}
62 63 64 65 66 67
   */
  public function setOperation($operation) {
    // If NULL is passed, do not overwrite the operation.
    if ($operation) {
      $this->operation = $operation;
    }
68
    return $this;
69 70 71
  }

  /**
72
   * {@inheritdoc}
73
   */
74
  public function getBaseFormId() {
75 76 77 78
    // Assign ENTITYTYPE_form as base form ID to invoke corresponding
    // hook_form_alter(), #validate, #submit, and #theme callbacks, but only if
    // it is different from the actual form ID, since callbacks would be invoked
    // twice otherwise.
79
    $base_form_id = $this->entity->getEntityTypeId() . '_form';
80
    if ($base_form_id == $this->getFormId()) {
81
      $base_form_id = NULL;
82 83
    }
    return $base_form_id;
84
  }
85

86 87 88
  /**
   * {@inheritdoc}
   */
89
  public function getFormId() {
90 91
    $form_id = $this->entity->getEntityTypeId();
    if ($this->entity->getEntityType()->hasKey('bundle')) {
92
      $form_id .= '_' . $this->entity->bundle();
93 94
    }
    if ($this->operation != 'default') {
95
      $form_id = $form_id . '_' . $this->operation;
96 97 98 99 100 101 102
    }
    return $form_id . '_form';
  }

  /**
   * {@inheritdoc}
   */
103
  public function buildForm(array $form, FormStateInterface $form_state) {
104
    // During the initial form build, add this form object to the form state and
105
    // allow for initial preparation before form building and processing.
106
    if (!$form_state->has('entity_form_initialized')) {
107
      $this->init($form_state);
108 109
    }

110 111 112 113 114 115
    // Ensure that edit forms have the correct cacheability metadata so they can
    // be cached.
    if (!$this->entity->isNew()) {
      \Drupal::service('renderer')->addCacheableDependency($form, $this->entity);
    }

116
    // Retrieve the form array using the possibly updated entity in form state.
117
    $form = $this->form($form, $form_state);
118 119 120 121 122 123 124 125 126 127 128 129 130

    // Retrieve and add the form actions array.
    $actions = $this->actionsElement($form, $form_state);
    if (!empty($actions)) {
      $form['actions'] = $actions;
    }

    return $form;
  }

  /**
   * Initialize the form state and the entity before the first form build.
   */
131
  protected function init(FormStateInterface $form_state) {
132
    // Flag that this form has been initialized.
133
    $form_state->set('entity_form_initialized', TRUE);
134

135
    // Prepare the entity to be presented in the entity form.
136
    $this->prepareEntity();
137

138 139
    // Invoke the prepare form hooks.
    $this->prepareInvokeAll('entity_prepare_form', $form_state);
140
    $this->prepareInvokeAll($this->entity->getEntityTypeId() . '_prepare_form', $form_state);
141 142 143
  }

  /**
144
   * Gets the actual form array to be built.
145
   *
146 147
   * @see \Drupal\Core\Entity\EntityForm::processForm()
   * @see \Drupal\Core\Entity\EntityForm::afterBuild()
148
   */
149
  public function form(array $form, FormStateInterface $form_state) {
150
    // Add #process and #after_build callbacks.
151
    $form['#process'][] = '::processForm';
152
    $form['#after_build'][] = '::afterBuild';
153

154 155 156
    return $form;
  }

157 158 159
  /**
   * Process callback: assigns weights and hides extra fields.
   *
160
   * @see \Drupal\Core\Entity\EntityForm::form()
161
   */
162
  public function processForm($element, FormStateInterface $form_state, $form) {
163 164
    // If the form is cached, process callbacks may not have a valid reference
    // to the entity object, hence we must restore it.
165
    $this->entity = $form_state->getFormObject()->getEntity();
166

167 168 169
    return $element;
  }

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
  /**
   * Form element #after_build callback: Updates the entity with submitted data.
   *
   * Updates the internal $this->entity object with submitted values when the
   * form is being rebuilt (e.g. submitted via AJAX), so that subsequent
   * processing (e.g. AJAX callbacks) can rely on it.
   */
  public function afterBuild(array $element, FormStateInterface $form_state) {
    // Rebuild the entity if #after_build is being called as part of a form
    // rebuild, i.e. if we are processing input.
    if ($form_state->isProcessingInput()) {
      $this->entity = $this->buildEntity($element, $form_state);
    }

    return $element;
  }

187 188 189
  /**
   * Returns the action form element for the current entity form.
   */
190
  protected function actionsElement(array $form, FormStateInterface $form_state) {
191 192
    $element = $this->actions($form, $form_state);

193
    if (isset($element['delete'])) {
194 195 196 197 198
      // Move the delete action as last one, unless weights are explicitly
      // provided.
      $delete = $element['delete'];
      unset($element['delete']);
      $element['delete'] = $delete;
199 200 201 202 203 204
      $element['delete']['#button_type'] = 'danger';
    }

    if (isset($element['submit'])) {
      // Give the primary submit button a #button_type of primary.
      $element['submit']['#button_type'] = 'primary';
205 206 207
    }

    $count = 0;
208
    foreach (Element::children($element) as $action) {
209
      $element[$action] += [
210
        '#weight' => ++$count * 5,
211
      ];
212 213 214 215 216 217 218 219 220 221 222
    }

    if (!empty($element)) {
      $element['#type'] = 'actions';
    }

    return $element;
  }

  /**
   * Returns an array of supported actions for the current entity form.
223
   *
224 225 226 227 228 229 230 231 232 233 234
   * This function generates a list of Form API elements which represent
   * actions supported by the current entity form.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   An array of supported Form API action elements keyed by name.
   *
235 236
   * @todo Consider introducing a 'preview' action here, since it is used by
   *   many entity types.
237
   */
238
  protected function actions(array $form, FormStateInterface $form_state) {
239
    // @todo Consider renaming the action key from submit to save. The impacts
240 241
    //   are hard to predict. For example, see
    //   \Drupal\language\Element\LanguageConfiguration::processLanguageConfiguration().
242
    $actions['submit'] = [
243 244
      '#type' => 'submit',
      '#value' => $this->t('Save'),
245 246
      '#submit' => ['::submitForm', '::save'],
    ];
247 248 249 250 251 252 253 254

    if (!$this->entity->isNew() && $this->entity->hasLinkTemplate('delete-form')) {
      $route_info = $this->entity->urlInfo('delete-form');
      if ($this->getRequest()->query->has('destination')) {
        $query = $route_info->getOption('query');
        $query['destination'] = $this->getRequest()->query->get('destination');
        $route_info->setOption('query', $query);
      }
255
      $actions['delete'] = [
256 257
        '#type' => 'link',
        '#title' => $this->t('Delete'),
258
        '#access' => $this->entity->access('delete'),
259 260 261 262
        '#attributes' => [
          'class' => ['button', 'button--danger'],
        ],
      ];
263
      $actions['delete']['#url'] = $route_info;
264 265 266
    }

    return $actions;
267 268 269
  }

  /**
270
   * {@inheritdoc}
271 272
   *
   * This is the default entity object builder function. It is called before any
273
   * other submit handler to build the new entity object to be used by the
274 275
   * following submit handlers. At this point of the form workflow the entity is
   * validated and the form state can be updated, this way the subsequently
276 277
   * invoked handlers can retrieve a regular entity object to act on. Generally
   * this method should not be overridden unless the entity requires the same
278 279
   * preparation for two actions, see \Drupal\comment\CommentForm for an example
   * with the save and preview actions.
280 281 282
   *
   * @param array $form
   *   An associative array containing the structure of the form.
283 284
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
285
   */
286
  public function submitForm(array &$form, FormStateInterface $form_state) {
287
    // Remove button and internal Form API values from submitted values.
288
    $form_state->cleanValues();
289
    $this->entity = $this->buildEntity($form, $form_state);
290 291 292
  }

  /**
293
   * {@inheritdoc}
294
   */
295
  public function save(array $form, FormStateInterface $form_state) {
296
    return $this->entity->save();
297 298 299
  }

  /**
300
   * {@inheritdoc}
301
   */
302
  public function buildEntity(array $form, FormStateInterface $form_state) {
303
    $entity = clone $this->entity;
304
    $this->copyFormValuesToEntity($entity, $form, $form_state);
305 306 307 308 309

    // Invoke all specified builders for copying form values to entity
    // properties.
    if (isset($form['#entity_builders'])) {
      foreach ($form['#entity_builders'] as $function) {
310
        call_user_func_array($form_state->prepareCallback($function), [$entity->getEntityTypeId(), $entity, &$form, &$form_state]);
311 312 313
      }
    }

314 315 316
    return $entity;
  }

317 318 319 320 321 322 323 324
  /**
   * Copies top-level form values to entity properties
   *
   * This should not change existing entity properties that are not being edited
   * by this form.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity the current form should operate upon.
325 326
   * @param array $form
   *   A nested array of form elements comprising the form.
327 328
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
329
   */
330
  protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
331 332 333 334 335 336 337
    $values = $form_state->getValues();

    if ($this->entity instanceof EntityWithPluginCollectionInterface) {
      // Do not manually update values represented by plugin collections.
      $values = array_diff_key($values, $this->entity->getPluginCollections());
    }

338 339 340
    // @todo: This relies on a method that only exists for config and content
    //   entities, in a different way. Consider moving this logic to a config
    //   entity specific implementation.
341
    foreach ($values as $key => $value) {
342 343 344 345
      $entity->set($key, $value);
    }
  }

346
  /**
347
   * {@inheritdoc}
348
   */
349 350
  public function getEntity() {
    return $this->entity;
351 352
  }

353
  /**
354
   * {@inheritdoc}
355
   */
356 357 358
  public function setEntity(EntityInterface $entity) {
    $this->entity = $entity;
    return $this;
359 360
  }

361 362 363 364 365 366 367 368
  /**
   * {@inheritdoc}
   */
  public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
    if ($route_match->getRawParameter($entity_type_id) !== NULL) {
      $entity = $route_match->getParameter($entity_type_id);
    }
    else {
369 370 371 372 373 374 375 376 377 378 379 380 381
      $values = [];
      // If the entity has bundles, fetch it from the route match.
      $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
      if ($bundle_key = $entity_type->getKey('bundle')) {
        if (($bundle_entity_type_id = $entity_type->getBundleEntityType()) && $route_match->getRawParameter($bundle_entity_type_id)) {
          $values[$bundle_key] = $route_match->getParameter($bundle_entity_type_id)->id();
        }
        elseif ($route_match->getRawParameter($bundle_key)) {
          $values[$bundle_key] = $route_match->getParameter($bundle_key);
        }
      }

      $entity = $this->entityTypeManager->getStorage($entity_type_id)->create($values);
382 383 384 385 386
    }

    return $entity;
  }

387 388 389
  /**
   * Prepares the entity object before the form is built first.
   */
390 391 392 393 394 395 396
  protected function prepareEntity() {}

  /**
   * Invokes the specified prepare hook variant.
   *
   * @param string $hook
   *   The hook variant name.
397 398
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
399
   */
400
  protected function prepareInvokeAll($hook, FormStateInterface $form_state) {
401 402 403 404 405 406
    $implementations = $this->moduleHandler->getImplementations($hook);
    foreach ($implementations as $module) {
      $function = $module . '_' . $hook;
      if (function_exists($function)) {
        // Ensure we pass an updated translation object and form display at
        // each invocation, since they depend on form state which is alterable.
407
        $args = [$this->entity, $this->operation, &$form_state];
408 409 410
        call_user_func_array($function, $args);
      }
    }
411 412 413
  }

  /**
414
   * {@inheritdoc}
415 416 417 418
   */
  public function getOperation() {
    return $this->operation;
  }
419 420 421 422 423 424 425 426 427

  /**
   * {@inheritdoc}
   */
  public function setModuleHandler(ModuleHandlerInterface $module_handler) {
    $this->moduleHandler = $module_handler;
    return $this;
  }

428 429 430 431 432 433 434 435
  /**
   * {@inheritdoc}
   */
  public function setEntityManager(EntityManagerInterface $entity_manager) {
    $this->entityManager = $entity_manager;
    return $this;
  }

436 437 438 439 440 441 442 443
  /**
   * {@inheritdoc}
   */
  public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
    return $this;
  }

444
}