Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.25% |
91 / 112 |
|
37.50% |
3 / 8 |
CRAP | |
0.00% |
0 / 1 |
UpdateMapper | |
81.25% |
91 / 112 |
|
37.50% |
3 / 8 |
71.52 | |
0.00% |
0 / 1 |
update | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
executeUpdate | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
4.02 | |||
updateModel | |
84.85% |
28 / 33 |
|
0.00% |
0 / 1 |
20.26 | |||
updateBelongsTo | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
updateOwnsOne | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
updateHasMany | |
67.65% |
23 / 34 |
|
0.00% |
0 / 1 |
22.62 | |||
updateRelationTable | |
86.96% |
20 / 23 |
|
0.00% |
0 / 1 |
9.18 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\DataStorage\Database\Mapper |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\DataStorage\Database\Mapper; |
16 | |
17 | use phpOMS\DataStorage\Database\Exception\InvalidMapperException; |
18 | use phpOMS\DataStorage\Database\Query\Builder; |
19 | use phpOMS\Utils\ArrayUtils; |
20 | |
21 | /** |
22 | * Update mapper (CREATE). |
23 | * |
24 | * @package phpOMS\DataStorage\Database\Mapper |
25 | * @license OMS License 2.0 |
26 | * @link https://jingga.app |
27 | * @since 1.0.0 |
28 | */ |
29 | final class UpdateMapper extends DataMapperAbstract |
30 | { |
31 | /** |
32 | * Create update mapper |
33 | * |
34 | * @return self |
35 | * |
36 | * @since 1.0.0 |
37 | */ |
38 | public function update() : self |
39 | { |
40 | $this->type = MapperType::UPDATE; |
41 | |
42 | return $this; |
43 | } |
44 | |
45 | /** |
46 | * Execute mapper |
47 | * |
48 | * @param mixed ...$options Options to pass to update mapper |
49 | * |
50 | * @return mixed |
51 | * |
52 | * @since 1.0.0 |
53 | */ |
54 | public function execute(mixed ...$options) : mixed |
55 | { |
56 | switch($this->type) { |
57 | case MapperType::UPDATE: |
58 | /** @var object ...$options */ |
59 | return $this->executeUpdate(...$options); |
60 | default: |
61 | return null; |
62 | } |
63 | } |
64 | |
65 | /** |
66 | * Execute mapper |
67 | * |
68 | * @param object $obj Object to update |
69 | * |
70 | * @return mixed |
71 | * |
72 | * @since 1.0.0 |
73 | */ |
74 | public function executeUpdate(object $obj) : mixed |
75 | { |
76 | $refClass = new \ReflectionClass($obj); |
77 | $objId = $this->mapper::getObjectId($obj); |
78 | |
79 | if ($this->mapper::isNullModel($obj)) { |
80 | return $objId === 0 ? null : $objId; |
81 | } |
82 | |
83 | $this->updateHasMany($refClass, $obj, $objId); |
84 | |
85 | if (empty($objId)) { |
86 | return $this->mapper::create(db: $this->db)->execute($obj); |
87 | } |
88 | |
89 | $this->updateModel($obj, $objId, $refClass); |
90 | |
91 | return $objId; |
92 | } |
93 | |
94 | /** |
95 | * Update model |
96 | * |
97 | * @param object $obj Object to update |
98 | * @param mixed $objId Id of the object to update |
99 | * @param \ReflectionClass $refClass Reflection of the object ot update |
100 | * |
101 | * @return void |
102 | * |
103 | * @since 1.0.0 |
104 | */ |
105 | private function updateModel(object $obj, mixed $objId, \ReflectionClass $refClass = null) : void |
106 | { |
107 | try { |
108 | // Model doesn't have anything to update |
109 | if (\count($this->mapper::COLUMNS) < 2) { |
110 | return; |
111 | } |
112 | |
113 | $query = new Builder($this->db); |
114 | $query->update($this->mapper::TABLE) |
115 | ->where($this->mapper::TABLE . '.' . $this->mapper::PRIMARYFIELD, '=', $objId); |
116 | |
117 | foreach ($this->mapper::COLUMNS as $column) { |
118 | $propertyName = \stripos($column['internal'], '/') !== false ? \explode('/', $column['internal'])[0] : $column['internal']; |
119 | if (isset($this->mapper::HAS_MANY[$propertyName]) |
120 | || $column['internal'] === $this->mapper::PRIMARYFIELD |
121 | || (($column['readonly'] ?? false) && !isset($this->with[$propertyName])) |
122 | || (($column['writeonly'] ?? false) && !isset($this->with[$propertyName])) |
123 | ) { |
124 | continue; |
125 | } |
126 | |
127 | $refClass = $refClass ?? new \ReflectionClass($obj); |
128 | $property = $refClass->getProperty($propertyName); |
129 | |
130 | $tValue = $property->isPublic() ? $obj->{$propertyName} : $property->getValue($obj); |
131 | |
132 | if (isset($this->mapper::OWNS_ONE[$propertyName])) { |
133 | $id = \is_object($tValue) ? $this->updateOwnsOne($propertyName, $tValue) : $tValue; |
134 | $value = $this->parseValue($column['type'], $id); |
135 | |
136 | $query->set([$column['name'] => $value]); |
137 | } elseif (isset($this->mapper::BELONGS_TO[$propertyName])) { |
138 | $id = \is_object($tValue) ? $this->updateBelongsTo($propertyName, $tValue) : $tValue; |
139 | $value = $this->parseValue($column['type'], $id); |
140 | |
141 | $query->set([$column['name'] => $value]); |
142 | } elseif ($column['name'] !== $this->mapper::PRIMARYFIELD) { |
143 | if (\stripos($column['internal'], '/') !== false) { |
144 | $path = \substr($column['internal'], \stripos($column['internal'], '/') + 1); |
145 | $tValue = ArrayUtils::getArray($path, $tValue, '/'); |
146 | } |
147 | |
148 | $value = $this->parseValue($column['type'], $tValue); |
149 | |
150 | $query->set([$column['name'] => $value]); |
151 | } |
152 | } |
153 | |
154 | // @todo: |
155 | // @bug: Sqlite doesn't allow table_name.column_name in set queries for whatver reason. |
156 | |
157 | $sth = $this->db->con->prepare($query->toSql()); |
158 | if ($sth !== false) { |
159 | $sth->execute(); |
160 | } |
161 | } catch (\Throwable $t) { |
162 | // @codeCoverageIgnoreStart |
163 | \phpOMS\Log\FileLogger::getInstance()->error( |
164 | \phpOMS\Log\FileLogger::MSG_FULL, [ |
165 | 'message' => $t->getMessage() . ':' . $query->toSql(), |
166 | 'line' => __LINE__, |
167 | 'file' => self::class, |
168 | ] |
169 | ); |
170 | // @codeCoverageIgnoreEnd |
171 | } |
172 | } |
173 | |
174 | /** |
175 | * Update belongs to |
176 | * |
177 | * @param string $propertyName Name of the property to update |
178 | * @param object $obj Object to update |
179 | * |
180 | * @return mixed |
181 | * |
182 | * @since 1.0.0 |
183 | */ |
184 | private function updateBelongsTo(string $propertyName, object $obj) : mixed |
185 | { |
186 | /** @var class-string<DataMapperFactory> $mapper */ |
187 | $mapper = $this->mapper::BELONGS_TO[$propertyName]['mapper']; |
188 | |
189 | /** @var self $relMapper */ |
190 | $relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName); |
191 | $relMapper->depth = $this->depth + 1; |
192 | |
193 | return $relMapper->execute($obj); |
194 | } |
195 | |
196 | /** |
197 | * Update owns one |
198 | * |
199 | * @param string $propertyName Name of the property to update |
200 | * @param object $obj Object to update |
201 | * |
202 | * @return mixed |
203 | * |
204 | * @since 1.0.0 |
205 | */ |
206 | private function updateOwnsOne(string $propertyName, object $obj) : mixed |
207 | { |
208 | /** @var class-string<DataMapperFactory> $mapper */ |
209 | $mapper = $this->mapper::OWNS_ONE[$propertyName]['mapper']; |
210 | |
211 | /** @var self $relMapper */ |
212 | $relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName); |
213 | $relMapper->depth = $this->depth + 1; |
214 | |
215 | return $relMapper->execute($obj); |
216 | } |
217 | |
218 | /** |
219 | * Update has many relations |
220 | * |
221 | * @param \ReflectionClass $refClass Reflection of the object containing the relations |
222 | * @param object $obj Object which contains the relations |
223 | * @param mixed $objId Object id which contains the relations |
224 | * |
225 | * @return void |
226 | * |
227 | * @throws InvalidMapperException |
228 | * |
229 | * @since 1.0.0 |
230 | */ |
231 | private function updateHasMany(\ReflectionClass $refClass, object $obj, mixed $objId) : void |
232 | { |
233 | if (empty($this->with) || empty($this->mapper::HAS_MANY)) { |
234 | return; |
235 | } |
236 | |
237 | $objsIds = []; |
238 | |
239 | foreach ($this->mapper::HAS_MANY as $propertyName => $rel) { |
240 | if ($rel['readonly'] ?? false === true) { |
241 | continue; |
242 | } |
243 | |
244 | if (!isset($this->mapper::HAS_MANY[$propertyName]['mapper'])) { |
245 | throw new InvalidMapperException(); |
246 | } |
247 | |
248 | $property = $refClass->getProperty($propertyName); |
249 | |
250 | $values = ($isPublic = $property->isPublic()) ? $obj->{$propertyName} : $property->getValue($obj); |
251 | |
252 | if (!\is_array($values) || empty($values)) { |
253 | continue; |
254 | } |
255 | |
256 | /** @var class-string<DataMapperFactory> $mapper */ |
257 | $mapper = $this->mapper::HAS_MANY[$propertyName]['mapper']; |
258 | $relReflectionClass = new \ReflectionClass(\reset($values)); |
259 | $objsIds[$propertyName] = []; |
260 | |
261 | foreach ($values as $key => &$value) { |
262 | if (!\is_object($value)) { |
263 | // Is scalar => already in database |
264 | $objsIds[$propertyName][$key] = $value; |
265 | |
266 | continue; |
267 | } |
268 | |
269 | $primaryKey = $mapper::getObjectId($value); |
270 | |
271 | // already in db |
272 | if (!empty($primaryKey)) { |
273 | /** @var self $relMapper */ |
274 | $relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName); |
275 | $relMapper->depth = $this->depth + 1; |
276 | |
277 | $relMapper->execute($value); |
278 | |
279 | $objsIds[$propertyName][$key] = $primaryKey; |
280 | |
281 | continue; |
282 | } |
283 | |
284 | // create if not existing |
285 | if ($this->mapper::HAS_MANY[$propertyName]['table'] === $this->mapper::HAS_MANY[$propertyName]['mapper']::TABLE |
286 | && isset($mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]) |
287 | ) { |
288 | $relProperty = $relReflectionClass->getProperty($mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['internal']); |
289 | |
290 | if (!$isPublic) { |
291 | $relProperty->setValue($value, $objId); |
292 | } else { |
293 | $value->{$mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['internal']} = $objId; |
294 | } |
295 | } |
296 | |
297 | $objsIds[$propertyName][$key] = $mapper::create(db: $this->db)->execute($value); |
298 | } |
299 | } |
300 | |
301 | $this->updateRelationTable($objsIds, $objId); |
302 | } |
303 | |
304 | /** |
305 | * Update has many relations if the relation is handled in a relation table |
306 | * |
307 | * @param array $objsIds Objects which should be related to the parent object |
308 | * @param mixed $objId Parent object id |
309 | * |
310 | * @return void |
311 | * |
312 | * @since 1.0.0 |
313 | */ |
314 | private function updateRelationTable(array $objsIds, mixed $objId) : void |
315 | { |
316 | foreach ($this->mapper::HAS_MANY as $member => $many) { |
317 | if (isset($many['column']) || !isset($this->with[$member])) { |
318 | continue; |
319 | } |
320 | |
321 | $query = new Builder($this->db); |
322 | $src = $many['external'] ?? $many['mapper']::PRIMARYFIELD; |
323 | |
324 | // @todo: what if a specific column name is defined instead of primaryField for the join? Fix, it should be stored in 'column' |
325 | $query->select($many['table'] . '.' . $src) |
326 | ->from($many['table']) |
327 | ->where($many['table'] . '.' . $many['self'], '=', $objId); |
328 | |
329 | if ($many['table'] !== $many['mapper']::TABLE) { |
330 | $query->leftJoin($many['mapper']::TABLE) |
331 | ->on($many['table'] . '.' . $src, '=', $many['mapper']::TABLE . '.' . $many['mapper']::PRIMARYFIELD); |
332 | } |
333 | |
334 | $sth = $this->db->con->prepare($query->toSql()); |
335 | if ($sth === false) { |
336 | continue; |
337 | } |
338 | |
339 | $sth->execute(); |
340 | $result = $sth->fetchAll(\PDO::FETCH_COLUMN); |
341 | |
342 | if ($result === false) { |
343 | return; // @codeCoverageIgnore |
344 | } |
345 | |
346 | $removes = \array_diff($result, \array_values($objsIds[$member] ?? [])); |
347 | $adds = \array_diff(\array_values($objsIds[$member] ?? []), $result); |
348 | |
349 | if (!empty($removes)) { |
350 | $this->mapper::remover(db: $this->db)->deleteRelationTable($member, $removes, $objId); |
351 | } |
352 | |
353 | if (!empty($adds)) { |
354 | $this->mapper::writer(db: $this->db)->createRelationTable($member, $adds, $objId); |
355 | } |
356 | } |
357 | } |
358 | } |