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

voku / Arrayy / 24917960344

25 Apr 2026 12:27AM UTC coverage: 89.25% (-0.09%) from 89.336%
24917960344

push

github

web-flow
Merge pull request #159 from voku/copilot/add-native-properties-type-checking

Fix PHP 8.0 intersection-type CI failure, strengthen type-check coverage, and refresh PHP 8.0+ docs/CI matrix

143 of 164 new or added lines in 6 files covered. (87.2%)

12 existing lines in 1 file now uncovered.

2607 of 2921 relevant lines covered (89.25%)

238.01 hits per line

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

85.4
/src/TypeCheck/TypeCheckPhpDoc.php
1
<?php
2

3
/** @noinspection TransitiveDependenciesUsageInspection */
4
/** @noinspection ClassReImplementsParentInterfaceInspection */
5

6
declare(strict_types=1);
7

8
namespace Arrayy\TypeCheck;
9

10
use phpDocumentor\Reflection\Type;
11

12
/**
13
 * inspired by https://github.com/spatie/value-object
14
 *
15
 * @internal
16
 */
17
final class TypeCheckPhpDoc extends AbstractTypeCheck implements TypeCheckInterface
18
{
19
    /**
20
     * @var bool
21
     */
22
    private $hasTypeDeclaration = false;
23

24
    /**
25
     * @var string
26
     */
27
    private $property_name;
28

29
    /**
30
     * @param string $reflectionPropertyName
31
     */
32
    public function __construct($reflectionPropertyName)
33
    {
34
        $this->property_name = $reflectionPropertyName;
96✔
35
    }
36

37
    /**
38
     * @param \phpDocumentor\Reflection\DocBlock\Tags\Property $phpDocumentorReflectionProperty
39
     * @param string                                           $property
40
     *
41
     * @return self|null
42
     */
43
    public static function fromPhpDocumentorProperty(\phpDocumentor\Reflection\DocBlock\Tags\Property $phpDocumentorReflectionProperty, string $property = '')
44
    {
45
        if (!$property) {
49✔
46
            /** @var string|null $propertyTmp */
47
            $propertyTmp = $phpDocumentorReflectionProperty->getVariableName();
14✔
48
            if ($propertyTmp === null) {
14✔
49
                return null;
×
50
            }
51

52
            $property = $propertyTmp;
14✔
53
        }
54

55
        $tmpObject = new \stdClass();
49✔
56
        $tmpObject->{$property} = null;
49✔
57

58
        $tmpReflection = new self((new \ReflectionProperty($tmpObject, $property))->getName());
49✔
59

60
        $type = $phpDocumentorReflectionProperty->getType();
49✔
61

62
        /** @noinspection PhpSillyAssignmentInspection */
63
        /** @var Type|null $type */
64
        $type = $type;
49✔
65

66
        if ($type) {
49✔
67
            $tmpReflection->hasTypeDeclaration = true;
49✔
68

69
            $docTypes = self::parseDocTypeObject($type);
49✔
70
            if (\is_array($docTypes) === true) {
49✔
71
                foreach ($docTypes as $docType) {
14✔
72
                    $tmpReflection->types[] = $docType;
14✔
73
                }
74
            } else {
75
                $tmpReflection->types[] = $docTypes;
49✔
76
            }
77

78
            if (\in_array('null', $tmpReflection->types, true)) {
49✔
79
                $tmpReflection->isNullable = true;
21✔
80
            }
81
        }
82

83
        return $tmpReflection;
49✔
84
    }
85

86
    public static function fromReflectionProperty(\ReflectionProperty $reflectionProperty): self
87
    {
88
        $tmpReflection = new self($reflectionProperty->getName());
47✔
89
        $type = $reflectionProperty->getType();
47✔
90
        $docTypes = self::getTypesFromReflectionPropertyDocBlock($reflectionProperty);
47✔
91

92
        if ($docTypes !== null) {
47✔
93
            $tmpReflection->hasTypeDeclaration = true;
33✔
94

95
            if (\is_array($docTypes) === true) {
33✔
96
                foreach ($docTypes as $docType) {
7✔
97
                    $tmpReflection->types[] = $docType;
7✔
98
                }
99
            } else {
100
                $tmpReflection->types[] = $docTypes;
29✔
101
            }
102
        } elseif ($type === null) {
28✔
103
            $tmpReflection->types[] = 'mixed';
7✔
104
            $tmpReflection->isNullable = true;
7✔
105

106
            return $tmpReflection;
7✔
107
        } else {
108
            $tmpReflection->hasTypeDeclaration = true;
21✔
109

110
            $docTypes = self::parseReflectionTypeObject($type);
21✔
111
            if (\is_array($docTypes) === true) {
21✔
112
                foreach ($docTypes as $docType) {
14✔
113
                    $tmpReflection->types[] = $docType;
14✔
114
                }
115
            } else {
116
                $tmpReflection->types[] = $docTypes;
21✔
117
            }
118
        }
119

120
        if ($type !== null && self::typeAllowsNull($type) && \in_array('null', $tmpReflection->types, true) === false) {
40✔
121
            $tmpReflection->types[] = 'null';
21✔
122
        }
123

124
        if (\in_array('null', $tmpReflection->types, true)) {
40✔
125
            $tmpReflection->isNullable = true;
21✔
126
        }
127

128
        return $tmpReflection;
40✔
129
    }
130

131
    /**
132
     * @param \phpDocumentor\Reflection\Type $type
133
     *
134
     * @return string|string[]
135
     */
136
    public static function parseDocTypeObject($type)
137
    {
138
        if ($type instanceof \phpDocumentor\Reflection\Types\Object_) {
82✔
139
            $tmpObject = (string) $type->getFqsen();
28✔
140
            if ($tmpObject) {
28✔
141
                return $tmpObject;
28✔
142
            }
143

144
            return 'object';
×
145
        }
146

147
        if ($type instanceof \phpDocumentor\Reflection\Types\Compound) {
75✔
148
            $types = [];
21✔
149
            foreach ($type as $subType) {
21✔
150
                $typeTmp = self::parseDocTypeObject($subType);
21✔
151

152
                /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
153
                /** @var string $typeTmp */
154
                $typeTmp = $typeTmp;
21✔
155

156
                $types[] = $typeTmp;
21✔
157
            }
158

159
            return $types;
21✔
160
        }
161

162
        if ($type instanceof \phpDocumentor\Reflection\Types\Array_) {
75✔
163
            $valueTypeTmp = $type->getValueType()->__toString();
42✔
164
            if ($valueTypeTmp !== 'mixed') {
42✔
165
                return $valueTypeTmp . '[]';
42✔
166
            }
167

168
            return 'array';
×
169
        }
170

171
        if ($type instanceof \phpDocumentor\Reflection\Types\Null_) {
61✔
172
            return 'null';
21✔
173
        }
174

175
        if ($type instanceof \phpDocumentor\Reflection\Types\Mixed_) {
61✔
176
            return 'mixed';
7✔
177
        }
178

179
        foreach (self::getScalarPseudoTypeClasses() as $scalarTypeClass) {
61✔
180
            if ($type instanceof $scalarTypeClass) {
61✔
181
                return 'string|int|float|bool';
21✔
182
            }
183
        }
184

185
        if ($type instanceof \phpDocumentor\Reflection\Types\Boolean) {
54✔
186
            return 'bool';
7✔
187
        }
188

189
        if ($type instanceof \phpDocumentor\Reflection\Types\Callable_) {
54✔
190
            return 'callable';
7✔
191
        }
192

193
        if ($type instanceof \phpDocumentor\Reflection\Types\Float_) {
47✔
194
            return 'float';
7✔
195
        }
196

197
        if ($type instanceof \phpDocumentor\Reflection\Types\String_) {
47✔
198
            return 'string';
35✔
199
        }
200

201
        if ($type instanceof \phpDocumentor\Reflection\Types\Integer) {
33✔
202
            return 'int';
21✔
203
        }
204

205
        if ($type instanceof \phpDocumentor\Reflection\Types\Void_) {
12✔
206
            return 'void';
×
207
        }
208

209
        if ($type instanceof \phpDocumentor\Reflection\Types\Resource_) {
12✔
210
            return 'resource';
7✔
211
        }
212

213
        return $type->__toString();
5✔
214
    }
215

216
    /**
217
     * @return list<class-string>
218
     */
219
    private static function getScalarPseudoTypeClasses(): array
220
    {
221
        $classes = [];
61✔
222

223
        foreach (
224
            [
61✔
225
                '\phpDocumentor\Reflection\PseudoTypes\Scalar',
61✔
226
                '\phpDocumentor\Reflection\Types\Scalar',
61✔
227
            ] as $className
61✔
228
        ) {
229
            if (\class_exists($className)) {
61✔
230
                $classes[] = $className;
61✔
231
            }
232
        }
233

234
        return $classes;
61✔
235
    }
236

237
    /**
238
     * @return string|string[]|null
239
     */
240
    private static function getTypesFromReflectionPropertyDocBlock(\ReflectionProperty $reflectionProperty)
241
    {
242
        $docComment = $reflectionProperty->getDocComment();
47✔
243
        if ($docComment === false) {
47✔
244
            return null;
28✔
245
        }
246

247
        /** @var \phpDocumentor\Reflection\DocBlockFactoryInterface|null $factory cache factory to avoid recreating it per reflected property */
248
        static $factory = null;
33✔
249
        if ($factory === null) {
33✔
250
            $factory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
7✔
251
        }
252
        $docblock = $factory->create($docComment);
33✔
253
        foreach ($docblock->getTagsByName('var') as $tag) {
33✔
254
            if (!$tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Var_) {
33✔
NEW
255
                continue;
×
256
            }
257

258
            /** @var \phpDocumentor\Reflection\Type|null $type */
259
            $type = $tag->getType();
33✔
260
            if ($type === null) {
33✔
NEW
261
                continue;
×
262
            }
263

264
            return self::parseDocTypeObject($type);
33✔
265
        }
266

NEW
267
        return null;
×
268
    }
269

270
    private static function typeAllowsNull(\ReflectionType $type): bool
271
    {
272
        if (
273
            \class_exists(\ReflectionIntersectionType::class)
33✔
274
            &&
275
            $type instanceof \ReflectionIntersectionType
33✔
276
        ) {
277
            return false;
5✔
278
        }
279

280
        return $type->allowsNull();
28✔
281
    }
282

283
    /**
284
     * @param \ReflectionType $type
285
     *
286
     * @return string|string[]
287
     */
288
    public static function parseReflectionTypeObject(\ReflectionType $type)
289
    {
290
        if ($type instanceof \ReflectionNamedType) {
21✔
291
            $typeName = $type->getName();
21✔
292

293
            if ($type->isBuiltin()) {
21✔
294
                return $typeName;
21✔
295
            }
296

297
            return '\\' . \ltrim($typeName, '\\');
14✔
298
        }
299

300
        if ($type instanceof \ReflectionUnionType) {
14✔
301
            $types = [];
14✔
302
            foreach ($type->getTypes() as $subType) {
14✔
303
                $typeTmp = self::parseReflectionTypeObject($subType);
14✔
304
                if (\is_array($typeTmp)) {
14✔
NEW
305
                    foreach ($typeTmp as $typeTmpInner) {
×
NEW
306
                        $types[] = $typeTmpInner;
×
307
                    }
308
                } else {
309
                    $types[] = $typeTmp;
14✔
310
                }
311
            }
312

313
            return $types;
14✔
314
        }
315

316
        if (
NEW
317
            \class_exists(\ReflectionIntersectionType::class)
×
318
            &&
NEW
319
            $type instanceof \ReflectionIntersectionType
×
320
        ) {
NEW
321
            $types = [];
×
NEW
322
            foreach ($type->getTypes() as $subType) {
×
NEW
323
                $typeTmp = self::parseReflectionTypeObject($subType);
×
NEW
324
                if (\is_array($typeTmp)) {
×
NEW
325
                    foreach ($typeTmp as $typeTmpInner) {
×
NEW
326
                        $types[] = $typeTmpInner;
×
327
                    }
328
                } else {
NEW
329
                    $types[] = $typeTmp;
×
330
                }
331
            }
332

NEW
333
            return \implode('&', $types);
×
334
        }
335

NEW
336
        return (string) $type;
×
337
    }
338

339
    /**
340
     * @param string $expectedTypes
341
     * @param mixed  $value
342
     * @param string $type
343
     *
344
     * @return \TypeError
345
     */
346
    public function throwException($expectedTypes, $value, $type): \Throwable
347
    {
348
        throw new \TypeError("Invalid type: expected \"{$this->property_name}\" to be of type {{$expectedTypes}}, instead got value \"" . $this->valueToString($value) . '" (' . \print_r($value, true) . ") with type {{$type}}.");
140✔
349
    }
350
}
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