Passed
Pull Request — master (#78)
by Alexander
02:06
created

ArrayDefinitionBuilder::isIntegerIndexed()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 10
nop 1
dl 0
loc 25
ccs 15
cts 15
cp 1
crap 7
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Factory\Definitions;
6
7
use Psr\Container\ContainerInterface;
8
use Yiisoft\Factory\Exceptions\InvalidConfigException;
9
use Yiisoft\Factory\Exceptions\NotInstantiableException;
10
use Yiisoft\Factory\Extractors\DefinitionExtractor;
11
12
use function array_key_exists;
13
use function call_user_func_array;
14
use function is_string;
15
16
/**
17
 * @internal Builds object by ArrayDefinition.
18
 */
19
final class ArrayDefinitionBuilder
20
{
21
    private static ?self $instance = null;
22
    private static ?DefinitionExtractor $extractor = null;
23
24
    /**
25
     * @psalm-var array<string, array<string, DefinitionInterface>>
26
     */
27
    private static array $dependencies = [];
28
29 1
    private function __construct()
30
    {
31 1
    }
32
33 38
    public static function getInstance(): self
34
    {
35 38
        if (self::$instance === null) {
36 1
            self::$instance = new self();
37
        }
38
39 38
        return self::$instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::instance could return the type null which is incompatible with the type-hinted return Yiisoft\Factory\Definitions\ArrayDefinitionBuilder. Consider adding an additional type-check to rule them out.
Loading history...
40
    }
41
42
    /**
43
     * @throws NotInstantiableException
44
     * @throws InvalidConfigException
45
     */
46 38
    public function build(ContainerInterface $container, ArrayDefinition $definition): object
47
    {
48 38
        $class = $definition->getClass();
49 38
        $dependencies = $this->getDependencies($class);
50 38
        $constructorParameters = $definition->getConstructorParameters();
51
52 38
        $this->injectParameters($dependencies, $constructorParameters);
53
54 37
        $resolved = DefinitionResolver::resolveArray($container, $dependencies);
55
56
        /** @psalm-suppress MixedMethodCall */
57 35
        $object = new $class(...array_values($resolved));
58
59 35
        $properties = DefinitionResolver::resolveArray($container, $definition->getSetProperties());
60
        /** @var mixed $value */
61 35
        foreach ($properties as $property => $value) {
62 2
            $object->$property = $value;
63
        }
64
65
        /** @psalm-var array<string,array> $calls */
66 35
        $calls = DefinitionResolver::resolveArray($container, $definition->getCallMethods());
67 35
        foreach ($calls as $method => $arguments) {
68
            /** @var mixed */
69 9
            $setter = call_user_func_array([$object, $method], $arguments);
70 9
            if ($setter instanceof $object) {
71
                /** @var object */
72 2
                $object = $setter;
73
            }
74
        }
75
76 35
        return $object;
77
    }
78
79
    /**
80
     * @psalm-param array<string, DefinitionInterface> $dependencies
81
     *
82
     * @throws InvalidConfigException
83
     */
84 38
    private function injectParameters(array &$dependencies, array $parameters): void
85
    {
86 38
        $isIntegerIndexed = $this->isIntegerIndexed($parameters);
87 37
        $dependencyIndex = 0;
88 37
        $usedParameters = [];
89 37
        $isVariadic = false;
90 37
        foreach ($dependencies as $key => &$value) {
91 36
            if ($value instanceof ParameterDefinition && $value->getParameter()->isVariadic()) {
92 17
                $isVariadic = true;
93
            }
94 36
            $index = $isIntegerIndexed ? $dependencyIndex : $key;
95 36
            if (array_key_exists($index, $parameters)) {
96 10
                $value = DefinitionResolver::ensureResolvable($parameters[$index]);
97 10
                $usedParameters[$index] = 1;
98
            }
99 36
            $dependencyIndex++;
100
        }
101 37
        unset($value);
102 37
        if ($isVariadic) {
103
            /** @var mixed $value */
104 17
            foreach ($parameters as $index => $value) {
105 5
                if (!isset($usedParameters[$index])) {
106 1
                    $dependencies[$index] = DefinitionResolver::ensureResolvable($value);
107
                }
108
            }
109
        }
110
        /** @psalm-var array<string, DefinitionInterface> $dependencies */
111 37
    }
112
113 38
    private function isIntegerIndexed(array $parameters): bool
114
    {
115 38
        $hasStringIndex = false;
116 38
        $hasIntegerIndex = false;
117
118 38
        foreach ($parameters as $index => $_parameter) {
119 11
            if (is_string($index)) {
120 6
                $hasStringIndex = true;
121 6
                if ($hasIntegerIndex) {
122 6
                    break;
123
                }
124
            } else {
125 6
                $hasIntegerIndex = true;
126 6
                if ($hasStringIndex) {
127 1
                    break;
128
                }
129
            }
130
        }
131 38
        if ($hasIntegerIndex && $hasStringIndex) {
132 1
            throw new InvalidConfigException(
133 1
                'Parameters indexed both by name and by position are not allowed in the same array.'
134
            );
135
        }
136
137 37
        return $hasIntegerIndex;
138
    }
139
140
    /**
141
     * Returns the dependencies of the specified class.
142
     *
143
     * @param class-string $class Class name or interface name.
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
144
     *
145
     * @throws NotInstantiableException
146
     *
147
     * @return DefinitionInterface[] The dependencies of the specified class.
148
     * @psalm-return array<string, DefinitionInterface>
149
     */
150 38
    private function getDependencies(string $class): array
151
    {
152 38
        if (!isset(self::$dependencies[$class])) {
153 6
            self::$dependencies[$class] = $this->getExtractor()->fromClassName($class);
154
        }
155
156 38
        return self::$dependencies[$class];
157
    }
158
159 6
    private function getExtractor(): DefinitionExtractor
160
    {
161 6
        if (self::$extractor === null) {
162
            // For now use hard coded extractor.
163 1
            self::$extractor = new DefinitionExtractor();
164
        }
165
166 6
        return self::$extractor;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::extractor could return the type null which is incompatible with the type-hinted return Yiisoft\Factory\Extractors\DefinitionExtractor. Consider adding an additional type-check to rule them out.
Loading history...
167
    }
168
}
169