Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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