How can I create a GET form with the Form API? 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-&gt;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-&gt;setMethod('get');
$form = \Drupal::formBuilder()-&gt;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:

&lt;?php

use Drupal\my_module\Form\TestForm;

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


My form is as simple as I could make it:

&lt;?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-&gt;setMethod('GET');

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

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

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

    return $form;
  }

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

  /**
   * @inheritDoc
   */
  public function submitForm(array &amp;$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-&gt;setMethod('GET'); and $form['#method'] = 'GET'; as suggested in the comments.
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.

&lt;?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-&gt;formBuilder = $formBuilder;
  }

  /**
   * @param $form AbstractForm
   * @param $method string GET|POST
   */
  public function getForm(AbstractForm $form) {
    if ($form-&gt;getMethod() !== 'GET' &amp;&amp; $form-&gt;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-&gt;setMethod($form-&gt;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-&gt;getMethod() === 'GET') {
      $form_state
        -&gt;setAlwaysProcess()
        -&gt;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-&gt;addBuildInfo('args', array_values($args));
    $build_form = $this-&gt;formBuilder-&gt;buildForm($form, $form_state);

    // Make sure that the form identifiers do not end up in a GET request
    if ($form-&gt;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.