Passed
Pull Request — master (#56)
by Dmitriy
05:12 queued 02:19
created

DefinitionValidator::validateClassName()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.25

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 10
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 15
ccs 6
cts 8
cp 0.75
crap 4.25
rs 9.9332
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Definitions\Helpers;
6
7
use Yiisoft\Definitions\ArrayDefinition;
8
use Yiisoft\Definitions\Contract\DefinitionInterface;
9
use Yiisoft\Definitions\Contract\ReferenceInterface;
10
use Yiisoft\Definitions\Exception\InvalidConfigException;
11
12
use function is_array;
13
use function is_callable;
14
use function is_object;
15
use function is_string;
16
17
/**
18
 * Definition validator checks if definition is valid.
19
 */
20
final class DefinitionValidator
21
{
22
    /**
23
     * Validates that definition is valid. Throws exception otherwise.
24
     *
25
     * @param mixed $definition Definition to validate.
26
     *
27
     * @throws InvalidConfigException If definition is not valid.
28
     */
29 24
    public static function validate(mixed $definition, ?string $id = null): void
30
    {
31
        // Reference or ready object
32 24
        if (is_object($definition) && self::isValidObject($definition)) {
33 2
            return;
34
        }
35
36
        // Class
37 22
        if (is_string($definition) && $definition !== '') {
38 1
            return;
39
        }
40
41
        // Callable definition
42 21
        if ($definition !== '' && is_callable($definition, true)) {
43 1
            return;
44
        }
45
46
        // Array definition
47 20
        if (is_array($definition)) {
48 18
            self::validateArrayDefinition($definition, $id);
49 1
            return;
50
        }
51
52 2
        throw new InvalidConfigException(
53
            'Invalid definition: '
54 2
            . ($definition === '' ? 'empty string.' : var_export($definition, true))
55
        );
56
    }
57
58
    /**
59
     * Validates that array definition is valid. Throws exception otherwise.
60
     *
61
     * @param array $definition Array definition to validate.
62
     *
63
     * @throws InvalidConfigException If definition is not valid.
64
     */
65 20
    public static function validateArrayDefinition(array $definition, ?string $id = null): void
66
    {
67
        /** @var string $className */
68 20
        $className = $definition[ArrayDefinition::CLASS_NAME] ?? $id ?? throw new InvalidConfigException(
69
            'Invalid definition: no class name specified.'
70
        );
71 19
        self::validateClassName($className);
72
73 17
        foreach ($definition as $key => $value) {
74 16
            if (!is_string($key)) {
75 1
                throw new InvalidConfigException(
76 1
                    sprintf(
77
                        'Invalid definition: invalid key in array definition. Allow only string keys, got %d.',
78
                        $key,
79
                    ),
80
                );
81
            }
82
83
            // Class
84 16
            if ($key === ArrayDefinition::CLASS_NAME) {
85 16
                continue;
86
            }
87
88
            // Constructor arguments
89 13
            if ($key === ArrayDefinition::CONSTRUCTOR) {
90 9
                if (!is_array($value)) {
91 1
                    throw new InvalidConfigException(
92 1
                        sprintf(
93
                            'Invalid definition: incorrect constructor arguments. Expected array, got %s.',
94 1
                            get_debug_type($value)
95
                        )
96
                    );
97
                }
98
                /** @var mixed $argument */
99 8
                foreach ($value as $argument) {
100 8
                    if (is_object($argument) && !self::isValidObject($argument)) {
101 1
                        throw new InvalidConfigException(
102
                            'Only references are allowed in constructor arguments, a definition object was provided: ' .
103 1
                            var_export($argument, true)
104
                        );
105
                    }
106
                }
107 7
                continue;
108
            }
109
110
            // Methods and properties
111 11
            if (str_ends_with($key, '()')) {
112
                /**
113
                 * Regular expression from https://www.php.net/manual/en/functions.user-defined.php
114
                 */
115 9
                if (!preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*\(\)$/', $key)) {
116 1
                    throw new InvalidConfigException(
117 1
                        sprintf(
118
                            'Invalid definition: incorrect method name. Got "%s".',
119
                            $key
120
                        )
121
                    );
122
                }
123 8
                if (!is_array($value)) {
124 1
                    throw new InvalidConfigException(
125 1
                        sprintf(
126
                            'Invalid definition: incorrect method "%s" arguments. Expected array, got "%s". ' .
127 1
                            'Probably you should wrap them into square brackets.',
128
                            $key,
129 1
                            get_debug_type($value),
130
                        )
131
                    );
132
                }
133 7
                continue;
134
            }
135 9
            if (str_starts_with($key, '$')) {
136 9
                $parsedKey = mb_substr($key, 1);
137 9
                if ($parsedKey === '') {
138 1
                    throw new InvalidConfigException(
139
                        'Invalid definition: incorrect property name must not be an empty string.',
140
                    );
141
                }
142 8
                if (is_numeric($parsedKey)) {
143 1
                    throw new InvalidConfigException(
144 1
                        sprintf(
145
                            'Invalid definition: incorrect property name "%s".',
146
                            $parsedKey,
147
                        )
148
                    );
149
                }
150 7
                continue;
151
            }
152
153 7
            self::throwInvalidArrayDefinitionKey($key);
154
        }
155
    }
156
157
    /**
158
     * @throws InvalidConfigException
159
     */
160 7
    private static function throwInvalidArrayDefinitionKey(string $key): void
161
    {
162 7
        $preparedKey = trim(strtr($key, [
163
            '()' => '',
164
            '$' => '',
165
        ]));
166
167 7
        if ($preparedKey === '' || !preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $preparedKey)) {
168 2
            throw new InvalidConfigException(
169 2
                sprintf('Invalid definition: key "%s" is not allowed.', $key)
170
            );
171
        }
172
173 5
        throw new InvalidConfigException(
174 5
            sprintf(
175
                'Invalid definition: key "%s" is not allowed. Did you mean "%s()" or "$%s"?',
176
                $key,
177
                $preparedKey,
178
                $preparedKey
179
            )
180
        );
181
    }
182
183
    /**
184
     * Deny `DefinitionInterface`, exclude `ReferenceInterface`
185
     */
186 3
    private static function isValidObject(object $value): bool
187
    {
188 3
        return !($value instanceof DefinitionInterface) || $value instanceof ReferenceInterface;
189
    }
190
191 19
    private static function validateClassName(mixed $class): void
192
    {
193 19
        if ($class === '' || !is_string($class)) {
194 2
            throw new InvalidConfigException(
195 2
                sprintf(
196
                    'Invalid definition: class name must be a non-empty string, got "%s".',
197 2
                    get_debug_type($class),
198
                )
199
            );
200
        }
201 17
        if (!class_exists($class)) {
202
            throw new InvalidConfigException(
203
                sprintf(
204
                    'Invalid definition: invalid class name "%s". Class must exist.',
205
                    $class,
206
                ),
207
            );
208
        }
209
    }
210
}
211