Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TwigComponent] Add new ComponentReflection to extract properties from TwigComponent #2498

Closed
wants to merge 9 commits into from
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to expose this class (with BC promise) for something like this. At least in a first step.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, BC is resolved now. Do you still want me to remove this change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, my point is i'm not sure we should make this class "public" for now. Because once it's done, we cannot go back.

And, as i said in the previous message, we need to clarifiy first whose class has which responsibility, the contracts, etc.

Halleck45 marked this conversation as resolved.
Show resolved Hide resolved

## 2.20.0

- Add Anonymous Component support for 3rd-party bundles #2019
Expand Down
92 changes: 12 additions & 80 deletions src/TwigComponent/src/Command/TwigComponentDebugCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,29 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\Finder;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentMetadata;
use Symfony\UX\TwigComponent\Twig\PropsNode;
use Symfony\UX\TwigComponent\ComponentPropertiesExtractor;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;

#[AsCommand(name: 'debug:twig-component', description: 'Display components and them usages for an application')]
class TwigComponentDebugCommand extends Command
{
private readonly string $anonymousDirectory;
private readonly ComponentPropertiesExtractor $componentPropertiesExtractor;

public function __construct(
private string $twigTemplatesPath,
private ComponentFactory $componentFactory,
private Environment $twig,
private readonly array $componentClassMap,
?string $anonymousDirectory = null,
?ComponentPropertiesExtractor $componentPropertiesExtractor = null,
) {
parent::__construct();
$this->anonymousDirectory = $anonymousDirectory ?? 'components';
$this->componentPropertiesExtractor = $componentPropertiesExtractor ?? new ComponentPropertiesExtractor($this->twig);
smnandre marked this conversation as resolved.
Show resolved Hide resolved
}

protected function configure(): void
Expand Down Expand Up @@ -212,12 +214,18 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void
['Template', $metadata->getTemplate()],
]);

$properties = $this->componentPropertiesExtractor->getComponentProperties($metadata);
$propertiesAsArrayOfStrings = array_filter(array_map(
fn (array $property) => $property['display'],
$properties,
));
Comment on lines +218 to +221
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$propertiesAsArrayOfStrings = array_filter(array_map(
fn (array $property) => $property['display'],
$properties,
));
$properties = array_column($properties, 'display');

1/3


// Anonymous Component
if ($metadata->isAnonymous()) {
$table->addRows([
['Type', '<comment>Anonymous</comment>'],
new TableSeparator(),
['Properties', implode("\n", $this->getAnonymousComponentProperties($metadata))],
['Properties', implode("\n", $propertiesAsArrayOfStrings)],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
['Properties', implode("\n", $propertiesAsArrayOfStrings)],
['Properties', implode("\n", $properties)],

2/3

]);
$table->render();

Expand All @@ -229,7 +237,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", $propertiesAsArrayOfStrings)],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
['Properties', implode("\n", $propertiesAsArrayOfStrings)],
['Properties', implode("\n", $properties)],

3/3

]);

$logMethod = function (\ReflectionMethod $m) {
Expand Down Expand Up @@ -280,80 +288,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;
}
}
141 changes: 141 additions & 0 deletions src/TwigComponent/src/ComponentPropertiesExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

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>
*/
smnandre marked this conversation as resolved.
Show resolved Hide resolved
public function getComponentProperties(ComponentMetadata $medata)
Halleck45 marked this conversation as resolved.
Show resolved Hide resolved
{
if ($medata->isAnonymous()) {
return $this->getAnonymousComponentProperties($medata);
}

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

/**
* @return array<string, string>
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the good type (you return an array of string => array{name: display: type: ...} for no)

private function getNonAnonymousComponentProperties(ComponentMetadata $metadata): array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid double negation if possible. Anonymous component are a special type of components.

But "non anonymous components" are just "components".

{
$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
) : '');
Comment on lines +64 to +66
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$propertyDisplay = $typeName.' $'.$propertyName.(null !== $value ? ' = '.json_encode(
$value
) : '');
$propertyDisplay = $typeName.' $'.$propertyName.(null !== $value ? ' = '.json_encode($value) : '');

$properties[$property->name] = [
'name' => $propertyName,
'display' => $propertyDisplay,
'type' => $typeName,
'default' => $value,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be json encoded as the other one

];
}

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,
];
}
}

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);
}
$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,
];
}
}

return $properties;
}
}
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,13 +135,19 @@ 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 AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)),
$config['anonymous_template_directory'],
new Reference('ux.twig_component.extractor_properties'),
])
->addTag('console.command')
;
Expand Down
Loading
Loading