Skip to content

Commit d103520

Browse files
committed
add debug:component command
1 parent 9265573 commit d103520

File tree

5 files changed

+377
-0
lines changed

5 files changed

+377
-0
lines changed

src/TwigComponent/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Add `RenderedComponent::crawler()` and `toString()` methods.
88
- Allow a block outside a Twig component to be available inside via `outerBlocks`.
99
- Fix `<twig:component>` syntax where an attribute is set to an empty value.
10+
- Add component debug command for TwigComponent and LiveComponent.
1011

1112
## 2.9.0
1213

src/TwigComponent/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"twig/twig": "^2.14.7|^3.0.4"
3535
},
3636
"require-dev": {
37+
"symfony/console": "^5.4|^6.0",
3738
"symfony/css-selector": "^5.4|^6.0",
3839
"symfony/dom-crawler": "^5.4|^6.0",
3940
"symfony/framework-bundle": "^5.4|^6.0",
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\Command;
13+
14+
use Exception;
15+
use Symfony\Component\Console\Attribute\AsCommand;
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Helper\Table;
18+
use Symfony\Component\Console\Input\InputArgument;
19+
use Symfony\Component\Console\Input\InputInterface;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
22+
use Symfony\Component\Finder\Finder;
23+
use Symfony\UX\TwigComponent\ComponentFactory;
24+
use Symfony\UX\TwigComponent\Twig\PropsNode;
25+
use Twig\Environment;
26+
27+
#[AsCommand(name: 'debug:component', description: 'Display current components and them usages for an application')]
28+
class ComponentDebugCommand extends Command
29+
{
30+
public function __construct(private string $twigTemplatesPath, private ComponentFactory $componentFactory, private Environment $twigEnvironment, private iterable $components)
31+
{
32+
parent::__construct();
33+
}
34+
35+
protected function configure(): void
36+
{
37+
$this
38+
->setDefinition([
39+
new InputArgument('name', InputArgument::OPTIONAL, 'A component name'),
40+
])
41+
->setHelp(<<<'EOF'
42+
The <info>%command.name%</info> display current components and them usages for an application:
43+
44+
<info>php %command.full_name%</info>
45+
46+
EOF
47+
)
48+
;
49+
}
50+
51+
protected function execute(InputInterface $input, OutputInterface $output): int
52+
{
53+
$io = new SymfonyStyle($input, $output);
54+
$name = $input->getArgument('name');
55+
56+
if ($name) {
57+
58+
try {
59+
$metadata = $this->componentFactory->metadataFor($name);
60+
} catch (Exception $e) {
61+
$io->error($e->getMessage());
62+
return Command::FAILURE;
63+
}
64+
65+
$class = $metadata->get('class');
66+
$allProperties = [];
67+
68+
if ($class) {
69+
70+
$propertyLabel = 'Properties (type / name / default value if exist)';
71+
72+
$reflectionClass = new \ReflectionClass($class);
73+
$properties = $reflectionClass->getProperties();
74+
75+
foreach ($properties as $property) {
76+
77+
if ($property->isPublic()) {
78+
79+
$visibility = $property->getType()?->getName();
80+
$propertyName = $property->getName();
81+
$value = $property->getDefaultValue();
82+
83+
$allProperties = [
84+
...$allProperties,
85+
$visibility . ' $' . $propertyName . ($value !== null ? ' = ' . $value : '')
86+
];
87+
}
88+
}
89+
} else {
90+
91+
$propertyLabel = 'Properties (name / default value if exist)';
92+
93+
$source = $this->twigEnvironment->load($metadata->getTemplate())->getSourceContext();
94+
$tokenStream = $this->twigEnvironment->tokenize($source);
95+
$bodyNode = $this->twigEnvironment->parse($tokenStream)->getNode('body')->getNode(0);
96+
97+
$propsNode = [];
98+
99+
foreach ($bodyNode as $node) {
100+
if ($node instanceof PropsNode) {
101+
$propsNode = $node;
102+
break;
103+
}
104+
}
105+
106+
if (count($propsNode) > 0) {
107+
108+
$allVariables = $propsNode->getAttribute('names');
109+
110+
foreach ($allVariables as $variable) {
111+
112+
if ($propsNode->hasNode($variable)) {
113+
114+
$value = $propsNode->getNode($variable)->getAttribute('value');
115+
116+
if (is_bool($value)) {
117+
$value = $value ? 'true' : 'false';
118+
}
119+
120+
$property = $variable . ' = ' . $value;
121+
122+
} else {
123+
$property = $variable;
124+
}
125+
126+
$allProperties = [
127+
...$allProperties,
128+
$property
129+
];
130+
}
131+
}
132+
}
133+
134+
$componentInfos = [
135+
['Component', $name],
136+
['Class', $class ?? 'Anonymous component'],
137+
['Template', $metadata->getTemplate()],
138+
[$propertyLabel, count($allProperties) > 0 ? implode("\n", $allProperties) : null],
139+
];
140+
141+
$table = new Table($output);
142+
$table->setHeaders(['Property', 'Value']);
143+
144+
foreach ($componentInfos as $info) {
145+
$table->addRow($info);
146+
}
147+
148+
$table->render();
149+
return Command::SUCCESS;
150+
}
151+
152+
$finderTemplates = new Finder();
153+
$finderTemplates->files()->in("{$this->twigTemplatesPath}/components");
154+
155+
$anonymousTemplatesComponents = [];
156+
foreach ($finderTemplates as $template) {
157+
$anonymousTemplatesComponents[] = $template->getRelativePathname();
158+
}
159+
160+
$componentsWithClass = [];
161+
foreach ($this->components as $class) {
162+
163+
$reflectionClass = new \ReflectionClass($class);
164+
$attributes = $reflectionClass->getAttributes();
165+
166+
foreach ($attributes as $attribute) {
167+
168+
$arguments = $attribute->getArguments();
169+
170+
$name = $arguments['name'] ?? $arguments[0] ?? null;
171+
$template = $arguments['template'] ?? $arguments[1] ?? null;
172+
173+
if ($template !== null || $name !== null) {
174+
175+
if ($template !== null && $name !== null) {
176+
177+
$templateFile = str_replace('components/', '', $template);
178+
$metadata = $this->componentFactory->metadataFor($name);
179+
180+
} elseif($name !== null) {
181+
182+
$templateFile = str_replace(':', '/', "{$name}.html.twig");
183+
$metadata = $this->componentFactory->metadataFor($name);
184+
185+
} else {
186+
187+
$templateFile = str_replace('components/', '', $template);
188+
$metadata = $this->componentFactory->metadataFor(str_replace('.html.twig', '', $templateFile));
189+
}
190+
} else {
191+
192+
$templateFile = "{$reflectionClass->getShortName()}.html.twig";;
193+
$metadata = $this->componentFactory->metadataFor($reflectionClass->getShortName());
194+
}
195+
196+
$componentsWithClass[] = $metadata->getName();
197+
198+
if (($key = array_search($templateFile, $anonymousTemplatesComponents)) !== false) {
199+
unset($anonymousTemplatesComponents[$key]);
200+
}
201+
}
202+
}
203+
204+
$anonymousComponents = array_map(fn($template): string => str_replace('/', ':', str_replace('.html.twig', '', $template)), $anonymousTemplatesComponents);
205+
206+
$allComponents = array_merge($componentsWithClass, $anonymousComponents);
207+
$dataToRender = [];
208+
foreach ($allComponents as $component) {
209+
210+
$metadata = $this->componentFactory->metadataFor($component);
211+
212+
$dataToRender = [ ...$dataToRender,
213+
[
214+
$metadata->getName(),
215+
$metadata->get('class') ?? 'Anonymous component',
216+
$metadata->getTemplate(),
217+
]
218+
];
219+
}
220+
221+
$table = new Table($output);
222+
$table->setHeaders(['Component', 'Class', 'Template']);
223+
224+
foreach ($dataToRender as $data) {
225+
$table->addRow($data);
226+
}
227+
228+
$table->render();
229+
230+
return Command::SUCCESS;
231+
}
232+
}

src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717
use Symfony\Component\DependencyInjection\Exception\LogicException;
1818
use Symfony\Component\DependencyInjection\Extension\Extension;
19+
use Symfony\Component\DependencyInjection\Parameter;
1920
use Symfony\Component\DependencyInjection\Reference;
2021
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
22+
use Symfony\UX\TwigComponent\Command\ComponentDebugCommand;
2123
use Symfony\UX\TwigComponent\ComponentFactory;
2224
use Symfony\UX\TwigComponent\ComponentRenderer;
2325
use Symfony\UX\TwigComponent\ComponentRendererInterface;
@@ -27,6 +29,7 @@
2729
use Symfony\UX\TwigComponent\Twig\ComponentExtension;
2830
use Symfony\UX\TwigComponent\Twig\ComponentLexer;
2931
use Symfony\UX\TwigComponent\Twig\TwigEnvironmentConfigurator;
32+
use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator;
3033

3134
/**
3235
* @author Kevin Bond <[email protected]>
@@ -91,5 +94,15 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %
9194
$container->register('ux.twig_component.twig.environment_configurator', TwigEnvironmentConfigurator::class)
9295
->setDecoratedService(new Reference('twig.configurator.environment'))
9396
->setArguments([new Reference('ux.twig_component.twig.environment_configurator.inner')]);
97+
98+
$container->register('console.command.stimulus_component_debug', ComponentDebugCommand::class)
99+
->setArguments([
100+
new Parameter('twig.default_path'),
101+
new Reference('ux.twig_component.component_factory'),
102+
new Reference('twig'),
103+
tagged_iterator('twig.component')
104+
])
105+
->addTag('console.command')
106+
;
94107
}
95108
}

0 commit comments

Comments
 (0)