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;
}
}
\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'sgetForm()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 tobuildForm()'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.