Passed
Push — master ( a6c2eb...033a25 )
by butschster
07:05 queued 12s
created

NamedArgumentsInstantiator::appendNamedArgs()   B

Complexity

Conditions 11
Paths 50

Size

Total Lines 75
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 37
c 2
b 0
f 0
nc 50
nop 5
dl 0
loc 75
ccs 0
cts 35
cp 0
crap 132
rs 7.3166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of Spiral Framework package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Attributes\Internal\Instantiator;
13
14
use Spiral\Attributes\Internal\Exception;
15
16
/**
17
 * @internal NamedArgumentsInstantiator is an internal library class, please do not use it in your code.
18
 * @psalm-internal Spiral\Attributes
19
 */
20
final class NamedArgumentsInstantiator extends Instantiator
21
{
22
    /**
23
     * @var string
24
     */
25
    private const ERROR_ARGUMENT_NOT_PASSED = '%s::__construct(): Argument #%d ($%s) not passed';
26
27
    /**
28
     * @var string
29
     */
30
    private const ERROR_OVERWRITE_ARGUMENT = 'Named parameter $%s overwrites previous argument';
31
32
    /**
33
     * @var string
34
     */
35
    private const ERROR_NAMED_ARG_TO_VARIADIC = 'Cannot pass named argument $%s to variadic parameter $%s in PHP < 8';
36
37
    /**
38
     * @var string
39
     */
40
    private const ERROR_UNKNOWN_ARGUMENT = 'Unknown named parameter $%s';
41
42
    /**
43
     * @var string
44
     */
45
    private const ERROR_POSITIONAL_AFTER_NAMED = 'Cannot use positional argument after named argument';
46
47
    /**
48
     * {@inheritDoc}
49
     */
50 26
    public function instantiate(\ReflectionClass $attr, array $arguments, \Reflector $context = null): object
51
    {
52 26
        if ($this->isNamedArgumentsSupported()) {
53
            try {
54 26
                return $attr->newInstanceArgs($arguments);
55 6
            } catch (\Throwable $e) {
56 6
                throw Exception::withLocation($e, $attr->getFileName(), $attr->getStartLine());
57
            }
58
        }
59
60
        $constructor = $this->getConstructor($attr);
61
62
        if ($constructor === null) {
63
            return $attr->newInstanceWithoutConstructor();
64
        }
65
66
        return $attr->newInstanceArgs(
67
            $this->resolveParameters($attr, $constructor, $arguments)
68
        );
69
    }
70
71 26
    private function isNamedArgumentsSupported(): bool
72
    {
73 26
        return \version_compare(\PHP_VERSION, '8.0') >= 0;
74
    }
75
76
    /**
77
     * @throws \Throwable
78
     */
79
    private function resolveParameters(\ReflectionClass $ctx, \ReflectionMethod $constructor, array $arguments): array
80
    {
81
        try {
82
            return $this->doResolveParameters($ctx, $constructor, $arguments);
83
        } catch (\Throwable $e) {
84
            throw Exception::withLocation($e, $constructor->getFileName(), $constructor->getStartLine());
85
        }
86
    }
87
88
    /**
89
     * @throws \Throwable
90
     */
91
    private function doResolveParameters(\ReflectionClass $ctx, \ReflectionMethod $constructor, array $arguments): array
92
    {
93
        $namedArgsBegin = $this->analyzeKeys($arguments);
94
95
        if ($namedArgsBegin === null) {
96
            // Only numeric / positional keys exist.
97
            return $arguments;
98
        }
99
100
        if ($namedArgsBegin === 0) {
101
            // Only named keys exist.
102
            $passed = [];
103
            $named = $arguments;
104
        } else {
105
            // Numeric/positional keys followed by named keys.
106
            // No need to preserve numeric keys.
107
            $passed = array_slice($arguments, 0, $namedArgsBegin);
108
            $named = array_slice($arguments, $namedArgsBegin);
109
        }
110
111
        return $this->appendNamedArgs(
112
            $ctx,
113
            $passed,
114
            $named,
115
            $namedArgsBegin,
116
            $constructor->getParameters()
117
        );
118
    }
119
120
    /**
121
     * Analyzes keys of an arguments array.
122
     *
123
     * @param array $arguments Arguments array.
124
     *        By reference. Numeric keys will be reordered.
125
     *        Before (success): Mixed numeric keys, then only string keys.
126
     *        Before (fail): Some string keys are followed by numeric keys.
127
     *        After (success): Seq. numeric keys starting from 0, then string keys.
128
     *        After (failure): Seq. numeric keys starting from 0, mixed with string keys.
129
     *
130
     * @return int|null Position of the first string key, or NULL if all keys are numeric.
131
     */
132
    private function analyzeKeys(array &$arguments): ?int
133
    {
134
        // Normalize all numeric keys, but keep string keys.
135
        $arguments = \array_merge($arguments);
136
137
        $i = 0;
138
        foreach ($arguments as $k => $_) {
139
            if ($k !== $i) {
140
                // This must be a string key.
141
                // Any further numeric keys are illegal.
142
                if (\array_key_exists($i, $arguments)) {
143
                    throw new \BadMethodCallException(self::ERROR_POSITIONAL_AFTER_NAMED);
144
                }
145
                return $i;
146
            }
147
            ++$i;
148
        }
149
150
        // All keys must be numeric.
151
        return null;
152
    }
153
154
    /**
155
     * @param array $passed Positional arguments.
156
     *        Format: $[] = $value.
157
     * @param array $named Named arguments.
158
     *        Format: $[$name] = $value.
159
     * @param int $namedArgsBegin Position of first named argument.
160
     *        This is identical to count($passed).
161
     * @param \ReflectionParameter[] $parameters Full list of parameters.
162
     *
163
     * @return array Sequential list of all parameter values.
164
     *         Format: $[] = $value.
165
     *
166
     * @throws \Throwable
167
     *   Arguments provided are incompatible with the parameters.
168
     */
169
    private function appendNamedArgs(
170
        \ReflectionClass $ctx,
171
        array $passed,
172
        array $named,
173
        int $namedArgsBegin,
174
        array $parameters
175
    ): array {
176
        // Analyze parameters.
177
        $n = count($parameters);
178
        if ($n > 0 && end($parameters)->isVariadic()) {
179
            $variadicParameter = end($parameters);
180
            // Don't include the variadic parameter in the mapping process.
181
            --$n;
182
        } else {
183
            $variadicParameter = null;
184
        }
185
186
        // Process parameters that are not already filled with positional args.
187
        // This loop will do nothing if $namedArgsBegin >= $n. That's ok.
188
        for ($i = $namedArgsBegin; $i < $n; ++$i) {
189
            $parameter = $parameters[$i];
190
            $k = $parameter->getName();
191
            if (array_key_exists($k, $named)) {
192
                $passed[] = $named[$k];
193
                unset($named[$k]);
194
            } elseif ($parameter->isDefaultValueAvailable()) {
195
                $passed[] = $parameter->getDefaultValue();
196
            } else {
197
                $message = \vsprintf(self::ERROR_ARGUMENT_NOT_PASSED, [
198
                    $ctx->getName(),
199
                    $parameter->getPosition() + 1,
200
                    $parameter->getName(),
201
                ]);
202
203
                throw new \ArgumentCountError($message);
204
            }
205
        }
206
207
        if ($named === []) {
208
            // No unknown argument names exist.
209
            return $passed;
210
        }
211
212
        // Analyze the first bad argument name, ignore the rest.
213
        reset($named);
214
        $badArgName = key($named);
215
216
        // Check collision with positional arguments.
217
        foreach ($parameters as $i => $parameter) {
218
            if ($i >= $namedArgsBegin) {
219
                break;
220
            }
221
            if ($parameter->getName() === $badArgName) {
222
                // The named argument overwrites a positional argument.
223
                $message = \sprintf(self::ERROR_OVERWRITE_ARGUMENT, $badArgName);
224
                throw new \BadMethodCallException($message);
225
            }
226
        }
227
228
        // Special handling if a variadic parameter is present.
229
        if ($variadicParameter !== null) {
230
            // The last parameter is variadic.
231
            // Since PHP 8+, variadic parameters can consume named arguments.
232
            // However, this code only runs if PHP < 8.
233
            $message = \vsprintf(self::ERROR_NAMED_ARG_TO_VARIADIC, [
234
                $badArgName,
235
                $variadicParameter->getName(),
236
            ]);
237
            throw new \BadMethodCallException($message);
238
        }
239
240
        // No variadic parameter exists.
241
        // Unknown named arguments are illegal in this case, even in PHP 8.
242
        $message = \sprintf(self::ERROR_UNKNOWN_ARGUMENT, $badArgName);
243
        throw new \BadMethodCallException($message);
244
    }
245
}
246