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

tempestphp / tempest-framework / 14024978163

23 Mar 2025 05:55PM UTC coverage: 79.391% (-0.05%) from 79.441%
14024978163

push

github

web-flow
feat(view): cache Blade and Twig templates in internal storage (#1061)

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

912 existing lines in 110 files now uncovered.

10478 of 13198 relevant lines covered (79.39%)

91.09 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
    ) {
23
    }
126✔
24

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

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

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

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

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

46
        $missingValues = [];
124✔
47

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

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

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

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

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

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

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

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

75
                continue;
7✔
76
            }
77

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

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

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

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

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

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

99
        return $object;
122✔
100
    }
101

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

195
        // Otherwise we'll return the value as-is
UNCOV
196
        return $value;
×
197
    }
198
}
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