Passed
Pull Request — master (#57)
by Dmitriy
02:28
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 30
    public static function validate(mixed $definition, ?string $id = null): void
33
    {
34
        // Reference or ready object
35 30
        if (is_object($definition) && self::isValidObject($definition)) {
36 2
            return;
37
        }
38
39
        // Class
40 28
        if (is_string($definition)) {
41 3
            self::validateClassName($definition);
42 1
            return;
43
        }
44
45
        // Callable definition
46 25
        if ($definition !== '' && is_callable($definition, true)) {
47 1
            return;
48
        }
49
50
        // Array definition
51 24
        if (is_array($definition)) {
52 23
            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 25
    public static function validateArrayDefinition(array $definition, ?string $id = null): void
70
    {
71
        /** @var class-string $className */
72 25
        $className = $definition[ArrayDefinition::CLASS_NAME] ?? $id ?? throw new InvalidConfigException(
73
            'Invalid definition: no class name specified.'
74
        );
75 24
        self::validateClassName($className);
76 22
        $classReflection = new ReflectionClass($className);
77 22
        $classPublicMethods = [];
78 22
        foreach ($classReflection->getMethods() as $reflectionMethod) {
79 18
            if ($reflectionMethod->getModifiers() & ReflectionMethod::IS_PUBLIC) {
80 18
                $classPublicMethods[] = $reflectionMethod->getName();
81
            }
82
        }
83 22
        $classPublicProperties = [];
84 22
        foreach ($classReflection->getProperties(ReflectionProperty::IS_PUBLIC) as $reflectionProperty) {
85 16
            if ($reflectionProperty->getModifiers() & ReflectionProperty::IS_PUBLIC) {
86 16
                $classPublicProperties[] = $reflectionProperty->getName();
87
            }
88
        }
89
90 22
        foreach ($definition as $key => $value) {
91 21
            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 21
            if ($key === ArrayDefinition::CLASS_NAME) {
102 21
                continue;
103
            }
104
105
            // Constructor arguments
106 18
            if ($key === ArrayDefinition::CONSTRUCTOR) {
107 11
                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 10
                foreach ($value as $argument) {
117 10
                    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 9
                continue;
125
            }
126
127
            // Methods and properties
128 16
            if (str_ends_with($key, '()')) {
129 13
                $parsedKey = mb_substr($key, 0, -2);
130 13
                if (!$classReflection->hasMethod($parsedKey)) {
131 3
                    $possiblePropertiesMessage = $classPublicMethods === []
132 1
                        ? 'No public methods available to call.'
133 2
                        : sprintf(
134
                            'Possible methods to call: %s',
135 2
                            '"' . implode('", "', $classPublicMethods) . '"',
136
                        );
137 3
                    throw new InvalidConfigException(
138 3
                        sprintf(
139
                            'Invalid definition: class "%s" does not have the public method with name "%s". ' . $possiblePropertiesMessage,
140
                            $className,
141
                            $parsedKey,
142
                        )
143
                    );
144
                }
145 10
                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 10
                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 9
                continue;
164
            }
165 12
            if (str_starts_with($key, '$')) {
166 12
                $parsedKey = mb_substr($key, 1);
167 12
                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 10
                } elseif (!in_array($parsedKey, $classPublicProperties, true)) {
183 1
                    throw new InvalidConfigException(
184 1
                        sprintf(
185
                            'Invalid definition: property "%s" must be public.',
186
                            $className . '::' . $key,
187
                        )
188
                    );
189
                }
190 9
                continue;
191
            }
192
193 9
            $possibleOptionsMessage = self::generatePossibleMessage(
194
                $key,
195
                $classPublicMethods,
196
                $classPublicProperties,
197
                $classReflection,
198
                $className
199
            );
200
201 9
            throw new InvalidConfigException(
202 9
                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 27
    private static function validateClassName(mixed $class): void
219
    {
220 27
        if ($class === '' || !is_string($class)) {
221 3
            throw new InvalidConfigException(
222 3
                sprintf(
223
                    'Invalid definition: class name must be a non-empty string, got %s.',
224 3
                    is_string($class) ? '"' . $class . '"' : get_debug_type($class),
225
                )
226
            );
227
        }
228 24
        if (!class_exists($class)) {
229 1
            throw new InvalidConfigException(
230 1
                sprintf(
231
                    'Invalid definition: class "%s" does not exist.',
232
                    $class,
233
                ),
234
            );
235
        }
236
    }
237
238 9
    private static function generatePossibleMessage(
239
        string $key,
240
        array $classPublicMethods,
241
        array $classPublicProperties,
242
        ReflectionClass $classReflection,
243
        string $className
244
    ): string {
245 9
        $parsedKey = trim(
246 9
            strtr($key, [
247
                '()' => '',
248
                '$' => '',
249
            ])
250
        );
251 9
        if (in_array($parsedKey, $classPublicMethods, true)) {
252 1
            return sprintf(
253
                'Did you mean "%s"?',
254
                $parsedKey . '()',
255
            );
256
        }
257 8
        if (in_array($parsedKey, $classPublicProperties, true)) {
258 1
            return sprintf(
259
                'Did you mean "%s"?',
260
                '$' . $parsedKey,
261
            );
262
        }
263 7
        if ($classReflection->hasMethod($parsedKey)) {
264 1
            return sprintf(
265
                'Method "%s" must be public to be able to be called.',
266
                $className . '::' . $parsedKey . '()',
267
            );
268
        }
269 6
        if ($classReflection->hasProperty($parsedKey)) {
270 1
            return sprintf(
271
                'Property "%s" must be public to be able to be called.',
272
                $className . '::$' . $parsedKey,
273
            );
274
        }
275
276 5
        return 'The key may be a call of a method or a setting of a property.';
277
    }
278
}
279