Replace render arrays with markup based on DOM

Created on 23 November 2024, 28 days ago

What is the problem to solve?

Render arrays. They scare newcomers because of bad DX. No typehints ( ✨ Use the builder pattern to make it easier to create render arrays Needs work ), hard to find documentation for available properties.

Steps to reproduce

Quiz: What's the proper way to attach a library to a render array?

  1. #attach['library'] = 'foo/bar'
  2. #attached['libraries'][] = 'foo/bar'
  3. #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).

✨ Feature request
Status

Active

Component

Idea

Created by

πŸ‡·πŸ‡ΊRussia Chi

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @Chi
  • πŸ‡·πŸ‡ΊRussia Chi
  • πŸ‡·πŸ‡ΊRussia Chi

    Meanwhile, we can use a render element to gradually migrate from render arrays to DOM.

    /**
     * Provides a render element to build markup using document object model.
     *
     * Properties:
     * - #dom: Document Object Model
     *
     * Usage Example:
     * @code
     * $build['dom'] = [
     *   '#type' => 'dom',
     *   '#dom' => $dom,
     * ];
     * @endcode
     *
     * @RenderElement("dom")
     *
     * @see https://www.php.net/manual/en/book.dom.php
     */
    final class Dom extends RenderElementBase {
    
      /**
       * {@inheritdoc}
       */
      public function getInfo(): array {
        return [
          '#pre_render' => $this->preRenderEntityElement(...),
          '#dom' => NULL,
        ];
      }
    
      /**
       * Pre-render callback.
       */
      public static function preRenderEntityElement(array $element): array {
        $dom = $element['#dom'] ?? NULL;
        if (!$dom instanceof \DOMDocument) {
          throw new \InvalidArgumentException('#dom property must contain instance of DOMDocument');
        }
    
        // Instead of using saveHtml, it should traverse the tree recursively,  
        // rendering child nodes while respecting theme hooks and bubbling attached  
        // metadata. 
        return ['#markup' => $dom->saveHTML()];
      }
    
    }
    
  • πŸ‡ΊπŸ‡ΈUnited States effulgentsia

    Interesting idea!

    $time_el->setAttribute('class', 'example__time');

    How would this work for #attributes values that aren't strings, such as arrays and objects?

  • πŸ‡ΊπŸ‡ΈUnited States effulgentsia

    How's the performance of creating and calling methods on DOMElement and 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.

  • πŸ‡·πŸ‡ΊRussia Chi

    Actually, 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.

  • πŸ‡·πŸ‡ΊRussia Chi

    Re #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.

  • πŸ‡©πŸ‡ͺGermany geek-merlin Freiburg, Germany

    Yay, 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?

  • πŸ‡·πŸ‡ΊRussia Chi

    Re #8 That make sense.

    Technically, the Drupal already has sort of DOM based on render arrays (should we call it DAM?). The issue merely about replacing associative arrays with objects.

    it's better to create a lightweight element tree with a few helper methods to simplify manipulation and traversal

    nicmart/tree seems like the right tool for this, at first glance.

  • πŸ‡¬πŸ‡§United Kingdom joachim

    What'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.

  • πŸ‡·πŸ‡ΊRussia Chi

    Re #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 ✨ Use the builder pattern to make it easier to create render arrays Needs work for details.

    How do we handle BC, especially with alter hooks that expect to find arrays?

    I've no idea at this moment. Another hook? Anyway, this needs some brainstorming.

  • πŸ‡¬πŸ‡§United Kingdom joachim

    > 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!

  • πŸ‡¬πŸ‡§United Kingdom jonathanshaw Stroud, UK

    We already have an issue for something like this: #2602368: Allow objects that implement RenderableInterface to used interchangeably with render arrays. β†’

    And it can be achieved in a fairly BC way.

  • πŸ‡·πŸ‡ΊRussia Chi

    #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.

  • πŸ‡¬πŸ‡§United Kingdom catch

    I think this should be in the core queue - it's an API proposal not a product change.

  • πŸ‡¬πŸ‡§United Kingdom joachim

    DomDocument 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:

    $q->blockquote(
    		     "Many years ago on the island of",
    		     $q->a({href=>"http://crete.org/"},"Crete"),
    		     "there lived a Minotaur named",
    		     $q->strong("Fred."),
    		    ),
    $q->hr;
    
  • πŸ‡«πŸ‡·France andypost

    Looking ahead it sounds like good idea, moreover in a light of new HTML 5 DOM in PHP 8.4

  • πŸ‡·πŸ‡ΊRussia Chi

    DomDocument is still pretty verbose.

    DomDocument 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.

Production build 0.71.5 2024