From f3576306f345ef2f91e011625846b759338fbb89 Mon Sep 17 00:00:00 2001 From: Chi Date: Sun, 13 Dec 2020 14:04:20 +0000 Subject: [PATCH] Add twig-debug command --- composer.json | 1 + drush.services.yml | 5 + src/Command/DebugCommand.php | 217 +++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 src/Command/DebugCommand.php diff --git a/composer.json b/composer.json index b5887b4..53bf28d 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ }, "require": { "php": ">=7.3", + "ext-json": "*", "drupal/core": "^9.0", "twig/twig": "^2.12" }, diff --git a/drush.services.yml b/drush.services.yml index 6cfc0aa..9521b80 100644 --- a/drush.services.yml +++ b/drush.services.yml @@ -4,3 +4,8 @@ services: arguments: ['@twig'] tags: - { name: console.command } + twig_tweak.debug: + class: Drupal\twig_tweak\Command\DebugCommand + arguments: ['@twig'] + tags: + - { name: console.command } diff --git a/src/Command/DebugCommand.php b/src/Command/DebugCommand.php new file mode 100644 index 0000000..1206bfe --- /dev/null +++ b/src/Command/DebugCommand.php @@ -0,0 +1,217 @@ +twig = $twig; + } + + /** + * {@inheritdoc} + */ + protected function configure(): void { + $this + ->setName('twig-tweak:debug') + ->setAliases(['twig-debug']) + ->addOption('filter', NULL, InputOption::VALUE_REQUIRED, 'Show details for all entries matching this filter.') + ->setDescription('Shows a list of twig functions, filters, globals and tests') + ->setHelp(<<<'EOF' + The %command.name% command outputs a list of twig functions, + filters, globals and tests. + + drush %command.name% + + The command lists all functions, filters, etc. + + drush %command.name% --filter=date + + The command lists everything that contains the word date. + EOF); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $io = new SymfonyStyle($input, $output); + $filter = $input->getOption('filter'); + + $types = ['functions', 'filters', 'tests']; + foreach ($types as $type) { + $items = []; + foreach ($this->twig->{'get' . ucfirst($type)}() as $name => $entity) { + if (!$filter || \strpos($name, $filter) !== FALSE) { + + $signature = ''; + // Tests are typically implemented as Twig nodes so that it is hard + // to get their signatures through reflection. + if ($type == 'filters' || $type == 'functions') { + try { + $meta = $this->getMetadata($type, $entity); + $default_signature = $type == 'functions' ? '()' : ''; + $signature = $meta ? '(' . implode(', ', $meta) . ')' : $default_signature; + } + catch (\UnexpectedValueException $exception) { + $signature = sprintf(' %s', OutputFormatter::escape($exception->getMessage())); + } + } + + $items[$name] = $name . $signature; + } + } + + if (!$items) { + continue; + } + + $io->section(\ucfirst($type)); + + ksort($items); + $io->listing($items); + } + + if (!$filter && $loaderPaths = $this->getLoaderPaths()) { + $io->section('Loader Paths'); + $rows = []; + foreach ($loaderPaths as $namespace => $paths) { + foreach ($paths as $path) { + $rows[] = [$namespace, $path . \DIRECTORY_SEPARATOR]; + } + } + $io->table(['Namespace', 'Paths'], $rows); + + } + + return 0; + } + + /** + * Gets loader paths. + */ + private function getLoaderPaths(): array { + $loaderPaths = []; + foreach ($this->getFilesystemLoaders() as $loader) { + foreach ($loader->getNamespaces() as $namespace) { + $paths = $loader->getPaths($namespace); + $namespace = FilesystemLoader::MAIN_NAMESPACE === $namespace ? + '(None)' : '@' . $namespace; + $loaderPaths[$namespace] = array_merge($loaderPaths[$namespace] ?? [], $paths); + } + } + return $loaderPaths; + } + + /** + * Gets metadata. + */ + private function getMetadata(string $type, $entity): ?array { + + $callable = $entity->getCallable(); + + if (!$callable) { + return NULL; + } + if (\is_array($callable)) { + if (!method_exists($callable[0], $callable[1])) { + return NULL; + } + $reflection = new \ReflectionMethod($callable[0], $callable[1]); + } + elseif (\is_object($callable) && method_exists($callable, '__invoke')) { + $reflection = new \ReflectionMethod($callable, '__invoke'); + } + elseif (\function_exists($callable)) { + $reflection = new \ReflectionFunction($callable); + } + elseif (\is_string($callable) && preg_match('{^(.+)::(.+)$}', $callable, $m) && method_exists($m[1], $m[2])) { + $reflection = new \ReflectionMethod($m[1], $m[2]); + } + else { + throw new \UnexpectedValueException('Unsupported callback type.'); + } + + $args = $reflection->getParameters(); + + // Filter out context/environment args. + if ($entity->needsEnvironment()) { + array_shift($args); + } + if ($entity->needsContext()) { + array_shift($args); + } + + if ($type == 'filters') { + // Remove the value the filter is applied on. + array_shift($args); + } + + // Format args. + $args = array_map( + static function (\ReflectionParameter $param): string { + $arg = $param->getName(); + if ($param->isDefaultValueAvailable()) { + $arg .= ' = ' . \json_encode($param->getDefaultValue()); + } + return $arg; + }, + $args, + ); + + return $args; + } + + /** + * Returns files system loaders. + * + * @return \Twig\Loader\FilesystemLoader[] + * File system loaders. + */ + private function getFilesystemLoaders(): array { + $loaders = []; + + $loader = $this->twig->getLoader(); + if ($loader instanceof FilesystemLoader) { + $loaders[] = $loader; + } + elseif ($loader instanceof ChainLoader) { + foreach ($loader->getLoaders() as $chained_loaders) { + if ($chained_loaders instanceof FilesystemLoader) { + $loaders[] = $chained_loaders; + } + } + } + + return $loaders; + } + +}