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

tempestphp / tempest-framework / 14049246919

24 Mar 2025 09:42PM UTC coverage: 79.353% (-0.04%) from 79.391%
14049246919

push

github

web-flow
feat(support): support array parameters in string manipulations (#1073)

48 of 48 new or added lines in 2 files covered. (100.0%)

735 existing lines in 126 files now uncovered.

10492 of 13222 relevant lines covered (79.35%)

90.78 hits per line

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

97.44
/src/Tempest/Mapper/src/Mappers/ArrayToObjectMapper.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Mapper\Mappers;
6

7
use Tempest\Mapper\CasterFactory;
8
use Tempest\Mapper\Exceptions\MissingValuesException;
9
use Tempest\Mapper\MapFrom;
10
use Tempest\Mapper\Mapper;
11
use Tempest\Mapper\Strict;
12
use Tempest\Reflection\ClassReflector;
13
use Tempest\Reflection\PropertyReflector;
14
use Throwable;
15

16
use function Tempest\Support\arr;
17

18
final readonly class ArrayToObjectMapper implements Mapper
19
{
20
    public function __construct(
126✔
21
        private CasterFactory $casterFactory,
22
    ) {}
126✔
23

24
    public function canMap(mixed $from, mixed $to): bool
122✔
25
    {
26
        if (! is_array($from)) {
122✔
27
            return false;
4✔
28
        }
29

30
        try {
31
            $class = new ClassReflector($to);
121✔
32

33
            return $class->isInstantiable();
120✔
34
        } catch (Throwable) {
1✔
35
            return false;
1✔
36
        }
37
    }
38

39
    public function map(mixed $from, mixed $to): object
124✔
40
    {
41
        $class = new ClassReflector($to);
124✔
42

43
        $object = $this->resolveObject($to);
124✔
44

45
        $missingValues = [];
124✔
46

47
        /** @var PropertyReflector[] $unsetProperties */
48
        $unsetProperties = [];
124✔
49

50
        $from = arr($from)->undot()->toArray();
124✔
51

52
        $isStrictClass = $class->hasAttribute(Strict::class);
124✔
53

54
        foreach ($class->getPublicProperties() as $property) {
124✔
55
            if ($property->isVirtual()) {
124✔
56
                continue;
40✔
57
            }
58

59
            $propertyName = $this->resolvePropertyName($property, $from);
124✔
60

61
            if (! array_key_exists($propertyName, $from)) {
124✔
62
                $isStrictProperty = $isStrictClass || $property->hasAttribute(Strict::class);
74✔
63

64
                if ($property->hasDefaultValue()) {
74✔
65
                    continue;
70✔
66
                }
67

68
                if ($isStrictProperty) {
7✔
69
                    $missingValues[] = $propertyName;
2✔
70
                } else {
71
                    $unsetProperties[] = $property;
6✔
72
                }
73

74
                continue;
7✔
75
            }
76

77
            $value = $this->resolveValue($property, $from[$propertyName]);
120✔
78

79
            $property->setValue($object, $value);
120✔
80
        }
81

82
        if ($missingValues !== []) {
124✔
83
            throw new MissingValuesException($to, $missingValues);
2✔
84
        }
85

86
        $this->setParentRelations($object, $class);
122✔
87

88
        // Non-strict properties that weren't passed are unset,
89
        // which means that they can now be accessed via `__get`
90
        foreach ($unsetProperties as $property) {
122✔
91
            if ($property->isVirtual()) {
5✔
UNCOV
92
                continue;
×
93
            }
94

95
            $property->unset($object);
5✔
96
        }
97

98
        return $object;
122✔
99
    }
100

101
    /**
102
     * @param array<mixed> $from
103
     */
104
    private function resolvePropertyName(PropertyReflector $property, array $from): string
124✔
105
    {
106
        $mapFrom = $property->getAttribute(MapFrom::class);
124✔
107

108
        if ($mapFrom !== null) {
124✔
109
            return arr($from)->keys()->intersect($mapFrom->names)->first() ?? $property->getName();
4✔
110
        }
111

112
        return $property->getName();
120✔
113
    }
114

115
    private function resolveObject(mixed $objectOrClass): object
124✔
116
    {
117
        if (is_object($objectOrClass)) {
124✔
118
            return $objectOrClass;
2✔
119
        }
120

121
        return new ClassReflector($objectOrClass)->newInstanceWithoutConstructor();
124✔
122
    }
123

124
    private function setParentRelations(
122✔
125
        object $parent,
126
        ClassReflector $parentClass,
127
    ): void {
128
        foreach ($parentClass->getPublicProperties() as $property) {
122✔
129
            if (! $property->isInitialized($parent)) {
122✔
130
                continue;
15✔
131
            }
132

133
            if ($property->isVirtual()) {
122✔
134
                continue;
40✔
135
            }
136

137
            $type = $property->getIterableType() ?? $property->getType();
122✔
138

139
            if (! $type->isClass()) {
122✔
140
                continue;
120✔
141
            }
142

143
            $child = $property->getValue($parent);
107✔
144

145
            if ($child === null) {
107✔
146
                continue;
67✔
147
            }
148

149
            $childClass = $type->asClass();
60✔
150

151
            foreach ($childClass->getPublicProperties() as $childProperty) {
60✔
152
                // Determine the value to set in the child property
153
                if ($childProperty->getType()->equals($parent::class)) {
60✔
154
                    $valueToSet = $parent;
17✔
155
                } elseif ($childProperty->getIterableType()?->equals($parent::class)) {
60✔
156
                    $valueToSet = [$parent];
9✔
157
                } else {
158
                    continue;
60✔
159
                }
160

161
                if (is_array($child)) {
17✔
162
                    // Set the value for each child element if the child is an array
163
                    foreach ($child as $childItem) {
17✔
164
                        $childProperty->setValue($childItem, $valueToSet);
1✔
165
                    }
166
                } else {
167
                    // Set the value directly on the child element if it's an object
168
                    $childProperty->setValue($child, $valueToSet);
9✔
169
                }
170
            }
171
        }
172
    }
173

174
    public function resolveValue(PropertyReflector $property, mixed $value): mixed
120✔
175
    {
176
        // If this isn't a property with iterable type defined, and the type accepts the value, we don't have to cast it
177
        // We need to check the iterable type, because otherwise raw array input might incorrectly be seen as "accepted by the property's array type",
178
        // which isn't sufficient a check.
179
        // Oh how we long for the day that PHP gets generics…
180
        if ($property->getIterableType() === null && $property->getType()->accepts($value)) {
120✔
181
            return $value;
119✔
182
        }
183

184
        // If there is an iterable type, and it accepts the value within the array given, we don't have to cast it either
185
        if ($property->getIterableType()?->accepts(arr($value)->first())) {
48✔
186
            return $value;
2✔
187
        }
188

189
        // If there's a caster, we'll cast the value
190
        if (($caster = $this->casterFactory->forProperty($property)) !== null) {
46✔
191
            return $caster->cast($value);
46✔
192
        }
193

194
        // Otherwise we'll return the value as-is
UNCOV
195
        return $value;
×
196
    }
197
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc