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