You are here

public function FormBuilder::buildForm in Drupal 10

Same name and namespace in other branches
  1. 8 core/lib/Drupal/Core/Form/FormBuilder.php \Drupal\Core\Form\FormBuilder::buildForm()
  2. 9 core/lib/Drupal/Core/Form/FormBuilder.php \Drupal\Core\Form\FormBuilder::buildForm()

File

core/lib/Drupal/Core/Form/FormBuilder.php, line 225

Class

FormBuilder
Provides form building and processing.

Namespace

Drupal\Core\Form

Code

public function buildForm($form_arg, FormStateInterface &$form_state) {

  // Ensure the form ID is prepared.
  $form_id = $this
    ->getFormId($form_arg, $form_state);
  $request = $this->requestStack
    ->getCurrentRequest();

  // Inform $form_state about the request method that's building it, so that
  // it can prevent persisting state changes during HTTP methods for which
  // that is disallowed by HTTP: GET and HEAD.
  $form_state
    ->setRequestMethod($request
    ->getMethod());

  // Initialize the form's user input. The user input should include only the
  // input meant to be treated as part of what is submitted to the form, so
  // we base it on the form's method rather than the request's method. For
  // example, when someone does a GET request for
  // /node/add/article?destination=foo, which is a form that expects its
  // submission method to be POST, the user input during the GET request
  // should be initialized to empty rather than to ['destination' => 'foo'].
  $input = $form_state
    ->getUserInput();
  if (!isset($input)) {
    $input = $form_state
      ->isMethodType('get') ? $request->query
      ->all() : $request->request
      ->all();
    $form_state
      ->setUserInput($input);
  }
  if (isset($_SESSION['batch_form_state'])) {

    // We've been redirected here after a batch processing. The form has
    // already been processed, but needs to be rebuilt. See _batch_finished().
    $form_state = $_SESSION['batch_form_state'];
    unset($_SESSION['batch_form_state']);
    return $this
      ->rebuildForm($form_id, $form_state);
  }

  // If the incoming input contains a form_build_id, we'll check the cache for
  // a copy of the form in question. If it's there, we don't have to rebuild
  // the form to proceed. In addition, if there is stored form_state data from
  // a previous step, we'll retrieve it so it can be passed on to the form
  // processing code.
  $check_cache = isset($input['form_id']) && $input['form_id'] == $form_id && !empty($input['form_build_id']);
  if ($check_cache) {
    $form = $this
      ->getCache($input['form_build_id'], $form_state);
  }

  // If the previous bit of code didn't result in a populated $form object, we
  // are hitting the form for the first time and we need to build it from
  // scratch.
  if (!isset($form)) {

    // If we attempted to serve the form from cache, uncacheable $form_state
    // keys need to be removed after retrieving and preparing the form, except
    // any that were already set prior to retrieving the form.
    if ($check_cache) {
      $form_state_before_retrieval = clone $form_state;
    }
    $form = $this
      ->retrieveForm($form_id, $form_state);
    $this
      ->prepareForm($form_id, $form, $form_state);

    // self::setCache() removes uncacheable $form_state keys (see properties
    // in \Drupal\Core\Form\FormState) in order for multi-step forms to work
    // properly. This means that form processing logic for single-step forms
    // using $form_state->isCached() may depend on data stored in those keys
    // during self::retrieveForm()/self::prepareForm(), but form processing
    // should not depend on whether the form is cached or not, so $form_state
    // is adjusted to match what it would be after a
    // self::setCache()/self::getCache() sequence. These exceptions are
    // allowed to survive here:
    // - always_process: Does not make sense in conjunction with form caching
    //   in the first place, since passing form_build_id as a GET parameter is
    //   not desired.
    // - temporary: Any assigned data is expected to survives within the same
    //   page request.
    if ($check_cache) {
      $cache_form_state = $form_state
        ->getCacheableArray();
      $cache_form_state['always_process'] = $form_state
        ->getAlwaysProcess();
      $cache_form_state['temporary'] = $form_state
        ->getTemporary();
      $form_state = $form_state_before_retrieval;
      $form_state
        ->setFormState($cache_form_state);
    }
  }

  // If this form is an AJAX request, disable all form redirects.
  if ($ajax_form_request = $request->query
    ->has(static::AJAX_FORM_REQUEST)) {
    $form_state
      ->disableRedirect();
  }

  // Now that we have a constructed form, process it. This is where:
  // - Element #process functions get called to further refine $form.
  // - User input, if any, gets incorporated in the #value property of the
  //   corresponding elements and into $form_state->getValues().
  // - Validation and submission handlers are called.
  // - If this submission is part of a multistep workflow, the form is rebuilt
  //   to contain the information of the next step.
  // - If necessary, the form and form state are cached or re-cached, so that
  //   appropriate information persists to the next page request.
  // All of the handlers in the pipeline receive $form_state by reference and
  // can use it to know or update information about the state of the form.
  $response = $this
    ->processForm($form_id, $form, $form_state);

  // In case the post request exceeds the configured allowed size
  // (post_max_size), the post request is potentially broken. Add some
  // protection against that and at the same time have a nice error message.
  if ($ajax_form_request && !$request->request
    ->has('form_id')) {
    throw new BrokenPostRequestException($this
      ->getFileUploadMaxSize());
  }

  // After processing the form, if this is an AJAX form request, interrupt
  // form rendering and return by throwing an exception that contains the
  // processed form and form state. This exception will be caught by
  // \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber::onException() and
  // then passed through
  // \Drupal\Core\Form\FormAjaxResponseBuilderInterface::buildResponse() to
  // build a proper AJAX response.
  // Only do this when the form ID matches, since there is no guarantee from
  // $ajax_form_request that it's an AJAX request for this particular form.
  if ($ajax_form_request && $form_state
    ->isProcessingInput() && $request->request
    ->get('form_id') == $form_id) {
    throw new FormAjaxException($form, $form_state);
  }

  // If the form returns a response, skip subsequent page construction by
  // throwing an exception.
  // @see Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber
  //
  // @todo Exceptions should not be used for code flow control. However, the
  //   Form API does not integrate with the HTTP Kernel based architecture of
  //   Drupal 8. In order to resolve this issue properly it is necessary to
  //   completely separate form submission from rendering.
  //   @see https://www.drupal.org/node/2367555
  if ($response instanceof Response) {
    throw new EnforcedResponseException($response);
  }

  // If this was a successful submission of a single-step form or the last
  // step of a multi-step form, then self::processForm() issued a redirect to
  // another page, or back to this page, but as a new request. Therefore, if
  // we're here, it means that this is either a form being viewed initially
  // before any user input, or there was a validation error requiring the form
  // to be re-displayed, or we're in a multi-step workflow and need to display
  // the form's next step. In any case, we have what we need in $form, and can
  // return it for rendering.
  return $form;
}