Passed
Push — master ( 56f7f0...182f6a )
by Gareth
02:15
created

TypeConverter::convertToType()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.0178

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 13
nc 6
nop 2
dl 0
loc 27
ccs 13
cts 14
cp 0.9286
crap 7.0178
rs 8.8333
c 1
b 0
f 0
1
<?php
2
3
namespace garethp\ews\API;
4
5
/**
6
 * Type converter for handling SOAP stdClass objects
7
 * Converts stdClass objects to proper EWS type objects
8
 */
9
class TypeConverter
10
{
11
    /**
12
     * Convert stdClass objects to proper EWS type objects
13
     *
14
     * @param object $object The object containing the property
15
     * @param mixed $value The value to convert
16
     * @param string $propertyName The property name
17
     * @return mixed Converted value
18
     */
19 46
    public static function convertValueToExpectedType($object, $value, string $propertyName)
20
    {
21 46
        if (!($value instanceof \stdClass || (is_array($value) && current($value) instanceof \stdClass))) {
22 41
            return $value;
23
        }
24
25 9
        $type = self::getSetterType($object, $propertyName);
26 9
        return $type ? self::convertToType($value, $type) : $value;
27
    }
28
29
    /**
30
     * Convert stdClass objects to proper EWS types using reflection
31
     *
32
     * @param mixed $value The value to convert
33
     * @param string $targetType The expected EWS type class name
34
     * @return mixed Converted value or original if no conversion needed
35
     */
36 9
    public static function convertToType($value, string $type)
37
    {
38 9
        if (!class_exists($type)) {
39
            return $value;
40
        }
41
42 9
        if (is_array($value)) {
43 5
            return array_map(fn($v) => self::convertToType($v, $type), $value);
44
        }
45
46 9
        if (!($value instanceof \stdClass)) {
47 1
            return $value;
48
        }
49
50
        // We'll sometimes get objects where they only hold a single key that, rather than matching the type that it
51
        // goes into it'll be containing the items that the type expects.
52 9
        $valueProperties = get_object_vars($value);
53 9
        if (count($valueProperties) === 1 && !property_exists($type, lcfirst(key($valueProperties)))) {
54 1
            return self::convertToType($value->{key($valueProperties)}, $type);
55
        }
56
57 9
        $object = new $type();
58 9
        foreach ($valueProperties as $prop => $val) {
59 8
            self::setProperty($object, $prop, $val);
60
        }
61
62 9
        return $object;
63
    }
64
65
    /**
66
     * Get the setter method type for a property
67
     *
68
     * @param object $object The object to inspect
69
     * @param string $property The property name
70
     * @return string|null The expected type or null if no setter exists
71
     */
72 9
    private static function getSetterType($object, string $property): ?string
73
    {
74 9
        $method = 'set' . ucfirst($property);
75 9
        if (!method_exists($object, $method)) {
76
            return null;
77
        }
78
79 9
        $type = (new \ReflectionMethod($object, $method))
80 9
            ->getParameters()[0]?->getType();
81
82 9
        if ($type instanceof \ReflectionUnionType) {
83 5
            foreach ($type->getTypes() as $t) {
84 5
                if (!$t->isBuiltin() && $t->getName() !== 'array') {
85 5
                    return $t->getName();
86
                }
87
            }
88
        }
89
90 4
        return ($type instanceof \ReflectionNamedType && !$type->isBuiltin())
91 4
            ? $type->getName()
92 4
            : null;
93
    }
94
95
    /**
96
     * Set property on target object using setter method or direct assignment
97
     *
98
     * @param object $instance Target object
99
     * @param string $property Property name
100
     * @param mixed $value Property value
101
     */
102 8
    private static function setProperty(object $instance, string $property, $value): void
103
    {
104 8
        $setter = 'set' . ucfirst($property);
105 8
        if (!method_exists($instance, $setter)) {
106 5
            if (property_exists($instance, $property)) {
107 5
                $instance->$property = $value;
108
            }
109 5
            return;
110
        }
111
112 8
        $type = (new \ReflectionMethod($instance, $setter))->getParameters()[0]?->getType();
113 8
        $value = self::convertIfStdClass($value, $type);
114 8
        $instance->$setter($value);
115
    }
116
117 8
    private static function convertIfStdClass($value, ?\ReflectionType $type)
118
    {
119 8
        if (!($value instanceof \stdClass) || !$type) {
120 8
            return $value;
121
        }
122
123 2
        if ($type instanceof \ReflectionUnionType) {
124 1
            foreach ($type->getTypes() as $t) {
125 1
                $name = $t->getName();
126 1
                if (!$t->isBuiltin() && class_exists($name)) {
127 1
                    return self::convertToType($value, $name);
128
                }
129
                if ($name === 'array') {
130
                    $props = get_object_vars($value);
131
                    $arr = count($props) === 1 ? current($props) : (array)$value;
132
                    return is_array($arr) ? $arr : [$arr];
133
                }
134
            }
135
        }
136
137 2
        if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
138 2
            return self::convertToType($value, $type->getName());
139
        }
140
141
        return $value;
142
    }
143
}
144