Completed
Push — master ( ab2421...ac78a3 )
by Matthieu
02:21
created

Compiler::compileArrayValues()   A

Complexity

Conditions 2
Paths 1

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 22
Code Lines 16

Importance

Changes 0
Metric Value
cc 2
eloc 16
nc 1
nop 1
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
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
22
/**
23
 * Compiles the container into PHP code much more optimized for performances.
24
 *
25
 * @author Matthieu Napoli <[email protected]>
26
 */
27
class Compiler
28
{
29
    /**
30
     * @var string
31
     */
32
    private $containerClass;
33
34
    /**
35
     * Map of entry names to method names.
36
     *
37
     * @var string[]
38
     */
39
    private $entryToMethodMapping = [];
40
41
    /**
42
     * @var string[]
43
     */
44
    private $methods = [];
45
46
    /**
47
     * Compile the container.
48
     *
49
     * @return string The compiled container class name.
50
     */
51
    public function compile(DefinitionSource $definitionSource, string $fileName) : string
52
    {
53
        $this->containerClass = basename($fileName, '.php');
54
55
        // Validate that it's a valid class name
56
        $validClassName = preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $this->containerClass);
57
        if (!$validClassName) {
58
            throw new InvalidArgumentException("The file in which to compile the container must have a name that is a valid class name: {$this->containerClass} is not a valid PHP class name");
59
        }
60
61
        if (file_exists($fileName)) {
62
            // The container is already compiled
63
            return $this->containerClass;
64
        }
65
66
        $definitions = $definitionSource->getDefinitions();
67
68
        foreach ($definitions as $entryName => $definition) {
69
            // Check that the definition can be compiled
70
            $errorMessage = $this->isCompilable($definition);
71
            if ($errorMessage !== true) {
72
                continue;
73
            }
74
            $this->compileDefinition($entryName, $definition);
75
        }
76
77
        ob_start();
78
        require __DIR__ . '/Compiler/Template.php';
79
        $fileContent = ob_get_contents();
80
        ob_end_clean();
81
82
        $fileContent = "<?php\n" . $fileContent;
83
84
        $this->createCompilationDirectory(dirname($fileName));
85
        file_put_contents($fileName, $fileContent);
86
87
        return $this->containerClass;
88
    }
89
90
    /**
91
     * @return string The method name
92
     */
93
    private function compileDefinition(string $entryName, Definition $definition) : string
94
    {
95
        // Generate a unique method name
96
        $methodName = uniqid('get');
97
        $this->entryToMethodMapping[$entryName] = $methodName;
98
99
        switch (true) {
100
            case $definition instanceof ValueDefinition:
101
                $value = $definition->getValue();
102
                $code = 'return ' . $this->compileValue($value) . ';';
103
                break;
104
            case $definition instanceof AliasDefinition:
105
                $targetEntryName = $definition->getTargetEntryName();
106
                $code = 'return $this->delegateContainer->get(' . $this->compileValue($targetEntryName) . ');';
107
                break;
108
            case $definition instanceof StringDefinition:
109
                $entryName = $this->compileValue($definition->getName());
110
                $expression = $this->compileValue($definition->getExpression());
111
                $code = 'return \DI\Definition\StringDefinition::resolveExpression(' . $entryName . ', ' . $expression . ', $this->delegateContainer);';
112
                break;
113
            case $definition instanceof EnvironmentVariableDefinition:
114
                $variableName = $this->compileValue($definition->getVariableName());
115
                $isOptional = $this->compileValue($definition->isOptional());
116
                $defaultValue = $this->compileValue($definition->getDefaultValue());
117
                $code = <<<PHP
118
        \$value = getenv($variableName);
119
        if (false !== \$value) return \$value;
120
        if (!$isOptional) {
121
            throw new \DI\Definition\Exception\InvalidDefinition("The environment variable '{$definition->getVariableName()}' has not been defined");
122
        }
123
        return $defaultValue;
124
PHP;
125
                break;
126
            case $definition instanceof ArrayDefinition:
127
                $values = $this->compileArrayValues($definition);
128
                $values = implode('', $values);
129
                $code = "return [\n$values        ];";
130
                break;
131
            case $definition instanceof ObjectDefinition:
132
                $compiler = new ObjectCreationCompiler($this);
133
                $code = $compiler->compile($definition);
134
                $code .= "\n        return \$object;";
135
                break;
136
            default:
137
                // This case should not happen (so it cannot be tested)
138
                throw new \Exception('Cannot compile definition of type ' . get_class($definition));
139
        }
140
141
        $this->methods[$methodName] = $code;
142
143
        return $methodName;
144
    }
145
146
    public function compileValue($value) : string
147
    {
148
        if ($value instanceof DefinitionHelper) {
149
            $value = $value->getDefinition('');
150
        }
151
152
        // Check that the value can be compiled
153
        $errorMessage = $this->isCompilable($value);
154
        if ($errorMessage !== true) {
155
            throw new InvalidDefinition($errorMessage);
156
        }
157
158
        if ($value instanceof Definition) {
159
            // Give it an arbitrary unique name
160
            $subEntryName = uniqid('SubEntry');
161
            // Compile the sub-definition in another method
162
            $methodName = $this->compileDefinition($subEntryName, $value);
163
            // The value is now a method call to that method (which returns the value)
164
            return "\$this->$methodName()";
165
        }
166
167
        return var_export($value, true);
168
    }
169
170
    private function createCompilationDirectory(string $directory)
171
    {
172
        if (!is_dir($directory) && [email protected]($directory, 0777, true)) {
173
            throw new InvalidArgumentException(sprintf('Compilation directory does not exist and cannot be created: %s.', $directory));
174
        }
175
        if (!is_writable($directory)) {
176
            throw new InvalidArgumentException(sprintf('Compilation directory is not writable: %s.', $directory));
177
        }
178
    }
179
180
    /**
181
     * @return string[]
182
     */
183
    private function compileArrayValues(ArrayDefinition $definition) : array
184
    {
185
        $values = $definition->getValues();
186
        $keys = array_keys($values);
187
188
        $values = array_map(function ($value, $key) use ($definition) {
189
            try {
190
                $compiledValue = $this->compileValue($value);
191
            } catch (\Exception $e) {
192
                throw new DependencyException(sprintf(
193
                    'Error while compiling %s[%s]. %s',
194
                    $definition->getName(),
195
                    $key,
196
                    $e->getMessage()
197
                ), 0, $e);
198
            }
199
200
            return '            ' . $compiledValue . ",\n";
201
        }, $values, $keys);
202
203
        return $values;
204
    }
205
206
    /**
207
     * @return string|null If null is returned that means that the value is compilable.
208
     */
209
    private function isCompilable($value)
210
    {
211
        if ($value instanceof ValueDefinition) {
212
            return $this->isCompilable($value->getValue());
213
        }
214
        if ($value instanceof DecoratorDefinition) {
215
            if (empty($value->getName())) {
216
                return 'Decorators cannot be nested in another definition';
217
            }
218
219
            return 'A decorator definition was found but decorators cannot be compiled';
220
        }
221
        if ($value instanceof FactoryDefinition) {
222
            return 'A factory definition was found but factories cannot be compiled';
223
        }
224
        // All other definitions are compilable
225
        if ($value instanceof Definition) {
226
            return true;
227
        }
228
        if (is_array($value)) {
229
            $compilable = true;
230
            array_walk_recursive($value, function ($value) use (&$compilable) {
231
                // The if avoids unnecessary checks
232
                if ($compilable === true) {
233
                    $message = $this->isCompilable($value);
234
                    if ($message !== true) {
235
                        $compilable = $message;
236
                    }
237
                }
238
            });
239
240
            return $compilable;
241
        }
242
        if (is_object($value)) {
243
            return 'An object was found but objects cannot be compiled';
244
        }
245
        if (is_resource($value)) {
246
            return 'A resource was found but resources cannot be compiled';
247
        }
248
249
        return true;
250
    }
251
}
252