Passed
Pull Request — master (#278)
by Gareth
07:28
created

TypeConverter::setProperty()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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