diff --git a/src/Element/ComponentElementAlter.php b/src/Element/ComponentElementAlter.php index f4e944e77e7b94a4c5de7e61863c4e3c0dc98752..f87f8a4f59a1590cc3978bbd3186e47ca073faa6 100644 --- a/src/Element/ComponentElementAlter.php +++ b/src/Element/ComponentElementAlter.php @@ -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. * diff --git a/src/Template/ComponentNodeVisitor.php b/src/Template/ComponentNodeVisitor.php deleted file mode 100755 index 6da6ff0e1fb1b73bf360a953b7f265ecd8be4432..0000000000000000000000000000000000000000 --- a/src/Template/ComponentNodeVisitor.php +++ /dev/null @@ -1,153 +0,0 @@ -<?php - -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 - */ -class ComponentNodeVisitor implements NodeVisitorInterface { - - /** - * Node name: expr. - */ - const NODE_NAME_EXPR = 'expr'; - - /** - * The component plugin manager. - */ - protected ComponentPluginManager $componentManager; - - /** - * Constructs a new ComponentNodeVisitor object. - * - * @param \Drupal\Core\Theme\ComponentPluginManager $component_plugin_manager - * The component plugin manager. - */ - public function __construct(ComponentPluginManager $component_plugin_manager) { - $this->componentManager = $component_plugin_manager; - } - - /** - * {@inheritdoc} - */ - public function enterNode(Node $node, Environment $env): 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. - * - * A duplicate of \Drupal\Core\Template\ComponentNodeVisitor::getComponent() - * - * @param \Twig\Node\Node $node - * The node. - * - * @return \Drupal\Core\Plugin\Component|null - * The component, if any. - */ - protected function getComponent(Node $node): ?Component { - $component_id = $node->getTemplateName(); - if (!preg_match('/^[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*:[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*$/', $component_id)) { - return NULL; - } - try { - return $this->componentManager->find($component_id); - } - catch (ComponentNotFoundException $e) { - return NULL; - } - } - - /** - * 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. - * - * The function will be injected direct after validate_component_props - * function already injected by SDC's ComponentNodeVisitor. - * - * @param \Twig\Node\Node $node - * The node where we will inject the function in. - * @param \Twig\Node\Node $function - * The Twig function. - * - * @return \Twig\Node\Node - * The node with the function inserted. - */ - protected function injectFunction(Node $node, Node $function): Node { - $insertion = new Node([$node->getNode('display_start'), $function]); - $node->setNode('display_start', $insertion); - return $node; - } - -} diff --git a/src/Template/ModuleNodeVisitorAfterSdc.php b/src/Template/ModuleNodeVisitorAfterSdc.php new file mode 100755 index 0000000000000000000000000000000000000000..ecff57dc64783d97223366876e4b52fd212d0210 --- /dev/null +++ b/src/Template/ModuleNodeVisitorAfterSdc.php @@ -0,0 +1,76 @@ +<?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); + } + +} diff --git a/src/Template/ModuleNodeVisitorBase.php b/src/Template/ModuleNodeVisitorBase.php new file mode 100755 index 0000000000000000000000000000000000000000..d00f757bbb9b3e883953db98d950c98b6f74952f --- /dev/null +++ b/src/Template/ModuleNodeVisitorBase.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\ui_patterns\Template; + +use Drupal\Core\Plugin\Component; +use Drupal\Core\Render\Component\Exception\ComponentNotFoundException; +use Drupal\Core\Theme\ComponentPluginManager; +use Twig\Environment; +use Twig\Node\Node; +use Twig\NodeVisitor\NodeVisitorInterface; + +/** + * Provides a Node Visitor to change the generated parse-tree. + */ +abstract class ModuleNodeVisitorBase implements NodeVisitorInterface { + + /** + * The component plugin manager. + */ + protected ComponentPluginManager $componentManager; + + /** + * Constructs a new ComponentNodeVisitor object. + * + * @param \Drupal\Core\Theme\ComponentPluginManager $component_plugin_manager + * The component plugin manager. + */ + public function __construct(ComponentPluginManager $component_plugin_manager) { + $this->componentManager = $component_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public function enterNode(Node $node, Environment $env): Node { + return $node; + } + + /** + * Finds the SDC for the current module node. + * + * A duplicate of \Drupal\Core\Template\ComponentNodeVisitor::getComponent() + * + * @param \Twig\Node\Node $node + * The node. + * + * @return \Drupal\Core\Plugin\Component|null + * The component, if any. + */ + protected function getComponent(Node $node): ?Component { + $component_id = $node->getTemplateName(); + if (!preg_match('/^[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*:[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*$/', $component_id)) { + return NULL; + } + try { + return $this->componentManager->find($component_id); + } + catch (ComponentNotFoundException $e) { + return NULL; + } + } + + /** + * Injects custom Twig nodes into given node as child nodes. + * + * The function will be injected direct after validate_component_props + * function already injected by SDC's ComponentNodeVisitor. + * + * @param \Twig\Node\Node $node + * The node where we will inject the function in. + * @param \Twig\Node\Node $function + * The Twig function. + * + * @return \Twig\Node\Node + * The node with the function inserted. + */ + protected function injectFunction(Node $node, Node $function): Node { + $insertion = new Node([$node->getNode('display_start'), $function]); + $node->setNode('display_start', $insertion); + return $node; + } + +} diff --git a/src/Template/ModuleNodeVisitorBeforeSdc.php b/src/Template/ModuleNodeVisitorBeforeSdc.php new file mode 100755 index 0000000000000000000000000000000000000000..fea34b2d707f509d27eb43dba664c872734938a5 --- /dev/null +++ b/src/Template/ModuleNodeVisitorBeforeSdc.php @@ -0,0 +1,76 @@ +<?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); + } + +} diff --git a/src/Template/TwigExtension.php b/src/Template/TwigExtension.php index d193e8ab09c636b7e2664a4e18a4764849023ffd..c6f2e79e79092b8114198ed5e05fbfe349bebe49 100644 --- a/src/Template/TwigExtension.php +++ b/src/Template/TwigExtension.php @@ -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 diff --git a/tests/modules/ui_patterns_test/components/alert/alert.twig b/tests/modules/ui_patterns_test/components/alert/alert.twig index 04952e93a93d61ce32b1105071b2d5b8aac006b3..15212aeec969d0ae9d001dea651dae803401adaf 100644 --- a/tests/modules/ui_patterns_test/components/alert/alert.twig +++ b/tests/modules/ui_patterns_test/components/alert/alert.twig @@ -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 %} diff --git a/ui_patterns.services.yml b/ui_patterns.services.yml index abffbf352b4d9e402649948f7be24826407bb364..593f10246a27be5dcc66fbb7aab491a982d0e57e 100644 --- a/ui_patterns.services.yml +++ b/ui_patterns.services.yml @@ -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