Completed
Push — master ( 5a160c...5540f7 )
by Matthieu
04:15
created

Compiler   B

Coupling/Cohesion

Components 1
Dependencies 16

Complexity

Total Complexity 41

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 277
rs 7.0874
c 0
b 0
f 0
wmc 41
lcom 1
cbo 16

6 Methods

Rating   Name   Duplication   Size   Complexity  
B compile() 0 42 5
D compileDefinition() 0 83 14
B compileValue() 0 39 6
A createCompilationDirectory() 0 9 4
C isCompilable() 0 28 8
B compileClosure() 0 33 4

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