Passed
Push — master ( 3a280d...539d3e )
by Alexander
02:49 queued 01:18
created

ArrayBuilder::getExtractor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 8
ccs 4
cts 4
cp 1
crap 2
rs 10
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\NotInstantiableException;
9
use Yiisoft\Factory\Extractors\DefinitionExtractor;
10
11
/**
12
 * Builds object by ArrayDefinition.
13
 */
14
class ArrayBuilder
15
{
16
    private static ?DefinitionExtractor $extractor = null;
17
    private static array $dependencies = [];
18
19 42
    public function build(ContainerInterface $container, ArrayDefinition $definition)
20
    {
21 42
        $class = $definition->getClass();
22 42
        $dependencies = $this->getDependencies($class);
23 39
        $parameters = $definition->getParams();
24
25 39
        if (!empty($parameters)) {
26 10
            $this->validateParameters($parameters);
27
28 8
            foreach ($parameters as $index => $parameter) {
29 8
                if ($parameter instanceof ReferenceInterface || is_array($parameter)) {
30 2
                    $this->injectParameter($dependencies, $index, $parameter);
31
                } else {
32 6
                    $this->injectParameter($dependencies, $index, new ValueDefinition($parameter));
33
                }
34
            }
35
        }
36
37 37
        $resolved = $this->resolveDependencies($container, $dependencies);
38 35
        $object = new $class(...array_values($resolved));
39 35
        return $this->configure($container, $object, $definition->getConfig());
40
    }
41
42 10
    private function validateParameters(array $parameters): void
43
    {
44 10
        $hasStringParameter = false;
45 10
        $hasIntParameter = false;
46 10
        foreach ($parameters as $index => $parameter) {
47 10
            if (is_string($index)) {
48 6
                $hasStringParameter = true;
49 6
                if ($hasIntParameter) {
50 6
                    break;
51
                }
52
            } else {
53 6
                $hasIntParameter = true;
54 6
                if ($hasStringParameter) {
55 2
                    break;
56
                }
57
            }
58
        }
59 10
        if ($hasIntParameter && $hasStringParameter) {
60 2
            throw new \InvalidArgumentException(
61 2
                'Parameters indexed by name and by position in the same array are not allowed.'
62
            );
63
        }
64 8
    }
65
66 8
    private function injectParameter(array &$dependencies, $index, $parameter): void
67
    {
68 8
        if (is_string($index)) {
69 4
            $dependencies[$index] = $parameter;
70
        } else {
71 4
            reset($dependencies);
72 4
            $dependencyIndex = 0;
73 4
            while (current($dependencies)) {
74 4
                if ($index === $dependencyIndex) {
75 4
                    $dependencies[key($dependencies)] = $parameter;
76 4
                    break;
77
                }
78 2
                next($dependencies);
79 2
                $dependencyIndex++;
80
            }
81
        }
82 8
    }
83
84
    /**
85
     * Resolves dependencies by replacing them with the actual object instances.
86
     * @param ContainerInterface $container
87
     * @param DefinitionInterface[] $dependencies the dependencies
88
     * @return array the resolved dependencies
89
     */
90 37
    private function resolveDependencies(ContainerInterface $container, array $dependencies): array
91
    {
92 37
        $result = [];
93 37
        foreach ($dependencies as $key => $dependency) {
94 16
            $result[$key] = $this->resolveDependency($container, $dependency);
95
        }
96
97 35
        return $result;
98
    }
99
100
    /**
101
     * This function resolves a dependency recursively, checking for loops.
102
     * TODO add checking for loops
103
     * @param ContainerInterface $container
104
     * @param mixed $dependency
105
     * @return mixed
106
     */
107 16
    private function resolveDependency(ContainerInterface $container, $dependency)
108
    {
109 16
        if ($dependency instanceof DefinitionInterface) {
110 16
            $dependency = $dependency->resolve($container);
111 2
        } elseif (is_array($dependency)) {
112 2
            return $this->resolveDependencies($container, $dependency);
113
        }
114
115 14
        return $dependency;
116
    }
117
118
    /**
119
     * Returns the dependencies of the specified class.
120
     * @param string $class class name, interface name or alias name
121
     * @return DefinitionInterface[] the dependencies of the specified class.
122
     * @throws NotInstantiableException
123
     * @internal
124
     */
125 42
    private function getDependencies(string $class): array
126
    {
127 42
        if (!isset(self::$dependencies[$class])) {
128 11
            self::$dependencies[$class] = $this->getExtractor()->fromClassName($class);
129
        }
130
131 39
        return self::$dependencies[$class];
132
    }
133
134 11
    private function getExtractor(): DefinitionExtractor
135
    {
136 11
        if (static::$extractor === null) {
0 ignored issues
show
Bug introduced by
Since $extractor is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $extractor to at least protected.
Loading history...
137
            // For now use hard coded extractor.
138 1
            static::$extractor = new DefinitionExtractor();
139
        }
140
141 11
        return static::$extractor;
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::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...
142
    }
143
144
    /**
145
     * Configures an object with the given configuration.
146
     * @param ContainerInterface $container
147
     * @param object $object the object to be configured
148
     * @param iterable $config property values and methods to call
149
     * @return object the object itself
150
     */
151 35
    private function configure(ContainerInterface $container, $object, iterable $config)
152
    {
153 35
        foreach ($config as $action => $arguments) {
154 5
            if (substr($action, -2) === '()') {
155
                // method call
156 5
                $setter = \call_user_func_array([$object, substr($action, 0, -2)], $arguments);
157 5
                if ($setter instanceof $object) {
158 5
                    $object = $setter;
159
                }
160
            } else {
161
                // property
162
                if ($arguments instanceof DefinitionInterface) {
163
                    $arguments = $arguments->resolve($container);
164
                }
165
                $object->$action = $arguments;
166
            }
167
        }
168
169 35
        return $object;
170
    }
171
}
172