Problem/Motivation
There are 2 issues in our Twig templating which can be fixed in a single change:
1. Attribute PHP objects in templates.
Attribute PHP objects are great when coding with PHP, but they don't belong to a Twig template:
Also, it would be great to help SDC component authors to stop using a PHP namespace as a prop type, which is not JSON Schema valid:
props:
type: object
properties:
wrapper_attributes:
title: Wrapper attributes
type: Drupal\Core\Template\Attribute
2. We have a fatal error when we print a mapping
{{ {foo: "bar"} }}
Raises:
InvalidArgumentException: "pop" is an invalid render array key. Value should be an array but got a string. in Drupal\Core\Render\Element::children() (line 97 of core/lib/Drupal/Core/Render/Element.php).
Because print nodes are caught by TwigNodeVisitor::leaveNode():
if ($node instanceof PrintNode) {
...
$class = get_class($node);
$line = $node->getTemplateLine();
return new $class(
new FunctionExpression('render_var', new Node([$node->getNode('expr')]), $line),
$line
);
}
Then TwigExtension::renderVar() calls the rendering service, considering any array is a render array.
Twig template is supposed to be a "safe" job, so we must avoid any situation raising errors.
On Symfony 7, the same Twig snippet raises Warning: Array to string conversion
.
Proposed resolution
Print mapping as attributes
So {{ {foo: "bar"} }}
will be printed foo="bar"
Not sure where, not sure how.
For example, and without any tests, we can leverage Element::isRenderArray and act as soon as renderVar:
public function renderVar($arg) {
...
if (is_array($arg) && !Element::isRenderArray($arg)) {
$arg = new Attribute($arg);
}
if (is_object($arg)) {
...
}
...
return $this->renderer->render($arg);
}
Or later, in the renderer service.
We are not raising the error anymore.
Use filters to manipulate mappings
"Chainable" methods from Attribute PHP object needs to be converted to filters:
If we have same add_class
and set_attribute
filters for renderable arrays and attributes mapping, we need to decide how to handle empty mappings:
- empty or missing renderables must stay empty and resolve to false
- empty or missing attribute mapping will need to be created and/or altered
What do we do with the other methods from Attribute PHP object?
- ::getClass(): AttributeValueBase ➤ my_attributes["class"] ?
- ::getIterator(): ArrayIterator ➤ Useless because we already have an associative array
- ::hasAttribute($name) : bool ➤
is x in my_attributes|keys()
?
- ::hasClass($class) : bool ➤
x in my_attributes["class"]
?
- ::jsonSerialize() : string ➤ https://twig.symfony.com/doc/3.x/filters/json_encode.html
- ::offsetExists($name) : bool ➤
is x in my_attributes|keys()
?
- ::offsetGet($name) : mixed ➤
my_attributes[x]
?
- ::offsetSet($name, $value) : void ➤ Useless if we have
set_attribute
?
- ::offsetUnset($name) : void ➤ Useless if we already have a filters replacing
::removeAttribute
?
- ::storage(): array ➤ Useless because we already have an associative array
- ::toArray(): array ➤ Useless because we already have an associative array
Change management
Once merged in Core, let's promote the use of attributes as mappings instead of PHP objects in our documentation. Especially for SDC templates which are expected to be more "dumb" (it is a compliment) and UI oriented, so with minimal use of PHP objects.
Out of scope
Default attribute variable
Both ThemeManager et SDC are injecting a default attribute
variable which is such an Attribute
PHP object.
This automatic injection is useful and must be kept. Maybe this variable will be a mapping instead of a PHP object in the future, but it will be a compatibility break.
Printing logic
Let's use the logic currently implemented in Attribute::__toString():
{ foo: "bar", baz: ["lorem", "ipsum"] }
foo="bar" baz="lorem ipsum"
{ foo: "bar", baz: [{"lorem": "hello"}, "ipsum"] }
foo="bar" baz="Array ipsum"
{ foo: "bar", baz: {"lorem": {"ipsum": "hello"} } }
foo="bar" baz="Array"
If we are unhappy with it, let's create a distinct ticket.
API changes
Let's try to not break anything yet.