Skip to content
Snippets Groups Projects
Commit da82fbb5 authored by Pierre Dureau's avatar Pierre Dureau
Browse files

Merge branch '3449653-only-twig-visitor' into '2.0.x'

Issue #3449653 by pdureau: Execute normalization when Twig include and embed are used

See merge request !267
parents 78aba575 11ee0d79
No related branches found
No related tags found
No related merge requests found
Pipeline #343898 passed
......@@ -5,11 +5,9 @@ declare(strict_types=1);
namespace Drupal\ui_patterns\Element;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Plugin\Component;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Theme\ComponentPluginManager;
use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType;
use Drupal\ui_patterns\PropTypeAdapterPluginManager;
/**
* Our additions to the SDC render element.
......@@ -19,23 +17,26 @@ class ComponentElementAlter implements TrustedCallbackInterface {
/**
* Constructs a ComponentElementAlter.
*/
public function __construct(protected ComponentPluginManager $componentPluginManager, protected PropTypeAdapterPluginManager $adaptersManager) {
}
public function __construct(protected ComponentPluginManager $componentPluginManager) {}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['alter'];
}
/**
* Alter SDC component element.
*
* There ::normalizeProps() methods logic has been moved to
* TwigExtension::normalizeProps() in order to be executed also when
* components are loaded from Twig include or embed.
*/
public function alter(array $element): array {
$element = $this->normalizeSlots($element);
$component = $this->componentPluginManager->find($element['#component']);
$element = $this->normalizeProps($element, $component);
// Attributes prop must never be empty, to avoid the processing of SDC's
// ComponentsTwigExtension::mergeAdditionalRenderContext() which is adding
// an Attribute PHP object before running the validator.
......@@ -64,32 +65,6 @@ class ComponentElementAlter implements TrustedCallbackInterface {
return $element;
}
/**
* Normalize props.
*/
public function normalizeProps(array $element, Component $component): array {
$props = $component->metadata->schema['properties'] ?? [];
foreach ($element["#props"] as $prop_id => $prop) {
if (!isset($props[$prop_id])) {
continue;
}
$definition = $props[$prop_id];
$prop_type = $definition['ui_patterns']['type_definition'];
// Normalizing attributes to an array is not working
// if the prop type is defined by type=Drupal\Core\Template\Attribute
// This should actually be done by the normalize function.
$data = $prop_type->normalize($prop);
if (isset($definition['ui_patterns']['prop_type_adapter'])) {
$prop_type_adapter_id = $definition['ui_patterns']['prop_type_adapter'];
/** @var \Drupal\ui_patterns\PropTypeAdapterInterface $prop_type_adapter */
$prop_type_adapter = $this->adaptersManager->createInstance($prop_type_adapter_id);
$data = $prop_type_adapter->transform($data);
}
$element["#props"][$prop_id] = $data;
}
return $element;
}
/**
* Process #attributes render property.
*
......
<?php
namespace Drupal\ui_patterns\Template;
use Drupal\Core\Plugin\Component;
use Drupal\Core\Template\ComponentNodeVisitor as CoreComponentNodeVisitor;
use Twig\Environment;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\TwigFunction;
/**
* Provides a Node Visitor to change the generated parse-tree.
*/
class ModuleNodeVisitorAfterSdc extends ModuleNodeVisitorBase {
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node, Environment $env): ?Node {
if (!$node instanceof ModuleNode) {
return $node;
}
$component = $this->getComponent($node);
if (!($component instanceof Component)) {
return $node;
}
$line = $node->getTemplateLine();
$function = $this->buildPreprocessPropsFunction($line, $component, $env);
$node = $this->injectFunction($node, $function);
return $node;
}
/**
* {@inheritdoc}
*/
public function getPriority(): int {
$priority = &drupal_static(__METHOD__);
if (!isset($priority)) {
$original_node_visitor = new CoreComponentNodeVisitor($this->componentManager);
// Ensure that this node visitor's priority is higher than core's visitor,
// because this class has to run after core's class.
$priority = $original_node_visitor->getPriority() + 1;
}
return is_numeric($priority) ? (int) $priority : 0;
}
/**
* Build the _ui_patterns_preprocess_props Twig function.
*
* @param int $line
* The line .
* @param \Drupal\Core\Plugin\Component $component
* The component.
* @param \Twig\Environment $env
* A Twig Environment instance.
*
* @return \Twig\Node\Node
* The Twig function.
*/
protected function buildPreprocessPropsFunction(int $line, Component $component, Environment $env): Node {
$component_id = $component->getPluginId();
$function_parameter = new ConstantExpression($component_id, $line);
$function_parameters_node = new Node([$function_parameter]);
$function = new FunctionExpression(
new TwigFunction('_ui_patterns_preprocess_props', [$env->getExtension(TwigExtension::class), 'preprocessProps'], ['needs_context' => TRUE]),
$function_parameters_node,
$line
);
return new PrintNode($function, $line);
}
}
......@@ -4,28 +4,15 @@ namespace Drupal\ui_patterns\Template;
use Drupal\Core\Plugin\Component;
use Drupal\Core\Render\Component\Exception\ComponentNotFoundException;
use Drupal\Core\Template\ComponentNodeVisitor as CoreComponentNodeVisitor;
use Drupal\Core\Theme\ComponentPluginManager;
use Twig\Environment;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TwigFunction;
/**
* Provides a ComponentNodeVisitor to change the generated parse-tree.
*
* @internal
* Provides a Node Visitor to change the generated parse-tree.
*/
class ComponentNodeVisitor implements NodeVisitorInterface {
/**
* Node name: expr.
*/
const NODE_NAME_EXPR = 'expr';
abstract class ModuleNodeVisitorBase implements NodeVisitorInterface {
/**
* The component plugin manager.
......@@ -49,38 +36,6 @@ class ComponentNodeVisitor implements NodeVisitorInterface {
return $node;
}
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node, Environment $env): ?Node {
if (!$node instanceof ModuleNode) {
return $node;
}
$component = $this->getComponent($node);
if (!($component instanceof Component)) {
return $node;
}
$line = $node->getTemplateLine();
$function = $this->buildPreprocessPropsFunction($line, $component, $env);
$node = $this->injectFunction($node, $function);
return $node;
}
/**
* {@inheritdoc}
*/
public function getPriority(): int {
$priority = &drupal_static(__METHOD__);
if (!isset($priority)) {
$original_node_visitor = new CoreComponentNodeVisitor($this->componentManager);
// Ensure that this component node visitor's priority is higher than
// core's node visitor class for components, because this class has to run
// core's class.
$priority = $original_node_visitor->getPriority() + 1;
}
return is_numeric($priority) ? (int) $priority : 0;
}
/**
* Finds the SDC for the current module node.
*
......@@ -105,31 +60,6 @@ class ComponentNodeVisitor implements NodeVisitorInterface {
}
}
/**
* Build the _ui_patterns_preprocess_props Twig function.
*
* @param int $line
* The line .
* @param \Drupal\Core\Plugin\Component $component
* The component.
* @param \Twig\Environment $env
* A Twig Environment instance.
*
* @return \Twig\Node\Node
* The Twig function.
*/
protected function buildPreprocessPropsFunction(int $line, Component $component, Environment $env): Node {
$component_id = $component->getPluginId();
$function_parameter = new ConstantExpression($component_id, $line);
$function_parameters_node = new Node([$function_parameter]);
$function = new FunctionExpression(
new TwigFunction('_ui_patterns_preprocess_props', [$env->getExtension(TwigExtension::class), 'preprocessProps'], ['needs_context' => TRUE]),
$function_parameters_node,
$line
);
return new PrintNode($function, $line);
}
/**
* Injects custom Twig nodes into given node as child nodes.
*
......
<?php
namespace Drupal\ui_patterns\Template;
use Drupal\Core\Plugin\Component;
use Drupal\Core\Template\ComponentNodeVisitor as CoreComponentNodeVisitor;
use Twig\Environment;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\TwigFunction;
/**
* Provides a Node Visitor to change the generated parse-tree.
*/
class ModuleNodeVisitorBeforeSdc extends ModuleNodeVisitorBase {
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node, Environment $env): ?Node {
if (!$node instanceof ModuleNode) {
return $node;
}
$component = $this->getComponent($node);
if (!($component instanceof Component)) {
return $node;
}
$line = $node->getTemplateLine();
$function = $this->buildNormalizePropsFunction($line, $component, $env);
$node = $this->injectFunction($node, $function);
return $node;
}
/**
* {@inheritdoc}
*/
public function getPriority(): int {
$priority = &drupal_static(__METHOD__);
if (!isset($priority)) {
$original_node_visitor = new CoreComponentNodeVisitor($this->componentManager);
// Ensure that this node visitor's priority is lower than core's visitor,
// because this class has to run before core's class.
$priority = $original_node_visitor->getPriority() - 1;
}
return is_numeric($priority) ? (int) $priority : 0;
}
/**
* Build the _ui_patterns_preprocess_props Twig function.
*
* @param int $line
* The line .
* @param \Drupal\Core\Plugin\Component $component
* The component.
* @param \Twig\Environment $env
* A Twig Environment instance.
*
* @return \Twig\Node\Node
* The Twig function.
*/
protected function buildNormalizePropsFunction(int $line, Component $component, Environment $env): Node {
$component_id = $component->getPluginId();
$function_parameter = new ConstantExpression($component_id, $line);
$function_parameters_node = new Node([$function_parameter]);
$function = new FunctionExpression(
new TwigFunction('_ui_patterns_normalize_props', [$env->getExtension(TwigExtension::class), 'normalizeProps'], ['needs_context' => TRUE]),
$function_parameters_node,
$line
);
return new PrintNode($function, $line);
}
}
......@@ -4,7 +4,10 @@ declare(strict_types=1);
namespace Drupal\ui_patterns\Template;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\ui_patterns\ComponentPluginManager;
use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType;
use Drupal\ui_patterns\PropTypeAdapterPluginManager;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
......@@ -23,9 +26,15 @@ class TwigExtension extends AbstractExtension {
*
* @param \Drupal\ui_patterns\ComponentPluginManager $componentManager
* The component plugin manager.
* @param \Drupal\ui_patterns\PropTypeAdapterPluginManager $adapterManager
* The prop type adapter plugin manager.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(
protected ComponentPluginManager $componentManager,
protected PropTypeAdapterPluginManager $adapterManager,
protected MessengerInterface $messenger,
) {}
/**
......@@ -40,7 +49,8 @@ class TwigExtension extends AbstractExtension {
*/
public function getNodeVisitors(): array {
return [
new ComponentNodeVisitor($this->componentManager),
new ModuleNodeVisitorBeforeSdc($this->componentManager),
new ModuleNodeVisitorAfterSdc($this->componentManager),
];
}
......@@ -49,7 +59,11 @@ class TwigExtension extends AbstractExtension {
*/
public function getFunctions() {
return [
// @todo Remove component() before 2.0.0 release.
new TwigFunction('component', [$this, 'renderComponent']),
// For ComponentNodeVisitorBeforeSdc.
new TwigFunction('_ui_patterns_normalize_props', [$this, 'normalizeProps'], ['needs_context' => TRUE]),
// For ComponentNodeVisitorAfterSdc.
new TwigFunction('_ui_patterns_preprocess_props', [$this, 'preprocessProps'], ['needs_context' => TRUE]),
];
}
......@@ -80,6 +94,7 @@ class TwigExtension extends AbstractExtension {
* @see \Drupal\Core\Theme\Element\ComponentElement
*/
public function renderComponent(string $component_id, array $slots = [], array $props = []) {
$this->messenger->addWarning("component() Twig function is deprecated in favor of include() function and will be removed before 2.0.0.");
return [
'#type' => 'component',
'#component' => $component_id,
......@@ -88,6 +103,45 @@ class TwigExtension extends AbstractExtension {
];
}
/**
* Normalize props (and slots).
*
* This function must not be used by the templates authors. In a perfect
* world, it would not be necessary to set such a function. We did that to be
* compatible with SDC's ComponentNodeVisitor, in order to execute props
* normalization before SDC's validate_component_props Twig function.
*
* See ModuleNodeVisitorBeforeSdc.
*
* @param array $context
* The context provided to the component.
* @param string $component_id
* The component ID.
*
* @throws \Drupal\Core\Render\Component\Exception\InvalidComponentException
*/
public function normalizeProps(array &$context, string $component_id): void {
$component = $this->componentManager->find($component_id);
$props = $component->metadata->schema['properties'] ?? [];
foreach ($context as $variable => $value) {
if (isset($component->metadata->slots[$variable])) {
$context[$variable] = SlotPropType::normalize($value);
continue;
}
if (!isset($props[$variable])) {
continue;
}
$prop_type = $props[$variable]['ui_patterns']['type_definition'];
$context[$variable] = $prop_type->normalize($value);
if (isset($props[$variable]['ui_patterns']['prop_type_adapter'])) {
$prop_type_adapter_id = $props[$variable]['ui_patterns']['prop_type_adapter'];
/** @var \Drupal\ui_patterns\PropTypeAdapterInterface $prop_type_adapter */
$prop_type_adapter = $this->adapterManager->createInstance($prop_type_adapter_id);
$context[$variable] = $prop_type_adapter->transform($context[$variable]);
}
}
}
/**
* Preprocess props.
*
......@@ -96,6 +150,8 @@ class TwigExtension extends AbstractExtension {
* compatible with SDC's ComponentNodeVisitor, in order to execute props
* preprocessing after SDC's validate_component_props Twig function.
*
* See ModuleNodeVisitorAfterSdc.
*
* @param array $context
* The context provided to the component.
* @param string $component_id
......
......@@ -12,10 +12,10 @@
{% endif %}
{{ message|add_class('alert-link') }}
{% if dismissible %}
{{ component('ui_patterns_test:close_button', {}, {
attributes: create_attribute({
'data-bs-dismiss': 'alert'
}),
{{ include('ui_patterns_test:close_button', {
attributes: {
'data-bs-dismiss': 'alert',
},
aria_label: 'Close'|t,
}) }}
{% endif %}
......
......@@ -75,6 +75,8 @@ services:
- { name: twig.extension }
arguments:
- "@plugin.manager.sdc"
- "@plugin.manager.ui_patterns_prop_type_adapter"
- '@messenger'
ui_patterns.sample_entity_generator:
class: Drupal\ui_patterns\Entity\SampleEntityGenerator
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment