Skip to content

Three options compared

This page compares three ways of building Drupal forms:

  • A: bare render arrays, the status quo. It is the floor: every object layer adds cost on top of it, so A is the baseline the other two are measured from, not a competitor.
  • B: element-plugin object wrappers, as proposed in #3525331.
  • C: Form Factory Kits, this module.

The useful comparison is B against C: both offer objects instead of arrays, and they make different trade-offs.

Sources. B is described from its merge request code (!12173, committed to 11.x in June 2025 and reverted in August 2025, and the rebased !13985), which was applied to a copy of Drupal 11.4.8 and run for the benchmarks. It is not in any Drupal release, and the issue may change the design. C is described from this repository, with links to the page or file that shows each point.

The same element, three ways

A:

$form['test_rebuild'] = [
  '#type' => 'submit',
  '#value' => $this->t('Rebuild'),
  '#submit' => [[Callbacks::class, 'userRegisterFormRebuild']],
];

B (the issue summary's example; in the merge request code the first method is called fromRenderable(), the summary calls it fromRenderArray()):

$submit = $this->elementInfoManager->fromRenderable($form)
  ->createChild('test_rebuild', Submit::class);
$submit->value = $this->t('Rebuild');
$submit->submit = [[Callbacks::class, 'userRegisterFormRebuild']];

C:

$form_factory = $this->formFactory->load($form);
$form_factory->append(
  $this->kits->submit('test_rebuild')
    ->setValue($this->t('Rebuild'))
    ->set('submit', [[Callbacks::class, 'userRegisterFormRebuild']])
);
$form = $form_factory->getForm();

Kits have no setSubmit(), so the generic set() from Drupal\kits\Kit is used for #submit.

Side by side

Aspect A: bare arrays B: element wrappers (#3525331) C: Form Factory Kits
Construction Array literals. ElementInfoManager::fromRenderable($form) or fromClass(), then createChild($name, Element::class) / addChild(). Factory methods on the formfactorykits service, one per element type (kit reference); children with append(); FormFactory::getForm() assembles the array (Concepts).
Property access $el['#title']. Magic __get / __set: $el->title maps to #title. Any name is accepted. Typed setters from traits (setTitle(), setRequired(), ...) (trait reference); anything else through set('name', $value).
Open an existing array in place Yes (it is the array). Yes. fromRenderable($form) wraps the existing array by reference, so code can edit elements another module added. No. Kits only build new elements. load($form) keeps the existing array and adds kits next to it; existing elements stay array code (Concepts).
References None unless the code adds them. Yes, by design: initializeInternalStorage() does $this->storage = &$element, toRenderable() returns by reference, addChild() stores &$child->toRenderable(). None in kit output. grep -rn '&\$' src finds only two core method signatures (Concerns).
Objects inside the array None. One ##object entry per element, pointing back at the object that holds the array: 506 for the 500-field form (Benchmarks). None; kit objects are discarded after getForm() (Benchmarks).
Copying $form Independent copy. Measured: the copy stays coupled to the original (Benchmarks). Measured: independent copy (Benchmarks).
Recursion and serialization Plain data. Circular (##object → storage → array containing ##object); var_export() warns about circular references. serialize() works through __sleep() / __wakeup(), gives 2.8× the bytes of A, and removes ##object from the live array as a side effect (Benchmarks). Plain data; serialized size identical to A (Benchmarks).
AJAX and form cache The current, working behavior. Reverted after #3539320, which states that after $form is copied, "elements between the two stay coupled, because of use of objects and references to arrays within the render array". The array contains no kit objects or references, so that coupling cannot come from kits. Not proven by an AJAX test in this repository (Readiness).
BC with render arrays Is the render array. Wrappers are the core element plugins; the array they hold goes through the existing pipeline, with ##object entries added. Output is a plain render array, so nothing downstream changes (Altering any $form array).
Memory and time (build, 500 fields) 464 KiB peak, 456 KiB retained, ~0.3 ms. 884 KiB peak, 884 KiB retained, ~3 ms. 1,040 KiB peak, 550 KiB retained, ~1.0–2.0 ms (Benchmarks).
Round trip (alter existing form) ~138 KiB for the edit. ~313 KiB peak (the array is not copied; the cost is the element objects). Not possible (Benchmarks).
Nesting Nested arrays. addChild(), createChild(), getChild(), getChildren(), removeChild(). append(); vertical tabs group their children (Nesting). No remove or get-child. Nested children get #parents set to ancestors only, which needs review (Readiness).
Extensibility Any #type. Every element plugin, core or contrib, is a wrapper automatically. One kit class per element type (52 *Kit.php classes in src/Kits/, including abstract bases) (kit reference); new ones extend FormFactoryKit (Writing your own kit).
Source of truth for properties Element plugin docs. The element plugin itself. The kit class, separate from the element plugin (Concerns).
Dependency and merge cost None. Core changes only: ElementInfoManager, RenderElementBase, ElementInterface, every element class, FormBase, some widgets (74 files in !13985, tests included). A new module (76 PHP files in src/) plus the contributed kits, formfactory and query modules it depends on, all beta (Merge path).
Current status Shipping. Reverted; issue "Needs work"; rebased MR open. Contrib, 2.1.0-beta1; several open readiness items (Readiness).

What B does that C does not

  1. Open an existing array in place. Code can take a form built by someone else and edit its elements through objects. With kits, editing existing elements is still array code.
  2. Cover every element type with no extra classes. A wrapper is the element plugin, so contrib element types work at once. Kits need a class per type, kept in step with the element's properties.
  3. Accept any property directly. $element->anything = ... works; kits use the less discoverable set('anything', ...).
  4. Lower peak memory while building. B builds straight into the array; kits briefly hold the kit objects and the finished array together (about 18% higher peak than B in the benchmark).
  5. Need nothing outside core. Kits currently depend on three beta contributed modules.

What C does that B does not

  1. Return a plain array. No objects and no references stay in the form, so copies are independent, serialization is unchanged, and the class of coupling described in #3539320 cannot come from kits.
  2. Free its objects after building. The retained form is about 38% smaller than B's and about 21% larger than A's.
  3. Build faster on the 500-field form. About 0.3–0.4× B's median in the 28 October 2026 repeats (still several times A's). On 5,000 fields that run was within noise.
  4. Typed, discoverable setters. IDEs can list what a TextFieldKit supports, and PHP checks argument types.
  5. Work today without core changes. The module declares support for Drupal 10.2 and 11, with the known bugs listed under Readiness.