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;
6
 
7
use DI\Definition\Definition;
8
use DI\Definition\Exception\InvalidDefinition;
9
use DI\Definition\FactoryDefinition;
10
use DI\Definition\Helper\DefinitionHelper;
11
use DI\Definition\InstanceDefinition;
12
use DI\Definition\ObjectDefinition;
13
use DI\Definition\Resolver\DefinitionResolver;
14
use DI\Definition\Resolver\ResolverDispatcher;
15
use DI\Definition\Source\DefinitionArray;
16
use DI\Definition\Source\MutableDefinitionSource;
17
use DI\Definition\Source\ReflectionBasedAutowiring;
18
use DI\Definition\Source\SourceChain;
19
use DI\Definition\ValueDefinition;
20
use DI\Invoker\DefinitionParameterResolver;
21
use DI\Proxy\ProxyFactory;
22
use InvalidArgumentException;
23
use Invoker\Invoker;
24
use Invoker\InvokerInterface;
25
use Invoker\ParameterResolver\AssociativeArrayResolver;
26
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
27
use Invoker\ParameterResolver\DefaultValueResolver;
28
use Invoker\ParameterResolver\NumericArrayResolver;
29
use Invoker\ParameterResolver\ResolverChain;
30
use Psr\Container\ContainerInterface;
31
 
32
/**
33
 * Dependency Injection Container.
34
 *
35
 * @api
36
 *
37
 * @author Matthieu Napoli <matthieu@mnapoli.fr>
38
 */
39
class Container implements ContainerInterface, FactoryInterface, InvokerInterface
40
{
41
    /**
42
     * Map of entries that are already resolved.
43
     */
44
    protected array $resolvedEntries = [];
45
 
46
    private MutableDefinitionSource $definitionSource;
47
 
48
    private DefinitionResolver $definitionResolver;
49
 
50
    /**
51
     * Map of definitions that are already fetched (local cache).
52
     *
53
     * @var array<Definition|null>
54
     */
55
    private array $fetchedDefinitions = [];
56
 
57
    /**
58
     * Array of entries being resolved. Used to avoid circular dependencies and infinite loops.
59
     */
60
    protected array $entriesBeingResolved = [];
61
 
62
    private ?InvokerInterface $invoker = null;
63
 
64
    /**
65
     * Container that wraps this container. If none, points to $this.
66
     */
67
    protected ContainerInterface $delegateContainer;
68
 
69
    protected ProxyFactory $proxyFactory;
70
 
71
    public static function create(
72
        array $definitions
73
    ) : static {
74
        $source = new SourceChain([new ReflectionBasedAutowiring]);
75
        $source->setMutableDefinitionSource(new DefinitionArray($definitions, new ReflectionBasedAutowiring));
76
 
77
        return new static($definitions);
78
    }
79
 
80
    /**
81
     * Use `$container = new Container()` if you want a container with the default configuration.
82
     *
83
     * If you want to customize the container's behavior, you are discouraged to create and pass the
84
     * dependencies yourself, the ContainerBuilder class is here to help you instead.
85
     *
86
     * @see ContainerBuilder
87
     *
88
     * @param ContainerInterface $wrapperContainer If the container is wrapped by another container.
89
     */
90
    public function __construct(
91
        array|MutableDefinitionSource $definitions = [],
92
        ProxyFactory $proxyFactory = null,
93
        ContainerInterface $wrapperContainer = null
94
    ) {
95
        if (is_array($definitions)) {
96
            $this->definitionSource = $this->createDefaultDefinitionSource($definitions);
97
        } else {
98
            $this->definitionSource = $definitions;
99
        }
100
 
101
        $this->delegateContainer = $wrapperContainer ?: $this;
102
        $this->proxyFactory = $proxyFactory ?: new ProxyFactory;
103
        $this->definitionResolver = new ResolverDispatcher($this->delegateContainer, $this->proxyFactory);
104
 
105
        // Auto-register the container
106
        $this->resolvedEntries = [
107
            self::class => $this,
108
            ContainerInterface::class => $this->delegateContainer,
109
            FactoryInterface::class => $this,
110
            InvokerInterface::class => $this,
111
        ];
112
    }
113
 
114
    /**
115
     * Returns an entry of the container by its name.
116
     *
117
     * @template T
118
     * @param string|class-string<T> $id Entry name or a class name.
119
     *
120
     * @return mixed|T
121
     * @throws DependencyException Error while resolving the entry.
122
     * @throws NotFoundException No entry found for the given name.
123
     */
124
    public function get(string $id) : mixed
125
    {
126
        // If the entry is already resolved we return it
127
        if (isset($this->resolvedEntries[$id]) || array_key_exists($id, $this->resolvedEntries)) {
128
            return $this->resolvedEntries[$id];
129
        }
130
 
131
        $definition = $this->getDefinition($id);
132
        if (! $definition) {
133
            throw new NotFoundException("No entry or class found for '$id'");
134
        }
135
 
136
        $value = $this->resolveDefinition($definition);
137
 
138
        $this->resolvedEntries[$id] = $value;
139
 
140
        return $value;
141
    }
142
 
143
    private function getDefinition(string $name) : ?Definition
144
    {
145
        // Local cache that avoids fetching the same definition twice
146
        if (!array_key_exists($name, $this->fetchedDefinitions)) {
147
            $this->fetchedDefinitions[$name] = $this->definitionSource->getDefinition($name);
148
        }
149
 
150
        return $this->fetchedDefinitions[$name];
151
    }
152
 
153
    /**
154
     * Build an entry of the container by its name.
155
     *
156
     * This method behave like get() except resolves the entry again every time.
157
     * For example if the entry is a class then a new instance will be created each time.
158
     *
159
     * This method makes the container behave like a factory.
160
     *
161
     * @template T
162
     * @param string|class-string<T> $name       Entry name or a class name.
163
     * @param array                  $parameters Optional parameters to use to build the entry. Use this to force
164
     *                                           specific parameters to specific values. Parameters not defined in this
165
     *                                           array will be resolved using the container.
166
     *
167
     * @return mixed|T
168
     * @throws InvalidArgumentException The name parameter must be of type string.
169
     * @throws DependencyException Error while resolving the entry.
170
     * @throws NotFoundException No entry found for the given name.
171
     */
172
    public function make(string $name, array $parameters = []) : mixed
173
    {
174
        $definition = $this->getDefinition($name);
175
        if (! $definition) {
176
            // If the entry is already resolved we return it
177
            if (array_key_exists($name, $this->resolvedEntries)) {
178
                return $this->resolvedEntries[$name];
179
            }
180
 
181
            throw new NotFoundException("No entry or class found for '$name'");
182
        }
183
 
184
        return $this->resolveDefinition($definition, $parameters);
185
    }
186
 
187
    public function has(string $id) : bool
188
    {
189
        if (array_key_exists($id, $this->resolvedEntries)) {
190
            return true;
191
        }
192
 
193
        $definition = $this->getDefinition($id);
194
        if ($definition === null) {
195
            return false;
196
        }
197
 
198
        return $this->definitionResolver->isResolvable($definition);
199
    }
200
 
201
    /**
202
     * Inject all dependencies on an existing instance.
203
     *
204
     * @template T
205
     * @param object|T $instance Object to perform injection upon
206
     * @return object|T $instance Returns the same instance
207
     * @throws InvalidArgumentException
208
     * @throws DependencyException Error while injecting dependencies
209
     */
210
    public function injectOn(object $instance) : object
211
    {
212
        $className = $instance::class;
213
 
214
        // If the class is anonymous, don't cache its definition
215
        // Checking for anonymous classes is cleaner via Reflection, but also slower
216
        $objectDefinition = str_contains($className, '@anonymous')
217
            ? $this->definitionSource->getDefinition($className)
218
            : $this->getDefinition($className);
219
 
220
        if (! $objectDefinition instanceof ObjectDefinition) {
221
            return $instance;
222
        }
223
 
224
        $definition = new InstanceDefinition($instance, $objectDefinition);
225
 
226
        $this->definitionResolver->resolve($definition);
227
 
228
        return $instance;
229
    }
230
 
231
    /**
232
     * Call the given function using the given parameters.
233
     *
234
     * Missing parameters will be resolved from the container.
235
     *
236
     * @param callable|array|string $callable Function to call.
237
     * @param array    $parameters Parameters to use. Can be indexed by the parameter names
238
     *                             or not indexed (same order as the parameters).
239
     *                             The array can also contain DI definitions, e.g. DI\get().
240
     *
241
     * @return mixed Result of the function.
242
     */
243
    public function call($callable, array $parameters = []) : mixed
244
    {
245
        return $this->getInvoker()->call($callable, $parameters);
246
    }
247
 
248
    /**
249
     * Define an object or a value in the container.
250
     *
251
     * @param string $name Entry name
252
     * @param mixed|DefinitionHelper $value Value, use definition helpers to define objects
253
     */
254
    public function set(string $name, mixed $value) : void
255
    {
256
        if ($value instanceof DefinitionHelper) {
257
            $value = $value->getDefinition($name);
258
        } elseif ($value instanceof \Closure) {
259
            $value = new FactoryDefinition($name, $value);
260
        }
261
 
262
        if ($value instanceof ValueDefinition) {
263
            $this->resolvedEntries[$name] = $value->getValue();
264
        } elseif ($value instanceof Definition) {
265
            $value->setName($name);
266
            $this->setDefinition($name, $value);
267
        } else {
268
            $this->resolvedEntries[$name] = $value;
269
        }
270
    }
271
 
272
    /**
273
     * Get defined container entries.
274
     *
275
     * @return string[]
276
     */
277
    public function getKnownEntryNames() : array
278
    {
279
        $entries = array_unique(array_merge(
280
            array_keys($this->definitionSource->getDefinitions()),
281
            array_keys($this->resolvedEntries)
282
        ));
283
        sort($entries);
284
 
285
        return $entries;
286
    }
287
 
288
    /**
289
     * Get entry debug information.
290
     *
291
     * @param string $name Entry name
292
     *
293
     * @throws InvalidDefinition
294
     * @throws NotFoundException
295
     */
296
    public function debugEntry(string $name) : string
297
    {
298
        $definition = $this->definitionSource->getDefinition($name);
299
        if ($definition instanceof Definition) {
300
            return (string) $definition;
301
        }
302
 
303
        if (array_key_exists($name, $this->resolvedEntries)) {
304
            return $this->getEntryType($this->resolvedEntries[$name]);
305
        }
306
 
307
        throw new NotFoundException("No entry or class found for '$name'");
308
    }
309
 
310
    /**
311
     * Get formatted entry type.
312
     */
313
    private function getEntryType(mixed $entry) : string
314
    {
315
        if (is_object($entry)) {
316
            return sprintf("Object (\n    class = %s\n)", $entry::class);
317
        }
318
 
319
        if (is_array($entry)) {
320
            return preg_replace(['/^array \(/', '/\)$/'], ['[', ']'], var_export($entry, true));
321
        }
322
 
323
        if (is_string($entry)) {
324
            return sprintf('Value (\'%s\')', $entry);
325
        }
326
 
327
        if (is_bool($entry)) {
328
            return sprintf('Value (%s)', $entry === true ? 'true' : 'false');
329
        }
330
 
331
        return sprintf('Value (%s)', is_scalar($entry) ? (string) $entry : ucfirst(gettype($entry)));
332
    }
333
 
334
    /**
335
     * Resolves a definition.
336
     *
337
     * Checks for circular dependencies while resolving the definition.
338
     *
339
     * @throws DependencyException Error while resolving the entry.
340
     */
341
    private function resolveDefinition(Definition $definition, array $parameters = []) : mixed
342
    {
343
        $entryName = $definition->getName();
344
 
345
        // Check if we are already getting this entry -> circular dependency
346
        if (isset($this->entriesBeingResolved[$entryName])) {
347
            throw new DependencyException("Circular dependency detected while trying to resolve entry '$entryName'");
348
        }
349
        $this->entriesBeingResolved[$entryName] = true;
350
 
351
        // Resolve the definition
352
        try {
353
            $value = $this->definitionResolver->resolve($definition, $parameters);
354
        } finally {
355
            unset($this->entriesBeingResolved[$entryName]);
356
        }
357
 
358
        return $value;
359
    }
360
 
361
    protected function setDefinition(string $name, Definition $definition) : void
362
    {
363
        // Clear existing entry if it exists
364
        if (array_key_exists($name, $this->resolvedEntries)) {
365
            unset($this->resolvedEntries[$name]);
366
        }
367
        $this->fetchedDefinitions = []; // Completely clear this local cache
368
 
369
        $this->definitionSource->addDefinition($definition);
370
    }
371
 
372
    private function getInvoker() : InvokerInterface
373
    {
374
        if (! $this->invoker) {
375
            $parameterResolver = new ResolverChain([
376
                new DefinitionParameterResolver($this->definitionResolver),
377
                new NumericArrayResolver,
378
                new AssociativeArrayResolver,
379
                new DefaultValueResolver,
380
                new TypeHintContainerResolver($this->delegateContainer),
381
            ]);
382
 
383
            $this->invoker = new Invoker($parameterResolver, $this);
384
        }
385
 
386
        return $this->invoker;
387
    }
388
 
389
    private function createDefaultDefinitionSource(array $definitions) : SourceChain
390
    {
391
        $autowiring = new ReflectionBasedAutowiring;
392
        $source = new SourceChain([$autowiring]);
393
        $source->setMutableDefinitionSource(new DefinitionArray($definitions, $autowiring));
394
 
395
        return $source;
396
    }
397
}