EntityForm.php 11.7 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\Core\Entity\EntityForm.
6 7
 */

8
namespace Drupal\Core\Entity;
9

10
use Drupal\Core\Form\FormBase;
11
use Drupal\Core\Extension\ModuleHandlerInterface;
12
use Drupal\Core\Form\FormStateInterface;
13
use Drupal\Core\Render\Element;
14

15
/**
16
 * Base class for entity forms.
17 18
 *
 * @ingroup entity_api
19
 */
20
class EntityForm extends FormBase implements EntityFormInterface {
21 22 23 24 25 26 27 28 29 30 31

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

32 33 34 35 36 37 38
  /**
   * The module handler service.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

39 40 41 42 43 44 45
  /**
   * The entity being used by this form.
   *
   * @var \Drupal\Core\Entity\EntityInterface
   */
  protected $entity;

46 47
  /**
   * {@inheritdoc}
48 49 50 51 52 53
   */
  public function setOperation($operation) {
    // If NULL is passed, do not overwrite the operation.
    if ($operation) {
      $this->operation = $operation;
    }
54
    return $this;
55 56 57
  }

  /**
58
   * {@inheritdoc}
59
   */
60
  public function getBaseFormID() {
61 62 63 64
    // 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.
65
    $base_form_id = $this->entity->getEntityTypeId() . '_form';
66
    if ($base_form_id == $this->getFormId()) {
67
      $base_form_id = NULL;
68 69
    }
    return $base_form_id;
70
  }
71

72 73 74
  /**
   * {@inheritdoc}
   */
75
  public function getFormId() {
76 77 78
    $form_id = $this->entity->getEntityTypeId();
    if ($this->entity->getEntityType()->hasKey('bundle')) {
      $form_id = $this->entity->bundle() . '_' . $form_id;
79 80 81 82 83 84 85 86 87 88
    }
    if ($this->operation != 'default') {
      $form_id = $form_id . '_' . $this->operation;
    }
    return $form_id . '_form';
  }

  /**
   * {@inheritdoc}
   */
89
  public function buildForm(array $form, FormStateInterface $form_state) {
90
    // During the initial form build, add this form object to the form state and
91 92 93
    // allow for initial preparation before form building and processing.
    if (!isset($form_state['controller'])) {
      $this->init($form_state);
94 95 96
    }

    // Retrieve the form array using the possibly updated entity in form state.
97
    $form = $this->form($form, $form_state);
98 99 100 101 102 103 104 105 106 107

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

    return $form;
  }

108 109 110
  /**
   * {@inheritdoc}
   */
111
  public function submitForm(array &$form, FormStateInterface $form_state) {
112 113
  }

114 115 116
  /**
   * Initialize the form state and the entity before the first form build.
   */
117
  protected function init(FormStateInterface $form_state) {
118
    // Add the form object to the form state so it can be easily accessed by
119 120
    // module-provided form handlers there.
    $form_state['controller'] = $this;
121

122
    // Prepare the entity to be presented in the entity form.
123
    $this->prepareEntity();
124

125 126
    // Invoke the prepare form hooks.
    $this->prepareInvokeAll('entity_prepare_form', $form_state);
127
    $this->prepareInvokeAll($this->entity->getEntityTypeId() . '_prepare_form', $form_state);
128 129 130 131 132
  }

  /**
   * Returns the actual form array to be built.
   *
133
   * @see \Drupal\Core\Entity\EntityForm::build()
134
   */
135
  public function form(array $form, FormStateInterface $form_state) {
136
    $entity = $this->entity;
137

138
    // Add a process callback.
139
    $form['#process'][] = '::processForm';
140

141 142 143 144 145 146
    if (!isset($form['langcode'])) {
      // If the form did not specify otherwise, default to keeping the existing
      // language of the entity or defaulting to the site default language for
      // new entities.
      $form['langcode'] = array(
        '#type' => 'value',
147
        '#value' => !$entity->isNew() ? $entity->language()->id : language_default()->id,
148 149
      );
    }
150 151 152
    return $form;
  }

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

163 164 165
    return $element;
  }

166 167 168
  /**
   * Returns the action form element for the current entity form.
   */
169
  protected function actionsElement(array $form, FormStateInterface $form_state) {
170 171
    $element = $this->actions($form, $form_state);

172
    if (isset($element['delete'])) {
173 174 175 176 177
      // Move the delete action as last one, unless weights are explicitly
      // provided.
      $delete = $element['delete'];
      unset($element['delete']);
      $element['delete'] = $delete;
178 179 180 181 182 183
      $element['delete']['#button_type'] = 'danger';
    }

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

    $count = 0;
187
    foreach (Element::children($element) as $action) {
188 189 190 191 192 193 194 195 196 197 198 199 200 201
      $element[$action] += array(
        '#weight' => ++$count * 5,
      );
    }

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

    return $element;
  }

  /**
   * Returns an array of supported actions for the current entity form.
202 203 204
   *
   * @todo Consider introducing a 'preview' action here, since it is used by
   *   many entity types.
205
   */
206
  protected function actions(array $form, FormStateInterface $form_state) {
207 208 209 210
    // @todo Rename the action key from submit to save.
    $actions['submit'] = array(
      '#type' => 'submit',
      '#value' => $this->t('Save'),
211 212
      '#validate' => array('::validate'),
      '#submit' => array('::submit', '::save'),
213
    );
214 215 216 217 218 219 220 221 222 223 224

    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);
      }
      $actions['delete'] = array(
        '#type' => 'link',
        '#title' => $this->t('Delete'),
225
        '#access' => $this->entity->access('delete'),
226 227 228 229 230 231 232 233
        '#attributes' => array(
          'class' => array('button', 'button--danger'),
        ),
      );
      $actions['delete'] += $route_info->toRenderArray();
    }

    return $actions;
234 235 236
  }

  /**
237
   * {@inheritdoc}
238
   */
239
  public function validate(array $form, FormStateInterface $form_state) {
240
    $this->updateFormLangcode($form_state);
241 242 243 244 245 246 247
    // @todo Remove this.
    // Execute legacy global validation handlers.
    unset($form_state['validate_handlers']);
    form_execute_handlers('validate', $form, $form_state);
  }

  /**
248
   * {@inheritdoc}
249 250 251 252 253 254 255 256 257
   *
   * This is the default entity object builder function. It is called before any
   * other submit handler to build the new entity object to be passed to the
   * 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
   * invoked handlers can retrieve a regular entity object to act on.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
258 259
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
260
   */
261
  public function submit(array $form, FormStateInterface $form_state) {
262 263
    // Remove button and internal Form API values from submitted values.
    form_state_values_clean($form_state);
264 265
    $this->entity = $this->buildEntity($form, $form_state);
    return $this->entity;
266 267 268 269 270 271 272
  }

  /**
   * Form submission handler for the 'save' action.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
273 274
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
275
   */
276
  public function save(array $form, FormStateInterface $form_state) {
277 278 279 280
    // @todo Perform common save operations.
  }

  /**
281
   * {@inheritdoc}
282
   */
283
  public function getFormLangcode(FormStateInterface $form_state) {
284
    return $this->entity->language()->id;
285 286
  }

287
  /**
288
   * {@inheritdoc}
289
   */
290
  public function isDefaultFormLangcode(FormStateInterface $form_state) {
291 292
    // The entity is not translatable, this is always the default language.
    return TRUE;
293 294 295
  }

  /**
296
   * Updates the form language to reflect any change to the entity language.
297
   *
298 299
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
300
   */
301
  protected function updateFormLangcode(FormStateInterface $form_state) {
302
    // Update the form language as it might have changed.
303 304
    if ($form_state->hasValue('langcode') && $this->isDefaultFormLangcode($form_state)) {
      $form_state['langcode'] = $form_state->getValue('langcode');
305
    }
306
  }
307

308
  /**
309
   * {@inheritdoc}
310
   */
311
  public function buildEntity(array $form, FormStateInterface $form_state) {
312
    $entity = clone $this->entity;
313
    $this->copyFormValuesToEntity($entity, $form, $form_state);
314 315 316 317 318

    // Invoke all specified builders for copying form values to entity
    // properties.
    if (isset($form['#entity_builders'])) {
      foreach ($form['#entity_builders'] as $function) {
319
        call_user_func_array($function, array($entity->getEntityTypeId(), $entity, &$form, &$form_state));
320 321 322
      }
    }

323 324 325
    return $entity;
  }

326 327 328 329 330 331 332 333
  /**
   * 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.
334 335
   * @param array $form
   *   A nested array of form elements comprising the form.
336 337
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
338
   */
339
  protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
340 341 342
    // @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.
343
    foreach ($form_state->getValues() as $key => $value) {
344 345 346 347
      $entity->set($key, $value);
    }
  }

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

355
  /**
356
   * {@inheritdoc}
357
   */
358 359 360
  public function setEntity(EntityInterface $entity) {
    $this->entity = $entity;
    return $this;
361 362 363 364 365
  }

  /**
   * Prepares the entity object before the form is built first.
   */
366 367 368 369 370 371 372
  protected function prepareEntity() {}

  /**
   * Invokes the specified prepare hook variant.
   *
   * @param string $hook
   *   The hook variant name.
373 374
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
375
   */
376
  protected function prepareInvokeAll($hook, FormStateInterface $form_state) {
377 378 379 380 381 382
    $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.
383
        $args = array($this->entity, $this->operation, &$form_state);
384 385 386
        call_user_func_array($function, $args);
      }
    }
387 388 389
  }

  /**
390
   * {@inheritdoc}
391 392 393 394
   */
  public function getOperation() {
    return $this->operation;
  }
395 396 397 398 399 400 401 402 403

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

404
}