Passed
Push — master ( e066b9...73e2c9 )
by Alexander
02:31
created

Factory::create()   B

Complexity

Conditions 8
Paths 68

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 8
eloc 15
nc 68
nop 1
dl 0
loc 24
ccs 14
cts 14
cp 1
crap 8
rs 8.4444
c 3
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Factory;
6
7
use Psr\Container\ContainerInterface;
8
use Yiisoft\Definitions\ArrayDefinition;
9
use Yiisoft\Definitions\Contract\DefinitionInterface;
10
use Yiisoft\Definitions\Contract\ReferenceInterface;
11
use Yiisoft\Definitions\Infrastructure\DefinitionValidator;
12
use Yiisoft\Definitions\Exception\CircularReferenceException;
13
use Yiisoft\Definitions\Exception\InvalidConfigException;
14
use Yiisoft\Definitions\Exception\NotFoundException;
15
use Yiisoft\Definitions\Exception\NotInstantiableException;
16
use Yiisoft\Definitions\Infrastructure\Normalizer;
17
18
use function is_string;
19
20
/**
21
 * Factory allows creating objects passing arguments runtime.
22
 * A factory will try to use a PSR-11 compliant container to get dependencies,
23
 * but will fall back to manual instantiation
24
 * if the container cannot provide a required dependency.
25
 */
26
final class Factory
27
{
28
    private ?ContainerInterface $container;
29
    private FactoryContainer $factoryContainer;
30
31
    /**
32
     * @var bool $validate Validate definitions when set
33
     */
34
    private bool $validate;
35
36
    /**
37
     * Factory constructor.
38
     *
39
     * @psalm-param array<string, mixed> $definitions
40
     *
41
     * @throws InvalidConfigException
42
     */
43 88
    public function __construct(
44
        ContainerInterface $container = null,
45
        array $definitions = [],
46
        bool $validate = true
47
    ) {
48 88
        $this->container = $container;
49 88
        $this->factoryContainer = new FactoryContainer($container);
50 88
        $this->validate = $validate;
51 88
        $this->setMultiple($definitions);
52 86
    }
53
54
    /**
55
     * Creates a new object using the given configuration.
56
     *
57
     * You may view this method as an enhanced version of the `new` operator.
58
     * The method supports creating an object based on a class name, a configuration array or
59
     * an anonymous function.
60
     *
61
     * Below are some usage examples:
62
     *
63
     * ```php
64
     * // create an object using a class name
65
     * $object = $factory->create(\Yiisoft\Db\Connection::class);
66
     *
67
     * // create an object using a configuration array
68
     * $object = $factory->create([
69
     *     'class' => \Yiisoft\Db\Connection\Connection::class,
70
     *     '__construct()' => [
71
     *         'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
72
     *     ],
73
     *     'setUsername()' => ['root'],
74
     *     'setPassword()' => [''],
75
     *     'setCharset()' => ['utf8'],
76
     * ]);
77
     * ```
78
     *
79
     * Using [[Container|dependency injection container]], this method can also identify
80
     * dependent objects, instantiate them and inject them into the newly created object.
81
     *
82
     * @param mixed $config The object configuration. This can be specified in one of the following forms:
83
     *
84
     * - A string: representing the class name of the object to be created.
85
     *
86
     * - A configuration array: the array must contain a `class` element which is treated as the object class,
87
     *   and the rest of the name-value pairs will be used to initialize the corresponding object properties.
88
     *
89
     * - A PHP callable: either an anonymous function or an array representing a class method
90
     *   (`[$class or $object, $method]`). The callable should return a new instance of the object being created.
91
     *
92
     * @throws InvalidConfigException If the configuration is invalid.
93
     * @throws CircularReferenceException
94
     * @throws NotFoundException
95
     * @throws NotInstantiableException
96
     *
97
     * @return mixed|object The created object.
98
     *
99
     * @psalm-template T
100
     * @psalm-param mixed|class-string<T> $config
101
     * @psalm-return ($config is class-string ? T : mixed)
102
     * @psalm-suppress MixedReturnStatement
103
     */
104 85
    public function create($config)
105
    {
106 85
        if ($this->validate) {
107 81
            DefinitionValidator::validate($config);
108
        }
109
110 81
        if (is_string($config)) {
111 62
            if ($this->factoryContainer->hasDefinition($config)) {
112 59
                return $this->factoryContainer->get($config);
113
            }
114 3
            throw new NotFoundException($config);
115
        }
116
117 21
        $definition = $this->createDefinition($config);
118
119 20
        if ($definition instanceof ArrayDefinition) {
120 18
            $definition->setReferenceContainer($this->factoryContainer);
121
        }
122
        try {
123 20
            $container = ($this->container === null || $definition instanceof ReferenceInterface) ? $this->factoryContainer : $this->container;
124 20
            return $definition->resolve($container);
125
        } finally {
126 20
            if ($definition instanceof ArrayDefinition) {
127 20
                $definition->setReferenceContainer(null);
128
            }
129
        }
130
    }
131
132
    /**
133
     * Sets a definition to the factory.
134
     *
135
     * @param mixed $definition
136
     *
137
     * @throws InvalidConfigException
138
     */
139 56
    public function set(string $id, $definition): void
140
    {
141 56
        if ($this->validate) {
142 53
            DefinitionValidator::validate($definition, $id);
143
        }
144
145 53
        $this->factoryContainer->setDefinition($id, $definition);
146 53
    }
147
148
    /**
149
     * Sets multiple definitions at once.
150
     *
151
     * @param array $definitions definitions indexed by their ids
152
     *
153
     * @psalm-param array<string, mixed> $definitions
154
     *
155
     * @throws InvalidConfigException
156
     */
157 88
    public function setMultiple(array $definitions): void
158
    {
159
        /** @var mixed $definition */
160 88
        foreach ($definitions as $id => $definition) {
161 54
            $this->set($id, $definition);
162
        }
163 86
    }
164
165
    /**
166
     * @param mixed $config
167
     *
168
     * @throws InvalidConfigException
169
     */
170 21
    private function createDefinition($config): DefinitionInterface
171
    {
172 21
        $definition = Normalizer::normalize($config);
173
174
        if (
175 20
            ($definition instanceof ArrayDefinition) &&
176 20
            $this->factoryContainer->hasDefinition($definition->getClass()) &&
177 20
            ($containerDefinition = $this->factoryContainer->getDefinition($definition->getClass())) instanceof ArrayDefinition
178
        ) {
179 18
            $definition = $this->mergeDefinitions(
180 18
                $containerDefinition,
181
                $definition
182
            );
183
        }
184
185 20
        return $definition;
186
    }
187
188 18
    private function mergeDefinitions(DefinitionInterface $one, ArrayDefinition $two): DefinitionInterface
189
    {
190 18
        return $one instanceof ArrayDefinition ? $one->merge($two) : $two;
191
    }
192
}
193