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

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

parent d2ca5776
No related branches found
No related tags found
No related merge requests found
...@@ -5,11 +5,9 @@ declare(strict_types=1); ...@@ -5,11 +5,9 @@ declare(strict_types=1);
namespace Drupal\ui_patterns\Element; namespace Drupal\ui_patterns\Element;
use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Plugin\Component;
use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Theme\ComponentPluginManager; use Drupal\Core\Theme\ComponentPluginManager;
use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType; use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType;
use Drupal\ui_patterns\PropTypeAdapterPluginManager;
/** /**
* Our additions to the SDC render element. * Our additions to the SDC render element.
...@@ -19,23 +17,25 @@ class ComponentElementAlter implements TrustedCallbackInterface { ...@@ -19,23 +17,25 @@ class ComponentElementAlter implements TrustedCallbackInterface {
/** /**
* Constructs a ComponentElementAlter. * Constructs a ComponentElementAlter.
*/ */
public function __construct(protected ComponentPluginManager $componentPluginManager, protected PropTypeAdapterPluginManager $adaptersManager) { public function __construct(protected ComponentPluginManager $componentPluginManager) {}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function trustedCallbacks() { public static function trustedCallbacks() {
return ['alter']; return ['alter'];
} }
/** /**
* Alter SDC component element. * 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 { public function alter(array $element): array {
$element = $this->normalizeSlots($element); $element = $this->normalizeSlots($element);
$component = $this->componentPluginManager->find($element['#component']); $component = $this->componentPluginManager->find($element['#component']);
$element = $this->normalizeProps($element, $component);
// Attributes prop must never be empty, to avoid the processing of SDC's // Attributes prop must never be empty, to avoid the processing of SDC's
// ComponentsTwigExtension::mergeAdditionalRenderContext() which is adding // ComponentsTwigExtension::mergeAdditionalRenderContext() which is adding
// an Attribute PHP object before running the validator. // an Attribute PHP object before running the validator.
...@@ -64,32 +64,6 @@ class ComponentElementAlter implements TrustedCallbackInterface { ...@@ -64,32 +64,6 @@ class ComponentElementAlter implements TrustedCallbackInterface {
return $element; 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. * 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; ...@@ -4,28 +4,15 @@ namespace Drupal\ui_patterns\Template;
use Drupal\Core\Plugin\Component; use Drupal\Core\Plugin\Component;
use Drupal\Core\Render\Component\Exception\ComponentNotFoundException; use Drupal\Core\Render\Component\Exception\ComponentNotFoundException;
use Drupal\Core\Template\ComponentNodeVisitor as CoreComponentNodeVisitor;
use Drupal\Core\Theme\ComponentPluginManager; use Drupal\Core\Theme\ComponentPluginManager;
use Twig\Environment; use Twig\Environment;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\ModuleNode;
use Twig\Node\Node; use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\NodeVisitor\NodeVisitorInterface; use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TwigFunction;
/** /**
* Provides a ComponentNodeVisitor to change the generated parse-tree. * Provides a Node Visitor to change the generated parse-tree.
*
* @internal
*/ */
class ComponentNodeVisitor implements NodeVisitorInterface { abstract class ModuleNodeVisitorBase implements NodeVisitorInterface {
/**
* Node name: expr.
*/
const NODE_NAME_EXPR = 'expr';
/** /**
* The component plugin manager. * The component plugin manager.
...@@ -49,38 +36,6 @@ class ComponentNodeVisitor implements NodeVisitorInterface { ...@@ -49,38 +36,6 @@ class ComponentNodeVisitor implements NodeVisitorInterface {
return $node; 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. * Finds the SDC for the current module node.
* *
...@@ -105,31 +60,6 @@ class ComponentNodeVisitor implements NodeVisitorInterface { ...@@ -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. * 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); ...@@ -4,7 +4,10 @@ declare(strict_types=1);
namespace Drupal\ui_patterns\Template; namespace Drupal\ui_patterns\Template;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\ui_patterns\ComponentPluginManager; use Drupal\ui_patterns\ComponentPluginManager;
use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType;
use Drupal\ui_patterns\PropTypeAdapterPluginManager;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
use Twig\TwigFunction; use Twig\TwigFunction;
...@@ -23,9 +26,15 @@ class TwigExtension extends AbstractExtension { ...@@ -23,9 +26,15 @@ class TwigExtension extends AbstractExtension {
* *
* @param \Drupal\ui_patterns\ComponentPluginManager $componentManager * @param \Drupal\ui_patterns\ComponentPluginManager $componentManager
* The component plugin manager. * The component plugin manager.
* @param \Drupal\ui_patterns\PropTypeAdapterPluginManager $adapterManager
* The prop tytpe adapter plugin manager.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/ */
public function __construct( public function __construct(
protected ComponentPluginManager $componentManager, protected ComponentPluginManager $componentManager,
protected PropTypeAdapterPluginManager $adapterManager,
protected MessengerInterface $messenger,
) {} ) {}
/** /**
...@@ -40,7 +49,8 @@ class TwigExtension extends AbstractExtension { ...@@ -40,7 +49,8 @@ class TwigExtension extends AbstractExtension {
*/ */
public function getNodeVisitors(): array { public function getNodeVisitors(): array {
return [ return [
new ComponentNodeVisitor($this->componentManager), new ModuleNodeVisitorBeforeSdc($this->componentManager),
new ModuleNodeVisitorAfterSdc($this->componentManager),
]; ];
} }
...@@ -49,7 +59,11 @@ class TwigExtension extends AbstractExtension { ...@@ -49,7 +59,11 @@ class TwigExtension extends AbstractExtension {
*/ */
public function getFunctions() { public function getFunctions() {
return [ return [
// @todo Remove component() before 2.0.0 release.
new TwigFunction('component', [$this, 'renderComponent']), 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]), new TwigFunction('_ui_patterns_preprocess_props', [$this, 'preprocessProps'], ['needs_context' => TRUE]),
]; ];
} }
...@@ -80,6 +94,7 @@ class TwigExtension extends AbstractExtension { ...@@ -80,6 +94,7 @@ class TwigExtension extends AbstractExtension {
* @see \Drupal\Core\Theme\Element\ComponentElement * @see \Drupal\Core\Theme\Element\ComponentElement
*/ */
public function renderComponent(string $component_id, array $slots = [], array $props = []) { 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 [ return [
'#type' => 'component', '#type' => 'component',
'#component' => $component_id, '#component' => $component_id,
...@@ -88,6 +103,45 @@ class TwigExtension extends AbstractExtension { ...@@ -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. * Preprocess props.
* *
...@@ -96,6 +150,8 @@ class TwigExtension extends AbstractExtension { ...@@ -96,6 +150,8 @@ class TwigExtension extends AbstractExtension {
* compatible with SDC's ComponentNodeVisitor, in order to execute props * compatible with SDC's ComponentNodeVisitor, in order to execute props
* preprocessing after SDC's validate_component_props Twig function. * preprocessing after SDC's validate_component_props Twig function.
* *
* See ModuleNodeVisitorAftereSdc.
*
* @param array $context * @param array $context
* The context provided to the component. * The context provided to the component.
* @param string $component_id * @param string $component_id
......
...@@ -75,6 +75,8 @@ services: ...@@ -75,6 +75,8 @@ services:
- { name: twig.extension } - { name: twig.extension }
arguments: arguments:
- "@plugin.manager.sdc" - "@plugin.manager.sdc"
- "@plugin.manager.ui_patterns_prop_type_adapter"
- '@messenger'
ui_patterns.sample_entity_generator: ui_patterns.sample_entity_generator:
class: Drupal\ui_patterns\Entity\SampleEntityGenerator 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