PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.4.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.4.0
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Dependencies / DI / Compiler / Compiler.php

Compiler.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 3.4.0, at includes/Dependencies/DI/Compiler/Compiler.php

364 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace WPDeveloper\BetterDocs\Dependencies\DI\Compiler;
6
7 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\ArrayDefinition;
8 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\DecoratorDefinition;
9 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\Definition;
10 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\EnvironmentVariableDefinition;
11 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\Exception\InvalidDefinition;
12 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\FactoryDefinition;
13 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\ObjectDefinition;
14 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\Reference;
15 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\Source\DefinitionSource;
16 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\StringDefinition;
17 use WPDeveloper\BetterDocs\Dependencies\DI\Definition\ValueDefinition;
18 use WPDeveloper\BetterDocs\Dependencies\DI\DependencyException;
19 use InvalidArgumentException;
20 use WPDeveloper\BetterDocs\Dependencies\PhpParser\Node\Expr\Closure;
21 use WPDeveloper\BetterDocs\Dependencies\SuperClosure\Analyzer\AstAnalyzer;
22 use WPDeveloper\BetterDocs\Dependencies\SuperClosure\Exception\ClosureAnalysisException;
23
24 /**
25 * Compiles the container into PHP code much more optimized for performances.
26 *
27 * @author Matthieu Napoli <matthieu@mnapoli.fr>
28 */
29 class Compiler
30 {
31 /**
32 * @var string
33 */
34 private $containerClass;
35
36 /**
37 * @var string
38 */
39 private $containerParentClass;
40
41 /**
42 * Definitions indexed by the entry name. The value can be null if the definition needs to be fetched.
43 *
44 * Keys are strings, values are `Definition` objects or null.
45 *
46 * @var \ArrayIterator
47 */
48 private $entriesToCompile;
49
50 /**
51 * Map of entry names to method names.
52 *
53 * @var string[]
54 */
55 private $entryToMethodMapping = [];
56
57 /**
58 * @var string[]
59 */
60 private $methods = [];
61
62 /**
63 * @var bool
64 */
65 private $autowiringEnabled;
66
67 /**
68 * Compile the container.
69 *
70 * @return string The compiled container file name.
71 */
72 public function compile(
73 DefinitionSource $definitionSource,
74 string $directory,
75 string $className,
76 string $parentClassName,
77 bool $autowiringEnabled
78 ) : string {
79 $fileName = rtrim($directory, '/') . '/' . $className . '.php';
80
81 if (file_exists($fileName)) {
82 // The container is already compiled
83 return $fileName;
84 }
85
86 $this->autowiringEnabled = $autowiringEnabled;
87
88 // Validate that a valid class name was provided
89 $validClassName = preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $className);
90 if (!$validClassName) {
91 throw new InvalidArgumentException("The container cannot be compiled: `$className` is not a valid PHP class name");
92 }
93
94 $this->entriesToCompile = new \ArrayIterator($definitionSource->getDefinitions());
95
96 // We use an ArrayIterator so that we can keep adding new items to the list while we compile entries
97 foreach ($this->entriesToCompile as $entryName => $definition) {
98 $silenceErrors = false;
99 // This is an entry found by reference during autowiring
100 if (!$definition) {
101 $definition = $definitionSource->getDefinition($entryName);
102 // We silence errors for those entries because type-hints may reference interfaces/abstract classes
103 // which could later be defined, or even not used (we don't want to block the compilation for those)
104 $silenceErrors = true;
105 }
106 if (!$definition) {
107 // We do not throw a `NotFound` exception here because the dependency
108 // could be defined at runtime
109 continue;
110 }
111 // Check that the definition can be compiled
112 $errorMessage = $this->isCompilable($definition);
113 if ($errorMessage !== true) {
114 continue;
115 }
116 try {
117 $this->compileDefinition($entryName, $definition);
118 } catch (InvalidDefinition $e) {
119 if ($silenceErrors) {
120 // forget the entry
121 unset($this->entryToMethodMapping[$entryName]);
122 } else {
123 throw $e;
124 }
125 }
126 }
127
128 $this->containerClass = $className;
129 $this->containerParentClass = $parentClassName;
130
131 ob_start();
132 require __DIR__ . '/Template.php';
133 $fileContent = ob_get_contents();
134 ob_end_clean();
135
136 $fileContent = "<?php\n" . $fileContent;
137
138 $this->createCompilationDirectory(dirname($fileName));
139 file_put_contents($fileName, $fileContent);
140
141 return $fileName;
142 }
143
144 /**
145 * @throws DependencyException
146 * @throws InvalidDefinition
147 * @return string The method name
148 */
149 private function compileDefinition(string $entryName, Definition $definition) : string
150 {
151 // Generate a unique method name
152 $methodName = str_replace('.', '', uniqid('get', true));
153 $this->entryToMethodMapping[$entryName] = $methodName;
154
155 switch (true) {
156 case $definition instanceof ValueDefinition:
157 $value = $definition->getValue();
158 $code = 'return ' . $this->compileValue($value) . ';';
159 break;
160 case $definition instanceof Reference:
161 $targetEntryName = $definition->getTargetEntryName();
162 $code = 'return $this->delegateContainer->get(' . $this->compileValue($targetEntryName) . ');';
163 // If this method is not yet compiled we store it for compilation
164 if (!isset($this->entriesToCompile[$targetEntryName])) {
165 $this->entriesToCompile[$targetEntryName] = null;
166 }
167 break;
168 case $definition instanceof StringDefinition:
169 $entryName = $this->compileValue($definition->getName());
170 $expression = $this->compileValue($definition->getExpression());
171 $code = 'return \WPDeveloper\BetterDocs\Dependencies\DI\Definition\StringDefinition::resolveExpression(' . $entryName . ', ' . $expression . ', $this->delegateContainer);';
172 break;
173 case $definition instanceof EnvironmentVariableDefinition:
174 $variableName = $this->compileValue($definition->getVariableName());
175 $isOptional = $this->compileValue($definition->isOptional());
176 $defaultValue = $this->compileValue($definition->getDefaultValue());
177 $code = <<<PHP
178 \$value = getenv($variableName);
179 if (false !== \$value) return \$value;
180 if (!$isOptional) {
181 throw new \WPDeveloper\BetterDocs\Dependencies\DI\Definition\Exception\InvalidDefinition("The environment variable '{$definition->getVariableName()}' has not been defined");
182 }
183 return $defaultValue;
184 PHP;
185 break;
186 case $definition instanceof ArrayDefinition:
187 try {
188 $code = 'return ' . $this->compileValue($definition->getValues()) . ';';
189 } catch (\Exception $e) {
190 throw new DependencyException(sprintf(
191 'Error while compiling %s. %s',
192 $definition->getName(),
193 $e->getMessage()
194 ), 0, $e);
195 }
196 break;
197 case $definition instanceof ObjectDefinition:
198 $compiler = new ObjectCreationCompiler($this);
199 $code = $compiler->compile($definition);
200 $code .= "\n return \$object;";
201 break;
202 case $definition instanceof DecoratorDefinition:
203 $decoratedDefinition = $definition->getDecoratedDefinition();
204 if (! $decoratedDefinition instanceof Definition) {
205 if (! $definition->getName()) {
206 throw new InvalidDefinition('Decorators cannot be nested in another definition');
207 }
208 throw new InvalidDefinition(sprintf(
209 'Entry "%s" decorates nothing: no previous definition with the same name was found',
210 $definition->getName()
211 ));
212 }
213 $code = sprintf(
214 'return call_user_func(%s, %s, $this->delegateContainer);',
215 $this->compileValue($definition->getCallable()),
216 $this->compileValue($decoratedDefinition)
217 );
218 break;
219 case $definition instanceof FactoryDefinition:
220 $value = $definition->getCallable();
221
222 // Custom error message to help debugging
223 $isInvokableClass = is_string($value) && class_exists($value) && method_exists($value, '__invoke');
224 if ($isInvokableClass && !$this->autowiringEnabled) {
225 throw new InvalidDefinition(sprintf(
226 'Entry "%s" cannot be compiled. Invokable classes cannot be automatically resolved if autowiring is disabled on the container, you need to enable autowiring or define the entry manually.',
227 $entryName
228 ));
229 }
230
231 $definitionParameters = '';
232 if (!empty($definition->getParameters())) {
233 $definitionParameters = ', ' . $this->compileValue($definition->getParameters());
234 }
235
236 $code = sprintf(
237 'return $this->resolveFactory(%s, %s%s);',
238 $this->compileValue($value),
239 var_export($entryName, true),
240 $definitionParameters
241 );
242
243 break;
244 default:
245 // This case should not happen (so it cannot be tested)
246 throw new \Exception('Cannot compile definition of type ' . get_class($definition));
247 }
248
249 $this->methods[$methodName] = $code;
250
251 return $methodName;
252 }
253
254 public function compileValue($value) : string
255 {
256 // Check that the value can be compiled
257 $errorMessage = $this->isCompilable($value);
258 if ($errorMessage !== true) {
259 throw new InvalidDefinition((string) $errorMessage);
260 }
261
262 if ($value instanceof Definition) {
263 // Give it an arbitrary unique name
264 $subEntryName = uniqid('SubEntry');
265 // Compile the sub-definition in another method
266 $methodName = $this->compileDefinition($subEntryName, $value);
267 // The value is now a method call to that method (which returns the value)
268 return "\$this->$methodName()";
269 }
270
271 if (is_array($value)) {
272 $value = array_map(function ($value, $key) {
273 $compiledValue = $this->compileValue($value);
274 $key = var_export($key, true);
275
276 return " $key => $compiledValue,\n";
277 }, $value, array_keys($value));
278 $value = implode('', $value);
279
280 return "[\n$value ]";
281 }
282
283 if ($value instanceof \Closure) {
284 return $this->compileClosure($value);
285 }
286
287 return var_export($value, true);
288 }
289
290 private function createCompilationDirectory(string $directory)
291 {
292 if (!is_dir($directory) && !@mkdir($directory, 0777, true)) {
293 throw new InvalidArgumentException(sprintf('Compilation directory does not exist and cannot be created: %s.', $directory));
294 }
295 if (!is_writable($directory)) {
296 throw new InvalidArgumentException(sprintf('Compilation directory is not writable: %s.', $directory));
297 }
298 }
299
300 /**
301 * @return string|true If null is returned that means that the value is compilable.
302 */
303 private function isCompilable($value)
304 {
305 if ($value instanceof ValueDefinition) {
306 return $this->isCompilable($value->getValue());
307 }
308 if ($value instanceof DecoratorDefinition) {
309 if (empty($value->getName())) {
310 return 'Decorators cannot be nested in another definition';
311 }
312 }
313 // All other definitions are compilable
314 if ($value instanceof Definition) {
315 return true;
316 }
317 if ($value instanceof \Closure) {
318 return true;
319 }
320 if (is_object($value)) {
321 return 'An object was found but objects cannot be compiled';
322 }
323 if (is_resource($value)) {
324 return 'A resource was found but resources cannot be compiled';
325 }
326
327 return true;
328 }
329
330 private function compileClosure(\Closure $closure) : string
331 {
332 $closureAnalyzer = new AstAnalyzer;
333
334 try {
335 $closureData = $closureAnalyzer->analyze($closure);
336 } catch (ClosureAnalysisException $e) {
337 if (stripos($e->getMessage(), 'Two closures were declared on the same line') !== false) {
338 throw new InvalidDefinition('Cannot compile closures when two closures are defined on the same line', 0, $e);
339 }
340
341 throw $e;
342 }
343
344 /** @var Closure $ast */
345 $ast = $closureData['ast'];
346
347 // Force all closures to be static (add the `static` keyword), i.e. they can't use
348 // $this, which makes sense since their code is copied into another class.
349 $ast->static = true;
350
351 // Check if the closure imports variables with `use`
352 if (! empty($ast->uses)) {
353 throw new InvalidDefinition('Cannot compile closures which import variables using the `use` keyword');
354 }
355
356 $code = (new \WPDeveloper\BetterDocs\Dependencies\PhpParser\PrettyPrinter\Standard)->prettyPrint([$ast]);
357
358 // Trim spaces and the last `;`
359 $code = trim($code, "\t\n\r;");
360
361 return $code;
362 }
363 }
364