Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace DI\Compiler;
6
 
7
use DI\Definition\Exception\InvalidDefinition;
8
use DI\Definition\ObjectDefinition;
9
use DI\Definition\ObjectDefinition\MethodInjection;
10
use ReflectionClass;
11
use ReflectionMethod;
12
use ReflectionParameter;
13
use ReflectionProperty;
14
 
15
/**
16
 * Compiles an object definition into native PHP code that, when executed, creates the object.
17
 *
18
 * @author Matthieu Napoli <matthieu@mnapoli.fr>
19
 */
20
class ObjectCreationCompiler
21
{
22
    public function __construct(
23
        private Compiler $compiler,
24
    ) {
25
    }
26
 
27
    public function compile(ObjectDefinition $definition) : string
28
    {
29
        $this->assertClassIsNotAnonymous($definition);
30
        $this->assertClassIsInstantiable($definition);
31
        /** @var class-string $className At this point we have checked the class is valid */
32
        $className = $definition->getClassName();
33
 
34
        // Lazy?
35
        if ($definition->isLazy()) {
36
            return $this->compileLazyDefinition($definition);
37
        }
38
 
39
        try {
40
            $classReflection = new ReflectionClass($className);
41
            $constructorArguments = $this->resolveParameters($definition->getConstructorInjection(), $classReflection->getConstructor());
42
            $dumpedConstructorArguments = array_map(function ($value) {
43
                return $this->compiler->compileValue($value);
44
            }, $constructorArguments);
45
 
46
            $code = [];
47
            $code[] = sprintf(
48
                '$object = new %s(%s);',
49
                $className,
50
                implode(', ', $dumpedConstructorArguments)
51
            );
52
 
53
            // Property injections
54
            foreach ($definition->getPropertyInjections() as $propertyInjection) {
55
                $value = $propertyInjection->getValue();
56
                $value = $this->compiler->compileValue($value);
57
 
58
                $propertyClassName = $propertyInjection->getClassName() ?: $className;
59
                $property = new ReflectionProperty($propertyClassName, $propertyInjection->getPropertyName());
60
                if ($property->isPublic() && !(\PHP_VERSION_ID >= 80100 && $property->isReadOnly())) {
61
                    $code[] = sprintf('$object->%s = %s;', $propertyInjection->getPropertyName(), $value);
62
                } else {
63
                    // Private/protected/readonly property
64
                    $code[] = sprintf(
65
                        '\DI\Definition\Resolver\ObjectCreator::setPrivatePropertyValue(%s, $object, \'%s\', %s);',
66
                        var_export($propertyInjection->getClassName(), true),
67
                        $propertyInjection->getPropertyName(),
68
                        $value
69
                    );
70
                }
71
            }
72
 
73
            // Method injections
74
            foreach ($definition->getMethodInjections() as $methodInjection) {
75
                $methodReflection = new ReflectionMethod($className, $methodInjection->getMethodName());
76
                $parameters = $this->resolveParameters($methodInjection, $methodReflection);
77
 
78
                $dumpedParameters = array_map(function ($value) {
79
                    return $this->compiler->compileValue($value);
80
                }, $parameters);
81
 
82
                $code[] = sprintf(
83
                    '$object->%s(%s);',
84
                    $methodInjection->getMethodName(),
85
                    implode(', ', $dumpedParameters)
86
                );
87
            }
88
        } catch (InvalidDefinition $e) {
89
            throw InvalidDefinition::create($definition, sprintf(
90
                'Entry "%s" cannot be compiled: %s',
91
                $definition->getName(),
92
                $e->getMessage()
93
            ));
94
        }
95
 
96
        return implode("\n        ", $code);
97
    }
98
 
99
    public function resolveParameters(?MethodInjection $definition, ?ReflectionMethod $method) : array
100
    {
101
        $args = [];
102
 
103
        if (! $method) {
104
            return $args;
105
        }
106
 
107
        $definitionParameters = $definition ? $definition->getParameters() : [];
108
 
109
        foreach ($method->getParameters() as $index => $parameter) {
110
            if (array_key_exists($index, $definitionParameters)) {
111
                // Look in the definition
112
                $value = &$definitionParameters[$index];
113
            } elseif ($parameter->isOptional()) {
114
                // If the parameter is optional and wasn't specified, we take its default value
115
                $args[] = $this->getParameterDefaultValue($parameter, $method);
116
                continue;
117
            } else {
118
                throw new InvalidDefinition(sprintf(
119
                    'Parameter $%s of %s has no value defined or guessable',
120
                    $parameter->getName(),
121
                    $this->getFunctionName($method)
122
                ));
123
            }
124
 
125
            $args[] = &$value;
126
        }
127
 
128
        return $args;
129
    }
130
 
131
    private function compileLazyDefinition(ObjectDefinition $definition) : string
132
    {
133
        $subDefinition = clone $definition;
134
        $subDefinition->setLazy(false);
135
        $subDefinition = $this->compiler->compileValue($subDefinition);
136
 
137
        /** @var class-string $className At this point we have checked the class is valid */
138
        $className = $definition->getClassName();
139
 
140
        $this->compiler->getProxyFactory()->generateProxyClass($className);
141
 
142
        return <<<STR
143
                    \$object = \$this->proxyFactory->createProxy(
144
                        '{$definition->getClassName()}',
145
                        function (&\$wrappedObject, \$proxy, \$method, \$params, &\$initializer) {
146
                            \$wrappedObject = $subDefinition;
147
                            \$initializer = null; // turning off further lazy initialization
148
                            return true;
149
                        }
150
                    );
151
            STR;
152
    }
153
 
154
    /**
155
     * Returns the default value of a function parameter.
156
     *
157
     * @throws InvalidDefinition Can't get default values from PHP internal classes and functions
158
     */
159
    private function getParameterDefaultValue(ReflectionParameter $parameter, ReflectionMethod $function) : mixed
160
    {
161
        try {
162
            return $parameter->getDefaultValue();
163
        } catch (\ReflectionException) {
164
            throw new InvalidDefinition(sprintf(
165
                'The parameter "%s" of %s has no type defined or guessable. It has a default value, '
166
                . 'but the default value can\'t be read through Reflection because it is a PHP internal class.',
167
                $parameter->getName(),
168
                $this->getFunctionName($function)
169
            ));
170
        }
171
    }
172
 
173
    private function getFunctionName(ReflectionMethod $method) : string
174
    {
175
        return $method->getName() . '()';
176
    }
177
 
178
    private function assertClassIsNotAnonymous(ObjectDefinition $definition) : void
179
    {
180
        if (str_contains($definition->getClassName(), '@')) {
181
            throw InvalidDefinition::create($definition, sprintf(
182
                'Entry "%s" cannot be compiled: anonymous classes cannot be compiled',
183
                $definition->getName()
184
            ));
185
        }
186
    }
187
 
188
    private function assertClassIsInstantiable(ObjectDefinition $definition) : void
189
    {
190
        if ($definition->isInstantiable()) {
191
            return;
192
        }
193
 
194
        $message = ! $definition->classExists()
195
            ? 'Entry "%s" cannot be compiled: the class doesn\'t exist'
196
            : 'Entry "%s" cannot be compiled: the class is not instantiable';
197
 
198
        throw InvalidDefinition::create($definition, sprintf($message, $definition->getName()));
199
    }
200
}