Passed
Pull Request — master (#57)
by Dmitriy
12:15
created

DefinitionValidator::validateArrayDefinition()   C

Complexity

Conditions 12
Paths 63

Size

Total Lines 64
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 12

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 38
nc 63
nop 2
dl 0
loc 64
ccs 31
cts 31
cp 1
crap 12
rs 6.9666
c 2
b 0
f 0

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
declare(strict_types=1);
4
5
namespace Yiisoft\Definitions\Helpers;
6
7
use ReflectionClass;
8
use ReflectionMethod;
9
use ReflectionProperty;
10
use Yiisoft\Definitions\ArrayDefinition;
11
use Yiisoft\Definitions\Contract\DefinitionInterface;
12
use Yiisoft\Definitions\Contract\ReferenceInterface;
13
use Yiisoft\Definitions\Exception\InvalidConfigException;
14
15
use function is_array;
16
use function is_callable;
17
use function is_object;
18
use function is_string;
19
20
/**
21
 * Definition validator checks if definition is valid.
22
 */
23
final class DefinitionValidator
24
{
25
    /**
26
     * Validates that definition is valid. Throws exception otherwise.
27
     *
28
     * @param mixed $definition Definition to validate.
29
     *
30
     * @throws InvalidConfigException If definition is not valid.
31
     */
32 31
    public static function validate(mixed $definition, ?string $id = null): void
33
    {
34
        // Reference or ready object
35 31
        if (is_object($definition) && self::isValidObject($definition)) {
36
            return;
37
        }
38
39
        // Class
40 31
        if (is_string($definition)) {
41 3
            self::validateClassName($definition);
42
            return;
43
        }
44
45
        // Callable definition
46 28
        if ($definition !== '' && is_callable($definition, true)) {
47
            return;
48
        }
49
50
        // Array definition
51 28
        if (is_array($definition)) {
52 27
            self::validateArrayDefinition($definition, $id);
53
            return;
54
        }
55
56 1
        throw new InvalidConfigException(
57
            'Invalid definition: '
58 1
            . ($definition === '' ? 'empty string.' : var_export($definition, true))
59
        );
60
    }
61
62
    /**
63
     * Validates that array definition is valid. Throws exception otherwise.
64
     *
65
     * @param array $definition Array definition to validate.
66
     *
67
     * @throws InvalidConfigException If definition is not valid.
68
     */
69 30
    public static function validateArrayDefinition(array $definition, ?string $id = null): void
70
    {
71
        /** @var class-string $className */
72 30
        $className = $definition[ArrayDefinition::CLASS_NAME] ?? $id ?? throw new InvalidConfigException(
73
            'Invalid definition: no class name specified.'
74
        );
75 29
        self::validateClassName($className);
76 27
        $classReflection = new ReflectionClass($className);
77 27
        $classPublicMethods = [];
78 27
        foreach ($classReflection->getMethods() as $reflectionMethod) {
79 23
            if (($reflectionMethod->getModifiers() & ReflectionMethod::IS_PUBLIC) !== 0 && !self::isMagicMethod(
80 23
                    $reflectionMethod->getName()
81
                )) {
82 22
                $classPublicMethods[] = $reflectionMethod->getName();
83
            }
84
        }
85 27
        $classPublicProperties = [];
86 27
        foreach ($classReflection->getProperties() as $reflectionProperty) {
87 22
            if (($reflectionProperty->getModifiers() & ReflectionProperty::IS_PUBLIC) !== 0) {
88 20
                $classPublicProperties[] = $reflectionProperty->getName();
89
            }
90
        }
91
92 27
        foreach ($definition as $key => $value) {
93 26
            if (!is_string($key)) {
94 1
                throw new InvalidConfigException(
95 1
                    sprintf(
96
                        'Invalid definition: invalid key in array definition. Allow only string keys, got %d.',
97
                        $key,
98
                    ),
99
                );
100
            }
101
102
            // Class
103 26
            if ($key === ArrayDefinition::CLASS_NAME) {
104 26
                continue;
105
            }
106
107
            // Constructor arguments
108 23
            if ($key === ArrayDefinition::CONSTRUCTOR) {
109 13
                self::validateConstructor($value);
110 11
                continue;
111
            }
112
113
            // Methods and properties
114 21
            if (str_ends_with($key, '()')) {
115 17
                self::validateMethod($key, $classReflection, $classPublicMethods, $className, $value);
116 11
                continue;
117
            }
118 15
            if (str_starts_with($key, '$')) {
119 15
                self::validateProperty($key, $classReflection, $classPublicProperties, $className);
120 11
                continue;
121
            }
122
123 11
            $possibleOptionsMessage = self::generatePossibleMessage(
124
                $key,
125
                $classPublicMethods,
126
                $classPublicProperties,
127
                $classReflection,
128
                $className
129
            );
130
131 11
            throw new InvalidConfigException(
132 11
                "Invalid definition: key \"$key\" is not allowed. $possibleOptionsMessage",
133
            );
134
        }
135
    }
136
137
    /**
138
     * Deny `DefinitionInterface`, exclude `ReferenceInterface`
139
     */
140 1
    private static function isValidObject(object $value): bool
141
    {
142 1
        return !($value instanceof DefinitionInterface) || $value instanceof ReferenceInterface;
143
    }
144
145 32
    private static function validateClassName(mixed $class): void
146
    {
147 32
        if (!is_string($class)) {
148 1
            throw new InvalidConfigException(
149 1
                sprintf(
150
                    'Invalid definition: class name must be a non-empty string, got %s.',
151 1
                    get_debug_type($class),
152
                )
153
            );
154
        }
155 31
        if (trim($class) === '') {
156 3
            throw new InvalidConfigException(
157 3
                sprintf(
158
                    'Invalid definition: class name must be a non-empty string.',
159
                )
160
            );
161
        }
162 28
        if (!class_exists($class)) {
163 1
            throw new InvalidConfigException(
164 1
                sprintf(
165
                    'Invalid definition: class "%s" does not exist.',
166
                    $class,
167
                ),
168
            );
169
        }
170
    }
171
172 11
    private static function generatePossibleMessage(
173
        string $key,
174
        array $classPublicMethods,
175
        array $classPublicProperties,
176
        ReflectionClass $classReflection,
177
        string $className
178
    ): string {
179 11
        $parsedKey = trim(
180 11
            strtr($key, [
181
                '()' => '',
182
                '$' => '',
183
            ])
184
        );
185 11
        if (in_array($parsedKey, $classPublicMethods, true)) {
186 3
            return sprintf(
187
                'Did you mean "%s"?',
188
                $parsedKey . '()',
189
            );
190
        }
191 8
        if (in_array($parsedKey, $classPublicProperties, true)) {
192 1
            return sprintf(
193
                'Did you mean "%s"?',
194
                '$' . $parsedKey,
195
            );
196
        }
197 7
        if ($classReflection->hasMethod($parsedKey)) {
198 1
            return sprintf(
199
                'Method "%s" must be public to be able to be called.',
200
                $className . '::' . $parsedKey . '()',
201
            );
202
        }
203 6
        if ($classReflection->hasProperty($parsedKey)) {
204 1
            return sprintf(
205
                'Property "%s" must be public to be able to be called.',
206
                $className . '::$' . $parsedKey,
207
            );
208
        }
209
210 5
        return 'The key may be a call of a method or a setting of a property.';
211
    }
212
213 17
    private static function validateMethod(
214
        string $key,
215
        ReflectionClass $classReflection,
216
        array $classPublicMethods,
217
        string $className,
218
        mixed $value
219
    ): void {
220 17
        $parsedKey = substr($key, 0, -2);
221 17
        if (!$classReflection->hasMethod($parsedKey)) {
222 4
            if ($classReflection->hasMethod('__call') || $classReflection->hasMethod('__callStatic')) {
223
                /**
224
                 * Magic method may intercept the call, but reflection does not know about it.
225
                 */
226
                return;
227
            }
228 4
            $possiblePropertiesMessage = $classPublicMethods === []
229 1
                ? 'No public methods available to call.'
230 3
                : sprintf(
231
                    'Possible methods to call: %s',
232 3
                    '"' . implode('", "', $classPublicMethods) . '"',
233
                );
234 4
            throw new InvalidConfigException(
235 4
                sprintf(
236
                    'Invalid definition: class "%s" does not have the public method with name "%s". ' . $possiblePropertiesMessage,
237
                    $className,
238
                    $parsedKey,
239
                )
240
            );
241
        }
242 13
        if (!in_array($parsedKey, $classPublicMethods, true)) {
243 1
            throw new InvalidConfigException(
244 1
                sprintf(
245
                    'Invalid definition: method "%s" must be public.',
246
                    $className . '::' . $key,
247
                )
248
            );
249
        }
250 12
        if (!is_array($value)) {
251 1
            throw new InvalidConfigException(
252 1
                sprintf(
253
                    'Invalid definition: incorrect method "%s" arguments. Expected array, got "%s". ' .
254
                    'Probably you should wrap them into square brackets.',
255
                    $key,
256 1
                    get_debug_type($value),
257
                )
258
            );
259
        }
260
    }
261
262 15
    private static function validateProperty(
263
        string $key,
264
        ReflectionClass $classReflection,
265
        array $classPublicProperties,
266
        string $className
267
    ): void {
268 15
        $parsedKey = substr($key, 1);
269 15
        if (!$classReflection->hasProperty($parsedKey)) {
270 2
            if ($classReflection->hasMethod('__set')) {
271
                /**
272
                 * Magic method may intercept the call, but reflection does not know about it.
273
                 */
274
                return;
275
            }
276 2
            if ($classPublicProperties === []) {
277 1
                $message = sprintf(
278
                    'Invalid definition: class "%s" does not have any public properties.',
279
                    $className,
280
                );
281
            } else {
282 1
                $message = sprintf(
283
                    'Invalid definition: class "%s" does not have the public property with name "%s". Possible properties to set: %s.',
284
                    $className,
285
                    $parsedKey,
286 1
                    '"' . implode('", "', $classPublicProperties) . '"',
287
                );
288
            }
289 2
            throw new InvalidConfigException($message);
290 13
        } elseif (!in_array($parsedKey, $classPublicProperties, true)) {
291 2
            throw new InvalidConfigException(
292 2
                sprintf(
293
                    'Invalid definition: property "%s" must be public.',
294
                    $className . '::' . $key,
295
                )
296
            );
297
        }
298
    }
299
300 13
    private static function validateConstructor(mixed $value): void
301
    {
302 13
        if (!is_array($value)) {
303 1
            throw new InvalidConfigException(
304 1
                sprintf(
305
                    'Invalid definition: incorrect constructor arguments. Expected array, got %s.',
306 1
                    get_debug_type($value)
307
                )
308
            );
309
        }
310
        /** @var mixed $argument */
311 12
        foreach ($value as $argument) {
312 12
            if (is_object($argument) && !self::isValidObject($argument)) {
313 1
                throw new InvalidConfigException(
314
                    'Only references are allowed in constructor arguments, a definition object was provided: ' .
315 1
                    var_export($argument, true)
316
                );
317
            }
318
        }
319
    }
320
321 23
    private static function isMagicMethod(string $getName): bool
322
    {
323 23
        return in_array($getName, [
324
            '__construct',
325
            '__destruct',
326
            '__call',
327
            '__callStatic',
328
            '__get',
329
            '__set',
330
            '__isset',
331
            '__unset',
332
            '__sleep',
333
            '__wakeup',
334
            '__serialize',
335
            '__unserialize',
336
            '__toString',
337
            '__invoke',
338
            '__set_state',
339
            '__clone',
340
            '__debugInfo',
341
        ], true);
342
    }
343
}
344