Completed
Push — master ( f79950...4e1fd0 )
by Matthieu
12s
created

Compiler   B

Coupling/Cohesion

Components 1
Dependencies 15

Complexity

Total Complexity 43

Size/Duplication

Total Lines 299
Duplicated Lines 3.01 %

Importance

Changes 0
Metric Value
dl 9
loc 299
rs 7.5895
c 0
b 0
f 0
wmc 43
lcom 1
cbo 15

6 Methods

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

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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