1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file is part of the Explicit Architecture POC, |
7
|
|
|
* which is created on top of the Symfony Demo application. |
8
|
|
|
* |
9
|
|
|
* (c) Herberto Graça <[email protected]> |
10
|
|
|
* |
11
|
|
|
* For the full copyright and license information, please view the LICENSE |
12
|
|
|
* file that was distributed with this source code. |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
namespace Acme\App\Infrastructure\TemplateEngine\Twig\Extension\SourceCode; |
16
|
|
|
|
17
|
|
|
use ReflectionFunction; |
18
|
|
|
use ReflectionMethod; |
19
|
|
|
use ReflectionObject; |
20
|
|
|
use Twig\Environment; |
21
|
|
|
use Twig\Extension\AbstractExtension; |
22
|
|
|
use Twig\Template; |
23
|
|
|
use Twig\TwigFunction; |
24
|
|
|
use Twig_Template; |
25
|
|
|
use Twig_TemplateWrapper; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* CAUTION: this is an extremely advanced Twig extension. It's used to get the |
29
|
|
|
* source code of the controller and the template used to render the current |
30
|
|
|
* page. If you are starting with Symfony, don't look at this code and consider |
31
|
|
|
* studying instead the code of the Md2HtmlExtension.php extension. |
32
|
|
|
* |
33
|
|
|
* @author Ryan Weaver <[email protected]> |
34
|
|
|
* @author Javier Eguiluz <[email protected]> |
35
|
|
|
*/ |
36
|
|
|
class SourceCodeExtension extends AbstractExtension |
37
|
|
|
{ |
38
|
|
|
/** |
39
|
|
|
* @var callable| null |
40
|
|
|
*/ |
41
|
|
|
private $controller; |
42
|
|
|
|
43
|
|
|
public function setController(?callable $controller): void |
44
|
|
|
{ |
45
|
|
|
$this->controller = $controller; |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* {@inheritdoc} |
50
|
|
|
*/ |
51
|
|
|
public function getFunctions(): array |
52
|
|
|
{ |
53
|
|
|
return [ |
54
|
|
|
new TwigFunction('show_source_code', [$this, 'showSourceCode'], ['is_safe' => ['html'], 'needs_environment' => true]), |
55
|
|
|
]; |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @param string|Twig_Template|Twig_TemplateWrapper|array $template |
60
|
|
|
* |
61
|
|
|
* @throws \Twig_Error_Loader |
62
|
|
|
* @throws \Twig_Error_Runtime |
63
|
|
|
* @throws \Twig_Error_Syntax |
64
|
|
|
*/ |
65
|
|
|
public function showSourceCode(Environment $twig, $template): string |
66
|
|
|
{ |
67
|
|
|
return $twig->render('@TemplateEngine/Twig/Extension/SourceCode/source_code.html.twig', [ |
68
|
|
|
'controller' => $this->getController(), |
69
|
|
|
'template' => $this->getTemplateSource($twig->resolveTemplate($template)), |
|
|
|
|
70
|
|
|
]); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
private function getController(): ?array |
74
|
|
|
{ |
75
|
|
|
// this happens for example for exceptions (404 errors, etc.) |
76
|
|
|
if ($this->controller === null) { |
77
|
|
|
return null; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
$method = $this->getCallableReflector($this->controller); |
81
|
|
|
|
82
|
|
|
$classCode = file($method->getFileName()); |
83
|
|
|
$methodCode = \array_slice($classCode, $method->getStartLine() - 1, $method->getEndLine() - $method->getStartLine() + 1); |
84
|
|
|
$controllerCode = ' ' . $method->getDocComment() . "\n" . implode('', $methodCode); |
85
|
|
|
|
86
|
|
|
return [ |
87
|
|
|
'file_path' => $method->getFileName(), |
88
|
|
|
'starting_line' => $method->getStartLine(), |
89
|
|
|
'source_code' => $this->unIndentCode($controllerCode), |
90
|
|
|
]; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Gets a reflector for a callable. |
95
|
|
|
* |
96
|
|
|
* This logic is copied from Symfony\Component\HttpKernel\Controller\ControllerResolver::getArguments |
97
|
|
|
* |
98
|
|
|
* @throws \ReflectionException |
99
|
|
|
*/ |
100
|
|
|
private function getCallableReflector(callable $callable): \ReflectionFunctionAbstract |
101
|
|
|
{ |
102
|
|
|
if (\is_array($callable)) { |
103
|
|
|
return new ReflectionMethod($callable[0], $callable[1]); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
if (\is_object($callable) && !$callable instanceof \Closure) { |
107
|
|
|
$r = new ReflectionObject($callable); |
108
|
|
|
|
109
|
|
|
return $r->getMethod('__invoke'); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
return new ReflectionFunction($callable); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
private function getTemplateSource(Template $template): array |
116
|
|
|
{ |
117
|
|
|
$templateSource = $template->getSourceContext(); |
118
|
|
|
|
119
|
|
|
return [ |
120
|
|
|
// Twig templates are not always stored in files (they can be stored |
121
|
|
|
// in a database for example). However, for the needs of the Symfony |
122
|
|
|
// Demo app, we consider that all templates are stored in files and |
123
|
|
|
// that their file paths can be obtained through the source context. |
124
|
|
|
'file_path' => $templateSource->getPath(), |
125
|
|
|
'starting_line' => 1, |
126
|
|
|
'source_code' => $templateSource->getCode(), |
127
|
|
|
]; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Utility method that "unIndents" the given $code when all its lines start |
132
|
|
|
* with a tabulation of four white spaces. |
133
|
|
|
*/ |
134
|
|
|
private function unIndentCode(string $code): string |
135
|
|
|
{ |
136
|
|
|
$formattedCode = $code; |
137
|
|
|
$codeLines = explode("\n", $code); |
138
|
|
|
|
139
|
|
|
$indentedLines = array_filter($codeLines, function ($lineOfCode) { |
140
|
|
|
return $lineOfCode === '' || mb_substr($lineOfCode, 0, 4) === ' '; |
141
|
|
|
}); |
142
|
|
|
|
143
|
|
|
if (\count($indentedLines) === \count($codeLines)) { |
144
|
|
|
$formattedCode = array_map(function ($lineOfCode) { |
145
|
|
|
return mb_substr($lineOfCode, 4); |
146
|
|
|
}, $codeLines); |
147
|
|
|
$formattedCode = implode("\n", $formattedCode); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
return $formattedCode; |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.