Skip to content

Commit

Permalink
Add new ComponentPropertiesExtractor to extract properties from TwigC…
Browse files Browse the repository at this point in the history
…omponent
  • Loading branch information
Halleck45 committed Jan 10, 2025
1 parent a471878 commit 93ba131
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 79 deletions.
4 changes: 4 additions & 0 deletions src/TwigComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
82 changes: 4 additions & 78 deletions src/TwigComponent/src/Command/TwigComponentDebugCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
) {
Expand Down Expand Up @@ -217,7 +219,7 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void
$table->addRows([
['Type', '<comment>Anonymous</comment>'],
new TableSeparator(),
['Properties', implode("\n", $this->getAnonymousComponentProperties($metadata))],
['Properties', implode("\n", $this->componentPropertiesExtractor->getComponentProperties($metadata))],
]);
$table->render();

Expand All @@ -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) {
Expand Down Expand Up @@ -280,80 +282,4 @@ private function displayComponentsTable(SymfonyStyle $io, array $components): vo
}
$table->render();
}

/**
* @return array<string, string>
*/
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<string, string>
*/
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;
}
}
106 changes: 106 additions & 0 deletions src/TwigComponent/src/ComponentPropertiesExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace Symfony\UX\TwigComponent;

use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\Twig\PropsNode;
use Twig\Environment;

final class ComponentPropertiesExtractor
{
public function __construct(
private readonly Environment $twig
)
{
}

/**
* @return array<string, string>
*/
public function getComponentProperties(ComponentMetadata $medata)
{
if ($medata->isAnonymous()) {
return $this->getAnonymousComponentProperties($medata);
}

return $this->getNonAnonymousComponentProperties($medata);
}

/**
* @return array<string, string>
*/
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<string, string>
*/
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'],
])
Expand Down

0 comments on commit 93ba131

Please sign in to comment.