Change record status: 
Project: 
Introduced in branch: 
8.x
Description: 

hook_forms() has been removed, since the functionality can be achieved by class inheritance.

All forms that rely on hook_forms() need to be converted to a class that implements \Drupal\Core\Form\FormInterface, and which gets the desired form variation name injected:

Drupal 7

/**
 * Implements hook_forms().
 */
function foo_forms() {
  return array(
    'foo_bar_form' => 'foo_form',
    'foo_baz_form' => 'foo_form',
    'foo_qux_form' => 'foo_form',
  );
}

/**
 * Form constructor for foo_form().
 */
function foo_form(array $form, array &$form_state) {
  $form['bar'] = array(
    '#title' => t('Bar'),
    '#type' => 'textfield',
  );
  return $form;
}

/**
 * Menu page callback: Presents the Foo form.
 */
function foo_page() {
  return drupal_get_form('foo_bar_form');
}

Drupal 8

class FooForm implements FormInterface {

  protected $type;
 
  public function __construct($type) {
    $this->type = $type;
  }

  public function getFormId() {
    return 'foo_' . $this->type . '_form';
  }
}

class FooRouteController {

  public function fooBarForm($type) {
    $form = new FooForm('bar');

    return \Drupal::formBuilder()->getForm($form);
  }
}
Impacts: 
Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other: 
Other updates done

Comments

jedihe’s picture

For forms using Dependency Injection, the example shown in the change record won't work that easily. A possible approach in that case is this:

Example form, with ->getformId() resolving to a unique value (via the $location property):


namespace Drupal\my_module\Form;

use Drupal\Core\Url;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Form with dynamic ID and AJAX.
 */
class MyForm extends FormBase {

  /**
   * The email validator.
   *
   * @var \Egulias\EmailValidator\EmailValidator
   */
  protected $emailValidator;

  /**
   * The form 'location'.
   *
   * @var string
   */
  protected $location;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    $instance = parent::create($container);
    $instance->emailValidator = $container->get('email.validator');
    $instance->location = '';

    return $instance;
  }

  /**
   * Sets the location for the form.
   *
   * @param string $location
   *   The location value for this form; must be unique for the current
   *   request, to ensure uniqueness of the form ID (and proper working AJAX,
   *   etc).
   */
  public function setLocation($location) {
    $this->location = $location;
  }

  /**
   * Build the form.
   *
   * @param array $form
   *   Default form array structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Object containing current form state.
   * @param array $params
   *   Additional parameters for the form.
   *   - param1: controls something.
   *
   * @return array
   *   The form renderable.
   */
  public function buildForm(array $form, FormStateInterface $form_state, array $params = NULL) {
    $param1 = $params['param1'];

    $form = [];

    // Force the id attribute in the form tag. This is safe to do, as long as
    // the 'location' value is unique for each instance of the form in a given
    // HTML document.
    $form['#id'] = $this->getFormId();

    // ... Add elements to $form.

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
      '#ajax' => [
        'wrapper' => $this->getFormId(),
        'callback' => '::ajaxCallback',
        'method' => 'replace',
        'effect' => 'none',
        'event' => 'click',
        'disable-refocus' => TRUE,
      ],
    ];

    // It may be useful to have the location value available for
    // validation/submission.
    if (!empty($this->location)) {
      $form['location'] = [
        '#type' => 'hidden',
        '#value' => $this->location,
      ];
    }

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'my_form_id_' . $this->location;
  }

  /**
   * Form submit handler.
   *
   * @param array $form
   *   The render array of the currently built form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Object describing the current state of the form.
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->messenger()->addMessage($this->t('Submitted location: @loc', ['@loc' => $form_state->getValue('location')]));
  }

  /**
   * Ajax callback handler.
   *
   * @param array $form
   *   The render array of the currently built form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Object describing the current state of the form.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The AJAX response.
   */
  public function ajaxCallback(array &$form, FormStateInterface $form_state) {
    return $form;
  }

}

Each unique instance of the form can be created with a helper function like this:

function my_module_get_form_instance($location) {
  $options = [
    'param1' => 'value1',
  ];
  $form_obj = \Drupal::service('class_resolver')->getInstanceFromDefinition('Drupal\my_module\Form\MyForm');
  $form_obj->setLocation($location);
  $form_renderable = \Drupal::formBuilder()->getForm($form_obj, $options);
  return $form_renderable;
}

Notice that this helper method can be converted to a lazy builder, so that the form can be rendered in a way that doesn't prevent caching by Dynamic Page Cache (for logged-in users, the form token varies per-user, so it causes an 'UNCACHEABLE' situation).