Skip to content

Commit

Permalink
feature #1088 Add debug:component command (StevenRenaux)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
weaverryan committed Sep 22, 2023
2 parents 7ae6aa1 + 9d2108b commit 44d5d6b
Show file tree
Hide file tree
Showing 9 changed files with 561 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/LiveComponent/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3145,6 +3145,12 @@ the change of one specific key:
}
}
Debugging Components
--------------------

Need to list or debug some component issues.
The `Twig Component debug command`_ can help you.

Test Helper
-----------

Expand Down Expand Up @@ -3254,3 +3260,4 @@ bound to Symfony's BC policy for the moment.
.. _`How to Work with Form Themes`: https://symfony.com/doc/current/form/form_themes.html
.. _`Symfony's built-in form theming techniques`: https://symfony.com/doc/current/form/form_themes.html
.. _`pass content to Twig Components`: https://symfony.com/bundles/ux-twig-component/current/index.html#passing-blocks
.. _`Twig Component debug command`: https://symfony.com/bundles/ux-twig-component/current/index.html#debugging-components
1 change: 1 addition & 0 deletions src/TwigComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Add `RenderedComponent::crawler()` and `toString()` methods.
- Allow a block outside a Twig component to be available inside via `outerBlocks`.
- Fix `<twig:component>` syntax where an attribute is set to an empty value.
- Add component debug command for TwigComponent and LiveComponent.

## 2.9.0

Expand Down
1 change: 1 addition & 0 deletions src/TwigComponent/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"twig/twig": "^2.14.7|^3.0.4"
},
"require-dev": {
"symfony/console": "^5.4|^6.0",
"symfony/css-selector": "^5.4|^6.0",
"symfony/dom-crawler": "^5.4|^6.0",
"symfony/framework-bundle": "^5.4|^6.0",
Expand Down
59 changes: 59 additions & 0 deletions src/TwigComponent/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,65 @@ To tell the system that ``icon`` and ``type`` are props and not attributes, use
{% endif %}
</button>

Debugging Components
--------------------

As your application grows, you'll eventually have a lot of components.
This command will help you to debug some components issues.
First, the debug:twig-component command lists all your application components
who live in ``templates/components``:

.. code-block:: terminal
$ php bin/console debug:component
+---------------+-----------------------------+------------------------------------+------+
| Component | Class | Template | Live |
+---------------+-----------------------------+------------------------------------+------+
| Coucou | App\Components\Alert | components/Coucou.html.twig | |
| RandomNumber | App\Components\RandomNumber | components/RandomNumber.html.twig | X |
| 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 | |
+---------------+-----------------------------+------------------------------------+------+
.. tip::

The Live column show you which component is a LiveComponent.

If you have some components who doesn't live in ``templates/components``,
but in ``templates/bar`` for example you can pass an option:

.. code-block:: terminal
$ php bin/console debug:twig-component --dir=bar
+----------------+-------------------------------+------------------------------+------+
| Component | Class | Template | Live |
+----------------+-------------------------------+------------------------------+------+
| OtherDirectory | App\Components\OtherDirectory | bar/OtherDirectory.html.twig | |
+----------------+-------------------------------+------------------------------+------+
And the name of some component to this argument to print the
component details:

.. code-block:: terminal
$ php bin/console debug:component RandomNumber
+---------------------------------------------------+-----------------------------------+
| Property | Value |
+---------------------------------------------------+-----------------------------------+
| Component | RandomNumber |
| Live | X |
| Class | App\Components\RandomNumber |
| Template | components/RandomNumber.html.twig |
| Properties (type / name / default value if exist) | string $name = toto |
| | string $type = test |
| Live Properties | int $max = 1000 |
| | int $min = 10 |
+---------------------------------------------------+-----------------------------------+
Test Helpers
------------

Expand Down
273 changes: 273 additions & 0 deletions src/TwigComponent/src/Command/ComponentDebugCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<?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\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\Finder;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\TwigComponent\Attribute\PostMount;
use Symfony\UX\TwigComponent\Attribute\PreMount;
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentMetadata;
use Symfony\UX\TwigComponent\Twig\PropsNode;
use Twig\Environment;

#[AsCommand(name: 'debug:twig-component', description: 'Display current components and them usages for an application')]
class ComponentDebugCommand extends Command
{
public function __construct(private string $twigTemplatesPath, private ComponentFactory $componentFactory, private Environment $twigEnvironment, private iterable $components)
{
parent::__construct();
}

protected function configure(): void
{
$this
->setDefinition([
new InputArgument('name', InputArgument::OPTIONAL, 'A component name'),
new InputOption('dir', null, InputOption::VALUE_REQUIRED, 'Show all components with a specific directory in templates', 'components'),
])
->setHelp(<<<'EOF'
The <info>%command.name%</info> display all components in your application:
<info>php %command.full_name%</info>
Find all components within a specific directory in templates by specifying the directory name with the <info>--dir</info> option:
<info>php %command.full_name% --dir=bar/foo</info>

EOF
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$name = $input->getArgument('name');
$componentsDir = $input->getOption('dir');

if (null !== $name) {
try {
$metadata = $this->componentFactory->metadataFor($name);
} catch (\Exception $e) {
$io->error($e->getMessage());

return Command::FAILURE;
}

$class = $metadata->get('class');
$live = null;
$allProperties = [];

if ($class) {
if ($metadata->get('live')) {
$live = 'X';
}

$reflectionClass = new \ReflectionClass($class);
$properties = $reflectionClass->getProperties();
$allLiveProperties = [];

foreach ($properties as $property) {
if ($property->isPublic()) {
$visibility = $property->getType()?->getName();
$propertyName = $property->getName();
$value = $property->getDefaultValue();
$propertyAttributes = $property->getAttributes(LiveProp::class);

$propertyDisplay = $visibility.' $'.$propertyName.(null !== $value ? ' = '.$value : '');

if (\count($propertyAttributes) > 0) {
$allLiveProperties[] = $propertyDisplay;
} else {
$allProperties[] = $propertyDisplay;
}
}
}

$methods = $reflectionClass->getMethods();
$allEvents = [];
$allActions = [];

foreach ($methods as $method) {
if ('mount' === $method->getName()) {
$allEvents[] = 'Mount';
}

foreach ($method->getAttributes() as $attribute) {
if (PreMount::class === $attribute->getName()) {
$allEvents[] = 'PreMount';
break;
}

if (PostMount::class === $attribute->getName()) {
$allEvents[] = 'PostMount';
break;
}

if (LiveAction::class === $attribute->getName()) {
$allActions[] = $method->getName();
break;
}
}
}
} else {
$allProperties = $this->getPropertiesForAnonymousComponent($metadata);
}

$componentInfos = [
['Component', $name],
['Live', $live],
['Class', $class ?? 'Anonymous component'],
['Template', $metadata->getTemplate()],
['Properties', \count($allProperties) > 0 ? implode("\n", $allProperties) : null],
];

if (isset($allLiveProperties) && \count($allLiveProperties) > 0) {
$componentInfos[] = ['Live Properties', implode("\n", $allLiveProperties)];
}
if (isset($allEvents) && \count($allEvents) > 0) {
$componentInfos[] = ['Events', implode("\n", $allEvents)];
}
if (isset($allActions) && \count($allActions) > 0) {
$componentInfos[] = ['LiveAction Methods', implode("\n", $allActions)];
}

$table = new Table($output);
$table->setHeaders(['Property', 'Value'])->setRows($componentInfos);
$table->render();

return Command::SUCCESS;
}

$finderTemplates = new Finder();
$finderTemplates->files()->in("{$this->twigTemplatesPath}/components");

$anonymousTemplatesComponents = [];
foreach ($finderTemplates as $template) {
$anonymousTemplatesComponents[] = $template->getRelativePathname();
}

$componentsWithClass = [];
foreach ($this->components as $class) {
$reflectionClass = new \ReflectionClass($class);
$attributes = $reflectionClass->getAttributes();

foreach ($attributes as $attribute) {
$arguments = $attribute->getArguments();

$name = $arguments['name'] ?? $arguments[0] ?? null;
$template = $arguments['template'] ?? $arguments[1] ?? null;

if (null !== $template || null !== $name) {
if (null !== $template && null !== $name) {
$templateFile = str_replace('components/', '', $template);
$metadata = $this->componentFactory->metadataFor($name);
} elseif (null !== $name) {
$templateFile = str_replace(':', '/', "{$name}.html.twig");
$metadata = $this->componentFactory->metadataFor($name);
} else {
$templateFile = str_replace('components/', '', $template);
$metadata = $this->componentFactory->metadataFor(str_replace('.html.twig', '', $templateFile));
}
} else {
$templateFile = "{$reflectionClass->getShortName()}.html.twig";
$metadata = $this->componentFactory->metadataFor($reflectionClass->getShortName());
}

$componentsWithClass[] = [
'name' => $metadata->getName(),
'live' => null !== $metadata->get('live') ? 'X' : null,
];

if (($key = array_search($templateFile, $anonymousTemplatesComponents)) !== false) {
unset($anonymousTemplatesComponents[$key]);
}
}
}

$anonymousComponents = array_map(fn ($template): array => [
'name' => str_replace('/', ':', str_replace('.html.twig', '', $template)),
'live' => null,
], $anonymousTemplatesComponents);

$allComponents = array_merge($componentsWithClass, $anonymousComponents);
$dataToRender = [];
foreach ($allComponents as $component) {
$metadata = $this->componentFactory->metadataFor($component['name']);

if (str_contains($metadata->getTemplate(), $componentsDir)) {
$dataToRender[] = [
$metadata->getName(),
$metadata->get('class') ?? 'Anonymous component',
$metadata->getTemplate(),
$component['live'],
];
}
}

$table = new Table($output);
$table->setHeaders(['Component', 'Class', 'Template', 'Live'])->setRows($dataToRender);
$table->render();

return Command::SUCCESS;
}

private function getPropertiesForAnonymousComponent(ComponentMetadata $metadata): array
{
$allProperties = [];

$source = $this->twigEnvironment->load($metadata->getTemplate())->getSourceContext();
$tokenStream = $this->twigEnvironment->tokenize($source);
$bodyNode = $this->twigEnvironment->parse($tokenStream)->getNode('body')->getNode(0);

$propsNode = [];

foreach ($bodyNode as $node) {
if ($node instanceof PropsNode) {
$propsNode = $node;
break;
}
}

if (\count($propsNode) > 0) {
$allVariables = $propsNode->getAttribute('names');

foreach ($allVariables as $variable) {
if ($propsNode->hasNode($variable)) {
$value = $propsNode->getNode($variable)->getAttribute('value');

if (\is_bool($value)) {
$value = $value ? 'true' : 'false';
}

$property = $variable.' = '.$value;
} else {
$property = $variable;
}

$allProperties[] = $property;
}
}

return $allProperties;
}
}
Loading

0 comments on commit 44d5d6b

Please sign in to comment.