Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e54dd4e

Browse files
committed
feature #27981 [TwigBridge] Added template "name" argument to debug:twig command to find their paths (yceruto)
This PR was squashed before being merged into the 4.2-dev branch (closes #27981). Discussion ---------- [TwigBridge] Added template "name" argument to debug:twig command to find their paths | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #27911 | License | MIT | Doc PR | symfony/symfony-docs#10232 Find the template file (to load by Twig) from a given template name (useful to know which file will be loaded exactly and which ones don't): ![debug-twig-loader-overridden](https://user-images.githubusercontent.com/2028198/42849959-81a8c49a-89f3-11e8-8d93-21581fe606a9.png) This will also show the overridden files if they exist and the paths corresponding to their namespace. In addition, the command suggests alternatives if you made a typo (this way you can check your template name quickly): | namespace typo | template name typo | | --- | --- | | ![debug-twig-loader-ns-typo-alt](https://user-images.githubusercontent.com/2028198/42850624-81803e3c-89f6-11e8-8a92-11f09c99d13c.png) | ![debug-twig-loader-typo-alt](https://user-images.githubusercontent.com/2028198/42850644-99571238-89f6-11e8-9cf7-ed9b880f3d81.png) | <details> <summary><strong>Other outputs</strong></summary> Discovering more alternatives: ![debug-twig-loader-not-found-many-alt](https://user-images.githubusercontent.com/2028198/42850815-82a30eb0-89f7-11e8-8d23-530f8ff325bc.png) Unknown template name: ![debug-twig-loader-not-found](https://user-images.githubusercontent.com/2028198/42850882-d647aad0-89f7-11e8-9735-94149895437f.png) </details> ## Update The feature was introduced into `debug:twig` command and the `filter` argument was converted to `--filter` option. The `name` argument is now the first one of the command. Commits ------- 7ef3d39 [TwigBridge] Added template \"name\" argument to debug:twig command to find their paths
2 parents 4ad01a9 + 7ef3d39 commit e54dd4e

File tree

7 files changed

+464
-79
lines changed

7 files changed

+464
-79
lines changed

src/Symfony/Bridge/Twig/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

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

910
4.1.0
1011
-----

src/Symfony/Bridge/Twig/Command/DebugCommand.php

Lines changed: 256 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
namespace Symfony\Bridge\Twig\Command;
1313

1414
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Exception\InvalidArgumentException;
1516
use Symfony\Component\Console\Input\InputArgument;
1617
use Symfony\Component\Console\Input\InputInterface;
1718
use Symfony\Component\Console\Input\InputOption;
1819
use Symfony\Component\Console\Output\OutputInterface;
1920
use Symfony\Component\Console\Style\SymfonyStyle;
21+
use Symfony\Component\Finder\Finder;
2022
use Twig\Environment;
2123
use Twig\Loader\FilesystemLoader;
2224

@@ -50,19 +52,24 @@ protected function configure()
5052
{
5153
$this
5254
->setDefinition(array(
53-
new InputArgument('filter', InputArgument::OPTIONAL, 'Show details for all entries matching this filter'),
55+
new InputArgument('name', InputArgument::OPTIONAL, 'The template name'),
56+
new InputOption('filter', null, InputOption::VALUE_REQUIRED, 'Show details for all entries matching this filter'),
5457
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (text or json)', 'text'),
5558
))
5659
->setDescription('Shows a list of twig functions, filters, globals and tests')
5760
->setHelp(<<<'EOF'
5861
The <info>%command.name%</info> command outputs a list of twig functions,
59-
filters, globals and tests. Output can be filtered with an optional argument.
62+
filters, globals and tests.
6063
6164
<info>php %command.full_name%</info>
6265
6366
The command lists all functions, filters, etc.
6467
65-
<info>php %command.full_name% date</info>
68+
<info>php %command.full_name% @Twig/Exception/error.html.twig</info>
69+
70+
The command lists all paths that match the given template name.
71+
72+
<info>php %command.full_name% --filter=date</info>
6673
6774
The command lists everything that contains the word date.
6875
@@ -77,28 +84,107 @@ protected function configure()
7784
protected function execute(InputInterface $input, OutputInterface $output)
7885
{
7986
$io = new SymfonyStyle($input, $output);
80-
$types = array('functions', 'filters', 'tests', 'globals');
87+
$name = $input->getArgument('name');
88+
$filter = $input->getOption('filter');
8189

82-
if ('json' === $input->getOption('format')) {
83-
$data = array();
84-
foreach ($types as $type) {
85-
foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) {
86-
$data[$type][$name] = $this->getMetadata($type, $entity);
90+
if (null !== $name && !$this->twig->getLoader() instanceof FilesystemLoader) {
91+
throw new InvalidArgumentException(sprintf('Argument "name" not supported, it requires the Twig loader "%s"', FilesystemLoader::class));
92+
}
93+
94+
switch ($input->getOption('format')) {
95+
case 'text':
96+
return $name ? $this->displayPathsText($io, $name) : $this->displayGeneralText($io, $filter);
97+
case 'json':
98+
return $name ? $this->displayPathsJson($io, $name) : $this->displayGeneralJson($io, $filter);
99+
default:
100+
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format')));
101+
}
102+
}
103+
104+
private function displayPathsText(SymfonyStyle $io, string $name)
105+
{
106+
$files = $this->findTemplateFiles($name);
107+
$paths = $this->getLoaderPaths($name);
108+
109+
$io->section('Matched File');
110+
if ($files) {
111+
$io->success(array_shift($files));
112+
113+
if ($files) {
114+
$io->section('Overridden Files');
115+
$io->listing($files);
116+
}
117+
} else {
118+
$alternatives = array();
119+
120+
if ($paths) {
121+
$shortnames = array();
122+
$dirs = array();
123+
foreach (current($paths) as $path) {
124+
$dirs[] = $this->isAbsolutePath($path) ? $path : $this->projectDir.'/'.$path;
125+
}
126+
foreach (Finder::create()->files()->followLinks()->in($dirs) as $file) {
127+
$shortnames[] = str_replace('\\', '/', $file->getRelativePathname());
128+
}
129+
130+
list($namespace, $shortname) = $this->parseTemplateName($name);
131+
$alternatives = $this->findAlternatives($shortname, $shortnames);
132+
if (FilesystemLoader::MAIN_NAMESPACE !== $namespace) {
133+
$alternatives = array_map(function ($shortname) use ($namespace) {
134+
return '@'.$namespace.'/'.$shortname;
135+
}, $alternatives);
87136
}
88137
}
89-
$data['tests'] = array_keys($data['tests']);
90-
$data['loader_paths'] = $this->getLoaderPaths();
91-
if ($wrongBundles = $this->findWrongBundleOverrides()) {
92-
$data['warnings'] = $this->buildWarningMessages($wrongBundles);
138+
139+
$this->error($io, sprintf('Template name "%s" not found', $name), $alternatives);
140+
}
141+
142+
$io->section('Configured Paths');
143+
if ($paths) {
144+
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
145+
} else {
146+
$alternatives = array();
147+
$namespace = $this->parseTemplateName($name)[0];
148+
149+
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
150+
$message = 'No template paths configured for your application';
151+
} else {
152+
$message = sprintf('No template paths configured for "@%s" namespace', $namespace);
153+
$namespaces = $this->twig->getLoader()->getNamespaces();
154+
foreach ($this->findAlternatives($namespace, $namespaces) as $namespace) {
155+
$alternatives[] = '@'.$namespace;
156+
}
93157
}
94158

95-
$io->writeln(json_encode($data));
159+
$this->error($io, $message, $alternatives);
96160

97-
return 0;
161+
if (!$alternatives && $paths = $this->getLoaderPaths()) {
162+
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
163+
}
98164
}
165+
}
99166

100-
$filter = $input->getArgument('filter');
167+
private function displayPathsJson(SymfonyStyle $io, string $name)
168+
{
169+
$files = $this->findTemplateFiles($name);
170+
$paths = $this->getLoaderPaths($name);
171+
172+
if ($files) {
173+
$data['matched_file'] = array_shift($files);
174+
if ($files) {
175+
$data['overridden_files'] = $files;
176+
}
177+
} else {
178+
$data['matched_file'] = sprintf('Template name "%s" not found', $name);
179+
}
180+
$data['loader_paths'] = $paths;
101181

182+
$io->writeln(json_encode($data));
183+
}
184+
185+
private function displayGeneralText(SymfonyStyle $io, string $filter = null)
186+
{
187+
$types = array('functions', 'filters', 'tests', 'globals');
102188
foreach ($types as $index => $type) {
103189
$items = array();
104190
foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) {
@@ -117,46 +203,56 @@ protected function execute(InputInterface $input, OutputInterface $output)
117203
$io->listing($items);
118204
}
119205

120-
$rows = array();
121-
$firstNamespace = true;
122-
$prevHasSeparator = false;
123-
foreach ($this->getLoaderPaths() as $namespace => $paths) {
124-
if (!$firstNamespace && !$prevHasSeparator && \count($paths) > 1) {
125-
$rows[] = array('', '');
126-
}
127-
$firstNamespace = false;
128-
foreach ($paths as $path) {
129-
$rows[] = array($namespace, $path.\DIRECTORY_SEPARATOR);
130-
$namespace = '';
206+
if (!$filter && $paths = $this->getLoaderPaths()) {
207+
$io->section('Loader Paths');
208+
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
209+
}
210+
211+
if ($wronBundles = $this->findWrongBundleOverrides()) {
212+
foreach ($this->buildWarningMessages($wronBundles) as $message) {
213+
$io->warning($message);
131214
}
132-
if (\count($paths) > 1) {
133-
$rows[] = array('', '');
134-
$prevHasSeparator = true;
135-
} else {
136-
$prevHasSeparator = false;
215+
}
216+
}
217+
218+
private function displayGeneralJson(SymfonyStyle $io, $filter)
219+
{
220+
$types = array('functions', 'filters', 'tests', 'globals');
221+
$data = array();
222+
foreach ($types as $type) {
223+
foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) {
224+
if (!$filter || false !== strpos($name, $filter)) {
225+
$data[$type][$name] = $this->getMetadata($type, $entity);
226+
}
137227
}
138228
}
139-
if ($prevHasSeparator) {
140-
array_pop($rows);
229+
if (isset($data['tests'])) {
230+
$data['tests'] = array_keys($data['tests']);
231+
}
232+
233+
if (!$filter && $paths = $this->getLoaderPaths($filter)) {
234+
$data['loader_paths'] = $paths;
141235
}
142-
$io->section('Loader Paths');
143-
$io->table(array('Namespace', 'Paths'), $rows);
144-
$messages = $this->buildWarningMessages($this->findWrongBundleOverrides());
145-
foreach ($messages as $message) {
146-
$io->warning($message);
236+
237+
if ($wronBundles = $this->findWrongBundleOverrides()) {
238+
$data['warnings'] = $this->buildWarningMessages($wronBundles);
147239
}
148240

149-
return 0;
241+
$io->writeln(json_encode($data));
150242
}
151243

152-
private function getLoaderPaths()
244+
private function getLoaderPaths(string $name = null): array
153245
{
154-
if (!($loader = $this->twig->getLoader()) instanceof FilesystemLoader) {
155-
return array();
246+
/** @var FilesystemLoader $loader */
247+
$loader = $this->twig->getLoader();
248+
$loaderPaths = array();
249+
$namespaces = $loader->getNamespaces();
250+
if (null !== $name) {
251+
$namespace = $this->parseTemplateName($name)[0];
252+
$namespaces = array_intersect(array($namespace), $namespaces);
156253
}
157254

158-
$loaderPaths = array();
159-
foreach ($loader->getNamespaces() as $namespace) {
255+
foreach ($namespaces as $namespace) {
160256
$paths = array_map(function ($path) {
161257
if (null !== $this->projectDir && 0 === strpos($path, $this->projectDir)) {
162258
$path = ltrim(substr($path, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR);
@@ -345,4 +441,119 @@ private function buildWarningMessages(array $wrongBundles): array
345441

346442
return $messages;
347443
}
444+
445+
private function error(SymfonyStyle $io, string $message, array $alternatives = array()): void
446+
{
447+
if ($alternatives) {
448+
if (1 === \count($alternatives)) {
449+
$message .= "\n\nDid you mean this?\n ";
450+
} else {
451+
$message .= "\n\nDid you mean one of these?\n ";
452+
}
453+
$message .= implode("\n ", $alternatives);
454+
}
455+
456+
$io->block($message, null, 'fg=white;bg=red', ' ', true);
457+
}
458+
459+
private function findTemplateFiles(string $name): array
460+
{
461+
/** @var FilesystemLoader $loader */
462+
$loader = $this->twig->getLoader();
463+
$files = array();
464+
list($namespace, $shortname) = $this->parseTemplateName($name);
465+
466+
foreach ($loader->getPaths($namespace) as $path) {
467+
if (!$this->isAbsolutePath($path)) {
468+
$path = $this->projectDir.'/'.$path;
469+
}
470+
$filename = $path.'/'.$shortname;
471+
472+
if (is_file($filename)) {
473+
if (false !== $realpath = realpath($filename)) {
474+
$files[] = $this->getRelativePath($realpath);
475+
} else {
476+
$files[] = $this->getRelativePath($filename);
477+
}
478+
}
479+
}
480+
481+
return $files;
482+
}
483+
484+
private function parseTemplateName(string $name, string $default = FilesystemLoader::MAIN_NAMESPACE): array
485+
{
486+
if (isset($name[0]) && '@' === $name[0]) {
487+
if (false === ($pos = strpos($name, '/')) || $pos === \strlen($name) - 1) {
488+
throw new InvalidArgumentException(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
489+
}
490+
491+
$namespace = substr($name, 1, $pos - 1);
492+
$shortname = substr($name, $pos + 1);
493+
494+
return array($namespace, $shortname);
495+
}
496+
497+
return array($default, $name);
498+
}
499+
500+
private function buildTableRows(array $loaderPaths): array
501+
{
502+
$rows = array();
503+
$firstNamespace = true;
504+
$prevHasSeparator = false;
505+
506+
foreach ($loaderPaths as $namespace => $paths) {
507+
if (!$firstNamespace && !$prevHasSeparator && \count($paths) > 1) {
508+
$rows[] = array('', '');
509+
}
510+
$firstNamespace = false;
511+
foreach ($paths as $path) {
512+
$rows[] = array($namespace, $path.\DIRECTORY_SEPARATOR);
513+
$namespace = '';
514+
}
515+
if (\count($paths) > 1) {
516+
$rows[] = array('', '');
517+
$prevHasSeparator = true;
518+
} else {
519+
$prevHasSeparator = false;
520+
}
521+
}
522+
if ($prevHasSeparator) {
523+
array_pop($rows);
524+
}
525+
526+
return $rows;
527+
}
528+
529+
private function findAlternatives(string $name, array $collection): array
530+
{
531+
$alternatives = array();
532+
foreach ($collection as $item) {
533+
$lev = levenshtein($name, $item);
534+
if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
535+
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
536+
}
537+
}
538+
539+
$threshold = 1e3;
540+
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
541+
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
542+
543+
return array_keys($alternatives);
544+
}
545+
546+
private function getRelativePath(string $path): string
547+
{
548+
if (null !== $this->projectDir && 0 === strpos($path, $this->projectDir)) {
549+
return ltrim(substr($path, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR);
550+
}
551+
552+
return $path;
553+
}
554+
555+
private function isAbsolutePath(string $file): bool
556+
{
557+
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);
558+
}
348559
}

0 commit comments

Comments
 (0)