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

nette / utils / 21972698538

13 Feb 2026 02:47AM UTC coverage: 93.125% (-0.07%) from 93.199%
21972698538

push

github

dg
added CLAUDE.md

2086 of 2240 relevant lines covered (93.13%)

0.93 hits per line

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

97.14
/src/Utils/Type.php
1
<?php
1✔
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Utils;
11

12
use Nette;
13
use function array_map, array_search, array_splice, array_values, count, explode, implode, is_a, is_resource, is_string, strcasecmp, strtolower, substr, trim;
14

15

16
/**
17
 * PHP type reflection.
18
 */
19
final readonly class Type
20
{
21
        /** @var list<string|self> */
22
        private array $types;
23
        private ?string $singleName;
24
        private string $kind; // | &
25

26

27
        /**
28
         * Creates a Type object based on reflection. Resolves self, static and parent to the actual class name.
29
         * If the subject has no type, it returns null.
30
         */
31
        public static function fromReflection(
1✔
32
                \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection,
33
        ): ?self
34
        {
35
                $type = $reflection instanceof \ReflectionFunctionAbstract
1✔
36
                        ? $reflection->getReturnType() ?? ($reflection instanceof \ReflectionMethod ? $reflection->getTentativeReturnType() : null)
1✔
37
                        : $reflection->getType();
1✔
38

39
                return $type ? self::fromReflectionType($type, $reflection, asObject: true) : null;
1✔
40
        }
41

42

43
        /**
44
         * @param  \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty  $of
45
         * @return ($asObject is true ? self : self|string)
46
         */
47
        private static function fromReflectionType(\ReflectionType $type, $of, bool $asObject): self|string
1✔
48
        {
49
                if ($type instanceof \ReflectionNamedType) {
1✔
50
                        $name = self::resolve($type->getName(), $of);
1✔
51
                        return $asObject
1✔
52
                                ? new self($type->allowsNull() && $name !== 'mixed' ? [$name, 'null'] : [$name])
1✔
53
                                : $name;
1✔
54

55
                } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
1✔
56
                        return new self(
1✔
57
                                array_map(fn($t) => self::fromReflectionType($t, $of, asObject: false), $type->getTypes()),
1✔
58
                                $type instanceof \ReflectionUnionType ? '|' : '&',
1✔
59
                        );
60

61
                } else {
62
                        throw new Nette\InvalidStateException('Unexpected type of ' . Reflection::toString($of));
×
63
                }
64
        }
65

66

67
        /**
68
         * Creates the Type object according to the text notation.
69
         */
70
        public static function fromString(string $type): self
1✔
71
        {
72
                if (!Validators::isTypeDeclaration($type)) {
1✔
73
                        throw new Nette\InvalidArgumentException("Invalid type '$type'.");
×
74
                }
75

76
                if ($type[0] === '?') {
1✔
77
                        return new self([substr($type, 1), 'null']);
1✔
78
                }
79

80
                $unions = [];
1✔
81
                foreach (explode('|', $type) as $part) {
1✔
82
                        $part = explode('&', trim($part, '()'));
1✔
83
                        $unions[] = count($part) === 1 ? $part[0] : new self($part, '&');
1✔
84
                }
85

86
                return count($unions) === 1 && $unions[0] instanceof self
1✔
87
                        ? $unions[0]
1✔
88
                        : new self($unions);
1✔
89
        }
90

91

92
        /**
93
         * Creates a Type object based on the actual type of value.
94
         */
95
        public static function fromValue(mixed $value): self
1✔
96
        {
97
                $type = get_debug_type($value);
1✔
98
                if (is_resource($value)) {
1✔
99
                        $type = 'mixed';
1✔
100
                } elseif (str_ends_with($type, '@anonymous')) {
1✔
101
                        $parent = substr($type, 0, -10);
1✔
102
                        $type = $parent === 'class' ? 'object' : $parent;
1✔
103
                }
104

105
                return new self([$type]);
1✔
106
        }
107

108

109
        /**
110
         * Resolves 'self', 'static' and 'parent' to the actual class name.
111
         */
112
        public static function resolve(
1✔
113
                string $type,
114
                \ReflectionFunction|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $of,
115
        ): string
116
        {
117
                $lower = strtolower($type);
1✔
118
                if ($of instanceof \ReflectionFunction) {
1✔
119
                        return $type;
1✔
120
                }
121

122
                $class = $of->getDeclaringClass();
1✔
123
                if ($class === null) {
1✔
124
                        return $type;
1✔
125
                } elseif ($lower === 'self') {
1✔
126
                        return $class->name;
1✔
127
                } elseif ($lower === 'static') {
1✔
128
                        return ($of instanceof ReflectionMethod ? $of->getOriginalClass() : $class)->name;
1✔
129
                } elseif ($lower === 'parent' && $class->getParentClass()) {
1✔
130
                        return $class->getParentClass()->name;
1✔
131
                } else {
132
                        return $type;
1✔
133
                }
134
        }
135

136

137
        /** @param  array<string|self>  $types */
138
        private function __construct(array $types, string $kind = '|')
1✔
139
        {
140
                $o = array_search('null', $types, strict: true);
1✔
141
                if ($o !== false) { // null as last
1✔
142
                        array_splice($types, (int) $o, 1);
1✔
143
                        $types[] = 'null';
1✔
144
                }
145

146
                $this->types = array_values($types);
1✔
147
                $this->singleName = is_string($types[0]) && ($types[1] ?? 'null') === 'null' ? $types[0] : null;
1✔
148
                $this->kind = count($types) > 1 ? $kind : '';
1✔
149
        }
1✔
150

151

152
        public function __toString(): string
153
        {
154
                $multi = count($this->types) > 1;
1✔
155
                if ($this->singleName !== null) {
1✔
156
                        return ($multi ? '?' : '') . $this->singleName;
1✔
157
                }
158

159
                $res = [];
1✔
160
                foreach ($this->types as $type) {
1✔
161
                        $res[] = $type instanceof self && $multi ? "($type)" : $type;
1✔
162
                }
163
                return implode($this->kind, $res);
1✔
164
        }
165

166

167
        /**
168
         * Returns a type that accepts both the current type and the given type.
169
         */
170
        public function with(string|self $type): self
1✔
171
        {
172
                $type = is_string($type) ? self::fromString($type) : $type;
1✔
173
                return match (true) {
174
                        $this->allows($type) => $this,
1✔
175
                        $type->allows($this) => $type,
1✔
176
                        default => new self(array_unique(
1✔
177
                                array_merge($this->isIntersection() ? [$this] : $this->types, $type->isIntersection() ? [$type] : $type->types),
1✔
178
                                SORT_REGULAR,
1✔
179
                        ), '|'),
1✔
180
                };
181
        }
182

183

184
        /**
185
         * Returns the array of subtypes that make up the compound type as strings.
186
         * @return list<string|array<string|array<mixed>>>
187
         */
188
        public function getNames(): array
189
        {
190
                return array_map(fn($t) => $t instanceof self ? $t->getNames() : $t, $this->types);
1✔
191
        }
192

193

194
        /**
195
         * Returns the array of subtypes that make up the compound type as Type objects.
196
         * @return list<self>
197
         */
198
        public function getTypes(): array
199
        {
200
                return array_map(fn($t) => $t instanceof self ? $t : new self([$t]), $this->types);
1✔
201
        }
202

203

204
        /**
205
         * Returns the type name for simple types, otherwise null.
206
         */
207
        public function getSingleName(): ?string
208
        {
209
                return $this->singleName;
1✔
210
        }
211

212

213
        /**
214
         * Returns true whether it is a union type.
215
         */
216
        public function isUnion(): bool
217
        {
218
                return $this->kind === '|';
1✔
219
        }
220

221

222
        /**
223
         * Returns true whether it is an intersection type.
224
         */
225
        public function isIntersection(): bool
226
        {
227
                return $this->kind === '&';
1✔
228
        }
229

230

231
        /**
232
         * Returns true whether it is a simple type. Single nullable types are also considered to be simple types.
233
         */
234
        public function isSimple(): bool
235
        {
236
                return $this->singleName !== null;
1✔
237
        }
238

239

240
        #[\Deprecated('use isSimple()')]
241
        public function isSingle(): bool
242
        {
243
                return $this->singleName !== null;
×
244
        }
245

246

247
        /**
248
         * Returns true whether the type is both a simple and a PHP built-in type.
249
         */
250
        public function isBuiltin(): bool
251
        {
252
                return $this->singleName !== null && Validators::isBuiltinType($this->singleName);
1✔
253
        }
254

255

256
        /**
257
         * Returns true whether the type is both a simple and a class name.
258
         */
259
        public function isClass(): bool
260
        {
261
                return $this->singleName !== null && !Validators::isBuiltinType($this->singleName);
1✔
262
        }
263

264

265
        /**
266
         * Determines if type is special class name self/parent/static.
267
         */
268
        public function isClassKeyword(): bool
269
        {
270
                return $this->singleName !== null && Validators::isClassKeyword($this->singleName);
1✔
271
        }
272

273

274
        /**
275
         * Verifies type compatibility. For example, it checks if a value of a certain type could be passed as a parameter.
276
         */
277
        public function allows(string|self $type): bool
1✔
278
        {
279
                if ($this->types === ['mixed']) {
1✔
280
                        return true;
1✔
281
                }
282

283
                $type = is_string($type) ? self::fromString($type) : $type;
1✔
284
                return $type->isUnion()
1✔
285
                        ? Arrays::every($type->types, fn($t) => $this->allowsAny($t instanceof self ? $t->types : [$t]))
1✔
286
                        : $this->allowsAny($type->types);
1✔
287
        }
288

289

290
        /** @param array<string>  $givenTypes */
291
        private function allowsAny(array $givenTypes): bool
1✔
292
        {
293
                return $this->isUnion()
1✔
294
                        ? Arrays::some($this->types, fn($t) => $this->allowsAll($t instanceof self ? $t->types : [$t], $givenTypes))
1✔
295
                        : $this->allowsAll($this->types, $givenTypes);
1✔
296
        }
297

298

299
        /**
300
         * @param array<string>  $ourTypes
301
         * @param array<string>  $givenTypes
302
         */
303
        private function allowsAll(array $ourTypes, array $givenTypes): bool
1✔
304
        {
305
                return Arrays::every(
1✔
306
                        $ourTypes,
1✔
307
                        fn(string $ourType) => Arrays::some(
1✔
308
                                $givenTypes,
1✔
309
                                fn(string $givenType) => Validators::isBuiltinType($ourType)
1✔
310
                                        ? strcasecmp($ourType, $givenType) === 0
1✔
311
                                        : is_a($givenType, $ourType, allow_string: true),
1✔
312
                        ),
1✔
313
                );
314
        }
315
}
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