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

Skip to content

Commit d541670

Browse files
committed
Added new command to debug template name and paths
1 parent cc170eb commit d541670

File tree

3 files changed

+288
-0
lines changed

3 files changed

+288
-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: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 <[email protected]>
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+
$finder = new Finder();
92+
$finder->files()->name('*.twig')->in(current($paths));
93+
foreach ($finder as $file) {
94+
$shortnames[] = $file->getRelativePathname();
95+
}
96+
97+
list($namespace, $shortname) = $this->parseTemplateName($name);
98+
$alternatives = $this->findAlternatives($shortname, $shortnames);
99+
if (FilesystemLoader::MAIN_NAMESPACE !== $namespace) {
100+
$alternatives = array_map(function ($shortname) use ($namespace) {
101+
return '@'.$namespace.'/'.$shortname;
102+
}, $alternatives);
103+
}
104+
}
105+
106+
$this->error($io, sprintf('Template name "%s" not found', $name), $alternatives);
107+
}
108+
}
109+
110+
$io->section('Configured Paths');
111+
if ($paths) {
112+
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
113+
} elseif ($name) {
114+
$alternatives = array();
115+
$namespace = $this->parseTemplateName($name)[0];
116+
117+
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
118+
$message = 'No template paths configured for your application';
119+
} else {
120+
$message = sprintf('No template paths configured for "@%s" namespace', $namespace);
121+
$namespaces = $this->twig->getLoader()->getNamespaces();
122+
foreach ($this->findAlternatives($namespace, $namespaces) as $namespace) {
123+
$alternatives[] = '@'.$namespace;
124+
}
125+
}
126+
127+
$this->error($io, $message, $alternatives);
128+
129+
if (!$alternatives && $paths = $this->getLoaderPaths()) {
130+
$io->table(array('Namespace', 'Paths'), $this->buildTableRows($paths));
131+
}
132+
} else {
133+
$this->error($io, 'No template paths configured');
134+
}
135+
}
136+
137+
private function error(SymfonyStyle $io, string $message, array $alternatives = array()): void
138+
{
139+
if ($alternatives) {
140+
if (1 === \count($alternatives)) {
141+
$message .= "\n\nDid you mean this?\n ";
142+
} else {
143+
$message .= "\n\nDid you mean one of these?\n ";
144+
}
145+
$message .= implode("\n ", $alternatives);
146+
}
147+
148+
$io->block($message, null, 'fg=white;bg=red', ' ', true);
149+
}
150+
151+
private function getLoaderPaths(string $name = null): array
152+
{
153+
/** @var FilesystemLoader $loader */
154+
$loader = $this->twig->getLoader();
155+
$loaderPaths = array();
156+
157+
$namespaces = $loader->getNamespaces();
158+
if (null !== $name) {
159+
list($namespace) = $this->parseTemplateName($name);
160+
$namespaces = array_intersect(array($namespace), $namespaces);
161+
}
162+
163+
foreach ($namespaces as $namespace) {
164+
$paths = array_map(function ($path) {
165+
return ($this->getRelativePath($path) ?: $path).\DIRECTORY_SEPARATOR;
166+
}, $loader->getPaths($namespace));
167+
168+
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
169+
$namespace = '(None)';
170+
} else {
171+
$namespace = '@'.$namespace;
172+
}
173+
174+
$loaderPaths[$namespace] = $paths;
175+
}
176+
177+
return $loaderPaths;
178+
}
179+
180+
private function findTemplateFiles(string $name): array
181+
{
182+
/** @var FilesystemLoader $loader */
183+
$loader = $this->twig->getLoader();
184+
$files = array();
185+
186+
list($namespace, $shortname) = $this->parseTemplateName($name);
187+
$paths = $loader->getPaths($namespace);
188+
189+
foreach ($paths as $path) {
190+
if (!$this->isAbsolutePath($path)) {
191+
$path = $this->projectDir.'/'.$path;
192+
}
193+
194+
if (is_file($path.'/'.$shortname)) {
195+
if (false !== $realpath = realpath($path.'/'.$shortname)) {
196+
$files[] = $this->getRelativePath($realpath);
197+
} else {
198+
$files[] = $this->getRelativePath($path.'/'.$shortname);
199+
}
200+
}
201+
}
202+
203+
return $files;
204+
}
205+
206+
private function parseTemplateName(string $name, string $default = FilesystemLoader::MAIN_NAMESPACE): array
207+
{
208+
if (isset($name[0]) && '@' === $name[0]) {
209+
if (false === ($pos = strpos($name, '/')) || $pos === \strlen($name) - 1) {
210+
throw new InvalidArgumentException(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
211+
}
212+
213+
$namespace = substr($name, 1, $pos - 1);
214+
$shortname = substr($name, $pos + 1);
215+
216+
return array($namespace, $shortname);
217+
}
218+
219+
return array($default, $name);
220+
}
221+
222+
private function buildTableRows(array $loaderPaths): array
223+
{
224+
$rows = array();
225+
$firstNamespace = true;
226+
$prevHasSeparator = false;
227+
228+
foreach ($loaderPaths as $namespace => $paths) {
229+
if (!$firstNamespace && !$prevHasSeparator && \count($paths) > 1) {
230+
$rows[] = array('', '');
231+
}
232+
$firstNamespace = false;
233+
foreach ($paths as $path) {
234+
$rows[] = array($namespace, $path);
235+
$namespace = '';
236+
}
237+
if (\count($paths) > 1) {
238+
$rows[] = array('', '');
239+
$prevHasSeparator = true;
240+
} else {
241+
$prevHasSeparator = false;
242+
}
243+
}
244+
if ($prevHasSeparator) {
245+
array_pop($rows);
246+
}
247+
248+
return $rows;
249+
}
250+
251+
private function findAlternatives(string $name, array $collection): array
252+
{
253+
$alternatives = array();
254+
foreach ($collection as $item) {
255+
$lev = levenshtein($name, $item);
256+
if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
257+
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
258+
}
259+
}
260+
261+
$threshold = 1e3;
262+
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
263+
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
264+
265+
return array_keys($alternatives);
266+
}
267+
268+
private function getRelativePath(string $path): ?string
269+
{
270+
if (null !== $this->projectDir && 0 === strpos($path, $this->projectDir)) {
271+
return ltrim(substr($path, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR);
272+
}
273+
274+
return null;
275+
}
276+
277+
private function isAbsolutePath(string $file): bool
278+
{
279+
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);
280+
}
281+
}

src/Symfony/Bundle/TwigBundle/Resources/config/console.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
<tag name="console.command" command="debug:twig" />
1717
</service>
1818

19+
<service id="twig.command.debug_loader" class="Symfony\Bridge\Twig\Command\DebugLoaderCommand">
20+
<argument type="service" id="twig" />
21+
<argument>%kernel.project_dir%</argument>
22+
<tag name="console.command" command="debug:twig:loader" />
23+
</service>
24+
1925
<service id="twig.command.lint" class="Symfony\Bundle\TwigBundle\Command\LintCommand">
2026
<argument type="service" id="twig" />
2127
<tag name="console.command" command="lint:twig" />

0 commit comments

Comments
 (0)