Completed
Push — master ( f1b7b1...e4c85d )
by Matthieu
9s
created

Compiler::compile()   C

Complexity

Conditions 9
Paths 13

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 70
Code Lines 35

Importance

Changes 0
Metric Value
cc 9
eloc 35
nc 13
nop 5
dl 0
loc 70
rs 6.1585
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace DI\Compiler;
6
7
use DI\Definition\ArrayDefinition;
8
use DI\Definition\DecoratorDefinition;
9
use DI\Definition\Definition;
10
use DI\Definition\EnvironmentVariableDefinition;
11
use DI\Definition\Exception\InvalidDefinition;
12
use DI\Definition\FactoryDefinition;
13
use DI\Definition\ObjectDefinition;
14
use DI\Definition\Reference;
15
use DI\Definition\Source\DefinitionSource;
16
use DI\Definition\StringDefinition;
17
use DI\Definition\ValueDefinition;
18
use DI\DependencyException;
19
use InvalidArgumentException;
20
use PhpParser\Node\Expr\Closure;
21
use SuperClosure\Analyzer\AstAnalyzer;
22
use SuperClosure\Exception\ClosureAnalysisException;
23
24
/**
25
 * Compiles the container into PHP code much more optimized for performances.
26
 *
27
 * @author Matthieu Napoli <[email protected]>
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) {
0 ignored issues
show
introduced by Matthieu Napoli
The condition $errorMessage !== true can never be false.
Loading history...
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 \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 \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) {
0 ignored issues
show
introduced by Matthieu Napoli
The condition $errorMessage !== true can never be false.
Loading history...
259
            throw new InvalidDefinition($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) && [email protected]($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|null 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 \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