EntityFormController.php 18.2 KB
Newer Older
1 2 3 4
<?php

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

8
namespace Drupal\Core\Entity;
9

10
use Drupal\Core\Form\FormBase;
11
use Drupal\entity\EntityFormDisplayInterface;
12
use Drupal\Core\Extension\ModuleHandlerInterface;
13
use Drupal\Core\Language\Language;
14
use Symfony\Component\DependencyInjection\ContainerInterface;
15

16 17 18
/**
 * Base class for entity form controllers.
 */
19
class EntityFormController extends FormBase implements EntityFormControllerInterface {
20 21 22 23 24 25 26 27 28 29 30

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

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

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

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

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

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
  /**
   * {@inheritdoc}
   */
  public function getFormID() {
    $entity_type = $this->entity->entityType();
    $bundle = $this->entity->bundle();
    $form_id = $entity_type;
    if ($bundle != $entity_type) {
      $form_id = $bundle . '_' . $form_id;
    }
    if ($this->operation != 'default') {
      $form_id = $form_id . '_' . $this->operation;
    }
    return $form_id . '_form';
  }

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

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

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

    return $form;
  }

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

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

123 124 125 126 127
    // Ensure we act on the translation object corresponding to the current form
    // language.
    $this->entity = $this->getTranslatedEntity($form_state);

    // Prepare the entity to be presented in the entity form.
128
    $this->prepareEntity();
129

130
    $form_display = entity_get_render_form_display($this->entity, $this->getOperation());
131 132 133 134 135

    // Let modules alter the form display.
    $form_display_context = array(
      'entity_type' => $this->entity->entityType(),
      'bundle' => $this->entity->bundle(),
136
      'form_mode' => $this->getOperation(),
137
    );
138
    $this->moduleHandler->alter('entity_form_display', $form_display, $form_display_context);
139 140

    $this->setFormDisplay($form_display, $form_state);
141 142 143 144

    // Invoke the prepare form hooks.
    $this->prepareInvokeAll('entity_prepare_form', $form_state);
    $this->prepareInvokeAll($this->entity->entityType() . '_prepare_form', $form_state);
145 146 147 148 149
  }

  /**
   * Returns the actual form array to be built.
   *
150
   * @see Drupal\Core\Entity\EntityFormController::build()
151
   */
152 153
  public function form(array $form, array &$form_state) {
    $entity = $this->entity;
154
    // @todo Exploit the Field API to generate the default widgets for the
155 156 157
    // entity properties.
    $info = $entity->entityInfo();
    if (!empty($info['fieldable'])) {
158
      field_attach_form($entity, $form, $form_state, $this->getFormLangcode($form_state));
159
    }
160

161 162
    // Add a process callback so we can assign weights and hide extra fields.
    $form['#process'][] = array($this, 'processForm');
163

164 165 166 167 168 169
    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',
170
        '#value' => !$entity->isNew() ? $entity->getUntranslated()->language()->id : language_default()->id,
171 172
      );
    }
173 174 175
    return $form;
  }

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
  /**
   * Process callback: assigns weights and hides extra fields.
   *
   * @see \Drupal\Core\Entity\EntityFormController::form()
   */
  public function processForm($element, $form_state, $form) {
    // Assign the weights configured in the form display.
    foreach ($this->getFormDisplay($form_state)->getComponents() as $name => $options) {
      if (isset($element[$name])) {
        $element[$name]['#weight'] = $options['weight'];
      }
    }

    // Hide extra fields.
    $extra_fields = field_info_extra_fields($this->entity->entityType(), $this->entity->bundle(), 'form');
    foreach ($extra_fields as $extra_field => $info) {
      if (!$this->getFormDisplay($form_state)->getComponent($extra_field)) {
        $element[$extra_field]['#access'] = FALSE;
      }
    }

    return $element;
  }

200 201 202 203 204 205 206
  /**
   * Returns the action form element for the current entity form.
   */
  protected function actionsElement(array $form, array &$form_state) {
    $element = $this->actions($form, $form_state);

    // We cannot delete an entity that has not been created yet.
207
    if ($this->entity->isNew()) {
208 209 210 211 212 213 214 215
      unset($element['delete']);
    }
    elseif (isset($element['delete'])) {
      // Move the delete action as last one, unless weights are explicitly
      // provided.
      $delete = $element['delete'];
      unset($element['delete']);
      $element['delete'] = $delete;
216 217 218 219 220 221
      $element['delete']['#button_type'] = 'danger';
    }

    if (isset($element['submit'])) {
      // Give the primary submit button a #button_type of primary.
      $element['submit']['#button_type'] = 'primary';
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
    }

    $count = 0;
    foreach (element_children($element) as $action) {
      $element[$action] += array(
        '#type' => 'submit',
        '#weight' => ++$count * 5,
      );
    }

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

    return $element;
  }

  /**
   * Returns an array of supported actions for the current entity form.
   */
  protected function actions(array $form, array &$form_state) {
    return array(
      // @todo Rename the action key from submit to save.
      'submit' => array(
246
        '#value' => $this->t('Save'),
247 248 249 250 251 252 253 254 255
        '#validate' => array(
          array($this, 'validate'),
        ),
        '#submit' => array(
          array($this, 'submit'),
          array($this, 'save'),
        ),
      ),
      'delete' => array(
256
        '#value' => $this->t('Delete'),
257 258 259 260 261 262 263 264 265 266 267
        // No need to validate the form when deleting the entity.
        '#submit' => array(
          array($this, 'delete'),
        ),
      ),
      // @todo Consider introducing a 'preview' action here, since it is used by
      // many entity types.
    );
  }

  /**
268
   * Implements \Drupal\Core\Entity\EntityFormControllerInterface::validate().
269 270 271
   */
  public function validate(array $form, array &$form_state) {
    $entity = $this->buildEntity($form, $form_state);
272
    $entity_type = $entity->entityType();
273
    $entity_langcode = $entity->language()->id;
274

275 276 277 278 279 280 281 282 283 284 285 286 287 288
    $violations = array();

    // @todo Simplify when all entity types are converted to EntityNG.
    if ($entity instanceof EntityNG) {
      foreach ($entity as $field_name => $field) {
        $field_violations = $field->validate();
        if (count($field_violations)) {
          $violations[$field_name] = $field_violations;
        }
      }
    }
    else {
      // For BC entities, iterate through each field instance and
      // instantiate NG items objects manually.
289 290 291
      $definitions = \Drupal::entityManager()->getFieldDefinitions($entity_type, $entity->bundle());
      foreach (field_info_instances($entity_type, $entity->bundle()) as $field_name => $instance) {
        $langcode = field_is_translatable($entity_type, $instance->getField()) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED;
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307

        // Create the field object.
        $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
        // @todo Exception : calls setValue(), tries to set the 'formatted'
        // property.
        $field = \Drupal::typedData()->create($definitions[$field_name], $items, $field_name, $entity);
        $field_violations = $field->validate();
        if (count($field_violations)) {
          $violations[$field->getName()] = $field_violations;
        }
      }
    }

    // Map errors back to form elements.
    if ($violations) {
      foreach ($violations as $field_name => $field_violations) {
308
        $langcode = field_is_translatable($entity_type , field_info_field($entity_type, $field_name)) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED;
309 310 311 312 313 314
        $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state);
        $field_state['constraint_violations'] = $field_violations;
        field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state);
      }

      field_invoke_method('flagErrors', _field_invoke_widget_target($form_state['form_display']), $entity, $form, $form_state);
315 316 317 318 319 320 321 322 323
    }

    // @todo Remove this.
    // Execute legacy global validation handlers.
    unset($form_state['validate_handlers']);
    form_execute_handlers('validate', $form, $form_state);
  }

  /**
324
   * Implements \Drupal\Core\Entity\EntityFormControllerInterface::submit().
325 326 327 328 329 330 331 332 333 334 335 336 337
   *
   * 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.
   * @param array $form_state
   *   A reference to a keyed array containing the current state of the form.
   */
  public function submit(array $form, array &$form_state) {
338 339 340
    // Remove button and internal Form API values from submitted values.
    form_state_values_clean($form_state);

341
    $this->updateFormLangcode($form_state);
342
    $this->submitEntityLanguage($form, $form_state);
343 344
    $this->entity = $this->buildEntity($form, $form_state);
    return $this->entity;
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
  }

  /**
   * Form submission handler for the 'save' action.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param array $form_state
   *   A reference to a keyed array containing the current state of the form.
   */
  public function save(array $form, array &$form_state) {
    // @todo Perform common save operations.
  }

  /**
   * Form submission handler for the 'delete' action.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param array $form_state
   *   A reference to a keyed array containing the current state of the form.
   */
  public function delete(array $form, array &$form_state) {
    // @todo Perform common delete operations.
  }

  /**
372
   * Implements \Drupal\Core\Entity\EntityFormControllerInterface::getFormLangcode().
373
   */
374
  public function getFormLangcode(array $form_state) {
375
    $entity = $this->entity;
376 377 378 379 380 381 382 383

    if (!empty($form_state['langcode'])) {
      $langcode = $form_state['langcode'];
    }
    else {
      // If no form langcode was provided we default to the current content
      // language and inspect existing translations to find a valid fallback,
      // if any.
384
      $translations = $entity->getTranslationLanguages();
385
      $langcode = language(Language::TYPE_CONTENT)->id;
386 387 388 389 390 391 392 393
      $fallback = language_multilingual() ? language_fallback_get_candidates() : array();
      while (!empty($langcode) && !isset($translations[$langcode])) {
        $langcode = array_shift($fallback);
      }
    }

    // If the site is not multilingual or no translation for the given form
    // language is available, fall back to the entity language.
394
    return !empty($langcode) ? $langcode : $entity->getUntranslated()->language()->id;
395 396
  }

397
  /**
398
   * Implements \Drupal\Core\Entity\EntityFormControllerInterface::isDefaultFormLangcode().
399
   */
400
  public function isDefaultFormLangcode(array $form_state) {
401
    return $this->getFormLangcode($form_state) == $this->entity->getUntranslated()->language()->id;
402 403 404
  }

  /**
405
   * Updates the form language to reflect any change to the entity language.
406 407
   *
   * @param array $form_state
408
   *   A reference to a keyed array containing the current state of the form.
409
   */
410
  protected function updateFormLangcode(array &$form_state) {
411 412 413 414
    // Update the form language as it might have changed.
    if (isset($form_state['values']['langcode']) && $this->isDefaultFormLangcode($form_state)) {
      $form_state['langcode'] = $form_state['values']['langcode'];
    }
415
  }
416

417 418 419 420 421 422 423 424 425
  /**
   * Handle possible entity language changes.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param array $form_state
   *   A reference to a keyed array containing the current state of the form.
   */
  protected function submitEntityLanguage(array $form, array &$form_state) {
426
    $entity = $this->entity;
427 428 429 430 431 432 433
    $entity_type = $entity->entityType();

    if (field_has_translation_handler($entity_type)) {
      // If we are editing the default language values, we use the submitted
      // entity language as the new language for fields to handle any language
      // change. Otherwise the current form language is the proper value, since
      // in this case it is not supposed to change.
434
      $current_langcode = $this->isDefaultFormLangcode($form_state) ? $form_state['values']['langcode'] : $this->getFormLangcode($form_state);
435 436

      foreach (field_info_instances($entity_type, $entity->bundle()) as $instance) {
437 438
        $field = $instance->getField();
        $field_name = $field->name;
439 440 441 442 443 444 445 446 447
        if (isset($form[$field_name]['#language'])) {
          $previous_langcode = $form[$field_name]['#language'];

          // Handle a possible language change: new language values are inserted,
          // previous ones are deleted.
          if ($field['translatable'] && $previous_langcode != $current_langcode) {
            $form_state['values'][$field_name][$current_langcode] = $form_state['values'][$field_name][$previous_langcode];
            $form_state['values'][$field_name][$previous_langcode] = array();
          }
448 449 450 451 452
        }
      }
    }
  }

453
  /**
454
   * Implements \Drupal\Core\Entity\EntityFormControllerInterface::buildEntity().
455 456
   */
  public function buildEntity(array $form, array &$form_state) {
457
    $entity = clone $this->entity;
458 459 460 461
    // If you submit a form, the form state comes from caching, which forces
    // the controller to be the one before caching. Ensure to have the
    // controller of the current request.
    $form_state['controller'] = $this;
462
    // @todo Move entity_form_submit_build_entity() here.
463
    // @todo Exploit the Field API to process the submitted entity field.
464
    entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state, array('langcode' => $this->getFormLangcode($form_state)));
465 466 467 468
    return $entity;
  }

  /**
469
   * Implements \Drupal\Core\Entity\EntityFormControllerInterface::getEntity().
470
   */
471 472
  public function getEntity() {
    return $this->entity;
473 474
  }

475 476 477 478 479 480 481 482
  /**
   * Returns the translation object corresponding to the form language.
   *
   * @param array $form_state
   *   A keyed array containing the current state of the form.
   */
  protected function getTranslatedEntity(array $form_state) {
    $langcode = $this->getFormLangcode($form_state);
483
    return $this->entity->getTranslation($langcode);
484 485
  }

486
  /**
487
   * Implements \Drupal\Core\Entity\EntityFormControllerInterface::setEntity().
488
   */
489 490 491
  public function setEntity(EntityInterface $entity) {
    $this->entity = $entity;
    return $this;
492 493 494 495 496
  }

  /**
   * Prepares the entity object before the form is built first.
   */
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
  protected function prepareEntity() {}

  /**
   * Invokes the specified prepare hook variant.
   *
   * @param string $hook
   *   The hook variant name.
   * @param array $form_state
   *   An associative array containing the current state of the form.
   */
  protected function prepareInvokeAll($hook, array &$form_state) {
    $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.
514
        $args = array($this->getTranslatedEntity($form_state), $this->getFormDisplay($form_state), $this->operation, &$form_state);
515 516 517
        call_user_func_array($function, $args);
      }
    }
518 519
  }

520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
  /**
   * {@inheritdoc}
   */
  public function getFormDisplay(array $form_state) {
    return isset($form_state['form_display']) ? $form_state['form_display'] : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function setFormDisplay(EntityFormDisplayInterface $form_display, array &$form_state) {
    $form_state['form_display'] = $form_display;
    return $this;
  }

535
  /**
536
   * Implements \Drupal\Core\Entity\EntityFormControllerInterface::getOperation().
537 538 539 540
   */
  public function getOperation() {
    return $this->operation;
  }
541 542 543 544 545 546 547 548 549

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

550
}