Factory   A
last analyzed

Complexity

Total Complexity 16

Size/Duplication

Total Lines 145
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 33
dl 0
loc 145
ccs 36
cts 36
cp 1
rs 10
c 4
b 0
f 0
wmc 16

6 Methods

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