• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nextras / orm / 28890847993

07 Jul 2026 06:54PM UTC coverage: 92.095%. Remained the same
28890847993

push

github

web-flow
Merge pull request #806 from nextras/renovate/phpstan-packages

Update PHPStan packages

4299 of 4668 relevant lines covered (92.1%)

5.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.0
/src/Entity/Reflection/ModifierParser.php
1
<?php declare(strict_types = 1);
2

3
namespace Nextras\Orm\Entity\Reflection;
4

5

6
use BackedEnum;
7
use Nette\Utils\Reflection;
8
use Nextras\Orm\Entity\Reflection\Parser\Token;
9
use Nextras\Orm\Entity\Reflection\Parser\TokenLexer;
10
use Nextras\Orm\Entity\Reflection\Parser\TokenStream;
11
use Nextras\Orm\Exception\InvalidStateException;
12
use ReflectionClass;
13
use ReflectionEnum;
14

15

16
class ModifierParser
17
{
18
        private TokenLexer $tokenLexer;
19

20

21
        public function __construct()
22
        {
23
                $this->tokenLexer = new TokenLexer();
6✔
24
        }
6✔
25

26

27
        /**
28
         * @return list<string>
29
         */
30
        public function matchModifiers(string $input): array
31
        {
32
                preg_match_all('#
6✔
33
                        \{(
34
                                (?:
35
                                        ' . TokenLexer::RE_STRING . ' |
6✔
36
                                        [^}]
37
                                )++
38
                        )}#x', $input, $matches);
39
                return $matches[1];
6✔
40
        }
41

42

43
        /**
44
         * @param ReflectionClass<covariant object> $reflectionClass
45
         * @return array{string, array<int|string, mixed>}
46
         * @throws InvalidModifierDefinitionException
47
         */
48
        public function parse(string $string, ReflectionClass $reflectionClass): array
49
        {
50
                $iterator = $this->tokenLexer->lex($string);
6✔
51
                return [
52
                        $name = $this->processName($iterator),
6✔
53
                        $this->processArgs($iterator, $reflectionClass, $name, false),
6✔
54
                ];
55
        }
56

57

58
        private function processName(TokenStream $iterator): string
59
        {
60
                $iterator->position++;
6✔
61
                $currentToken = $iterator->currentToken();
6✔
62
                if ($currentToken === null) {
6✔
63
                        throw new InvalidModifierDefinitionException("Modifier does not have a name.");
×
64
                }
65
                if ($currentToken->type !== Token::KEYWORD) {
6✔
66
                        throw new InvalidModifierDefinitionException("Modifier does not have a name.");
6✔
67
                } elseif (isset($iterator->tokens[$iterator->position + 1])) {
6✔
68
                        $nextToken = $iterator->tokens[$iterator->position + 1];
6✔
69
                        if ($nextToken->type === Token::SEPARATOR) {
6✔
70
                                throw new InvalidModifierDefinitionException("After the {{$currentToken->value}}'s modifier name cannot be a comma separator.");
6✔
71
                        }
72
                }
73

74
                return $currentToken->value;
6✔
75
        }
76

77

78
        /**
79
         * @param ReflectionClass<covariant object> $reflectionClass
80
         * @return array<int|string, mixed>
81
         */
82
        private function processArgs(
83
                TokenStream $iterator,
84
                ReflectionClass $reflectionClass,
85
                string $modifierName,
86
                bool $inArray,
87
        ): array
88
        {
89
                $result = [];
6✔
90
                while (($currentToken = $iterator->nextToken()) !== null) {
6✔
91
                        $type = $currentToken->type;
6✔
92

93
                        if ($type === Token::RBRACKET) {
6✔
94
                                if ($inArray) {
6✔
95
                                        return $result;
6✔
96
                                } else {
97
                                        throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} mismatches brackets.");
6✔
98
                                }
99
                        } elseif ($type === Token::STRING || $type === Token::KEYWORD) {
6✔
100
                                $iterator->position++;
6✔
101
                                $nextToken = $iterator->currentToken();
6✔
102
                                $nextTokenType = $nextToken?->type;
6✔
103

104
                                if ($nextTokenType === Token::EQUAL) {
6✔
105
                                        $iterator->position++;
6✔
106
                                        $nextToken = $iterator->currentToken();
6✔
107
                                        $nextTokenType = $nextToken?->type;
6✔
108

109
                                        if ($nextTokenType === Token::LBRACKET) {
6✔
110
                                                $value = $this->processValue($currentToken, $reflectionClass);
6✔
111
                                                assert(!is_array($value));
112
                                                $result[$value] = $this->processArgs($iterator, $reflectionClass, $modifierName, true);
6✔
113
                                        } elseif ($nextTokenType === Token::STRING || $nextTokenType === Token::KEYWORD) {
6✔
114
                                                $value = $this->processValue($currentToken, $reflectionClass);
6✔
115
                                                assert(!is_array($value));
116
                                                $result[$value] = $this->processValue($nextToken, $reflectionClass);
6✔
117
                                        } elseif ($nextTokenType !== null) {
×
118
                                                throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} has invalid token after =.");
2✔
119
                                        }
120
                                } else {
121
                                        $iterator->position--;
6✔
122
                                        $value = $this->processValue($currentToken, $reflectionClass);
6✔
123
                                        if (is_array($value)) {
6✔
124
                                                foreach ($value as $subValue) {
6✔
125
                                                        $result[] = $subValue;
6✔
126
                                                }
127
                                        } else {
128
                                                $result[] = $value;
6✔
129
                                        }
130
                                }
131
                        } elseif ($type === Token::LBRACKET) {
6✔
132
                                $result[] = $this->processArgs($iterator, $reflectionClass, $modifierName, true);
6✔
133
                        } else {
134
                                throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} has invalid token, expected string, keyword, or array.");
6✔
135
                        }
136

137
                        $iterator->position++;
6✔
138
                        $currentToken2 = $iterator->currentToken();
6✔
139
                        $type = $currentToken2?->type;
6✔
140
                        if ($type === Token::RBRACKET && $inArray) {
6✔
141
                                return $result;
6✔
142
                        } elseif ($type !== null && $type !== Token::SEPARATOR) {
6✔
143
                                throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} misses argument separator.");
6✔
144
                        }
145
                }
146

147
                if ($inArray) {
6✔
148
                        throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} has unclosed array argument.");
6✔
149
                }
150

151
                return $result;
6✔
152
        }
153

154

155
        /**
156
         * @param ReflectionClass<covariant object> $reflectionClass
157
         */
158
        private function processValue(Token $token, ReflectionClass $reflectionClass): mixed
159
        {
160
                if ($token->type === Token::STRING) {
6✔
161
                        return stripslashes(substr($token->value, 1, -1));
6✔
162
                } elseif ($token->type === Token::KEYWORD) {
6✔
163
                        return $this->processKeyword($token->value, $reflectionClass);
6✔
164
                } else {
165
                        throw new InvalidStateException();
×
166
                }
167
        }
168

169

170
        /**
171
         * @param ReflectionClass<covariant object> $reflectionClass
172
         */
173
        private function processKeyword(string $value, ReflectionClass $reflectionClass): mixed
174
        {
175
                if (strcasecmp($value, 'true') === 0) {
6✔
176
                        return true;
6✔
177
                } elseif (strcasecmp($value, 'false') === 0) {
6✔
178
                        return false;
6✔
179
                } elseif (strcasecmp($value, 'null') === 0) {
6✔
180
                        return null;
6✔
181
                } elseif (is_numeric($value)) {
6✔
182
                        return $value * 1;
6✔
183
                } elseif (preg_match('#^[a-z0-9_\\\\]+::[a-z0-9_]*(\\*)?$#i', $value) === 1) {
6✔
184
                        [$className, $const] = explode('::', $value, 2);
6✔
185
                        if ($className === 'self' || $className === 'static') {
6✔
186
                                $reflection = $reflectionClass;
6✔
187
                        } else {
188
                                $className = Reflection::expandClassName($className, $reflectionClass); // @phpstan-ignore argument.type (https://github.com/phpstan/phpstan/issues/12459#issuecomment-2607123277)
6✔
189
                                assert(class_exists($className) || interface_exists($className));
190
                                $reflection = new ReflectionClass($className);
6✔
191
                        }
192

193
                        if ($reflection->isEnum() && is_subclass_of($className, BackedEnum::class)) {
6✔
194
                                return (new ReflectionEnum($className))->getCase($const)->getValue();
6✔
195
                        }
196
                        $enum = [];
6✔
197
                        $constants = $reflection->getConstants();
6✔
198
                        if (str_contains($const, '*')) {
6✔
199
                                $prefix = rtrim($const, '*');
6✔
200
                                $prefixLength = strlen($prefix);
6✔
201
                                $count = 0;
6✔
202
                                foreach ($constants as $name => $constantValue) {
6✔
203
                                        if (substr($name, 0, $prefixLength) === $prefix) {
6✔
204
                                                $enum[] = $constantValue;
6✔
205
                                                $count += 1;
6✔
206
                                        }
207
                                }
208
                                if ($count === 0) {
6✔
209
                                        throw new InvalidModifierDefinitionException("No constant matches $reflection->name::$const pattern.");
6✔
210
                                }
211
                        } else {
212
                                if (!array_key_exists($const, $constants)) {
6✔
213
                                        throw new InvalidModifierDefinitionException("Constant $reflection->name::$const does not exist.");
6✔
214
                                }
215
                                $value = $reflection->getConstant($const);
6✔
216
                                $enum[] = $value;
6✔
217
                        }
218
                        return $enum;
6✔
219
                } else {
220
                        return $value;
6✔
221
                }
222
        }
223
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc