Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
27 changes: 27 additions & 0 deletions src/TwigHooks/config/services/command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Sylius\TwigHooks\Command\DebugTwigHooksCommand;

return static function (ContainerConfigurator $configurator): void {
$services = $configurator->services();

$services->set('sylius_twig_hooks.command.debug', DebugTwigHooksCommand::class)
->args([
service('sylius_twig_hooks.registry.hookables'),
])
->tag('console.command')
;
};
269 changes: 269 additions & 0 deletions src/TwigHooks/src/Command/DebugTwigHooksCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\TwigHooks\Command;

use Sylius\TwigHooks\Hookable\AbstractHookable;
use Sylius\TwigHooks\Hookable\DisabledHookable;
use Sylius\TwigHooks\Hookable\HookableComponent;
use Sylius\TwigHooks\Hookable\HookableTemplate;
use Sylius\TwigHooks\Registry\HookablesRegistry;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
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;

#[AsCommand(name: 'sylius:debug:twig-hooks', description: 'Display hooks and their hookables')]
final class DebugTwigHooksCommand extends Command
{
public function __construct(
private readonly HookablesRegistry $hookablesRegistry,
) {
parent::__construct();
}

protected function configure(): void
{
$this
->setDefinition([
new InputArgument('name', InputArgument::OPTIONAL, 'A hook name or part of the hook name'),
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show all hookables including disabled ones'),
new InputOption('config', 'c', InputOption::VALUE_NONE, 'Show hookables configuration'),
])
->setHelp(
<<<'EOF'
The <info>%command.name%</info> displays all Twig hooks in your application.

To list all hooks:

<info>php %command.full_name%</info>

To filter hooks by name:

<info>php %command.full_name% sylius_admin</info>

To get specific information about a hook:

<info>php %command.full_name% sylius_admin.product.index</info>

To include disabled hookables:

<info>php %command.full_name% sylius_admin.product.index --all</info>

To show hookables configuration:

<info>php %command.full_name% sylius_admin.product.index --config</info>
EOF
);
}

public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('name')) {
$suggestions->suggestValues($this->hookablesRegistry->getHookNames());
}
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$name = $input->getArgument('name');
/** @var bool $showAll */
$showAll = $input->getOption('all');
/** @var bool $showConfig */
$showConfig = $input->getOption('config');

$hookNames = $this->hookablesRegistry->getHookNames();
sort($hookNames);

if (\is_string($name)) {
// Exact match - show details
if (\in_array($name, $hookNames, true)) {
$this->displayHookDetails($io, $name, $showAll, $showConfig);

return Command::SUCCESS;
}

// Partial match - filter and show table or details (case-insensitive)
$filteredHooks = array_filter(
$hookNames,
static fn (string $hookName): bool => false !== stripos($hookName, $name),
);

if (0 === \count($filteredHooks)) {
$io->warning(\sprintf('No hooks found matching "%s".', $name));

return Command::SUCCESS;
}

if (1 === \count($filteredHooks)) {
$this->displayHookDetails($io, reset($filteredHooks), $showAll, $showConfig);

return Command::SUCCESS;
}

$this->displayHooksTable($io, $filteredHooks, $showAll);

return Command::SUCCESS;
}

if (0 === \count($hookNames)) {
$io->warning('No hooks registered.');

return Command::SUCCESS;
}

$this->displayHooksTable($io, $hookNames, $showAll);

return Command::SUCCESS;
}

/**
* @param array<string> $hookNames
*/
private function displayHooksTable(SymfonyStyle $io, array $hookNames, bool $showAll): void
{
$rows = [];

foreach ($hookNames as $hookName) {
$hookables = $this->hookablesRegistry->getAllFor($hookName);
$enabledCount = \count(array_filter(
$hookables,
static fn (AbstractHookable $h): bool => !$h instanceof DisabledHookable,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
static fn (AbstractHookable $h): bool => !$h instanceof DisabledHookable,
static fn (AbstractHookable $hookable): bool => !$hookable instanceof DisabledHookable,

));
$disabledCount = \count($hookables) - $enabledCount;

$countDisplay = $showAll && $disabledCount > 0
? \sprintf('%d (%d disabled)', \count($hookables), $disabledCount)
: (string) $enabledCount;

$rows[] = [
$hookName,
$countDisplay,
];
}

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

private function displayHookDetails(SymfonyStyle $io, string $hookName, bool $showAll, bool $showConfig): void
{
$io->title($hookName);

$hookables = $this->hookablesRegistry->getAllFor($hookName);
if (!$showAll) {
$hookables = array_filter(
$hookables,
static fn (AbstractHookable $h): bool => !$h instanceof DisabledHookable,
);
}

if (0 === \count($hookables)) {
$io->warning('No hookables registered for this hook.');

return;
}

$headers = ['Name', 'Type', 'Target', 'Priority'];
if ($showAll) {
$headers[] = 'Status';
}
if ($showConfig) {
$headers[] = 'Configuration';
}

$rows = [];
foreach ($hookables as $hookable) {
$row = [
$hookable->name,
$this->getHookableType($hookable),
$this->getHookableTarget($hookable),
$hookable->priority(),
];

if ($showAll) {
$row[] = $hookable instanceof DisabledHookable ? 'disabled' : 'enabled';
}

if ($showConfig) {
$row[] = $this->formatConfiguration($hookable->configuration);
}

$rows[] = $row;
}

$io->table($headers, $rows);
}

/**
* @param array<string, mixed> $configuration
*/
private function formatConfiguration(array $configuration): string
{
if (0 === \count($configuration)) {
return '-';
}

$parts = [];
foreach ($configuration as $key => $value) {
Copy link
Member

Choose a reason for hiding this comment

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

Could we use VarDumper on the $configuration var itself?

$parts[] = \sprintf('%s: %s', $key, $this->formatValue($value));
}

return implode("\n", $parts);
}

private function formatValue(mixed $value): string
{
if (\is_array($value)) {
if (!array_is_list($value)) {
return '{...}';
}

return '[' . implode(', ', array_map($this->formatValue(...), $value)) . ']';
Copy link
Member

Choose a reason for hiding this comment

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

I think you can use the VarDumper from Symfony instead.

Usage example I use in a future package:

private function formatValue(mixed $value): string
{
    if (is_string($value) && str_ends_with($value, '::class')) {
        $stringValue = $value;
    } else {
        $stringValue = VarExporter::export($value);
    }

    $indentedValue = preg_replace(
        '/^/m',
        str_repeat('    ', $this->indentLevel), // 4 spaces per indent level
        $stringValue,
    );

    return ltrim($indentedValue);
}

}

if (\is_bool($value)) {
return $value ? 'true' : 'false';
}

if (null === $value) {
return 'null';
}

return (string) $value;
}

private function getHookableType(AbstractHookable $hookable): string
{
return match (true) {
$hookable instanceof HookableTemplate => 'template',
$hookable instanceof HookableComponent => 'component',
default => '-',
};
}

private function getHookableTarget(AbstractHookable $hookable): string
{
return match (true) {
$hookable instanceof HookableTemplate => $hookable->template,
$hookable instanceof HookableComponent => $hookable->component,
default => '-',
};
}
}
26 changes: 26 additions & 0 deletions src/TwigHooks/src/Registry/HookablesRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ public function __construct(
}
}

/**
* @return array<string>
*/
public function getHookNames(): array
{
return array_keys($this->hookables);
}

/**
* @param string|array<string> $hooksNames
*
Expand All @@ -66,6 +74,24 @@ public function getEnabledFor(string|array $hooksNames): array
return $priorityQueue->toArray();
}

/**
* @param string|array<string> $hooksNames
*
* @return array<AbstractHookable>
*/
public function getAllFor(string|array $hooksNames): array
{
$hooksNames = is_string($hooksNames) ? [$hooksNames] : $hooksNames;
$hookables = $this->mergeHookables($hooksNames);

$priorityQueue = new SplPriorityQueue();
foreach ($hookables as $hookable) {
$priorityQueue->insert($hookable, $hookable->priority());
}

return $priorityQueue->toArray();
}

/**
* @param array<string> $hooksNames
*
Expand Down