• 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

99.13
/src/Tempest/Database/src/Mappers/QueryToModelMapper.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Database\Mappers;
6

7
use Tempest\Database\DatabaseModel;
8
use Tempest\Database\Query;
9
use Tempest\Mapper\CasterFactory;
10
use Tempest\Mapper\Mapper;
11
use Tempest\Reflection\ClassReflector;
12
use Tempest\Reflection\PropertyReflector;
13

14
final readonly class QueryToModelMapper implements Mapper
15
{
16
    public function __construct(
123✔
17
        private CasterFactory $casterFactory,
18
    ) {}
123✔
19

20
    public function canMap(mixed $from, mixed $to): bool
123✔
21
    {
22
        return $from instanceof Query;
123✔
23
    }
24

25
    public function map(mixed $from, mixed $to): array
49✔
26
    {
27
        /** @var \Tempest\Database\Query $from */
28
        /** @var class-string<DatabaseModel> $to */
29
        $class = new ClassReflector($to);
49✔
30
        $table = $class->callStatic('table');
49✔
31

32
        $models = [];
49✔
33

34
        foreach ($from->fetch() as $row) {
49✔
35
            $idField = $table->tableName . '.id';
46✔
36

37
            $id = $row[$idField];
46✔
38

39
            $model = $models[$id] ?? $class->newInstanceWithoutConstructor();
46✔
40

41
            $models[$id] = $this->parse($class, $model, $row);
46✔
42
        }
43

44
        return $this->makeLazyCollection($models);
48✔
45
    }
46

47
    private function parse(ClassReflector $class, DatabaseModel $model, array $row): DatabaseModel
46✔
48
    {
49
        foreach ($row as $key => $value) {
46✔
50
            $keyParts = explode('.', $key);
46✔
51

52
            $propertyName = $keyParts[1];
46✔
53

54
            $count = count($keyParts);
46✔
55

56
            // TODO: clean up and document
57
            if ($count > 3) {
46✔
58
                $property = $class->getProperty(rtrim($propertyName, '[]'));
15✔
59

60
                if ($property->getIterableType()?->matches(DatabaseModel::class)) {
15✔
61
                    $collection = $property->get($model, []);
10✔
62
                    $childId = $row[$keyParts[0] . '.' . $keyParts[1] . '.id'];
10✔
63

64
                    if ($childId) {
10✔
65
                        $iterableType = $property->getIterableType();
8✔
66

67
                        $childModel = $collection[$childId] ?? $iterableType->asClass()->newInstanceWithoutConstructor();
8✔
68

69
                        unset($keyParts[0]);
8✔
70

71
                        $collection[$childId] = $this->parse(
8✔
72
                            $iterableType->asClass(),
8✔
73
                            $childModel,
8✔
74
                            [implode('.', $keyParts) => $value],
8✔
75
                        );
8✔
76
                    }
77

78
                    $property->set($model, $collection);
10✔
79
                } else {
80
                    $childModelType = $property->getType();
5✔
81

82
                    $childModel = $property->get($model, $childModelType->asClass()->newInstanceWithoutConstructor());
5✔
83

84
                    unset($keyParts[0]);
5✔
85

86
                    $property->set($model, $this->parse(
5✔
87
                        $childModelType->asClass(),
5✔
88
                        $childModel,
5✔
89
                        [implode('.', $keyParts) => $value],
5✔
90
                    ));
5✔
91
                }
92
            } elseif ($count === 3) {
46✔
93
                $childId = $row[$keyParts[0] . '.' . $keyParts[1] . '.id'] ?? null;
20✔
94

95
                if (str_contains($keyParts[1], '[]')) {
20✔
96
                    $property = $class->getProperty(rtrim($propertyName, '[]'));
11✔
97

98
                    $model = $this->parseHasMany(
11✔
99
                        $property,
11✔
100
                        $model,
11✔
101
                        (string) $childId,
11✔
102
                        $keyParts[2],
11✔
103
                        $value,
11✔
104
                    );
11✔
105
                } else {
106
                    $property = $class->getProperty($propertyName);
17✔
107

108
                    $model = $this->parseBelongsTo(
17✔
109
                        $property,
17✔
110
                        $model,
17✔
111
                        $keyParts[2],
17✔
112
                        $value,
17✔
113
                    );
17✔
114
                }
115
            } else {
116
                $property = $class->getProperty($propertyName);
46✔
117

118
                $model = $this->parseProperty($property, $model, $value);
46✔
119
            }
120
        }
121

122
        return $model;
46✔
123
    }
124

125
    private function parseProperty(PropertyReflector $property, DatabaseModel $model, mixed $value): DatabaseModel
46✔
126
    {
127
        $caster = $this->casterFactory->forProperty($property);
46✔
128

129
        if ($value && $caster !== null) {
46✔
130
            $value = $caster->cast($value);
46✔
131
        }
132

133
        if ($value === null && ! $property->isNullable()) {
46✔
UNCOV
134
            return $model;
×
135
        }
136

137
        $property->set($model, $value);
46✔
138

139
        return $model;
46✔
140
    }
141

142
    private function parseBelongsTo(
17✔
143
        PropertyReflector $property,
144
        DatabaseModel $model,
145
        string $childProperty,
146
        mixed $value,
147
    ): DatabaseModel {
148
        $childModel = $property->get(
17✔
149
            $model,
17✔
150
            $property->getType()->asClass()->newInstanceWithoutConstructor(),
17✔
151
        );
17✔
152

153
        $childProperty = new ClassReflector($childModel)->getProperty($childProperty);
17✔
154

155
        // TODO: must pass through the mapper
156
        $this->parseProperty(
17✔
157
            $childProperty,
17✔
158
            $childModel,
17✔
159
            $value,
17✔
160
        );
17✔
161

162
        $property->set($model, $childModel);
17✔
163

164
        return $model;
17✔
165
    }
166

167
    private function parseHasMany(
11✔
168
        PropertyReflector $property,
169
        DatabaseModel $model,
170
        ?string $childId,
171
        string $childProperty,
172
        mixed $value,
173
    ): DatabaseModel {
174
        $collection = $property->get($model, []);
11✔
175

176
        if (! $childId) {
11✔
177
            $property->set($model, $collection);
2✔
178

179
            return $model;
2✔
180
        }
181

182
        $childModel = $collection[$childId] ?? $property->getIterableType()->asClass()->newInstanceWithoutConstructor();
9✔
183

184
        $childProperty = new ClassReflector($childModel)->getProperty($childProperty);
9✔
185

186
        // TODO: must pass through the mapper
187
        $this->parseProperty(
9✔
188
            $childProperty,
9✔
189
            $childModel,
9✔
190
            $value,
9✔
191
        );
9✔
192

193
        $collection[$childId] = $childModel;
9✔
194

195
        $property->set($model, $collection);
9✔
196

197
        return $model;
9✔
198
    }
199

200
    private function makeLazyCollection(array $models): array
48✔
201
    {
202
        $lazy = [];
48✔
203

204
        foreach ($models as $model) {
48✔
205
            $lazy[] = $this->makeLazyModel($model);
46✔
206
        }
207

208
        return $lazy;
48✔
209
    }
210

211
    private function makeLazyModel(DatabaseModel $model): DatabaseModel
46✔
212
    {
213
        $classReflector = new ClassReflector($model);
46✔
214

215
        foreach ($classReflector->getPublicProperties() as $property) {
46✔
216
            if ($property->isUninitialized($model)) {
46✔
217
                $property->unset($model);
16✔
218

219
                continue;
16✔
220
            }
221

222
            if ($property->getIterableType()?->matches(DatabaseModel::class)) {
46✔
223
                foreach ($property->get($model) as $childModel) {
23✔
224
                    $this->makeLazyModel($childModel);
9✔
225
                }
226

227
                break;
23✔
228
            }
229
        }
230

231
        return $model;
46✔
232
    }
233
}
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