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'],
])