Passed
Pull Request — master (#151)
by Alexander
02:14
created

Factory::create()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

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