Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.66% covered (warning)
69.66%
62 / 89
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
TypeUtils
69.66% covered (warning)
69.66%
62 / 89
0.00% covered (danger)
0.00%
0 / 4
112.33
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 findAttributes
75.00% covered (warning)
75.00%
21 / 28
0.00% covered (danger)
0.00%
0 / 1
6.56
 allowEmptyString
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
13.10
 couldBeAStream
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 matchesType
67.86% covered (warning)
67.86%
19 / 28
0.00% covered (danger)
0.00%
0 / 1
38.07
1<?php
2namespace Apie\Core;
3
4use Apie\Core\Utils\ConverterUtils;
5use Apie\Core\ValueObjects\Interfaces\ValueObjectInterface;
6use Psr\Http\Message\UploadedFileInterface;
7use ReflectionClass;
8use ReflectionIntersectionType;
9use ReflectionNamedType;
10use ReflectionType;
11use ReflectionUnionType;
12use Throwable;
13
14final class TypeUtils
15{
16    /**
17     * @codeCoverageIgnore
18     */
19    private function __construct()
20    {
21    }
22
23    /**
24     * @var array<string, object>
25     */
26    private static array $alreadyChecked = [];
27
28    /**
29     * @template T of object
30     *
31     * @param ReflectionClass<object> $class
32     * @param class-string<T> $attributeClass
33     * @return T[]
34     */
35    public static function findAttributes(ReflectionClass $class, string $attributeClass): array
36    {
37        $key = $class->name . ':' . $attributeClass;
38        if (!isset(self::$alreadyChecked[$key])) {
39            self::$alreadyChecked[$key] = [];
40            foreach ($class->getAttributes($attributeClass) as $input) {
41                self::$alreadyChecked[$key][] = $input->newInstance();
42            }
43            foreach ($class->getInterfaceNames() as $interfaceName) {
44                array_push(
45                    self::$alreadyChecked[$key],
46                    ...self::findAttributes(
47                        new ReflectionClass($interfaceName),
48                        $attributeClass
49                    )
50                );
51            }
52            foreach ($class->getTraitNames() as $traitName) {
53                array_push(
54                    self::$alreadyChecked[$key],
55                    ...self::findAttributes(
56                        new ReflectionClass($traitName),
57                        $attributeClass
58                    )
59                );
60            }
61            $parent = $class->getParentClass();
62            if ($parent) {
63                array_push(
64                    self::$alreadyChecked[$key],
65                    ...self::findAttributes($parent, $attributeClass)
66                );
67            }
68        }
69        return self::$alreadyChecked[$key];
70    }
71
72    public static function allowEmptyString(
73        ?ReflectionType $type
74    ): bool {
75        if ($type === null) {
76            return true;
77        }
78        if ($type instanceof ReflectionNamedType) {
79            if ($type->getName() === 'string' || $type->getName() === 'mixed') {
80                return true;
81            }
82            $class = ConverterUtils::toReflectionClass($type);
83            if (!$class) {
84                return false;
85            }
86            if (in_array(ValueObjectInterface::class, $class->getInterfaceNames())) {
87                try {
88                    $class->getMethod('fromNative')->invoke(null, '');
89                    return true;
90                } catch (Throwable) {
91                    return false;
92                }
93            }
94            return false;
95        }
96        if ($type instanceof ReflectionIntersectionType) {
97            foreach ($type->getTypes() as $type) {
98                if (!self::allowEmptyString($type)) {
99                    return false;
100                }
101            }
102            return true;
103        }
104        assert($type instanceof ReflectionUnionType);
105        foreach ($type->getTypes() as $type) {
106            if (self::allowEmptyString($type)) {
107                return true;
108            }
109        }
110        return false;
111    }
112
113    public static function couldBeAStream(
114        ?ReflectionType $type
115    ): bool {
116        if ($type === null) {
117            return true;
118        }
119        if ($type instanceof ReflectionNamedType) {
120            return in_array($type->getName(), ['mixed', 'resource', UploadedFileInterface::class]);
121        }
122        assert($type instanceof ReflectionIntersectionType || $type instanceof ReflectionUnionType);
123    
124        foreach ($type->getTypes() as $type) {
125            if (self::couldBeAStream($type)) {
126                return true;
127            }
128        }
129        return false;
130    }
131
132    public static function matchesType(
133        ?ReflectionType $type,
134        mixed $input
135    ): bool {
136        if ($type === null) {
137            return true;
138        }
139        if ($input === null && $type->allowsNull()) {
140            return true;
141        }
142        if ($type instanceof ReflectionNamedType) {
143            return match ($type->getName()) {
144                'mixed' => true,
145                'bool' => is_bool($input),
146                'int' => is_int($input),
147                'string' => is_string($input),
148                'null' => $input === null,
149                'true' => $input === true,
150                'false' => $input === false,
151                default => get_debug_type($input) === $type->getName()
152                    || ((interface_exists($type->getName()) || class_exists($type->getName()))
153                        && is_object($input)
154                        && (new ReflectionClass($type->getName()))->isInstance($input))
155            };
156        }
157        if ($type instanceof ReflectionUnionType) {
158            foreach ($type->getTypes() as $type) {
159                if (self::matchesType($type, $input)) {
160                    return true;
161                }
162            }
163            return false;
164        }
165        assert($type instanceof ReflectionIntersectionType);
166        foreach ($type->getTypes() as $type) {
167            if (!self::matchesType($type, $input)) {
168                return false;
169            }
170        }
171        return true;
172    }
173}