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

voku / Arrayy / 24922580098

25 Apr 2026 04:29AM UTC coverage: 92.372% (+3.1%) from 89.25%
24922580098

Pull #162

github

web-flow
Merge 6e1aa2682 into e1b421950
Pull Request #162: Increase regression coverage for Json mapper, collection helpers, and array-shape property contracts

149 of 167 new or added lines in 3 files covered. (89.22%)

2749 of 2976 relevant lines covered (92.37%)

249.91 hits per line

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

84.62
/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
/**
11
 * inspired by https://github.com/spatie/value-object
12
 *
13
 * @internal
14
 */
15
final class TypeCheckPhpDoc extends AbstractTypeCheck implements TypeCheckInterface
16
{
17
    /**
18
     * @var bool
19
     */
20
    private $hasTypeDeclaration = false;
21

22
    /**
23
     * @var string
24
     */
25
    private $property_name;
26

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

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

50
            $property = $propertyTmp;
14 ✔
51
        }
52

53
        return self::fromDocTypeObject($property, $phpDocumentorReflectionProperty->getType());
56 ✔
54
    }
55

56
    /**
57
     * Create a type checker from a phpDocumentor type object and an explicit property name.
58
     *
59
     * @param string                              $property
60
     * @param \phpDocumentor\Reflection\Type|null $type
61
     *
62
     * @phpstan-param \phpDocumentor\Reflection\Type|null $type
63
     *
64
     * @return self
65
     */
66
    public static function fromDocTypeObject(string $property, $type)
67
    {
68
        $tmpReflection = new self($property);
133 ✔
69

70
        if ($type) {
133 ✔
71
            $tmpReflection->hasTypeDeclaration = true;
126 ✔
72

73
            $docTypes = self::parseDocTypeObject($type);
126 ✔
74
            if (\is_array($docTypes) === true) {
126 ✔
75
                foreach ($docTypes as $docType) {
35 ✔
76
                    $tmpReflection->types[] = $docType;
35 ✔
77
                }
78
            } else {
79
                $tmpReflection->types[] = $docTypes;
119 ✔
80
            }
81

82
            if (\in_array('null', $tmpReflection->types, true)) {
126 ✔
83
                $tmpReflection->isNullable = true;
42 ✔
84
            }
85
        }
86

87
        return $tmpReflection;
133 ✔
88
    }
89

90
    public static function fromReflectionProperty(\ReflectionProperty $reflectionProperty): self
91
    {
92
        $tmpReflection = new self($reflectionProperty->getName());
47 ✔
93
        $type = $reflectionProperty->getType();
47 ✔
94
        $docTypes = self::getTypesFromReflectionPropertyDocBlock($reflectionProperty);
47 ✔
95

96
        if ($docTypes !== null) {
47 ✔
97
            $tmpReflection->hasTypeDeclaration = true;
33 ✔
98

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

110
            return $tmpReflection;
7 ✔
111
        } else {
112
            $tmpReflection->hasTypeDeclaration = true;
21 ✔
113

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

124
        if ($type !== null && self::typeAllowsNull($type) && \in_array('null', $tmpReflection->types, true) === false) {
40 ✔
125
            $tmpReflection->types[] = 'null';
21 ✔
126
        }
127

128
        if (\in_array('null', $tmpReflection->types, true)) {
40 ✔
129
            $tmpReflection->isNullable = true;
21 ✔
130
        }
131

132
        return $tmpReflection;
40 ✔
133
    }
134

135
    /**
136
     * @param \phpDocumentor\Reflection\Type $type
137
     *
138
     * @return string|string[]
139
     */
140
    public static function parseDocTypeObject($type)
141
    {
142
        if ($type instanceof \phpDocumentor\Reflection\Types\Object_) {
159 ✔
143
            $tmpObject = (string) $type->getFqsen();
56 ✔
144
            if ($tmpObject) {
56 ✔
145
                return $tmpObject;
56 ✔
146
            }
147

148
            return 'object';
×
149
        }
150

151
        if ($type instanceof \phpDocumentor\Reflection\Types\Compound) {
145 ✔
152
            $types = [];
35 ✔
153
            foreach ($type as $subType) {
35 ✔
154
                $typeTmp = self::parseDocTypeObject($subType);
35 ✔
155

156
                /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
157
                /** @var string $typeTmp */
158
                $typeTmp = $typeTmp;
35 ✔
159

160
                $types[] = $typeTmp;
35 ✔
161
            }
162

163
            return $types;
35 ✔
164
        }
165

166
        if ($type instanceof \phpDocumentor\Reflection\Types\Nullable) {
145 ✔
167
            $typeTmp = self::parseDocTypeObject($type->getActualType());
7 ✔
168
            if (\is_array($typeTmp) === true) {
7 ✔
NEW
169
                $typeTmp[] = 'null';
×
170

NEW
171
                return $typeTmp;
×
172
            }
173

174
            return [$typeTmp, 'null'];
7 ✔
175
        }
176

177
        if (
178
            \class_exists('\phpDocumentor\Reflection\PseudoTypes\ArrayShape')
138 ✔
179
            &&
180
            $type instanceof \phpDocumentor\Reflection\PseudoTypes\ArrayShape
138 ✔
181
        ) {
182
            return 'array';
7 ✔
183
        }
184

185
        if ($type instanceof \phpDocumentor\Reflection\Types\Array_) {
131 ✔
186
            $valueTypeTmp = $type->getValueType()->__toString();
49 ✔
187
            if ($valueTypeTmp !== 'mixed') {
49 ✔
188
                return $valueTypeTmp . '[]';
49 ✔
189
            }
190

191
            return 'array';
×
192
        }
193

194
        if ($type instanceof \phpDocumentor\Reflection\Types\Null_) {
117 ✔
195
            return 'null';
35 ✔
196
        }
197

198
        if ($type instanceof \phpDocumentor\Reflection\Types\Mixed_) {
117 ✔
199
            return 'mixed';
7 ✔
200
        }
201

202
        foreach (self::getScalarPseudoTypeClasses() as $scalarTypeClass) {
117 ✔
203
            if ($type instanceof $scalarTypeClass) {
117 ✔
204
                return 'string|int|float|bool';
21 ✔
205
            }
206
        }
207

208
        if ($type instanceof \phpDocumentor\Reflection\Types\Boolean) {
110 ✔
209
            return 'bool';
7 ✔
210
        }
211

212
        if ($type instanceof \phpDocumentor\Reflection\Types\Callable_) {
110 ✔
213
            return 'callable';
7 ✔
214
        }
215

216
        if ($type instanceof \phpDocumentor\Reflection\Types\Float_) {
103 ✔
217
            return 'float';
7 ✔
218
        }
219

220
        if ($type instanceof \phpDocumentor\Reflection\Types\String_) {
103 ✔
221
            return 'string';
56 ✔
222
        }
223

224
        if ($type instanceof \phpDocumentor\Reflection\Types\Integer) {
82 ✔
225
            return 'int';
70 ✔
226
        }
227

228
        if ($type instanceof \phpDocumentor\Reflection\Types\Void_) {
12 ✔
229
            return 'void';
×
230
        }
231

232
        if ($type instanceof \phpDocumentor\Reflection\Types\Resource_) {
12 ✔
233
            return 'resource';
7 ✔
234
        }
235

236
        return $type->__toString();
5 ✔
237
    }
238

239
    /**
240
     * @return list<class-string>
241
     */
242
    private static function getScalarPseudoTypeClasses(): array
243
    {
244
        $classes = [];
117 ✔
245

246
        foreach (
247
            [
117 ✔
248
                '\phpDocumentor\Reflection\PseudoTypes\Scalar',
117 ✔
249
                '\phpDocumentor\Reflection\Types\Scalar',
117 ✔
250
            ] as $className
117 ✔
251
        ) {
252
            if (\class_exists($className)) {
117 ✔
253
                $classes[] = $className;
117 ✔
254
            }
255
        }
256

257
        return $classes;
117 ✔
258
    }
259

260
    /**
261
     * @return string|string[]|null
262
     */
263
    private static function getTypesFromReflectionPropertyDocBlock(\ReflectionProperty $reflectionProperty)
264
    {
265
        $docComment = $reflectionProperty->getDocComment();
47 ✔
266
        if ($docComment === false) {
47 ✔
267
            return null;
28 ✔
268
        }
269

270
        /** @var \phpDocumentor\Reflection\DocBlockFactoryInterface|null $factory cache factory to avoid recreating it per reflected property */
271
        static $factory = null;
33 ✔
272
        if ($factory === null) {
33 ✔
273
            $factory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
7 ✔
274
        }
275
        $docblock = $factory->create($docComment);
33 ✔
276
        foreach ($docblock->getTagsByName('var') as $tag) {
33 ✔
277
            if (!$tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Var_) {
33 ✔
278
                continue;
×
279
            }
280

281
            /** @var \phpDocumentor\Reflection\Type|null $type */
282
            $type = $tag->getType();
33 ✔
283
            if ($type === null) {
33 ✔
284
                continue;
×
285
            }
286

287
            return self::parseDocTypeObject($type);
33 ✔
288
        }
289

290
        return null;
×
291
    }
292

293
    private static function typeAllowsNull(\ReflectionType $type): bool
294
    {
295
        if (
296
            \class_exists(\ReflectionIntersectionType::class)
33 ✔
297
            &&
298
            $type instanceof \ReflectionIntersectionType
33 ✔
299
        ) {
300
            return false;
5 ✔
301
        }
302

303
        return $type->allowsNull();
28 ✔
304
    }
305

306
    /**
307
     * @param \ReflectionType $type
308
     *
309
     * @return string|string[]
310
     */
311
    public static function parseReflectionTypeObject(\ReflectionType $type)
312
    {
313
        if ($type instanceof \ReflectionNamedType) {
21 ✔
314
            $typeName = $type->getName();
21 ✔
315

316
            if ($type->isBuiltin()) {
21 ✔
317
                return $typeName;
21 ✔
318
            }
319

320
            return '\\' . \ltrim($typeName, '\\');
14 ✔
321
        }
322

323
        if ($type instanceof \ReflectionUnionType) {
14 ✔
324
            $types = [];
14 ✔
325
            foreach ($type->getTypes() as $subType) {
14 ✔
326
                $typeTmp = self::parseReflectionTypeObject($subType);
14 ✔
327
                if (\is_array($typeTmp)) {
14 ✔
328
                    foreach ($typeTmp as $typeTmpInner) {
×
329
                        $types[] = $typeTmpInner;
×
330
                    }
331
                } else {
332
                    $types[] = $typeTmp;
14 ✔
333
                }
334
            }
335

336
            return $types;
14 ✔
337
        }
338

339
        if (
340
            \class_exists(\ReflectionIntersectionType::class)
×
341
            &&
342
            $type instanceof \ReflectionIntersectionType
×
343
        ) {
344
            $types = [];
×
345
            foreach ($type->getTypes() as $subType) {
×
346
                $typeTmp = self::parseReflectionTypeObject($subType);
×
347
                if (\is_array($typeTmp)) {
×
348
                    foreach ($typeTmp as $typeTmpInner) {
×
349
                        $types[] = $typeTmpInner;
×
350
                    }
351
                } else {
352
                    $types[] = $typeTmp;
×
353
                }
354
            }
355

356
            return \implode('&', $types);
×
357
        }
358

359
        return (string) $type;
×
360
    }
361

362
    /**
363
     * @param string $expectedTypes
364
     * @param mixed  $value
365
     * @param string $type
366
     *
367
     * @return \TypeError
368
     */
369
    public function throwException($expectedTypes, $value, $type): \Throwable
370
    {
371
        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}}.");
189 ✔
372
    }
373
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc