diff --git a/src/TwigComponent/CHANGELOG.md b/src/TwigComponent/CHANGELOG.md index 34d110e3b36..fd4fd592ef5 100644 --- a/src/TwigComponent/CHANGELOG.md +++ b/src/TwigComponent/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 2.23.0 + +- Add `ComponentPropertiesExtractor` to extract component properties from a Twig component + ## 2.20.0 - Add Anonymous Component support for 3rd-party bundles #2019 diff --git a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php index f883f59ef96..7593686bd41 100644 --- a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php +++ b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php @@ -24,6 +24,7 @@ use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate; use Symfony\UX\TwigComponent\ComponentFactory; use Symfony\UX\TwigComponent\ComponentMetadata; +use Symfony\UX\TwigComponent\ComponentPropertiesExtractor; use Symfony\UX\TwigComponent\Twig\PropsNode; use Twig\Environment; use Twig\Loader\FilesystemLoader; @@ -37,6 +38,7 @@ public function __construct( private string $twigTemplatesPath, private ComponentFactory $componentFactory, private Environment $twig, + private readonly ComponentPropertiesExtractor $componentPropertiesExtractor, private readonly array $componentClassMap, ?string $anonymousDirectory = null, ) { @@ -217,7 +219,7 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void $table->addRows([ ['Type', 'Anonymous'], new TableSeparator(), - ['Properties', implode("\n", $this->getAnonymousComponentProperties($metadata))], + ['Properties', implode("\n", $this->componentPropertiesExtractor->getComponentProperties($metadata))], ]); $table->render(); @@ -229,7 +231,7 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void new TableSeparator(), // ['Attributes Var', $metadata->get('attributes_var')], ['Public Props', $metadata->isPublicPropsExposed() ? 'Yes' : 'No'], - ['Properties', implode("\n", $this->getComponentProperties($metadata))], + ['Properties', implode("\n", $this->componentPropertiesExtractor->getComponentProperties($metadata))], ]); $logMethod = function (\ReflectionMethod $m) { @@ -280,80 +282,4 @@ private function displayComponentsTable(SymfonyStyle $io, array $components): vo } $table->render(); } - - /** - * @return array - */ - private function getComponentProperties(ComponentMetadata $metadata): array - { - $properties = []; - $reflectionClass = new \ReflectionClass($metadata->getClass()); - foreach ($reflectionClass->getProperties() as $property) { - $propertyName = $property->getName(); - - if ($metadata->isPublicPropsExposed() && $property->isPublic()) { - $type = $property->getType(); - if ($type instanceof \ReflectionNamedType) { - $typeName = $type->getName(); - } else { - $typeName = (string) $type; - } - $value = $property->getDefaultValue(); - $propertyDisplay = $typeName.' $'.$propertyName.(null !== $value ? ' = '.json_encode($value) : ''); - $properties[$property->name] = $propertyDisplay; - } - - foreach ($property->getAttributes(ExposeInTemplate::class) as $exposeAttribute) { - /** @var ExposeInTemplate $attribute */ - $attribute = $exposeAttribute->newInstance(); - $properties[$property->name] = $attribute->name ?? $property->name; - } - } - - return $properties; - } - - /** - * Extract properties from {% props %} tag in anonymous template. - * - * @return array - */ - private function getAnonymousComponentProperties(ComponentMetadata $metadata): array - { - $source = $this->twig->load($metadata->getTemplate())->getSourceContext(); - $tokenStream = $this->twig->tokenize($source); - $moduleNode = $this->twig->parse($tokenStream); - - $propsNode = null; - foreach ($moduleNode->getNode('body') as $bodyNode) { - foreach ($bodyNode as $node) { - if (PropsNode::class === $node::class) { - $propsNode = $node; - break 2; - } - } - } - if (!$propsNode instanceof PropsNode) { - return []; - } - - $propertyNames = $propsNode->getAttribute('names'); - $properties = array_combine($propertyNames, $propertyNames); - foreach ($propertyNames as $propName) { - if ($propsNode->hasNode($propName) - && ($valueNode = $propsNode->getNode($propName)) - && $valueNode->hasAttribute('value') - ) { - $value = $valueNode->getAttribute('value'); - if (\is_bool($value)) { - $value = $value ? 'true' : 'false'; - } else { - $value = json_encode($value); - } - $properties[$propName] = $propName.' = '.$value; - } - } - - return $properties; - } } diff --git a/src/TwigComponent/src/ComponentPropertiesExtractor.php b/src/TwigComponent/src/ComponentPropertiesExtractor.php new file mode 100644 index 00000000000..f07fbe7db1a --- /dev/null +++ b/src/TwigComponent/src/ComponentPropertiesExtractor.php @@ -0,0 +1,106 @@ + + */ + public function getComponentProperties(ComponentMetadata $medata) + { + if ($medata->isAnonymous()) { + return $this->getAnonymousComponentProperties($medata); + } + + return $this->getNonAnonymousComponentProperties($medata); + } + + /** + * @return array + */ + private function getNonAnonymousComponentProperties(ComponentMetadata $metadata): array + { + $properties = []; + $reflectionClass = new \ReflectionClass($metadata->getClass()); + foreach ($reflectionClass->getProperties() as $property) { + $propertyName = $property->getName(); + + if ($metadata->isPublicPropsExposed() && $property->isPublic()) { + $type = $property->getType(); + if ($type instanceof \ReflectionNamedType) { + $typeName = $type->getName(); + } else { + $typeName = (string)$type; + } + $value = $property->getDefaultValue(); + $propertyDisplay = $typeName . ' $' . $propertyName . (null !== $value ? ' = ' . json_encode( + $value + ) : ''); + $properties[$property->name] = $propertyDisplay; + } + + foreach ($property->getAttributes(ExposeInTemplate::class) as $exposeAttribute) { + /** @var ExposeInTemplate $attribute */ + $attribute = $exposeAttribute->newInstance(); + $properties[$property->name] = $attribute->name ?? $property->name; + } + } + + return $properties; + } + + /** + * Extract properties from {% props %} tag in anonymous template. + * + * @return array + */ + private function getAnonymousComponentProperties(ComponentMetadata $metadata): array + { + $source = $this->twig->load($metadata->getTemplate())->getSourceContext(); + $tokenStream = $this->twig->tokenize($source); + $moduleNode = $this->twig->parse($tokenStream); + + $propsNode = null; + foreach ($moduleNode->getNode('body') as $bodyNode) { + foreach ($bodyNode as $node) { + if (PropsNode::class === $node::class) { + $propsNode = $node; + break 2; + } + } + } + if (!$propsNode instanceof PropsNode) { + return []; + } + + $propertyNames = $propsNode->getAttribute('names'); + $properties = array_combine($propertyNames, $propertyNames); + foreach ($propertyNames as $propName) { + if ($propsNode->hasNode($propName) + && ($valueNode = $propsNode->getNode($propName)) + && $valueNode->hasAttribute('value') + ) { + $value = $valueNode->getAttribute('value'); + if (\is_bool($value)) { + $value = $value ? 'true' : 'false'; + } else { + $value = json_encode($value); + } + $properties[$propName] = $propName . ' = ' . $value; + } + } + + return $properties; + } +} \ No newline at end of file diff --git a/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php b/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php index b39ae4535dc..2709c7e896f 100644 --- a/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php +++ b/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php @@ -86,7 +86,7 @@ public function process(ContainerBuilder $container): void $componentPropertiesDefinition->setArgument(1, array_fill_keys(array_keys($componentClassMap), null)); $debugCommandDefinition = $container->findDefinition('ux.twig_component.command.debug'); - $debugCommandDefinition->setArgument(3, $componentClassMap); + $debugCommandDefinition->setArgument(4, $componentClassMap); } private function findMatchingDefaults(string $className, array $componentDefaults): ?array diff --git a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php index ebe2bb1753b..e7a1f5729c3 100644 --- a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php +++ b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php @@ -32,6 +32,7 @@ use Symfony\UX\TwigComponent\Command\TwigComponentDebugCommand; use Symfony\UX\TwigComponent\ComponentFactory; use Symfony\UX\TwigComponent\ComponentProperties; +use Symfony\UX\TwigComponent\ComponentPropertiesExtractor; use Symfony\UX\TwigComponent\ComponentRenderer; use Symfony\UX\TwigComponent\ComponentRendererInterface; use Symfony\UX\TwigComponent\ComponentStack; @@ -134,11 +135,17 @@ static function (ChildDefinition $definition, AsTwigComponent $attribute) { ->setDecoratedService(new Reference('twig.configurator.environment')) ->setArguments([new Reference('ux.twig_component.twig.environment_configurator.inner')]); + $container->register('ux.twig_component.extractor_properties', ComponentPropertiesExtractor::class) + ->setArguments([ + new Reference('twig'), + ]); + $container->register('ux.twig_component.command.debug', TwigComponentDebugCommand::class) ->setArguments([ new Parameter('twig.default_path'), new Reference('ux.twig_component.component_factory'), new Reference('twig'), + new Reference('ux.twig_component.extractor_properties'), new AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)), $config['anonymous_template_directory'], ])