10000 [TwigBridge] Added template "name" argument to debug:twig command to find their paths by yceruto · Pull Request #27981 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[TwigBridge] Added template "name" argument to debug:twig command to find their paths #27981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions src/Symfony/Bridge/Twig/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
-----

* add bundle name suggestion on wrongly overridden templates paths
* added `name` argument in `debug:twig` command and changed `filter` argument as `--filter` option

4.1.0
-----
Expand Down
301 changes: 256 additions & 45 deletions src/Symfony/Bridge/Twig/Command/DebugCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
namespace Symfony\Bridge\Twig\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
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 Symfony\Component\Finder\Finder;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;

Expand Down Expand Up @@ -50,19 +52,24 @@ protected function configure()
{
$this
->setDefinition(array(
new InputArgument('filter', InputArgument::OPTIONAL, 'Show details for all entries matching this filter'),
new InputArgument('name', InputArgument::OPTIONAL, 'The template name'),
new InputOption('filter', null, InputOption::VALUE_REQUIRED, '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 <info>%command.name%</info> command outputs a list of twig functions,
filters, globals and tests. Output can be filtered with an optional argument.
filters, globals and tests.

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

The command lists all functions, filters, etc.

<info>php %command.full_name% date</info>
<info>php %command.full_name% @Twig/Exception/error.html.twig</info>

The command lists all paths that match the given template name.

<info>php %command.full_name% --filter=date</info>

The command lists everything that contains the word date.

Expand All @@ -77,28 +84,107 @@ protected function configure()
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$types = array('functions', 'filters', 'tests', 'globals');
$name = $input->getArgument('name');
$filter = $input->getOption('filter');

if ('json' === $input->getOption('format')) {
$data = array();
foreach ($types as $type) {
foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) {
$data[$type][$name] = $this->getMetadata($type, $entity);
if (null !== $name && !$this->twig->getLoader() instanceof FilesystemLoader) {
throw new InvalidArgumentException(sprintf('Argument "name" not supported, it requires the Twig loader "%s"', FilesystemLoader::class));
}

switch ($input->getOption('format')) {
case 'text':
return $name ? $this->displayPathsText($io, $name) : $this->displayGeneralText($io, $filter);
case 'json':
return $name ? $this->displayPathsJson($io, $name) : $this->displayGeneralJson($io, $filter);
default:
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format')));
}
}

private function displayPathsText(SymfonyStyle $io, string $name)
{
$files = $this->findTemplateFiles($name);
$paths = $this->getLoaderPaths($name);

$io->section('Matched File');
if ($files) {
$io->success(array_shift($files));

if ($files) {
$io->section('Overridden Files');
$io->listing($files);
}
} else {
$alternatives = array();

if ($paths) {
$shortnames = array();
$dirs = array();
foreach (current($paths) as $path) {
$dirs[] = $this->isAbsolutePath($path) ? $path : $this->projectDir.'/'.$path;
}
foreach (Finder::create()->files()->followLinks()->in($dirs) as $file) {
$shortnames[] = str_replace('\\', '/', $file->getRelativePathname());
}

list($namespace, $shortname) = $this->parseTemplateName($name);
$alternatives = $this->findAlternatives($shortname, $shortnames);
if (FilesystemLoader::MAIN_NAMESPACE !== $namespace) {
$alternatives = array_map(function ($shortname) use ($namespace) {
return '@'.$namespace.'/'.$shortname;
}, $alternatives);
}
}
$data['tests'] = array_keys($data['tests']);
$data['loader_paths'] = $this->getLoaderPaths();
if ($wrongBundles = $this->findWrongBundleOverrides()) {
$data['warnings'] = $this->buildWarningMessages($wrongBundles);

$this->error($io, sprintf('Template name "%s" not found', $name), $alternatives);
}

$io->section('Configured Paths');
if ($paths) {
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
} else {
$alternatives = array();
$namespace = $this->parseTemplateName($name)[0];

if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
$message = 'No template paths configured for your application';
} else {
$message = sprintf('No template paths configured for "@%s" namespace', $namespace);
$namespaces = $this->twig->getLoader()->getNamespaces();
foreach ($this->findAlternatives($namespace, $namespaces) as $namespace) {
$alternatives[] = '@'.$namespace;
}
}

$io->writeln(json_encode($data));
$this->error($io, $message, $alternatives);

return 0;
if (!$alternatives && $paths = $this->getLoaderPaths()) {
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
}
}
}

$filter = $input->getArgument('filter');
private function displayPathsJson(SymfonyStyle $io, string $name)
{
$files = $this->findTemplateFiles($name);
$paths = $this->getLoaderPaths($name);

if ($files) {
$data['matched_file'] = array_shift($files);
if ($files) {
$data['overridden_files'] = $files;
}
} else {
$data['matched_file'] = sprintf('Template name "%s" not found', $name);
}
$data['loader_paths'] = $paths;

$io->writeln(json_encode($data));
}

private function displayGeneralText(SymfonyStyle $io, string $filter = null)
{
$types = array('functions', 'filters', 'tests', 'globals');
foreach ($types as $index => $type) {
$items = array();
foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) {
Expand All @@ -117,46 +203,56 @@ protected function execute(InputInterface $input, OutputInterface $output)
$io->listing($items);
}

$rows = array();
$firstNamespace = true;
$prevHasSeparator = false;
foreach ($this->getLoaderPaths() as $namespace => $paths) {
if (!$firstNamespace && !$prevHasSeparator && \count($paths) > 1) {
$rows[] = array('', '');
}
$firstNamespace = false;
foreach ($paths as $path) {
$rows[] = array($namespace, $path.\DIRECTORY_SEPARATOR);
$namespace = '';
if (!$filter && $paths = $this->getLoaderPaths()) {
$io->section('Loader Paths');
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
}

if ($wronBundles = $this->findWrongBundleOverrides()) {
foreach ($this->buildWarningMessages($wronBundles) as $message) {
$io->warning($message);
}
if (\count($paths) > 1) {
$rows[] = array('', '');
$prevHasSeparator = true;
} else {
$prevHasSeparator = false;
}
}

private function displayGeneralJson(SymfonyStyle $io, $filter)
{
$types = array('functions', 'filters', 'tests', 'globals');
$data = array();
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 ($prevHasSeparator) {
array_pop($rows);
if (isset($data['tests'])) {
$data['tests'] = array_keys($data['tests']);
}

if (!$filter && $paths = $this->getLoaderPaths($filter)) {
$data['loader_paths'] = $paths;
}
$io->section('Loader Paths');
$io->table(array('Namespace', 'Paths'), $rows);
$messages = $this->buildWarningMessages($this->findWrongBundleOverrides());
foreach ($messages as $message) {
$io->warning($message);

if ($wronBundles = $this->findWrongBundleOverrides()) {
$data['warnings'] = $this->buildWarningMessages($wronBundles);
}

return 0;
$io->writeln(json_encode($data));
}

private function getLoaderPaths()
private function getLoaderPaths(string $name = null): array
{
if (!($loader = $this->twig->getLoader()) instanceof FilesystemLoader) {
return array();
/** @var FilesystemLoader $loader */
$loader = $this->twig->getLoader();
$loaderPaths = array();
$namespaces = $loader->getNamespaces();
if (null !== $name) {
$namespace = $this->parseTemplateName($name)[0];
$namespaces = array_intersect(array($namespace), $namespaces);
}

$loaderPaths = array();
foreach ($loader->getNamespaces() as $namespace) {
foreach ($namespaces 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);
Expand Down Expand Up @@ -345,4 +441,119 @@ private function buildWarningMessages(array $wrongBundles): array

return $messages;
}

private function error(SymfonyStyle $io, string $message, array $alternatives = array()): void
{
if ($alternatives) {
if (1 === \count($alternatives)) {
$message .= "\n\nDid you mean this?\n ";
} else {
$message .= "\n\nDid you mean one of these?\n ";
}
$message .= implode("\n ", $alternatives);
}

$io->block($message, null, 'fg=white;bg=red', ' ', true);
}

private function findTemplateFiles(string $name): array
{
/** @var FilesystemLoader $loader */
$loader = $this->twig->getLoader();
$files = array();
list($namespace, $shortname) = $this->parseTemplateName($name);

foreach ($loader->getPaths($namespace) as $path) {
if (!$this->isAbsolutePath($path)) {
$path = $this->projectDir.'/'.$path;
}
$filename = $path.'/'.$shortname;

if (is_file($filename)) {
if (false !== $realpath = realpath($filename)) {
$files[] = $this->getRelativePath($realpath);
} else {
$files[] = $this->getRelativePath($filename);
}
}
}

return $files;
}

private function parseTemplateName(string $name, string $default = FilesystemLoader::MAIN_NAMESPACE): array
{
if (isset($name[0]) && '@' === $name[0]) {
if (false === ($pos = strpos($name, '/')) || $pos === \strlen($name) - 1) {
throw new InvalidArgumentException(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
}

$namespace = substr($name, 1, $pos - 1);
$shortname = substr($name, $pos + 1);

return array($namespace, $shortname);
}

return array($default, $name);
}

private function buildTableRows(array $loaderPaths): array
{
$rows = array();
$firstNamespace = true;
$prevHasSeparator = false;

foreach ($loaderPaths as $namespace => $paths) {
if (!$firstNamespace && !$prevHasSeparator && \count($paths) > 1) {
$rows[] = array('', '');
}
$firstNamespace = false;
foreach ($paths as $path) {
$rows[] = array($namespace, $path.\DIRECTORY_SEPARATOR);
$namespace = '';
}
if (\count($paths) > 1) {
$rows[] = array('', '');
$prevHasSeparator = true;
} else {
$prevHasSeparator = false;
}
}
if ($prevHasSeparator) {
array_pop($rows);
}

return $rows;
}

private function findAlternatives(string $name, array $collection): array
{
$alternatives = array();
foreach ($collection as $item) {
$lev = levenshtein($name, $item);
if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
}
}

$threshold = 1e3;
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);

return array_keys($alternatives);
}

private function getRelativePath(string $path): string
{
if (null !== $this->projectDir && 0 === strpos($path, $this->projectDir)) {
return ltrim(substr($path, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR);
}

return $path;
}

private function isAbsolutePath(string $file): bool
{
return strspn($file, '/\\', 0, 1) || (\strlen($file) > 3 && ctype_alpha($file[0]) && ':' === $file[1] && strspn($file, '/\\', 2, 1)) || null !== parse_url($file, PHP_URL_SCHEME);
}
}
Loading
0