diff --git a/src/TwigComponent/config/debug.php b/src/TwigComponent/config/debug.php index 6fb748d98ef..15043abfb40 100644 --- a/src/TwigComponent/config/debug.php +++ b/src/TwigComponent/config/debug.php @@ -9,14 +9,12 @@ * file that was distributed with this source code. */ -namespace Symfony\UX\TwigComponent\DependencyInjection\Loader\Configurator; +namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\UX\TwigComponent\Command\TwigComponentDebugCommand; use Symfony\UX\TwigComponent\DataCollector\TwigComponentDataCollector; use Symfony\UX\TwigComponent\EventListener\TwigComponentLoggerListener; -use function Symfony\Component\DependencyInjection\Loader\Configurator\service; - return static function (ContainerConfigurator $container) { $container->services() @@ -35,5 +33,16 @@ 'template' => '@TwigComponent/Collector/twig_component.html.twig', 'id' => 'twig_component', 'priority' => 256, - ]); + ]) + + ->set('ux.twig_component.command.debug', TwigComponentDebugCommand::class) + ->args([ + param('twig.default_path'), + service('ux.twig_component.component_factory'), + service('twig'), + param('ux.twig_component.class_component_map'), + param('ux.twig_component.anonymous_template_directory'), + ]) + ->tag('console.command') + ; }; diff --git a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php index 7fb65b8031d..c0f55e426a6 100644 --- a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php +++ b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php @@ -28,20 +28,20 @@ use Symfony\UX\TwigComponent\Twig\PropsNode; use Twig\Environment; -#[AsCommand(name: 'debug:twig-component', description: 'Display components and them usages for an application')] +#[AsCommand(name: 'debug:twig-component', description: 'Display Twig components and their usages')] class TwigComponentDebugCommand extends Command { - private readonly string $anonymousDirectory; + /** @var array */ + private array $components; public function __construct( private string $twigTemplatesPath, private ComponentFactory $componentFactory, private Environment $twig, private readonly array $componentClassMap, - ?string $anonymousDirectory = null, + private string $anonymousDirectory = 'components', ) { parent::__construct(); - $this->anonymousDirectory = $anonymousDirectory ?? 'components'; } protected function configure(): void @@ -51,28 +51,26 @@ protected function configure(): void new InputArgument('name', InputArgument::OPTIONAL, 'A component name or part of the component name'), ]) ->setHelp( - <<<'EOF' -The %command.name% display all the Twig components in your application. + <<%command.name% display all the Twig components in your application. -To list all components: + To list all components: - php %command.full_name% + php %command.full_name% -To get specific information about a component, specify its name (or a part of it): + To get specific information about a component, specify its name (or a part of it): - php %command.full_name% Alert -EOF + php %command.full_name% Alert + EOF ); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $name = $input->getArgument('name'); - if (\is_string($name)) { - $component = $this->findComponentName($io, $name, $input->isInteractive()); - if (null === $component) { + if ($name = $input->getArgument('name')) { + if (!$component = $this->findComponentName($io, $name, $input->isInteractive())) { $io->error(sprintf('Unknown component "%s".', $name)); return Command::FAILURE; @@ -99,25 +97,31 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti private function findComponentName(SymfonyStyle $io, string $name, bool $interactive): ?string { $components = []; - foreach ($this->componentClassMap as $componentName) { + + foreach ($this->findComponents() as $componentName => $metadata) { if ($name === $componentName) { return $name; } + if (str_contains($componentName, $name)) { $components[$componentName] = $componentName; } } + foreach ($this->findAnonymousComponents() as $componentName) { if (isset($components[$componentName])) { continue; } + if ($name === $componentName) { return $name; } + if (str_contains($componentName, $name)) { $components[$componentName] = $componentName; } } + if ($interactive && \count($components)) { return $io->choice('Select one of the following component to display its information', array_values($components), 0); } @@ -130,15 +134,21 @@ private function findComponentName(SymfonyStyle $io, string $name, bool $interac */ private function findComponents(): array { - $components = []; - foreach ($this->componentClassMap as $class => $name) { - $components[$name] ??= $this->componentFactory->metadataFor($name); + if (isset($this->components)) { + return $this->components; } + + $this->components = []; + + foreach ($this->componentClassMap as $name) { + $this->components[$name] ??= $this->componentFactory->metadataFor($name); + } + foreach ($this->findAnonymousComponents() as $name => $template) { - $components[$name] ??= $this->componentFactory->metadataFor($name); + $this->components[$name] ??= $this->componentFactory->metadataFor($name); } - return $components; + return $this->components; } /** @@ -152,11 +162,14 @@ private function findAnonymousComponents(): array $anonymousPath = $this->twigTemplatesPath.'/'.$this->anonymousDirectory; $finderTemplates = new Finder(); $finderTemplates->files()->in($anonymousPath)->notPath('/_'); + foreach ($finderTemplates as $template) { $component = str_replace('/', ':', $template->getRelativePathname()); + if (str_ends_with($component, '.html.twig')) { $component = substr($component, 0, -10); } + $components[$component] = $component; } @@ -204,16 +217,21 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void return sprintf('%s(%s)', $m->getName(), implode(', ', $params)); }; + $hooks = []; + if ($method = AsTwigComponent::mountMethod($metadata->getClass())) { $hooks[] = ['Mount', $logMethod($method)]; } + foreach (AsTwigComponent::preMountMethods($metadata->getClass()) as $method) { $hooks[] = ['PreMount', $logMethod($method)]; } + foreach (AsTwigComponent::postMountMethods($metadata->getClass()) as $method) { $hooks[] = ['PostMount', $logMethod($method)]; } + if ($hooks) { $table->addRows([ new TableSeparator(), @@ -233,12 +251,17 @@ private function displayComponentsTable(SymfonyStyle $io, array $components): vo $table->setStyle('default'); $table->setHeaderTitle('Components'); $table->setHeaders(['Name', 'Class', 'Template', 'Type']); + foreach ($components as $metadata) { $table->addRow([ $metadata->getName(), $metadata->get('class') ? $metadata->getClass() : '', $metadata->getTemplate(), - $metadata->get('live') ? 'Live' : ($metadata->get('class') ? '' : 'Anon'), + match (true) { + null === $metadata->get('class') => 'Anon', + $metadata->get('live') => 'Live', + default => '', + }, ]); } $table->render(); @@ -251,16 +274,13 @@ 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; - } + $typeName = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; $value = $property->getDefaultValue(); $propertyDisplay = $typeName.' $'.$propertyName.(null !== $value ? ' = '.json_encode($value) : ''); $properties[$property->name] = $propertyDisplay; @@ -286,33 +306,32 @@ private function getAnonymousComponentProperties(ComponentMetadata $metadata): a $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); - } + $value = \is_bool($value) ? ($value ? 'true' : 'false') : json_encode($value); $properties[$propName] = $propName.' = '.$value; } } diff --git a/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php b/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php index 10440aded2f..74e164caa23 100644 --- a/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php +++ b/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php @@ -80,8 +80,9 @@ public function process(ContainerBuilder $container): void $factoryDefinition->setArgument(4, $componentConfig); $factoryDefinition->setArgument(5, $componentClassMap); - $debugCommandDefinition = $container->findDefinition('ux.twig_component.command.debug'); - $debugCommandDefinition->setArgument(3, $componentClassMap); + if ($container->hasDefinition('ux.twig_component.command.debug')) { + $container->setParameter('ux.twig_component.class_component_map', $componentClassMap); + } } private function findMatchingDefaults(string $className, array $componentDefaults): ?array diff --git a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php index f236bb72033..dac00d8a580 100644 --- a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php +++ b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php @@ -22,10 +22,8 @@ use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; -use Symfony\UX\TwigComponent\Command\TwigComponentDebugCommand; use Symfony\UX\TwigComponent\ComponentFactory; use Symfony\UX\TwigComponent\ComponentRenderer; use Symfony\UX\TwigComponent\ComponentRendererInterface; @@ -111,22 +109,13 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in % ->setDecoratedService(new Reference('twig.configurator.environment')) ->setArguments([new Reference('ux.twig_component.twig.environment_configurator.inner')]); - $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'), - class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %s.', TwigComponentPass::class)) : [], - $config['anonymous_template_directory'], - ]) - ->addTag('console.command') - ; - $container->setAlias('console.command.stimulus_component_debug', 'ux.twig_component.command.debug') ->setDeprecated('symfony/ux-twig-component', '2.13', '%alias_id%'); if ($container->getParameter('kernel.debug')) { $loader->load('debug.php'); + + $container->setParameter('ux.twig_component.anonymous_template_directory', $config['anonymous_template_directory']); } } @@ -155,7 +144,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->always(function ($v) { foreach ($v as $namespace => $defaults) { if (!str_ends_with($namespace, '\\')) { - throw new InvalidConfigurationException(sprintf('The twig_component.defaults namespace "%s" is invalid: it must end in a "\"', $namespace)); + throw new InvalidConfigurationException(sprintf('The twig_component.defaults namespace "%s" is invalid: it must end in a "\".', $namespace)); } }