@@ -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+
6570To 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>
7285EOF
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