Skip to content

Commit eca453f

Browse files
authored
[TwigHooks] Improve sylius:debug:twig-hooks command (#357)
## Summary Improve `sylius:debug:twig-hooks` command with tree view, multi-hooks support and extended config display. ## Features - `--tree` option to display the full hooks hierarchy as a tree - Multi-hooks support: pass multiple hook names to see the merged result (as resolved at runtime) - `--config` now displays context, configuration and props - Tree display aligned with the debug toolbar format ## Usage ```bash # Display full hooks hierarchy as a tree bin/console sylius:debug:twig-hooks sylius_admin.common.create --tree # Show context, configuration and props bin/console sylius:debug:twig-hooks sylius_admin.common.create --config # Display merged result of multiple hooks bin/console sylius:debug:twig-hooks sylius_admin.dashboard.index.content sylius_admin.common.index.content # Display merged tree of multiple hooks bin/console sylius:debug:twig-hooks sylius_admin.dashboard.index.content sylius_admin.common.index.content --tree ``` <img width="1026" height="780" alt="Capture d’écran 2026-03-24 à 20 10 36" src="https://github.com/user-attachments/assets/18832509-615e-4517-9974-e4c0431f5151" />
2 parents 3133e34 + 2822370 commit eca453f

2 files changed

Lines changed: 393 additions & 41 deletions

File tree

src/TwigHooks/src/Console/Command/DebugTwigHooksCommand.php

Lines changed: 174 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ protected function configure(): void
4242
{
4343
$this
4444
->setDefinition([
45-
new InputArgument('name', InputArgument::OPTIONAL, 'A hook name or part of the hook name'),
45+
new InputArgument('name', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'One or more hook names'),
4646
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show all hookables including disabled ones'),
47-
new InputOption('config', 'c', InputOption::VALUE_NONE, 'Show hookables configuration'),
47+
new InputOption('config', 'c', InputOption::VALUE_NONE, 'Show hookables context, configuration and props'),
48+
new InputOption('tree', 't', InputOption::VALUE_NONE, 'Display hooks as a tree'),
4849
])
4950
->setHelp(
5051
<<<'EOF'
@@ -62,13 +63,25 @@ protected function configure(): void
6263
6364
<info>php %command.full_name% sylius_admin.product.index</info>
6465
66+
To display the merged result of multiple hooks (as resolved at runtime):
67+
68+
<info>php %command.full_name% sylius_admin.dashboard.index sylius_admin.common.index</info>
69+
6570
To include disabled hookables:
6671
6772
<info>php %command.full_name% sylius_admin.product.index --all</info>
6873
69-
To show hookables configuration:
74+
To show hookables context, configuration and props:
7075
7176
<info>php %command.full_name% sylius_admin.product.index --config</info>
77+
78+
To display the full hooks hierarchy as a tree:
79+
80+
<info>php %command.full_name% sylius_admin.common.create --tree</info>
81+
82+
To display the merged tree of multiple hooks (as resolved at runtime):
83+
84+
<info>php %command.full_name% sylius_admin.dashboard.index sylius_admin.common.index --tree</info>
7285
EOF
7386
);
7487
}
@@ -83,37 +96,77 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
8396
protected function execute(InputInterface $input, OutputInterface $output): int
8497
{
8598
$io = new SymfonyStyle($input, $output);
86-
$name = $input->getArgument('name');
99+
/** @var array<string> $names */
100+
$names = array_unique((array) $input->getArgument('name'));
87101
/** @var bool $showAll */
88102
$showAll = $input->getOption('all');
89103
/** @var bool $showConfig */
90104
$showConfig = $input->getOption('config');
105+
/** @var bool $showTree */
106+
$showTree = $input->getOption('tree');
107+
108+
if ($showTree && $showConfig) {
109+
$io->note('The --config option has no effect with --tree and will be ignored.');
110+
}
111+
112+
$registeredHookNames = $this->hookablesRegistry->getHookNames();
113+
sort($registeredHookNames);
114+
115+
// Multiple hooks — direct merge
116+
if (count($names) > 1) {
117+
$unknownNames = array_diff($names, $registeredHookNames);
118+
if (0 < count($unknownNames)) {
119+
$io->warning(sprintf('Hook(s) not found: "%s".', implode('", "', $unknownNames)));
120+
121+
return Command::SUCCESS;
122+
}
123+
124+
$io->title(implode(', ', $names));
125+
if ($showTree) {
126+
$this->displayHookTree($output, $names, $showAll);
127+
} else {
128+
$this->displayHookDetails($io, $names, $showAll, $showConfig);
129+
}
130+
131+
return Command::SUCCESS;
132+
}
91133

92-
$hookNames = $this->hookablesRegistry->getHookNames();
93-
sort($hookNames);
134+
// Single hook name
135+
if (1 === count($names)) {
136+
$singleName = $names[0];
94137

95-
if (\is_string($name)) {
96-
// Exact match - show details
97-
if (\in_array($name, $hookNames, true)) {
98-
$this->displayHookDetails($io, $name, $showAll, $showConfig);
138+
// Exact match
139+
if (in_array($singleName, $registeredHookNames, true)) {
140+
$io->title($singleName);
141+
if ($showTree) {
142+
$this->displayHookTree($output, [$singleName], $showAll);
143+
} else {
144+
$this->displayHookDetails($io, [$singleName], $showAll, $showConfig);
145+
}
99146

100147
return Command::SUCCESS;
101148
}
102149

103-
// Partial match - filter and show table or details (case-insensitive)
150+
// Partial match (case-insensitive)
104151
$filteredHooks = array_filter(
105-
$hookNames,
106-
static fn (string $hookName): bool => false !== stripos($hookName, $name),
152+
$registeredHookNames,
153+
static fn (string $hookName): bool => false !== stripos($hookName, $singleName),
107154
);
108155

109-
if (0 === \count($filteredHooks)) {
110-
$io->warning(\sprintf('No hooks found matching "%s".', $name));
156+
if (0 === count($filteredHooks)) {
157+
$io->warning(sprintf('No hooks found matching "%s".', $singleName));
111158

112159
return Command::SUCCESS;
113160
}
114161

115-
if (1 === \count($filteredHooks)) {
116-
$this->displayHookDetails($io, reset($filteredHooks), $showAll, $showConfig);
162+
if (1 === count($filteredHooks)) {
163+
$firstHook = reset($filteredHooks);
164+
$io->title($firstHook);
165+
if ($showTree) {
166+
$this->displayHookTree($output, [$firstHook], $showAll);
167+
} else {
168+
$this->displayHookDetails($io, [$firstHook], $showAll, $showConfig);
169+
}
117170

118171
return Command::SUCCESS;
119172
}
@@ -123,13 +176,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int
123176
return Command::SUCCESS;
124177
}
125178

126-
if (0 === \count($hookNames)) {
179+
if (0 === count($registeredHookNames)) {
127180
$io->warning('No hooks registered.');
128181

129182
return Command::SUCCESS;
130183
}
131184

132-
$this->displayHooksTable($io, $hookNames, $showAll);
185+
$this->displayHooksTable($io, $registeredHookNames, $showAll);
133186

134187
return Command::SUCCESS;
135188
}
@@ -143,14 +196,14 @@ private function displayHooksTable(SymfonyStyle $io, array $hookNames, bool $sho
143196

144197
foreach ($hookNames as $hookName) {
145198
$hookables = $this->hookablesRegistry->getFor($hookName);
146-
$enabledCount = \count(array_filter(
199+
$enabledCount = count(array_filter(
147200
$hookables,
148201
static fn (AbstractHookable $hookable): bool => !$hookable instanceof DisabledHookable,
149202
));
150-
$disabledCount = \count($hookables) - $enabledCount;
203+
$disabledCount = count($hookables) - $enabledCount;
151204

152205
$countDisplay = $showAll && $disabledCount > 0
153-
? \sprintf('%d (%d disabled)', \count($hookables), $disabledCount)
206+
? sprintf('%d (%d disabled)', count($hookables), $disabledCount)
154207
: (string) $enabledCount;
155208

156209
$rows[] = [
@@ -160,23 +213,24 @@ private function displayHooksTable(SymfonyStyle $io, array $hookNames, bool $sho
160213
}
161214

162215
$io->table(['Hook', 'Hookables'], $rows);
163-
$io->text(\sprintf('Total: %d hooks', \count($hookNames)));
216+
$io->text(sprintf('Total: %d hooks', count($hookNames)));
164217
}
165218

166-
private function displayHookDetails(SymfonyStyle $io, string $hookName, bool $showAll, bool $showConfig): void
219+
/**
220+
* @param array<string> $hookNames
221+
*/
222+
private function displayHookDetails(SymfonyStyle $io, array $hookNames, bool $showAll, bool $showConfig): void
167223
{
168-
$io->title($hookName);
169-
170-
$hookables = $this->hookablesRegistry->getFor($hookName);
171-
if (!$showAll) {
172-
$hookables = array_filter(
173-
$hookables,
174-
static fn (AbstractHookable $hookable): bool => !$hookable instanceof DisabledHookable,
224+
$hookables = $showAll
225+
? $this->hookablesRegistry->getFor($hookNames)
226+
: $this->hookablesRegistry->getEnabledFor($hookNames);
227+
228+
if (0 === count($hookables)) {
229+
$io->warning(
230+
1 === count($hookNames)
231+
? 'No hookables registered for this hook.'
232+
: 'No hookables registered for these hooks.',
175233
);
176-
}
177-
178-
if (0 === \count($hookables)) {
179-
$io->warning('No hookables registered for this hook.');
180234

181235
return;
182236
}
@@ -186,7 +240,9 @@ private function displayHookDetails(SymfonyStyle $io, string $hookName, bool $sh
186240
$headers[] = 'Status';
187241
}
188242
if ($showConfig) {
243+
$headers[] = 'Context';
189244
$headers[] = 'Configuration';
245+
$headers[] = 'Props';
190246
}
191247

192248
$rows = [];
@@ -203,7 +259,11 @@ private function displayHookDetails(SymfonyStyle $io, string $hookName, bool $sh
203259
}
204260

205261
if ($showConfig) {
262+
$row[] = $this->formatConfiguration($hookable->context);
206263
$row[] = $this->formatConfiguration($hookable->configuration);
264+
$row[] = $hookable instanceof HookableComponent
265+
? $this->formatConfiguration($hookable->props)
266+
: '-';
207267
}
208268

209269
$rows[] = $row;
@@ -212,12 +272,88 @@ private function displayHookDetails(SymfonyStyle $io, string $hookName, bool $sh
212272
$io->table($headers, $rows);
213273
}
214274

275+
/**
276+
* @param array<string> $hookNames
277+
*/
278+
private function displayHookTree(OutputInterface $output, array $hookNames, bool $showAll, string $prefix = ''): void
279+
{
280+
$hookables = $showAll
281+
? $this->hookablesRegistry->getFor($hookNames)
282+
: $this->hookablesRegistry->getEnabledFor($hookNames);
283+
284+
$childGroups = $this->getDirectChildHookGroups($hookNames);
285+
$hookablesList = array_values($hookables);
286+
$lastHookableIndex = count($hookablesList) - 1;
287+
288+
foreach ($hookablesList as $index => $hookable) {
289+
$isLast = $index === $lastHookableIndex && 0 === count($childGroups);
290+
$connector = $isLast ? '└── ' : '├── ';
291+
292+
$output->writeln($prefix . $connector . $this->formatHookableLine($hookable));
293+
}
294+
295+
$childGroupsList = array_values($childGroups);
296+
foreach ($childGroupsList as $index => $childHookNames) {
297+
$isLast = $index === count($childGroupsList) - 1;
298+
$connector = $isLast ? '└── ' : '├── ';
299+
$childPrefix = $prefix . ($isLast ? ' ' : '');
300+
301+
$output->writeln(sprintf('%s%s<fg=cyan>(Hook)</> %s', $prefix, $connector, implode(', ', $childHookNames)));
302+
$this->displayHookTree($output, $childHookNames, $showAll, $childPrefix);
303+
}
304+
}
305+
306+
private function formatHookableLine(AbstractHookable $hookable): string
307+
{
308+
$type = $this->getHookableType($hookable);
309+
$target = $this->getHookableTarget($hookable);
310+
$status = $hookable instanceof DisabledHookable ? ' <comment>[disabled]</comment>' : '';
311+
312+
$coloredType = $hookable instanceof HookableComponent
313+
? sprintf('<fg=yellow>(%s)</>', $type)
314+
: sprintf('<fg=green>(%s)</>', $type);
315+
316+
return sprintf('%s [↑ %d] %s (%s)%s', $coloredType, $hookable->priority(), $hookable->name, $target, $status);
317+
}
318+
319+
/**
320+
* @param array<string> $hookNames
321+
*
322+
* @return array<string, array<string>>
323+
*/
324+
private function getDirectChildHookGroups(array $hookNames): array
325+
{
326+
$groups = [];
327+
$allHookNames = $this->hookablesRegistry->getHookNames();
328+
329+
foreach ($hookNames as $hookName) {
330+
foreach ($allHookNames as $registeredName) {
331+
foreach (['.', '#'] as $separator) {
332+
if (!str_starts_with($registeredName, $hookName . $separator)) {
333+
continue;
334+
}
335+
336+
$rest = substr($registeredName, strlen($hookName) + 1);
337+
if (str_contains($rest, '.') || str_contains($rest, '#')) {
338+
continue;
339+
}
340+
341+
$groups[$separator . $rest][] = $registeredName;
342+
}
343+
}
344+
}
345+
346+
ksort($groups);
347+
348+
return $groups;
349+
}
350+
215351
/**
216352
* @param array<string, mixed> $configuration
217353
*/
218354
private function formatConfiguration(array $configuration): string
219355
{
220-
if (0 === \count($configuration)) {
356+
if (0 === count($configuration)) {
221357
return '-';
222358
}
223359

@@ -227,8 +363,8 @@ private function formatConfiguration(array $configuration): string
227363
private function getHookableType(AbstractHookable $hookable): string
228364
{
229365
return match (true) {
230-
$hookable instanceof HookableTemplate => 'template',
231-
$hookable instanceof HookableComponent => 'component',
366+
$hookable instanceof HookableTemplate => 'Template',
367+
$hookable instanceof HookableComponent => 'Component',
232368
default => '-',
233369
};
234370
}

0 commit comments

Comments
 (0)