operation = $operation; } return $this; } /** * {@inheritdoc} */ public function getBaseFormId() { // Assign ENTITY_TYPE_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->getEntityTypeId() . '_form'; if ($base_form_id == $this->getFormId()) { $base_form_id = NULL; } return $base_form_id; } /** * {@inheritdoc} */ public function getFormId() { $form_id = $this->entity->getEntityTypeId(); if ($this->entity->getEntityType()->hasKey('bundle')) { $form_id .= '_' . $this->entity->bundle(); } if ($this->operation != 'default') { $form_id = $form_id . '_' . $this->operation; } return $form_id . '_form'; } /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { // During the initial form build, add this form object to the form state and // allow for initial preparation before form building and processing. if (!$form_state->has('entity_form_initialized')) { $this->init($form_state); } // 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); } // Retrieve the form array using the possibly updated entity in form state. $form = $this->form($form, $form_state); // 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. */ protected function init(FormStateInterface $form_state) { // Flag that this form has been initialized. $form_state->set('entity_form_initialized', TRUE); // Prepare the entity to be presented in the entity form. $this->prepareEntity(); // Invoke the prepare form hooks. $this->prepareInvokeAll('entity_prepare_form', $form_state); $this->prepareInvokeAll($this->entity->getEntityTypeId() . '_prepare_form', $form_state); } /** * Gets the actual form array to be built. * * @see \Drupal\Core\Entity\EntityForm::processForm() * @see \Drupal\Core\Entity\EntityForm::afterBuild() */ public function form(array $form, FormStateInterface $form_state) { // Add #process and #after_build callbacks. $form['#process'][] = '::processForm'; $form['#after_build'][] = '::afterBuild'; return $form; } /** * Process callback: assigns weights and hides extra fields. * * @see \Drupal\Core\Entity\EntityForm::form() */ public function processForm($element, FormStateInterface $form_state, $form) { // 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->getFormObject()->getEntity(); return $element; } /** * 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; } /** * Returns the action form element for the current entity form. */ protected function actionsElement(array $form, FormStateInterface $form_state) { $element = $this->actions($form, $form_state); if (isset($element['delete'])) { // Move the delete action as last one, unless weights are explicitly // provided. $delete = $element['delete']; unset($element['delete']); $element['delete'] = $delete; $element['delete']['#button_type'] = 'danger'; } if (isset($element['submit'])) { // Give the primary submit button a #button_type of primary. $element['submit']['#button_type'] = 'primary'; } $count = 0; foreach (Element::children($element) as $action) { $element[$action] += [ '#weight' => ++$count * 5, ]; } if (!empty($element)) { $element['#type'] = 'actions'; } return $element; } /** * Returns an array of supported actions for the current entity form. * * 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. * * @todo Consider introducing a 'preview' action here, since it is used by * many entity types. */ protected function actions(array $form, FormStateInterface $form_state) { // @todo Consider renaming the action key from submit to save. The impacts // are hard to predict. For example, see // \Drupal\language\Element\LanguageConfiguration::processLanguageConfiguration(). $actions['submit'] = [ '#type' => 'submit', '#value' => $this->t('Save'), '#submit' => ['::submitForm', '::save'], ]; if (!$this->entity->isNew() && $this->entity->hasLinkTemplate('delete-form')) { $route_info = $this->entity->toUrl('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'] = [ '#type' => 'link', '#title' => $this->t('Delete'), '#access' => $this->entity->access('delete'), '#attributes' => [ 'class' => ['button', 'button--danger', 'use-ajax'], 'data-dialog-type' => 'modal', 'data-dialog-options' => Json::encode([ 'width' => 880, ]), ], '#url' => $route_info, '#attached' => [ 'library' => ['core/drupal.dialog.ajax'], ], ]; } return $actions; } /** * {@inheritdoc} * * This is the default entity object builder function. It is called before any * other submit handler to build the new entity object to be used by 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. Generally * this method should not be overridden unless the entity requires the same * preparation for two actions, see \Drupal\comment\CommentForm for an example * with the save and preview actions. * * @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. */ public function submitForm(array &$form, FormStateInterface $form_state) { // Remove button and internal Form API values from submitted values. $form_state->cleanValues(); $this->entity = $this->buildEntity($form, $form_state); } /** * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { return $this->entity->save(); } /** * {@inheritdoc} */ public function buildEntity(array $form, FormStateInterface $form_state) { $entity = clone $this->entity; $this->copyFormValuesToEntity($entity, $form, $form_state); // Invoke all specified builders for copying form values to entity // properties. if (isset($form['#entity_builders'])) { foreach ($form['#entity_builders'] as $function) { call_user_func_array($form_state->prepareCallback($function), [$entity->getEntityTypeId(), $entity, &$form, &$form_state]); } } return $entity; } /** * 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. * @param array $form * A nested array of form elements comprising the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * * @see \Drupal\Core\Form\ConfigFormBase::copyFormValuesToConfig() */ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { $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()); } // @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. foreach ($values as $key => $value) { $entity->set($key, $value); } } /** * {@inheritdoc} */ public function getEntity() { return $this->entity; } /** * {@inheritdoc} */ public function setEntity(EntityInterface $entity) { $this->entity = $entity; return $this; } /** * {@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 { $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); } return $entity; } /** * Prepares the entity object before the form is built first. */ protected function prepareEntity() {} /** * Invokes the specified prepare hook variant. * * @param string $hook * The hook variant name. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. */ protected function prepareInvokeAll($hook, FormStateInterface $form_state) { $this->moduleHandler->invokeAllWith($hook, function (callable $hook, string $module) use ($form_state) { // Ensure we pass an updated translation object and form display at // each invocation, since they depend on form state which is alterable. $hook($this->entity, $this->operation, $form_state); }); } /** * {@inheritdoc} */ public function getOperation() { return $this->operation; } /** * {@inheritdoc} */ public function setModuleHandler(ModuleHandlerInterface $module_handler) { $this->moduleHandler = $module_handler; return $this; } /** * {@inheritdoc} */ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) { $this->entityTypeManager = $entity_type_manager; return $this; } }