Test Failed
Pull Request — master (#57)
by Sergei
04:42 queued 02:04
created

DefinitionValidator::validateArrayDefinition()   F

Complexity

Conditions 22
Paths 153

Size

Total Lines 125
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 22

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 22
eloc 79
c 2
b 0
f 0
nc 153
nop 2
dl 0
loc 125
ccs 43
cts 43
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 22
     *
30
     * @throws InvalidConfigException If definition is not valid.
31
     */
32 22
    public static function validate(mixed $definition, ?string $id = null): void
33 2
    {
34
        // Reference or ready object
35
        if (is_object($definition) && self::isValidObject($definition)) {
36
            return;
37 20
        }
38 1
39
        // Class
40
        if (is_string($definition)) {
41
            self::validateClassName($definition);
42 19
            return;
43 1
        }
44
45
        // Callable definition
46
        if ($definition !== '' && is_callable($definition, true)) {
47 18
            return;
48 16
        }
49 1
50
        // Array definition
51
        if (is_array($definition)) {
52 2
            self::validateArrayDefinition($definition, $id);
53
            return;
54 2
        }
55
56
        throw new InvalidConfigException(
57
            'Invalid definition: '
58
            . ($definition === '' ? 'empty string.' : var_export($definition, true))
59
        );
60
    }
61
62
    /**
63
     * Validates that array definition is valid. Throws exception otherwise.
64
     *
65 18
     * @param array $definition Array definition to validate.
66
     *
67 18
     * @throws InvalidConfigException If definition is not valid.
68 16
     */
69 1
    public static function validateArrayDefinition(array $definition, ?string $id = null): void
70 1
    {
71
        /** @var class-string $className */
72
        $className = $definition[ArrayDefinition::CLASS_NAME] ?? $id ?? throw new InvalidConfigException(
73
            'Invalid definition: no class name specified.'
74
        );
75
        self::validateClassName($className);
76
        $classReflection = new ReflectionClass($className);
77
        $classPublicMethods = [];
78 16
        foreach ($classReflection->getMethods() as $reflectionMethod) {
79 16
            if ($reflectionMethod->getModifiers() & ReflectionMethod::IS_PUBLIC) {
80 1
                $classPublicMethods[] = $reflectionMethod->getName();
81 1
            }
82
        }
83 1
        $classPublicProperties = [];
84
        foreach ($classReflection->getProperties(ReflectionProperty::IS_PUBLIC) as $reflectionProperty) {
85
            if ($reflectionProperty->getModifiers() & ReflectionProperty::IS_PUBLIC) {
86
                $classPublicProperties[] = $reflectionProperty->getName();
87 15
            }
88 1
        }
89
90 14
        foreach ($definition as $key => $value) {
91
            if (!is_string($key)) {
92
                throw new InvalidConfigException(
93
                    sprintf(
94 11
                        'Invalid definition: invalid key in array definition. Allow only string keys, got %d.',
95 9
                        $key,
96 1
                    ),
97 1
                );
98
            }
99 1
100
            // Class
101
            if ($key === ArrayDefinition::CLASS_NAME) {
102
                continue;
103
            }
104 8
105 8
            // Constructor arguments
106 1
            if ($key === ArrayDefinition::CONSTRUCTOR) {
107
                if (!is_array($value)) {
108 1
                    throw new InvalidConfigException(
109
                        sprintf(
110
                            'Invalid definition: incorrect constructor arguments. Expected array, got %s.',
111
                            get_debug_type($value)
112 7
                        )
113
                    );
114
                }
115
                /** @var mixed $argument */
116 9
                foreach ($value as $argument) {
117
                    if (is_object($argument) && !self::isValidObject($argument)) {
118
                        throw new InvalidConfigException(
119
                            'Only references are allowed in constructor arguments, a definition object was provided: ' .
120 9
                            var_export($argument, true)
121 1
                        );
122 1
                    }
123
                }
124
                continue;
125
            }
126
127
            // Methods and properties
128 8
            if (str_ends_with($key, '()')) {
129 1
                $parsedKey = mb_substr($key, 0, -2);
130 1
                if (!$classReflection->hasMethod($parsedKey)) {
131
                    $possiblePropertiesMessage = $classPublicMethods === []
132 1
                        ? 'No public methods available to call.'
133
                        : sprintf(
134
                            'Possible methods to call: %s',
135
                            '"' . implode('", "', $classPublicMethods) . '"',
136 7
                        );
137
                    throw new InvalidConfigException(
138 7
                        sprintf(
139 7
                            'Invalid definition: class "%s" does not have the public method with name "%s". ' . $possiblePropertiesMessage,
140
                            $className,
141
                            $parsedKey,
142 7
                        )
143
                    );
144
                }
145 4
                if (!in_array($parsedKey, $classPublicMethods, true)) {
146 1
                    throw new InvalidConfigException(
147
                        sprintf(
148
                            'Invalid definition: method "%s" must be public.' .
149
                            $className . '::' . $key,
150
                        )
151
                    );
152
                }
153 7
                if (!is_array($value)) {
154
                    throw new InvalidConfigException(
155 7
                        sprintf(
156
                            'Invalid definition: incorrect method "%s" arguments. Expected array, got "%s". ' .
157
                            'Probably you should wrap them into square brackets.',
158
                            $key,
159
                            get_debug_type($value),
160 7
                        )
161 2
                    );
162 2
                }
163
                continue;
164
            }
165
            if (str_starts_with($key, '$')) {
166 5
                $parsedKey = mb_substr($key, 1);
167 5
                if (!$classReflection->hasProperty($parsedKey)) {
168
                    if ($classPublicProperties === []) {
169
                        $message = sprintf(
170
                            'Invalid definition: class "%s" does not have any public properties.',
171
                            $className,
172
                        );
173
                    } else {
174
                        $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
                            '"' . implode('", "', $classPublicProperties) . '"',
179 3
                        );
180
                    }
181 3
                    throw new InvalidConfigException($message);
182
                } elseif (!in_array($parsedKey, $classPublicProperties, true)) {
183
                    throw new InvalidConfigException(
184
                        sprintf(
185
                            'Invalid definition: property "%s" must be public.',
186
                            $className . '::' . $key,
187
                        )
188
                    );
189
                }
190
                continue;
191
            }
192
193
            self::throwInvalidArrayDefinitionKey($key);
194
        }
195
    }
196
197
    /**
198
     * @throws InvalidConfigException
199
     */
200
    private static function throwInvalidArrayDefinitionKey(string $key): void
201
    {
202
        $preparedKey = trim(strtr($key, [
203
            '()' => '',
204
            '$' => '',
205
        ]));
206
207
        if ($preparedKey === '' || !preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $preparedKey)) {
208
            throw new InvalidConfigException(
209
                sprintf('Invalid definition: key "%s" is not allowed.', $key)
210
            );
211
        }
212
213
        throw new InvalidConfigException(
214
            sprintf(
215
                'Invalid definition: key "%s" is not allowed. Did you mean "%s()" or "$%s"?',
216
                $key,
217
                $preparedKey,
218
                $preparedKey
219
            )
220
        );
221
    }
222
223
    /**
224
     * Deny `DefinitionInterface`, exclude `ReferenceInterface`
225
     */
226
    private static function isValidObject(object $value): bool
227
    {
228
        return !($value instanceof DefinitionInterface) || $value instanceof ReferenceInterface;
229
    }
230
231
    private static function validateClassName(mixed $class): void
232
    {
233
        if ($class === '' || !is_string($class)) {
234
            throw new InvalidConfigException(
235
                sprintf(
236
                    'Invalid definition: class name must be a non-empty string, got %s.',
237
                    is_string($class) ? '"' . $class . '"' : get_debug_type($class),
238
                )
239
            );
240
        }
241
        if (!class_exists($class)) {
242
            throw new InvalidConfigException(
243
                sprintf(
244
                    'Invalid definition: class "%s" does not exist.',
245
                    $class,
246
                ),
247
            );
248
        }
249
    }
250
}
251