8000 Added new command to debug template name and paths · symfony/symfony@0e16bc7 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0e16bc7

Browse files
committed
Added new command to debug template name and paths
1 parent 1f629c8 commit 0e16bc7

File tree

8 files changed

+567
-0
lines changed

8 files changed

+567
-0
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 command `DebugLoaderCommand`
89

910
4.1.0
1011
-----
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Command;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Exception\InvalidArgumentException;
16+
use Symfony\Component\Console\Input\InputArgument;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
use Symfony\Component\Console\Style\SymfonyStyle;
20+
use Symfony\Component\Finder\Finder;
21+
use Twig\Environment;
22+
use Twig\Loader\FilesystemLoader;
23+
24+
/**
25+
* @author Yonel Ceruto <yonelceruto@gmail.com>
26+
*/
27+
class DebugLoaderCommand extends Command
28+
{
29+
protected static $defaultName = 'debug:twig:loader';
30+
31+
private $twig;
32+
private $projectDir;
33+
34+
public function __construct(Environment $twig, string $projectDir)
35+
{
36+
parent::__construct();
37+
38+
$this->twig = $twig;
39+
$this->projectDir = $projectDir;
40+
}
41+
42+
public function isEnabled()
43+
{
44+
return $this->twig->getLoader() instanceof FilesystemLoader;
45+
}
46+
47+
protected function configure()
48+
{
49+
$this
50+
->setDefinition(array(
51+
new InputArgument('name', InputArgument::OPTIONAL, 'The template name'),
52+
))
53+
->setDescription('Shows details for a template name')
54+
->setHelp(<<<'EOF'
55+
The <info>%command.name%</info> command displays all configured paths and
56+
looks for a template name.
57+
58+
<info>php %command.full_name% base.html.twig</info>
59+
<info>php %command.full_name% @Twig/Exception/error.html.twig</info>
60+
61+
The command displays the file to load that match the template name as well as
62+
the overridden files and their configured paths.
63+
EOF
64+
)
65+
;
66+
}
67+
68+
protected function execute(InputInterface $input, OutputInterface $output)
69+
{
70+
$io = new SymfonyStyle($input, $output);
71+
72+
$name = $input->getArgument('name');
73+
$files = null !== $name ? $this->findTemplateFiles($name) : array();
74+
$paths = $this->getLoaderPaths($name);
75+
76+
if ($name) {
77+
$io->section('Matched File');
78+
79+
if ($files) {
80+
$io->success(array_shift($files));
81+
82+
if ($files) {
83+
$io->section('Overridden Files');
84+
$io->listing($files);
85+
}
86+
} else {
87+
$alternatives = array();
88+
89+
if ($paths) {
90+
$shortnames = array();
91+
$dirs = array();
92+
foreach (current($paths) as $path) {
93+
$dirs[] = $this->isAbsolutePath($path) ? $path : $this->projectDir.'/'.$path;
94+
}
95+
foreach (Finder::create()->files()->followLinks()->in($dirs) as $file) {
96+
$shortnames[] = str_replace('\\', '/', $file->getRelativePathname());
97+
}
98+
99+
list($namespace, $shortname) = $this->parseTemplateName($name);
100+
$alternatives = $this->findAlternatives($shortname, $shortnames);
101+
if (FilesystemLoader::MAIN_NAMESPACE !== $namespace) {
102+
$alternatives = array_map(function ($shortname) use ($namespace) {
103+
return '@'.$namespace.'/'.$shortname;
104+
}, $alternatives);
105+
}
106+
}
107+
108+
$this->error($io, sprintf('Template name "%s" not found', $name), $alternatives);
109+
}
110+
}
111+
112+
$io->section('Configured Paths');
113+
if ($paths) {
114+
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
115+
} elseif ($name) {
116+
$alternatives = array();
117+
$namespace = $this->parseTemplateName($name)[0];
118+
119+
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
120+
$message = 'No template paths configured for your application';
121+
} else {
122+
$message = sprintf('No template paths configured for "@%s" namespace', $namespace);
123+
$namespaces = $this->twig->getLoader()->getNamespaces();
124+
foreach ($this->findAlternatives($namespace, $namespaces) as $namespace) {
125+
$alternatives[] = '@'.$namespace;
126+
}
127+
}
128+
129+
$this->error($io, $message, $alternatives);
130+
131+
if (!$alternatives && $paths = $this->getLoaderPaths()) {
132+
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
133+
}
134+
} else {
135+
$this->error($io, 'No template paths configured');
136+
}
137+
}
138+
139+
private function error(SymfonyStyle $io, string $message, array $alternatives = array()): void
140+
{
141+
if ($alternatives) {
142+
if (1 === \count($alternatives)) {
143+
$message .= "\n\nDid you mean this?\n ";
144+
} else {
145+
$message .= "\n\nDid you mean one of these?\n ";
146+
}
147+
$message .= implode("\n ", $alternatives);
148+
}
149+
150+
$io->block($message, null, 'fg=white;bg=red', ' ', true);
151+
}
152+
153+
private function getLoaderPaths(string $name = null): array
154+
{
155+
/** @var FilesystemLoader $loader */
156+
$loader = $this->twig->getLoader();
157+
$loaderPaths = array();
158+
159+
$namespaces = $loader->getNamespaces();
160+
if (null !== $name) {
161+
$namespace = $this->parseTemplateName($name)[0];
162+
$namespaces = array_intersect(array($namespace), $namespaces);
163+
}
164+
165+
foreach ($namespaces as $namespace) {
166+
$paths = array_map(function ($path) {
167+
return ($this->getRelativePath($path) ?: $path).\DIRECTORY_SEPARATOR;
168+
}, $loader->getPaths($namespace));
169+
170+
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
171+
$namespace = '(None)';
172+
} else {
173+
$namespace = '@'.$namespace;
174+
}
175+
176+
$loaderPaths[$namespace] = $paths;
177+
}
178+
179+
return $loaderPaths;
180+
}
181+
182+
private function findTemplateFiles(string $name): array
183+
{
184+
/** @var FilesystemLoader $loader */
185+
$loader = $this->twig->getLoader();
186+
$files = array();
187+
list($namespace, $shortname) = $this->parseTemplateName($name);
188+
189+
foreach ($loader->getPaths($namespace) as $path) {
190+
if (!$this->isAbsolutePath($path)) {
191+
$path = $this->projectDir.'/'.$path;
192+
}
193+
$filename = $path.'/'.$shortname;
194+
195+
if (is_file($filename)) {
196+
if (false !== $realpath = realpath($filename)) {
197+
$files[] = $this->getRelativePath($realpath);
198+
} else {
199+
$files[] = $this->getRelativePath($filename);
200+
}
201+
}
202+
}
203+
204+
return $files;
205+
}
206+
207+
private function parseTemplateName(string $name, string $default = FilesystemLoader::MAIN_NAMESPACE): array
208+
{
209+
if (isset($name[0]) && '@' === $name[0]) {
210+
if (false === ($pos = strpos($name, '/')) || $pos === \strlen($name) - 1) {
211+
throw new InvalidArgumentException(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
212+
}
213+
214+
$namespace = substr($name, 1, $pos - 1);
215+
$shortname = substr($name, $pos + 1);
216+
217+
return array($namespace, $shortname);
218+
}
219+
220+
return array($default, $name);
221+
}
222+
223+
private function buildTableRows(array $loaderPaths): array
224+
{
225+
$rows = array();
226+
$firstNamespace = true;
227+
$prevHasSeparator = false;
228+
229+
foreach ($loaderPaths as $namespace => $paths) {
230+
if (!$firstNamespace && !$prevHasSeparator && \count($paths) > 1) {
231+
$rows[] = array('', '');
232+
}
233+
$firstNamespace = false;
234+
foreach ($paths as $path) {
235+
$rows[] = array($namespace, $path);
236+
$namespace = '';
237+
}
238+
if (\count($paths) > 1) {
239+
$rows[] = array('', '');
240+
$prevHasSeparator = true;
241+
} else {
242+
$prevHasSeparator = false;
243+
}
244+
}
245+
if ($prevHasSeparator) {
246+
array_pop($rows);
247+
}
248+
249+
return $rows;
250+
}
251+
252+
private function findAlternatives(string $name, array $collection): array
253+
{
254+
$alternatives = array();
255+
foreach ($collection as $item) {
256+
$lev = levenshtein($name, $item);
257+
if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
258+
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
259+
}
260+
}
261+
262+
$threshold = 1e3;
263+
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
264+
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
265+
266+
return array_keys($alternatives);
267+
}
268+
269+
private function getRelativePath(string $path): ?string
270+
{
271+
if (null !== $this->projectDir && 0 === strpos($path, $this->projectDir)) {
272+
return ltrim(substr($path, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR);
273+
}
274+
275+
return null;
276+
}
277+
278+
private function isAbsolutePath(string $file): bool
279+
{
280+
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);
281+
}
282+
}

0 commit comments

Comments
 (0)
0