Passed
Pull Request — master (#156)
by Alexander
02:12
created

Factory::validateDefinitions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

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