Problem/Motivation
Render arrays. They scare newcomers because of bad DX. No typehints (#2316941: Use the builder pattern to make it easier to create render arrays), hard to find documentation for available properties.
Steps to reproduce
Quiz: What's the proper way to attach a library to a render array?
#attach['library'] = 'foo/bar'#attached['libraries'][] = 'foo/bar'#attachment['libraries'] = ['foo/bar']
Answer: Try all options. Hopefully, some will work. I was never able to remember the correct one.
Proposed resolution
Implement markup with DOM.
This approach will improve DX, leveraging PHP’s type system. Additionally, most developers are already familiar with the DOM concept.
I could not find any good library implementing a virtual DOM, except PHP’s built-in DOM extension. We could decorate it to better fit Drupal’s needs. For example, using the PHP DOM Wrapper.
Examples:
Before:
public function build(): array {
$build = [
'#type' => 'container',
'#attributes' => ['class' => ['example']],
];
$timestamp = new \DateTimeImmutable();
$build['time'] = [
'#theme' => 'time',
'#text' => $timestamp->format('Y-m-d H:i:s'),
'#attributes' => [
'datetime' => $timestamp->format('Y-m-d\TH:i:s.v\Z'),
'class' => [
'example__time',
],
],
'#cache' => [
'contexts' => ['timezone'],
],
];
return $build;
}
After:
public function build(): \DOMDocument {
$dom = new \DOMDocument();
// Custom element that extends DOMElement.
$time_el = new Time(
timestamp: new \DateTimeImmutable(),
format: 'Y-m-d H:i:s',
);
$time_el->cache->addContext('timezone');
$time_el->setAttribute('class', 'example__time');
// Pure DOMElement from DOM extension.
$container_el = $dom->createElement('div');
$container_el->setAttribute('class', 'example');
$container_el->appendChild($time_el);
$dom->appendChild($container_el);
return $dom;
}
For custom elements it should be possible to implement any properties that render arrays have (#theme, #cache, #pre_render, etc).
Comments
Comment #2
chi commentedComment #3
chi commentedMeanwhile, we can use a render element to gradually migrate from render arrays to DOM.
Comment #4
effulgentsia commentedInteresting idea!
How would this work for #attributes values that aren't strings, such as arrays and objects?[Edit: never mind. I misread your setAttribute() suggestion as something for random render element properties (#foo), but you're just intending it for #attributes.]
Comment #5
effulgentsia commentedHow's the performance of creating and calling methods on
DOMElementand its subclasses relative to PHP arrays? It's possible that modern PHP versions are fast enough with this, but I think it used to be that something like your suggestion would incur a lot of overhead for the size of render element trees we typically see in Drupal.Comment #6
chi commentedActually, I think, having a full-fledged DOM implementation is not needed. We only need DOM as backing store for content tree. From this point, it's better to create a lightweight element tree with a few helper methods to simplify manipulation and traversal.
PHP’s built-in DOM extension feels overly complex for such tasks and has some pitfalls when dealing with multiple DOMDocument objects. On the other hand, it offers some cool features that could be nice to have, such as traversing in the upward direction, XPath, and CSS selectors (introduced in PHP 8.4) for finding elements. However, none of these features are essential for Drupal's rendering system.
Comment #7
chi commentedRe #5
That requires benchmarks. My guess is that manipulating an object tree costs almost nothing compared to other operations performed by Drupal's theme layer, such as preprocessing variables and rendering Twig templates.
Comment #8
geek-merlinYay, this impulse may set some energy free in the community. For me it certainly does.
Some thoughts:
1) For all i know, objects having performenca penalty is long ago (php5?).
2) I'm maintaining RenderArrayTool Library and have gotten to love that approach, for building, but much more for altering render arrays.
3) So this is an interesting approach: to have the Renderer eat render arrays OR RenderableObjectInterface. If the render elements then have typed properties, altering would be a lot more fun.
4) Another win of this approach: It would finally allow evolution. A RenderableObjectInterface (which is different to RenderElementBase or its interfaces) is something that spits out a html string in the end, BubbleableMetadata (roughly cacheability plus libraries) and may or may not use a render array in between (so it is also not RenderableInterface).
5) Hacking together a ComponentRenderElement would be trivial.
6) A RenderableFormElementInterface for form elements would need a bit more bells and whistles, as it has to interact with the form workflow.
7) We may even have a DrupalFormElement and other form elements, which would allow using other form libraries like symfony forms (though i strongly doubt they will ever be interoparable.)
8) As of the API: There are lots of properties and methods (especially for forms) that have no relation to the DOM. But without doubt, having the dom related methods resemble DomExtension, has a lot of charm.
9) Yes, we don't want to extend DomNode for a variety of reasons. Whether the New-Dom empty marker interfaces \DOM\ParentNode and \DOM\ChildNode should play a role here, is up to debate.
So in the end, for me it boils down to opening a core issue like "Add RenderableObjectInterface and allow it in render arrays".
WDYT?
Comment #9
chi commentedRe #8 That make sense.
Technically, Drupal already has sort of DOM based on render arrays (should we call it DAM?). The issue is merely about replacing associative arrays with objects.
nicmart/tree seems like the right tool for this, at first glance.
Comment #10
joachim commentedWhat's the benefit of using DOMElement as our base?
How do we handle BC, especially with alter hooks that expect to find arrays?
> hard to find documentation for available properties.
I'm not against this direction of movement at all, but that is a reason to **fix our documentation**, not to replace a system with a new system that will need just as much documentation. People have been saying for years that form/render arrays need documentation.
Comment #11
chi commentedRe #10
By "documentation," I mean a list of available properties with their types and a brief description. This documentation may already exist somewhere, but you would need to look it up yourself. For objects with typed properties and methods, IDE can provide that information for you. See #2316941: Use the builder pattern to make it easier to create render arrays for details.
I've no idea at this moment. Another hook? Anyway, this needs some brainstorming.
Comment #12
joachim commented> By "documentation," I mean a list of available properties with their types and a brief description. This documentation may already exist somewhere, but you would need to look it up yourself. For objects with typed properties and methods, IDE can provide that information for you.
In both cases, people need to write the documentation.
Though I get your point that with named parameters or object properties, IDEs will more easily provide information. But it has to actually be there!
Comment #13
jonathanshawWe already have an issue for something like this: #2602368: Allow objects that implement RenderableInterface to used interchangeably with render arrays.
This can be achieved in a fairly BC way as explained in comment #42 in that issue.
Comment #14
andypostComment #15
chi commented#2602368: Allow objects that implement RenderableInterface to used interchangeably with render arrays. indeed is very close to this one. Though the proposed approach is a bit different.
Here we aim to completely remove render arrays.
In #2602368 the render arrays will remain as intermediate layer between elements (objects) and renderer service. That is much easier to implement. The submitted patch is actually a tree-lines change.
As of BC, we don't have to implement it imminently. Custom projects can benefit from this change right now. Contributed projects may start using element objects in major releases where BC breaks are allowed.
Comment #16
catchI think this should be in the core queue - it's an API proposal not a product change.
Comment #17
joachim commentedDomDocument is still pretty verbose.
I'd love to see something inspired by perl's CGI (https://perldoc.perl.org/5.12.1/CGI#CREATING-STANDARD-HTML-ELEMENTS), which allows you to nest things:
Comment #18
andypostLooking ahead it sounds like good idea, moreover in a light of new HTML 5 DOM in PHP 8.4
Comment #19
chi commentedDomDocument was referenced mainly as an example. I don't think it's the right tool for this. See note #6.
I was looking for something Yii HTML but with support of attachments and custom render callbacks.