1

I have been struggling with the Drupal 8 Form API all afternoon. It seems that the API is not meant to create GET forms, while the comments state otherwise. I am trying to build sort of a filter-form that should pass it's parameters to the URL so that it can be bookmarked, etc.

My first problem is that the FormBuilder->getForm($myForm) method creates a FormState object that defaults to method POST. Even when I set my form's method to 'GET', the FormState object will check the request method before the form is even being created and since the requestmethod is GET and the FormState's method is POST it will not process anything at all.

So instead I tried to create my own FormState object (which I obviously set to GET) and call buildForm instead (that way bypassing the getForm logic):

$form = new MyForm();
$form_state = new FormState();
$form_state->setMethod('get');
$form = \Drupal::formBuilder()->buildForm($form, $form_state);

This seems to work better. However, as soon as I give my form a submit-button it then gets in some sort of redirect loop, which I haven't been able to pinpoint yet. I don't have such problems when using POST.

It's really weird that the API doesn't seem to be able to handle GET forms.

Also: Would I get this to work, the comments in the Form API code state that GET forms do not use a form-ID, however the FormInterface does require a getFormId method.

Could someone please clarify what I am doing wrong, or point out some documentation about this?


A proper example

I decided to simplify my testscenario a lot, in order to make sure that I wasn't doing anything weird. So now my testscenario is as follows:

I created a simple module with a hook_preprocess_node. There I render my form and add it to the template:

<?php

use Drupal\my_module\Form\TestForm;

function my_module_preprocess_node(&$variables) {
    $form = new TestForm();
    $variables['test_form'] = \Drupal::formBuilder()->getForm($form);
}

My form is as simple as I could make it:

<?php

namespace Drupal\my_module\Form;

use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class TestForm extends FormBase {

  /**
   * @inheritDoc
   */
  public function getFormId()
  {
    return 'test_form';
  }

  /**
   * @inheritDoc
   */
  public function buildForm(array $form, FormStateInterface $form_state)
  {
    $form_state->setMethod('GET');

    $form['test_input'] = [
      '#type' => 'textfield',
    ];

    $form['actions'] = array(
      '#type' => 'actions',
    );

    $form['actions']['submit'] = [
      '#name' => 'test-submit',
      '#id' => Html::getUniqueId('edit-submit-test-form'),
      '#type' => 'submit',
      '#value' => 'Submit it',
    ];

    return $form;
  }

  /**
   * @inheritDoc
   */
  public function validateForm(array &$form, FormStateInterface $form_state)
  {
    // TODO: Implement validateForm() method.
  }

  /**
   * @inheritDoc
   */
  public function submitForm(array &$form, FormStateInterface $form_state)
  {
    // TODO: Implement submitForm() method.
  }

}

Now my node template renders a simple form containing an input field and a submit button. When I submit the form it submits using a GET-request and I see the fields appear as GET-parameters. However the Drupal formbuilder does not pick up the form as being submitted. It does not call the validate nor submit methods.

The form contains both $form_state->setMethod('GET'); and $form['#method'] = 'GET'; as suggested in the comments.

3
  • I haven't created a GET form in Drupal 8, but I would doubt that the method can be set outside the form definition. You will likely need to call $form_state->setMethod() inside the form definition.
    – Jaypan
    Commented Mar 7, 2020 at 17:51
  • 1
    Did you try $form['#method'] = 'GET' inside of your custom form class?
    – Kevin
    Commented Mar 7, 2020 at 17:52
  • I did both of these things before, but now I have setup a seperated simplified example to test this again (see my updated post). It doesn't work.
    – vrijdenker
    Commented Mar 7, 2020 at 21:01

2 Answers 2

3

Here is an example of how to create a pure GET form with the form API, that does not require having to build a form state or create a custom form builder. With this example, the form can be called as any form (in a route, or by FormBuilder::getForm()).

namespace Drupal\example\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class TestForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'test_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    // Set the method.
    $form_state->setMethod('GET');

    // GET forms must not be cached, so that the page output responds without
    // caching.
    $form['#cache'] = [
      'max-age' => 0,
    ];

    // The after_build removes elements from GET parameters. See
    // TestForm::afterBuild().
    $form['#after_build'] = ['::afterBuild'];

    // Retrieve the parameter from the URL if it exists.
    if ($name = $this->getRequest()->query->get('name')) {
      // Show the user the submitted value.
      $form['display'] = [
        '#prefix' => '<p>',
        '#suffix' => '</p>',
        '#markup' => $this->t('Your name is @name', ['@name' => $name]),
      ];
    }

    $form['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Name'),
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Say your name'),
      // Set #name to an empty string so it does not get passed to the
      // GET parameters.
      '#name' => '',
    ];

    return $form;
  }

  /**
   * Custom after build to remove elements from being submitted as GET variables.
   */
  public function afterBuild(array $element, FormStateInterface $form_state) {
    // Remove the form_token, form_build_id and form_id from the GET parameters.
    unset($element['form_token']);
    unset($element['form_build_id']);
    unset($element['form_id']);

    return $element;
   }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {}

}
6
  • Thanks for the explanation. However in my case I'm writing a module with which is must be possible to pass in any form you create. I wouldn't want to have to place all this custom code in each of these forms, since that would create a lot of duplicate code.
    – vrijdenker
    Commented Mar 9, 2020 at 10:18
  • Well, that goes against the idea of keeping everything self contained and modular, which is Drupal style. Als, you shouldn't need to duplicate code, that's why you use base classes with methods and extend them OOP style.
    – Jaypan
    Commented Mar 9, 2020 at 15:45
  • I copied the idea from the core's Views Exposed Filters, so you're saying that the Drupal core goes against the Drupal style? I personally try to keep to the SOLID principles, so the Form shouldn't need to know how it's being rendered. I do that from the outside by using a specific renderer to choose how the form is being rendered. I believe that's how it should be.
    – vrijdenker
    Commented Mar 10, 2020 at 11:58
  • Views exposed filters have some weirdness going on about them, like trying to set default values. It's likely a result of this approach. And you already said you had to change your form submit to a button to make it work, which means that the form already has to 'know how its being rendered'.
    – Jaypan
    Commented Mar 10, 2020 at 16:18
  • Yeah, that was a bug I fixed. That was something else :)
    – vrijdenker
    Commented Mar 10, 2020 at 16:21
1

I have looked at Views's exposed filters to copy the behaviour I want. Views however manipulates the behaviour in several places. I created my own FormBuilder instead (which wraps around the core's FormBuilder) in order to get the behaviour I want.

<?php

namespace Drupal\my_module\Service;

use Drupal\Core\Form\FormBuilder as CoreFormBuilder;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormState;
use Drupal\my_module\Form\AbstractForm;

class FormBuilder {

  /**
   * @var CoreFormBuilder
   */
  private $formBuilder;

  public function __construct(CoreFormBuilder $formBuilder)
  {
    $this->formBuilder = $formBuilder;
  }

  /**
   * @param $form AbstractForm
   * @param $method string GET|POST
   */
  public function getForm(AbstractForm $form) {
    if ($form->getMethod() !== 'GET' && $form->getMethod() !== 'POST') {
      throw new \Exception('Method must be either GET or POST');
    }

    /**
     * Other than core's FormBuilder, we provide a way to set the form's method to GET
     * Note that the getMethod()-method is not a default FormInterface method.
     * It is part of our own AbstractForm which implements both Drupal\Core\Form\FormInterface,
     * plus our own needed interfaces.
     */
    $form_state = new FormState();
    $form_state->setMethod($form->getMethod());

    /**
     * Because we remove the form ID's from the GET request, 
     * we must set the form to always process.
     * And as a result of that, we must also disable the redirect,
     * or the submit process will become an endless loop.
     */
    if ($form->getMethod() === 'GET') {
      $form_state
        ->setAlwaysProcess()
        ->disableRedirect();
    }

    // Default behaviour copied from core's FormBuilder
    $args = func_get_args();
    // Remove $form_arg from the arguments.
    unset($args[0], $args[1]);
    $form_state->addBuildInfo('args', array_values($args));
    $build_form = $this->formBuilder->buildForm($form, $form_state);

    // Make sure that the form identifiers do not end up in a GET request
    if ($form->getMethod() === 'GET') {
      $build_form['form_build_id']['#access'] = FALSE;
      $build_form['form_token']['#access'] = FALSE;
      $build_form['form_id']['#access'] = FALSE;
    }

    return $build_form;
  }

}

By using this FormBuilder, it will:

  • remove form_id, form_build_id and form_token from the rendered GET-form, so that these won't be send to the GET request when the form is being submitted;
  • and because those identifiers are not there, the form_state will be set to setAlwaysProcess(), so that the form will be processed.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.