Passed
Pull Request — master (#146)
by Dmitriy
11:01
created

Factory::createDefinition()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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