1 |
efrain |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
declare(strict_types=1);
|
|
|
4 |
|
|
|
5 |
namespace DI\Compiler;
|
|
|
6 |
|
|
|
7 |
use function chmod;
|
|
|
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 DI\DependencyException;
|
|
|
20 |
use DI\Proxy\ProxyFactory;
|
|
|
21 |
use function dirname;
|
|
|
22 |
use function file_put_contents;
|
|
|
23 |
use InvalidArgumentException;
|
|
|
24 |
use Laravel\SerializableClosure\Support\ReflectionClosure;
|
|
|
25 |
use function rename;
|
|
|
26 |
use function sprintf;
|
|
|
27 |
use function tempnam;
|
|
|
28 |
use function unlink;
|
|
|
29 |
|
|
|
30 |
/**
|
|
|
31 |
* Compiles the container into PHP code much more optimized for performances.
|
|
|
32 |
*
|
|
|
33 |
* @author Matthieu Napoli <matthieu@mnapoli.fr>
|
|
|
34 |
*/
|
|
|
35 |
class Compiler
|
|
|
36 |
{
|
|
|
37 |
private string $containerClass;
|
|
|
38 |
|
|
|
39 |
private string $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 |
private \ArrayIterator $entriesToCompile;
|
|
|
47 |
|
|
|
48 |
/**
|
|
|
49 |
* Progressive counter for definitions.
|
|
|
50 |
*
|
|
|
51 |
* Each key in $entriesToCompile is defined as 'SubEntry' + counter
|
|
|
52 |
* and each definition has always the same key in the CompiledContainer
|
|
|
53 |
* if PHP-DI configuration does not change.
|
|
|
54 |
*/
|
|
|
55 |
private int $subEntryCounter = 0;
|
|
|
56 |
|
|
|
57 |
/**
|
|
|
58 |
* Progressive counter for CompiledContainer get methods.
|
|
|
59 |
*
|
|
|
60 |
* Each CompiledContainer method name is defined as 'get' + counter
|
|
|
61 |
* and remains the same after each recompilation
|
|
|
62 |
* if PHP-DI configuration does not change.
|
|
|
63 |
*/
|
|
|
64 |
private int $methodMappingCounter = 0;
|
|
|
65 |
|
|
|
66 |
/**
|
|
|
67 |
* Map of entry names to method names.
|
|
|
68 |
*
|
|
|
69 |
* @var string[]
|
|
|
70 |
*/
|
|
|
71 |
private array $entryToMethodMapping = [];
|
|
|
72 |
|
|
|
73 |
/**
|
|
|
74 |
* @var string[]
|
|
|
75 |
*/
|
|
|
76 |
private array $methods = [];
|
|
|
77 |
|
|
|
78 |
private bool $autowiringEnabled;
|
|
|
79 |
|
|
|
80 |
public function __construct(
|
|
|
81 |
private ProxyFactory $proxyFactory,
|
|
|
82 |
) {
|
|
|
83 |
}
|
|
|
84 |
|
|
|
85 |
public function getProxyFactory() : ProxyFactory
|
|
|
86 |
{
|
|
|
87 |
return $this->proxyFactory;
|
|
|
88 |
}
|
|
|
89 |
|
|
|
90 |
/**
|
|
|
91 |
* Compile the container.
|
|
|
92 |
*
|
|
|
93 |
* @return string The compiled container file name.
|
|
|
94 |
*/
|
|
|
95 |
public function compile(
|
|
|
96 |
DefinitionSource $definitionSource,
|
|
|
97 |
string $directory,
|
|
|
98 |
string $className,
|
|
|
99 |
string $parentClassName,
|
|
|
100 |
bool $autowiringEnabled
|
|
|
101 |
) : string {
|
|
|
102 |
$fileName = rtrim($directory, '/') . '/' . $className . '.php';
|
|
|
103 |
|
|
|
104 |
if (file_exists($fileName)) {
|
|
|
105 |
// The container is already compiled
|
|
|
106 |
return $fileName;
|
|
|
107 |
}
|
|
|
108 |
|
|
|
109 |
$this->autowiringEnabled = $autowiringEnabled;
|
|
|
110 |
|
|
|
111 |
// Validate that a valid class name was provided
|
|
|
112 |
$validClassName = preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $className);
|
|
|
113 |
if (!$validClassName) {
|
|
|
114 |
throw new InvalidArgumentException("The container cannot be compiled: `$className` is not a valid PHP class name");
|
|
|
115 |
}
|
|
|
116 |
|
|
|
117 |
$this->entriesToCompile = new \ArrayIterator($definitionSource->getDefinitions());
|
|
|
118 |
|
|
|
119 |
// We use an ArrayIterator so that we can keep adding new items to the list while we compile entries
|
|
|
120 |
foreach ($this->entriesToCompile as $entryName => $definition) {
|
|
|
121 |
$silenceErrors = false;
|
|
|
122 |
// This is an entry found by reference during autowiring
|
|
|
123 |
if (!$definition) {
|
|
|
124 |
$definition = $definitionSource->getDefinition($entryName);
|
|
|
125 |
// We silence errors for those entries because type-hints may reference interfaces/abstract classes
|
|
|
126 |
// which could later be defined, or even not used (we don't want to block the compilation for those)
|
|
|
127 |
$silenceErrors = true;
|
|
|
128 |
}
|
|
|
129 |
if (!$definition) {
|
|
|
130 |
// We do not throw a `NotFound` exception here because the dependency
|
|
|
131 |
// could be defined at runtime
|
|
|
132 |
continue;
|
|
|
133 |
}
|
|
|
134 |
// Check that the definition can be compiled
|
|
|
135 |
$errorMessage = $this->isCompilable($definition);
|
|
|
136 |
if ($errorMessage !== true) {
|
|
|
137 |
continue;
|
|
|
138 |
}
|
|
|
139 |
try {
|
|
|
140 |
$this->compileDefinition($entryName, $definition);
|
|
|
141 |
} catch (InvalidDefinition $e) {
|
|
|
142 |
if ($silenceErrors) {
|
|
|
143 |
// forget the entry
|
|
|
144 |
unset($this->entryToMethodMapping[$entryName]);
|
|
|
145 |
} else {
|
|
|
146 |
throw $e;
|
|
|
147 |
}
|
|
|
148 |
}
|
|
|
149 |
}
|
|
|
150 |
|
|
|
151 |
$this->containerClass = $className;
|
|
|
152 |
$this->containerParentClass = $parentClassName;
|
|
|
153 |
|
|
|
154 |
ob_start();
|
|
|
155 |
require __DIR__ . '/Template.php';
|
|
|
156 |
$fileContent = ob_get_clean();
|
|
|
157 |
|
|
|
158 |
$fileContent = "<?php\n" . $fileContent;
|
|
|
159 |
|
|
|
160 |
$this->createCompilationDirectory(dirname($fileName));
|
|
|
161 |
$this->writeFileAtomic($fileName, $fileContent);
|
|
|
162 |
|
|
|
163 |
return $fileName;
|
|
|
164 |
}
|
|
|
165 |
|
|
|
166 |
private function writeFileAtomic(string $fileName, string $content) : void
|
|
|
167 |
{
|
|
|
168 |
$tmpFile = @tempnam(dirname($fileName), 'swap-compile');
|
|
|
169 |
if ($tmpFile === false) {
|
|
|
170 |
throw new InvalidArgumentException(
|
|
|
171 |
sprintf('Error while creating temporary file in %s', dirname($fileName))
|
|
|
172 |
);
|
|
|
173 |
}
|
|
|
174 |
@chmod($tmpFile, 0666);
|
|
|
175 |
|
|
|
176 |
$written = file_put_contents($tmpFile, $content);
|
|
|
177 |
if ($written === false) {
|
|
|
178 |
@unlink($tmpFile);
|
|
|
179 |
|
|
|
180 |
throw new InvalidArgumentException(sprintf('Error while writing to %s', $tmpFile));
|
|
|
181 |
}
|
|
|
182 |
|
|
|
183 |
@chmod($tmpFile, 0666);
|
|
|
184 |
$renamed = @rename($tmpFile, $fileName);
|
|
|
185 |
if (!$renamed) {
|
|
|
186 |
@unlink($tmpFile);
|
|
|
187 |
|
|
|
188 |
throw new InvalidArgumentException(sprintf('Error while renaming %s to %s', $tmpFile, $fileName));
|
|
|
189 |
}
|
|
|
190 |
}
|
|
|
191 |
|
|
|
192 |
/**
|
|
|
193 |
* @return string The method name
|
|
|
194 |
* @throws DependencyException
|
|
|
195 |
* @throws InvalidDefinition
|
|
|
196 |
*/
|
|
|
197 |
private function compileDefinition(string $entryName, Definition $definition) : string
|
|
|
198 |
{
|
|
|
199 |
// Generate a unique method name
|
|
|
200 |
$methodName = 'get' . (++$this->methodMappingCounter);
|
|
|
201 |
$this->entryToMethodMapping[$entryName] = $methodName;
|
|
|
202 |
|
|
|
203 |
switch (true) {
|
|
|
204 |
case $definition instanceof ValueDefinition:
|
|
|
205 |
$value = $definition->getValue();
|
|
|
206 |
$code = 'return ' . $this->compileValue($value) . ';';
|
|
|
207 |
break;
|
|
|
208 |
case $definition instanceof Reference:
|
|
|
209 |
$targetEntryName = $definition->getTargetEntryName();
|
|
|
210 |
$code = 'return $this->delegateContainer->get(' . $this->compileValue($targetEntryName) . ');';
|
|
|
211 |
// If this method is not yet compiled we store it for compilation
|
|
|
212 |
if (!isset($this->entriesToCompile[$targetEntryName])) {
|
|
|
213 |
$this->entriesToCompile[$targetEntryName] = null;
|
|
|
214 |
}
|
|
|
215 |
break;
|
|
|
216 |
case $definition instanceof StringDefinition:
|
|
|
217 |
$entryName = $this->compileValue($definition->getName());
|
|
|
218 |
$expression = $this->compileValue($definition->getExpression());
|
|
|
219 |
$code = 'return \DI\Definition\StringDefinition::resolveExpression(' . $entryName . ', ' . $expression . ', $this->delegateContainer);';
|
|
|
220 |
break;
|
|
|
221 |
case $definition instanceof EnvironmentVariableDefinition:
|
|
|
222 |
$variableName = $this->compileValue($definition->getVariableName());
|
|
|
223 |
$isOptional = $this->compileValue($definition->isOptional());
|
|
|
224 |
$defaultValue = $this->compileValue($definition->getDefaultValue());
|
|
|
225 |
$code = <<<PHP
|
|
|
226 |
\$value = \$_ENV[$variableName] ?? \$_SERVER[$variableName] ?? getenv($variableName);
|
|
|
227 |
if (false !== \$value) return \$value;
|
|
|
228 |
if (!$isOptional) {
|
|
|
229 |
throw new \DI\Definition\Exception\InvalidDefinition("The environment variable '{$definition->getVariableName()}' has not been defined");
|
|
|
230 |
}
|
|
|
231 |
return $defaultValue;
|
|
|
232 |
PHP;
|
|
|
233 |
break;
|
|
|
234 |
case $definition instanceof ArrayDefinition:
|
|
|
235 |
try {
|
|
|
236 |
$code = 'return ' . $this->compileValue($definition->getValues()) . ';';
|
|
|
237 |
} catch (\Exception $e) {
|
|
|
238 |
throw new DependencyException(sprintf(
|
|
|
239 |
'Error while compiling %s. %s',
|
|
|
240 |
$definition->getName(),
|
|
|
241 |
$e->getMessage()
|
|
|
242 |
), 0, $e);
|
|
|
243 |
}
|
|
|
244 |
break;
|
|
|
245 |
case $definition instanceof ObjectDefinition:
|
|
|
246 |
$compiler = new ObjectCreationCompiler($this);
|
|
|
247 |
$code = $compiler->compile($definition);
|
|
|
248 |
$code .= "\n return \$object;";
|
|
|
249 |
break;
|
|
|
250 |
case $definition instanceof DecoratorDefinition:
|
|
|
251 |
$decoratedDefinition = $definition->getDecoratedDefinition();
|
|
|
252 |
if (! $decoratedDefinition instanceof Definition) {
|
|
|
253 |
if (! $definition->getName()) {
|
|
|
254 |
throw new InvalidDefinition('Decorators cannot be nested in another definition');
|
|
|
255 |
}
|
|
|
256 |
throw new InvalidDefinition(sprintf(
|
|
|
257 |
'Entry "%s" decorates nothing: no previous definition with the same name was found',
|
|
|
258 |
$definition->getName()
|
|
|
259 |
));
|
|
|
260 |
}
|
|
|
261 |
$code = sprintf(
|
|
|
262 |
'return call_user_func(%s, %s, $this->delegateContainer);',
|
|
|
263 |
$this->compileValue($definition->getCallable()),
|
|
|
264 |
$this->compileValue($decoratedDefinition)
|
|
|
265 |
);
|
|
|
266 |
break;
|
|
|
267 |
case $definition instanceof FactoryDefinition:
|
|
|
268 |
$value = $definition->getCallable();
|
|
|
269 |
|
|
|
270 |
// Custom error message to help debugging
|
|
|
271 |
$isInvokableClass = is_string($value) && class_exists($value) && method_exists($value, '__invoke');
|
|
|
272 |
if ($isInvokableClass && !$this->autowiringEnabled) {
|
|
|
273 |
throw new InvalidDefinition(sprintf(
|
|
|
274 |
'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.',
|
|
|
275 |
$entryName
|
|
|
276 |
));
|
|
|
277 |
}
|
|
|
278 |
|
|
|
279 |
$definitionParameters = '';
|
|
|
280 |
if (!empty($definition->getParameters())) {
|
|
|
281 |
$definitionParameters = ', ' . $this->compileValue($definition->getParameters());
|
|
|
282 |
}
|
|
|
283 |
|
|
|
284 |
$code = sprintf(
|
|
|
285 |
'return $this->resolveFactory(%s, %s%s);',
|
|
|
286 |
$this->compileValue($value),
|
|
|
287 |
var_export($entryName, true),
|
|
|
288 |
$definitionParameters
|
|
|
289 |
);
|
|
|
290 |
|
|
|
291 |
break;
|
|
|
292 |
default:
|
|
|
293 |
// This case should not happen (so it cannot be tested)
|
|
|
294 |
throw new \Exception('Cannot compile definition of type ' . $definition::class);
|
|
|
295 |
}
|
|
|
296 |
|
|
|
297 |
$this->methods[$methodName] = $code;
|
|
|
298 |
|
|
|
299 |
return $methodName;
|
|
|
300 |
}
|
|
|
301 |
|
|
|
302 |
public function compileValue(mixed $value) : string
|
|
|
303 |
{
|
|
|
304 |
// Check that the value can be compiled
|
|
|
305 |
$errorMessage = $this->isCompilable($value);
|
|
|
306 |
if ($errorMessage !== true) {
|
|
|
307 |
throw new InvalidDefinition($errorMessage);
|
|
|
308 |
}
|
|
|
309 |
|
|
|
310 |
if ($value instanceof Definition) {
|
|
|
311 |
// Give it an arbitrary unique name
|
|
|
312 |
$subEntryName = 'subEntry' . (++$this->subEntryCounter);
|
|
|
313 |
// Compile the sub-definition in another method
|
|
|
314 |
$methodName = $this->compileDefinition($subEntryName, $value);
|
|
|
315 |
|
|
|
316 |
// The value is now a method call to that method (which returns the value)
|
|
|
317 |
return "\$this->$methodName()";
|
|
|
318 |
}
|
|
|
319 |
|
|
|
320 |
if (is_array($value)) {
|
|
|
321 |
$value = array_map(function ($value, $key) {
|
|
|
322 |
$compiledValue = $this->compileValue($value);
|
|
|
323 |
$key = var_export($key, true);
|
|
|
324 |
|
|
|
325 |
return " $key => $compiledValue,\n";
|
|
|
326 |
}, $value, array_keys($value));
|
|
|
327 |
$value = implode('', $value);
|
|
|
328 |
|
|
|
329 |
return "[\n$value ]";
|
|
|
330 |
}
|
|
|
331 |
|
|
|
332 |
if ($value instanceof \Closure) {
|
|
|
333 |
return $this->compileClosure($value);
|
|
|
334 |
}
|
|
|
335 |
|
|
|
336 |
return var_export($value, true);
|
|
|
337 |
}
|
|
|
338 |
|
|
|
339 |
private function createCompilationDirectory(string $directory) : void
|
|
|
340 |
{
|
|
|
341 |
if (!is_dir($directory) && !@mkdir($directory, 0777, true) && !is_dir($directory)) {
|
|
|
342 |
throw new InvalidArgumentException(sprintf('Compilation directory does not exist and cannot be created: %s.', $directory));
|
|
|
343 |
}
|
|
|
344 |
if (!is_writable($directory)) {
|
|
|
345 |
throw new InvalidArgumentException(sprintf('Compilation directory is not writable: %s.', $directory));
|
|
|
346 |
}
|
|
|
347 |
}
|
|
|
348 |
|
|
|
349 |
/**
|
|
|
350 |
* @return string|true If true is returned that means that the value is compilable.
|
|
|
351 |
*/
|
|
|
352 |
private function isCompilable($value) : string|bool
|
|
|
353 |
{
|
|
|
354 |
if ($value instanceof ValueDefinition) {
|
|
|
355 |
return $this->isCompilable($value->getValue());
|
|
|
356 |
}
|
|
|
357 |
if (($value instanceof DecoratorDefinition) && empty($value->getName())) {
|
|
|
358 |
return 'Decorators cannot be nested in another definition';
|
|
|
359 |
}
|
|
|
360 |
// All other definitions are compilable
|
|
|
361 |
if ($value instanceof Definition) {
|
|
|
362 |
return true;
|
|
|
363 |
}
|
|
|
364 |
if ($value instanceof \Closure) {
|
|
|
365 |
return true;
|
|
|
366 |
}
|
|
|
367 |
/** @psalm-suppress UndefinedClass */
|
|
|
368 |
if ((\PHP_VERSION_ID >= 80100) && ($value instanceof \UnitEnum)) {
|
|
|
369 |
return true;
|
|
|
370 |
}
|
|
|
371 |
if (is_object($value)) {
|
|
|
372 |
return 'An object was found but objects cannot be compiled';
|
|
|
373 |
}
|
|
|
374 |
if (is_resource($value)) {
|
|
|
375 |
return 'A resource was found but resources cannot be compiled';
|
|
|
376 |
}
|
|
|
377 |
|
|
|
378 |
return true;
|
|
|
379 |
}
|
|
|
380 |
|
|
|
381 |
/**
|
|
|
382 |
* @throws InvalidDefinition
|
|
|
383 |
*/
|
|
|
384 |
private function compileClosure(\Closure $closure) : string
|
|
|
385 |
{
|
|
|
386 |
$reflector = new ReflectionClosure($closure);
|
|
|
387 |
|
|
|
388 |
if ($reflector->getUseVariables()) {
|
|
|
389 |
throw new InvalidDefinition('Cannot compile closures which import variables using the `use` keyword');
|
|
|
390 |
}
|
|
|
391 |
|
|
|
392 |
if ($reflector->isBindingRequired() || $reflector->isScopeRequired()) {
|
|
|
393 |
throw new InvalidDefinition('Cannot compile closures which use $this or self/static/parent references');
|
|
|
394 |
}
|
|
|
395 |
|
|
|
396 |
// Force all closures to be static (add the `static` keyword), i.e. they can't use
|
|
|
397 |
// $this, which makes sense since their code is copied into another class.
|
|
|
398 |
$code = ($reflector->isStatic() ? '' : 'static ') . $reflector->getCode();
|
|
|
399 |
|
|
|
400 |
return trim($code, "\t\n\r;");
|
|
|
401 |
}
|
|
|
402 |
}
|