From 111715c7e927c504fcdf3e8a013bf09597e71368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20L=C3=A9pine?= Date: Wed, 15 Jan 2025 17:46:31 +0100 Subject: [PATCH] changed named of PropertiesExtractor, and uses typed ComponentPropertyReflection --- src/TwigComponent/CHANGELOG.md | 2 +- .../src/Command/TwigComponentDebugCommand.php | 13 +- .../src/ComponentPropertyReflection.php | 59 ++++++++ ...sExtractor.php => ComponentReflection.php} | 67 +++------ .../TwigComponentExtension.php | 4 +- .../Unit/ComponentPropertiesExtractorTest.php | 106 -------------- .../tests/Unit/ComponentReflectionTest.php | 134 ++++++++++++++++++ 7 files changed, 226 insertions(+), 159 deletions(-) create mode 100644 src/TwigComponent/src/ComponentPropertyReflection.php rename src/TwigComponent/src/{ComponentPropertiesExtractor.php => ComponentReflection.php} (59%) delete mode 100644 src/TwigComponent/tests/Unit/ComponentPropertiesExtractorTest.php create mode 100644 src/TwigComponent/tests/Unit/ComponentReflectionTest.php diff --git a/src/TwigComponent/CHANGELOG.md b/src/TwigComponent/CHANGELOG.md index fd4fd592ef5..fb41a87ab0b 100644 --- a/src/TwigComponent/CHANGELOG.md +++ b/src/TwigComponent/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.23.0 -- Add `ComponentPropertiesExtractor` to extract component properties from a Twig component +- Add `ComponentReflection` to extract component properties from a Twig component ## 2.20.0 diff --git a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php index cfb31ad31f2..4a78a54c16e 100644 --- a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php +++ b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php @@ -23,7 +23,8 @@ use Symfony\Component\Finder\Finder; use Symfony\UX\TwigComponent\ComponentFactory; use Symfony\UX\TwigComponent\ComponentMetadata; -use Symfony\UX\TwigComponent\ComponentPropertiesExtractor; +use Symfony\UX\TwigComponent\ComponentPropertyReflection; +use Symfony\UX\TwigComponent\ComponentReflection; use Twig\Environment; use Twig\Loader\FilesystemLoader; @@ -31,7 +32,7 @@ class TwigComponentDebugCommand extends Command { private readonly string $anonymousDirectory; - private readonly ComponentPropertiesExtractor $componentPropertiesExtractor; + private readonly ComponentReflection $componentReflection; public function __construct( private string $twigTemplatesPath, @@ -39,11 +40,11 @@ public function __construct( private Environment $twig, private readonly array $componentClassMap, ?string $anonymousDirectory = null, - ?ComponentPropertiesExtractor $componentPropertiesExtractor = null, + ?ComponentReflection $componentReflection = null, ) { parent::__construct(); $this->anonymousDirectory = $anonymousDirectory ?? 'components'; - $this->componentPropertiesExtractor = $componentPropertiesExtractor ?? new ComponentPropertiesExtractor($this->twig); + $this->componentReflection = $componentReflection ?? new ComponentReflection($this->twig); } protected function configure(): void @@ -214,9 +215,9 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void ['Template', $metadata->getTemplate()], ]); - $properties = $this->componentPropertiesExtractor->getComponentProperties($metadata); + $properties = $this->componentReflection->getProperties($metadata); $propertiesAsArrayOfStrings = array_filter(array_map( - fn (array $property) => $property['display'], + fn (ComponentPropertyReflection $property) => $property->getCode(), $properties, )); diff --git a/src/TwigComponent/src/ComponentPropertyReflection.php b/src/TwigComponent/src/ComponentPropertyReflection.php new file mode 100644 index 00000000000..892297e5ff1 --- /dev/null +++ b/src/TwigComponent/src/ComponentPropertyReflection.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent; + +/** + * @author Jean-François Lépine + */ +class ComponentPropertyReflection +{ + public function __construct( + private readonly ComponentMetadata $metadata, + private readonly string $name, + private readonly string $type = 'mixed', + private readonly mixed $defaultValue = null, + ) { + } + + public function getCode(): string + { + if (null === $this->defaultValue) { + return \sprintf('%s $%s = ""', $this->type, $this->name); + } + + if (\is_bool($this->defaultValue)) { + return \sprintf('%s $%s = %s', $this->type, $this->name, $this->defaultValue ? 'true' : 'false'); + } + + return \sprintf('%s $%s = %s', $this->type, $this->name, json_encode($this->defaultValue)); + } + + public function getName(): string + { + return $this->name; + } + + public function getType(): string + { + return $this->type; + } + + public function getDefaultValue(): mixed + { + return $this->defaultValue; + } + + public function getMetadata(): ComponentMetadata + { + return $this->metadata; + } +} diff --git a/src/TwigComponent/src/ComponentPropertiesExtractor.php b/src/TwigComponent/src/ComponentReflection.php similarity index 59% rename from src/TwigComponent/src/ComponentPropertiesExtractor.php rename to src/TwigComponent/src/ComponentReflection.php index b7e5d06123b..a3403a444ab 100644 --- a/src/TwigComponent/src/ComponentPropertiesExtractor.php +++ b/src/TwigComponent/src/ComponentReflection.php @@ -15,7 +15,10 @@ use Symfony\UX\TwigComponent\Twig\PropsNode; use Twig\Environment; -final class ComponentPropertiesExtractor +/** + * @author Jean-François Lépine + */ +final class ComponentReflection { public function __construct( private readonly Environment $twig, @@ -23,9 +26,9 @@ public function __construct( } /** - * @return array + * @return ComponentPropertyReflection[] */ - public function getComponentProperties(ComponentMetadata $medata) + public function getProperties(ComponentMetadata $medata): array { if ($medata->isAnonymous()) { return $this->getAnonymousComponentProperties($medata); @@ -34,8 +37,15 @@ public function getComponentProperties(ComponentMetadata $medata) return $this->getNonAnonymousComponentProperties($medata); } + public function getProperty(ComponentMetadata $medata, string $name): ?ComponentPropertyReflection + { + $properties = $this->getProperties($medata); + + return $properties[$name] ?? null; + } + /** - * @return array + * @return ComponentPropertyReflection[] */ private function getNonAnonymousComponentProperties(ComponentMetadata $metadata): array { @@ -52,26 +62,13 @@ private function getNonAnonymousComponentProperties(ComponentMetadata $metadata) $typeName = (string) $type; } $value = $property->getDefaultValue(); - $propertyDisplay = $typeName.' $'.$propertyName.(null !== $value ? ' = '.json_encode( - $value - ) : ''); - $properties[$property->name] = [ - 'name' => $propertyName, - 'display' => $propertyDisplay, - 'type' => $typeName, - 'default' => $value, - ]; + $properties[$propertyName] = new ComponentPropertyReflection($metadata, $propertyName, $typeName, $value); } foreach ($property->getAttributes(ExposeInTemplate::class) as $exposeAttribute) { /** @var ExposeInTemplate $attribute */ $attribute = $exposeAttribute->newInstance(); - $properties[$property->name] = [ - 'name' => $attribute->name ?? $property->name, - 'display' => $attribute->name ?? $property->name, - 'type' => 'mixed', - 'default' => null, - ]; + $properties[$property->name] = new ComponentPropertyReflection($metadata, $attribute->name ?? $property->name); } } @@ -81,7 +78,7 @@ private function getNonAnonymousComponentProperties(ComponentMetadata $metadata) /** * Extract properties from {% props %} tag in anonymous template. * - * @return array + * @return ComponentPropertyReflection[] */ private function getAnonymousComponentProperties(ComponentMetadata $metadata): array { @@ -103,36 +100,18 @@ private function getAnonymousComponentProperties(ComponentMetadata $metadata): a } $propertyNames = $propsNode->getAttribute('names'); - $properties = array_combine($propertyNames, $propertyNames); + $properties = []; + foreach ($propertyNames as $propName) { + $properties[$propName] = new ComponentPropertyReflection($metadata, $propName, 'mixed'); + } + 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); - } - $display = $propName.' = '.$value; - $properties[$propName] = [ - 'name' => $propName, - 'display' => $display, - 'type' => \is_bool($value) ? 'bool' : 'mixed', - 'default' => $value, - ]; - } - } - - foreach ($properties as $propertyData) { - if (\is_string($propertyData)) { - $properties[$propertyData] = [ - 'name' => $propertyData, - 'display' => $propertyData, - 'type' => 'mixed', - 'default' => null, - ]; + $properties[$propName] = new ComponentPropertyReflection($metadata, $propName, 'mixed', $value); } } diff --git a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php index 90b9fc46c2a..6980503a5d0 100644 --- a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php +++ b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php @@ -32,7 +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\ComponentReflection; use Symfony\UX\TwigComponent\ComponentRenderer; use Symfony\UX\TwigComponent\ComponentRendererInterface; use Symfony\UX\TwigComponent\ComponentStack; @@ -135,7 +135,7 @@ 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) + $container->register('ux.twig_component.extractor_properties', ComponentReflection::class) ->setArguments([ new Reference('twig'), ]); diff --git a/src/TwigComponent/tests/Unit/ComponentPropertiesExtractorTest.php b/src/TwigComponent/tests/Unit/ComponentPropertiesExtractorTest.php deleted file mode 100644 index 4c6b31ca524..00000000000 --- a/src/TwigComponent/tests/Unit/ComponentPropertiesExtractorTest.php +++ /dev/null @@ -1,106 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\TwigComponent\Tests\Unit; - -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\UX\TwigComponent\ComponentFactory; -use Symfony\UX\TwigComponent\ComponentPropertiesExtractor; -use Twig\Environment; - -class ComponentPropertiesExtractorTest extends KernelTestCase -{ - public function testPropsAreFoundInTwigComponent(): void - { - /** @var ComponentFactory $factory */ - $factory = self::getContainer()->get('ux.twig_component.component_factory'); - $twig = self::getContainer()->get(Environment::class); - $metadata = $factory->metadataFor('DivComponent5'); - - $extractor = new ComponentPropertiesExtractor($twig); - $attributes = $extractor->getComponentProperties($metadata); - - $this->assertEquals([ - 'divComponentName' => [ - 'display' => 'string $divComponentName = "foo"', - 'name' => 'divComponentName', - 'type' => 'string', - 'default' => 'foo', - ], - ], $attributes); - } - - public function testPropsAreFoundInTwigComponentWithoutProps(): void - { - /** @var ComponentFactory $factory */ - $factory = self::getContainer()->get('ux.twig_component.component_factory'); - $twig = self::getContainer()->get(Environment::class); - $metadata = $factory->metadataFor('DivComponent6'); - - $extractor = new ComponentPropertiesExtractor($twig); - $attributes = $extractor->getComponentProperties($metadata); - - $this->assertEmpty($attributes); - } - - public function testPropsAreFoundInTwigAnonymousComponent(): void - { - /** @var ComponentFactory $factory */ - $factory = self::getContainer()->get('ux.twig_component.component_factory'); - $twig = self::getContainer()->get(Environment::class); - $metadata = $factory->metadataFor('Button'); - - $extractor = new ComponentPropertiesExtractor($twig); - $attributes = $extractor->getComponentProperties($metadata); - - $expected = [ - 'label' => [ - 'display' => 'label', - 'name' => 'label', - 'type' => 'mixed', - 'default' => null, - ], - 'primary' => [ - 'display' => 'primary = true', - 'name' => 'primary', - 'type' => 'mixed', - 'default' => 'true', - ], - ]; - $this->assertEquals($expected, $attributes); - } - - public function testPropsAreFoundInTwigAnonymousComponentWithJusteAttributes(): void - { - /** @var ComponentFactory $factory */ - $factory = self::getContainer()->get('ux.twig_component.component_factory'); - $twig = self::getContainer()->get(Environment::class); - $metadata = $factory->metadataFor('JustAttributes'); - - $extractor = new ComponentPropertiesExtractor($twig); - $attributes = $extractor->getComponentProperties($metadata); - - $this->assertEmpty($attributes); - } - - public function testPropsAreFoundInTwigAnonymousComponentWithEmptyProps(): void - { - /** @var ComponentFactory $factory */ - $factory = self::getContainer()->get('ux.twig_component.component_factory'); - $twig = self::getContainer()->get(Environment::class); - $metadata = $factory->metadataFor('EmptyProps'); - - $extractor = new ComponentPropertiesExtractor($twig); - $attributes = $extractor->getComponentProperties($metadata); - - $this->assertEmpty($attributes); - } -} diff --git a/src/TwigComponent/tests/Unit/ComponentReflectionTest.php b/src/TwigComponent/tests/Unit/ComponentReflectionTest.php new file mode 100644 index 00000000000..636f2c95761 --- /dev/null +++ b/src/TwigComponent/tests/Unit/ComponentReflectionTest.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Tests\Unit; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\UX\TwigComponent\ComponentFactory; +use Symfony\UX\TwigComponent\ComponentPropertyReflection; +use Symfony\UX\TwigComponent\ComponentReflection; +use Twig\Environment; + +class ComponentReflectionTest extends KernelTestCase +{ + public function testPropsAreFoundInTwigComponent(): void + { + /** @var ComponentFactory $factory */ + $factory = self::getContainer()->get('ux.twig_component.component_factory'); + $twig = self::getContainer()->get(Environment::class); + $metadata = $factory->metadataFor('DivComponent5'); + + $extractor = new ComponentReflection($twig); + $attributes = $extractor->getProperties($metadata); + + $this->assertCount(1, $attributes); + $this->assertInstanceOf(ComponentPropertyReflection::class, $attributes['divComponentName']); + $property = $attributes['divComponentName']; + $this->assertEquals('string $divComponentName = "foo"', $property->getCode()); + $this->assertEquals('divComponentName', $property->getName()); + $this->assertEquals('string', $property->getType()); + $this->assertEquals('foo', $property->getDefaultValue()); + } + + public function testPropsAreFoundInTwigComponentWithoutProps(): void + { + /** @var ComponentFactory $factory */ + $factory = self::getContainer()->get('ux.twig_component.component_factory'); + $twig = self::getContainer()->get(Environment::class); + $metadata = $factory->metadataFor('DivComponent6'); + + $extractor = new ComponentReflection($twig); + $attributes = $extractor->getProperties($metadata); + + $this->assertEmpty($attributes); + } + + public function testPropsAreFoundInTwigAnonymousComponent(): void + { + /** @var ComponentFactory $factory */ + $factory = self::getContainer()->get('ux.twig_component.component_factory'); + $twig = self::getContainer()->get(Environment::class); + $metadata = $factory->metadataFor('Button'); + + $extractor = new ComponentReflection($twig); + $attributes = $extractor->getProperties($metadata); + + $this->assertCount(2, $attributes); + $this->assertInstanceOf(ComponentPropertyReflection::class, $attributes['label']); + $this->assertInstanceOf(ComponentPropertyReflection::class, $attributes['primary']); + + $this->assertEquals('mixed $label = ""', $attributes['label']->getCode()); + $this->assertEquals('label', $attributes['label']->getName()); + $this->assertEquals('mixed', $attributes['label']->getType()); + $this->assertNull($attributes['label']->getDefaultValue()); + + $this->assertEquals('mixed $primary = true', $attributes['primary']->getCode()); + $this->assertEquals('primary', $attributes['primary']->getName()); + $this->assertEquals('mixed', $attributes['primary']->getType()); + $this->assertEquals('true', $attributes['primary']->getDefaultValue()); + } + + public function testPropsAreFoundInTwigAnonymousComponentWithJusteAttributes(): void + { + /** @var ComponentFactory $factory */ + $factory = self::getContainer()->get('ux.twig_component.component_factory'); + $twig = self::getContainer()->get(Environment::class); + $metadata = $factory->metadataFor('JustAttributes'); + + $extractor = new ComponentReflection($twig); + $attributes = $extractor->getProperties($metadata); + + $this->assertEmpty($attributes); + } + + public function testPropsAreFoundInTwigAnonymousComponentWithEmptyProps(): void + { + /** @var ComponentFactory $factory */ + $factory = self::getContainer()->get('ux.twig_component.component_factory'); + $twig = self::getContainer()->get(Environment::class); + $metadata = $factory->metadataFor('EmptyProps'); + + $extractor = new ComponentReflection($twig); + $attributes = $extractor->getProperties($metadata); + + $this->assertEmpty($attributes); + } + + public function testGetPropertyByName(): void + { + /** @var ComponentFactory $factory */ + $factory = self::getContainer()->get('ux.twig_component.component_factory'); + $twig = self::getContainer()->get(Environment::class); + $metadata = $factory->metadataFor('DivComponent5'); + + $extractor = new ComponentReflection($twig); + $property = $extractor->getProperty($metadata, 'divComponentName'); + + $this->assertInstanceOf(ComponentPropertyReflection::class, $property); + $this->assertEquals('string $divComponentName = "foo"', $property->getCode()); + $this->assertEquals('divComponentName', $property->getName()); + $this->assertEquals('string', $property->getType()); + $this->assertEquals('foo', $property->getDefaultValue()); + } + + public function testUnexistantPropertyByNameReturnsNull(): void + { + /** @var ComponentFactory $factory */ + $factory = self::getContainer()->get('ux.twig_component.component_factory'); + $twig = self::getContainer()->get(Environment::class); + $metadata = $factory->metadataFor('DivComponent5'); + + $extractor = new ComponentReflection($twig); + $property = $extractor->getProperty($metadata, 'unexistant'); + + $this->assertNull($property); + } +}