Passed
Pull Request — master (#57)
by Dmitriy
02:25
created

DefinitionValidator::validateArrayDefinition()   F

Complexity

Conditions 22
Paths 153

Size

Total Lines 136
Code Lines 88

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 59
CRAP Score 22

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 22
eloc 88
c 2
b 0
f 0
nc 153
nop 2
dl 0
loc 136
ccs 59
cts 59
cp 1
crap 22
rs 3.725

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 35
    public static function validate(mixed $definition, ?string $id = null): void
33
    {
34
        // Reference or ready object
35 35
        if (is_object($definition) && self::isValidObject($definition)) {
36 2
            return;
37
        }
38
39
        // Class
40 33
        if (is_string($definition)) {
41 4
            self::validateClassName($definition);
42 1
            return;
43
        }
44
45
        // Callable definition
46 29
        if ($definition !== '' && is_callable($definition, true)) {
47 1
            return;
48
        }
49
50
        // Array definition
51 28
        if (is_array($definition)) {
52 27
            self::validateArrayDefinition($definition, $id);
53 1
            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) {
80 23
                $classPublicMethods[] = $reflectionMethod->getName();
81
            }
82
        }
83 27
        $classPublicProperties = [];
84 27
        foreach ($classReflection->getProperties() as $reflectionProperty) {
85 21
            if (($reflectionProperty->getModifiers() & ReflectionProperty::IS_PUBLIC) !== 0) {
86 20
                $classPublicProperties[] = $reflectionProperty->getName();
87
            }
88
        }
89
90 27
        foreach ($definition as $key => $value) {
91 26
            if (!is_string($key)) {
92 1
                throw new InvalidConfigException(
93 1
                    sprintf(
94
                        'Invalid definition: invalid key in array definition. Allow only string keys, got %d.',
95
                        $key,
96
                    ),
97
                );
98
            }
99
100
            // Class
101 26
            if ($key === ArrayDefinition::CLASS_NAME) {
102 26
                continue;
103
            }
104
105
            // Constructor arguments
106 22
            if ($key === ArrayDefinition::CONSTRUCTOR) {
107 13
                if (!is_array($value)) {
108 1
                    throw new InvalidConfigException(
109 1
                        sprintf(
110
                            'Invalid definition: incorrect constructor arguments. Expected array, got %s.',
111 1
                            get_debug_type($value)
112
                        )
113
                    );
114
                }
115
                /** @var mixed $argument */
116 12
                foreach ($value as $argument) {
117 12
                    if (is_object($argument) && !self::isValidObject($argument)) {
118 1
                        throw new InvalidConfigException(
119
                            'Only references are allowed in constructor arguments, a definition object was provided: ' .
120 1
                            var_export($argument, true)
121
                        );
122
                    }
123
                }
124 11
                continue;
125
            }
126
127
            // Methods and properties
128 20
            if (str_ends_with($key, '()')) {
129 16
                $parsedKey = substr($key, 0, -2);
130 16
                if (!$classReflection->hasMethod($parsedKey)) {
131 4
                    $possiblePropertiesMessage = $classPublicMethods === []
132 1
                        ? 'No public methods available to call.'
133 3
                        : sprintf(
134
                            'Possible methods to call: %s',
135 3
                            '"' . implode('", "', $classPublicMethods) . '"',
136
                        );
137 4
                    throw new InvalidConfigException(
138 4
                        sprintf(
139
                            'Invalid definition: class "%s" does not have the public method with name "%s". ' . $possiblePropertiesMessage,
140
                            $className,
141
                            $parsedKey,
142
                        )
143
                    );
144
                }
145 12
                if (!in_array($parsedKey, $classPublicMethods, true)) {
146
                    throw new InvalidConfigException(
147
                        sprintf(
148
                            'Invalid definition: method "%s" must be public.' .
149
                            $className . '::' . $key,
150
                        )
151
                    );
152
                }
153 12
                if (!is_array($value)) {
154 1
                    throw new InvalidConfigException(
155 1
                        sprintf(
156
                            'Invalid definition: incorrect method "%s" arguments. Expected array, got "%s". ' .
157 1
                            'Probably you should wrap them into square brackets.',
158
                            $key,
159 1
                            get_debug_type($value),
160
                        )
161
                    );
162
                }
163 11
                continue;
164
            }
165 15
            if (str_starts_with($key, '$')) {
166 15
                $parsedKey = substr($key, 1);
167 15
                if (!$classReflection->hasProperty($parsedKey)) {
168 2
                    if ($classPublicProperties === []) {
169 1
                        $message = sprintf(
170
                            'Invalid definition: class "%s" does not have any public properties.',
171
                            $className,
172
                        );
173
                    } else {
174 1
                        $message = sprintf(
175
                            'Invalid definition: class "%s" does not have the public property with name "%s". Possible properties to set: %s.',
176
                            $className,
177
                            $parsedKey,
178 1
                            '"' . implode('", "', $classPublicProperties) . '"',
179
                        );
180
                    }
181 2
                    throw new InvalidConfigException($message);
182 13
                } elseif (!in_array($parsedKey, $classPublicProperties, true)) {
183 2
                    throw new InvalidConfigException(
184 2
                        sprintf(
185
                            'Invalid definition: property "%s" must be public.',
186
                            $className . '::' . $key,
187
                        )
188
                    );
189
                }
190 11
                continue;
191
            }
192
193 11
            $possibleOptionsMessage = self::generatePossibleMessage(
194
                $key,
195
                $classPublicMethods,
196
                $classPublicProperties,
197
                $classReflection,
198
                $className
199
            );
200
201 11
            throw new InvalidConfigException(
202 11
                sprintf(
203
                    'Invalid definition: key "%s" is not allowed. ' . $possibleOptionsMessage,
204
                    $key,
205
                )
206
            );
207
        }
208
    }
209
210
    /**
211
     * Deny `DefinitionInterface`, exclude `ReferenceInterface`
212
     */
213 3
    private static function isValidObject(object $value): bool
214
    {
215 3
        return !($value instanceof DefinitionInterface) || $value instanceof ReferenceInterface;
216
    }
217
218 33
    private static function validateClassName(mixed $class): void
219
    {
220 33
        if (!is_string($class)) {
221 1
            throw new InvalidConfigException(
222 1
                sprintf(
223
                    'Invalid definition: class name must be a non-empty string, got %s.',
224 1
                    get_debug_type($class),
225
                )
226
            );
227
        }
228 32
        if (trim($class) === '') {
229 3
            throw new InvalidConfigException(
230 3
                sprintf(
231
                    'Invalid definition: class name must be a non-empty string.',
232
                )
233
            );
234
        }
235 29
        if (!class_exists($class)) {
236 1
            throw new InvalidConfigException(
237 1
                sprintf(
238
                    'Invalid definition: class "%s" does not exist.',
239
                    $class,
240
                ),
241
            );
242
        }
243
    }
244
245 11
    private static function generatePossibleMessage(
246
        string $key,
247
        array $classPublicMethods,
248
        array $classPublicProperties,
249
        ReflectionClass $classReflection,
250
        string $className
251
    ): string {
252 11
        $parsedKey = trim(
253 11
            strtr($key, [
254
                '()' => '',
255
                '$' => '',
256
            ])
257
        );
258 11
        if (in_array($parsedKey, $classPublicMethods, true)) {
259 3
            return sprintf(
260
                'Did you mean "%s"?',
261
                $parsedKey . '()',
262
            );
263
        }
264 8
        if (in_array($parsedKey, $classPublicProperties, true)) {
265 1
            return sprintf(
266
                'Did you mean "%s"?',
267
                '$' . $parsedKey,
268
            );
269
        }
270 7
        if ($classReflection->hasMethod($parsedKey)) {
271 1
            return sprintf(
272
                'Method "%s" must be public to be able to be called.',
273
                $className . '::' . $parsedKey . '()',
274
            );
275
        }
276 6
        if ($classReflection->hasProperty($parsedKey)) {
277 1
            return sprintf(
278
                'Property "%s" must be public to be able to be called.',
279
                $className . '::$' . $parsedKey,
280
            );
281
        }
282
283 5
        return 'The key may be a call of a method or a setting of a property.';
284
    }
285
}
286