Lines 0.00% 0 / 53
Methods 0.00% 0 / 2
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 set 0.00% 0 / 13 0.00% 0 / 1 56
 coerceValueToType 0.00% 0 / 40 0.00% 0 / 1 812
14class TypedProperty
15{
16    /**
17     * Sets a ReflectionProperty's value with automatic casting / coercion
18     * according to its declared type.
19     */
20    public static function set(object $object, ReflectionProperty $prop, mixed $value): void
21    {
22        $type = $prop->getType();
23
24        // Untyped or mixed → assign directly
25        if ($type === null || ($type instanceof ReflectionNamedType && $type->getName() === 'mixed')) {
26            $prop->setValue($object, $value);
27            return;
28        }
29
30        // Handle union types
31        if ($type instanceof ReflectionUnionType) {
32            foreach ($type->getTypes() as $innerType) {
33                try {
34                    $coerced = self::coerceValueToType($innerType, $value);
35                    $prop->setValue($object, $coerced);
36                    return;
37                } catch (TypeError) {
38                    // try next type
39                }
40            }
41            throw new TypeError("Value does not match any allowed union type for \${$prop->getName()}");
42        }
43
44        // Single type
45        $coerced = self::coerceValueToType($type, $value);
46        $prop->setValue($object, $coerced);
47    }
48
49    /**
50     * Coerces a value to the given ReflectionNamedType.
51     *
52     * @param ReflectionNamedType $type The target type
53     * @param mixed $value The value to coerce
54     * @return mixed The coerced value
55     * @throws TypeError If the value cannot be coerced to the target type
56     */
57    private static function coerceValueToType(ReflectionNamedType $type, mixed $value): mixed
58    {
59        $typeName = $type->getName();
60        $allowsNull = $type->allowsNull();
61
62        if ($value === null) {
63            if ($allowsNull) {
64                return null;
65            }
66
67            throw new TypeError("Cannot assign null to non-nullable type {$typeName}");
68        }
69
70        // Built-in scalar types
71        if ($type->isBuiltin()) {
72            return match ($typeName) {
73                'int' => (int) $value,
74                'float' => (float) $value,
75                'string' => (string) $value,
76                'bool' => (bool) $value,
77                'array' => (array) $value,
78                'object' => (object) $value,
79                'mixed' => $value,
80                'callable' => is_callable($value) ? $value
81                : throw new TypeError("Value is not callable"),
82                default => throw new TypeError("Unsupported builtin type: {$typeName}"),
83            };
84        }
85
86        // Handle enums
87        if (enum_exists($typeName)) {
88            // Already the correct enum instance — return as-is
89            if ($value instanceof $typeName) {
90                return $value;
91            }
92
93            $refEnum = new ReflectionEnum($typeName);
94            if ($refEnum->isBacked()) {
95                // Only pass values tryFrom can accept for the backing type,
96                // otherwise it throws a TypeError of its own.
97                $backingType = $refEnum->getBackingType()->getName();
98                if ((is_string($value) && $backingType === 'string')
99                    || (is_int($value) && $backingType === 'int')) {
100                    $case = $typeName::tryFrom($value);
101                    if ($case !== null) {
102                        return $case;
103                    }
104                }
105            } else {
106                foreach ($refEnum->getCases() as $case) {
107                    if ($case->getName() === $value) {
108                        // getValue() on a unit enum case returns the enum instance
109                        return $case->getValue();
110                    }
111                }
112            }
113            throw new TypeError("Invalid enum value for {$typeName}");
114        }
115
116        // Handle class/interface types
117        if (class_exists($typeName) || interface_exists($typeName)) {
118            if ($value instanceof $typeName) {
119                return $value;
120            }
121            // Optionally attempt to build object from array
122            if (is_array($value)) {
123                return new $typeName(...$value);
124            }
125            throw new TypeError("Value is not instance of {$typeName}");
126        }
127
128        throw new TypeError("Unsupported type: {$typeName}");
129    }
130}