Completed
Push — master ( fb3de7...c76a36 )
by Andrii
11:38
created

AbstractContainer::buildWithDefinition()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 22
rs 8.8333
c 0
b 0
f 0
cc 7
nc 9
nop 3
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\di;
9
10
use Closure;
11
use Psr\Container\ContainerInterface;
12
use ReflectionClass;
13
use SplObjectStorage;
14
use yii\di\contracts\DeferredServiceProviderInterface;
15
use yii\di\contracts\ServiceProviderInterface;
16
use yii\di\exceptions\CircularReferenceException;
17
use yii\di\exceptions\InvalidConfigException;
18
use yii\di\exceptions\NotFoundException;
19
use yii\di\exceptions\NotInstantiableException;
20
21
/**
22
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
23
 *
24
 * @author Alexander Makarov <[email protected]>
25
 * @since 1.0
26
 */
27
abstract class AbstractContainer implements ContainerInterface
28
{
29
    /**
30
     * @var ContainerInterface
31
     */
32
    protected $parent;
33
    /**
34
     * @var array object definitions indexed by their types
35
     */
36
    private $definitions;
37
    /**
38
     * @var ReflectionClass[] cached ReflectionClass objects indexed by class/interface names
39
     */
40
    private $reflections = [];
41
    /**
42
     * @var array cached dependencies indexed by class/interface names. Each class name
43
     * is associated with a list of constructor parameter types or default values.
44
     */
45
    private $dependencies = [];
46
    /**
47
     * @var array used to collect ids instantiated during build
48
     * to detect circular references
49
     */
50
    private $building = [];
51
    /**
52
     * @var array used to collect ids during dereferencing
53
     * to detect circular references
54
     */
55
    private $dereferencing = [];
56
    /**
57
     * @var contracts\DeferredServiceProviderInterface[]|\SplObjectStorage list of providers
58
     * deferred to register till their services would be requested
59
     */
60
    private $deferredProviders;
61
    /**
62
     * @var Injector injector with this container.
63
     */
64
    protected $injector;
65
66
    /**
67
     * Container constructor.
68
     *
69
     * @param array $definitions
70
     * @param Container|null $parent
71
     *
72
     * @throws InvalidConfigException
73
     * @throws NotInstantiableException
74
     */
75
    public function __construct(array $definitions = [], Container $parent = null)
76
    {
77
        if (isset($definitions['providers'])) {
78
            $providers = $definitions['providers'];
79
            unset($definitions['providers']);
80
        } else {
81
            $providers = [];
82
        }
83
        $this->definitions = $definitions;
84
        $this->parent = $parent;
85
86
        $this->deferredProviders = new SplObjectStorage();
87
        foreach ($providers as $provider) {
88
            $this->addProvider($provider);
89
        }
90
    }
91
92
    /**
93
     * Returns original definition by id.
94
     *
95
     * @param string $id
96
     * @return null|array|object|Closure null if not defined
97
     */
98
    public function getDefinition($id)
99
    {
100
        return $this->definitions[$id] ?? null;
101
    }
102
103
    /**
104
     * Creates new instance by either interface name or alias.
105
     *
106
     * @param string $id the interface name or an alias name (e.g. `foo`) that was previously registered via [[set()]].
107
     * @param array $config
108
     * @return object new built instance of the specified class.
109
     * @throws CircularReferenceException
110
     * @throws InvalidConfigException
111
     * @throws NotFoundException if there is nothing registered with alias or interface specified
112
     * @throws NotInstantiableException
113
     */
114
    public function build($id, array $config = [])
115
    {
116
        $id = $this->dereference($id);
117
118
        if (isset($this->building[$id])) {
119
            throw new CircularReferenceException(sprintf(
120
                'Circular reference to "%s" detected while building: %s; dereferencing: %s',
121
                $id,
122
                implode(',', array_keys($this->building)),
123
                implode(',', array_keys($this->dereferencing))
124
            ));
125
        }
126
        $this->building[$id] = 1;
127
128
        $this->registerProviderIfDeferredFor($id);
129
130
        $object = isset($this->definitions[$id])
131
            ? $this->buildWithDefinition($id, $config, $this->definitions[$id])
132
            : $this->buildWithoutDefinition($id, $config)
133
        ;
134
135
        unset($this->building[$id]);
136
137
        return $object;
138
    }
139
140
    /**
141
     * Creates new instance without definition in container.
142
     *
143
     * @param string $id the interface name or an alias name (e.g. `foo`) that was previously registered via [[set()]].
144
     * @param array $config
145
     * @return object new built instance
146
     * @throws CircularReferenceException
147
     * @throws InvalidConfigException
148
     * @throws NotFoundException
149
     * @throws NotInstantiableException
150
     */
151
    protected function buildWithoutDefinition($id, array $config = [])
152
    {
153
        if (isset($config['__class'])) {
154
            return $this->buildFromConfig($id, $config);
155
        }
156
157
        if ($this->parent !== null) {
158
            return $this->parent->build($id, $config);
0 ignored issues
show
Bug introduced by
The method build() does not exist on Psr\Container\ContainerInterface. It seems like you code against a sub-type of said class. However, the method does not exist in yii\di\FactoryInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

158
            return $this->parent->/** @scrutinizer ignore-call */ build($id, $config);
Loading history...
159
        }
160
161
        if (class_exists($id)) {
162
            $config['__class'] = $id;
163
            return $this->buildFromConfig($id, $config);
164
        }
165
166
        throw new NotFoundException("No definition for \"$id\" found");
167
    }
168
169
    /**
170
     * Creates new instance by given config and definition.
171
     *
172
     * @param string $id the interface name or an alias name (e.g. `foo`) that was previously registered via [[set()]].
173
     * @param array $config
174
     * @param array|string|object $definition
175
     * @return object new built instance
176
     * @throws InvalidConfigException when definition type is not expected
177
     * @throws NotInstantiableException
178
     */
179
    protected function buildWithDefinition($id, array $config = [], $definition = null)
180
    {
181
        if (\is_string($definition)) {
182
            if ($definition !== $id) {
183
                return $this->build($definition, $config);
184
            }
185
            $definition = ['__class' => $definition];
186
        }
187
188
        if (\is_array($definition) && !isset($definition[0], $definition[1])) {
189
            return $this->buildFromConfig($id, array_merge($config, $definition));
190
        }
191
192
        if (\is_callable($definition)) {
193
            return $definition($this, $config);
194
        }
195
196
        if (\is_object($definition)) {
197
            return $definition;
198
        }
199
200
        throw new InvalidConfigException('Unexpected object definition type: ' . \gettype($definition));
201
    }
202
203
    /**
204
     * Register providers from {@link deferredProviders} if they provide
205
     * definition for given identifier.
206
     *
207
     * @param string $id class or identifier of a service.
208
     */
209
    protected function registerProviderIfDeferredFor($id): void
210
    {
211
        $providers = $this->deferredProviders;
212
        if ($providers->count() === 0) {
213
            return;
214
        }
215
216
        foreach ($providers as $provider) {
217
            if ($provider->hasDefinitionFor($id)) {
218
                $provider->register();
219
220
                // provider should be removed after registration to not be registered again
221
                $providers->detach($provider);
222
            }
223
        }
224
    }
225
226
    /**
227
     * Sets a definition to the container. Definition may be defined multiple ways.
228
     *
229
     * Interface name as string:
230
     *
231
     * ```php
232
     * $container->set('interface_name', EngineInterface::class);
233
     * ```
234
     *
235
     * A closure:
236
     *
237
     * ```php
238
     * $container->set('closure', function($container) {
239
     *     return new MyClass($container->get('db'));
240
     * });
241
     * ```
242
     *
243
     * A callable array:
244
     *
245
     * ```php
246
     * $container->set('static_call', [MyClass::class, 'create']);
247
     * ```
248
     *
249
     * A definition array:
250
     *
251
     * ```php
252
     * $container->set('full_definition', [
253
     *     '__class' => EngineMarkOne::class,
254
     *     '__construct()' => [42],
255
     *     'argName' => 'value',
256
     *     'setX()' => [42],
257
     * ]);
258
     * ```
259
     *
260
     * @param string $id
261
     * @param mixed $definition
262
     */
263
    public function set(string $id, $definition): void
264
    {
265
        $this->definitions[$id] = $definition;
266
    }
267
268
    /**
269
     * Sets multiple definitions at once.
270
     * @param array $config definitions indexed by their ids
271
     */
272
    public function setAll($config): void
273
    {
274
        foreach ($config as $id => $definition) {
275
            $this->set($id, $definition);
276
        }
277
    }
278
279
    /**
280
     * Returns a value indicating whether the container has the definition of the specified name.
281
     * @param string $id class name, interface name or alias name
282
     * @return bool whether the container is able to provide instance of id specified.
283
     * @throws CircularReferenceException
284
     * @see set()
285
     */
286
    public function has($id): bool
287
    {
288
        $id = $this->dereference($id);
289
290
        return isset($this->definitions[$id]);
291
    }
292
293
    /**
294
     * Follows references recursively to find the deepest ID.
295
     *
296
     * @param string $id
297
     * @return string
298
     * @throws CircularReferenceException when circular reference gets detected
299
     */
300
    protected function dereference($id): string
301
    {
302
        if ($id instanceof Reference) {
0 ignored issues
show
introduced by
$id is never a sub-type of yii\di\Reference.
Loading history...
303
            $id = $id->getId();
304
        }
305
306
        if (!isset($this->definitions[$id]) || !$this->definitions[$id] instanceof Reference) {
307
            return $id;
308
        }
309
310
        if (isset($this->dereferencing[$id])) {
311
            throw new CircularReferenceException(sprintf(
312
                'Circular reference to "%s" detected while dereferencing: %s; building: %s',
313
                $id,
314
                implode(',', array_keys($this->dereferencing)),
315
                implode(',', array_keys($this->building))
316
            ));
317
        }
318
319
        $this->dereferencing[$id] = 1;
320
        $newId = $this->dereference($this->definitions[$id]->getId());
321
        unset($this->dereferencing[$id]);
322
323
        return $newId;
324
    }
325
326
    /**
327
     * Creates an instance of the class definition with dependencies resolved
328
     * @param string $id the interface name or an alias name (e.g. `foo`) that was previously registered via [[set()]].
329
     * @param array $config
330
     * @return object the newly created instance of the specified class
331
     * @throws InvalidConfigException
332
     * @throws NotInstantiableException
333
     */
334
    protected function buildFromConfig($id, array $config)
335
    {
336
        if (empty($config['__class'])) {
337
            $config['__class'] = $id;
338
        }
339
        if (empty($config['__class'])) {
340
            throw new NotInstantiableException(var_export($config, true));
341
        }
342
        /* @var $reflection ReflectionClass */
343
        [$reflection, $dependencies] = $this->getDependencies($config['__class']);
344
        unset($config['__class']);
345
346
        if (isset($config['__construct()'])) {
347
            foreach (array_values($config['__construct()']) as $index => $param) {
348
                $dependencies[$index] = $param;
349
            }
350
            unset($config['__construct()']);
351
        }
352
353
        $dependencies = $this->resolveDependencies($dependencies, $reflection);
354
        if (!$reflection->isInstantiable()) {
355
            throw new NotInstantiableException($reflection->name);
356
        }
357
358
        $object = $reflection->newInstanceArgs($dependencies);
359
360
        $config = $this->resolveDependencies($config);
361
362
        return static::configure($object, $config);
0 ignored issues
show
Deprecated Code introduced by
The function yii\di\AbstractContainer::configure() has been deprecated: Not recommended for explicit use. Added only to support Yii 2.0 behavior. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

362
        return /** @scrutinizer ignore-deprecated */ static::configure($object, $config);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
363
    }
364
365
    /**
366
     * Does after build object initialization.
367
     * At the moment only `init()` if class implements Initiable interface.
368
     *
369
     * @param object $object
370
     * @return object
371
     */
372
    protected function initObject($object)
373
    {
374
        if ($object instanceof Initiable) {
375
            $object->init();
0 ignored issues
show
Deprecated Code introduced by
The function yii\di\Initiable::init() has been deprecated: use constructor and getters/setters instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

375
            /** @scrutinizer ignore-deprecated */ $object->init();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
376
        }
377
378
        return $object;
379
    }
380
381
    /**
382
     * Configures an object with the given configuration.
383
     * @deprecated Not recommended for explicit use. Added only to support Yii 2.0 behavior.
384
     * @param object $object the object to be configured
385
     * @param iterable $config property values and methods to call
386
     * @return object the object itself
387
     */
388
    public static function configure($object, iterable $config)
389
    {
390
        foreach ($config as $action => $arguments) {
391
            if (substr($action, -2) === '()') {
392
                // method call
393
                \call_user_func_array([$object, substr($action, 0, -2)], $arguments);
394
            } else {
395
                // property
396
                $object->$action = $arguments;
397
            }
398
        }
399
400
        return $object;
401
    }
402
403
    /**
404
     * Returns the dependencies of the specified class.
405
     * @param string $class class name, interface name or alias name
406
     * @return array the dependencies of the specified class.
407
     * @throws InvalidConfigException
408
     */
409
    protected function getDependencies($class): array
410
    {
411
        if (isset($this->reflections[$class])) {
412
            return [$this->reflections[$class], $this->dependencies[$class]];
413
        }
414
415
        $dependencies = [];
416
417
        try {
418
            $reflection = new ReflectionClass($class);
419
        } catch (\ReflectionException $e) {
420
            throw new InvalidConfigException('Failed to instantiate "' . $class . '".', 0, $e);
421
        }
422
423
        $constructor = $reflection->getConstructor();
424
        if ($constructor !== null) {
425
            foreach ($constructor->getParameters() as $param) {
426
                if ($param->isDefaultValueAvailable()) {
427
                    $dependencies[] = $param->getDefaultValue();
428
                } else {
429
                    $c = $param->getClass();
430
                    /// TODO think of disallowing `Reference(null)`
431
                    $dependencies[] = new Reference($c === null ? null : $c->getName());
432
                }
433
            }
434
        }
435
436
        $this->reflections[$class] = $reflection;
437
        $this->dependencies[$class] = $dependencies;
438
439
        return [$reflection, $dependencies];
440
    }
441
442
    /**
443
     * Resolves dependencies by replacing them with the actual object instances.
444
     * @param array $dependencies the dependencies
445
     * @param ReflectionClass $reflection the class reflection associated with the dependencies
446
     * @return array the resolved dependencies
447
     * @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
448
     */
449
    protected function resolveDependencies($dependencies, $reflection = null): array
450
    {
451
        foreach ($dependencies as $index => $dependency) {
452
            if ($dependency instanceof Reference) {
453
                if ($dependency->getId() !== null) {
454
                    $dependencies[$index] = $this->get($dependency->getId());
455
                } elseif ($reflection !== null) {
456
                    $name = $reflection->getConstructor()->getParameters()[$index]->getName();
457
                    $class = $reflection->getName();
458
                    throw new InvalidConfigException(
459
                        "Missing required parameter \"$name\" when instantiating \"$class\"."
460
                    );
461
                }
462
            }
463
        }
464
465
        return $dependencies;
466
    }
467
468
    /**
469
     * Adds service provider to the container. Unless service provider is deferred
470
     * it would be immediately registered.
471
     *
472
     * @param string|array $providerDefinition
473
     *
474
     * @throws InvalidConfigException
475
     * @throws NotInstantiableException
476
     * @see ServiceProvider
477
     * @see DeferredServiceProvider
478
     */
479
    public function addProvider($providerDefinition): void
480
    {
481
        $provider = $this->buildProvider($providerDefinition);
482
483
        if ($provider instanceof DeferredServiceProviderInterface) {
484
            $this->deferredProviders->attach($provider);
485
        } else {
486
            $provider->register();
487
        }
488
    }
489
490
    /**
491
     * Builds service provider by definition.
492
     *
493
     * @param string|array $providerDefinition class name or definition of provider.
494
     * @return ServiceProviderInterface instance of service provider;
495
     *
496
     * @throws InvalidConfigException
497
     * @throws NotInstantiableException
498
     */
499
    protected function buildProvider($providerDefinition): ServiceProviderInterface
500
    {
501
        if (\is_string($providerDefinition)) {
502
            $provider = $this->buildFromConfig(null, [
503
                '__class' => $providerDefinition,
504
                '__construct()' => [
505
                    $this,
506
                ]
507
            ]);
508
        } elseif (\is_array($providerDefinition) && isset($providerDefinition['__class'])) {
509
            $providerDefinition['__construct()'] = [
510
                $this
511
            ];
512
            $provider = $this->buildFromConfig(null, $providerDefinition);
513
        } else {
514
            throw new InvalidConfigException('Service provider definition should be a class name ' .
515
                'or array contains "__class" with a class name of provider.');
516
        }
517
518
        if (!($provider instanceof ServiceProviderInterface)) {
519
            throw new InvalidConfigException(
520
                'Service provider should be an instance of ' . ServiceProviderInterface::class
521
            );
522
        }
523
524
        return $provider;
525
    }
526
527
    /**
528
     * Returns injector.
529
     *
530
     * @return Injector
531
     */
532
    public function getInjector(): Injector
533
    {
534
        if ($this->injector === null) {
535
            $this->injector = new Injector($this);
536
        }
537
538
        return $this->injector;
539
    }
540
}
541