PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.7.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.7.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 4.7.0, at includes/Dependencies/DI/Compiler/Compiler.php

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