* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bridge\Twig\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; 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; use Twig\Environment; use Twig\Loader\FilesystemLoader; /** * Lists twig functions, filters, globals and tests present in the current project. * * @author Jordi Boggiano */ class DebugCommand extends Command { protected static $defaultName = 'debug:twig'; private $twig; private $projectDir; /** * @param Environment $twig * @param string|null $projectDir */ public function __construct($twig = null, $projectDir = null) { if (!$twig instanceof Environment) { @trigger_error(sprintf('Passing a command name as the first argument of "%s()" is deprecated since Symfony 3.4 and support for it will be removed in 4.0. If the command was registered by convention, make it a service instead.', __METHOD__), \E_USER_DEPRECATED); parent::__construct($twig); return; } parent::__construct(); $this->twig = $twig; $this->projectDir = $projectDir; } public function setTwigEnvironment(Environment $twig) { @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 3.4 and will be removed in 4.0.', __METHOD__), \E_USER_DEPRECATED); $this->twig = $twig; } /** * @return Environment $twig */ protected function getTwigEnvironment() { @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 3.4 and will be removed in 4.0.', __METHOD__), \E_USER_DEPRECATED); return $this->twig; } protected function configure() { $this ->setDefinition([ new InputArgument('filter', InputArgument::OPTIONAL, 'Show details for all entries matching this filter'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (text or json)', 'text'), ]) ->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. Output can be filtered with an optional argument. php %command.full_name% The command lists all functions, filters, etc. php %command.full_name% date The command lists everything that contains the word date. php %command.full_name% --format=json The command lists everything in a machine readable json format. EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $decorated = $io->isDecorated(); // BC to be removed in 4.0 if (__CLASS__ !== static::class) { $r = new \ReflectionMethod($this, 'getTwigEnvironment'); if (__CLASS__ !== $r->getDeclaringClass()->getName()) { @trigger_error(sprintf('Usage of method "%s" is deprecated since Symfony 3.4 and will no longer be supported in 4.0. Construct the command with its required arguments instead.', static::class.'::getTwigEnvironment'), \E_USER_DEPRECATED); $this->twig = $this->getTwigEnvironment(); } } if (null === $this->twig) { throw new \RuntimeException('The Twig environment needs to be set.'); } $filter = $input->getArgument('filter'); $types = ['functions', 'filters', 'tests', 'globals']; if ('json' === $input->getOption('format')) { $data = []; foreach ($types as $type) { foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) { if (!$filter || false !== strpos($name, $filter)) { $data[$type][$name] = $this->getMetadata($type, $entity); } } } if (isset($data['tests'])) { $data['tests'] = array_keys($data['tests']); } $data['loader_paths'] = $this->getLoaderPaths(); $data = json_encode($data, \JSON_PRETTY_PRINT); $io->writeln($decorated ? OutputFormatter::escape($data) : $data); return 0; } foreach ($types as $index => $type) { $items = []; foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) { if (!$filter || false !== strpos($name, $filter)) { $items[$name] = $name.$this->getPrettyMetadata($type, $entity, $decorated); } } if (!$items) { continue; } $io->section(ucfirst($type)); ksort($items); $io->listing($items); } $rows = []; $firstNamespace = true; $prevHasSeparator = false; foreach ($this->getLoaderPaths() as $namespace => $paths) { if (!$firstNamespace && !$prevHasSeparator && \count($paths) > 1) { $rows[] = ['', '']; } $firstNamespace = false; foreach ($paths as $path) { $rows[] = [$namespace, $path.\DIRECTORY_SEPARATOR]; $namespace = ''; } if (\count($paths) > 1) { $rows[] = ['', '']; $prevHasSeparator = true; } else { $prevHasSeparator = false; } } if ($prevHasSeparator) { array_pop($rows); } $io->section('Loader Paths'); $io->table(['Namespace', 'Paths'], $rows); return 0; } private function getLoaderPaths() { if (!($loader = $this->twig->getLoader()) instanceof FilesystemLoader) { return []; } $loaderPaths = []; foreach ($loader->getNamespaces() as $namespace) { $paths = array_map(function ($path) { if (null !== $this->projectDir && 0 === strpos($path, $this->projectDir)) { $path = ltrim(substr($path, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); } return $path; }, $loader->getPaths($namespace)); if (FilesystemLoader::MAIN_NAMESPACE === $namespace) { $namespace = '(None)'; } else { $namespace = '@'.$namespace; } $loaderPaths[$namespace] = $paths; } return $loaderPaths; } private function getMetadata($type, $entity) { if ('globals' === $type) { return $entity; } if ('tests' === $type) { return null; } if ('functions' === $type || 'filters' === $type) { $cb = $entity->getCallable(); if (null === $cb) { return null; } if (\is_array($cb)) { if (!method_exists($cb[0], $cb[1])) { return null; } $refl = new \ReflectionMethod($cb[0], $cb[1]); } elseif (\is_object($cb) && method_exists($cb, '__invoke')) { $refl = new \ReflectionMethod($cb, '__invoke'); } elseif (\function_exists($cb)) { $refl = new \ReflectionFunction($cb); } elseif (\is_string($cb) && preg_match('{^(.+)::(.+)$}', $cb, $m) && method_exists($m[1], $m[2])) { $refl = new \ReflectionMethod($m[1], $m[2]); } else { throw new \UnexpectedValueException('Unsupported callback type.'); } $args = $refl->getParameters(); // filter out context/environment args if ($entity->needsEnvironment()) { array_shift($args); } if ($entity->needsContext()) { array_shift($args); } if ('filters' === $type) { // remove the value the filter is applied on array_shift($args); } // format args $args = array_map(function ($param) { if ($param->isDefaultValueAvailable()) { return $param->getName().' = '.json_encode($param->getDefaultValue()); } return $param->getName(); }, $args); return $args; } return null; } private function getPrettyMetadata($type, $entity, $decorated) { if ('tests' === $type) { return ''; } try { $meta = $this->getMetadata($type, $entity); if (null === $meta) { return '(unknown?)'; } } catch (\UnexpectedValueException $e) { return sprintf(' %s', $decorated ? OutputFormatter::escape($e->getMessage()) : $e->getMessage()); } if ('globals' === $type) { if (\is_object($meta)) { return ' = object('.\get_class($meta).')'; } $description = substr(@json_encode($meta), 0, 50); return sprintf(' = %s', $decorated ? OutputFormatter::escape($description) : $description); } if ('functions' === $type) { return '('.implode(', ', $meta).')'; } if ('filters' === $type) { return $meta ? '('.implode(', ', $meta).')' : ''; } return null; } }