PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / scope-vendor / php-di / php-di / src / Compiler / Compiler.php

Compiler.php in The Innovative Form Builder – IvyForms 1.5.1, at backend/scope-vendor/php-di/php-di/src/Compiler/Compiler.php

423 lines 15.2 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 IvyForms\Vendor\DI\Compiler;
6
7 use function chmod;
8 use IvyForms\Vendor\DI\Definition\ArrayDefinition;
9 use IvyForms\Vendor\DI\Definition\DecoratorDefinition;
10 use IvyForms\Vendor\DI\Definition\Definition;
11 use IvyForms\Vendor\DI\Definition\EnvironmentVariableDefinition;
12 use IvyForms\Vendor\DI\Definition\Exception\InvalidDefinition;
13 use IvyForms\Vendor\DI\Definition\FactoryDefinition;
14 use IvyForms\Vendor\DI\Definition\ObjectDefinition;
15 use IvyForms\Vendor\DI\Definition\Reference;
16 use IvyForms\Vendor\DI\Definition\Source\DefinitionSource;
17 use IvyForms\Vendor\DI\Definition\StringDefinition;
18 use IvyForms\Vendor\DI\Definition\ValueDefinition;
19 use IvyForms\Vendor\DI\DependencyException;
20 use IvyForms\Vendor\DI\Proxy\ProxyFactory;
21 use function dirname;
22 use function file_put_contents;
23 use InvalidArgumentException;
24 use IvyForms\Vendor\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 <[email protected]>
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 \IvyForms\Vendor\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 \IvyForms\Vendor\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 \IvyForms\Vendor\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