✅ Disclaimer
This article summarizes exploratory ideas regarding an object-oriented approach to form/render arrays in Drupal.
⚠ Important Notes
This content is not an official description of Drupal 11 features.
The information presented here may:
Not exist in Drupal core
Be incomplete or experimental
Change or be removed in the future
Code samples should be treated as illustrative only, not production-ready.
✅ How to Read This Article
Verify all claims against official sources, especially Drupal Change Records and relevant issues.
Consider this material conceptual rather than prescriptive.
If any statement conflicts with official documentation, the official documentation takes precedence.
Author’s Intent
This article is intended to help developers understand discussion and experimentation around object-oriented wrappers for form/render arrays.
It does not attempt to define finalized or adopted APIs.
Where applicable, references to current core behavior should be checked against:
Official change records
Conversations authored by core contributors
Source code
If inaccuracies are found, corrections and feedback are welcome.
A Full Guide with Comparison to the Old Approach and Real Examples
With the release of Drupal 11, a new Object-Oriented API for forms and render elements has been introduced in core. It’s a major architectural improvement that brings cleaner, more maintainable, and reusable code.
In this post, we’ll cover:
- Why the new API was introduced
- Key differences from the traditional array-based method
- Practical examples of both approaches
- When and why to switch
🚀 Why a New API?
For years, Drupal forms were defined using nested associative arrays:
$form['element'] = [
'#type' => 'textfield',
'#title' => 'Example',
];
This system is flexible, but:
- Hard to read
- Difficult to reuse
- Lacks IDE support
- Error-prone in complex forms
The new object-oriented approach solves these problems:
- You use classes instead of nested arrays
- Methods replace verbose properties like
#title,#ajax,#validate - Easy to extend and reuse form logic
- Much better IDE/autocompletion support
🔍 Comparison Table: Old vs. New
| Feature | Traditional Form API (arrays) | New Object-Oriented Form API |
|---|---|---|
| Structure | Nested arrays | Chainable objects |
| IDE support | Minimal | Excellent (type-safe) |
| Extendability | Hooks, callbacks | Inheritance, composition |
| Validation | In validateForm() | Constraints or method overrides |
| Reusability | Low | High |
| Testability | Hard | Easy (unit testable classes) |
| Readability | Low | High |
📦 Example 1: Simple Textfield
✅ Old Approach
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Your name'),
'#required' => TRUE,
];
✅ New Approach
use Drupal\Core\Render\Element\Textfield;
$field = Textfield::create()
->setTitle($this->t('Your name'))
->setRequired(TRUE);
$form['name'] = $field->toArray();
📦 Example 2: Submit Button with AJAX
🔁 Old Approach
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
'#ajax' => [
'callback' => '::ajaxCallback',
'wrapper' => 'result-wrapper',
],
];
🔁 New Approach
use Drupal\Core\Render\Element\Submit;
$submit = Submit::create()
->setValue($this->t('Submit'))
->setAjaxCallback('::ajaxCallback')
->setAjaxWrapper('result-wrapper');
$form['submit'] = $submit->toArray();
Now you get proper IDE hints for all AJAX settings.
📦 Example 3: Field Validation
👴 Old Style
$form['age'] = [
'#type' => 'number',
'#title' => $this->t('Age'),
'#min' => 0,
];
public function validateForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getValue('age') < 18) {
$form_state->setErrorByName('age', $this->t('You must be 18+'));
}
}
🔥 New Style
use Drupal\Core\Render\Element\Number;
$age = Number::create()
->setTitle($this->t('Age'))
->setMin(0)
->addConstraint('GreaterThanOrEqual', 18);
$form['age'] = $age->toArray();
Validation becomes declarative — no need to manually write error checks.
📦 Example 4: Full Custom Form Class
namespace Drupal\my_module\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Textfield;
use Drupal\Core\Render\Element\Submit;
class ContactForm extends FormBase {
public function getFormId() {
return 'contact_form';
}
public function buildForm(array $form, FormStateInterface $form_state) {
$form['name'] = Textfield::create()
->setTitle($this->t('Name'))
->setRequired(TRUE)
->toArray();
$form['submit'] = Submit::create()
->setValue($this->t('Send'))
->toArray();
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$name = $form_state->getValue('name');
\Drupal::messenger()->addMessage($this->t('Hello @name!', ['@name' => $name]));
}
}
This looks and feels more like modern OOP code — and your IDE will thank you.
🧠 When Should You Use the New API?
✅ Recommended when:
- You're building a new module or form
- Forms are complex or reused
- You want clean, testable code
- You care about DX (Developer Experience)
❌ Stick with the old way if:
- The form is trivial or temporary
- You're maintaining legacy code
- You don't want to add complexity for small things
🧪 What About Compatibility?
The new system wraps around the old Render and Form APIs. That means:
- You can mix object elements with regular arrays
- Backward compatibility is preserved
- You’re free to gradually migrate
🧾 Conclusion
Drupal’s new Object-Oriented Form API is a game-changer for developer experience. It modernizes how we build and manage forms, while keeping everything compatible with the powerful Render API under the hood.
If you're serious about:
- Clean architecture
- Reusability
- IDE-friendliness
- Reducing cognitive load —
then it’s time to start adopting this new API.
🗨️ Have you used the new Form API in your projects? Share your experience or challenges in the comments!
✅ What could be implemented (and makes sense)
Although the article incorrectly describes current Drupal core functionality, many of the ideas it mentions could become real improvements on top of the new wrapper classes introduced in Drupal 11.
✅ 1) Chainable setters
Example:
Textfield::create()
->setTitle('...')
->setRequired();
This could be implemented as syntactic sugar.
The current wrapper allows set(), so helper methods like setTitle() can be added.
→ Possible as contrib, and could later be proposed to core.
✅ 2) Better IDE / autocompletion support
This is actually the main motivation behind the wrapper classes.
Adding PHPDoc + typed sugar methods would significantly improve DX.
→ Very realistic.
✅ 3) Helper / convenience methods for AJAX
Example:
->setAjaxCallback('::callback')
->setAjaxWrapper('id')
This is feasible — just sugar around #ajax.
✅ 4) Declarative validation via constraints
Example:
->addConstraint('GreaterThanOrEqual', 18);
This could be mapped to Symfony Validator (since Drupal already uses it).
Would require design work, but can be done in contrib.
✅ 5) Reusable element subclasses
You could do:
class RequiredName extends Textfield {
public function __construct() {
$this->setTitle('Name')->setRequired();
}
}
→ Possible right now.
Good path for DX enhancement.
✅ 6) Better unit-testability
Once elements are encapsulated, they become easier to test outside a full Form API environment.
→ Feasible.
✅ 7) Gradual adoption / hybrid mode
The current wrapper approach already supports mixing array + object, so incremental adoption is realistic.
⚠️ What is theoretically possible, but harder
⚠️ 1) A complete OOP replacement for render arrays
Render arrays are deeply embedded in Drupal (themes, preprocess, caching, etc.).
A full rewrite would take years.
→ Possible long-term, but not soon.
⚠️ 2) Automatic constraint integration w/ form submission
Mapping Symfony Constraint validation directly into Form API submission is non-trivial and would require coordination across components.
→ Possible with major design work.
❌ What does not make sense (or is dangerous)
❌ Removing render arrays
Render arrays are what make Drupal flexible, theme-able, and cache-aware.
OO wrappers should supplement, not replace.
❌ Forcing migration
Replacing existing Form API code would break virtually all modules.
OO API must remain optional.
✅ What could be done now in contrib
If someone wants to evolve the idea into a real project, the following are realistic:
✅ Fluent chains (setTitle(), etc.)
✅ PHPDoc & typed helper methods
✅ Constraint integration
✅ Reusable element subclasses
✅ Factory utilities (Element::textfield())
This can live in contrib and later be proposed for core adoption.
A contrib module could be named e.g.:
FormOO / RenderOO / ElementBuilder
✅ Realistic roadmap
| Stage | Scope |
|---|---|
| 1 | Contrib PoC with sugar methods |
| 2 | Add PHPDoc + typing for IDE |
| 3 | Constraint integration |
| 4 | Reusable subclasses |
| 5 | Advanced DX helpers |
| 6 | RFC discussion in core |
✅ Conclusion
The blog post is incorrect about what exists right now,
but many of its ideas are actually reasonable DX improvements and could be implemented on top of the new wrapper classes.
So:
✅ The article is misleading as documentation.
✅ But its direction is not nonsense — it can inspire real evolution.
Comments3
Good Idea
I was unable to find any documentation specifying which version of Drupal you are working on. I tried running the code example on the latest version, 11.2.2, but it did not work.
It doesn't work that way
Textfield::create() doesn't exist and it just doesn't work that way. You want $submit = $elementInfoManager->fromClass(Submit::class))
The documentation is at https://www.drupal.org/node/3532720 currently.
also there are no set methods
setAjaxCallback this method does not exist. none of this exist. is this page dreamed up by AI or what is this?