Passed
Pull Request — master (#78)
by Dmitriy
07:47
created

ArrayDefinitionBuilder::isIntegerIndexed()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7.392

Importance

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