Skip to content

Add debug:component command #1088

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

Merged
merged 1 commit into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it the right place to add this command? Maybe in the StimulusBundle?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doesn't know either and it's the reason why i putted here

"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 <fabien@symfony.com>
*
* 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it can be useful to display:

  • properties with the ExposeInTemplate attribute, and if the name is set show the name.
  • if the component has a mount, premount, and postmount hooks

And for LiveConponents:

  • as @kbond said show if the components is live
  • show live props
  • show live actions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @WebMamba and @kbond, an update with Events and LiveAction, it could be look like this for a Live component:

 $ php bin/console debug:component RandomNumber
+--------------------+-----------------------------------+
| Property           | Value                             |
+--------------------+-----------------------------------+
| Component          | RandomNumber                      |
| Type               | AsLiveComponent                   |
| Class              | App\Components\RandomNumber       |
| Template           | components/RandomNumber.html.twig |
| Properties         | string $name = toto               |
|                    | string $type = test               |
| Live Properties    | int $max = 1000                   |
|                    | int $min = 10                     |
| Events             | Mount                             |
|                    | PreMount                          |
| LiveAction Methods | resetMax                          |
+--------------------+-----------------------------------+

And for a TwigComponent

$ php bin/console debug:component Coucou
+------------+-----------------------------+
| Property   | Value                       |
+------------+-----------------------------+
| Component  | Coucou                      |
| Type       | AsTwigComponent             |
| Class      | App\Components\Alert        |
| Template   | components/Coucou.html.twig |
| Properties | string $type = success      |
|            | string $message             |
| Events     | Mount                       |
|            | PreMount                    |
+------------+-----------------------------+

But for the #[ExposeInTemplate] what could I render for you?
The name of the property in the class or the one one displayed in the template? And with the getter or not?

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