Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e54dd4e

Browse filesBrowse 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
Copy full SHA for e54dd4e

File tree

Expand file treeCollapse file tree

7 files changed

+464
-79
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+464
-79
lines changed

‎src/Symfony/Bridge/Twig/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Twig/CHANGELOG.md
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Twig/Command/DebugCommand.php
+256-45Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.