Passed
Push — master ( 731b37...2160f3 )
by Sergei
13:33 queued 36s
created

DefinitionValidator::validateMethod()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 39
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 7

Importance

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