Chi
4 years ago
3 changed files with 223 additions and 0 deletions
@ -0,0 +1,217 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak\Command; |
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command; |
||||||
|
use Symfony\Component\Console\Formatter\OutputFormatter; |
||||||
|
use Symfony\Component\Console\Input\InputInterface; |
||||||
|
use Symfony\Component\Console\Input\InputOption; |
||||||
|
use Symfony\Component\Console\Output\OutputInterface; |
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle; |
||||||
|
use Twig\Environment; |
||||||
|
use Twig\Loader\ChainLoader; |
||||||
|
use Twig\Loader\FilesystemLoader; |
||||||
|
|
||||||
|
/** |
||||||
|
* Lists twig functions, filters, and tests present in the current project. |
||||||
|
* |
||||||
|
* This is a simplified version of Symfony's Debug command. |
||||||
|
* |
||||||
|
* @see https://github.com/symfony/symfony/blob/5.x/src/Symfony/Bridge/Twig/Command/DebugCommand.php |
||||||
|
*/ |
||||||
|
final class DebugCommand extends Command { |
||||||
|
|
||||||
|
/** |
||||||
|
* Twig environment. |
||||||
|
* |
||||||
|
* @var \Twig\Environment |
||||||
|
*/ |
||||||
|
private $twig; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function __construct(Environment $twig) { |
||||||
|
parent::__construct(); |
||||||
|
$this->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 <info>%command.name%</info> command outputs a list of twig functions, |
||||||
|
filters, globals and tests. |
||||||
|
|
||||||
|
<info>drush %command.name%</info> |
||||||
|
|
||||||
|
The command lists all functions, filters, etc. |
||||||
|
|
||||||
|
<info>drush %command.name% --filter=date</info> |
||||||
|
|
||||||
|
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(' <error>%s</error>', 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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue