Passed
Pull Request — master (#78)
by Dmitriy
02:32
created

ArrayDefinitionBuilder::getDependencies()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Factory\Definition;
6
7
use Psr\Container\ContainerInterface;
8
use Yiisoft\Factory\Exception\InvalidConfigException;
9
use Yiisoft\Factory\Exception\NotInstantiableException;
10
use Yiisoft\Factory\Extractor\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 42
    public static function getInstance(): self
34
    {
35 42
        if (self::$instance === null) {
36 1
            self::$instance = new self();
37
        }
38
39 42
        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\Definition\ArrayDefinitionBuilder. Consider adding an additional type-check to rule them out.
Loading history...
40
    }
41
42
    /**
43
     * @throws NotInstantiableException
44
     * @throws InvalidConfigException
45
     */
46 42
    public function build(ContainerInterface $container, ArrayDefinition $definition): object
47
    {
48 42
        $class = $definition->getClass();
49 42
        $dependencies = $this->getDependencies($class);
50 39
        $constructorArguments = $definition->getConstructorArguments();
51
52 39
        $this->injectArguments($dependencies, $constructorArguments);
53
54 38
        $resolved = DefinitionResolver::resolveArray($container, $dependencies);
55
56
        /** @psalm-suppress MixedMethodCall */
57 36
        $object = new $class(...array_values($resolved));
58
59 36
        $methodsAndProperties = DefinitionResolver::resolveArray($container, $definition->getMethodsAndProperties());
60
        /** @var mixed $arguments */
61 36
        foreach ($methodsAndProperties as $name => $arguments) {
62 12
            if (substr($name, -2) === '()') {
63 10
                $methodName = substr($name, 0, -2);
64
65
                /** @var mixed */
66 10
                $setter = call_user_func_array([$object, $methodName], $arguments);
67 10
                if ($setter instanceof $object) {
68
                    /** @var object */
69 10
                    $object = $setter;
70
                }
71 3
            } elseif (strpos($name, '@') === 0) {
72 3
                $propertyName = substr($name, 1);
73 3
                $object->$propertyName = $arguments;
74
            }
75
        }
76
77 36
        return $object;
78
    }
79
80
    /**
81
     * @psalm-param array<string, DefinitionInterface> $dependencies
82
     *
83
     * @throws InvalidConfigException
84
     */
85 39
    private function injectArguments(array &$dependencies, array $arguments): void
86
    {
87 39
        $isIntegerIndexed = $this->isIntegerIndexed($arguments);
88 38
        $dependencyIndex = 0;
89 38
        $usedArguments = [];
90 38
        $isVariadic = false;
91 38
        foreach ($dependencies as $key => &$value) {
92 36
            if ($value instanceof ParameterDefinition && $value->getParameter()->isVariadic()) {
93 17
                $isVariadic = true;
94
            }
95 36
            $index = $isIntegerIndexed ? $dependencyIndex : $key;
96 36
            if (array_key_exists($index, $arguments)) {
97 10
                $value = DefinitionResolver::ensureResolvable($arguments[$index]);
98 10
                $usedArguments[$index] = 1;
99
            }
100 36
            $dependencyIndex++;
101
        }
102 38
        unset($value);
103 38
        if ($isVariadic) {
104
            /** @var mixed $value */
105 17
            foreach ($arguments as $index => $value) {
106 5
                if (!isset($usedArguments[$index])) {
107 1
                    $dependencies[$index] = DefinitionResolver::ensureResolvable($value);
108
                }
109
            }
110
        }
111
        /** @psalm-var array<string, DefinitionInterface> $dependencies */
112 38
    }
113
114 39
    private function isIntegerIndexed(array $arguments): bool
115
    {
116 39
        $hasStringIndex = false;
117 39
        $hasIntegerIndex = false;
118
119 39
        foreach ($arguments as $index => $_argument) {
120 11
            if (is_string($index)) {
121 6
                $hasStringIndex = true;
122 6
                if ($hasIntegerIndex) {
123 6
                    break;
124
                }
125
            } else {
126 6
                $hasIntegerIndex = true;
127 6
                if ($hasStringIndex) {
128 1
                    break;
129
                }
130
            }
131
        }
132 39
        if ($hasIntegerIndex && $hasStringIndex) {
133 1
            throw new InvalidConfigException(
134 1
                'Arguments indexed both by name and by position are not allowed in the same array.'
135
            );
136
        }
137
138 38
        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 42
    private function getDependencies(string $class): array
152
    {
153 42
        if (!isset(self::$dependencies[$class])) {
154 12
            self::$dependencies[$class] = $this->getExtractor()->fromClassName($class);
155
        }
156
157 39
        return self::$dependencies[$class];
158
    }
159
160 12
    private function getExtractor(): DefinitionExtractor
161
    {
162 12
        if (self::$extractor === null) {
163
            // For now use hard coded extractor.
164 1
            self::$extractor = new DefinitionExtractor();
165
        }
166
167 12
        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\Extractor\DefinitionExtractor. Consider adding an additional type-check to rule them out.
Loading history...
168
    }
169
}
170