PluginProbe
WPIDE – File Manager & Code Editor / 3.5.9
WPIDE – File Manager & Code Editor v3.5.9
3.5.9 3.5.8 3.5.7 2.0.14 2.0.15 2.0.16 2.0.2 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1 2.2 2.3 2.3.1 2.3.2 2.4.0 2.5 2.6 3.0 3.1 3.2 3.3 All 55 releases
wpide / vendor / php-di / php-di / src / Compiler / Compiler.php

Compiler.php in WPIDE – File Manager & Code Editor 3.5.9, at vendor/php-di/php-di/src/Compiler/Compiler.php

423 lines 14.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 DI\Compiler;
6
7 use function chmod;
8 use DI\Definition\ArrayDefinition;
9 use DI\Definition\DecoratorDefinition;
10 use DI\Definition\Definition;
11 use DI\Definition\EnvironmentVariableDefinition;
12 use DI\Definition\Exception\InvalidDefinition;
13 use DI\Definition\FactoryDefinition;
14 use DI\Definition\ObjectDefinition;
15 use DI\Definition\Reference;
16 use DI\Definition\Source\DefinitionSource;
17 use DI\Definition\StringDefinition;
18 use DI\Definition\ValueDefinition;
19 use DI\DependencyException;
20 use DI\Proxy\ProxyFactory;
21 use function dirname;
22 use function file_put_contents;
23 use InvalidArgumentException;
24 use Laravel\SerializableClosure\Support\ReflectionClosure;
25 use function rename;
26 use function sprintf;
27 use function tempnam;
28 use function unlink;
29
30 /**
31 * Compiles the container into PHP code much more optimized for performances.
32 *
33 * @author Matthieu Napoli <matthieu@mnapoli.fr>
34 */
35 class Compiler
36 {
37 /**
38 * @var string
39 */
40 private $containerClass;
41
42 /**
43 * @var string
44 */
45 private $containerParentClass;
46
47 /**
48 * Definitions indexed by the entry name. The value can be null if the definition needs to be fetched.
49 *
50 * Keys are strings, values are `Definition` objects or null.
51 *
52 * @var \ArrayIterator
53 */
54 private $entriesToCompile;
55
56 /**
57 * Progressive counter for definitions.
58 *
59 * Each key in $entriesToCompile is defined as 'SubEntry' + counter
60 * and each definition has always the same key in the CompiledContainer
61 * if PHP-DI configuration does not change.
62 *
63 * @var int
64 */
65 private $subEntryCounter;
66
67 /**
68 * Progressive counter for CompiledContainer get methods.
69 *
70 * Each CompiledContainer method name is defined as 'get' + counter
71 * and remains the same after each recompilation
72 * if PHP-DI configuration does not change.
73 *
74 * @var int
75 */
76 private $methodMappingCounter;
77
78 /**
79 * Map of entry names to method names.
80 *
81 * @var string[]
82 */
83 private $entryToMethodMapping = [];
84
85 /**
86 * @var string[]
87 */
88 private $methods = [];
89
90 /**
91 * @var bool
92 */
93 private $autowiringEnabled;
94
95 /**
96 * @var ProxyFactory
97 */
98 private $proxyFactory;
99
100 public function __construct(ProxyFactory $proxyFactory)
101 {
102 $this->proxyFactory = $proxyFactory;
103 }
104
105 public function getProxyFactory() : ProxyFactory
106 {
107 return $this->proxyFactory;
108 }
109
110 /**
111 * Compile the container.
112 *
113 * @return string The compiled container file name.
114 */
115 public function compile(
116 DefinitionSource $definitionSource,
117 string $directory,
118 string $className,
119 string $parentClassName,
120 bool $autowiringEnabled
121 ) : string {
122 $fileName = rtrim($directory, '/') . '/' . $className . '.php';
123
124 if (file_exists($fileName)) {
125 // The container is already compiled
126 return $fileName;
127 }
128
129 $this->autowiringEnabled = $autowiringEnabled;
130
131 // Validate that a valid class name was provided
132 $validClassName = preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $className);
133 if (!$validClassName) {
134 throw new InvalidArgumentException("The container cannot be compiled: `$className` is not a valid PHP class name");
135 }
136
137 $this->entriesToCompile = new \ArrayIterator($definitionSource->getDefinitions());
138
139 // We use an ArrayIterator so that we can keep adding new items to the list while we compile entries
140 foreach ($this->entriesToCompile as $entryName => $definition) {
141 $silenceErrors = false;
142 // This is an entry found by reference during autowiring
143 if (!$definition) {
144 $definition = $definitionSource->getDefinition($entryName);
145 // We silence errors for those entries because type-hints may reference interfaces/abstract classes
146 // which could later be defined, or even not used (we don't want to block the compilation for those)
147 $silenceErrors = true;
148 }
149 if (!$definition) {
150 // We do not throw a `NotFound` exception here because the dependency
151 // could be defined at runtime
152 continue;
153 }
154 // Check that the definition can be compiled
155 $errorMessage = $this->isCompilable($definition);
156 if ($errorMessage !== true) {
157 continue;
158 }
159 try {
160 $this->compileDefinition($entryName, $definition);
161 } catch (InvalidDefinition $e) {
162 if ($silenceErrors) {
163 // forget the entry
164 unset($this->entryToMethodMapping[$entryName]);
165 } else {
166 throw $e;
167 }
168 }
169 }
170
171 $this->containerClass = $className;
172 $this->containerParentClass = $parentClassName;
173
174 ob_start();
175 require __DIR__ . '/Template.php';
176 $fileContent = ob_get_clean();
177
178 $fileContent = "<?php\n" . $fileContent;
179
180 $this->createCompilationDirectory(dirname($fileName));
181 $this->writeFileAtomic($fileName, $fileContent);
182
183 return $fileName;
184 }
185
186 private function writeFileAtomic(string $fileName, string $content) : int
187 {
188 $tmpFile = @tempnam(dirname($fileName), 'swap-compile');
189 if ($tmpFile === false) {
190 throw new InvalidArgumentException(
191 sprintf('Error while creating temporary file in %s', dirname($fileName))
192 );
193 }
194 @chmod($tmpFile, 0666);
195
196 $written = file_put_contents($tmpFile, $content);
197 if ($written === false) {
198 @unlink($tmpFile);
199
200 throw new InvalidArgumentException(sprintf('Error while writing to %s', $tmpFile));
201 }
202
203 @chmod($tmpFile, 0666);
204 $renamed = @rename($tmpFile, $fileName);
205 if (!$renamed) {
206 @unlink($tmpFile);
207 throw new InvalidArgumentException(sprintf('Error while renaming %s to %s', $tmpFile, $fileName));
208 }
209
210 return $written;
211 }
212
213 /**
214 * @throws DependencyException
215 * @throws InvalidDefinition
216 * @return string The method name
217 */
218 private function compileDefinition(string $entryName, Definition $definition) : string
219 {
220 // Generate a unique method name
221 $methodName = 'get' . (++$this->methodMappingCounter);
222 $this->entryToMethodMapping[$entryName] = $methodName;
223
224 switch (true) {
225 case $definition instanceof ValueDefinition:
226 $value = $definition->getValue();
227 $code = 'return ' . $this->compileValue($value) . ';';
228 break;
229 case $definition instanceof Reference:
230 $targetEntryName = $definition->getTargetEntryName();
231 $code = 'return $this->delegateContainer->get(' . $this->compileValue($targetEntryName) . ');';
232 // If this method is not yet compiled we store it for compilation
233 if (!isset($this->entriesToCompile[$targetEntryName])) {
234 $this->entriesToCompile[$targetEntryName] = null;
235 }
236 break;
237 case $definition instanceof StringDefinition:
238 $entryName = $this->compileValue($definition->getName());
239 $expression = $this->compileValue($definition->getExpression());
240 $code = 'return \DI\Definition\StringDefinition::resolveExpression(' . $entryName . ', ' . $expression . ', $this->delegateContainer);';
241 break;
242 case $definition instanceof EnvironmentVariableDefinition:
243 $variableName = $this->compileValue($definition->getVariableName());
244 $isOptional = $this->compileValue($definition->isOptional());
245 $defaultValue = $this->compileValue($definition->getDefaultValue());
246 $code = <<<PHP
247 \$value = \$_ENV[$variableName] ?? \$_SERVER[$variableName] ?? getenv($variableName);
248 if (false !== \$value) return \$value;
249 if (!$isOptional) {
250 throw new \DI\Definition\Exception\InvalidDefinition("The environment variable '{$definition->getVariableName()}' has not been defined");
251 }
252 return $defaultValue;
253 PHP;
254 break;
255 case $definition instanceof ArrayDefinition:
256 try {
257 $code = 'return ' . $this->compileValue($definition->getValues()) . ';';
258 } catch (\Exception $e) {
259 throw new DependencyException(sprintf(
260 'Error while compiling %s. %s',
261 $definition->getName(),
262 $e->getMessage()
263 ), 0, $e);
264 }
265 break;
266 case $definition instanceof ObjectDefinition:
267 $compiler = new ObjectCreationCompiler($this);
268 $code = $compiler->compile($definition);
269 $code .= "\n return \$object;";
270 break;
271 case $definition instanceof DecoratorDefinition:
272 $decoratedDefinition = $definition->getDecoratedDefinition();
273 if (! $decoratedDefinition instanceof Definition) {
274 if (! $definition->getName()) {
275 throw new InvalidDefinition('Decorators cannot be nested in another definition');
276 }
277 throw new InvalidDefinition(sprintf(
278 'Entry "%s" decorates nothing: no previous definition with the same name was found',
279 $definition->getName()
280 ));
281 }
282 $code = sprintf(
283 'return call_user_func(%s, %s, $this->delegateContainer);',
284 $this->compileValue($definition->getCallable()),
285 $this->compileValue($decoratedDefinition)
286 );
287 break;
288 case $definition instanceof FactoryDefinition:
289 $value = $definition->getCallable();
290
291 // Custom error message to help debugging
292 $isInvokableClass = is_string($value) && class_exists($value) && method_exists($value, '__invoke');
293 if ($isInvokableClass && !$this->autowiringEnabled) {
294 throw new InvalidDefinition(sprintf(
295 '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.',
296 $entryName
297 ));
298 }
299
300 $definitionParameters = '';
301 if (!empty($definition->getParameters())) {
302 $definitionParameters = ', ' . $this->compileValue($definition->getParameters());
303 }
304
305 $code = sprintf(
306 'return $this->resolveFactory(%s, %s%s);',
307 $this->compileValue($value),
308 var_export($entryName, true),
309 $definitionParameters
310 );
311
312 break;
313 default:
314 // This case should not happen (so it cannot be tested)
315 throw new \Exception('Cannot compile definition of type ' . get_class($definition));
316 }
317
318 $this->methods[$methodName] = $code;
319
320 return $methodName;
321 }
322
323 public function compileValue($value) : string
324 {
325 // Check that the value can be compiled
326 $errorMessage = $this->isCompilable($value);
327 if ($errorMessage !== true) {
328 throw new InvalidDefinition($errorMessage);
329 }
330
331 if ($value instanceof Definition) {
332 // Give it an arbitrary unique name
333 $subEntryName = 'subEntry' . (++$this->subEntryCounter);
334 // Compile the sub-definition in another method
335 $methodName = $this->compileDefinition($subEntryName, $value);
336 // The value is now a method call to that method (which returns the value)
337 return "\$this->$methodName()";
338 }
339
340 if (is_array($value)) {
341 $value = array_map(function ($value, $key) {
342 $compiledValue = $this->compileValue($value);
343 $key = var_export($key, true);
344
345 return " $key => $compiledValue,\n";
346 }, $value, array_keys($value));
347 $value = implode('', $value);
348
349 return "[\n$value ]";
350 }
351
352 if ($value instanceof \Closure) {
353 return $this->compileClosure($value);
354 }
355
356 return var_export($value, true);
357 }
358
359 private function createCompilationDirectory(string $directory)
360 {
361 if (!is_dir($directory) && !@mkdir($directory, 0777, true) && !is_dir($directory)) {
362 throw new InvalidArgumentException(sprintf('Compilation directory does not exist and cannot be created: %s.', $directory));
363 }
364 if (!is_writable($directory)) {
365 throw new InvalidArgumentException(sprintf('Compilation directory is not writable: %s.', $directory));
366 }
367 }
368
369 /**
370 * @return string|true If true is returned that means that the value is compilable.
371 */
372 private function isCompilable($value)
373 {
374 if ($value instanceof ValueDefinition) {
375 return $this->isCompilable($value->getValue());
376 }
377 if ($value instanceof DecoratorDefinition) {
378 if (empty($value->getName())) {
379 return 'Decorators cannot be nested in another definition';
380 }
381 }
382 // All other definitions are compilable
383 if ($value instanceof Definition) {
384 return true;
385 }
386 if ($value instanceof \Closure) {
387 return true;
388 }
389 if (is_object($value)) {
390 return 'An object was found but objects cannot be compiled';
391 }
392 if (is_resource($value)) {
393 return 'A resource was found but resources cannot be compiled';
394 }
395
396 return true;
397 }
398
399 /**
400 * @throws \DI\Definition\Exception\InvalidDefinition
401 */
402 private function compileClosure(\Closure $closure) : string
403 {
404 $reflector = new ReflectionClosure($closure);
405
406 if ($reflector->getUseVariables()) {
407 throw new InvalidDefinition('Cannot compile closures which import variables using the `use` keyword');
408 }
409
410 if ($reflector->isBindingRequired() || $reflector->isScopeRequired()) {
411 throw new InvalidDefinition('Cannot compile closures which use $this or self/static/parent references');
412 }
413
414 // Force all closures to be static (add the `static` keyword), i.e. they can't use
415 // $this, which makes sense since their code is copied into another class.
416 $code = ($reflector->isStatic() ? '' : 'static ') . $reflector->getCode();
417
418 $code = trim($code, "\t\n\r;");
419
420 return $code;
421 }
422 }
423