EntityForm.php

Same filename in other branches
  1. 9 core/lib/Drupal/Core/Entity/EntityForm.php
  2. 8.9.x core/lib/Drupal/Core/Entity/EntityForm.php
  3. 10 core/lib/Drupal/Core/Entity/EntityForm.php

Namespace

Drupal\Core\Entity

File

core/lib/Drupal/Core/Entity/EntityForm.php

View source
<?php

namespace Drupal\Core\Entity;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;

/**
 * Base class for entity forms.
 *
 * @ingroup entity_api
 */
class EntityForm extends FormBase implements EntityFormInterface {
    
    /**
     * The name of the current operation.
     *
     * Subclasses may use this to implement different behaviors depending on its
     * value.
     *
     * @var string
     */
    protected $operation;
    
    /**
     * The module handler service.
     *
     * @var \Drupal\Core\Extension\ModuleHandlerInterface
     */
    protected $moduleHandler;
    
    /**
     * The entity type manager.
     *
     * @var \Drupal\Core\Entity\EntityTypeManagerInterface
     */
    protected $entityTypeManager;
    
    /**
     * The entity being used by this form.
     *
     * @var \Drupal\Core\Entity\EntityInterface
     */
    protected $entity;
    
    /**
     * {@inheritdoc}
     */
    public function setOperation($operation) {
        // If NULL is passed, do not overwrite the operation.
        if ($operation) {
            $this->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;
    }

}

Classes

Title Deprecated Summary
EntityForm Base class for entity forms.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.