A form built with FormBuilderInterface doesn't need a dedicated page or route to exist — it can be dropped anywhere a render array is accepted, including a sidebar block. Think of the form as a self-contained appliance: it doesn't care which room of the house it's plugged into. A block plugin's build() method is just another socket.
What you'll learn in this lesson
- How to inject the form builder service into a block plugin
- How
getForm()turns a form class into a ready-to-render array - Why the form's own cacheability metadata still applies even though it's inside a block
- A real gotcha with block caching and forms that carry per-request state
Injecting the form builder into a block plugin
A block plugin gets its dependencies through the same create()/constructor pattern as any other injectable Drupal class. The service you need here is form_builder:
namespace Drupal\mymodule\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'Quick contact' block.
*
* @Block(
* id = "quick_contact_block",
* admin_label = @Translation("Quick contact")
* )
*/
class QuickContactBlock extends BlockBase implements ContainerFactoryPluginInterface {
protected FormBuilderInterface $formBuilder;
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = new static($configuration, $plugin_id, $plugin_definition);
$instance->formBuilder = $container->get('form_builder');
return $instance;
}
public function build() {
return $this->formBuilder->getForm('Drupal\mymodule\Form\QuickContactForm');
}
}
getForm() takes a fully-qualified form class name and returns a complete, ready-to-render array — the exact same array a route's _form controller would produce. The block's build() method just hands that array straight back; there's no form-specific logic in the block plugin itself.
The form's own metadata still governs the block
Because getForm() returns a genuine render array, it carries its own cache metadata, contextual links, and attached libraries — all of which get merged into the surrounding block's render output automatically. You don't need to declare anything extra in the block plugin for the form's CSRF token or AJAX behaviors to keep working; they travel with the array.
getCacheContexts() to include 'user' — otherwise one user's cached block render can leak into another user's page. This is the same cache-context discipline that applies to any dynamic content in a block, forms included.Quick check: does the block plugin need to implement its own
validateForm()/submitForm()methods to make the embedded form work? No —getForm()already returns the fully wired render array, complete with the form's own validation and submission handlers baked in via its#validate/#submitproperties. The block plugin is just a delivery mechanism, not a re-implementation of the form.
Key takeaways
- Inject the
form_builderservice into any block plugin via the standardcreate()factory pattern. $this->formBuilder->getForm('Fully\Qualified\FormClass')returns a complete render array — hand it straight back frombuild(), no extra wiring needed.- The embedded form's cache metadata, attached libraries, and CSRF token all travel with the returned array automatically.
- If the form's content varies per user or per request, make sure the block's own cache contexts account for that — block-level caching doesn't automatically know a form inside it needs to vary.
Coming up next
The next lesson, "Altering Form Render Arrays," rounds out this topic by covering how to reach into a form's render array after it's built — the same technique hook_form_alter() and a block's own build() method both rely on — and change what actually gets rendered.