Extending FormBase is like inheriting a fully-equipped workshop instead of building your tools from scratch — translation helpers, messenger service access, current-user lookups, and the entire Form API contract all come free the moment your class extends it. This lesson builds one, piece by piece, from an empty class to a working form.
What you'll learn in this lesson
- What extending
FormBaseactually gives you for free - The minimum viable form class that Drupal will accept
- Registering the form so a route can actually reach it
The minimum viable class
namespace Drupal\mymodule\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class ContactForm extends FormBase {
public function getFormId() {
return 'mymodule_contact_form';
}
public function buildForm(array $form, FormStateInterface $form_state) {
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}
This compiles and runs — an empty form with just the invisible CSRF/build-tracking fields Drupal always adds. Every real form starts from exactly this shape before fields get added.
What FormBase gives you for free
$this->t()— the translation helper, viaStringTranslationTrait, without injecting the translation service manually.$this->messenger()— status/error message helper, viaMessengerTrait.$this->currentUser()— the logged-in user, without injecting the current_user service manually.$this->config()— shorthand for reading configuration objects.
FormInterface (the interface FormBase itself implements) gets none of this — it has to declare its own create() factory and manually inject every service it needs, the more verbose, fully-explicit approach you'll see in a later lesson. FormBase is the practical default for the vast majority of forms; the bare interface is for cases needing full explicit control.Registering the route
# mymodule.routing.yml
mymodule.contact_form:
path: '/contact-form-example'
defaults:
_form: '\Drupal\mymodule\Form\ContactForm'
_title: 'Contact us'
requirements:
_permission: 'access content'
The _form route default (rather than _controller) tells Drupal's routing system this route resolves directly to a Form API class — Drupal handles instantiating it and calling buildForm() automatically, no controller wrapper code needed.
Quick check: your form class needs to inject a custom service that isn't one of FormBase's built-in helpers (translation, messenger, current user, config). Does extending FormBase prevent you from doing that? If you said no — you still implement
create()and a constructor to inject additional services, FormBase just means you don't have to re-implement the common ones — you've understood that FormBase adds convenience without removing flexibility.
Key takeaways
FormBaseprovides translation, messenger, current-user, and config helpers automatically via traits.- The minimum viable form class needs only
getFormId(),buildForm(), andsubmitForm()— even empty implementations satisfy the contract. - A route resolves to a form class via the
_formdefault, not_controller. - Extending FormBase doesn't prevent injecting additional custom services — it just covers the common ones for free.
Coming up next
You've got an empty form registered and reachable. Next, we fill in buildForm() with actual fields.