Using formBuilder service to render formsfor Drupal 11 , 10 , 9 , and 8

Last updated :  

So far your form has appeared because a route pointed straight at it with _form. But sometimes you need a form to appear somewhere a route can't reach directly — inside a controller that also renders other content, or embedded in a block. The form_builder service is what lets you build and render a form programmatically, from any PHP code, not just from a dedicated route.

What you'll learn in this lesson

  • The difference between a route resolving to a form and building one manually
  • Using \Drupal::formBuilder() to get a form's render array on demand
  • Injecting the service properly instead of using the static shortcut

Building a form manually

// Inside a controller that also renders other content around the form:
public function build() {
  $build['intro'] = [
    '#markup' => $this->t('Please fill out the form below.'),
  ];
  $build['form'] = \Drupal::formBuilder()->getForm(ContactForm::class);
  return $build;
}

getForm() takes a form class (or an already-instantiated form object) and returns its fully-built render array — the exact same array a _form route would have gotten Drupal to build automatically, just obtained explicitly so you can combine it with other render array content around it.

Injecting the service properly

use Drupal\Core\Form\FormBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class MyController extends ControllerBase {

  public function __construct(
    protected FormBuilderInterface $formBuilder,
  ) {}

  public static function create(ContainerInterface $container) {
    return new static($container->get('form_builder'));
  }

  public function build() {
    $build['form'] = $this->formBuilder->getForm(ContactForm::class);
    return $build;
  }
}
Why prefer injection over the static \Drupal::formBuilder() shortcut? Exactly the same reasoning as any other service in Drupal: an injected dependency is explicit (visible in the constructor, easy to mock in a unit test) where a static call to \Drupal:: hides the dependency inside the method body, making the class harder to test in isolation. The static shortcut is fine for a quick script or a one-off procedural hook; a controller or service class should inject properly.

Passing arguments to the form

// Extra arguments after the class name get passed through to buildForm().
$build['form'] = $this->formBuilder->getForm(EditItemForm::class, $item_id);

// In the form class:
public function buildForm(array $form, FormStateInterface $form_state, $item_id = NULL) {
  // $item_id is available here, from the extra getForm() argument.
}

Quick check: you're writing a one-off drush command script, not a class. Is the static \Drupal::formBuilder() call or full dependency injection more appropriate? If you said the static call — a script has no constructor to inject into, and the testability benefit of injection doesn't apply the same way — you've understood that this is a real trade-off based on context, not a rule to follow blindly everywhere.

Key takeaways

  • form_builder's getForm() builds a form's render array programmatically, for use anywhere in PHP, not just from a dedicated route.
  • Extra arguments passed to getForm() after the class name get forwarded to buildForm()'s own parameters.
  • Prefer dependency injection over the static \Drupal::formBuilder() shortcut inside classes, for testability — the static form is fine for scripts and procedural code.

Coming up next

You've built a form both via a route and programmatically. Next, we look specifically at the two most common places a form actually gets displayed: dedicated routes, and blocks.