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

tempestphp / tempest-framework / 14122915110

28 Mar 2025 05:56AM UTC coverage: 79.691% (+0.4%) from 79.334%
14122915110

Pull #1076

github

web-flow
Merge 2ccad9efc into 6af05d563
Pull Request #1076: refactor(database): remove `DatabaseModel` interface

558 of 571 new or added lines in 42 files covered. (97.72%)

1 existing line in 1 file now uncovered.

10767 of 13511 relevant lines covered (79.69%)

93.27 hits per line

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

91.07
/src/Tempest/Reflection/src/TypeReflector.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Reflection;
6

7
use BackedEnum;
8
use DateTimeInterface;
9
use Exception;
10
use Generator;
11
use Iterator;
12
use ReflectionClass as PHPReflectionClass;
13
use ReflectionIntersectionType as PHPReflectionIntersectionType;
14
use ReflectionNamedType as PHPReflectionNamedType;
15
use ReflectionParameter as PHPReflectionParameter;
16
use ReflectionProperty as PHPReflectionProperty;
17
use ReflectionType as PHPReflectionType;
18
use ReflectionUnionType as PHPReflectionUnionType;
19
use Reflector as PHPReflector;
20
use Stringable;
21
use UnitEnum;
22

23
final readonly class TypeReflector implements Reflector
24
{
25
    private const array BUILTIN_VALIDATION = [
26
        'array' => 'is_array',
27
        'bool' => 'is_bool',
28
        'callable' => 'is_callable',
29
        'float' => 'is_float',
30
        'int' => 'is_int',
31
        'null' => 'is_null',
32
        'object' => 'is_object',
33
        'resource' => 'is_resource',
34
        'string' => 'is_string',
35
        // these are handled explicitly
36
        'false' => null,
37
        'mixed' => null,
38
        'never' => null,
39
        'true' => null,
40
        'void' => null,
41
    ];
42

43
    private const array SCALAR_TYPES = [
44
        'bool',
45
        'string',
46
        'int',
47
        'float',
48
    ];
49

50
    private string $definition;
51

52
    private string $cleanDefinition;
53

54
    private bool $isNullable;
55

56
    public function __construct(
766✔
57
        private PHPReflector|PHPReflectionType|string $reflector,
58
    ) {
59
        $this->definition = $this->resolveDefinition($this->reflector);
766✔
60
        $this->isNullable = $this->resolveIsNullable($this->reflector);
766✔
61
        $this->cleanDefinition = str_replace('?', '', $this->definition);
766✔
62
    }
63

64
    public function asClass(): ClassReflector
127✔
65
    {
66
        return new ClassReflector($this->cleanDefinition);
127✔
67
    }
68

69
    public function equals(string|TypeReflector $type): bool
79✔
70
    {
71
        if (is_string($type)) {
79✔
72
            $type = new TypeReflector($type);
68✔
73
        }
74

75
        return $this->definition === $type->definition;
79✔
76
    }
77

78
    public function accepts(mixed $input): bool
713✔
79
    {
80
        if ($this->isNullable && $input === null) {
713✔
81
            return true;
1✔
82
        }
83

84
        if ($this->isBuiltIn()) {
712✔
85
            return match ($this->cleanDefinition) {
234✔
86
                'false' => $input === false,
×
87
                'mixed' => true,
×
88
                'never' => false,
×
89
                'true' => $input === true,
×
90
                'void' => false,
×
91
                default => self::BUILTIN_VALIDATION[$this->cleanDefinition]($input),
234✔
92
            };
234✔
93
        }
94

95
        if ($this->isClass()) {
708✔
96
            if (is_string($input)) {
707✔
97
                return $this->matches($input);
4✔
98
            }
99

100
            $cleanDefinition = $this->cleanDefinition;
707✔
101

102
            return $input instanceof $cleanDefinition;
707✔
103
        }
104

105
        if ($this->isIterable()) {
693✔
106
            return is_iterable($input);
×
107
        }
108

109
        if (str_contains($this->definition, '|')) {
693✔
110
            return array_any($this->split(), static fn ($type) => $type->accepts($input));
1✔
111
        }
112

113
        if (str_contains($this->definition, '&')) {
692✔
114
            return array_all($this->split(), static fn ($type) => $type->accepts($input));
×
115
        }
116

117
        return false;
692✔
118
    }
119

120
    public function matches(string $className): bool
731✔
121
    {
122
        return is_a($this->cleanDefinition, $className, true);
731✔
123
    }
124

125
    public function getName(): string
741✔
126
    {
127
        return $this->definition;
741✔
128
    }
129

130
    public function getShortName(): string
699✔
131
    {
132
        $parts = explode('\\', $this->definition);
699✔
133

134
        return $parts[array_key_last($parts)];
699✔
135
    }
136

137
    public function isBuiltIn(): bool
720✔
138
    {
139
        return isset(self::BUILTIN_VALIDATION[$this->cleanDefinition]);
720✔
140
    }
141

142
    public function isScalar(): bool
19✔
143
    {
144
        return in_array($this->cleanDefinition, self::SCALAR_TYPES, strict: true);
19✔
145
    }
146

147
    public function isClass(): bool
717✔
148
    {
149
        return class_exists($this->cleanDefinition);
717✔
150
    }
151

152
    public function isEnum(): bool
61✔
153
    {
154
        if ($this->matches(UnitEnum::class)) {
61✔
155
            return true;
17✔
156
        }
157

158
        return $this->matches(BackedEnum::class);
60✔
159
    }
160

161
    // TODO: should be refactored outside of the reflector component
162
    public function isRelation(): bool
70✔
163
    {
164
        return $this->isClass() && ! $this->isEnum() && ! $this->isIterable() && ! $this->isStringable() && ! $this->matches(DateTimeInterface::class);
70✔
165
    }
166

UNCOV
167
    public function isInterface(): bool
×
168
    {
169
        return interface_exists($this->cleanDefinition);
×
170
    }
171

172
    public function isIterable(): bool
700✔
173
    {
174
        if ($this->matches(Iterator::class)) {
700✔
175
            return true;
2✔
176
        }
177

178
        return in_array(
700✔
179
            $this->cleanDefinition,
700✔
180
            [
700✔
181
                'array',
700✔
182
                'iterable',
700✔
183
                Generator::class,
700✔
184
            ],
700✔
185
            strict: true,
700✔
186
        );
700✔
187
    }
188

189
    public function isStringable(): bool
60✔
190
    {
191
        if ($this->matches(Stringable::class)) {
60✔
192
            return true;
53✔
193
        }
194

195
        return in_array(
46✔
196
            $this->cleanDefinition,
46✔
197
            [
46✔
198
                'string',
46✔
199
            ],
46✔
200
            strict: true,
46✔
201
        );
46✔
202
    }
203

204
    public function isNullable(): bool
19✔
205
    {
206
        return $this->isNullable;
19✔
207
    }
208

209
    /** @return self[] */
210
    public function split(): array
716✔
211
    {
212
        return array_map(
716✔
213
            fn (string $part) => new self($part),
716✔
214
            preg_split('/[&|]/', $this->definition),
716✔
215
        );
716✔
216
    }
217

218
    private function resolveDefinition(PHPReflector|PHPReflectionType|string $reflector): string
766✔
219
    {
220
        if (is_string($reflector)) {
766✔
221
            return $reflector;
728✔
222
        }
223

224
        if ($reflector instanceof PHPReflectionParameter || $reflector instanceof PHPReflectionProperty) {
753✔
225
            return $this->resolveDefinition($reflector->getType());
736✔
226
        }
227

228
        if ($reflector instanceof PHPReflectionClass) {
753✔
229
            return $reflector->getName();
726✔
230
        }
231

232
        if ($reflector instanceof PHPReflectionNamedType) {
744✔
233
            return $reflector->getName();
744✔
234
        }
235

236
        if ($reflector instanceof PHPReflectionUnionType) {
695✔
237
            return implode('|', array_map(
694✔
238
                fn (PHPReflectionType $reflectionType) => $this->resolveDefinition($reflectionType),
694✔
239
                $reflector->getTypes(),
694✔
240
            ));
694✔
241
        }
242

243
        if ($reflector instanceof PHPReflectionIntersectionType) {
1✔
244
            return implode('&', array_map(
1✔
245
                fn (PHPReflectionType $reflectionType) => $this->resolveDefinition($reflectionType),
1✔
246
                $reflector->getTypes(),
1✔
247
            ));
1✔
248
        }
249

250
        throw new Exception('Could not resolve type');
×
251
    }
252

253
    private function resolveIsNullable(PHPReflectionType|PHPReflector|string $reflector): bool
766✔
254
    {
255
        if (is_string($reflector)) {
766✔
256
            return str_contains($this->definition, '?') || str_contains($this->definition, 'null');
728✔
257
        }
258

259
        if ($reflector instanceof PHPReflectionParameter || $reflector instanceof PHPReflectionProperty) {
753✔
260
            return $reflector->getType()->allowsNull();
736✔
261
        }
262

263
        if ($reflector instanceof PHPReflectionType) {
726✔
264
            return $reflector->allowsNull();
702✔
265
        }
266

267
        return false;
726✔
268
    }
269
}
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