Skip to content

Commit 44d5d6b

Browse files
committed
feature #1088 Add debug:component command (StevenRenaux)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- Add `debug:component` command | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Tickets | #102 | License | MIT Related to the Future Ideas section of #102 I added a debug:component command. ### Debugging component The `debug:component` command lists all your application components (TwigComponent and LiveComponent) who lives in templates/components directory: ``` $ php bin/console debug:component +---------------+-----------------------------+------------------------------------+ | Component | Class | Template | +---------------+-----------------------------+------------------------------------+ | Coucou | App\Components\Alert | components/Coucou.html.twig | | RandomNumber | App\Components\RandomNumber | components/RandomNumber.html.twig | | Test | App\Components\foo\Test | components/foo/Test.html.twig | | Button | Anonymous component | components/Button.html.twig | | foo:Anonymous | Anonymous component | components/foo/Anonymous.html.twig | +---------------+-----------------------------+------------------------------------+ ``` Pass the name of some component to this argument to print the component details: ``` $ php bin/console debug:component Test +---------------------------------------------------+-------------------------------+ | Property | Value | +---------------------------------------------------+-------------------------------+ | Component | Test | | Class | App\Components\foo\Test | | Template | components/foo/Test.html.twig | | Properties (type / name / default value if exist) | string $type = success | | | string $message | +---------------------------------------------------+-------------------------------+ ``` To get the details about an anonymous component who is rendered with sub directory, just add it to the name: ``` <div> <twig:foo:Anonymous label="Click Me!" :disabled="true" /> </div> ``` ``` $ php bin/console debug:component foo:Anonymous +--------------------------------------------+------------------------------------+ | Property | Value | +--------------------------------------------+------------------------------------+ | Component | foo:Anonymous | | Class | Anonymous component | | Template | components/foo/Anonymous.html.twig | | Properties (name / default value if exist) | label | | | name = toto | +--------------------------------------------+------------------------------------+ ``` Commits ------- 9d2108b Add `debug:component` command
2 parents 7ae6aa1 + 9d2108b commit 44d5d6b

File tree

9 files changed

+561
-0
lines changed

9 files changed

+561
-0
lines changed

src/LiveComponent/doc/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3145,6 +3145,12 @@ the change of one specific key:
31453145
}
31463146
}
31473147
3148+
Debugging Components
3149+
--------------------
3150+
3151+
Need to list or debug some component issues.
3152+
The `Twig Component debug command`_ can help you.
3153+
31483154
Test Helper
31493155
-----------
31503156

@@ -3254,3 +3260,4 @@ bound to Symfony's BC policy for the moment.
32543260
.. _`How to Work with Form Themes`: https://symfony.com/doc/current/form/form_themes.html
32553261
.. _`Symfony's built-in form theming techniques`: https://symfony.com/doc/current/form/form_themes.html
32563262
.. _`pass content to Twig Components`: https://symfony.com/bundles/ux-twig-component/current/index.html#passing-blocks
3263+
.. _`Twig Component debug command`: https://symfony.com/bundles/ux-twig-component/current/index.html#debugging-components

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",

src/TwigComponent/doc/index.rst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,65 @@ To tell the system that ``icon`` and ``type`` are props and not attributes, use
11141114
{% endif %}
11151115
</button>
11161116

1117+
Debugging Components
1118+
--------------------
1119+
1120+
As your application grows, you'll eventually have a lot of components.
1121+
This command will help you to debug some components issues.
1122+
First, the debug:twig-component command lists all your application components
1123+
who live in ``templates/components``:
1124+
1125+
.. code-block:: terminal
1126+
1127+
$ php bin/console debug:component
1128+
1129+
+---------------+-----------------------------+------------------------------------+------+
1130+
| Component | Class | Template | Live |
1131+
+---------------+-----------------------------+------------------------------------+------+
1132+
| Coucou | App\Components\Alert | components/Coucou.html.twig | |
1133+
| RandomNumber | App\Components\RandomNumber | components/RandomNumber.html.twig | X |
1134+
| Test | App\Components\foo\Test | components/foo/Test.html.twig | |
1135+
| Button | Anonymous component | components/Button.html.twig | |
1136+
| foo:Anonymous | Anonymous component | components/foo/Anonymous.html.twig | |
1137+
+---------------+-----------------------------+------------------------------------+------+
1138+
1139+
.. tip::
1140+
1141+
The Live column show you which component is a LiveComponent.
1142+
1143+
If you have some components who doesn't live in ``templates/components``,
1144+
but in ``templates/bar`` for example you can pass an option:
1145+
1146+
.. code-block:: terminal
1147+
1148+
$ php bin/console debug:twig-component --dir=bar
1149+
1150+
+----------------+-------------------------------+------------------------------+------+
1151+
| Component | Class | Template | Live |
1152+
+----------------+-------------------------------+------------------------------+------+
1153+
| OtherDirectory | App\Components\OtherDirectory | bar/OtherDirectory.html.twig | |
1154+
+----------------+-------------------------------+------------------------------+------+
1155+
1156+
And the name of some component to this argument to print the
1157+
component details:
1158+
1159+
.. code-block:: terminal
1160+
1161+
$ php bin/console debug:component RandomNumber
1162+
1163+
+---------------------------------------------------+-----------------------------------+
1164+
| Property | Value |
1165+
+---------------------------------------------------+-----------------------------------+
1166+
| Component | RandomNumber |
1167+
| Live | X |
1168+
| Class | App\Components\RandomNumber |
1169+
| Template | components/RandomNumber.html.twig |
1170+
| Properties (type / name / default value if exist) | string $name = toto |
1171+
| | string $type = test |
1172+
| Live Properties | int $max = 1000 |
1173+
| | int $min = 10 |
1174+
+---------------------------------------------------+-----------------------------------+
1175+
11171176
Test Helpers
11181177
------------
11191178

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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 Symfony\Component\Console\Attribute\AsCommand;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Helper\Table;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Input\InputOption;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
22+
use Symfony\Component\Finder\Finder;
23+
use Symfony\UX\LiveComponent\Attribute\LiveAction;
24+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
25+
use Symfony\UX\TwigComponent\Attribute\PostMount;
26+
use Symfony\UX\TwigComponent\Attribute\PreMount;
27+
use Symfony\UX\TwigComponent\ComponentFactory;
28+
use Symfony\UX\TwigComponent\ComponentMetadata;
29+
use Symfony\UX\TwigComponent\Twig\PropsNode;
30+
use Twig\Environment;
31+
32+
#[AsCommand(name: 'debug:twig-component', description: 'Display current components and them usages for an application')]
33+
class ComponentDebugCommand extends Command
34+
{
35+
public function __construct(private string $twigTemplatesPath, private ComponentFactory $componentFactory, private Environment $twigEnvironment, private iterable $components)
36+
{
37+
parent::__construct();
38+
}
39+
40+
protected function configure(): void
41+
{
42+
$this
43+
->setDefinition([
44+
new InputArgument('name', InputArgument::OPTIONAL, 'A component name'),
45+
new InputOption('dir', null, InputOption::VALUE_REQUIRED, 'Show all components with a specific directory in templates', 'components'),
46+
])
47+
->setHelp(<<<'EOF'
48+
The <info>%command.name%</info> display all components in your application:
49+
50+
<info>php %command.full_name%</info>
51+
52+
Find all components within a specific directory in templates by specifying the directory name with the <info>--dir</info> option:
53+
54+
<info>php %command.full_name% --dir=bar/foo</info>
55+
56+
EOF
57+
)
58+
;
59+
}
60+
61+
protected function execute(InputInterface $input, OutputInterface $output): int
62+
{
63+
$io = new SymfonyStyle($input, $output);
64+
$name = $input->getArgument('name');
65+
$componentsDir = $input->getOption('dir');
66+
67+
if (null !== $name) {
68+
try {
69+
$metadata = $this->componentFactory->metadataFor($name);
70+
} catch (\Exception $e) {
71+
$io->error($e->getMessage());
72+
73+
return Command::FAILURE;
74+
}
75+
76+
$class = $metadata->get('class');
77+
$live = null;
78+
$allProperties = [];
79+
80+
if ($class) {
81+
if ($metadata->get('live')) {
82+
$live = 'X';
83+
}
84+
85+
$reflectionClass = new \ReflectionClass($class);
86+
$properties = $reflectionClass->getProperties();
87+
$allLiveProperties = [];
88+
89+
foreach ($properties as $property) {
90+
if ($property->isPublic()) {
91+
$visibility = $property->getType()?->getName();
92+
$propertyName = $property->getName();
93+
$value = $property->getDefaultValue();
94+
$propertyAttributes = $property->getAttributes(LiveProp::class);
95+
96+
$propertyDisplay = $visibility.' $'.$propertyName.(null !== $value ? ' = '.$value : '');
97+
98+
if (\count($propertyAttributes) > 0) {
99+
$allLiveProperties[] = $propertyDisplay;
100+
} else {
101+
$allProperties[] = $propertyDisplay;
102+
}
103+
}
104+
}
105+
106+
$methods = $reflectionClass->getMethods();
107+
$allEvents = [];
108+
$allActions = [];
109+
110+
foreach ($methods as $method) {
111+
if ('mount' === $method->getName()) {
112+
$allEvents[] = 'Mount';
113+
}
114+
115+
foreach ($method->getAttributes() as $attribute) {
116+
if (PreMount::class === $attribute->getName()) {
117+
$allEvents[] = 'PreMount';
118+
break;
119+
}
120+
121+
if (PostMount::class === $attribute->getName()) {
122+
$allEvents[] = 'PostMount';
123+
break;
124+
}
125+
126+
if (LiveAction::class === $attribute->getName()) {
127+
$allActions[] = $method->getName();
128+
break;
129+
}
130+
}
131+
}
132+
} else {
133+
$allProperties = $this->getPropertiesForAnonymousComponent($metadata);
134+
}
135+
136+
$componentInfos = [
137+
['Component', $name],
138+
['Live', $live],
139+
['Class', $class ?? 'Anonymous component'],
140+
['Template', $metadata->getTemplate()],
141+
['Properties', \count($allProperties) > 0 ? implode("\n", $allProperties) : null],
142+
];
143+
144+
if (isset($allLiveProperties) && \count($allLiveProperties) > 0) {
145+
$componentInfos[] = ['Live Properties', implode("\n", $allLiveProperties)];
146+
}
147+
if (isset($allEvents) && \count($allEvents) > 0) {
148+
$componentInfos[] = ['Events', implode("\n", $allEvents)];
149+
}
150+
if (isset($allActions) && \count($allActions) > 0) {
151+
$componentInfos[] = ['LiveAction Methods', implode("\n", $allActions)];
152+
}
153+
154+
$table = new Table($output);
155+
$table->setHeaders(['Property', 'Value'])->setRows($componentInfos);
156+
$table->render();
157+
158+
return Command::SUCCESS;
159+
}
160+
161+
$finderTemplates = new Finder();
162+
$finderTemplates->files()->in("{$this->twigTemplatesPath}/components");
163+
164+
$anonymousTemplatesComponents = [];
165+
foreach ($finderTemplates as $template) {
166+
$anonymousTemplatesComponents[] = $template->getRelativePathname();
167+
}
168+
169+
$componentsWithClass = [];
170+
foreach ($this->components as $class) {
171+
$reflectionClass = new \ReflectionClass($class);
172+
$attributes = $reflectionClass->getAttributes();
173+
174+
foreach ($attributes as $attribute) {
175+
$arguments = $attribute->getArguments();
176+
177+
$name = $arguments['name'] ?? $arguments[0] ?? null;
178+
$template = $arguments['template'] ?? $arguments[1] ?? null;
179+
180+
if (null !== $template || null !== $name) {
181+
if (null !== $template && null !== $name) {
182+
$templateFile = str_replace('components/', '', $template);
183+
$metadata = $this->componentFactory->metadataFor($name);
184+
} elseif (null !== $name) {
185+
$templateFile = str_replace(':', '/', "{$name}.html.twig");
186+
$metadata = $this->componentFactory->metadataFor($name);
187+
} else {
188+
$templateFile = str_replace('components/', '', $template);
189+
$metadata = $this->componentFactory->metadataFor(str_replace('.html.twig', '', $templateFile));
190+
}
191+
} else {
192+
$templateFile = "{$reflectionClass->getShortName()}.html.twig";
193+
$metadata = $this->componentFactory->metadataFor($reflectionClass->getShortName());
194+
}
195+
196+
$componentsWithClass[] = [
197+
'name' => $metadata->getName(),
198+
'live' => null !== $metadata->get('live') ? 'X' : null,
199+
];
200+
201+
if (($key = array_search($templateFile, $anonymousTemplatesComponents)) !== false) {
202+
unset($anonymousTemplatesComponents[$key]);
203+
}
204+
}
205+
}
206+
207+
$anonymousComponents = array_map(fn ($template): array => [
208+
'name' => str_replace('/', ':', str_replace('.html.twig', '', $template)),
209+
'live' => null,
210+
], $anonymousTemplatesComponents);
211+
212+
$allComponents = array_merge($componentsWithClass, $anonymousComponents);
213+
$dataToRender = [];
214+
foreach ($allComponents as $component) {
215+
$metadata = $this->componentFactory->metadataFor($component['name']);
216+
217+
if (str_contains($metadata->getTemplate(), $componentsDir)) {
218+
$dataToRender[] = [
219+
$metadata->getName(),
220+
$metadata->get('class') ?? 'Anonymous component',
221+
$metadata->getTemplate(),
222+
$component['live'],
223+
];
224+
}
225+
}
226+
227+
$table = new Table($output);
228+
$table->setHeaders(['Component', 'Class', 'Template', 'Live'])->setRows($dataToRender);
229+
$table->render();
230+
231+
return Command::SUCCESS;
232+
}
233+
234+
private function getPropertiesForAnonymousComponent(ComponentMetadata $metadata): array
235+
{
236+
$allProperties = [];
237+
238+
$source = $this->twigEnvironment->load($metadata->getTemplate())->getSourceContext();
239+
$tokenStream = $this->twigEnvironment->tokenize($source);
240+
$bodyNode = $this->twigEnvironment->parse($tokenStream)->getNode('body')->getNode(0);
241+
242+
$propsNode = [];
243+
244+
foreach ($bodyNode as $node) {
245+
if ($node instanceof PropsNode) {
246+
$propsNode = $node;
247+
break;
248+
}
249+
}
250+
251+
if (\count($propsNode) > 0) {
252+
$allVariables = $propsNode->getAttribute('names');
253+
254+
foreach ($allVariables as $variable) {
255+
if ($propsNode->hasNode($variable)) {
256+
$value = $propsNode->getNode($variable)->getAttribute('value');
257+
258+
if (\is_bool($value)) {
259+
$value = $value ? 'true' : 'false';
260+
}
261+
262+
$property = $variable.' = '.$value;
263+
} else {
264+
$property = $variable;
265+
}
266+
267+
$allProperties[] = $property;
268+
}
269+
}
270+
271+
return $allProperties;
272+
}
273+
}

0 commit comments

Comments
 (0)