Problem/Motivation
It's 2025. The render API suspiciously looks like form API when we added it 20 years ago.
Steps to reproduce
Proposed resolution
Let's reuse the existing render element plugins as object wrappers around render arrays. To achieve this, let's add:
ElementInfoManager::fromClass. It's likecreateInstancebut it takes a class instead of a plugin id. We will see why this is good in the next point.@propertydocumentation on the render element plugin classes based on existing documentation. This together with the previous point allows for this:
ElementInfoManager::fromRenderArray()which creates anElementInterfaceplugin from a render array and stores a reference to that render array.__setand__gettoRenderElementBaseto allow using render properties as object properties.- A few methods to work with children:
addChild,removeChild,getChildren. These will return objects. RenderElementBasee::toRenderArraymethod which returns the stored render array.FormBase::elementInfoManager()to get the element info manager.- WidgetBase::singleElementObject to demonstrate the modern API. While hook_form_alter, FormInterface::getForm etc for now need to use ::fromRenderable at the start and ::toRenderable at the end to use the new API, widgets implementing this method instead of formElement do not need to interact with render arrays at all.
Converted form alter. Before:
public function formUserRegisterFormAlter(&$form, FormStateInterface $form_state) : void {
$form['test_rebuild'] = [
'#type' => 'submit',
'#value' => $this->t('Rebuild'),
'#submit' => [
[Callbacks::class, 'userRegisterFormRebuild'],
],
];
}
After
public function formUserRegisterFormAlter(&$form, FormStateInterface $form_state) : void {
$submit = $this->elementInfoManager->fromRenderArray($form)
->createChild('test_rebuild', Submit::class);
$submit->value = $this->t('Rebuild');
$submit->submit = [[Callbacks::class, 'userRegisterFormRebuild']];
}
The time is now because
plugin.manager.element_infobecomes central for all things dealing with render API but all hooks now can use dependency injection to get it. Widgets always been plugins which also can use DI and forms are easily dealt with as above. Maybe various twig functionality will need it too but a) preprocess is also OOP now (yay) b) twig extensions are tagged services.- PHP 8.4 -- which will likely be required soon -- introduces property hooks. So it'll be possible to have setters and getters. So it's possible to start using properties without methods and seamlessly change core to add logic to them as needed. That will also allow deprecating properties.
- While phpstan was added to core a few years ago, a recent policy change made phpstan types canonical and phpstan generics are very useful here. https://www.drupal.org/node/3505429
Note: Because I know this will come up, the current implementation creates recursion in the render array, PHP handles this gracefully in serialize and dumping too so it's not a problem any more. var_dump and print_r prints ** RECURSION **, var_export produces a warning but all three work and doesn't fall into an infinite recursion. serialize also works but to make sure it works with other serializers on sleep/wakeup the recursion is broken/rebuilt.
Remaining tasks
Pull tests from #3539320: Element plugin object wrappers causing AJAX failures
Keep this issue in mind for internal properties: #3533842: Use of @internal in RenderElementBase and FormElementBase property attributes yields phpstan errors
Review
Followups:
- In a followup, add a traversal helper and then convert form the renderer/builder/validator/submit to visitors -- I think that'd be cleaner as it centralizes recursion
- In a followup we can fill the defaults for createInstance from getInfo
- Once the minimum is php 8.4 we can use property hooks
- In the ultimate followup, convert all core and contrib to the new API and drop render arrays. (Should be an easy, simple issue.)
User interface changes
N/A
Introduced terminology
Render objects
API changes
Render objects can now be created replacing difficult to remember render arrays with objects that can provide suggestions.
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| #84 | formfactorykits.PNG | 248.25 KB | yesnoio |
| #15 | 2025-05-22 05 18 43.png | 10.02 KB | ghost of drupal past |
Issue fork drupal-3525331
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
ghost of drupal pastComment #3
ghost of drupal pastComment #5
ghost of drupal pastComment #6
ghost of drupal pastComment #7
ghost of drupal pastComment #8
ghost of drupal pastComment #9
ghost of drupal pastComment #10
ghost of drupal pastComment #11
ghost of drupal pastComment #12
ghost of drupal pastComment #13
ghost of drupal pastComment #14
ghost of drupal pastComment #15
ghost of drupal pastComment #19
dwwComment #20
dwwComment #21
ghost of drupal pastComment #22
andypostIt looks great idea to get in for D12!
btw it will raise a question of DI for elements as related still)
Comment #24
nicxvan commentedI converted
FieldThirdPartyTestHooksas well so we convert something that isn't aform_alter.I think a couple of examples outside of tests would be good too.
Comment #25
nicxvan commentedThe functional JS test was failing, in hindsight it was kind of obvious what I missed.
I passed the objects back into the array assuming that toRenderArray would be called as necessary upstream. The idea that @moshe had to add a key property we can set and return the object directly would be nice too.
I got InvalidArgumentException: "field_test_field_formatter_third_party_settings_form" is an invalid render array key. Value should be an array but got a object. in Drupal\Core\Render\Element::children() (line 97 of /var/www/html/core/lib/Drupal/Core/Render/Element.php).
when running through the test manually.
For this particular test: https://git.drupalcode.org/project/drupal/-/merge_requests/12173/diffs?c... fixes it by converting back to the render array.
Comment #27
dwwBy request from chx in Slack, adding more credits
Comment #28
ghost of drupal pastComment #29
godotislateOne test failure:
Drupal\Tests\system\Functional\Form\ArbitraryRebuildTest::testUserRegistrationRebuild
Behat\Mink\Exception\ElementNotFoundException: Button with id|name|label|value "Rebuild" not found.
Don't think I've seen this one as an intermittent failure.
Comment #30
nicxvan commentedComment #31
dwwFirst real look at the MR. Mostly nits. A few questions of substance. Out of time for now. More later...
Comment #32
ghost of drupal pastphpcs won, I am done.
Comment #33
dww@chx: really? Please. The drama is helping no one. phpcs is a tool. It’s not hard to use. You can configure your IDE to get it right. You don’t need to take it so personally and get so worked up and angry over it. Please take some deep breaths, and maybe a day off, but spare us the public agony and tantrum over how having standards is an impediment to velocity.
We’ve been in this together for nearly 2 decades now. You know how much respect I have for you and your abilities. Please know I’m only writing this out of love and care. I invite you to relax and have some perspective.
Thanks,
-Derek
Comment #34
nicxvan commentedI've added skip rules for a bunch of jobs while we continue to work on architecture, maybe it's worth configuring this so draft MRs don't run these tests by default so that the actual architecture can happen unimpeded, then once the MR is no longer draft we can fix phpcs etc.
I've also added the fix for the fromClass work. I tried running the test locally but for some reason they wouldn't start.
Comment #35
nicxvan commentedTurns out core seems to ignore these flags, I just set them to allow failure.
Comment #36
nicxvan commentedComment #37
nicxvan commentedAdded a test that swaps out a class and added something to attempt to make the standalone wrappers nicer.
There are several failures, we could possibly revert that, but let's think on it a bit.
I updated the IS as well with some next steps, this could use a pass on comments, and we need to finish tests and tidy up the deprecations.
Comment #38
nod_Adding just in case
Comment #39
nicxvan commentedThanks @nod_
I removed the skip since it doesn't work anyway here is the issue to track: #3526516: Consider allowing first stage tests to fail when an MR is in draft.
Comment #40
nicxvan commentedOk this needs some additional tests, but we are in a good spot for another review.
Comment #41
nicxvan commentedComment #42
nicxvan commentedI think this is ready for review again, we've added tests and addressed most of the feedback.
I also added the deprecation.
Beyond review it might be worth identifying a few more things to convert.
Comment #43
nicxvan commentedComment #44
nicxvan commentedComment #45
nicxvan commentedComment #46
nicxvan commentedThank you for the reviews! The latest round of comments should all be addressed. I left a few open that still need confirmation that the answers are acceptable.
Comment #48
andypostComment #50
nicxvan commentedOk I've merged in the changes from the other branch exploration.
I've also rebased on 11.x
High level notes.
We replaced ModernWidget with WidgetInterface.
We added a Generic Element.
We added changetype.
Comment #51
nicxvan commentedTagging for myself later.
Comment #52
nicxvan commentedComment #53
nicxvan commentedComment #54
moshe weitzman commentedI've reviewed this a few times and its ready IMO. Great work especially to @ghost of drupal past and @nicxvan
Comment #55
larowlanTook a look at this and it wasn't what I was expecting but agree it is a logical first step towards object oriented form/render array.
It see some comments in the code around longer-term plans for D12/D13 of deprecating arrays and using the element plugin instances everywhere.
In light of that I think it would be good to have a parent meta created with some sibling issues for the next steps.
The longer term things I would expect we would move towards would be replacing the
@propertydoc block comments with real properties and using$storagefor random overflow (people have gotten used to dumping random things in#somekey, this would give us language level type-checking and possibly memory improvementsAside from that I think the best course of action is to get this into 11.x for 11.3.x early and unblock the follow up work, but let's articulate what those next steps are first.
Thanks folks.
Comment #56
nicxvan commentedI will try to update this, I won't have much contribution time this next week though.
We probably need a cr with the new functionality too.
Comment #57
larowlan@catch let me know that the longer term plan is to wait for PHP 8.4 and property hooks rather than dedicated properties. Would be good to document that somewhere
Comment #58
nicxvan commentedI addressed all feedback and created the follow up.
Comment #59
nicxvan commentedComment #65
nicxvan commentedTook a pass at credit.
Comment #66
moshe weitzman commentedAll feedback addressed. Back to RTBC.
Comment #67
pdureau commentedFollowing the advice of @larowlan, I have checked if this proposal is OK with #3508641: Define form elements from SDC
It looks good to me so far:
ComponentElementinternal logic was not changed, so it will still be able to extendFormElementBaseand manipulate some form properties.ElementInterfaceand their implementation inRenderElementBaseare not disturbing and are welcomedThanks for the work.
Comment #68
larowlanComment #71
larowlanCommitted to 11.x and wrote a change record with the new APIs as well as published the existing one
🎉🎉🎉
Great work folks
Comment #72
catchComment #73
larowlanCame to add that tag and catch already has 🎉
Comment #74
larowlanFirst follow-up #3533842: Use of @internal in RenderElementBase and FormElementBase property attributes yields phpstan errors - getting this in early ++
Comment #76
catchI think per #3539320: Element plugin object wrappers causing AJAX failures we should roll it back and re-commit it once we have a solution to that issue. I'm not around much this week though so probably can't actually do that.
Comment #78
larowlanReverted per #76 and removed tag/unpublished change record
Comment #79
larowlanComment #80
ghost of drupal pastWhat can be salvaged? Properties and magic set/get except the elements array is used to initialize, not a reference. Use it more strategic not so general. This here:
Should works fine without references. Inside you are in the "future" Drupal world where you deal with render objects and not render arrays. You can switch in your form alter to such and return back if you so want. But as long as the form and render system is concerned, nothing changes. It's just a helper for more convenient interacting with render arrays.
I am not going to work on this (or indeed, on anything else).
Comment #81
geek-merlinThanks for pointing me to this. Terrific.
Comment #82
nicxvan commentedComment #83
nicxvan commentedComment #84
yesnoio commentedExcellent work, so far! I'm late to this convo & just reviewed the MR in full. I'd like to help with this & similar efforts.
Please consider taking a look at my old Form Factory Kits project. It does similar (form render array creation/modification). I just released a new version compatible with D11 after doing a broad search to see if the module is still necessary. I had always planned to revise the module functionality to directly enhance core render elements & I agree with your pending changes. I would merely like to ask you to consider the pros/cons of the element "Kits" I created long ago & whether Core should offer similar objects by default. I addressed most Form API use cases, as documented here. If you decide your pending changes are sufficient, that's fine! I'll update/simplify formfactorykits to utilize your new functionality, or close that project if it makes sense to do so :) Eager to see a more powerful Form API! Thanks for all your great work!!
Comment #85
nicxvan commented@yesnoio I would love to see this move forward, does your implementation handle the case in https://www.drupal.org/project/drupal/issues/3539320 ?
Comment #86
andypostIn a light of the new PHP 8.5 cloning feature and pipes instead of wrappers elements could be readonly classes with some with* methods (wither pattern)
So kinda
Also it could use compatible with PHP 8.3
Comment #87
yesnoio commented@nicxvan it avoids that issue. `formfactory` provides functionality for `array $form` import/modification/export (typically done within the `FormInterface::buildForm` method). The module merely provides form array alteration functionality. I expect the issue documented in 3539320 would not occur, since `formfactory` refrains from wrapping form arrays for very long.
If something like formfactorykits were rolled into Core, the same pattern could be repeated (near term): keep form arrays "unwrapped" by default & create a new optional form `FormObjectInterface`. Preparations for more powerful core form objects could continue, if desired. Something like the following could be quickly implemented:
Comment #90
volegerRebased 3525331-slowly-very-slowly branch and opened MR !13985 to be able to address reported issues
Comment #91
ghost of drupal pastvoleger, thanks for the work but I think the project needs to very seriously evaluate the formfactorykits approach first. I very much like what I see and I would love if someone could lead a structured review of it for core inclusion. I am willing to participate in raising concerns and problems we had with forms over these many, many long years but I can't do it alone. If neclimdul were still around I would ask him to lead if he has the time, he is excellent in doing this.
The decision certainly can be "no". I am not advocating for it. I am not advocating for anything, any more. I am saying it has potential. But what do I know, these days.
Comment #92
yesnoio commentedI have just created a separate issue for
FormFactoryKitsconsideration, so this thread can remain focused on the great work you all have already contributed. I plan to create another beta release later today that includes the proposedFormObjectBaseclass. Discussion about the pros/cons ofFormFactoryKitscan be had in that & similar issues. Thank you for your consideration & great contributions to the Drupal Community!Comment #94
andypostso what is the state of issue now? maybe it needs split somehow at least to evaluate formFactorykits
Comment #95
godotislateI gave this one some thought a few months ago, but haven't had a chance to investigate or try anything, so here's vaguely what I remember thinking:
I haven't looked at form factory kits at all, but yes, perhaps it deserves a separate issue to look at it.