Skip to content

Commit 77e5808

Browse files
authored
[TwigHooks] Add debug command to display hooks and hookables (#326)
## Summary Add `sylius:debug:twig-hooks` console command to list and inspect Twig hooks configuration. ### Features - List all hooks with hookable count - Filter hooks by name (case-insensitive) - Show hook details with hookables (name, type, target, priority) - `--all` option to include disabled hookables with status column - `--config` option to display hookable configuration - Shell autocompletion for hook names ### Usage ```bash # List all hooks bin/console sylius:debug:twig-hooks # Filter by name bin/console sylius:debug:twig-hooks sylius_admin # Show details for a specific hook bin/console sylius:debug:twig-hooks sylius_admin.common.component.sidebar # Include disabled hookables bin/console sylius:debug:twig-hooks sylius_admin.common.component.sidebar --all # Show configuration bin/console sylius:debug:twig-hooks sylius_admin.common.component.sidebar --config ``` ## Changes - Add `DebugTwigHooksCommand` class - Add `getHookNames()` and `getAllFor()` methods to `HookablesRegistry` - Register command as service
2 parents 8c59e56 + 7ed2b46 commit 77e5808

4 files changed

Lines changed: 603 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
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+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
15+
16+
use Sylius\TwigHooks\Console\Command\DebugTwigHooksCommand;
17+
18+
return static function (ContainerConfigurator $configurator): void {
19+
$services = $configurator->services();
20+
21+
$services->set('sylius_twig_hooks.command.debug', DebugTwigHooksCommand::class)
22+
->args([
23+
service('sylius_twig_hooks.registry.hookables'),
24+
])
25+
->tag('console.command')
26+
;
27+
};
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
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+
declare(strict_types=1);
13+
14+
namespace Sylius\TwigHooks\Console\Command;
15+
16+
use Sylius\TwigHooks\Hookable\AbstractHookable;
17+
use Sylius\TwigHooks\Hookable\DisabledHookable;
18+
use Sylius\TwigHooks\Hookable\HookableComponent;
19+
use Sylius\TwigHooks\Hookable\HookableTemplate;
20+
use Sylius\TwigHooks\Registry\HookablesRegistry;
21+
use Symfony\Component\Console\Attribute\AsCommand;
22+
use Symfony\Component\Console\Command\Command;
23+
use Symfony\Component\Console\Completion\CompletionInput;
24+
use Symfony\Component\Console\Completion\CompletionSuggestions;
25+
use Symfony\Component\Console\Input\InputArgument;
26+
use Symfony\Component\Console\Input\InputInterface;
27+
use Symfony\Component\Console\Input\InputOption;
28+
use Symfony\Component\Console\Output\OutputInterface;
29+
use Symfony\Component\Console\Style\SymfonyStyle;
30+
use Symfony\Component\VarExporter\VarExporter;
31+
32+
#[AsCommand(name: 'sylius:debug:twig-hooks', description: 'Debug twig hooks configuration.')]
33+
final class DebugTwigHooksCommand extends Command
34+
{
35+
public function __construct(
36+
private readonly HookablesRegistry $hookablesRegistry,
37+
) {
38+
parent::__construct();
39+
}
40+
41+
protected function configure(): void
42+
{
43+
$this
44+
->setDefinition([
45+
new InputArgument('name', InputArgument::OPTIONAL, 'A hook name or part of the hook name'),
46+
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show all hookables including disabled ones'),
47+
new InputOption('config', 'c', InputOption::VALUE_NONE, 'Show hookables configuration'),
48+
])
49+
->setHelp(
50+
<<<'EOF'
51+
The <info>%command.name%</info> displays all Twig hooks in your application.
52+
53+
To list all hooks:
54+
55+
<info>php %command.full_name%</info>
56+
57+
To filter hooks by name:
58+
59+
<info>php %command.full_name% sylius_admin</info>
60+
61+
To get specific information about a hook:
62+
63+
<info>php %command.full_name% sylius_admin.product.index</info>
64+
65+
To include disabled hookables:
66+
67+
<info>php %command.full_name% sylius_admin.product.index --all</info>
68+
69+
To show hookables configuration:
70+
71+
<info>php %command.full_name% sylius_admin.product.index --config</info>
72+
EOF
73+
);
74+
}
75+
76+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
77+
{
78+
if ($input->mustSuggestArgumentValuesFor('name')) {
79+
$suggestions->suggestValues($this->hookablesRegistry->getHookNames());
80+
}
81+
}
82+
83+
protected function execute(InputInterface $input, OutputInterface $output): int
84+
{
85+
$io = new SymfonyStyle($input, $output);
86+
$name = $input->getArgument('name');
87+
/** @var bool $showAll */
88+
$showAll = $input->getOption('all');
89+
/** @var bool $showConfig */
90+
$showConfig = $input->getOption('config');
91+
92+
$hookNames = $this->hookablesRegistry->getHookNames();
93+
sort($hookNames);
94+
95+
if (\is_string($name)) {
96+
// Exact match - show details
97+
if (\in_array($name, $hookNames, true)) {
98+
$this->displayHookDetails($io, $name, $showAll, $showConfig);
99+
100+
return Command::SUCCESS;
101+
}
102+
103+
// Partial match - filter and show table or details (case-insensitive)
104+
$filteredHooks = array_filter(
105+
$hookNames,
106+
static fn (string $hookName): bool => false !== stripos($hookName, $name),
107+
);
108+
109+
if (0 === \count($filteredHooks)) {
110+
$io->warning(\sprintf('No hooks found matching "%s".', $name));
111+
112+
return Command::SUCCESS;
113+
}
114+
115+
if (1 === \count($filteredHooks)) {
116+
$this->displayHookDetails($io, reset($filteredHooks), $showAll, $showConfig);
117+
118+
return Command::SUCCESS;
119+
}
120+
121+
$this->displayHooksTable($io, $filteredHooks, $showAll);
122+
123+
return Command::SUCCESS;
124+
}
125+
126+
if (0 === \count($hookNames)) {
127+
$io->warning('No hooks registered.');
128+
129+
return Command::SUCCESS;
130+
}
131+
132+
$this->displayHooksTable($io, $hookNames, $showAll);
133+
134+
return Command::SUCCESS;
135+
}
136+
137+
/**
138+
* @param array<string> $hookNames
139+
*/
140+
private function displayHooksTable(SymfonyStyle $io, array $hookNames, bool $showAll): void
141+
{
142+
$rows = [];
143+
144+
foreach ($hookNames as $hookName) {
145+
$hookables = $this->hookablesRegistry->getFor($hookName);
146+
$enabledCount = \count(array_filter(
147+
$hookables,
148+
static fn (AbstractHookable $hookable): bool => !$hookable instanceof DisabledHookable,
149+
));
150+
$disabledCount = \count($hookables) - $enabledCount;
151+
152+
$countDisplay = $showAll && $disabledCount > 0
153+
? \sprintf('%d (%d disabled)', \count($hookables), $disabledCount)
154+
: (string) $enabledCount;
155+
156+
$rows[] = [
157+
$hookName,
158+
$countDisplay,
159+
];
160+
}
161+
162+
$io->table(['Hook', 'Hookables'], $rows);
163+
$io->text(\sprintf('Total: %d hooks', \count($hookNames)));
164+
}
165+
166+
private function displayHookDetails(SymfonyStyle $io, string $hookName, bool $showAll, bool $showConfig): void
167+
{
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,
175+
);
176+
}
177+
178+
if (0 === \count($hookables)) {
179+
$io->warning('No hookables registered for this hook.');
180+
181+
return;
182+
}
183+
184+
$headers = ['Name', 'Type', 'Target', 'Priority'];
185+
if ($showAll) {
186+
$headers[] = 'Status';
187+
}
188+
if ($showConfig) {
189+
$headers[] = 'Configuration';
190+
}
191+
192+
$rows = [];
193+
foreach ($hookables as $hookable) {
194+
$row = [
195+
$hookable->name,
196+
$this->getHookableType($hookable),
197+
$this->getHookableTarget($hookable),
198+
$hookable->priority(),
199+
];
200+
201+
if ($showAll) {
202+
$row[] = $hookable instanceof DisabledHookable ? 'disabled' : 'enabled';
203+
}
204+
205+
if ($showConfig) {
206+
$row[] = $this->formatConfiguration($hookable->configuration);
207+
}
208+
209+
$rows[] = $row;
210+
}
211+
212+
$io->table($headers, $rows);
213+
}
214+
215+
/**
216+
* @param array<string, mixed> $configuration
217+
*/
218+
private function formatConfiguration(array $configuration): string
219+
{
220+
if (0 === \count($configuration)) {
221+
return '-';
222+
}
223+
224+
return VarExporter::export($configuration);
225+
}
226+
227+
private function getHookableType(AbstractHookable $hookable): string
228+
{
229+
return match (true) {
230+
$hookable instanceof HookableTemplate => 'template',
231+
$hookable instanceof HookableComponent => 'component',
232+
default => '-',
233+
};
234+
}
235+
236+
private function getHookableTarget(AbstractHookable $hookable): string
237+
{
238+
return match (true) {
239+
$hookable instanceof HookableTemplate => $hookable->template,
240+
$hookable instanceof HookableComponent => $hookable->component,
241+
default => '-',
242+
};
243+
}
244+
}

src/TwigHooks/src/Registry/HookablesRegistry.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ public function __construct(
4343
}
4444
}
4545

46+
/**
47+
* @return array<string>
48+
*/
49+
public function getHookNames(): array
50+
{
51+
return array_keys($this->hookables);
52+
}
53+
4654
/**
4755
* @param string|array<string> $hooksNames
4856
*
@@ -66,6 +74,24 @@ public function getEnabledFor(string|array $hooksNames): array
6674
return $priorityQueue->toArray();
6775
}
6876

77+
/**
78+
* @param string|array<string> $hooksNames
79+
*
80+
* @return array<AbstractHookable>
81+
*/
82+
public function getFor(string|array $hooksNames): array
83+
{
84+
$hooksNames = is_string($hooksNames) ? [$hooksNames] : $hooksNames;
85+
$hookables = $this->mergeHookables($hooksNames);
86+
87+
$priorityQueue = new SplPriorityQueue();
88+
foreach ($hookables as $hookable) {
89+
$priorityQueue->insert($hookable, $hookable->priority());
90+
}
91+
92+
return $priorityQueue->toArray();
93+
}
94+
6995
/**
7096
* @param array<string> $hooksNames
7197
*

0 commit comments

Comments
 (0)