Completed
Push — master ( 4d66a1...40cfc1 )
by Matthieu
01:32
created

Compiler   C

Coupling/Cohesion

Components 1
Dependencies 16

Complexity

Total Complexity 44

Size/Duplication

Total Lines 303
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 303
rs 6.916
c 0
b 0
f 0
wmc 44
lcom 1
cbo 16

6 Methods

Rating   Name   Duplication   Size   Complexity  
B compile() 0 48 5
B compileValue() 0 39 6
A createCompilationDirectory() 0 9 4
C isCompilable() 0 26 8
B compileClosure() 0 33 4
D compileDefinition() 0 100 17

How to fix   Complexity   

Complex Class

Complex classes like Compiler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Compiler, and based on these observations, apply Extract Interface, too.

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